From e5402d546414a19112a2a0c4c3046bb414a3ec68 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 14 Aug 2025 07:39:33 -0600 Subject: [PATCH 001/744] Allow editing Agent2 messages (#36155) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra Co-authored-by: Agus Zubiaga --- Cargo.lock | 1 + assets/keymaps/default-linux.json | 2 - assets/keymaps/default-macos.json | 2 - crates/acp_thread/Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 15 +- crates/acp_thread/src/mention.rs | 94 ++- crates/agent2/src/thread.rs | 4 +- crates/agent_ui/src/acp.rs | 3 +- .../agent_ui/src/acp/completion_provider.rs | 113 +-- crates/agent_ui/src/acp/message_editor.rs | 469 +++++++++++ crates/agent_ui/src/acp/message_history.rs | 88 -- crates/agent_ui/src/acp/thread_view.rs | 762 +++++++----------- crates/agent_ui/src/agent_panel.rs | 15 +- crates/zed_actions/src/lib.rs | 4 - 14 files changed, 954 insertions(+), 619 deletions(-) create mode 100644 crates/agent_ui/src/acp/message_editor.rs delete mode 100644 crates/agent_ui/src/acp/message_history.rs diff --git a/Cargo.lock b/Cargo.lock index f0fd3049c0a4d6dc8197086066b0a236afe987bb..cb087f43b7d11e909c846a93f7127c57696ff1ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,7 @@ dependencies = [ "collections", "editor", "env_logger 0.11.8", + "file_icons", "futures 0.3.31", "gpui", "indoc", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index dda26f406bc50b4c0451bcdf89f7bd7f15e6427a..01c0b4e9696f3ee31d599f171acd27f4c00fdf3c 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -331,8 +331,6 @@ "use_key_equivalents": true, "bindings": { "enter": "agent::Chat", - "up": "agent::PreviousHistoryMessage", - "down": "agent::NextHistoryMessage", "shift-ctrl-r": "agent::OpenAgentDiff", "ctrl-shift-y": "agent::KeepAll", "ctrl-shift-n": "agent::RejectAll" diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 3966efd8dfce9f1800ad0c9ac1c38b172709ce50..e5b7fff9e1ce269f4f1c2f630f6bd41d790ffd21 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -383,8 +383,6 @@ "use_key_equivalents": true, "bindings": { "enter": "agent::Chat", - "up": "agent::PreviousHistoryMessage", - "down": "agent::NextHistoryMessage", "shift-ctrl-r": "agent::OpenAgentDiff", "cmd-shift-y": "agent::KeepAll", "cmd-shift-n": "agent::RejectAll" diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 2ac15de08f331e555e80883cd66c9e5beefe0a32..2d0fe2d2645a3f61152211ec439105d4aa73f7ec 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -23,6 +23,7 @@ anyhow.workspace = true buffer_diff.workspace = true collections.workspace = true editor.workspace = true +file_icons.workspace = true futures.workspace = true gpui.workspace = true itertools.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index a5b512f31abec80cd4c79ef843471f95b0f4b22a..da4d82712a27f4783d764e409db0d8cdeaa26805 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -32,6 +32,7 @@ use util::ResultExt; pub struct UserMessage { pub id: Option, pub content: ContentBlock, + pub chunks: Vec, pub checkpoint: Option, } @@ -804,18 +805,25 @@ impl AcpThread { let entries_len = self.entries.len(); if let Some(last_entry) = self.entries.last_mut() - && let AgentThreadEntry::UserMessage(UserMessage { id, content, .. }) = last_entry + && let AgentThreadEntry::UserMessage(UserMessage { + id, + content, + chunks, + .. + }) = last_entry { *id = message_id.or(id.take()); - content.append(chunk, &language_registry, cx); + content.append(chunk.clone(), &language_registry, cx); + chunks.push(chunk); let idx = entries_len - 1; cx.emit(AcpThreadEvent::EntryUpdated(idx)); } else { - let content = ContentBlock::new(chunk, &language_registry, cx); + let content = ContentBlock::new(chunk.clone(), &language_registry, cx); self.push_entry( AgentThreadEntry::UserMessage(UserMessage { id: message_id, content, + chunks: vec![chunk], checkpoint: None, }), cx, @@ -1150,6 +1158,7 @@ impl AcpThread { AgentThreadEntry::UserMessage(UserMessage { id: message_id.clone(), content: block, + chunks: message.clone(), checkpoint: None, }), cx, diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 03174608fb0187687fb987ea640277baa25e01a2..b18cbfe18e8432eb8715556854a158b92b66e72b 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -1,16 +1,21 @@ use agent::ThreadId; use anyhow::{Context as _, Result, bail}; +use file_icons::FileIcons; use prompt_store::{PromptId, UserPromptId}; use std::{ fmt, ops::Range, path::{Path, PathBuf}, }; +use ui::{App, IconName, SharedString}; use url::Url; #[derive(Clone, Debug, PartialEq, Eq)] pub enum MentionUri { - File(PathBuf), + File { + abs_path: PathBuf, + is_directory: bool, + }, Symbol { path: PathBuf, name: String, @@ -75,8 +80,12 @@ impl MentionUri { } else { let file_path = PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path)); + let is_directory = input.ends_with("/"); - Ok(Self::File(file_path)) + Ok(Self::File { + abs_path: file_path, + is_directory, + }) } } "zed" => { @@ -108,9 +117,9 @@ impl MentionUri { } } - fn name(&self) -> String { + pub fn name(&self) -> String { match self { - MentionUri::File(path) => path + MentionUri::File { abs_path, .. } => abs_path .file_name() .unwrap_or_default() .to_string_lossy() @@ -126,15 +135,45 @@ impl MentionUri { } } + pub fn icon_path(&self, cx: &mut App) -> SharedString { + match self { + MentionUri::File { + abs_path, + is_directory, + } => { + if *is_directory { + FileIcons::get_folder_icon(false, cx) + .unwrap_or_else(|| IconName::Folder.path().into()) + } else { + FileIcons::get_icon(&abs_path, cx) + .unwrap_or_else(|| IconName::File.path().into()) + } + } + MentionUri::Symbol { .. } => IconName::Code.path().into(), + MentionUri::Thread { .. } => IconName::Thread.path().into(), + MentionUri::TextThread { .. } => IconName::Thread.path().into(), + MentionUri::Rule { .. } => IconName::Reader.path().into(), + MentionUri::Selection { .. } => IconName::Reader.path().into(), + MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(), + } + } + pub fn as_link<'a>(&'a self) -> MentionLink<'a> { MentionLink(self) } pub fn to_uri(&self) -> Url { match self { - MentionUri::File(path) => { + MentionUri::File { + abs_path, + is_directory, + } => { let mut url = Url::parse("file:///").unwrap(); - url.set_path(&path.to_string_lossy()); + let mut path = abs_path.to_string_lossy().to_string(); + if *is_directory && !path.ends_with("/") { + path.push_str("/"); + } + url.set_path(&path); url } MentionUri::Symbol { @@ -226,12 +265,53 @@ mod tests { let file_uri = "file:///path/to/file.rs"; let parsed = MentionUri::parse(file_uri).unwrap(); match &parsed { - MentionUri::File(path) => assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"), + MentionUri::File { + abs_path, + is_directory, + } => { + assert_eq!(abs_path.to_str().unwrap(), "/path/to/file.rs"); + assert!(!is_directory); + } _ => panic!("Expected File variant"), } assert_eq!(parsed.to_uri().to_string(), file_uri); } + #[test] + fn test_parse_directory_uri() { + let file_uri = "file:///path/to/dir/"; + let parsed = MentionUri::parse(file_uri).unwrap(); + match &parsed { + MentionUri::File { + abs_path, + is_directory, + } => { + assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir/"); + assert!(is_directory); + } + _ => panic!("Expected File variant"), + } + assert_eq!(parsed.to_uri().to_string(), file_uri); + } + + #[test] + fn test_to_directory_uri_with_slash() { + let uri = MentionUri::File { + abs_path: PathBuf::from("/path/to/dir/"), + is_directory: true, + }; + assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/"); + } + + #[test] + fn test_to_directory_uri_without_slash() { + let uri = MentionUri::File { + abs_path: PathBuf::from("/path/to/dir"), + is_directory: true, + }; + assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/"); + } + #[test] fn test_parse_symbol_uri() { let symbol_uri = "file:///path/to/file.rs?symbol=MySymbol#L10:20"; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 4156ec44d2b24ebab3e20f2bab9330e7ca13f53a..260aaaf550c89823be40ce2c48b5a0affdf64b72 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -124,12 +124,12 @@ impl UserMessage { } UserMessageContent::Mention { uri, content } => { match uri { - MentionUri::File(path) => { + MentionUri::File { abs_path, .. } => { write!( &mut symbol_context, "\n{}", MarkdownCodeBlock { - tag: &codeblock_tag(&path, None), + tag: &codeblock_tag(&abs_path, None), text: &content.to_string(), } ) diff --git a/crates/agent_ui/src/acp.rs b/crates/agent_ui/src/acp.rs index b9814adb2dc5fec075bf1128cbbba19a8889b3e6..630aa730a68a4167e468594897151cf41ada30a1 100644 --- a/crates/agent_ui/src/acp.rs +++ b/crates/agent_ui/src/acp.rs @@ -1,10 +1,9 @@ mod completion_provider; -mod message_history; +mod message_editor; mod model_selector; mod model_selector_popover; mod thread_view; -pub use message_history::MessageHistory; pub use model_selector::AcpModelSelector; pub use model_selector_popover::AcpModelSelectorPopover; pub use thread_view::AcpThreadView; diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 46c8aa92f1a689e4a58f33843bbc1f7feb2201bc..720ee23b00de5b2c19d15d53bfef5ef6eba6375e 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,5 +1,5 @@ use std::ops::Range; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; @@ -8,7 +8,7 @@ use anyhow::{Context as _, Result, anyhow}; use collections::{HashMap, HashSet}; use editor::display_map::CreaseId; use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _}; -use file_icons::FileIcons; + use futures::future::try_join_all; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{App, Entity, Task, WeakEntity}; @@ -28,10 +28,7 @@ use url::Url; use workspace::Workspace; use workspace::notifications::NotifyResultExt; -use agent::{ - context::RULES_ICON, - thread_store::{TextThreadStore, ThreadStore}, -}; +use agent::thread_store::{TextThreadStore, ThreadStore}; use crate::context_picker::fetch_context_picker::fetch_url_content; use crate::context_picker::file_context_picker::{FileMatch, search_files}; @@ -66,6 +63,11 @@ impl MentionSet { self.uri_by_crease_id.drain().map(|(id, _)| id) } + pub fn clear(&mut self) { + self.fetch_results.clear(); + self.uri_by_crease_id.clear(); + } + pub fn contents( &self, project: Entity, @@ -79,12 +81,13 @@ impl MentionSet { .iter() .map(|(&crease_id, uri)| { match uri { - MentionUri::File(path) => { + MentionUri::File { abs_path, .. } => { + // TODO directories let uri = uri.clone(); - let path = path.to_path_buf(); + let abs_path = abs_path.to_path_buf(); let buffer_task = project.update(cx, |project, cx| { let path = project - .find_project_path(path, cx) + .find_project_path(abs_path, cx) .context("Failed to find project path")?; anyhow::Ok(project.open_buffer(path, cx)) }); @@ -508,9 +511,14 @@ impl ContextPickerCompletionProvider { }) .unwrap_or_default(); let line_range = point_range.start.row..point_range.end.row; + + let uri = MentionUri::Selection { + path: path.clone(), + line_range: line_range.clone(), + }; let crease = crate::context_picker::crease_for_mention( selection_name(&path, &line_range).into(), - IconName::Reader.path().into(), + uri.icon_path(cx), range, editor.downgrade(), ); @@ -528,10 +536,7 @@ impl ContextPickerCompletionProvider { crease_ids.try_into().unwrap() }); - mention_set.lock().insert( - crease_id, - MentionUri::Selection { path, line_range }, - ); + mention_set.lock().insert(crease_id, uri); current_offset += text_len + 1; } @@ -569,13 +574,8 @@ impl ContextPickerCompletionProvider { recent: bool, editor: Entity, mention_set: Arc>, + cx: &mut App, ) -> Completion { - let icon_for_completion = if recent { - IconName::HistoryRerun - } else { - IconName::Thread - }; - let uri = match &thread_entry { ThreadContextEntry::Thread { id, title } => MentionUri::Thread { id: id.clone(), @@ -586,6 +586,13 @@ impl ContextPickerCompletionProvider { name: title.to_string(), }, }; + + let icon_for_completion = if recent { + IconName::HistoryRerun.path().into() + } else { + uri.icon_path(cx) + }; + let new_text = format!("{} ", uri.as_link()); let new_text_len = new_text.len(); @@ -596,9 +603,9 @@ impl ContextPickerCompletionProvider { documentation: None, insert_text_mode: None, source: project::CompletionSource::Custom, - icon_path: Some(icon_for_completion.path().into()), + icon_path: Some(icon_for_completion.clone()), confirm: Some(confirm_completion_callback( - IconName::Thread.path().into(), + uri.icon_path(cx), thread_entry.title().clone(), excerpt_id, source_range.start, @@ -616,6 +623,7 @@ impl ContextPickerCompletionProvider { source_range: Range, editor: Entity, mention_set: Arc>, + cx: &mut App, ) -> Completion { let uri = MentionUri::Rule { id: rule.prompt_id.into(), @@ -623,6 +631,7 @@ impl ContextPickerCompletionProvider { }; let new_text = format!("{} ", uri.as_link()); let new_text_len = new_text.len(); + let icon_path = uri.icon_path(cx); Completion { replace_range: source_range.clone(), new_text, @@ -630,9 +639,9 @@ impl ContextPickerCompletionProvider { documentation: None, insert_text_mode: None, source: project::CompletionSource::Custom, - icon_path: Some(RULES_ICON.path().into()), + icon_path: Some(icon_path.clone()), confirm: Some(confirm_completion_callback( - RULES_ICON.path().into(), + icon_path, rule.title.clone(), excerpt_id, source_range.start, @@ -654,7 +663,7 @@ impl ContextPickerCompletionProvider { editor: Entity, mention_set: Arc>, project: Entity, - cx: &App, + cx: &mut App, ) -> Option { let (file_name, directory) = crate::context_picker::file_context_picker::extract_file_name_and_directory( @@ -664,27 +673,21 @@ impl ContextPickerCompletionProvider { let label = build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx); - let full_path = if let Some(directory) = directory { - format!("{}{}", directory, file_name) - } else { - file_name.to_string() - }; - let crease_icon_path = if is_directory { - FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into()) - } else { - FileIcons::get_icon(Path::new(&full_path), cx) - .unwrap_or_else(|| IconName::File.path().into()) + let abs_path = project.read(cx).absolute_path(&project_path, cx)?; + + let file_uri = MentionUri::File { + abs_path, + is_directory, }; + + let crease_icon_path = file_uri.icon_path(cx); let completion_icon_path = if is_recent { IconName::HistoryRerun.path().into() } else { crease_icon_path.clone() }; - let abs_path = project.read(cx).absolute_path(&project_path, cx)?; - - let file_uri = MentionUri::File(abs_path); let new_text = format!("{} ", file_uri.as_link()); let new_text_len = new_text.len(); Some(Completion { @@ -729,16 +732,17 @@ impl ContextPickerCompletionProvider { }; let new_text = format!("{} ", uri.as_link()); let new_text_len = new_text.len(); + let icon_path = uri.icon_path(cx); Some(Completion { replace_range: source_range.clone(), new_text, label, documentation: None, source: project::CompletionSource::Custom, - icon_path: Some(IconName::Code.path().into()), + icon_path: Some(icon_path.clone()), insert_text_mode: None, confirm: Some(confirm_completion_callback( - IconName::Code.path().into(), + icon_path, symbol.name.clone().into(), excerpt_id, source_range.start, @@ -757,16 +761,23 @@ impl ContextPickerCompletionProvider { editor: Entity, mention_set: Arc>, http_client: Arc, + cx: &mut App, ) -> Option { let new_text = format!("@fetch {} ", url_to_fetch.clone()); let new_text_len = new_text.len(); + let mention_uri = MentionUri::Fetch { + url: url::Url::parse(url_to_fetch.as_ref()) + .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) + .ok()?, + }; + let icon_path = mention_uri.icon_path(cx); Some(Completion { replace_range: source_range.clone(), new_text, label: CodeLabel::plain(url_to_fetch.to_string(), None), documentation: None, source: project::CompletionSource::Custom, - icon_path: Some(IconName::ToolWeb.path().into()), + icon_path: Some(icon_path.clone()), insert_text_mode: None, confirm: Some({ let start = source_range.start; @@ -774,6 +785,7 @@ impl ContextPickerCompletionProvider { let editor = editor.clone(); let url_to_fetch = url_to_fetch.clone(); let source_range = source_range.clone(); + let icon_path = icon_path.clone(); Arc::new(move |_, window, cx| { let Some(url) = url::Url::parse(url_to_fetch.as_ref()) .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) @@ -781,12 +793,12 @@ impl ContextPickerCompletionProvider { else { return false; }; - let mention_uri = MentionUri::Fetch { url: url.clone() }; let editor = editor.clone(); let mention_set = mention_set.clone(); let http_client = http_client.clone(); let source_range = source_range.clone(); + let icon_path = icon_path.clone(); window.defer(cx, move |window, cx| { let url = url.clone(); @@ -795,7 +807,7 @@ impl ContextPickerCompletionProvider { start, content_len, url.to_string().into(), - IconName::ToolWeb.path().into(), + icon_path, editor.clone(), window, cx, @@ -814,8 +826,10 @@ impl ContextPickerCompletionProvider { .await .notify_async_err(cx) { - mention_set.lock().add_fetch_result(url, content); - mention_set.lock().insert(crease_id, mention_uri.clone()); + mention_set.lock().add_fetch_result(url.clone(), content); + mention_set + .lock() + .insert(crease_id, MentionUri::Fetch { url }); } else { // Remove crease if we failed to fetch editor @@ -911,8 +925,8 @@ impl CompletionProvider for ContextPickerCompletionProvider { for uri in mention_set.uri_by_crease_id.values() { match uri { - MentionUri::File(path) => { - excluded_paths.insert(path.clone()); + MentionUri::File { abs_path, .. } => { + excluded_paths.insert(abs_path.clone()); } MentionUri::Thread { id, .. } => { excluded_threads.insert(id.clone()); @@ -1001,6 +1015,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { is_recent, editor.clone(), mention_set.clone(), + cx, )), Match::Rules(user_rules) => Some(Self::completion_for_rules( @@ -1009,6 +1024,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { source_range.clone(), editor.clone(), mention_set.clone(), + cx, )), Match::Fetch(url) => Self::completion_for_fetch( @@ -1018,6 +1034,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { editor.clone(), mention_set.clone(), http_client.clone(), + cx, ), Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry( @@ -1179,7 +1196,7 @@ mod tests { use serde_json::json; use settings::SettingsStore; use smol::stream::StreamExt as _; - use std::{ops::Deref, rc::Rc}; + use std::{ops::Deref, path::Path, rc::Rc}; use util::path; use workspace::{AppState, Item}; diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs new file mode 100644 index 0000000000000000000000000000000000000000..fc34420d4e882334cbe74145595f192687dd6a5a --- /dev/null +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -0,0 +1,469 @@ +use crate::acp::completion_provider::ContextPickerCompletionProvider; +use crate::acp::completion_provider::MentionSet; +use acp_thread::MentionUri; +use agent::TextThreadStore; +use agent::ThreadStore; +use agent_client_protocol as acp; +use anyhow::Result; +use collections::HashSet; +use editor::{ + AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, + EditorStyle, MultiBuffer, +}; +use gpui::{ + AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, Task, TextStyle, WeakEntity, +}; +use language::Buffer; +use language::Language; +use parking_lot::Mutex; +use project::{CompletionIntent, Project}; +use settings::Settings; +use std::fmt::Write; +use std::rc::Rc; +use std::sync::Arc; +use theme::ThemeSettings; +use ui::{ + ActiveTheme, App, InteractiveElement, IntoElement, ParentElement, Render, Styled, TextSize, + Window, div, +}; +use util::ResultExt; +use workspace::Workspace; +use zed_actions::agent::Chat; + +pub struct MessageEditor { + editor: Entity, + project: Entity, + thread_store: Entity, + text_thread_store: Entity, + mention_set: Arc>, +} + +pub enum MessageEditorEvent { + Send, + Cancel, +} + +impl EventEmitter for MessageEditor {} + +impl MessageEditor { + pub fn new( + workspace: WeakEntity, + project: Entity, + thread_store: Entity, + text_thread_store: Entity, + mode: EditorMode, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let language = Language::new( + language::LanguageConfig { + completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']), + ..Default::default() + }, + None, + ); + + let mention_set = Arc::new(Mutex::new(MentionSet::default())); + let editor = cx.new(|cx| { + let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + + let mut editor = Editor::new(mode, buffer, None, window, cx); + editor.set_placeholder_text("Message the agent - @ to include files", cx); + editor.set_show_indent_guides(false, cx); + editor.set_soft_wrap(); + editor.set_use_modal_editing(true); + editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( + mention_set.clone(), + workspace, + thread_store.downgrade(), + text_thread_store.downgrade(), + cx.weak_entity(), + )))); + editor.set_context_menu_options(ContextMenuOptions { + min_entries_visible: 12, + max_entries_visible: 12, + placement: Some(ContextMenuPlacement::Above), + }); + editor + }); + + Self { + editor, + project, + mention_set, + thread_store, + text_thread_store, + } + } + + pub fn is_empty(&self, cx: &App) -> bool { + self.editor.read(cx).is_empty(cx) + } + + pub fn contents( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Task>> { + let contents = self.mention_set.lock().contents( + self.project.clone(), + self.thread_store.clone(), + self.text_thread_store.clone(), + window, + cx, + ); + let editor = self.editor.clone(); + + cx.spawn(async move |_, cx| { + let contents = contents.await?; + + editor.update(cx, |editor, cx| { + let mut ix = 0; + let mut chunks: Vec = Vec::new(); + let text = editor.text(cx); + editor.display_map.update(cx, |map, cx| { + let snapshot = map.snapshot(cx); + for (crease_id, crease) in snapshot.crease_snapshot.creases() { + // Skip creases that have been edited out of the message buffer. + if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { + continue; + } + + if let Some(mention) = contents.get(&crease_id) { + let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot); + if crease_range.start > ix { + chunks.push(text[ix..crease_range.start].into()); + } + chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource { + annotations: None, + resource: acp::EmbeddedResourceResource::TextResourceContents( + acp::TextResourceContents { + mime_type: None, + text: mention.content.clone(), + uri: mention.uri.to_uri().to_string(), + }, + ), + })); + ix = crease_range.end; + } + } + + if ix < text.len() { + let last_chunk = text[ix..].trim_end(); + if !last_chunk.is_empty() { + chunks.push(last_chunk.into()); + } + } + }); + + chunks + }) + }) + } + + pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { + self.editor.update(cx, |editor, cx| { + editor.clear(window, cx); + editor.remove_creases(self.mention_set.lock().drain(), cx) + }); + } + + fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context) { + cx.emit(MessageEditorEvent::Send) + } + + fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { + cx.emit(MessageEditorEvent::Cancel) + } + + pub fn insert_dragged_files( + &self, + paths: Vec, + window: &mut Window, + cx: &mut Context, + ) { + let buffer = self.editor.read(cx).buffer().clone(); + let Some((&excerpt_id, _, _)) = buffer.read(cx).snapshot(cx).as_singleton() else { + return; + }; + let Some(buffer) = buffer.read(cx).as_singleton() else { + return; + }; + for path in paths { + let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else { + continue; + }; + let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else { + continue; + }; + + let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); + let path_prefix = abs_path + .file_name() + .unwrap_or(path.path.as_os_str()) + .display() + .to_string(); + let Some(completion) = ContextPickerCompletionProvider::completion_for_path( + path, + &path_prefix, + false, + entry.is_dir(), + excerpt_id, + anchor..anchor, + self.editor.clone(), + self.mention_set.clone(), + self.project.clone(), + cx, + ) else { + continue; + }; + + self.editor.update(cx, |message_editor, cx| { + message_editor.edit( + [( + multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), + completion.new_text, + )], + cx, + ); + }); + if let Some(confirm) = completion.confirm.clone() { + confirm(CompletionIntent::Complete, window, cx); + } + } + } + + pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context) { + self.editor.update(cx, |editor, cx| { + editor.set_mode(mode); + cx.notify() + }); + } + + pub fn set_message( + &mut self, + message: &[acp::ContentBlock], + window: &mut Window, + cx: &mut Context, + ) { + let mut text = String::new(); + let mut mentions = Vec::new(); + + for chunk in message { + match chunk { + acp::ContentBlock::Text(text_content) => { + text.push_str(&text_content.text); + } + acp::ContentBlock::Resource(acp::EmbeddedResource { + resource: acp::EmbeddedResourceResource::TextResourceContents(resource), + .. + }) => { + if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() { + let start = text.len(); + write!(&mut text, "{}", mention_uri.as_link()).ok(); + let end = text.len(); + mentions.push((start..end, mention_uri)); + } + } + acp::ContentBlock::Image(_) + | acp::ContentBlock::Audio(_) + | acp::ContentBlock::Resource(_) + | acp::ContentBlock::ResourceLink(_) => {} + } + } + + let snapshot = self.editor.update(cx, |editor, cx| { + editor.set_text(text, window, cx); + editor.buffer().read(cx).snapshot(cx) + }); + + self.mention_set.lock().clear(); + for (range, mention_uri) in mentions { + let anchor = snapshot.anchor_before(range.start); + let crease_id = crate::context_picker::insert_crease_for_mention( + anchor.excerpt_id, + anchor.text_anchor, + range.end - range.start, + mention_uri.name().into(), + mention_uri.icon_path(cx), + self.editor.clone(), + window, + cx, + ); + + if let Some(crease_id) = crease_id { + self.mention_set.lock().insert(crease_id, mention_uri); + } + } + cx.notify(); + } + + #[cfg(test)] + pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context) { + self.editor.update(cx, |editor, cx| { + editor.set_text(text, window, cx); + }); + } +} + +impl Focusable for MessageEditor { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.editor.focus_handle(cx) + } +} + +impl Render for MessageEditor { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .key_context("MessageEditor") + .on_action(cx.listener(Self::chat)) + .on_action(cx.listener(Self::cancel)) + .flex_1() + .child({ + let settings = ThemeSettings::get_global(cx); + let font_size = TextSize::Small + .rems(cx) + .to_pixels(settings.agent_font_size(cx)); + let line_height = settings.buffer_line_height.value() * font_size; + + let text_style = TextStyle { + color: cx.theme().colors().text, + font_family: settings.buffer_font.family.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), + font_features: settings.buffer_font.features.clone(), + font_size: font_size.into(), + line_height: line_height.into(), + ..Default::default() + }; + + EditorElement::new( + &self.editor, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + syntax: cx.theme().syntax().clone(), + ..Default::default() + }, + ) + }) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use agent::{TextThreadStore, ThreadStore}; + use agent_client_protocol as acp; + use editor::EditorMode; + use fs::FakeFs; + use gpui::{AppContext, TestAppContext}; + use lsp::{CompletionContext, CompletionTriggerKind}; + use project::{CompletionIntent, Project}; + use serde_json::json; + use util::path; + use workspace::Workspace; + + use crate::acp::{message_editor::MessageEditor, thread_view::tests::init_test}; + + #[gpui::test] + async fn test_at_mention_removal(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/project", json!({"file": ""})).await; + let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; + + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + + let message_editor = cx.update(|window, cx| { + cx.new(|cx| { + MessageEditor::new( + workspace.downgrade(), + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + EditorMode::AutoHeight { + min_lines: 1, + max_lines: None, + }, + window, + cx, + ) + }) + }); + let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone()); + + cx.run_until_parked(); + + let excerpt_id = editor.update(cx, |editor, cx| { + editor + .buffer() + .read(cx) + .excerpt_ids() + .into_iter() + .next() + .unwrap() + }); + let completions = editor.update_in(cx, |editor, window, cx| { + editor.set_text("Hello @file ", window, cx); + let buffer = editor.buffer().read(cx).as_singleton().unwrap(); + let completion_provider = editor.completion_provider().unwrap(); + completion_provider.completions( + excerpt_id, + &buffer, + text::Anchor::MAX, + CompletionContext { + trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, + trigger_character: Some("@".into()), + }, + window, + cx, + ) + }); + let [_, completion]: [_; 2] = completions + .await + .unwrap() + .into_iter() + .flat_map(|response| response.completions) + .collect::>() + .try_into() + .unwrap(); + + editor.update_in(cx, |editor, window, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let start = snapshot + .anchor_in_excerpt(excerpt_id, completion.replace_range.start) + .unwrap(); + let end = snapshot + .anchor_in_excerpt(excerpt_id, completion.replace_range.end) + .unwrap(); + editor.edit([(start..end, completion.new_text)], cx); + (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx); + }); + + cx.run_until_parked(); + + // Backspace over the inserted crease (and the following space). + editor.update_in(cx, |editor, window, cx| { + editor.backspace(&Default::default(), window, cx); + editor.backspace(&Default::default(), window, cx); + }); + + let content = message_editor + .update_in(cx, |message_editor, window, cx| { + message_editor.contents(window, cx) + }) + .await + .unwrap(); + + // We don't send a resource link for the deleted crease. + pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]); + } +} diff --git a/crates/agent_ui/src/acp/message_history.rs b/crates/agent_ui/src/acp/message_history.rs deleted file mode 100644 index c8280573a0230ccd15890bba10745ab552b703e6..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/acp/message_history.rs +++ /dev/null @@ -1,88 +0,0 @@ -pub struct MessageHistory { - items: Vec, - current: Option, -} - -impl Default for MessageHistory { - fn default() -> Self { - MessageHistory { - items: Vec::new(), - current: None, - } - } -} - -impl MessageHistory { - pub fn push(&mut self, message: T) { - self.current.take(); - self.items.push(message); - } - - pub fn reset_position(&mut self) { - self.current.take(); - } - - pub fn prev(&mut self) -> Option<&T> { - if self.items.is_empty() { - return None; - } - - let new_ix = self - .current - .get_or_insert(self.items.len()) - .saturating_sub(1); - - self.current = Some(new_ix); - self.items.get(new_ix) - } - - pub fn next(&mut self) -> Option<&T> { - let current = self.current.as_mut()?; - *current += 1; - - self.items.get(*current).or_else(|| { - self.current.take(); - None - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_prev_next() { - let mut history = MessageHistory::default(); - - // Test empty history - assert_eq!(history.prev(), None); - assert_eq!(history.next(), None); - - // Add some messages - history.push("first"); - history.push("second"); - history.push("third"); - - // Test prev navigation - assert_eq!(history.prev(), Some(&"third")); - assert_eq!(history.prev(), Some(&"second")); - assert_eq!(history.prev(), Some(&"first")); - assert_eq!(history.prev(), Some(&"first")); - - assert_eq!(history.next(), Some(&"second")); - - // Test mixed navigation - history.push("fourth"); - assert_eq!(history.prev(), Some(&"fourth")); - assert_eq!(history.prev(), Some(&"third")); - assert_eq!(history.next(), Some(&"fourth")); - assert_eq!(history.next(), None); - - // Test that push resets navigation - history.prev(); - history.prev(); - history.push("fifth"); - assert_eq!(history.prev(), Some(&"fifth")); - } -} diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 5f67dc15b8b1e6154a1bdfd06d092755c462814c..2a72cc6f486e3dfc0d175fa4b6640f27e9da4ecf 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -12,34 +12,25 @@ use audio::{Audio, Sound}; use buffer_diff::BufferDiff; use collections::{HashMap, HashSet}; use editor::scroll::Autoscroll; -use editor::{ - AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, - EditorStyle, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects, -}; +use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; use gpui::{ - Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId, - FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay, - SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, - Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, - linear_gradient, list, percentage, point, prelude::*, pulsating_between, + Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, EdgesRefinement, Empty, Entity, + EntityId, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, + PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, + TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, + linear_color_stop, linear_gradient, list, percentage, point, prelude::*, pulsating_between, }; +use language::Buffer; use language::language_settings::SoftWrap; -use language::{Buffer, Language}; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; -use parking_lot::Mutex; -use project::{CompletionIntent, Project}; +use project::Project; use prompt_store::PromptId; use rope::Point; use settings::{Settings as _, SettingsStore}; -use std::fmt::Write as _; -use std::path::PathBuf; -use std::{ - cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc, - time::Duration, -}; +use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration}; use terminal_view::TerminalView; -use text::{Anchor, BufferSnapshot}; +use text::Anchor; use theme::ThemeSettings; use ui::{ Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, @@ -47,14 +38,12 @@ use ui::{ }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; -use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage, ToggleModelSelector}; +use zed_actions::agent::{Chat, ToggleModelSelector}; use zed_actions::assistant::OpenRulesLibrary; use crate::acp::AcpModelSelectorPopover; -use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet}; -use crate::acp::message_history::MessageHistory; +use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; -use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES}; use crate::ui::{AgentNotification, AgentNotificationEvent}; use crate::{ AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll, @@ -62,6 +51,9 @@ use crate::{ const RESPONSE_PADDING_X: Pixels = px(19.); +pub const MIN_EDITOR_LINES: usize = 4; +pub const MAX_EDITOR_LINES: usize = 8; + pub struct AcpThreadView { agent: Rc, workspace: WeakEntity, @@ -71,11 +63,8 @@ pub struct AcpThreadView { thread_state: ThreadState, diff_editors: HashMap>, terminal_views: HashMap>, - message_editor: Entity, + message_editor: Entity, model_selector: Option>, - message_set_from_history: Option, - _message_editor_subscription: Subscription, - mention_set: Arc>, notifications: Vec>, notification_subscriptions: HashMap, Vec>, last_error: Option>, @@ -88,9 +77,16 @@ pub struct AcpThreadView { plan_expanded: bool, editor_expanded: bool, terminal_expanded: bool, - message_history: Rc>>>, + editing_message: Option, _cancel_task: Option>, - _subscriptions: [Subscription; 1], + _subscriptions: [Subscription; 2], +} + +struct EditingMessage { + index: usize, + message_id: UserMessageId, + editor: Entity, + _subscription: Subscription, } enum ThreadState { @@ -117,83 +113,30 @@ impl AcpThreadView { project: Entity, thread_store: Entity, text_thread_store: Entity, - message_history: Rc>>>, - min_lines: usize, - max_lines: Option, window: &mut Window, cx: &mut Context, ) -> Self { - let language = Language::new( - language::LanguageConfig { - completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']), - ..Default::default() - }, - None, - ); - - let mention_set = Arc::new(Mutex::new(MentionSet::default())); - let message_editor = cx.new(|cx| { - let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - - let mut editor = Editor::new( + MessageEditor::new( + workspace.clone(), + project.clone(), + thread_store.clone(), + text_thread_store.clone(), editor::EditorMode::AutoHeight { - min_lines, - max_lines: max_lines, + min_lines: MIN_EDITOR_LINES, + max_lines: Some(MAX_EDITOR_LINES), }, - buffer, - None, window, cx, - ); - editor.set_placeholder_text("Message the agent - @ to include files", cx); - editor.set_show_indent_guides(false, cx); - editor.set_soft_wrap(); - editor.set_use_modal_editing(true); - editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( - mention_set.clone(), - workspace.clone(), - thread_store.downgrade(), - text_thread_store.downgrade(), - cx.weak_entity(), - )))); - editor.set_context_menu_options(ContextMenuOptions { - min_entries_visible: 12, - max_entries_visible: 12, - placement: Some(ContextMenuPlacement::Above), - }); - editor + ) }); - let message_editor_subscription = - cx.subscribe(&message_editor, |this, editor, event, cx| { - if let editor::EditorEvent::BufferEdited = &event { - let buffer = editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .unwrap() - .read(cx) - .snapshot(); - if let Some(message) = this.message_set_from_history.clone() - && message.version() != buffer.version() - { - this.message_set_from_history = None; - } - - if this.message_set_from_history.is_none() { - this.message_history.borrow_mut().reset_position(); - } - } - }); - - let mention_set = mention_set.clone(); - let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0)); - let subscription = cx.observe_global_in::(window, Self::settings_changed); + let subscriptions = [ + cx.observe_global_in::(window, Self::settings_changed), + cx.subscribe_in(&message_editor, window, Self::on_message_editor_event), + ]; Self { agent: agent.clone(), @@ -204,9 +147,6 @@ impl AcpThreadView { thread_state: Self::initial_state(agent, workspace, project, window, cx), message_editor, model_selector: None, - message_set_from_history: None, - _message_editor_subscription: message_editor_subscription, - mention_set, notifications: Vec::new(), notification_subscriptions: HashMap::default(), diff_editors: Default::default(), @@ -217,12 +157,12 @@ impl AcpThreadView { auth_task: None, expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), + editing_message: None, edits_expanded: false, plan_expanded: false, editor_expanded: false, terminal_expanded: true, - message_history, - _subscriptions: [subscription], + _subscriptions: subscriptions, _cancel_task: None, } } @@ -370,7 +310,7 @@ impl AcpThreadView { } } - pub fn cancel(&mut self, cx: &mut Context) { + pub fn cancel_generation(&mut self, cx: &mut Context) { self.last_error.take(); if let Some(thread) = self.thread() { @@ -390,193 +330,118 @@ impl AcpThreadView { fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context) { self.editor_expanded = is_expanded; - self.message_editor.update(cx, |editor, _| { - if self.editor_expanded { - editor.set_mode(EditorMode::Full { - scale_ui_elements_with_buffer_font_size: false, - show_active_line_background: false, - sized_by_content: false, - }) + self.message_editor.update(cx, |editor, cx| { + if is_expanded { + editor.set_mode( + EditorMode::Full { + scale_ui_elements_with_buffer_font_size: false, + show_active_line_background: false, + sized_by_content: false, + }, + cx, + ) } else { - editor.set_mode(EditorMode::AutoHeight { - min_lines: MIN_EDITOR_LINES, - max_lines: Some(MAX_EDITOR_LINES), - }) + editor.set_mode( + EditorMode::AutoHeight { + min_lines: MIN_EDITOR_LINES, + max_lines: Some(MAX_EDITOR_LINES), + }, + cx, + ) } }); cx.notify(); } - fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context) { - self.last_error.take(); + pub fn on_message_editor_event( + &mut self, + _: &Entity, + event: &MessageEditorEvent, + window: &mut Window, + cx: &mut Context, + ) { + match event { + MessageEditorEvent::Send => self.send(window, cx), + MessageEditorEvent::Cancel => self.cancel_generation(cx), + } + } - let mut ix = 0; - let mut chunks: Vec = Vec::new(); - let project = self.project.clone(); + fn send(&mut self, window: &mut Window, cx: &mut Context) { + let contents = self + .message_editor + .update(cx, |message_editor, cx| message_editor.contents(window, cx)); + self.send_impl(contents, window, cx) + } - let thread_store = self.thread_store.clone(); - let text_thread_store = self.text_thread_store.clone(); + fn send_impl( + &mut self, + contents: Task>>, + window: &mut Window, + cx: &mut Context, + ) { + self.last_error.take(); + self.editing_message.take(); - let contents = - self.mention_set - .lock() - .contents(project, thread_store, text_thread_store, window, cx); + let Some(thread) = self.thread().cloned() else { + return; + }; + let task = cx.spawn_in(window, async move |this, cx| { + let contents = contents.await?; - cx.spawn_in(window, async move |this, cx| { - let contents = match contents.await { - Ok(contents) => contents, - Err(e) => { - this.update(cx, |this, cx| { - this.last_error = - Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx))); - }) - .ok(); - return; - } - }; + if contents.is_empty() { + return Ok(()); + } this.update_in(cx, |this, window, cx| { - this.message_editor.update(cx, |editor, cx| { - let text = editor.text(cx); - editor.display_map.update(cx, |map, cx| { - let snapshot = map.snapshot(cx); - for (crease_id, crease) in snapshot.crease_snapshot.creases() { - // Skip creases that have been edited out of the message buffer. - if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { - continue; - } - - if let Some(mention) = contents.get(&crease_id) { - let crease_range = - crease.range().to_offset(&snapshot.buffer_snapshot); - if crease_range.start > ix { - chunks.push(text[ix..crease_range.start].into()); - } - chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource { - annotations: None, - resource: acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents { - mime_type: None, - text: mention.content.clone(), - uri: mention.uri.to_uri().to_string(), - }, - ), - })); - ix = crease_range.end; - } - } - - if ix < text.len() { - let last_chunk = text[ix..].trim_end(); - if !last_chunk.is_empty() { - chunks.push(last_chunk.into()); - } - } - }) - }); - - if chunks.is_empty() { - return; - } - - let Some(thread) = this.thread() else { - return; - }; - let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx)); - - cx.spawn(async move |this, cx| { - let result = task.await; - - this.update(cx, |this, cx| { - if let Err(err) = result { - this.last_error = - Some(cx.new(|cx| { - Markdown::new(err.to_string().into(), None, None, cx) - })) - } - }) - }) - .detach(); - - let mention_set = this.mention_set.clone(); - this.set_editor_is_expanded(false, cx); - - this.message_editor.update(cx, |editor, cx| { - editor.clear(window, cx); - editor.remove_creases(mention_set.lock().drain(), cx) - }); - this.scroll_to_bottom(cx); + this.message_editor.update(cx, |message_editor, cx| { + message_editor.clear(window, cx); + }); + })?; + let send = thread.update(cx, |thread, cx| thread.send(contents, cx))?; + send.await + }); - this.message_history.borrow_mut().push(chunks); - }) - .ok(); + cx.spawn(async move |this, cx| { + if let Err(e) = task.await { + this.update(cx, |this, cx| { + this.last_error = + Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx))); + cx.notify() + }) + .ok(); + } }) .detach(); } - fn previous_history_message( - &mut self, - _: &PreviousHistoryMessage, - window: &mut Window, - cx: &mut Context, - ) { - if self.message_set_from_history.is_none() && !self.message_editor.read(cx).is_empty(cx) { - self.message_editor.update(cx, |editor, cx| { - editor.move_up(&Default::default(), window, cx); - }); - return; - } - - self.message_set_from_history = Self::set_draft_message( - self.message_editor.clone(), - self.mention_set.clone(), - self.project.clone(), - self.message_history - .borrow_mut() - .prev() - .map(|blocks| blocks.as_slice()), - window, - cx, - ); + fn cancel_editing(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context) { + self.editing_message.take(); + cx.notify(); } - fn next_history_message( - &mut self, - _: &NextHistoryMessage, - window: &mut Window, - cx: &mut Context, - ) { - if self.message_set_from_history.is_none() { - self.message_editor.update(cx, |editor, cx| { - editor.move_down(&Default::default(), window, cx); - }); + fn regenerate(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { + let Some(editing_message) = self.editing_message.take() else { return; - } + }; - let mut message_history = self.message_history.borrow_mut(); - let next_history = message_history.next(); - - let set_draft_message = Self::set_draft_message( - self.message_editor.clone(), - self.mention_set.clone(), - self.project.clone(), - Some( - next_history - .map(|blocks| blocks.as_slice()) - .unwrap_or_else(|| &[]), - ), - window, - cx, - ); - // If we reset the text to an empty string because we ran out of history, - // we don't want to mark it as coming from the history - self.message_set_from_history = if next_history.is_some() { - set_draft_message - } else { - None + let Some(thread) = self.thread().cloned() else { + return; }; + + let rewind = thread.update(cx, |thread, cx| { + thread.rewind(editing_message.message_id, cx) + }); + + let contents = editing_message + .editor + .update(cx, |message_editor, cx| message_editor.contents(window, cx)); + let task = cx.foreground_executor().spawn(async move { + rewind.await?; + contents.await + }); + self.send_impl(task, window, cx); } fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context) { @@ -606,92 +471,6 @@ impl AcpThreadView { }) } - fn set_draft_message( - message_editor: Entity, - mention_set: Arc>, - project: Entity, - message: Option<&[acp::ContentBlock]>, - window: &mut Window, - cx: &mut Context, - ) -> Option { - cx.notify(); - - let message = message?; - - let mut text = String::new(); - let mut mentions = Vec::new(); - - for chunk in message { - match chunk { - acp::ContentBlock::Text(text_content) => { - text.push_str(&text_content.text); - } - acp::ContentBlock::Resource(acp::EmbeddedResource { - resource: acp::EmbeddedResourceResource::TextResourceContents(resource), - .. - }) => { - let path = PathBuf::from(&resource.uri); - let project_path = project.read(cx).project_path_for_absolute_path(&path, cx); - let start = text.len(); - let _ = write!(&mut text, "{}", MentionUri::File(path).to_uri()); - let end = text.len(); - if let Some(project_path) = project_path { - let filename: SharedString = project_path - .path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string() - .into(); - mentions.push((start..end, project_path, filename)); - } - } - acp::ContentBlock::Image(_) - | acp::ContentBlock::Audio(_) - | acp::ContentBlock::Resource(_) - | acp::ContentBlock::ResourceLink(_) => {} - } - } - - let snapshot = message_editor.update(cx, |editor, cx| { - editor.set_text(text, window, cx); - editor.buffer().read(cx).snapshot(cx) - }); - - for (range, project_path, filename) in mentions { - let crease_icon_path = if project_path.path.is_dir() { - FileIcons::get_folder_icon(false, cx) - .unwrap_or_else(|| IconName::Folder.path().into()) - } else { - FileIcons::get_icon(Path::new(project_path.path.as_ref()), cx) - .unwrap_or_else(|| IconName::File.path().into()) - }; - - let anchor = snapshot.anchor_before(range.start); - if let Some(project_path) = project.read(cx).absolute_path(&project_path, cx) { - let crease_id = crate::context_picker::insert_crease_for_mention( - anchor.excerpt_id, - anchor.text_anchor, - range.end - range.start, - filename, - crease_icon_path, - message_editor.clone(), - window, - cx, - ); - - if let Some(crease_id) = crease_id { - mention_set - .lock() - .insert(crease_id, MentionUri::File(project_path)); - } - } - } - - let snapshot = snapshot.as_singleton().unwrap().2.clone(); - Some(snapshot.text) - } - fn handle_thread_event( &mut self, thread: &Entity, @@ -968,12 +747,28 @@ impl AcpThreadView { .border_1() .border_color(cx.theme().colors().border) .text_xs() - .children(message.content.markdown().map(|md| { - self.render_markdown( - md.clone(), - user_message_markdown_style(window, cx), - ) - })), + .id("message") + .on_click(cx.listener({ + move |this, _, window, cx| this.start_editing_message(index, window, cx) + })) + .children( + if let Some(editing) = self.editing_message.as_ref() + && Some(&editing.message_id) == message.id.as_ref() + { + Some( + self.render_edit_message_editor(editing, cx) + .into_any_element(), + ) + } else { + message.content.markdown().map(|md| { + self.render_markdown( + md.clone(), + user_message_markdown_style(window, cx), + ) + .into_any_element() + }) + }, + ), ) .into_any(), AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { @@ -1035,7 +830,7 @@ impl AcpThreadView { }; let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); - if index == total_entries - 1 && !is_generating { + let primary = if index == total_entries - 1 && !is_generating { v_flex() .w_full() .child(primary) @@ -1043,6 +838,28 @@ impl AcpThreadView { .into_any_element() } else { primary + }; + + if let Some(editing) = self.editing_message.as_ref() + && editing.index < index + { + let backdrop = div() + .id(("backdrop", index)) + .size_full() + .absolute() + .inset_0() + .bg(cx.theme().colors().panel_background) + .opacity(0.8) + .block_mouse_except_scroll() + .on_click(cx.listener(Self::cancel_editing)); + + div() + .relative() + .child(backdrop) + .child(primary) + .into_any_element() + } else { + primary } } @@ -2561,34 +2378,7 @@ impl AcpThreadView { .size_full() .pt_1() .pr_2p5() - .child(div().flex_1().child({ - let settings = ThemeSettings::get_global(cx); - let font_size = TextSize::Small - .rems(cx) - .to_pixels(settings.agent_font_size(cx)); - let line_height = settings.buffer_line_height.value() * font_size; - - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_features: settings.buffer_font.features.clone(), - font_size: font_size.into(), - line_height: line_height.into(), - ..Default::default() - }; - - EditorElement::new( - &self.message_editor, - EditorStyle { - background: editor_bg_color, - local_player: cx.theme().players().local(), - text: text_style, - syntax: cx.theme().syntax().clone(), - ..Default::default() - }, - ) - })) + .child(self.message_editor.clone()) .child( h_flex() .absolute() @@ -2633,6 +2423,129 @@ impl AcpThreadView { .into_any() } + fn start_editing_message(&mut self, index: usize, window: &mut Window, cx: &mut Context) { + let Some(thread) = self.thread() else { + return; + }; + let Some(AgentThreadEntry::UserMessage(message)) = thread.read(cx).entries().get(index) + else { + return; + }; + let Some(message_id) = message.id.clone() else { + return; + }; + + self.list_state.scroll_to_reveal_item(index); + + let chunks = message.chunks.clone(); + let editor = cx.new(|cx| { + let mut editor = MessageEditor::new( + self.workspace.clone(), + self.project.clone(), + self.thread_store.clone(), + self.text_thread_store.clone(), + editor::EditorMode::AutoHeight { + min_lines: 1, + max_lines: None, + }, + window, + cx, + ); + editor.set_message(&chunks, window, cx); + editor + }); + let subscription = + cx.subscribe_in(&editor, window, |this, _, event, window, cx| match event { + MessageEditorEvent::Send => { + this.regenerate(&Default::default(), window, cx); + } + MessageEditorEvent::Cancel => { + this.cancel_editing(&Default::default(), window, cx); + } + }); + editor.focus_handle(cx).focus(window); + + self.editing_message.replace(EditingMessage { + index: index, + message_id: message_id.clone(), + editor, + _subscription: subscription, + }); + cx.notify(); + } + + fn render_edit_message_editor(&self, editing: &EditingMessage, cx: &Context) -> Div { + v_flex() + .w_full() + .gap_2() + .child(editing.editor.clone()) + .child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::Warning) + .color(Color::Warning) + .size(IconSize::XSmall), + ) + .child( + Label::new("Editing will restart the thread from this point.") + .color(Color::Muted) + .size(LabelSize::XSmall), + ) + .child(self.render_editing_message_editor_buttons(editing, cx)), + ) + } + + fn render_editing_message_editor_buttons( + &self, + editing: &EditingMessage, + cx: &Context, + ) -> Div { + h_flex() + .gap_0p5() + .flex_1() + .justify_end() + .child( + IconButton::new("cancel-edit-message", IconName::Close) + .shape(ui::IconButtonShape::Square) + .icon_color(Color::Error) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = editing.editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Cancel Edit", + &menu::Cancel, + &focus_handle, + window, + cx, + ) + } + }) + .on_click(cx.listener(Self::cancel_editing)), + ) + .child( + IconButton::new("confirm-edit-message", IconName::Return) + .disabled(editing.editor.read(cx).is_empty(cx)) + .shape(ui::IconButtonShape::Square) + .icon_color(Color::Muted) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = editing.editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Regenerate", + &menu::Confirm, + &focus_handle, + window, + cx, + ) + } + }) + .on_click(cx.listener(Self::regenerate)), + ) + } + fn render_send_button(&self, cx: &mut Context) -> AnyElement { if self.thread().map_or(true, |thread| { thread.read(cx).status() == ThreadStatus::Idle @@ -2649,7 +2562,7 @@ impl AcpThreadView { button.tooltip(Tooltip::text("Type a message to submit")) }) .on_click(cx.listener(|this, _, window, cx| { - this.chat(&Chat, window, cx); + this.send(window, cx); })) .into_any_element() } else { @@ -2659,7 +2572,7 @@ impl AcpThreadView { .tooltip(move |window, cx| { Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx) }) - .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx))) + .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx))) .into_any_element() } } @@ -2723,10 +2636,10 @@ impl AcpThreadView { if let Some(mention) = MentionUri::parse(&url).log_err() { workspace.update(cx, |workspace, cx| match mention { - MentionUri::File(path) => { + MentionUri::File { abs_path, .. } => { let project = workspace.project(); let Some((path, entry)) = project.update(cx, |project, cx| { - let path = project.find_project_path(path, cx)?; + let path = project.find_project_path(abs_path, cx)?; let entry = project.entry_for_path(&path, cx)?; Some((path, entry)) }) else { @@ -3175,57 +3088,11 @@ impl AcpThreadView { paths: Vec, _added_worktrees: Vec>, window: &mut Window, - cx: &mut Context<'_, Self>, + cx: &mut Context, ) { - let buffer = self.message_editor.read(cx).buffer().clone(); - let Some((&excerpt_id, _, _)) = buffer.read(cx).snapshot(cx).as_singleton() else { - return; - }; - let Some(buffer) = buffer.read(cx).as_singleton() else { - return; - }; - for path in paths { - let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else { - continue; - }; - let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else { - continue; - }; - - let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); - let path_prefix = abs_path - .file_name() - .unwrap_or(path.path.as_os_str()) - .display() - .to_string(); - let Some(completion) = ContextPickerCompletionProvider::completion_for_path( - path, - &path_prefix, - false, - entry.is_dir(), - excerpt_id, - anchor..anchor, - self.message_editor.clone(), - self.mention_set.clone(), - self.project.clone(), - cx, - ) else { - continue; - }; - - self.message_editor.update(cx, |message_editor, cx| { - message_editor.edit( - [( - multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), - completion.new_text, - )], - cx, - ); - }); - if let Some(confirm) = completion.confirm.clone() { - confirm(CompletionIntent::Complete, window, cx); - } - } + self.message_editor.update(cx, |message_editor, cx| { + message_editor.insert_dragged_files(paths, window, cx); + }) } } @@ -3242,9 +3109,6 @@ impl Render for AcpThreadView { v_flex() .size_full() .key_context("AcpThread") - .on_action(cx.listener(Self::chat)) - .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 { @@ -3540,13 +3404,16 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { } #[cfg(test)] -mod tests { +pub(crate) mod tests { + use std::{path::Path, sync::Arc}; + use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::SessionId; use editor::EditorSettings; use fs::FakeFs; use futures::future::try_join_all; use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; + use parking_lot::Mutex; use rand::Rng; use settings::SettingsStore; @@ -3576,7 +3443,7 @@ mod tests { cx.deactivate_window(); thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.chat(&Chat, window, cx); + thread_view.send(window, cx); }); cx.run_until_parked(); @@ -3603,7 +3470,7 @@ mod tests { cx.deactivate_window(); thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.chat(&Chat, window, cx); + thread_view.send(window, cx); }); cx.run_until_parked(); @@ -3649,7 +3516,7 @@ mod tests { cx.deactivate_window(); thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.chat(&Chat, window, cx); + thread_view.send(window, cx); }); cx.run_until_parked(); @@ -3683,9 +3550,6 @@ mod tests { project, thread_store.clone(), text_thread_store.clone(), - Rc::new(RefCell::new(MessageHistory::default())), - 1, - None, window, cx, ) @@ -3899,7 +3763,7 @@ mod tests { } } - fn init_test(cx: &mut TestAppContext) { + pub(crate) fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index e47cbe3714939aa3b839d575112c3b9699a0eeba..73915195f5f483ef16b825d5b0b81d2d64c6dced 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1,4 +1,3 @@ -use std::cell::RefCell; use std::ops::{Not, Range}; use std::path::Path; use std::rc::Rc; @@ -11,7 +10,6 @@ use serde::{Deserialize, Serialize}; use crate::NewExternalAgentThread; use crate::agent_diff::AgentDiffThread; -use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES}; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, @@ -477,8 +475,6 @@ pub struct AgentPanel { configuration_subscription: Option, local_timezone: UtcOffset, active_view: ActiveView, - acp_message_history: - Rc>>>, previous_view: Option, history_store: Entity, history: Entity, @@ -766,7 +762,6 @@ impl AgentPanel { .unwrap(), inline_assist_context_store, previous_view: None, - acp_message_history: Default::default(), history_store: history_store.clone(), history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)), hovered_recent_history_item: None, @@ -824,7 +819,9 @@ impl AgentPanel { thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx)); } ActiveView::ExternalAgentThread { thread_view, .. } => { - thread_view.update(cx, |thread_element, cx| thread_element.cancel(cx)); + thread_view.update(cx, |thread_element, cx| { + thread_element.cancel_generation(cx) + }); } ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} } @@ -963,7 +960,6 @@ impl AgentPanel { ) { let workspace = self.workspace.clone(); let project = self.project.clone(); - let message_history = self.acp_message_history.clone(); let fs = self.fs.clone(); const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent"; @@ -1016,9 +1012,6 @@ impl AgentPanel { project, thread_store.clone(), text_thread_store.clone(), - message_history, - MIN_EDITOR_LINES, - Some(MAX_EDITOR_LINES), window, cx, ) @@ -1575,8 +1568,6 @@ impl AgentPanel { self.active_view = new_view; } - self.acp_message_history.borrow_mut().reset_position(); - self.focus_handle(cx).focus(window); } diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 64891b6973bac04efdfb4cbadadd12cfcca3be10..9455369e9a2c234ba39572642d382d6d8dd76c46 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -285,10 +285,6 @@ pub mod agent { ResetOnboarding, /// Starts a chat conversation with the agent. Chat, - /// Displays the previous message in the history. - PreviousHistoryMessage, - /// Displays the next message in the history. - NextHistoryMessage, /// Toggles the language model selector dropdown. #[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])] ToggleModelSelector From ba2c45bc53194d3e2b94d909966a06f213017de5 Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Thu, 14 Aug 2025 17:02:51 +0200 Subject: [PATCH 002/744] Add FutureExt::with_timeout and use it for for Room::maintain_connection (#36175) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/call/src/call_impl/room.rs | 90 ++++++++++++++--------------- crates/gpui/src/app/test_context.rs | 4 +- crates/gpui/src/gpui.rs | 2 +- crates/gpui/src/util.rs | 73 +++++++++++++++++++---- 4 files changed, 107 insertions(+), 62 deletions(-) diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index afeee4c924feb2990668f953d5b2f7dfcff26f34..73cb8518a63781e8c2b3a92aa7b3111996f05f83 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -10,10 +10,10 @@ use client::{ }; use collections::{BTreeMap, HashMap, HashSet}; use fs::Fs; -use futures::{FutureExt, StreamExt}; +use futures::StreamExt; use gpui::{ - App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, ScreenCaptureSource, - ScreenCaptureStream, Task, WeakEntity, + App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FutureExt as _, + ScreenCaptureSource, ScreenCaptureStream, Task, Timeout, WeakEntity, }; use gpui_tokio::Tokio; use language::LanguageRegistry; @@ -370,57 +370,53 @@ impl Room { })?; // Wait for client to re-establish a connection to the server. - { - let mut reconnection_timeout = - cx.background_executor().timer(RECONNECT_TIMEOUT).fuse(); - let client_reconnection = async { - let mut remaining_attempts = 3; - while remaining_attempts > 0 { - if client_status.borrow().is_connected() { - log::info!("client reconnected, attempting to rejoin room"); - - let Some(this) = this.upgrade() else { break }; - match this.update(cx, |this, cx| this.rejoin(cx)) { - Ok(task) => { - if task.await.log_err().is_some() { - return true; - } else { - remaining_attempts -= 1; - } + let executor = cx.background_executor().clone(); + let client_reconnection = async { + let mut remaining_attempts = 3; + while remaining_attempts > 0 { + if client_status.borrow().is_connected() { + log::info!("client reconnected, attempting to rejoin room"); + + let Some(this) = this.upgrade() else { break }; + match this.update(cx, |this, cx| this.rejoin(cx)) { + Ok(task) => { + if task.await.log_err().is_some() { + return true; + } else { + remaining_attempts -= 1; } - Err(_app_dropped) => return false, } - } else if client_status.borrow().is_signed_out() { - return false; + Err(_app_dropped) => return false, } - - log::info!( - "waiting for client status change, remaining attempts {}", - remaining_attempts - ); - client_status.next().await; + } else if client_status.borrow().is_signed_out() { + return false; } - false + + log::info!( + "waiting for client status change, remaining attempts {}", + remaining_attempts + ); + client_status.next().await; } - .fuse(); - futures::pin_mut!(client_reconnection); - - futures::select_biased! { - reconnected = client_reconnection => { - if reconnected { - log::info!("successfully reconnected to room"); - // If we successfully joined the room, go back around the loop - // waiting for future connection status changes. - continue; - } - } - _ = reconnection_timeout => { - log::info!("room reconnection timeout expired"); - } + false + }; + + match client_reconnection + .with_timeout(RECONNECT_TIMEOUT, &executor) + .await + { + Ok(true) => { + log::info!("successfully reconnected to room"); + // If we successfully joined the room, go back around the loop + // waiting for future connection status changes. + continue; + } + Ok(false) => break, + Err(Timeout) => { + log::info!("room reconnection timeout expired"); + break; } } - - break; } } diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 35e60326714f049faeaac54e8d979a91f9d97bbc..a96c24432a8851cb8b7dbadb3f7e794971dfca0b 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -585,7 +585,7 @@ impl Entity { cx.executor().advance_clock(advance_clock_by); async move { - let notification = crate::util::timeout(duration, rx.recv()) + let notification = crate::util::smol_timeout(duration, rx.recv()) .await .expect("next notification timed out"); drop(subscription); @@ -629,7 +629,7 @@ impl Entity { let handle = self.downgrade(); async move { - crate::util::timeout(Duration::from_secs(1), async move { + crate::util::smol_timeout(Duration::from_secs(1), async move { loop { { let cx = cx.borrow(); diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 09799eb910f0eeece17fd9975c3c13f6accd2df6..f0ce04a915bba30fff6988ae42b7973bb286b49e 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -157,7 +157,7 @@ pub use taffy::{AvailableSpace, LayoutId}; #[cfg(any(test, feature = "test-support"))] pub use test::*; pub use text_system::*; -pub use util::arc_cow::ArcCow; +pub use util::{FutureExt, Timeout, arc_cow::ArcCow}; pub use view::*; pub use window::*; diff --git a/crates/gpui/src/util.rs b/crates/gpui/src/util.rs index 5e92335fdc86e331d3a469c4384043fd9799b00a..f357034fbf52eda780447ebd7c0ff32432eaac4a 100644 --- a/crates/gpui/src/util.rs +++ b/crates/gpui/src/util.rs @@ -1,13 +1,11 @@ -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering::SeqCst; -#[cfg(any(test, feature = "test-support"))] -use std::time::Duration; - -#[cfg(any(test, feature = "test-support"))] -use futures::Future; - -#[cfg(any(test, feature = "test-support"))] -use smol::future::FutureExt; +use crate::{BackgroundExecutor, Task}; +use std::{ + future::Future, + pin::Pin, + sync::atomic::{AtomicUsize, Ordering::SeqCst}, + task, + time::Duration, +}; pub use util::*; @@ -70,8 +68,59 @@ pub trait FluentBuilder { } } +/// Extensions for Future types that provide additional combinators and utilities. +pub trait FutureExt { + /// Requires a Future to complete before the specified duration has elapsed. + /// Similar to tokio::timeout. + fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout + where + Self: Sized; +} + +impl FutureExt for T { + fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout + where + Self: Sized, + { + WithTimeout { + future: self, + timer: executor.timer(timeout), + } + } +} + +pub struct WithTimeout { + future: T, + timer: Task<()>, +} + +#[derive(Debug, thiserror::Error)] +#[error("Timed out before future resolved")] +/// Error returned by with_timeout when the timeout duration elapsed before the future resolved +pub struct Timeout; + +impl Future for WithTimeout { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> task::Poll { + // SAFETY: the fields of Timeout are private and we never move the future ourselves + // And its already pinned since we are being polled (all futures need to be pinned to be polled) + let this = unsafe { self.get_unchecked_mut() }; + let future = unsafe { Pin::new_unchecked(&mut this.future) }; + let timer = unsafe { Pin::new_unchecked(&mut this.timer) }; + + if let task::Poll::Ready(output) = future.poll(cx) { + task::Poll::Ready(Ok(output)) + } else if timer.poll(cx).is_ready() { + task::Poll::Ready(Err(Timeout)) + } else { + task::Poll::Pending + } + } +} + #[cfg(any(test, feature = "test-support"))] -pub async fn timeout(timeout: Duration, f: F) -> Result +pub async fn smol_timeout(timeout: Duration, f: F) -> Result where F: Future, { @@ -80,7 +129,7 @@ where Err(()) }; let future = async move { Ok(f.await) }; - timer.race(future).await + smol::future::FutureExt::race(timer, future).await } /// Increment the given atomic counter if it is not zero. From f514c7cc187eeb814415d0e78546ac780c857900 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 14 Aug 2025 11:22:38 -0400 Subject: [PATCH 003/744] Emit a `BreadcrumbsChanged` event when associated settings changed (#36177) Closes https://github.com/zed-industries/zed/issues/36149 Release Notes: - Fixed a bug where changing the `toolbar.breadcrumbs` setting didn't immediately update the UI when saving the `settings.json` file. --- crates/editor/src/editor.rs | 6 ++++++ crates/editor/src/items.rs | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cbee9021ed6b22ce36ea9f0473eacab52329a971..689f3973412c2f2c884552364ca1646dfcb42457 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -20209,6 +20209,7 @@ impl Editor { ); let old_cursor_shape = self.cursor_shape; + let old_show_breadcrumbs = self.show_breadcrumbs; { let editor_settings = EditorSettings::get_global(cx); @@ -20222,6 +20223,10 @@ impl Editor { cx.emit(EditorEvent::CursorShapeChanged); } + if old_show_breadcrumbs != self.show_breadcrumbs { + cx.emit(EditorEvent::BreadcrumbsChanged); + } + let project_settings = ProjectSettings::get_global(cx); self.serialize_dirty_buffers = !self.mode.is_minimap() && project_settings.session.restore_unsaved_buffers; @@ -22843,6 +22848,7 @@ pub enum EditorEvent { }, Reloaded, CursorShapeChanged, + BreadcrumbsChanged, PushedToNavHistory { anchor: Anchor, is_deactivate: bool, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 1da82c605d5cbf0555f864efc50dac97f323f777..480757a4911ed9b2ecb5b2ae09af736edf0a2b45 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1036,6 +1036,10 @@ impl Item for Editor { f(ItemEvent::UpdateBreadcrumbs); } + EditorEvent::BreadcrumbsChanged => { + f(ItemEvent::UpdateBreadcrumbs); + } + EditorEvent::DirtyChanged => { f(ItemEvent::UpdateTab); } From 528d56e8072048b9b588fd60786c937be018f94d Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 14 Aug 2025 10:29:58 -0500 Subject: [PATCH 004/744] keymap_ui: Add open keymap JSON button (#36182) Closes #ISSUE Release Notes: - Keymap Editor: Added a button in the top left to allow opening the keymap JSON file. Right clicking the button provides shortcuts to opening the default Zed and Vim keymaps as well. --- Cargo.lock | 2 ++ assets/icons/json.svg | 4 ++++ crates/icons/src/icons.rs | 1 + crates/settings_ui/Cargo.toml | 2 ++ crates/settings_ui/src/keybindings.rs | 29 ++++++++++++++++++++++++++- 5 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 assets/icons/json.svg diff --git a/Cargo.lock b/Cargo.lock index cb087f43b7d11e909c846a93f7127c57696ff1ed..96cc1581a3f6a0717b01a69e0f0833bb18fa0fc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15054,8 +15054,10 @@ dependencies = [ "ui", "ui_input", "util", + "vim", "workspace", "workspace-hack", + "zed_actions", ] [[package]] diff --git a/assets/icons/json.svg b/assets/icons/json.svg new file mode 100644 index 0000000000000000000000000000000000000000..5f012f883837f689da5c38e905b2eb0b9723945a --- /dev/null +++ b/assets/icons/json.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index f5c2a83fec36fcf67647011bb1b123d8df3f8d02..8bd76cbecf59a8c515118bfe473386e2b05efac4 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -140,6 +140,7 @@ pub enum IconName { Image, Indicator, Info, + Json, Keyboard, Library, LineHeight, diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index a4c47081c60fc6a749a753607c61c94b643a7e00..8a151359ec4bb246e23c4a09fdbe63c23c69a98a 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -42,8 +42,10 @@ tree-sitter-rust.workspace = true ui.workspace = true ui_input.workspace = true util.workspace = true +vim.workspace = true workspace-hack.workspace = true workspace.workspace = true +zed_actions.workspace = true [dev-dependencies] db = {"workspace"= true, "features" = ["test-support"]} diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index a62c669488415daa689b755d3d970d6364da6dc0..1aaab211aa4efb039d934aa5970d503640c72d5e 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -23,7 +23,7 @@ use settings::{BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAss use ui::{ ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator, Modal, ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString, - Styled as _, Tooltip, Window, prelude::*, + Styled as _, Tooltip, Window, prelude::*, right_click_menu, }; use ui_input::SingleLineInput; use util::ResultExt; @@ -1536,6 +1536,33 @@ impl Render for KeymapEditor { .child( h_flex() .gap_2() + .child( + right_click_menu("open-keymap-menu") + .menu(|window, cx| { + ContextMenu::build(window, cx, |menu, _, _| { + menu.header("Open Keymap JSON") + .action("User", zed_actions::OpenKeymap.boxed_clone()) + .action("Zed Default", zed_actions::OpenDefaultKeymap.boxed_clone()) + .action("Vim Default", vim::OpenDefaultKeymap.boxed_clone()) + }) + }) + .anchor(gpui::Corner::TopLeft) + .trigger(|open, _, _| + IconButton::new( + "OpenKeymapJsonButton", + IconName::Json + ) + .shape(ui::IconButtonShape::Square) + .when(!open, |this| + this.tooltip(move |window, cx| { + Tooltip::with_meta("Open Keymap JSON", Some(&zed_actions::OpenKeymap),"Right click to view more options", window, cx) + }) + ) + .on_click(|_, window, cx| { + window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx); + }) + ) + ) .child( div() .key_context({ From 20be133713690fd92148a448bd57146fff73cbce Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Thu, 14 Aug 2025 18:04:01 +0100 Subject: [PATCH 005/744] helix: Allow yank without a selection (#35612) Related https://github.com/zed-industries/zed/issues/4642 Release Notes: - Helix: without active selection, pressing `y` in helix mode will yank a single character under cursor. --------- Co-authored-by: Conrad Irwin --- assets/keymaps/vim.json | 2 +- crates/vim/src/helix.rs | 69 +++++++++++++++++++++++++ crates/vim/src/test/vim_test_context.rs | 30 +++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 98f9cafc40e69f9eb7bcc248e02176f85e5d8838..a3f68a77303c7a61e0d88a10a861be58e88a71b2 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -390,7 +390,7 @@ "right": "vim::WrappingRight", "h": "vim::WrappingLeft", "l": "vim::WrappingRight", - "y": "editor::Copy", + "y": "vim::HelixYank", "alt-;": "vim::OtherEnd", "ctrl-r": "vim::Redo", "f": ["vim::PushFindForward", { "before": false, "multiline": true }], diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 686c74f65e96d547bedb30fbb744c04fb5361fd8..29633ddef9720cba4d954ab8f4f1583bc7acfa71 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -15,6 +15,8 @@ actions!( [ /// Switches to normal mode after the cursor (Helix-style). HelixNormalAfter, + /// Yanks the current selection or character if no selection. + HelixYank, /// Inserts at the beginning of the selection. HelixInsert, /// Appends at the end of the selection. @@ -26,6 +28,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::helix_normal_after); Vim::action(editor, cx, Vim::helix_insert); Vim::action(editor, cx, Vim::helix_append); + Vim::action(editor, cx, Vim::helix_yank); } impl Vim { @@ -310,6 +313,47 @@ impl Vim { } } + pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context) { + self.update_editor(cx, |vim, editor, cx| { + let has_selection = editor + .selections + .all_adjusted(cx) + .iter() + .any(|selection| !selection.is_empty()); + + if !has_selection { + // If no selection, expand to current character (like 'v' does) + editor.change_selections(Default::default(), window, cx, |s| { + s.move_with(|map, selection| { + let head = selection.head(); + let new_head = movement::saturating_right(map, head); + selection.set_tail(head, SelectionGoal::None); + selection.set_head(new_head, SelectionGoal::None); + }); + }); + vim.yank_selections_content( + editor, + crate::motion::MotionKind::Exclusive, + window, + cx, + ); + editor.change_selections(Default::default(), window, cx, |s| { + s.move_with(|_map, selection| { + selection.collapse_to(selection.start, SelectionGoal::None); + }); + }); + } else { + // Yank the selection(s) + vim.yank_selections_content( + editor, + crate::motion::MotionKind::Exclusive, + window, + cx, + ); + } + }); + } + fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context) { self.start_recording(cx); self.update_editor(cx, |_, editor, cx| { @@ -703,4 +747,29 @@ mod test { cx.assert_state("«xxˇ»", Mode::HelixNormal); } + + #[gpui::test] + async fn test_helix_yank(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + + // Test yanking current character with no selection + cx.set_state("hello ˇworld", Mode::HelixNormal); + cx.simulate_keystrokes("y"); + + // Test cursor remains at the same position after yanking single character + cx.assert_state("hello ˇworld", Mode::HelixNormal); + cx.shared_clipboard().assert_eq("w"); + + // Move cursor and yank another character + cx.simulate_keystrokes("l"); + cx.simulate_keystrokes("y"); + cx.shared_clipboard().assert_eq("o"); + + // Test yanking with existing selection + cx.set_state("hello «worlˇ»d", Mode::HelixNormal); + cx.simulate_keystrokes("y"); + cx.shared_clipboard().assert_eq("worl"); + cx.assert_state("hello «worlˇ»d", Mode::HelixNormal); + } } diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 904e48e5a3e6b273bbc50e5bee37faf6e996633f..5b6cb55e8c1a96db7c82510cecc4daf5cd8d000d 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -143,6 +143,16 @@ impl VimTestContext { }) } + pub fn enable_helix(&mut self) { + self.cx.update(|_, cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::(cx, |s| { + *s = Some(true) + }); + }); + }) + } + pub fn mode(&mut self) -> Mode { self.update_editor(|editor, _, cx| editor.addon::().unwrap().entity.read(cx).mode) } @@ -210,6 +220,26 @@ impl VimTestContext { assert_eq!(self.mode(), Mode::Normal, "{}", self.assertion_context()); assert_eq!(self.active_operator(), None, "{}", self.assertion_context()); } + + pub fn shared_clipboard(&mut self) -> VimClipboard { + VimClipboard { + editor: self + .read_from_clipboard() + .map(|item| item.text().unwrap().to_string()) + .unwrap_or_default(), + } + } +} + +pub struct VimClipboard { + editor: String, +} + +impl VimClipboard { + #[track_caller] + pub fn assert_eq(&self, expected: &str) { + assert_eq!(self.editor, expected); + } } impl Deref for VimTestContext { From 9a2b7ef372021e5bcad759a2dc871e0743b602c4 Mon Sep 17 00:00:00 2001 From: fantacell Date: Thu, 14 Aug 2025 19:04:07 +0200 Subject: [PATCH 006/744] helix: Change f and t motions (#35216) In vim and zed (vim and helix modes) typing "tx" will jump before the next `x`, but typing it again won't do anything. But in helix the cursor just jumps before the `x` after that. I added that in helix mode. This also solves another small issue where the selection doesn't include the first `x` after typing "fx" twice. And similarly after typing "Fx" or "Tx" the selection should include the character that the motion startet on. Release Notes: - helix: Fixed inconsistencies in the "f" and "t" motions --- crates/text/src/selection.rs | 13 ++ crates/vim/src/helix.rs | 282 ++++++++++++++++++----------------- crates/vim/src/motion.rs | 3 +- 3 files changed, 158 insertions(+), 140 deletions(-) diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 18b82dbb6a6326dfe07703ee6881e9cef8442a76..d3c280bde803b392767eecd13e65a70aafc5b1a5 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -104,6 +104,19 @@ impl Selection { self.goal = new_goal; } + pub fn set_head_tail(&mut self, head: T, tail: T, new_goal: SelectionGoal) { + if head < tail { + self.reversed = true; + self.start = head; + self.end = tail; + } else { + self.reversed = false; + self.start = tail; + self.end = head; + } + self.goal = new_goal; + } + pub fn swap_head_tail(&mut self) { if self.reversed { self.reversed = false; diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 29633ddef9720cba4d954ab8f4f1583bc7acfa71..0c8c06d8ab66f422df7c79c33e0f179db59f716f 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -1,9 +1,11 @@ +use editor::display_map::DisplaySnapshot; use editor::{DisplayPoint, Editor, SelectionEffects, ToOffset, ToPoint, movement}; use gpui::{Action, actions}; use gpui::{Context, Window}; use language::{CharClassifier, CharKind}; use text::{Bias, SelectionGoal}; +use crate::motion; use crate::{ Vim, motion::{Motion, right}, @@ -58,116 +60,103 @@ impl Vim { self.helix_move_cursor(motion, times, window, cx); } - fn helix_find_range_forward( + /// Updates all selections based on where the cursors are. + fn helix_new_selections( &mut self, - times: Option, window: &mut Window, cx: &mut Context, - mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, + mut change: impl FnMut( + // the start of the cursor + DisplayPoint, + &DisplaySnapshot, + ) -> Option<(DisplayPoint, DisplayPoint)>, ) { self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { - let times = times.unwrap_or(1); - let new_goal = SelectionGoal::None; - let mut head = selection.head(); - let mut tail = selection.tail(); - - if head == map.max_point() { - return; - } - - // collapse to block cursor - if tail < head { - tail = movement::left(map, head); + let cursor_start = if selection.reversed || selection.is_empty() { + selection.head() } else { - tail = head; - head = movement::right(map, head); - } - - // create a classifier - let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map)); - - for _ in 0..times { - let (maybe_next_tail, next_head) = - movement::find_boundary_trail(map, head, |left, right| { - is_boundary(left, right, &classifier) - }); - - if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail { - break; - } - - head = next_head; - if let Some(next_tail) = maybe_next_tail { - tail = next_tail; - } - } + movement::left(map, selection.head()) + }; + let Some((head, tail)) = change(cursor_start, map) else { + return; + }; - selection.set_tail(tail, new_goal); - selection.set_head(head, new_goal); + selection.set_head_tail(head, tail, SelectionGoal::None); }); }); }); } - fn helix_find_range_backward( + fn helix_find_range_forward( &mut self, times: Option, window: &mut Window, cx: &mut Context, mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, ) { - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let times = times.unwrap_or(1); - let new_goal = SelectionGoal::None; - let mut head = selection.head(); - let mut tail = selection.tail(); - - if head == DisplayPoint::zero() { - return; - } - - // collapse to block cursor - if tail < head { - tail = movement::left(map, head); - } else { - tail = head; - head = movement::right(map, head); - } - - selection.set_head(head, new_goal); - selection.set_tail(tail, new_goal); - // flip the selection - selection.swap_head_tail(); - head = selection.head(); - tail = selection.tail(); + let times = times.unwrap_or(1); + self.helix_new_selections(window, cx, |cursor, map| { + let mut head = movement::right(map, cursor); + let mut tail = cursor; + let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map)); + if head == map.max_point() { + return None; + } + for _ in 0..times { + let (maybe_next_tail, next_head) = + movement::find_boundary_trail(map, head, |left, right| { + is_boundary(left, right, &classifier) + }); - // create a classifier - let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map)); + if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail { + break; + } - for _ in 0..times { - let (maybe_next_tail, next_head) = - movement::find_preceding_boundary_trail(map, head, |left, right| { - is_boundary(left, right, &classifier) - }); + head = next_head; + if let Some(next_tail) = maybe_next_tail { + tail = next_tail; + } + } + Some((head, tail)) + }); + } - if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail { - break; - } + fn helix_find_range_backward( + &mut self, + times: Option, + window: &mut Window, + cx: &mut Context, + mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, + ) { + let times = times.unwrap_or(1); + self.helix_new_selections(window, cx, |cursor, map| { + let mut head = cursor; + // The original cursor was one character wide, + // but the search starts from the left side of it, + // so to include that space the selection must end one character to the right. + let mut tail = movement::right(map, cursor); + let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map)); + if head == DisplayPoint::zero() { + return None; + } + for _ in 0..times { + let (maybe_next_tail, next_head) = + movement::find_preceding_boundary_trail(map, head, |left, right| { + is_boundary(left, right, &classifier) + }); - head = next_head; - if let Some(next_tail) = maybe_next_tail { - tail = next_tail; - } - } + if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail { + break; + } - selection.set_tail(tail, new_goal); - selection.set_head(head, new_goal); - }); - }) + head = next_head; + if let Some(next_tail) = maybe_next_tail { + tail = next_tail; + } + } + Some((head, tail)) }); } @@ -255,58 +244,53 @@ impl Vim { found }) } - Motion::FindForward { .. } => { - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let goal = selection.goal; - let cursor = if selection.is_empty() || selection.reversed { - selection.head() - } else { - movement::left(map, selection.head()) - }; - - let (point, goal) = motion - .move_point( - map, - cursor, - selection.goal, - times, - &text_layout_details, - ) - .unwrap_or((cursor, goal)); - selection.set_tail(selection.head(), goal); - selection.set_head(movement::right(map, point), goal); - }) - }); + Motion::FindForward { + before, + char, + mode, + smartcase, + } => { + self.helix_new_selections(window, cx, |cursor, map| { + let start = cursor; + let mut last_boundary = start; + for _ in 0..times.unwrap_or(1) { + last_boundary = movement::find_boundary( + map, + movement::right(map, last_boundary), + mode, + |left, right| { + let current_char = if before { right } else { left }; + motion::is_character_match(char, current_char, smartcase) + }, + ); + } + Some((last_boundary, start)) }); } - Motion::FindBackward { .. } => { - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let goal = selection.goal; - let cursor = if selection.is_empty() || selection.reversed { - selection.head() - } else { - movement::left(map, selection.head()) - }; - - let (point, goal) = motion - .move_point( - map, - cursor, - selection.goal, - times, - &text_layout_details, - ) - .unwrap_or((cursor, goal)); - selection.set_tail(selection.head(), goal); - selection.set_head(point, goal); - }) - }); + Motion::FindBackward { + after, + char, + mode, + smartcase, + } => { + self.helix_new_selections(window, cx, |cursor, map| { + let start = cursor; + let mut last_boundary = start; + for _ in 0..times.unwrap_or(1) { + last_boundary = movement::find_preceding_boundary_display_point( + map, + last_boundary, + mode, + |left, right| { + let current_char = if after { left } else { right }; + motion::is_character_match(char, current_char, smartcase) + }, + ); + } + // The original cursor was one character wide, + // but the search started from the left side of it, + // so to include that space the selection must end one character to the right. + Some((last_boundary, movement::right(map, start))) }); } _ => self.helix_move_and_collapse(motion, times, window, cx), @@ -630,13 +614,33 @@ mod test { Mode::HelixNormal, ); - cx.simulate_keystrokes("2 T r"); + cx.simulate_keystrokes("F e F e"); cx.assert_state( indoc! {" - The quick br«ˇown - fox jumps over - the laz»y dog."}, + The quick brown + fox jumps ov«ˇer + the» lazy dog."}, + Mode::HelixNormal, + ); + + cx.simulate_keystrokes("e 2 F e"); + + cx.assert_state( + indoc! {" + Th«ˇe quick brown + fox jumps over» + the lazy dog."}, + Mode::HelixNormal, + ); + + cx.simulate_keystrokes("t r t r"); + + cx.assert_state( + indoc! {" + The quick «brown + fox jumps oveˇ»r + the lazy dog."}, Mode::HelixNormal, ); } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 7ef883f4061551e54a0a59d79ba0fcb804689aa8..a6a07e7b2f80ddde1bd9573d102c7f8480d21fff 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -2639,7 +2639,8 @@ fn find_backward( } } -fn is_character_match(target: char, other: char, smartcase: bool) -> bool { +/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true). +pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool { if smartcase { if target.is_uppercase() { target == other From 5a9546ff4badfb2c153663d51c41297f60ed25bc Mon Sep 17 00:00:00 2001 From: Mostafa Khaled <112074172+m04f@users.noreply.github.com> Date: Thu, 14 Aug 2025 20:04:38 +0300 Subject: [PATCH 007/744] Add alt-s to helix mode (#33918) Closes #31562 Release Notes: - Helix: bind alt-s to SplitSelectionIntoLines --------- Co-authored-by: Ben Kunkle --- assets/keymaps/vim.json | 1 + crates/editor/src/actions.rs | 12 ++++++++++-- crates/editor/src/editor.rs | 30 ++++++++++++++++++++++++++---- crates/editor/src/editor_tests.rs | 6 +++--- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index a3f68a77303c7a61e0d88a10a861be58e88a71b2..560ca3bdd807301a45f60c02f9b3aea905f47ad7 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -407,6 +407,7 @@ "g w": "vim::PushRewrap", "insert": "vim::InsertBefore", "alt-.": "vim::RepeatFind", + "alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }], // tree-sitter related commands "[ x": "editor::SelectLargerSyntaxNode", "] x": "editor::SelectSmallerSyntaxNode", diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 39433b3c279e101f47ad4b2eed4d180f82a38997..ce02c4d2bf39c6bc5513280a1d81b071a9e6cd6a 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -273,6 +273,16 @@ pub enum UuidVersion { V7, } +/// Splits selection into individual lines. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] +#[serde(deny_unknown_fields)] +pub struct SplitSelectionIntoLines { + /// Keep the text selected after splitting instead of collapsing to cursors. + #[serde(default)] + pub keep_selections: bool, +} + /// Goes to the next diagnostic in the file. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = editor)] @@ -672,8 +682,6 @@ actions!( SortLinesCaseInsensitive, /// Sorts selected lines case-sensitively. SortLinesCaseSensitive, - /// Splits selection into individual lines. - SplitSelectionIntoLines, /// Stops the language server for the current file. StopLanguageServer, /// Switches between source and header files. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 689f3973412c2f2c884552364ca1646dfcb42457..1f350cf0d0a8b9f18b42f6f3bbbbb6269f376263 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -13612,7 +13612,7 @@ impl Editor { pub fn split_selection_into_lines( &mut self, - _: &SplitSelectionIntoLines, + action: &SplitSelectionIntoLines, window: &mut Window, cx: &mut Context, ) { @@ -13629,8 +13629,21 @@ impl Editor { let buffer = self.buffer.read(cx).read(cx); for selection in selections { for row in selection.start.row..selection.end.row { - let cursor = Point::new(row, buffer.line_len(MultiBufferRow(row))); - new_selection_ranges.push(cursor..cursor); + let line_start = Point::new(row, 0); + let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row))); + + if action.keep_selections { + // Keep the selection range for each line + let selection_start = if row == selection.start.row { + selection.start + } else { + line_start + }; + new_selection_ranges.push(selection_start..line_end); + } else { + // Collapse to cursor at end of line + new_selection_ranges.push(line_end..line_end); + } } let is_multiline_selection = selection.start.row != selection.end.row; @@ -13638,7 +13651,16 @@ impl Editor { // so this action feels more ergonomic when paired with other selection operations let should_skip_last = is_multiline_selection && selection.end.column == 0; if !should_skip_last { - new_selection_ranges.push(selection.end..selection.end); + if action.keep_selections { + if is_multiline_selection { + let line_start = Point::new(selection.end.row, 0); + new_selection_ranges.push(line_start..selection.end); + } else { + new_selection_ranges.push(selection.start..selection.end); + } + } else { + new_selection_ranges.push(selection.end..selection.end); + } } } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 44218697032f2f03d6354092a31f3c0d921992e4..a5966b3301b4f05aef3465a1c12e957b4c27157f 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -6401,7 +6401,7 @@ async fn test_split_selection_into_lines(cx: &mut TestAppContext) { fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) { cx.set_state(initial_state); cx.update_editor(|e, window, cx| { - e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx) + e.split_selection_into_lines(&Default::default(), window, cx) }); cx.assert_editor_state(expected_state); } @@ -6489,7 +6489,7 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4), ]) }); - editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx); + editor.split_selection_into_lines(&Default::default(), window, cx); assert_eq!( editor.display_text(cx), "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i" @@ -6505,7 +6505,7 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1) ]) }); - editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx); + editor.split_selection_into_lines(&Default::default(), window, cx); assert_eq!( editor.display_text(cx), "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" From 1a169e0b16801b278140bb9a59fa45ab56644f4d Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 14 Aug 2025 13:54:19 -0400 Subject: [PATCH 008/744] git: Clear set of dirty paths when doing a full status scan (#36181) Related to #35780 Release Notes: - N/A --------- Co-authored-by: Kirill Bulatov --- crates/project/src/git_store.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 32deb0dbc4f50b9c436e7b051f4c6332be348b1b..3163a10239f6ccdba7452697b9d9cac18a721ec3 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -2349,7 +2349,7 @@ impl GitStore { return None; }; - let mut paths = vec![]; + let mut paths = Vec::new(); // All paths prefixed by a given repo will constitute a continuous range. while let Some(path) = entries.get(ix) && let Some(repo_path) = @@ -2358,7 +2358,11 @@ impl GitStore { paths.push((repo_path, ix)); ix += 1; } - Some((repo, paths)) + if paths.is_empty() { + None + } else { + Some((repo, paths)) + } }); tasks.push_back(task); } @@ -4338,7 +4342,8 @@ impl Repository { bail!("not a local repository") }; let (snapshot, events) = this - .read_with(&mut cx, |this, _| { + .update(&mut cx, |this, _| { + this.paths_needing_status_update.clear(); compute_snapshot( this.id, this.work_directory_abs_path.clone(), @@ -4568,6 +4573,9 @@ impl Repository { }; let paths = changed_paths.iter().cloned().collect::>(); + if paths.is_empty() { + return Ok(()); + } let statuses = backend.status(&paths).await?; let changed_path_statuses = cx From 2acfa5e948764cbe9ae5cbf9f95d6bf66ea904c2 Mon Sep 17 00:00:00 2001 From: smit Date: Thu, 14 Aug 2025 23:28:15 +0530 Subject: [PATCH 009/744] copilot: Fix Copilot fails to sign in on newer versions (#36195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up for #36093 and https://github.com/zed-industries/zed/pull/36138 Since v1.355.0, `@github/copilot-language-server` has stopped responding to `CheckStatus` requests if a `DidChangeConfiguration` notification hasn’t been sent beforehand. This causes `CheckStatus` to remain in an await state until it times out, leaving the connection stuck for a long period before finally throwing a timeout error. ```rs let status = server .request::(request::CheckStatusParams { local_checks_only: false, }) .await .into_response() // bails here with ConnectionResult::Timeout .context("copilot: check status")?; ```` This PR fixes the issue by sending the `DidChangeConfiguration` notification before making the `CheckStatus` request. It’s just an ordering change i.e. no other LSP actions occur between these two calls. Previously, we only updated our internal connection status and UI in between. Release Notes: - Fixed an issue where GitHub Copilot could get stuck and fail to sign in. --- crates/copilot/src/copilot.rs | 95 +++++++++++++------------ crates/languages/src/css.rs | 5 +- crates/languages/src/json.rs | 5 +- crates/languages/src/python.rs | 5 +- crates/languages/src/tailwind.rs | 5 +- crates/languages/src/typescript.rs | 5 +- crates/languages/src/vtsls.rs | 8 +-- crates/languages/src/yaml.rs | 5 +- crates/node_runtime/src/node_runtime.rs | 34 +++++---- 9 files changed, 83 insertions(+), 84 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 166a582c70aa54fb48e291133c65d651cf6fa66f..dcebeae7212119867bc582ce930d2f51fae49d34 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -21,7 +21,7 @@ use language::{ point_from_lsp, point_to_lsp, }; use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName}; -use node_runtime::{NodeRuntime, VersionCheck}; +use node_runtime::{NodeRuntime, VersionStrategy}; use parking_lot::Mutex; use project::DisableAiSettings; use request::StatusNotification; @@ -349,7 +349,11 @@ impl Copilot { this.start_copilot(true, false, cx); cx.observe_global::(move |this, cx| { this.start_copilot(true, false, cx); - this.send_configuration_update(cx); + if let Ok(server) = this.server.as_running() { + notify_did_change_config_to_server(&server.lsp, cx) + .context("copilot setting change: did change configuration") + .log_err(); + } }) .detach(); this @@ -438,43 +442,6 @@ impl Copilot { if env.is_empty() { None } else { Some(env) } } - fn send_configuration_update(&mut self, cx: &mut Context) { - let copilot_settings = all_language_settings(None, cx) - .edit_predictions - .copilot - .clone(); - - let settings = json!({ - "http": { - "proxy": copilot_settings.proxy, - "proxyStrictSSL": !copilot_settings.proxy_no_verify.unwrap_or(false) - }, - "github-enterprise": { - "uri": copilot_settings.enterprise_uri - } - }); - - if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) { - copilot_chat.update(cx, |chat, cx| { - chat.set_configuration( - copilot_chat::CopilotChatConfiguration { - enterprise_uri: copilot_settings.enterprise_uri.clone(), - }, - cx, - ); - }); - } - - if let Ok(server) = self.server.as_running() { - server - .lsp - .notify::( - &lsp::DidChangeConfigurationParams { settings }, - ) - .log_err(); - } - } - #[cfg(any(test, feature = "test-support"))] pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity, lsp::FakeLanguageServer) { use fs::FakeFs; @@ -573,6 +540,9 @@ impl Copilot { })? .await?; + this.update(cx, |_, cx| notify_did_change_config_to_server(&server, cx))? + .context("copilot: did change configuration")?; + let status = server .request::(request::CheckStatusParams { local_checks_only: false, @@ -598,8 +568,6 @@ impl Copilot { }); cx.emit(Event::CopilotLanguageServerStarted); this.update_sign_in_status(status, cx); - // Send configuration now that the LSP is fully started - this.send_configuration_update(cx); } Err(error) => { this.server = CopilotServer::Error(error.to_string().into()); @@ -1156,6 +1124,41 @@ fn uri_for_buffer(buffer: &Entity, cx: &App) -> Result { } } +fn notify_did_change_config_to_server( + server: &Arc, + cx: &mut Context, +) -> std::result::Result<(), anyhow::Error> { + let copilot_settings = all_language_settings(None, cx) + .edit_predictions + .copilot + .clone(); + + if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) { + copilot_chat.update(cx, |chat, cx| { + chat.set_configuration( + copilot_chat::CopilotChatConfiguration { + enterprise_uri: copilot_settings.enterprise_uri.clone(), + }, + cx, + ); + }); + } + + let settings = json!({ + "http": { + "proxy": copilot_settings.proxy, + "proxyStrictSSL": !copilot_settings.proxy_no_verify.unwrap_or(false) + }, + "github-enterprise": { + "uri": copilot_settings.enterprise_uri + } + }); + + server.notify::(&lsp::DidChangeConfigurationParams { + settings, + }) +} + async fn clear_copilot_dir() { remove_matching(paths::copilot_dir(), |_| true).await } @@ -1169,8 +1172,9 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: const SERVER_PATH: &str = "node_modules/@github/copilot-language-server/dist/language-server.js"; - // pinning it: https://github.com/zed-industries/zed/issues/36093 - const PINNED_VERSION: &str = "1.354"; + let latest_version = node_runtime + .npm_package_latest_version(PACKAGE_NAME) + .await?; let server_path = paths::copilot_dir().join(SERVER_PATH); fs.create_dir(paths::copilot_dir()).await?; @@ -1180,13 +1184,12 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: PACKAGE_NAME, &server_path, paths::copilot_dir(), - &PINNED_VERSION, - VersionCheck::VersionMismatch, + VersionStrategy::Latest(&latest_version), ) .await; if should_install { node_runtime - .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &PINNED_VERSION)]) + .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)]) .await?; } diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index 19329fcc6edeea8bce1a6e09ec774793f1098811..ffd9006c769a4ad14cc70beb988c7ea96a578872 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -4,7 +4,7 @@ use futures::StreamExt; use gpui::AsyncApp; use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{LanguageServerBinary, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::json; use smol::fs; @@ -107,8 +107,7 @@ impl LspAdapter for CssLspAdapter { Self::PACKAGE_NAME, &server_path, &container_dir, - &version, - Default::default(), + VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 019b45d396891b434c6b5e8457353ee0ee3e0d69..484631d01f0809334eecaacd2851be540771fe36 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -12,7 +12,7 @@ use language::{ LspAdapter, LspAdapterDelegate, }; use lsp::{LanguageServerBinary, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::{Value, json}; use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; @@ -344,8 +344,7 @@ impl LspAdapter for JsonLspAdapter { Self::PACKAGE_NAME, &server_path, &container_dir, - &version, - Default::default(), + VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 551332448770ffabc4b834662980bd5bb00248c5..40131089d1ccb8bc211df23f2c7def3810006181 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -13,7 +13,7 @@ use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery}; use language::{Toolchain, WorkspaceFoldersContent}; use lsp::LanguageServerBinary; use lsp::LanguageServerName; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionStrategy}; use pet_core::Configuration; use pet_core::os_environment::Environment; use pet_core::python_environment::PythonEnvironmentKind; @@ -205,8 +205,7 @@ impl LspAdapter for PythonLspAdapter { Self::SERVER_NAME.as_ref(), &server_path, &container_dir, - &version, - Default::default(), + VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 6f03eeda8d2414578dcda939eb89fb1b5f812768..0d647f07cf0c97969928d5f292a5101127368016 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -5,7 +5,7 @@ use futures::StreamExt; use gpui::AsyncApp; use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{LanguageServerBinary, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::{Value, json}; use smol::fs; @@ -112,8 +112,7 @@ impl LspAdapter for TailwindLspAdapter { Self::PACKAGE_NAME, &server_path, &container_dir, - &version, - Default::default(), + VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index a8ba880889b36071bdb0c474c4790f4c37ad165c..1877c86dc5278c7d8b5b2721125d50ae84ebbd01 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -10,7 +10,7 @@ use language::{ LspAdapterDelegate, }; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::{Value, json}; use smol::{fs, lock::RwLock, stream::StreamExt}; @@ -588,8 +588,7 @@ impl LspAdapter for TypeScriptLspAdapter { Self::PACKAGE_NAME, &server_path, &container_dir, - version.typescript_version.as_str(), - Default::default(), + VersionStrategy::Latest(version.typescript_version.as_str()), ) .await; diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index 73498fc5795b936bd45b973a8dbc87d3a3f1f5fb..90faf883ba8b20016ec5b614d03de39ccb3a94e8 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -4,7 +4,7 @@ use collections::HashMap; use gpui::AsyncApp; use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::Value; use std::{ @@ -115,8 +115,7 @@ impl LspAdapter for VtslsLspAdapter { Self::PACKAGE_NAME, &server_path, &container_dir, - &latest_version.server_version, - Default::default(), + VersionStrategy::Latest(&latest_version.server_version), ) .await { @@ -129,8 +128,7 @@ impl LspAdapter for VtslsLspAdapter { Self::TYPESCRIPT_PACKAGE_NAME, &container_dir.join(Self::TYPESCRIPT_TSDK_PATH), &container_dir, - &latest_version.typescript_version, - Default::default(), + VersionStrategy::Latest(&latest_version.typescript_version), ) .await { diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 28be2cc1a45130a723084529e0c6164ab2a042c2..15a4d590bc2fcd13f611b8afdbf189b1b76b1eb9 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -6,7 +6,7 @@ use language::{ LanguageToolchainStore, LspAdapter, LspAdapterDelegate, language_settings::AllLanguageSettings, }; use lsp::{LanguageServerBinary, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::Value; use settings::{Settings, SettingsLocation}; @@ -108,8 +108,7 @@ impl LspAdapter for YamlLspAdapter { Self::PACKAGE_NAME, &server_path, &container_dir, - &version, - Default::default(), + VersionStrategy::Latest(version), ) .await; diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 6fcc3a728af903f581046c9a3e069f9fbcfc9ecf..f92c122e71a00f08bcba1a4e16c510b00898cb56 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -29,13 +29,11 @@ pub struct NodeBinaryOptions { pub use_paths: Option<(PathBuf, PathBuf)>, } -#[derive(Default)] -pub enum VersionCheck { - /// Check whether the installed and requested version have a mismatch - VersionMismatch, - /// Only check whether the currently installed version is older than the newest one - #[default] - OlderVersion, +pub enum VersionStrategy<'a> { + /// Install if current version doesn't match pinned version + Pin(&'a str), + /// Install if current version is older than latest version + Latest(&'a str), } #[derive(Clone)] @@ -295,8 +293,7 @@ impl NodeRuntime { package_name: &str, local_executable_path: &Path, local_package_directory: &Path, - latest_version: &str, - version_check: VersionCheck, + version_strategy: VersionStrategy<'_>, ) -> bool { // In the case of the local system not having the package installed, // or in the instances where we fail to parse package.json data, @@ -317,13 +314,20 @@ impl NodeRuntime { let Some(installed_version) = Version::parse(&installed_version).log_err() else { return true; }; - let Some(latest_version) = Version::parse(latest_version).log_err() else { - return true; - }; - match version_check { - VersionCheck::VersionMismatch => installed_version != latest_version, - VersionCheck::OlderVersion => installed_version < latest_version, + match version_strategy { + VersionStrategy::Pin(pinned_version) => { + let Some(pinned_version) = Version::parse(pinned_version).log_err() else { + return true; + }; + installed_version != pinned_version + } + VersionStrategy::Latest(latest_version) => { + let Some(latest_version) = Version::parse(latest_version).log_err() else { + return true; + }; + installed_version < latest_version + } } } } From 43ee604179ccda222eed29a173ac19e0514e8679 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 14 Aug 2025 15:30:18 -0300 Subject: [PATCH 010/744] acp: Clean up entry views on rewind (#36197) We were leaking diffs and terminals on rewind, we'll now clean them up. This PR also introduces a refactor of how we mantain the entry view state to use a `Vec` that's kept in sync with the thread entries. Release Notes: - N/A --- crates/acp_thread/Cargo.toml | 3 +- crates/acp_thread/src/acp_thread.rs | 36 +- crates/acp_thread/src/connection.rs | 156 +++++- crates/agent2/src/agent.rs | 8 +- crates/agent2/src/tests/mod.rs | 2 +- crates/agent_servers/src/acp/v0.rs | 2 +- crates/agent_servers/src/acp/v1.rs | 2 +- crates/agent_servers/src/claude.rs | 2 +- crates/agent_servers/src/e2e_tests.rs | 4 +- crates/agent_ui/Cargo.toml | 1 + crates/agent_ui/src/acp.rs | 1 + crates/agent_ui/src/acp/entry_view_state.rs | 351 +++++++++++++ crates/agent_ui/src/acp/thread_view.rs | 536 +++++++++----------- 13 files changed, 758 insertions(+), 346 deletions(-) create mode 100644 crates/agent_ui/src/acp/entry_view_state.rs diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 2d0fe2d2645a3f61152211ec439105d4aa73f7ec..2b9a6513c8e91a165bbc51aae3e5b2e831cfb234 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -13,7 +13,7 @@ path = "src/acp_thread.rs" doctest = false [features] -test-support = ["gpui/test-support", "project/test-support"] +test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"] [dependencies] action_log.workspace = true @@ -29,6 +29,7 @@ gpui.workspace = true itertools.workspace = true language.workspace = true markdown.workspace = true +parking_lot = { workspace = true, optional = true } project.workspace = true prompt_store.workspace = true serde.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index da4d82712a27f4783d764e409db0d8cdeaa26805..4bdc42ea2ed1529f4baf00823438428c96ee7bb1 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1575,11 +1575,7 @@ mod tests { let project = Project::test(fs, [], cx).await; 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 - }) + .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) .await .unwrap(); @@ -1699,11 +1695,7 @@ mod tests { )); let thread = cx - .spawn(async move |mut cx| { - connection - .new_thread(project, Path::new(path!("/test")), &mut cx) - .await - }) + .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) .await .unwrap(); @@ -1786,7 +1778,7 @@ mod tests { .unwrap(); let thread = cx - .spawn(|mut cx| connection.new_thread(project, Path::new(path!("/tmp")), &mut cx)) + .update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx)) .await .unwrap(); @@ -1849,11 +1841,7 @@ mod tests { })); let thread = cx - .spawn(async move |mut cx| { - connection - .new_thread(project, Path::new(path!("/test")), &mut cx) - .await - }) + .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) .await .unwrap(); @@ -1961,10 +1949,11 @@ mod tests { } })); - let thread = connection - .new_thread(project, Path::new(path!("/test")), &mut cx.to_async()) + let thread = cx + .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) .await .unwrap(); + cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["Hi".into()], cx))) .await .unwrap(); @@ -2021,8 +2010,8 @@ mod tests { .boxed_local() } })); - let thread = connection - .new_thread(project, Path::new(path!("/test")), &mut cx.to_async()) + let thread = cx + .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) .await .unwrap(); @@ -2227,7 +2216,7 @@ mod tests { self: Rc, project: Entity, _cwd: &Path, - cx: &mut gpui::AsyncApp, + cx: &mut gpui::App, ) -> Task>> { let session_id = acp::SessionId( rand::thread_rng() @@ -2237,9 +2226,8 @@ mod tests { .collect::() .into(), ); - let thread = cx - .new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)) - .unwrap(); + let thread = + cx.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)); self.sessions.lock().insert(session_id, thread.downgrade()); Task::ready(Ok(thread)) } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index c3167eb2d4fbdf66e7c45f574a227d215d18dca0..0f531acbde7edc30d88c7e20608aeb3b1949baf4 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -2,7 +2,7 @@ use crate::AcpThread; use agent_client_protocol::{self as acp}; use anyhow::Result; use collections::IndexMap; -use gpui::{AsyncApp, Entity, SharedString, Task}; +use gpui::{Entity, SharedString, Task}; use project::Project; use std::{error::Error, fmt, path::Path, rc::Rc, sync::Arc}; use ui::{App, IconName}; @@ -22,7 +22,7 @@ pub trait AgentConnection { self: Rc, project: Entity, cwd: &Path, - cx: &mut AsyncApp, + cx: &mut App, ) -> Task>>; fn auth_methods(&self) -> &[acp::AuthMethod]; @@ -160,3 +160,155 @@ impl AgentModelList { } } } + +#[cfg(feature = "test-support")] +mod test_support { + use std::sync::Arc; + + use collections::HashMap; + use futures::future::try_join_all; + use gpui::{AppContext as _, WeakEntity}; + use parking_lot::Mutex; + + use super::*; + + #[derive(Clone, Default)] + pub struct StubAgentConnection { + sessions: Arc>>>, + permission_requests: HashMap>, + next_prompt_updates: Arc>>, + } + + impl StubAgentConnection { + pub fn new() -> Self { + Self { + next_prompt_updates: Default::default(), + permission_requests: HashMap::default(), + sessions: Arc::default(), + } + } + + pub fn set_next_prompt_updates(&self, updates: Vec) { + *self.next_prompt_updates.lock() = updates; + } + + pub fn with_permission_requests( + mut self, + permission_requests: HashMap>, + ) -> Self { + self.permission_requests = permission_requests; + self + } + + pub fn send_update( + &self, + session_id: acp::SessionId, + update: acp::SessionUpdate, + cx: &mut App, + ) { + self.sessions + .lock() + .get(&session_id) + .unwrap() + .update(cx, |thread, cx| { + thread.handle_session_update(update.clone(), cx).unwrap(); + }) + .unwrap(); + } + } + + impl AgentConnection for StubAgentConnection { + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] + } + + fn new_thread( + self: Rc, + project: Entity, + _cwd: &Path, + cx: &mut gpui::App, + ) -> Task>> { + let session_id = acp::SessionId(self.sessions.lock().len().to_string().into()); + let thread = + cx.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)); + self.sessions.lock().insert(session_id, thread.downgrade()); + Task::ready(Ok(thread)) + } + + fn authenticate( + &self, + _method_id: acp::AuthMethodId, + _cx: &mut App, + ) -> Task> { + unimplemented!() + } + + fn prompt( + &self, + _id: Option, + 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.next_prompt_updates.lock().drain(..) { + 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_authorization( + 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(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + }) + } + + fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { + unimplemented!() + } + + fn session_editor( + &self, + _session_id: &agent_client_protocol::SessionId, + _cx: &mut App, + ) -> Option> { + Some(Rc::new(StubAgentSessionEditor)) + } + } + + struct StubAgentSessionEditor; + + impl AgentSessionEditor for StubAgentSessionEditor { + fn truncate(&self, _: UserMessageId, _: &mut App) -> Task> { + Task::ready(Ok(())) + } + } +} + +#[cfg(feature = "test-support")] +pub use test_support::*; diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 6ebcece2b566886dc6503d57af1663a51f7e4c01..9ac3c2d0e517b55e038b9853594d834e1b4f7ec4 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -522,7 +522,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { self: Rc, project: Entity, cwd: &Path, - cx: &mut AsyncApp, + cx: &mut App, ) -> Task>> { let agent = self.0.clone(); log::info!("Creating new thread for project at: {:?}", cwd); @@ -940,11 +940,7 @@ mod tests { // Create a thread/session let acp_thread = cx .update(|cx| { - Rc::new(connection.clone()).new_thread( - project.clone(), - Path::new("/a"), - &mut cx.to_async(), - ) + Rc::new(connection.clone()).new_thread(project.clone(), Path::new("/a"), cx) }) .await .unwrap(); diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 637af73d1a3ab9cfe31225a4273d2d675b15e403..1df664c0296aa3314bd4a430d0ac69660ba4887f 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -841,7 +841,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) { // Create a thread using new_thread let connection_rc = Rc::new(connection.clone()); let acp_thread = cx - .update(|cx| connection_rc.new_thread(project, cwd, &mut cx.to_async())) + .update(|cx| connection_rc.new_thread(project, cwd, cx)) .await .expect("new_thread should succeed"); diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 327613de673baeea4964c90cdd88c9075ed56f11..15f8635cdef7a2d8431eee3169ae11eb3d817fc1 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -423,7 +423,7 @@ impl AgentConnection for AcpConnection { self: Rc, project: Entity, _cwd: &Path, - cx: &mut AsyncApp, + cx: &mut App, ) -> Task>> { let task = self.connection.request_any( acp_old::InitializeParams { diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index de397fddf0f303d64ff772e4a5fc9de27ae5f577..d93e3d023e336cd27cd8a98286413ed099b16e60 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -111,7 +111,7 @@ impl AgentConnection for AcpConnection { self: Rc, project: Entity, cwd: &Path, - cx: &mut AsyncApp, + cx: &mut App, ) -> Task>> { let conn = self.connection.clone(); let sessions = self.sessions.clone(); diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index c394ec4a9c23b64400e12a94adffcc65499a0ebd..dbcda00e488c3737abcefde4f037749662f1f489 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -74,7 +74,7 @@ impl AgentConnection for ClaudeAgentConnection { self: Rc, project: Entity, cwd: &Path, - cx: &mut AsyncApp, + cx: &mut App, ) -> Task>> { let cwd = cwd.to_owned(); cx.spawn(async move |cx| { diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index ec6ca29b9dd1a902708a8786ddc6853955da5532..5af7010f26faf31016b15b0625c8b96e384ea7a4 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -422,8 +422,8 @@ pub async fn new_test_thread( .await .unwrap(); - let thread = connection - .new_thread(project.clone(), current_dir.as_ref(), &mut cx.to_async()) + let thread = cx + .update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx)) .await .unwrap(); diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index b6a5710aa42ce64722985d934967703faf92bbdc..13fd9d13c5014d5683a41739fdff335049880de8 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -103,6 +103,7 @@ workspace.workspace = true zed_actions.workspace = true [dev-dependencies] +acp_thread = { workspace = true, features = ["test-support"] } agent = { workspace = true, features = ["test-support"] } assistant_context = { workspace = true, features = ["test-support"] } assistant_tools.workspace = true diff --git a/crates/agent_ui/src/acp.rs b/crates/agent_ui/src/acp.rs index 630aa730a68a4167e468594897151cf41ada30a1..831d296eebcf7edd29f3f84acbf6a7824be47a1b 100644 --- a/crates/agent_ui/src/acp.rs +++ b/crates/agent_ui/src/acp.rs @@ -1,4 +1,5 @@ mod completion_provider; +mod entry_view_state; mod message_editor; mod model_selector; mod model_selector_popover; diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs new file mode 100644 index 0000000000000000000000000000000000000000..2f5f855e90ded81066d8265cda9cf0449121107c --- /dev/null +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -0,0 +1,351 @@ +use std::{collections::HashMap, ops::Range}; + +use acp_thread::AcpThread; +use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer}; +use gpui::{ + AnyEntity, App, AppContext as _, Entity, EntityId, TextStyleRefinement, WeakEntity, Window, +}; +use language::language_settings::SoftWrap; +use settings::Settings as _; +use terminal_view::TerminalView; +use theme::ThemeSettings; +use ui::TextSize; +use workspace::Workspace; + +#[derive(Default)] +pub struct EntryViewState { + entries: Vec, +} + +impl EntryViewState { + pub fn entry(&self, index: usize) -> Option<&Entry> { + self.entries.get(index) + } + + pub fn sync_entry( + &mut self, + workspace: WeakEntity, + thread: Entity, + index: usize, + window: &mut Window, + cx: &mut App, + ) { + debug_assert!(index <= self.entries.len()); + let entry = if let Some(entry) = self.entries.get_mut(index) { + entry + } else { + self.entries.push(Entry::default()); + self.entries.last_mut().unwrap() + }; + + entry.sync_diff_multibuffers(&thread, index, window, cx); + entry.sync_terminals(&workspace, &thread, index, window, cx); + } + + pub fn remove(&mut self, range: Range) { + self.entries.drain(range); + } + + pub fn settings_changed(&mut self, cx: &mut App) { + for entry in self.entries.iter() { + for view in entry.views.values() { + if let Ok(diff_editor) = view.clone().downcast::() { + diff_editor.update(cx, |diff_editor, cx| { + diff_editor + .set_text_style_refinement(diff_editor_text_style_refinement(cx)); + cx.notify(); + }) + } + } + } + } +} + +pub struct Entry { + views: HashMap, +} + +impl Entry { + pub fn editor_for_diff(&self, diff: &Entity) -> Option> { + self.views + .get(&diff.entity_id()) + .cloned() + .map(|entity| entity.downcast::().unwrap()) + } + + pub fn terminal( + &self, + terminal: &Entity, + ) -> Option> { + self.views + .get(&terminal.entity_id()) + .cloned() + .map(|entity| entity.downcast::().unwrap()) + } + + fn sync_diff_multibuffers( + &mut self, + thread: &Entity, + index: usize, + window: &mut Window, + cx: &mut App, + ) { + let Some(entry) = thread.read(cx).entries().get(index) else { + return; + }; + + let multibuffers = entry + .diffs() + .map(|diff| diff.read(cx).multibuffer().clone()); + + let multibuffers = multibuffers.collect::>(); + + for multibuffer in multibuffers { + if self.views.contains_key(&multibuffer.entity_id()) { + return; + } + + let editor = cx.new(|cx| { + let mut editor = Editor::new( + EditorMode::Full { + scale_ui_elements_with_buffer_font_size: false, + show_active_line_background: false, + sized_by_content: true, + }, + multibuffer.clone(), + None, + window, + cx, + ); + editor.set_show_gutter(false, cx); + editor.disable_inline_diagnostics(); + editor.disable_expand_excerpt_buttons(cx); + editor.set_show_vertical_scrollbar(false, cx); + editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); + editor.set_soft_wrap_mode(SoftWrap::None, cx); + editor.scroll_manager.set_forbid_vertical_scroll(true); + editor.set_show_indent_guides(false, cx); + editor.set_read_only(true); + editor.set_show_breakpoints(false, cx); + editor.set_show_code_actions(false, cx); + editor.set_show_git_diff_gutter(false, cx); + editor.set_expand_all_diff_hunks(cx); + editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); + editor + }); + + let entity_id = multibuffer.entity_id(); + self.views.insert(entity_id, editor.into_any()); + } + } + + fn sync_terminals( + &mut self, + workspace: &WeakEntity, + thread: &Entity, + index: usize, + window: &mut Window, + cx: &mut App, + ) { + let Some(entry) = thread.read(cx).entries().get(index) else { + return; + }; + + let terminals = entry + .terminals() + .map(|terminal| terminal.clone()) + .collect::>(); + + for terminal in terminals { + if self.views.contains_key(&terminal.entity_id()) { + return; + } + + let Some(strong_workspace) = workspace.upgrade() else { + return; + }; + + let terminal_view = cx.new(|cx| { + let mut view = TerminalView::new( + terminal.read(cx).inner().clone(), + workspace.clone(), + None, + strong_workspace.read(cx).project().downgrade(), + window, + cx, + ); + view.set_embedded_mode(Some(1000), cx); + view + }); + + let entity_id = terminal.entity_id(); + self.views.insert(entity_id, terminal_view.into_any()); + } + } + + #[cfg(test)] + pub fn len(&self) -> usize { + self.views.len() + } +} + +fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement { + TextStyleRefinement { + font_size: Some( + TextSize::Small + .rems(cx) + .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx)) + .into(), + ), + ..Default::default() + } +} + +impl Default for Entry { + fn default() -> Self { + Self { + // Avoid allocating in the heap by default + views: HashMap::with_capacity(0), + } + } +} + +#[cfg(test)] +mod tests { + use std::{path::Path, rc::Rc}; + + use acp_thread::{AgentConnection, StubAgentConnection}; + use agent_client_protocol as acp; + use agent_settings::AgentSettings; + use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; + use editor::{EditorSettings, RowInfo}; + use fs::FakeFs; + use gpui::{SemanticVersion, TestAppContext}; + use multi_buffer::MultiBufferRow; + use pretty_assertions::assert_matches; + use project::Project; + use serde_json::json; + use settings::{Settings as _, SettingsStore}; + use theme::ThemeSettings; + use util::path; + use workspace::Workspace; + + use crate::acp::entry_view_state::EntryViewState; + + #[gpui::test] + async fn test_diff_sync(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/project", + json!({ + "hello.txt": "hi world" + }), + ) + .await; + let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; + + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let tool_call = acp::ToolCall { + id: acp::ToolCallId("tool".into()), + title: "Tool call".into(), + kind: acp::ToolKind::Other, + status: acp::ToolCallStatus::InProgress, + content: vec![acp::ToolCallContent::Diff { + diff: acp::Diff { + path: "/project/hello.txt".into(), + old_text: Some("hi world".into()), + new_text: "hello world".into(), + }, + }], + locations: vec![], + raw_input: None, + raw_output: None, + }; + let connection = Rc::new(StubAgentConnection::new()); + let thread = cx + .update(|_, cx| { + connection + .clone() + .new_thread(project, Path::new(path!("/project")), cx) + }) + .await + .unwrap(); + let session_id = thread.update(cx, |thread, _| thread.session_id().clone()); + + cx.update(|_, cx| { + connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx) + }); + + let mut view_state = EntryViewState::default(); + cx.update(|window, cx| { + view_state.sync_entry(workspace.downgrade(), thread.clone(), 0, window, cx); + }); + + let multibuffer = thread.read_with(cx, |thread, cx| { + thread + .entries() + .get(0) + .unwrap() + .diffs() + .next() + .unwrap() + .read(cx) + .multibuffer() + .clone() + }); + + cx.run_until_parked(); + + let entry = view_state.entry(0).unwrap(); + let diff_editor = entry.editor_for_diff(&multibuffer).unwrap(); + assert_eq!( + diff_editor.read_with(cx, |editor, cx| editor.text(cx)), + "hi world\nhello world" + ); + let row_infos = diff_editor.read_with(cx, |editor, cx| { + let multibuffer = editor.buffer().read(cx); + multibuffer + .snapshot(cx) + .row_infos(MultiBufferRow(0)) + .collect::>() + }); + assert_matches!( + row_infos.as_slice(), + [ + RowInfo { + multibuffer_row: Some(MultiBufferRow(0)), + diff_status: Some(DiffHunkStatus { + kind: DiffHunkStatusKind::Deleted, + .. + }), + .. + }, + RowInfo { + multibuffer_row: Some(MultiBufferRow(1)), + diff_status: Some(DiffHunkStatus { + kind: DiffHunkStatusKind::Added, + .. + }), + .. + } + ] + ); + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + AgentSettings::register(cx); + workspace::init_settings(cx); + ThemeSettings::register(cx); + release_channel::init(SemanticVersion::default(), cx); + EditorSettings::register(cx); + }); + } +} diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2a72cc6f486e3dfc0d175fa4b6640f27e9da4ecf..0e90b93f4d6836e167d0cc4dcceddbc9844a5b77 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -12,24 +12,22 @@ use audio::{Audio, Sound}; use buffer_diff::BufferDiff; use collections::{HashMap, HashSet}; use editor::scroll::Autoscroll; -use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects}; +use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; use gpui::{ Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, EdgesRefinement, Empty, Entity, - EntityId, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, - PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, - TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, - linear_color_stop, linear_gradient, list, percentage, point, prelude::*, pulsating_between, + FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay, + SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, + Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, + linear_gradient, list, percentage, point, prelude::*, pulsating_between, }; use language::Buffer; -use language::language_settings::SoftWrap; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use project::Project; use prompt_store::PromptId; use rope::Point; use settings::{Settings as _, SettingsStore}; use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration}; -use terminal_view::TerminalView; use text::Anchor; use theme::ThemeSettings; use ui::{ @@ -41,6 +39,7 @@ use workspace::{CollaboratorId, Workspace}; use zed_actions::agent::{Chat, ToggleModelSelector}; use zed_actions::assistant::OpenRulesLibrary; +use super::entry_view_state::EntryViewState; use crate::acp::AcpModelSelectorPopover; use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; @@ -61,8 +60,7 @@ pub struct AcpThreadView { thread_store: Entity, text_thread_store: Entity, thread_state: ThreadState, - diff_editors: HashMap>, - terminal_views: HashMap>, + entry_view_state: EntryViewState, message_editor: Entity, model_selector: Option>, notifications: Vec>, @@ -149,8 +147,7 @@ impl AcpThreadView { model_selector: None, notifications: Vec::new(), notification_subscriptions: HashMap::default(), - diff_editors: Default::default(), - terminal_views: Default::default(), + entry_view_state: EntryViewState::default(), list_state: list_state.clone(), scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), last_error: None, @@ -209,11 +206,18 @@ impl AcpThreadView { // }) // .ok(); - let result = match connection - .clone() - .new_thread(project.clone(), &root_dir, cx) - .await - { + let Some(result) = cx + .update(|_, cx| { + connection + .clone() + .new_thread(project.clone(), &root_dir, cx) + }) + .log_err() + else { + return; + }; + + let result = match result.await { Err(e) => { let mut cx = cx.clone(); if e.is::() { @@ -480,16 +484,29 @@ impl AcpThreadView { ) { match event { AcpThreadEvent::NewEntry => { - let index = thread.read(cx).entries().len() - 1; - self.sync_thread_entry_view(index, window, cx); + let len = thread.read(cx).entries().len(); + let index = len - 1; + self.entry_view_state.sync_entry( + self.workspace.clone(), + thread.clone(), + index, + window, + cx, + ); self.list_state.splice(index..index, 1); } AcpThreadEvent::EntryUpdated(index) => { - self.sync_thread_entry_view(*index, window, cx); + self.entry_view_state.sync_entry( + self.workspace.clone(), + thread.clone(), + *index, + window, + cx, + ); self.list_state.splice(*index..index + 1, 1); } AcpThreadEvent::EntriesRemoved(range) => { - // TODO: Clean up unused diff editors and terminal views + self.entry_view_state.remove(range.clone()); self.list_state.splice(range.clone(), 0); } AcpThreadEvent::ToolAuthorizationRequired => { @@ -523,128 +540,6 @@ impl AcpThreadView { cx.notify(); } - fn sync_thread_entry_view( - &mut self, - entry_ix: usize, - window: &mut Window, - cx: &mut Context, - ) { - self.sync_diff_multibuffers(entry_ix, window, cx); - self.sync_terminals(entry_ix, window, cx); - } - - fn sync_diff_multibuffers( - &mut self, - entry_ix: usize, - window: &mut Window, - cx: &mut Context, - ) { - let Some(multibuffers) = self.entry_diff_multibuffers(entry_ix, cx) else { - return; - }; - - let multibuffers = multibuffers.collect::>(); - - for multibuffer in multibuffers { - if self.diff_editors.contains_key(&multibuffer.entity_id()) { - return; - } - - let editor = cx.new(|cx| { - let mut editor = Editor::new( - EditorMode::Full { - scale_ui_elements_with_buffer_font_size: false, - show_active_line_background: false, - sized_by_content: true, - }, - multibuffer.clone(), - None, - window, - cx, - ); - editor.set_show_gutter(false, cx); - editor.disable_inline_diagnostics(); - editor.disable_expand_excerpt_buttons(cx); - editor.set_show_vertical_scrollbar(false, cx); - editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); - editor.set_soft_wrap_mode(SoftWrap::None, cx); - editor.scroll_manager.set_forbid_vertical_scroll(true); - editor.set_show_indent_guides(false, cx); - editor.set_read_only(true); - editor.set_show_breakpoints(false, cx); - editor.set_show_code_actions(false, cx); - editor.set_show_git_diff_gutter(false, cx); - editor.set_expand_all_diff_hunks(cx); - editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); - editor - }); - let entity_id = multibuffer.entity_id(); - cx.observe_release(&multibuffer, move |this, _, _| { - this.diff_editors.remove(&entity_id); - }) - .detach(); - - self.diff_editors.insert(entity_id, editor); - } - } - - fn entry_diff_multibuffers( - &self, - entry_ix: usize, - cx: &App, - ) -> Option>> { - let entry = self.thread()?.read(cx).entries().get(entry_ix)?; - Some( - entry - .diffs() - .map(|diff| diff.read(cx).multibuffer().clone()), - ) - } - - fn sync_terminals(&mut self, entry_ix: usize, window: &mut Window, cx: &mut Context) { - let Some(terminals) = self.entry_terminals(entry_ix, cx) else { - return; - }; - - let terminals = terminals.collect::>(); - - for terminal in terminals { - if self.terminal_views.contains_key(&terminal.entity_id()) { - return; - } - - let terminal_view = cx.new(|cx| { - let mut view = TerminalView::new( - terminal.read(cx).inner().clone(), - self.workspace.clone(), - None, - self.project.downgrade(), - window, - cx, - ); - view.set_embedded_mode(Some(1000), cx); - view - }); - - let entity_id = terminal.entity_id(); - cx.observe_release(&terminal, move |this, _, _| { - this.terminal_views.remove(&entity_id); - }) - .detach(); - - self.terminal_views.insert(entity_id, terminal_view); - } - } - - fn entry_terminals( - &self, - entry_ix: usize, - cx: &App, - ) -> Option>> { - let entry = self.thread()?.read(cx).entries().get(entry_ix)?; - Some(entry.terminals().map(|terminal| terminal.clone())) - } - fn authenticate( &mut self, method: acp::AuthMethodId, @@ -712,7 +607,7 @@ impl AcpThreadView { fn render_entry( &self, - index: usize, + entry_ix: usize, total_entries: usize, entry: &AgentThreadEntry, window: &mut Window, @@ -720,7 +615,7 @@ impl AcpThreadView { ) -> AnyElement { let primary = match &entry { AgentThreadEntry::UserMessage(message) => div() - .id(("user_message", index)) + .id(("user_message", entry_ix)) .py_4() .px_2() .children(message.id.clone().and_then(|message_id| { @@ -749,7 +644,9 @@ impl AcpThreadView { .text_xs() .id("message") .on_click(cx.listener({ - move |this, _, window, cx| this.start_editing_message(index, window, cx) + move |this, _, window, cx| { + this.start_editing_message(entry_ix, window, cx) + } })) .children( if let Some(editing) = self.editing_message.as_ref() @@ -787,7 +684,7 @@ impl AcpThreadView { AssistantMessageChunk::Thought { block } => { block.markdown().map(|md| { self.render_thinking_block( - index, + entry_ix, chunk_ix, md.clone(), window, @@ -803,7 +700,7 @@ impl AcpThreadView { v_flex() .px_5() .py_1() - .when(index + 1 == total_entries, |this| this.pb_4()) + .when(entry_ix + 1 == total_entries, |this| this.pb_4()) .w_full() .text_ui(cx) .child(message_body) @@ -815,10 +712,12 @@ impl AcpThreadView { div().w_full().py_1p5().px_5().map(|this| { if has_terminals { this.children(tool_call.terminals().map(|terminal| { - self.render_terminal_tool_call(terminal, tool_call, window, cx) + self.render_terminal_tool_call( + entry_ix, terminal, tool_call, window, cx, + ) })) } else { - this.child(self.render_tool_call(index, tool_call, window, cx)) + this.child(self.render_tool_call(entry_ix, tool_call, window, cx)) } }) } @@ -830,7 +729,7 @@ impl AcpThreadView { }; let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); - let primary = if index == total_entries - 1 && !is_generating { + let primary = if entry_ix == total_entries - 1 && !is_generating { v_flex() .w_full() .child(primary) @@ -841,10 +740,10 @@ impl AcpThreadView { }; if let Some(editing) = self.editing_message.as_ref() - && editing.index < index + && editing.index < entry_ix { let backdrop = div() - .id(("backdrop", index)) + .id(("backdrop", entry_ix)) .size_full() .absolute() .inset_0() @@ -1125,7 +1024,9 @@ impl AcpThreadView { .w_full() .children(tool_call.content.iter().map(|content| { div() - .child(self.render_tool_call_content(content, tool_call, window, cx)) + .child( + self.render_tool_call_content(entry_ix, content, tool_call, window, cx), + ) .into_any_element() })) .child(self.render_permission_buttons( @@ -1139,7 +1040,9 @@ impl AcpThreadView { .w_full() .children(tool_call.content.iter().map(|content| { div() - .child(self.render_tool_call_content(content, tool_call, window, cx)) + .child( + self.render_tool_call_content(entry_ix, content, tool_call, window, cx), + ) .into_any_element() })), ToolCallStatus::Rejected => v_flex().size_0(), @@ -1257,6 +1160,7 @@ impl AcpThreadView { fn render_tool_call_content( &self, + entry_ix: usize, content: &ToolCallContent, tool_call: &ToolCall, window: &Window, @@ -1273,10 +1177,10 @@ impl AcpThreadView { } } ToolCallContent::Diff(diff) => { - self.render_diff_editor(&diff.read(cx).multibuffer(), cx) + self.render_diff_editor(entry_ix, &diff.read(cx).multibuffer(), cx) } ToolCallContent::Terminal(terminal) => { - self.render_terminal_tool_call(terminal, tool_call, window, cx) + self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx) } } } @@ -1420,6 +1324,7 @@ impl AcpThreadView { fn render_diff_editor( &self, + entry_ix: usize, multibuffer: &Entity, cx: &Context, ) -> AnyElement { @@ -1428,7 +1333,9 @@ impl AcpThreadView { .border_t_1() .border_color(self.tool_card_border_color(cx)) .child( - if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) { + if let Some(entry) = self.entry_view_state.entry(entry_ix) + && let Some(editor) = entry.editor_for_diff(&multibuffer) + { editor.clone().into_any_element() } else { Empty.into_any() @@ -1439,6 +1346,7 @@ impl AcpThreadView { fn render_terminal_tool_call( &self, + entry_ix: usize, terminal: &Entity, tool_call: &ToolCall, window: &Window, @@ -1627,8 +1535,11 @@ impl AcpThreadView { })), ); - let show_output = - self.terminal_expanded && self.terminal_views.contains_key(&terminal.entity_id()); + let terminal_view = self + .entry_view_state + .entry(entry_ix) + .and_then(|entry| entry.terminal(&terminal)); + let show_output = self.terminal_expanded && terminal_view.is_some(); v_flex() .mb_2() @@ -1661,8 +1572,6 @@ impl AcpThreadView { ), ) .when(show_output, |this| { - let terminal_view = self.terminal_views.get(&terminal.entity_id()).unwrap(); - this.child( div() .pt_2() @@ -1672,7 +1581,7 @@ impl AcpThreadView { .bg(cx.theme().colors().editor_background) .rounded_b_md() .text_ui_sm(cx) - .child(terminal_view.clone()), + .children(terminal_view.clone()), ) }) .into_any() @@ -3075,12 +2984,7 @@ impl AcpThreadView { } fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context) { - for diff_editor in self.diff_editors.values() { - diff_editor.update(cx, |diff_editor, cx| { - diff_editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); - cx.notify(); - }) - } + self.entry_view_state.settings_changed(cx); } pub(crate) fn insert_dragged_files( @@ -3379,18 +3283,6 @@ fn plan_label_markdown_style( } } -fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement { - TextStyleRefinement { - font_size: Some( - TextSize::Small - .rems(cx) - .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx)) - .into(), - ), - ..Default::default() - } -} - fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { let default_md_style = default_markdown_style(true, window, cx); @@ -3405,16 +3297,16 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { #[cfg(test)] pub(crate) mod tests { - use std::{path::Path, sync::Arc}; + use std::path::Path; + use acp_thread::StubAgentConnection; use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::SessionId; use editor::EditorSettings; use fs::FakeFs; - use futures::future::try_join_all; use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; - use parking_lot::Mutex; - use rand::Rng; + use project::Project; + use serde_json::json; use settings::SettingsStore; use super::*; @@ -3497,8 +3389,8 @@ pub(crate) mod tests { raw_input: None, raw_output: None, }; - let connection = StubAgentConnection::new(vec![acp::SessionUpdate::ToolCall(tool_call)]) - .with_permission_requests(HashMap::from_iter([( + let connection = + StubAgentConnection::new().with_permission_requests(HashMap::from_iter([( tool_call_id, vec![acp::PermissionOption { id: acp::PermissionOptionId("1".into()), @@ -3506,6 +3398,9 @@ pub(crate) mod tests { kind: acp::PermissionOptionKind::AllowOnce, }], )])); + + connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]); + let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); @@ -3605,115 +3500,6 @@ pub(crate) mod tests { } } - #[derive(Clone, Default)] - struct StubAgentConnection { - sessions: Arc>>>, - permission_requests: HashMap>, - updates: Vec, - } - - impl StubAgentConnection { - fn new(updates: Vec) -> Self { - Self { - updates, - permission_requests: HashMap::default(), - sessions: Arc::default(), - } - } - - fn with_permission_requests( - mut self, - permission_requests: HashMap>, - ) -> Self { - self.permission_requests = permission_requests; - self - } - } - - impl AgentConnection for StubAgentConnection { - fn auth_methods(&self) -> &[acp::AuthMethod] { - &[] - } - - fn new_thread( - self: Rc, - project: Entity, - _cwd: &Path, - cx: &mut gpui::AsyncApp, - ) -> Task>> { - let session_id = SessionId( - rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(7) - .map(char::from) - .collect::() - .into(), - ); - let thread = 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, - _method_id: acp::AuthMethodId, - _cx: &mut App, - ) -> Task> { - unimplemented!() - } - - fn prompt( - &self, - _id: Option, - 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 { - 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_authorization( - 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(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - }) - }) - } - - fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { - unimplemented!() - } - } - #[derive(Clone)] struct SaboteurAgentConnection; @@ -3722,19 +3508,17 @@ pub(crate) mod tests { self: Rc, project: Entity, _cwd: &Path, - cx: &mut gpui::AsyncApp, + cx: &mut gpui::App, ) -> Task>> { - Task::ready(Ok(cx - .new(|cx| { - AcpThread::new( - "SaboteurAgentConnection", - self, - project, - SessionId("test".into()), - cx, - ) - }) - .unwrap())) + Task::ready(Ok(cx.new(|cx| { + AcpThread::new( + "SaboteurAgentConnection", + self, + project, + SessionId("test".into()), + cx, + ) + }))) } fn auth_methods(&self) -> &[acp::AuthMethod] { @@ -3776,4 +3560,142 @@ pub(crate) mod tests { EditorSettings::register(cx); }); } + + #[gpui::test] + async fn test_rewind_views(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/project", + json!({ + "test1.txt": "old content 1", + "test2.txt": "old content 2" + }), + ) + .await; + let project = Project::test(fs, [Path::new("/project")], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let thread_store = + cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx))); + let text_thread_store = + cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); + + let connection = Rc::new(StubAgentConnection::new()); + let thread_view = cx.update(|window, cx| { + cx.new(|cx| { + AcpThreadView::new( + Rc::new(StubAgentServer::new(connection.as_ref().clone())), + workspace.downgrade(), + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + window, + cx, + ) + }) + }); + + cx.run_until_parked(); + + let thread = thread_view + .read_with(cx, |view, _| view.thread().cloned()) + .unwrap(); + + // First user message + connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall { + id: acp::ToolCallId("tool1".into()), + title: "Edit file 1".into(), + kind: acp::ToolKind::Edit, + status: acp::ToolCallStatus::Completed, + content: vec![acp::ToolCallContent::Diff { + diff: acp::Diff { + path: "/project/test1.txt".into(), + old_text: Some("old content 1".into()), + new_text: "new content 1".into(), + }, + }], + locations: vec![], + raw_input: None, + raw_output: None, + })]); + + thread + .update(cx, |thread, cx| thread.send_raw("Give me a diff", cx)) + .await + .unwrap(); + cx.run_until_parked(); + + thread.read_with(cx, |thread, _| { + assert_eq!(thread.entries().len(), 2); + }); + + thread_view.read_with(cx, |view, _| { + assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0); + assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1); + }); + + // Second user message + connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall { + id: acp::ToolCallId("tool2".into()), + title: "Edit file 2".into(), + kind: acp::ToolKind::Edit, + status: acp::ToolCallStatus::Completed, + content: vec![acp::ToolCallContent::Diff { + diff: acp::Diff { + path: "/project/test2.txt".into(), + old_text: Some("old content 2".into()), + new_text: "new content 2".into(), + }, + }], + locations: vec![], + raw_input: None, + raw_output: None, + })]); + + thread + .update(cx, |thread, cx| thread.send_raw("Another one", cx)) + .await + .unwrap(); + cx.run_until_parked(); + + let second_user_message_id = thread.read_with(cx, |thread, _| { + assert_eq!(thread.entries().len(), 4); + let AgentThreadEntry::UserMessage(user_message) = thread.entries().get(2).unwrap() + else { + panic!(); + }; + user_message.id.clone().unwrap() + }); + + thread_view.read_with(cx, |view, _| { + assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0); + assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1); + assert_eq!(view.entry_view_state.entry(2).unwrap().len(), 0); + assert_eq!(view.entry_view_state.entry(3).unwrap().len(), 1); + }); + + // Rewind to first message + thread + .update(cx, |thread, cx| thread.rewind(second_user_message_id, cx)) + .await + .unwrap(); + + cx.run_until_parked(); + + thread.read_with(cx, |thread, _| { + assert_eq!(thread.entries().len(), 2); + }); + + thread_view.read_with(cx, |view, _| { + assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0); + assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1); + + // Old views should be dropped + assert!(view.entry_view_state.entry(2).is_none()); + assert!(view.entry_view_state.entry(3).is_none()); + }); + } } From eb9bbaacb1ccd0f4d92325e24a158739faa3872c Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 14 Aug 2025 15:07:28 -0400 Subject: [PATCH 011/744] Add onboarding reset restore script (#36202) Release Notes: - N/A --- script/onboarding | 176 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100755 script/onboarding diff --git a/script/onboarding b/script/onboarding new file mode 100755 index 0000000000000000000000000000000000000000..6cc878ec96562ff8f1ee9629060f90271407bb09 --- /dev/null +++ b/script/onboarding @@ -0,0 +1,176 @@ +#!/usr/bin/env bash + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +CHANNEL="$1" +COMMAND="$2" + +if [[ "$CHANNEL" != "stable" && "$CHANNEL" != "preview" && "$CHANNEL" != "nightly" && "$CHANNEL" != "dev" ]]; then + echo -e "${RED}Error: Invalid channel '$CHANNEL'. Must be one of: stable, preview, nightly, dev${NC}" + exit 1 +fi + +if [[ "$OSTYPE" == "darwin"* ]]; then + DB_BASE_DIR="$HOME/Library/Application Support/Zed/db" + DB_DIR="$DB_BASE_DIR/0-$CHANNEL" + DB_BACKUP_DIR="$DB_BASE_DIR/0-$CHANNEL.onboarding_backup" + case "$CHANNEL" in + stable) APP_NAME="Zed" ;; + preview) APP_NAME="Zed Preview" ;; + nightly) APP_NAME="Zed Nightly" ;; + dev) APP_NAME="Zed Dev" ;; + esac +elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + DB_BASE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/zed/db" + DB_DIR="$DB_BASE_DIR/0-$CHANNEL" + DB_BACKUP_DIR="$DB_BASE_DIR/0-$CHANNEL.onboarding_backup" + case "$CHANNEL" in + stable) APP_NAME="zed" ;; + preview) APP_NAME="zed-preview" ;; + nightly) APP_NAME="zed-nightly" ;; + dev) APP_NAME="zed-dev" ;; + esac +elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then + LOCALAPPDATA_PATH="${LOCALAPPDATA:-$USERPROFILE/AppData/Local}" + DB_BASE_DIR="$LOCALAPPDATA_PATH/Zed/db" + DB_DIR="$DB_BASE_DIR/0-$CHANNEL" + DB_BACKUP_DIR="$DB_BASE_DIR/0-$CHANNEL.onboarding_backup" + + case "$CHANNEL" in + stable) APP_NAME="Zed" ;; + preview) APP_NAME="Zed Preview" ;; + nightly) APP_NAME="Zed Nightly" ;; + dev) APP_NAME="Zed Dev" ;; + esac +else + echo -e "${RED}Error: Unsupported OS type: $OSTYPE${NC}" + exit 1 +fi + +reset_onboarding() { + echo -e "${BLUE}=== Resetting $APP_NAME to First-Time User State ===${NC}" + echo "" + + if [ ! -d "$DB_DIR" ]; then + echo -e "${YELLOW}No database directory found at: $DB_DIR${NC}" + echo "Zed will create a fresh database on next launch and show onboarding." + exit 0 + fi + + if [ -d "$DB_BACKUP_DIR" ]; then + echo -e "${RED}ERROR: Backup already exists at: $DB_BACKUP_DIR${NC}" + echo "" + echo "This suggests you've already run 'onboarding reset'." + echo "To avoid losing your original database, this script won't overwrite the backup." + echo "" + echo "Options:" + echo " 1. Run './script/onboarding $CHANNEL restore' to restore your original database" + echo " 2. Manually remove the backup if you're sure: rm -rf $DB_BACKUP_DIR" + exit 1 + fi + + echo -e "${YELLOW}Moving $DB_DIR to $DB_BACKUP_DIR${NC}" + mv "$DB_DIR" "$DB_BACKUP_DIR" + + echo -e "${GREEN}✓ Backed up: $DB_BACKUP_DIR${NC}" + echo "" + echo -e "${GREEN}Success! Zed has been reset to first-time user state.${NC}" + echo "" + echo "Next steps:" + echo " 1. Start Zed - you should see the onboarding flow" + echo " 2. When done testing, run: ./script/onboarding $CHANNEL restore" + echo "" + echo -e "${YELLOW}Note: All your workspace data is safely preserved in the backup.${NC}" +} + +restore_onboarding() { + echo -e "${BLUE}=== Restoring Original $APP_NAME Database ===${NC}" + echo "" + + if [ ! -d "$DB_BACKUP_DIR" ]; then + echo -e "${RED}ERROR: No backup found at: $DB_BACKUP_DIR${NC}" + echo "" + echo "Run './script/onboarding $CHANNEL reset' first to create a backup." + exit 1 + fi + + if [ -d "$DB_DIR" ]; then + echo -e "${YELLOW}Removing current database directory: $DB_DIR${NC}" + rm -rf "$DB_DIR" + fi + + echo -e "${YELLOW}Restoring $DB_BACKUP_DIR to $DB_DIR${NC}" + mv "$DB_BACKUP_DIR" "$DB_DIR" + + echo -e "${GREEN}✓ Restored: $DB_DIR${NC}" + echo "" + echo -e "${GREEN}Success! Your original database has been restored.${NC}" +} + +show_status() { + echo -e "${BLUE}=== Zed Onboarding Test Status ===${NC}" + echo "" + + if [ -d "$DB_BACKUP_DIR" ]; then + echo -e "${YELLOW}Status: TESTING MODE${NC}" + echo " • Original database: $DB_BACKUP_DIR" + echo " • Zed is using: $DB_DIR" + echo " • Run './script/onboarding $CHANNEL restore' to return to normal" + elif [ -d "$DB_DIR" ]; then + echo -e "${GREEN}Status: NORMAL${NC}" + echo " • Zed is using: $DB_DIR" + echo " • Run './script/onboarding $CHANNEL reset' to test onboarding" + else + echo -e "${BLUE}Status: NO DATABASE${NC}" + echo " • No Zed database directory exists yet" + echo " • Zed will show onboarding on next launch" + fi +} + +case "${COMMAND:-}" in + reset) + reset_onboarding + ;; + restore) + restore_onboarding + ;; + status) + show_status + ;; + *) + echo -e "${BLUE}Zed Onboarding Test Script${NC}" + echo "" + echo "Usage: $(basename $0) [channel] " + echo "" + echo "Commands:" + echo " reset - Back up current database and reset to show onboarding" + echo " restore - Restore the original database after testing" + echo " status - Show current testing status" + echo "" + echo "Channels:" + echo " stable, preview, nightly, dev" + echo "" + echo "Working with channel: $CHANNEL" + echo "Database directory: $DB_DIR" + echo "" + echo "Examples:" + echo " ./script/onboarding nightly reset # Reset nightly" + echo " ./script/onboarding stable reset # Reset stable" + echo " ./script/onboarding preview restore # Restore preview" + echo "" + echo "Workflow:" + echo " 1. Close Zed" + echo " 2. ./script/onboarding nightly reset" + echo " 3. Open Zed" + echo " 4. Test onboarding" + echo " 5. Close Zed" + echo " 6. ./script/onboarding nightly restore" + exit 1 + ;; +esac From b65e9af3e97a5198dd0b3665f7c712690cf19561 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 14 Aug 2025 13:08:35 -0600 Subject: [PATCH 012/744] Add [f/]f to follow the next collaborator (#36191) Release Notes: - vim: Add `[f`/`]f` to go to the next collaborator --- assets/keymaps/vim.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 560ca3bdd807301a45f60c02f9b3aea905f47ad7..be6d34a1342b6fabe0561643c74034d3c99a04b6 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -58,6 +58,8 @@ "[ space": "vim::InsertEmptyLineAbove", "[ e": "editor::MoveLineUp", "] e": "editor::MoveLineDown", + "[ f": "workspace::FollowNextCollaborator", + "] f": "workspace::FollowNextCollaborator", // Word motions "w": "vim::NextWordStart", From 3a711d08149dbaf1ac40ee5578d276dbc69e35c1 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 14 Aug 2025 15:19:37 -0400 Subject: [PATCH 013/744] Remove onboarding script (#36203) Just use `ZED_STATELESS=1 zed` instead! Release Notes: - N/A *or* Added/Fixed/Improved ... --- script/onboarding | 176 ---------------------------------------------- 1 file changed, 176 deletions(-) delete mode 100755 script/onboarding diff --git a/script/onboarding b/script/onboarding deleted file mode 100755 index 6cc878ec96562ff8f1ee9629060f90271407bb09..0000000000000000000000000000000000000000 --- a/script/onboarding +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env bash - -set -e - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -CHANNEL="$1" -COMMAND="$2" - -if [[ "$CHANNEL" != "stable" && "$CHANNEL" != "preview" && "$CHANNEL" != "nightly" && "$CHANNEL" != "dev" ]]; then - echo -e "${RED}Error: Invalid channel '$CHANNEL'. Must be one of: stable, preview, nightly, dev${NC}" - exit 1 -fi - -if [[ "$OSTYPE" == "darwin"* ]]; then - DB_BASE_DIR="$HOME/Library/Application Support/Zed/db" - DB_DIR="$DB_BASE_DIR/0-$CHANNEL" - DB_BACKUP_DIR="$DB_BASE_DIR/0-$CHANNEL.onboarding_backup" - case "$CHANNEL" in - stable) APP_NAME="Zed" ;; - preview) APP_NAME="Zed Preview" ;; - nightly) APP_NAME="Zed Nightly" ;; - dev) APP_NAME="Zed Dev" ;; - esac -elif [[ "$OSTYPE" == "linux-gnu"* ]]; then - DB_BASE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/zed/db" - DB_DIR="$DB_BASE_DIR/0-$CHANNEL" - DB_BACKUP_DIR="$DB_BASE_DIR/0-$CHANNEL.onboarding_backup" - case "$CHANNEL" in - stable) APP_NAME="zed" ;; - preview) APP_NAME="zed-preview" ;; - nightly) APP_NAME="zed-nightly" ;; - dev) APP_NAME="zed-dev" ;; - esac -elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then - LOCALAPPDATA_PATH="${LOCALAPPDATA:-$USERPROFILE/AppData/Local}" - DB_BASE_DIR="$LOCALAPPDATA_PATH/Zed/db" - DB_DIR="$DB_BASE_DIR/0-$CHANNEL" - DB_BACKUP_DIR="$DB_BASE_DIR/0-$CHANNEL.onboarding_backup" - - case "$CHANNEL" in - stable) APP_NAME="Zed" ;; - preview) APP_NAME="Zed Preview" ;; - nightly) APP_NAME="Zed Nightly" ;; - dev) APP_NAME="Zed Dev" ;; - esac -else - echo -e "${RED}Error: Unsupported OS type: $OSTYPE${NC}" - exit 1 -fi - -reset_onboarding() { - echo -e "${BLUE}=== Resetting $APP_NAME to First-Time User State ===${NC}" - echo "" - - if [ ! -d "$DB_DIR" ]; then - echo -e "${YELLOW}No database directory found at: $DB_DIR${NC}" - echo "Zed will create a fresh database on next launch and show onboarding." - exit 0 - fi - - if [ -d "$DB_BACKUP_DIR" ]; then - echo -e "${RED}ERROR: Backup already exists at: $DB_BACKUP_DIR${NC}" - echo "" - echo "This suggests you've already run 'onboarding reset'." - echo "To avoid losing your original database, this script won't overwrite the backup." - echo "" - echo "Options:" - echo " 1. Run './script/onboarding $CHANNEL restore' to restore your original database" - echo " 2. Manually remove the backup if you're sure: rm -rf $DB_BACKUP_DIR" - exit 1 - fi - - echo -e "${YELLOW}Moving $DB_DIR to $DB_BACKUP_DIR${NC}" - mv "$DB_DIR" "$DB_BACKUP_DIR" - - echo -e "${GREEN}✓ Backed up: $DB_BACKUP_DIR${NC}" - echo "" - echo -e "${GREEN}Success! Zed has been reset to first-time user state.${NC}" - echo "" - echo "Next steps:" - echo " 1. Start Zed - you should see the onboarding flow" - echo " 2. When done testing, run: ./script/onboarding $CHANNEL restore" - echo "" - echo -e "${YELLOW}Note: All your workspace data is safely preserved in the backup.${NC}" -} - -restore_onboarding() { - echo -e "${BLUE}=== Restoring Original $APP_NAME Database ===${NC}" - echo "" - - if [ ! -d "$DB_BACKUP_DIR" ]; then - echo -e "${RED}ERROR: No backup found at: $DB_BACKUP_DIR${NC}" - echo "" - echo "Run './script/onboarding $CHANNEL reset' first to create a backup." - exit 1 - fi - - if [ -d "$DB_DIR" ]; then - echo -e "${YELLOW}Removing current database directory: $DB_DIR${NC}" - rm -rf "$DB_DIR" - fi - - echo -e "${YELLOW}Restoring $DB_BACKUP_DIR to $DB_DIR${NC}" - mv "$DB_BACKUP_DIR" "$DB_DIR" - - echo -e "${GREEN}✓ Restored: $DB_DIR${NC}" - echo "" - echo -e "${GREEN}Success! Your original database has been restored.${NC}" -} - -show_status() { - echo -e "${BLUE}=== Zed Onboarding Test Status ===${NC}" - echo "" - - if [ -d "$DB_BACKUP_DIR" ]; then - echo -e "${YELLOW}Status: TESTING MODE${NC}" - echo " • Original database: $DB_BACKUP_DIR" - echo " • Zed is using: $DB_DIR" - echo " • Run './script/onboarding $CHANNEL restore' to return to normal" - elif [ -d "$DB_DIR" ]; then - echo -e "${GREEN}Status: NORMAL${NC}" - echo " • Zed is using: $DB_DIR" - echo " • Run './script/onboarding $CHANNEL reset' to test onboarding" - else - echo -e "${BLUE}Status: NO DATABASE${NC}" - echo " • No Zed database directory exists yet" - echo " • Zed will show onboarding on next launch" - fi -} - -case "${COMMAND:-}" in - reset) - reset_onboarding - ;; - restore) - restore_onboarding - ;; - status) - show_status - ;; - *) - echo -e "${BLUE}Zed Onboarding Test Script${NC}" - echo "" - echo "Usage: $(basename $0) [channel] " - echo "" - echo "Commands:" - echo " reset - Back up current database and reset to show onboarding" - echo " restore - Restore the original database after testing" - echo " status - Show current testing status" - echo "" - echo "Channels:" - echo " stable, preview, nightly, dev" - echo "" - echo "Working with channel: $CHANNEL" - echo "Database directory: $DB_DIR" - echo "" - echo "Examples:" - echo " ./script/onboarding nightly reset # Reset nightly" - echo " ./script/onboarding stable reset # Reset stable" - echo " ./script/onboarding preview restore # Restore preview" - echo "" - echo "Workflow:" - echo " 1. Close Zed" - echo " 2. ./script/onboarding nightly reset" - echo " 3. Open Zed" - echo " 4. Test onboarding" - echo " 5. Close Zed" - echo " 6. ./script/onboarding nightly restore" - exit 1 - ;; -esac From b7c562f359b65f2d529916503d579c027c49614d Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 14 Aug 2025 21:28:59 +0200 Subject: [PATCH 014/744] Bump `async-trait` (#36201) The latest release has span changes in it which prevents rust-analyzer from constantly showing `Box` and `Box::pin` on hover as well as those items polluting the go to definition feature on every identifier. See https://github.com/dtolnay/async-trait/pull/293 Release Notes: - N/A --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96cc1581a3f6a0717b01a69e0f0833bb18fa0fc8..b4e4d9f876a7e6d0fa01196891b34e6d94784637 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1304,9 +1304,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", From e2ce787c051032bd6d3ad61c6ffd26b062d0f246 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 14 Aug 2025 23:18:07 +0200 Subject: [PATCH 015/744] editor: Limit target names in hover links multibuffer titles (#36207) Release Notes: - N/A --- crates/editor/src/editor.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1f350cf0d0a8b9f18b42f6f3bbbbb6269f376263..a9780ed6c2711cd65fb4007abeed7795e69d5f57 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -15879,6 +15879,8 @@ impl Editor { .text_for_range(location.range.clone()) .collect::() }) + .unique() + .take(3) .join(", "); format!("{tab_kind} for {target}") }) @@ -16085,6 +16087,8 @@ impl Editor { .text_for_range(location.range.clone()) .collect::() }) + .unique() + .take(3) .join(", "); let title = format!("References to {target}"); Self::open_locations_in_multibuffer( From b1e806442aefd7cd5df740234b2d7c3539dc905a Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 14 Aug 2025 17:31:14 -0400 Subject: [PATCH 016/744] Support images in agent2 threads (#36152) - Support adding ImageContent to messages through copy/paste and through path completions - Ensure images are fully converted to LanguageModelImageContent before sending them to the model - Update ACP crate to v0.0.24 to enable passing image paths through the protocol Release Notes: - N/A --------- Co-authored-by: Conrad Irwin --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 9 +- crates/acp_thread/src/mention.rs | 9 + .../agent_ui/src/acp/completion_provider.rs | 220 ++++++++++----- crates/agent_ui/src/acp/message_editor.rs | 255 ++++++++++++++++-- crates/agent_ui/src/acp/thread_view.rs | 10 +- 7 files changed, 416 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4e4d9f876a7e6d0fa01196891b34e6d94784637..d0809bd8808864bf1e0cd7f3dd634561ce551d84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.23" +version = "0.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fad72b7b8ee4331b3a4c8d43c107e982a4725564b4ee658ae5c4e79d2b486e8" +checksum = "8fd68bbbef8e424fb8a605c5f0b00c360f682c4528b0a5feb5ec928aaf5ce28e" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index 1baa6d3d7497934b13a368eec5bad9c3c09445d4..a872cadd3966ac35857347c1f9b26b6d1eafa790 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -425,7 +425,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.23" +agent-client-protocol = "0.0.24" 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 4bdc42ea2ed1529f4baf00823438428c96ee7bb1..4005f27a0c9a7ca3d13ec0ae91b0170838ddeb62 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -443,9 +443,8 @@ impl ContentBlock { }), .. }) => Self::resource_link_md(&uri), - acp::ContentBlock::Image(_) - | acp::ContentBlock::Audio(_) - | acp::ContentBlock::Resource(_) => String::new(), + acp::ContentBlock::Image(image) => Self::image_md(&image), + acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => String::new(), } } @@ -457,6 +456,10 @@ impl ContentBlock { } } + fn image_md(_image: &acp::ImageContent) -> String { + "`Image`".into() + } + fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { match self { ContentBlock::Empty => "", diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index b18cbfe18e8432eb8715556854a158b92b66e72b..b9b021c4ca1f728ba82e7df111456ace656bb3bc 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -6,6 +6,7 @@ use std::{ fmt, ops::Range, path::{Path, PathBuf}, + str::FromStr, }; use ui::{App, IconName, SharedString}; use url::Url; @@ -224,6 +225,14 @@ impl MentionUri { } } +impl FromStr for MentionUri { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + Self::parse(s) + } +} + pub struct MentionLink<'a>(&'a MentionUri); impl fmt::Display for MentionLink<'_> { diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 720ee23b00de5b2c19d15d53bfef5ef6eba6375e..adcfab85b1e9cbf03a5ce5625ea235e49a73a8b6 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,5 +1,6 @@ +use std::ffi::OsStr; use std::ops::Range; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::AtomicBool; @@ -8,13 +9,14 @@ use anyhow::{Context as _, Result, anyhow}; use collections::{HashMap, HashSet}; use editor::display_map::CreaseId; use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _}; - -use futures::future::try_join_all; +use futures::future::{Shared, try_join_all}; +use futures::{FutureExt, TryFutureExt}; use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{App, Entity, Task, WeakEntity}; +use gpui::{App, Entity, ImageFormat, Img, Task, WeakEntity}; use http_client::HttpClientWithUrl; use itertools::Itertools as _; use language::{Buffer, CodeLabel, HighlightId}; +use language_model::LanguageModelImage; use lsp::CompletionContext; use parking_lot::Mutex; use project::{ @@ -43,24 +45,43 @@ use crate::context_picker::{ available_context_picker_entries, recent_context_picker_entries, selection_ranges, }; +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MentionImage { + pub abs_path: Option>, + pub data: SharedString, + pub format: ImageFormat, +} + #[derive(Default)] pub struct MentionSet { uri_by_crease_id: HashMap, - fetch_results: HashMap, + fetch_results: HashMap>>>, + images: HashMap>>>, } impl MentionSet { - pub fn insert(&mut self, crease_id: CreaseId, uri: MentionUri) { + pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) { self.uri_by_crease_id.insert(crease_id, uri); } - pub fn add_fetch_result(&mut self, url: Url, content: String) { + pub fn add_fetch_result(&mut self, url: Url, content: Shared>>) { self.fetch_results.insert(url, content); } + pub fn insert_image( + &mut self, + crease_id: CreaseId, + task: Shared>>, + ) { + self.images.insert(crease_id, task); + } + pub fn drain(&mut self) -> impl Iterator { self.fetch_results.clear(); - self.uri_by_crease_id.drain().map(|(id, _)| id) + self.uri_by_crease_id + .drain() + .map(|(id, _)| id) + .chain(self.images.drain().map(|(id, _)| id)) } pub fn clear(&mut self) { @@ -76,7 +97,7 @@ impl MentionSet { window: &mut Window, cx: &mut App, ) -> Task>> { - let contents = self + let mut contents = self .uri_by_crease_id .iter() .map(|(&crease_id, uri)| { @@ -85,19 +106,59 @@ impl MentionSet { // TODO directories let uri = uri.clone(); let abs_path = abs_path.to_path_buf(); - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(abs_path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; + let extension = abs_path.extension().and_then(OsStr::to_str).unwrap_or(""); + + if Img::extensions().contains(&extension) && !extension.contains("svg") { + let open_image_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(&abs_path, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_image(path, cx)) + }); + + cx.spawn(async move |cx| { + let image_item = open_image_task?.await?; + let (data, format) = image_item.update(cx, |image_item, cx| { + let format = image_item.image.format; + ( + LanguageModelImage::from_image( + image_item.image.clone(), + cx, + ), + format, + ) + })?; + let data = cx.spawn(async move |_| { + if let Some(data) = data.await { + Ok(data.source) + } else { + anyhow::bail!("Failed to convert image") + } + }); - anyhow::Ok((crease_id, Mention { uri, content })) - }) + anyhow::Ok(( + crease_id, + Mention::Image(MentionImage { + abs_path: Some(abs_path.as_path().into()), + data: data.await?, + format, + }), + )) + }) + } else { + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(abs_path, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; + + anyhow::Ok((crease_id, Mention::Text { uri, content })) + }) + } } MentionUri::Symbol { path, line_range, .. @@ -130,7 +191,7 @@ impl MentionSet { .collect() })?; - anyhow::Ok((crease_id, Mention { uri, content })) + anyhow::Ok((crease_id, Mention::Text { uri, content })) }) } MentionUri::Thread { id: thread_id, .. } => { @@ -145,7 +206,7 @@ impl MentionSet { thread.latest_detailed_summary_or_text().to_string() })?; - anyhow::Ok((crease_id, Mention { uri, content })) + anyhow::Ok((crease_id, Mention::Text { uri, content })) }) } MentionUri::TextThread { path, .. } => { @@ -156,7 +217,7 @@ impl MentionSet { cx.spawn(async move |cx| { let context = context.await?; let xml = context.update(cx, |context, cx| context.to_xml(cx))?; - anyhow::Ok((crease_id, Mention { uri, content: xml })) + anyhow::Ok((crease_id, Mention::Text { uri, content: xml })) }) } MentionUri::Rule { id: prompt_id, .. } => { @@ -169,25 +230,39 @@ impl MentionSet { cx.spawn(async move |_| { // TODO: report load errors instead of just logging let text = text_task.await?; - anyhow::Ok((crease_id, Mention { uri, content: text })) + anyhow::Ok((crease_id, Mention::Text { uri, content: text })) }) } MentionUri::Fetch { url } => { - let Some(content) = self.fetch_results.get(&url) else { + let Some(content) = self.fetch_results.get(&url).cloned() else { return Task::ready(Err(anyhow!("missing fetch result"))); }; - Task::ready(Ok(( - crease_id, - Mention { - uri: uri.clone(), - content: content.clone(), - }, - ))) + let uri = uri.clone(); + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Text { + uri, + content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, + }, + )) + }) } } }) .collect::>(); + contents.extend(self.images.iter().map(|(crease_id, image)| { + let crease_id = *crease_id; + let image = image.clone(); + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?), + )) + }) + })); + cx.spawn(async move |_cx| { let contents = try_join_all(contents).await?.into_iter().collect(); anyhow::Ok(contents) @@ -195,10 +270,10 @@ impl MentionSet { } } -#[derive(Debug)] -pub struct Mention { - pub uri: MentionUri, - pub content: String, +#[derive(Debug, Eq, PartialEq)] +pub enum Mention { + Text { uri: MentionUri, content: String }, + Image(MentionImage), } pub(crate) enum Match { @@ -536,7 +611,10 @@ impl ContextPickerCompletionProvider { crease_ids.try_into().unwrap() }); - mention_set.lock().insert(crease_id, uri); + mention_set.lock().insert_uri( + crease_id, + MentionUri::Selection { path, line_range }, + ); current_offset += text_len + 1; } @@ -786,6 +864,7 @@ impl ContextPickerCompletionProvider { let url_to_fetch = url_to_fetch.clone(); let source_range = source_range.clone(); let icon_path = icon_path.clone(); + let mention_uri = mention_uri.clone(); Arc::new(move |_, window, cx| { let Some(url) = url::Url::parse(url_to_fetch.as_ref()) .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) @@ -799,6 +878,7 @@ impl ContextPickerCompletionProvider { let http_client = http_client.clone(); let source_range = source_range.clone(); let icon_path = icon_path.clone(); + let mention_uri = mention_uri.clone(); window.defer(cx, move |window, cx| { let url = url.clone(); @@ -819,17 +899,24 @@ impl ContextPickerCompletionProvider { let mention_set = mention_set.clone(); let http_client = http_client.clone(); let source_range = source_range.clone(); + + let url_string = url.to_string(); + let fetch = cx + .background_executor() + .spawn(async move { + fetch_url_content(http_client, url_string) + .map_err(|e| e.to_string()) + .await + }) + .shared(); + mention_set.lock().add_fetch_result(url, fetch.clone()); + window .spawn(cx, async move |cx| { - if let Some(content) = - fetch_url_content(http_client, url.to_string()) - .await - .notify_async_err(cx) - { - mention_set.lock().add_fetch_result(url.clone(), content); + if fetch.await.notify_async_err(cx).is_some() { mention_set .lock() - .insert(crease_id, MentionUri::Fetch { url }); + .insert_uri(crease_id, mention_uri.clone()); } else { // Remove crease if we failed to fetch editor @@ -1121,7 +1208,9 @@ fn confirm_completion_callback( window, cx, ) { - mention_set.lock().insert(crease_id, mention_uri.clone()); + mention_set + .lock() + .insert_uri(crease_id, mention_uri.clone()); } }); false @@ -1499,11 +1588,12 @@ mod tests { .into_values() .collect::>(); - assert_eq!(contents.len(), 1); - assert_eq!(contents[0].content, "1"); - assert_eq!( - contents[0].uri.to_uri().to_string(), - "file:///dir/a/one.txt" + pretty_assertions::assert_eq!( + contents, + [Mention::Text { + content: "1".into(), + uri: "file:///dir/a/one.txt".parse().unwrap() + }] ); cx.simulate_input(" "); @@ -1567,11 +1657,13 @@ mod tests { .collect::>(); assert_eq!(contents.len(), 2); - let new_mention = contents - .iter() - .find(|mention| mention.uri.to_uri().to_string() == "file:///dir/b/eight.txt") - .unwrap(); - assert_eq!(new_mention.content, "8"); + pretty_assertions::assert_eq!( + contents[1], + Mention::Text { + content: "8".to_string(), + uri: "file:///dir/b/eight.txt".parse().unwrap(), + } + ); editor.update(&mut cx, |editor, cx| { assert_eq!( @@ -1689,13 +1781,15 @@ mod tests { .collect::>(); assert_eq!(contents.len(), 3); - let new_mention = contents - .iter() - .find(|mention| { - mention.uri.to_uri().to_string() == "file:///dir/a/one.txt?symbol=MySymbol#L1:1" - }) - .unwrap(); - assert_eq!(new_mention.content, "1"); + pretty_assertions::assert_eq!( + contents[2], + Mention::Text { + content: "1".into(), + uri: "file:///dir/a/one.txt?symbol=MySymbol#L1:1" + .parse() + .unwrap(), + } + ); cx.run_until_parked(); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index fc34420d4e882334cbe74145595f192687dd6a5a..8d512948dd6ca4fbe6a05721ed30eab11660c2e8 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1,4 +1,5 @@ use crate::acp::completion_provider::ContextPickerCompletionProvider; +use crate::acp::completion_provider::MentionImage; use crate::acp::completion_provider::MentionSet; use acp_thread::MentionUri; use agent::TextThreadStore; @@ -6,30 +7,44 @@ use agent::ThreadStore; use agent_client_protocol as acp; use anyhow::Result; use collections::HashSet; +use editor::ExcerptId; +use editor::actions::Paste; +use editor::display_map::CreaseId; use editor::{ AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, EditorStyle, MultiBuffer, }; +use futures::FutureExt as _; +use gpui::ClipboardEntry; +use gpui::Image; +use gpui::ImageFormat; use gpui::{ AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, Task, TextStyle, WeakEntity, }; use language::Buffer; use language::Language; +use language_model::LanguageModelImage; use parking_lot::Mutex; use project::{CompletionIntent, Project}; use settings::Settings; use std::fmt::Write; +use std::path::Path; use std::rc::Rc; use std::sync::Arc; use theme::ThemeSettings; +use ui::IconName; +use ui::SharedString; use ui::{ ActiveTheme, App, InteractiveElement, IntoElement, ParentElement, Render, Styled, TextSize, Window, div, }; use util::ResultExt; use workspace::Workspace; +use workspace::notifications::NotifyResultExt as _; use zed_actions::agent::Chat; +use super::completion_provider::Mention; + pub struct MessageEditor { editor: Entity, project: Entity, @@ -130,23 +145,41 @@ impl MessageEditor { continue; } - if let Some(mention) = contents.get(&crease_id) { - let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot); - if crease_range.start > ix { - chunks.push(text[ix..crease_range.start].into()); - } - chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource { - annotations: None, - resource: acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents { - mime_type: None, - text: mention.content.clone(), - uri: mention.uri.to_uri().to_string(), - }, - ), - })); - ix = crease_range.end; + let Some(mention) = contents.get(&crease_id) else { + continue; + }; + + let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot); + if crease_range.start > ix { + chunks.push(text[ix..crease_range.start].into()); } + let chunk = match mention { + Mention::Text { uri, content } => { + acp::ContentBlock::Resource(acp::EmbeddedResource { + annotations: None, + resource: acp::EmbeddedResourceResource::TextResourceContents( + acp::TextResourceContents { + mime_type: None, + text: content.clone(), + uri: uri.to_uri().to_string(), + }, + ), + }) + } + Mention::Image(mention_image) => { + acp::ContentBlock::Image(acp::ImageContent { + annotations: None, + data: mention_image.data.to_string(), + mime_type: mention_image.format.mime_type().into(), + uri: mention_image + .abs_path + .as_ref() + .map(|path| format!("file://{}", path.display())), + }) + } + }; + chunks.push(chunk); + ix = crease_range.end; } if ix < text.len() { @@ -177,6 +210,56 @@ impl MessageEditor { cx.emit(MessageEditorEvent::Cancel) } + fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { + let images = cx + .read_from_clipboard() + .map(|item| { + item.into_entries() + .filter_map(|entry| { + if let ClipboardEntry::Image(image) = entry { + Some(image) + } else { + None + } + }) + .collect::>() + }) + .unwrap_or_default(); + + if images.is_empty() { + return; + } + cx.stop_propagation(); + + let replacement_text = "image"; + for image in images { + let (excerpt_id, anchor) = self.editor.update(cx, |message_editor, cx| { + let snapshot = message_editor.snapshot(window, cx); + let (excerpt_id, _, snapshot) = snapshot.buffer_snapshot.as_singleton().unwrap(); + + let anchor = snapshot.anchor_before(snapshot.len()); + message_editor.edit( + [( + multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), + format!("{replacement_text} "), + )], + cx, + ); + (*excerpt_id, anchor) + }); + + self.insert_image( + excerpt_id, + anchor, + replacement_text.len(), + Arc::new(image), + None, + window, + cx, + ); + } + } + pub fn insert_dragged_files( &self, paths: Vec, @@ -234,6 +317,68 @@ impl MessageEditor { } } + fn insert_image( + &mut self, + excerpt_id: ExcerptId, + crease_start: text::Anchor, + content_len: usize, + image: Arc, + abs_path: Option>, + window: &mut Window, + cx: &mut Context, + ) { + let Some(crease_id) = insert_crease_for_image( + excerpt_id, + crease_start, + content_len, + self.editor.clone(), + window, + cx, + ) else { + return; + }; + self.editor.update(cx, |_editor, cx| { + let format = image.format; + let convert = LanguageModelImage::from_image(image, cx); + + let task = cx + .spawn_in(window, async move |editor, cx| { + if let Some(image) = convert.await { + Ok(MentionImage { + abs_path, + data: image.source, + format, + }) + } else { + editor + .update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let Some(anchor) = + snapshot.anchor_in_excerpt(excerpt_id, crease_start) + else { + return; + }; + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + Err("Failed to convert image".to_string()) + } + }) + .shared(); + + cx.spawn_in(window, { + let task = task.clone(); + async move |_, cx| task.clone().await.notify_async_err(cx) + }) + .detach(); + + self.mention_set.lock().insert_image(crease_id, task); + }); + } + pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.set_mode(mode); @@ -243,12 +388,13 @@ impl MessageEditor { pub fn set_message( &mut self, - message: &[acp::ContentBlock], + message: Vec, window: &mut Window, cx: &mut Context, ) { let mut text = String::new(); let mut mentions = Vec::new(); + let mut images = Vec::new(); for chunk in message { match chunk { @@ -266,8 +412,13 @@ impl MessageEditor { mentions.push((start..end, mention_uri)); } } - acp::ContentBlock::Image(_) - | acp::ContentBlock::Audio(_) + acp::ContentBlock::Image(content) => { + let start = text.len(); + text.push_str("image"); + let end = text.len(); + images.push((start..end, content)); + } + acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) | acp::ContentBlock::ResourceLink(_) => {} } @@ -293,7 +444,50 @@ impl MessageEditor { ); if let Some(crease_id) = crease_id { - self.mention_set.lock().insert(crease_id, mention_uri); + self.mention_set.lock().insert_uri(crease_id, mention_uri); + } + } + for (range, content) in images { + let Some(format) = ImageFormat::from_mime_type(&content.mime_type) else { + continue; + }; + let anchor = snapshot.anchor_before(range.start); + let abs_path = content + .uri + .as_ref() + .and_then(|uri| uri.strip_prefix("file://").map(|s| Path::new(s).into())); + + let name = content + .uri + .as_ref() + .and_then(|uri| { + uri.strip_prefix("file://") + .and_then(|path| Path::new(path).file_name()) + }) + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or("Image".to_owned()); + let crease_id = crate::context_picker::insert_crease_for_mention( + anchor.excerpt_id, + anchor.text_anchor, + range.end - range.start, + name.into(), + IconName::Image.path().into(), + self.editor.clone(), + window, + cx, + ); + let data: SharedString = content.data.to_string().into(); + + if let Some(crease_id) = crease_id { + self.mention_set.lock().insert_image( + crease_id, + Task::ready(Ok(MentionImage { + abs_path, + data, + format, + })) + .shared(), + ); } } cx.notify(); @@ -319,6 +513,7 @@ impl Render for MessageEditor { .key_context("MessageEditor") .on_action(cx.listener(Self::chat)) .on_action(cx.listener(Self::cancel)) + .capture_action(cx.listener(Self::paste)) .flex_1() .child({ let settings = ThemeSettings::get_global(cx); @@ -351,6 +546,26 @@ impl Render for MessageEditor { } } +pub(crate) fn insert_crease_for_image( + excerpt_id: ExcerptId, + anchor: text::Anchor, + content_len: usize, + editor: Entity, + window: &mut Window, + cx: &mut App, +) -> Option { + crate::context_picker::insert_crease_for_mention( + excerpt_id, + anchor, + content_len, + "Image".into(), + IconName::Image.path().into(), + editor, + window, + cx, + ) +} + #[cfg(test)] mod tests { use std::path::Path; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 0e90b93f4d6836e167d0cc4dcceddbc9844a5b77..ee016b750357df2f3dea96a3810f6a607cea1925 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -5,9 +5,10 @@ use acp_thread::{ use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; use agent::{TextThreadStore, ThreadStore}; -use agent_client_protocol as acp; +use agent_client_protocol::{self as acp}; use agent_servers::AgentServer; use agent_settings::{AgentSettings, NotifyWhenAgentWaiting}; +use anyhow::bail; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; use collections::{HashMap, HashSet}; @@ -2360,7 +2361,7 @@ impl AcpThreadView { window, cx, ); - editor.set_message(&chunks, window, cx); + editor.set_message(chunks, window, cx); editor }); let subscription = @@ -2725,7 +2726,7 @@ impl AcpThreadView { let project = workspace.project().clone(); if !project.read(cx).is_local() { - anyhow::bail!("failed to open active thread as markdown in remote project"); + bail!("failed to open active thread as markdown in remote project"); } let buffer = project.update(cx, |project, cx| { @@ -2990,12 +2991,13 @@ impl AcpThreadView { pub(crate) fn insert_dragged_files( &self, paths: Vec, - _added_worktrees: Vec>, + added_worktrees: Vec>, window: &mut Window, cx: &mut Context, ) { self.message_editor.update(cx, |message_editor, cx| { message_editor.insert_dragged_files(paths, window, cx); + drop(added_worktrees); }) } } From 8366b6ce549d7695a5a544d98a035208531e2e5d Mon Sep 17 00:00:00 2001 From: Cretezy Date: Thu, 14 Aug 2025 17:46:38 -0400 Subject: [PATCH 017/744] workspace: Disable padding on zoomed panels (#36012) Continuation of https://github.com/zed-industries/zed/pull/31913 | Before | After | | -------|------| | ![image](https://github.com/user-attachments/assets/629e7da2-6070-4abb-b469-3b0824524ca4) | ![image](https://github.com/user-attachments/assets/99e54412-2e0b-4df9-9c40-a89b0411f6d8) | Release Notes: - Disable padding on zoomed panels --- crates/workspace/src/workspace.rs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index fb78c62f9e888b32e1fb6ba9ca390ffa51d833d8..ba9e3bbb8a973a604d549269faa6d259cac681c7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6664,25 +6664,15 @@ impl Render for Workspace { } }) .children(self.zoomed.as_ref().and_then(|view| { - let zoomed_view = view.upgrade()?; - let div = div() + Some(div() .occlude() .absolute() .overflow_hidden() .border_color(colors.border) .bg(colors.background) - .child(zoomed_view) + .child(view.upgrade()?) .inset_0() - .shadow_lg(); - - Some(match self.zoomed_position { - Some(DockPosition::Left) => div.right_2().border_r_1(), - Some(DockPosition::Right) => div.left_2().border_l_1(), - Some(DockPosition::Bottom) => div.top_2().border_t_1(), - None => { - div.top_2().bottom_2().left_2().right_2().border_1() - } - }) + .shadow_lg()) })) .children(self.render_notifications(window, cx)), ) From 4d27b228f776725b6f0f090b4856a7028b3dfe95 Mon Sep 17 00:00:00 2001 From: Alvaro Parker <64918109+AlvaroParker@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:31:01 -0400 Subject: [PATCH 018/744] remote server: Use env flag to opt out of musl remote server build (#36069) Closes #ISSUE This will allow devs to opt out of the musl build when developing zed by running `ZED_BUILD_REMOTE_SERVER=nomusl cargo r` which also fixes remote builds on NixOS. Release Notes: - Add a env flag (`ZED_BUILD_REMOTE_SERVER=nomusl`) to opt out of musl builds when building the remote server --- crates/remote/src/ssh_session.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 4306251e44acf988ce90fcd640d8c8bed36f1ee7..df7212d44c478ac68168be213131583ae980d8fa 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -2069,11 +2069,17 @@ impl SshRemoteConnection { Ok(()) } + let use_musl = !build_remote_server.contains("nomusl"); let triple = format!( "{}-{}", self.ssh_platform.arch, match self.ssh_platform.os { - "linux" => "unknown-linux-musl", + "linux" => + if use_musl { + "unknown-linux-musl" + } else { + "unknown-linux-gnu" + }, "macos" => "apple-darwin", _ => anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform), } @@ -2086,7 +2092,7 @@ impl SshRemoteConnection { String::new() } }; - if self.ssh_platform.os == "linux" { + if self.ssh_platform.os == "linux" && use_musl { rust_flags.push_str(" -C target-feature=+crt-static"); } if build_remote_server.contains("mold") { From 23d04331584edc7656a480715cab9532ccfc5861 Mon Sep 17 00:00:00 2001 From: smit Date: Fri, 15 Aug 2025 12:51:32 +0530 Subject: [PATCH 019/744] linux: Fix keyboard events not working on first start in X11 (#36224) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #29083 On X11, `ibus-x11` crashes on some distros after Zed interacts with it. This is not unique to Zed, `xim-rs` shows the same behavior, and there are similar upstream `ibus` reports with apps like Blender: - https://github.com/ibus/ibus/issues/2697 I opened an upstream issue to track this: - https://github.com/ibus/ibus/issues/2789 When this crash happens, we don’t get a disconnect event, so Zed keeps sending events to the IM server and waits for a response. It works on subsequent starts because IM server doesn't exist now and we default to non-XIM path. This PR detects the crash via X11 events and falls back to the non-XIM path so typing keeps working. We still need to investigate whether the root cause is in `xim-rs` or `ibus-x11`. Release Notes: - Fixed an issue on X11 where keyboard input sometimes didn’t work on first start. --- Cargo.lock | 6 ++-- crates/gpui/Cargo.toml | 2 +- crates/gpui/src/platform/linux/x11/client.rs | 38 ++++++++++++-------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0809bd8808864bf1e0cd7f3dd634561ce551d84..0bafc3c386ad8443a3e02a1e39a4cb2e2fa0e7c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20277,7 +20277,7 @@ dependencies = [ [[package]] name = "xim" version = "0.4.0" -source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd" +source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d" dependencies = [ "ahash 0.8.11", "hashbrown 0.14.5", @@ -20290,7 +20290,7 @@ dependencies = [ [[package]] name = "xim-ctext" version = "0.3.0" -source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd" +source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d" dependencies = [ "encoding_rs", ] @@ -20298,7 +20298,7 @@ dependencies = [ [[package]] name = "xim-parser" version = "0.2.1" -source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd" +source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d" dependencies = [ "bitflags 2.9.0", ] diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index d720dfb2a16ac7e8ac9ecae71d30a52d62a2c257..6be8c5fd1f29e535ddcbd855dbafaa49cdbda591 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -209,7 +209,7 @@ xkbcommon = { version = "0.8.0", features = [ "wayland", "x11", ], optional = true } -xim = { git = "https://github.com/XDeme1/xim-rs", rev = "d50d461764c2213655cd9cf65a0ea94c70d3c4fd", features = [ +xim = { git = "https://github.com/zed-industries/xim-rs", rev = "c0a70c1bd2ce197364216e5e818a2cb3adb99a8d" , features = [ "x11rb-xcb", "x11rb-client", ], optional = true } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 573e4addf75b90d50e7f453555507462280fb3d4..053cd0387b25f418696f12838187088229aaf044 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -642,13 +642,7 @@ impl X11Client { let xim_connected = xim_handler.connected; drop(state); - let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) { - Ok(handled) => handled, - Err(err) => { - log::error!("XIMClientError: {}", err); - false - } - }; + let xim_filtered = ximc.filter_event(&event, &mut xim_handler); let xim_callback_event = xim_handler.last_callback_event.take(); let mut state = self.0.borrow_mut(); @@ -659,14 +653,28 @@ impl X11Client { self.handle_xim_callback_event(event); } - if xim_filtered { - continue; - } - - if xim_connected { - self.xim_handle_event(event); - } else { - self.handle_event(event); + match xim_filtered { + Ok(handled) => { + if handled { + continue; + } + if xim_connected { + self.xim_handle_event(event); + } else { + self.handle_event(event); + } + } + Err(err) => { + // this might happen when xim server crashes on one of the events + // we do lose 1-2 keys when crash happens since there is no reliable way to get that info + // luckily, x11 sends us window not found error when xim server crashes upon further key press + // hence we fall back to handle_event + log::error!("XIMClientError: {}", err); + let mut state = self.0.borrow_mut(); + state.take_xim(); + drop(state); + self.handle_event(event); + } } } } From 8d6982e78f2493bb3ef2a23010f38dab141dc76a Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 15 Aug 2025 09:56:47 +0200 Subject: [PATCH 020/744] search: Fix some inconsistencies between project and buffer search bars (#36103) - project search query string now turns red when no results are found matching buffer search behavior - General code deduplication as well as more consistent layout between the two bars, as some minor details have drifted apart - Tab cycling in buffer search now ends up in editor focus when cycling backwards, matching forward cycling - Report parse errors in filter include and exclude editors Release Notes: - N/A --- crates/search/src/buffer_search.rs | 597 ++++++++++++---------------- crates/search/src/project_search.rs | 528 ++++++++++-------------- crates/search/src/search_bar.rs | 83 +++- crates/workspace/src/workspace.rs | 8 +- 4 files changed, 536 insertions(+), 680 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 14703be7a22b5e6580202553976343e8e6a2c970..ccef198f042de631b4a9b9e6a3550716dcd679b4 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -3,20 +3,23 @@ mod registrar; use crate::{ FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex, - ToggleReplace, ToggleSelection, ToggleWholeWord, search_bar::render_nav_button, + ToggleReplace, ToggleSelection, ToggleWholeWord, + search_bar::{ + input_base_styles, render_action_button, render_text_input, toggle_replace_button, + }, }; use any_vec::AnyVec; use anyhow::Context as _; use collections::HashMap; use editor::{ - DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle, + DisplayPoint, Editor, EditorSettings, actions::{Backtab, Tab}, }; use futures::channel::oneshot; use gpui::{ - Action, App, ClickEvent, Context, Entity, EventEmitter, FocusHandle, Focusable, - InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, - Styled, Subscription, Task, TextStyle, Window, actions, div, + Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _, + IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task, + Window, actions, div, }; use language::{Language, LanguageRegistry}; use project::{ @@ -27,7 +30,6 @@ use schemars::JsonSchema; use serde::Deserialize; use settings::Settings; use std::sync::Arc; -use theme::ThemeSettings; use zed_actions::outline::ToggleOutline; use ui::{ @@ -125,46 +127,6 @@ pub struct BufferSearchBar { } impl BufferSearchBar { - fn render_text_input( - &self, - editor: &Entity, - color_override: Option, - cx: &mut Context, - ) -> impl IntoElement { - let (color, use_syntax) = if editor.read(cx).read_only(cx) { - (cx.theme().colors().text_disabled, false) - } else { - match color_override { - Some(color_override) => (color_override.color(cx), false), - None => (cx.theme().colors().text, true), - } - }; - - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: rems(0.875).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(1.3), - ..TextStyle::default() - }; - - let mut editor_style = EditorStyle { - background: cx.theme().colors().toolbar_background, - local_player: cx.theme().players().local(), - text: text_style, - ..EditorStyle::default() - }; - if use_syntax { - editor_style.syntax = cx.theme().syntax().clone(); - } - - EditorElement::new(editor, editor_style) - } - pub fn query_editor_focused(&self) -> bool { self.query_editor_focused } @@ -185,7 +147,14 @@ impl Render for BufferSearchBar { let hide_inline_icons = self.editor_needed_width > self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.; - let supported_options = self.supported_options(cx); + let workspace::searchable::SearchOptions { + case, + word, + regex, + replacement, + selection, + find_in_results, + } = self.supported_options(cx); if self.query_editor.update(cx, |query_editor, _cx| { query_editor.placeholder_text().is_none() @@ -220,268 +189,205 @@ impl Render for BufferSearchBar { } }) .unwrap_or_else(|| "0/0".to_string()); - let should_show_replace_input = self.replace_enabled && supported_options.replacement; + let should_show_replace_input = self.replace_enabled && replacement; let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window); - let mut key_context = KeyContext::new_with_defaults(); - key_context.add("BufferSearchBar"); - if in_replace { - key_context.add("in_replace"); - } + let theme_colors = cx.theme().colors(); let query_border = if self.query_error.is_some() { Color::Error.color(cx) } else { - cx.theme().colors().border + theme_colors.border }; - let replacement_border = cx.theme().colors().border; + let replacement_border = theme_colors.border; let container_width = window.viewport_size().width; let input_width = SearchInputWidth::calc_width(container_width); - let input_base_styles = |border_color| { - h_flex() - .min_w_32() - .w(input_width) - .h_8() - .pl_2() - .pr_1() - .py_1() - .border_1() - .border_color(border_color) - .rounded_lg() - }; + let input_base_styles = + |border_color| input_base_styles(border_color, |div| div.w(input_width)); - let search_line = h_flex() - .gap_2() - .when(supported_options.find_in_results, |el| { - el.child(Label::new("Find in results").color(Color::Hint)) - }) - .child( - input_base_styles(query_border) - .id("editor-scroll") - .track_scroll(&self.editor_scroll_handle) - .child(self.render_text_input(&self.query_editor, color_override, cx)) - .when(!hide_inline_icons, |div| { - div.child( - h_flex() - .gap_1() - .children(supported_options.case.then(|| { - self.render_search_option_button( - SearchOptions::CASE_SENSITIVE, - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_case_sensitive( - &ToggleCaseSensitive, - window, - cx, - ) - }), - ) - })) - .children(supported_options.word.then(|| { - self.render_search_option_button( - SearchOptions::WHOLE_WORD, - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_whole_word(&ToggleWholeWord, window, cx) - }), - ) - })) - .children(supported_options.regex.then(|| { - self.render_search_option_button( - SearchOptions::REGEX, - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_regex(&ToggleRegex, window, cx) - }), - ) - })), - ) - }), - ) - .child( - h_flex() - .gap_1() - .min_w_64() - .when(supported_options.replacement, |this| { - this.child( - IconButton::new( - "buffer-search-bar-toggle-replace-button", - IconName::Replace, - ) - .style(ButtonStyle::Subtle) - .shape(IconButtonShape::Square) - .when(self.replace_enabled, |button| { - button.style(ButtonStyle::Filled) - }) - .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { - this.toggle_replace(&ToggleReplace, window, cx); - })) - .toggle_state(self.replace_enabled) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Toggle Replace", - &ToggleReplace, - &focus_handle, - window, - cx, - ) - } - }), - ) - }) - .when(supported_options.selection, |this| { - this.child( - IconButton::new( - "buffer-search-bar-toggle-search-selection-button", - IconName::Quote, - ) - .style(ButtonStyle::Subtle) - .shape(IconButtonShape::Square) - .when(self.selection_search_enabled, |button| { - button.style(ButtonStyle::Filled) - }) - .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { - this.toggle_selection(&ToggleSelection, window, cx); - })) - .toggle_state(self.selection_search_enabled) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Toggle Search Selection", - &ToggleSelection, - &focus_handle, - window, - cx, - ) - } - }), - ) - }) - .when(!supported_options.find_in_results, |el| { - el.child( - IconButton::new("select-all", ui::IconName::SelectAll) - .on_click(|_, window, cx| { - window.dispatch_action(SelectAllMatches.boxed_clone(), cx) - }) - .shape(IconButtonShape::Square) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Select All Matches", - &SelectAllMatches, - &focus_handle, - window, - cx, - ) - } + let query_column = input_base_styles(query_border) + .id("editor-scroll") + .track_scroll(&self.editor_scroll_handle) + .child(render_text_input(&self.query_editor, color_override, cx)) + .when(!hide_inline_icons, |div| { + div.child( + h_flex() + .gap_1() + .when(case, |div| { + div.child(SearchOptions::CASE_SENSITIVE.as_button( + self.search_options.contains(SearchOptions::CASE_SENSITIVE), + focus_handle.clone(), + cx.listener(|this, _, window, cx| { + this.toggle_case_sensitive(&ToggleCaseSensitive, window, cx) }), - ) - .child( - h_flex() - .pl_2() - .ml_1() - .border_l_1() - .border_color(cx.theme().colors().border_variant) - .child(render_nav_button( - ui::IconName::ChevronLeft, - self.active_match_index.is_some(), - "Select Previous Match", - &SelectPreviousMatch, - focus_handle.clone(), - )) - .child(render_nav_button( - ui::IconName::ChevronRight, - self.active_match_index.is_some(), - "Select Next Match", - &SelectNextMatch, - focus_handle.clone(), - )), - ) - .when(!narrow_mode, |this| { - this.child(h_flex().ml_2().min_w(rems_from_px(40.)).child( - Label::new(match_text).size(LabelSize::Small).color( - if self.active_match_index.is_some() { - Color::Default - } else { - Color::Disabled - }, - ), )) }) + .when(word, |div| { + div.child(SearchOptions::WHOLE_WORD.as_button( + self.search_options.contains(SearchOptions::WHOLE_WORD), + focus_handle.clone(), + cx.listener(|this, _, window, cx| { + this.toggle_whole_word(&ToggleWholeWord, window, cx) + }), + )) + }) + .when(regex, |div| { + div.child(SearchOptions::REGEX.as_button( + self.search_options.contains(SearchOptions::REGEX), + focus_handle.clone(), + cx.listener(|this, _, window, cx| { + this.toggle_regex(&ToggleRegex, window, cx) + }), + )) + }), + ) + }); + + let mode_column = h_flex() + .gap_1() + .min_w_64() + .when(replacement, |this| { + this.child(toggle_replace_button( + "buffer-search-bar-toggle-replace-button", + focus_handle.clone(), + self.replace_enabled, + cx.listener(|this, _: &ClickEvent, window, cx| { + this.toggle_replace(&ToggleReplace, window, cx); + }), + )) + }) + .when(selection, |this| { + this.child( + IconButton::new( + "buffer-search-bar-toggle-search-selection-button", + IconName::Quote, + ) + .style(ButtonStyle::Subtle) + .shape(IconButtonShape::Square) + .when(self.selection_search_enabled, |button| { + button.style(ButtonStyle::Filled) }) - .when(supported_options.find_in_results, |el| { - el.child( - IconButton::new(SharedString::from("Close"), IconName::Close) - .shape(IconButtonShape::Square) - .tooltip(move |window, cx| { - Tooltip::for_action("Close Search Bar", &Dismiss, window, cx) - }) - .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { - this.dismiss(&Dismiss, window, cx) - })), - ) + .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { + this.toggle_selection(&ToggleSelection, window, cx); + })) + .toggle_state(self.selection_search_enabled) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Toggle Search Selection", + &ToggleSelection, + &focus_handle, + window, + cx, + ) + } }), - ); - - let replace_line = should_show_replace_input.then(|| { - h_flex() - .gap_2() - .child( - input_base_styles(replacement_border).child(self.render_text_input( - &self.replacement_editor, - None, - cx, - )), ) - .child( - h_flex() - .min_w_64() - .gap_1() - .child( - IconButton::new("search-replace-next", ui::IconName::ReplaceNext) - .shape(IconButtonShape::Square) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Replace Next Match", - &ReplaceNext, - &focus_handle, - window, - cx, - ) - } - }) - .on_click(cx.listener(|this, _, window, cx| { - this.replace_next(&ReplaceNext, window, cx) - })), - ) - .child( - IconButton::new("search-replace-all", ui::IconName::ReplaceAll) - .shape(IconButtonShape::Square) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Replace All Matches", - &ReplaceAll, - &focus_handle, - window, - cx, - ) - } - }) - .on_click(cx.listener(|this, _, window, cx| { - this.replace_all(&ReplaceAll, window, cx) - })), - ), - ) - }); + }) + .when(!find_in_results, |el| { + let query_focus = self.query_editor.focus_handle(cx); + let matches_column = h_flex() + .pl_2() + .ml_2() + .border_l_1() + .border_color(theme_colors.border_variant) + .child(render_action_button( + "buffer-search-nav-button", + ui::IconName::ChevronLeft, + self.active_match_index.is_some(), + "Select Previous Match", + &SelectPreviousMatch, + query_focus.clone(), + )) + .child(render_action_button( + "buffer-search-nav-button", + ui::IconName::ChevronRight, + self.active_match_index.is_some(), + "Select Next Match", + &SelectNextMatch, + query_focus.clone(), + )) + .when(!narrow_mode, |this| { + this.child(div().ml_2().min_w(rems_from_px(40.)).child( + Label::new(match_text).size(LabelSize::Small).color( + if self.active_match_index.is_some() { + Color::Default + } else { + Color::Disabled + }, + ), + )) + }); + + el.child(render_action_button( + "buffer-search-nav-button", + IconName::SelectAll, + true, + "Select All Matches", + &SelectAllMatches, + query_focus, + )) + .child(matches_column) + }) + .when(find_in_results, |el| { + el.child(render_action_button( + "buffer-search", + IconName::Close, + true, + "Close Search Bar", + &Dismiss, + focus_handle.clone(), + )) + }); + + let search_line = h_flex() + .w_full() + .gap_2() + .when(find_in_results, |el| { + el.child(Label::new("Find in results").color(Color::Hint)) + }) + .child(query_column) + .child(mode_column); + + let replace_line = + should_show_replace_input.then(|| { + let replace_column = input_base_styles(replacement_border) + .child(render_text_input(&self.replacement_editor, None, cx)); + let focus_handle = self.replacement_editor.read(cx).focus_handle(cx); + + let replace_actions = h_flex() + .min_w_64() + .gap_1() + .child(render_action_button( + "buffer-search-replace-button", + IconName::ReplaceNext, + true, + "Replace Next Match", + &ReplaceNext, + focus_handle.clone(), + )) + .child(render_action_button( + "buffer-search-replace-button", + IconName::ReplaceAll, + true, + "Replace All Matches", + &ReplaceAll, + focus_handle, + )); + h_flex() + .w_full() + .gap_2() + .child(replace_column) + .child(replace_actions) + }); + + let mut key_context = KeyContext::new_with_defaults(); + key_context.add("BufferSearchBar"); + if in_replace { + key_context.add("in_replace"); + } let query_error_line = self.query_error.as_ref().map(|error| { Label::new(error) @@ -491,10 +397,26 @@ impl Render for BufferSearchBar { .ml_2() }); + let search_line = + h_flex() + .relative() + .child(search_line) + .when(!narrow_mode && !find_in_results, |div| { + div.child(h_flex().absolute().right_0().child(render_action_button( + "buffer-search", + IconName::Close, + true, + "Close Search Bar", + &Dismiss, + focus_handle.clone(), + ))) + .w_full() + }); v_flex() .id("buffer_search") .gap_2() .py(px(1.0)) + .w_full() .track_scroll(&self.scroll_handle) .key_context(key_context) .capture_action(cx.listener(Self::tab)) @@ -509,43 +431,26 @@ impl Render for BufferSearchBar { active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx); } })) - .when(self.supported_options(cx).replacement, |this| { + .when(replacement, |this| { this.on_action(cx.listener(Self::toggle_replace)) .when(in_replace, |this| { this.on_action(cx.listener(Self::replace_next)) .on_action(cx.listener(Self::replace_all)) }) }) - .when(self.supported_options(cx).case, |this| { + .when(case, |this| { this.on_action(cx.listener(Self::toggle_case_sensitive)) }) - .when(self.supported_options(cx).word, |this| { + .when(word, |this| { this.on_action(cx.listener(Self::toggle_whole_word)) }) - .when(self.supported_options(cx).regex, |this| { + .when(regex, |this| { this.on_action(cx.listener(Self::toggle_regex)) }) - .when(self.supported_options(cx).selection, |this| { + .when(selection, |this| { this.on_action(cx.listener(Self::toggle_selection)) }) - .child(h_flex().relative().child(search_line.w_full()).when( - !narrow_mode && !supported_options.find_in_results, - |div| { - div.child( - h_flex().absolute().right_0().child( - IconButton::new(SharedString::from("Close"), IconName::Close) - .shape(IconButtonShape::Square) - .tooltip(move |window, cx| { - Tooltip::for_action("Close Search Bar", &Dismiss, window, cx) - }) - .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { - this.dismiss(&Dismiss, window, cx) - })), - ), - ) - .w_full() - }, - )) + .child(search_line) .children(query_error_line) .children(replace_line) } @@ -792,7 +697,7 @@ impl BufferSearchBar { active_editor.search_bar_visibility_changed(false, window, cx); active_editor.toggle_filtered_search_ranges(false, window, cx); let handle = active_editor.item_focus_handle(cx); - self.focus(&handle, window, cx); + self.focus(&handle, window); } cx.emit(Event::UpdateLocation); cx.emit(ToolbarItemEvent::ChangeLocation( @@ -948,7 +853,7 @@ impl BufferSearchBar { } pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context) { - self.focus(&self.replacement_editor.focus_handle(cx), window, cx); + self.focus(&self.replacement_editor.focus_handle(cx), window); cx.notify(); } @@ -975,16 +880,6 @@ impl BufferSearchBar { self.update_matches(!updated, window, cx) } - fn render_search_option_button( - &self, - option: SearchOptions, - focus_handle: FocusHandle, - action: Action, - ) -> impl IntoElement + use { - let is_active = self.search_options.contains(option); - option.as_button(is_active, focus_handle, action) - } - pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context) { if let Some(active_editor) = self.active_searchable_item.as_ref() { let handle = active_editor.item_focus_handle(cx); @@ -1400,28 +1295,32 @@ impl BufferSearchBar { } fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { - // Search -> Replace -> Editor - let focus_handle = if self.replace_enabled && self.query_editor_focused { - self.replacement_editor.focus_handle(cx) - } else if let Some(item) = self.active_searchable_item.as_ref() { - item.item_focus_handle(cx) - } else { - return; - }; - self.focus(&focus_handle, window, cx); - cx.stop_propagation(); + self.cycle_field(Direction::Next, window, cx); } fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context) { - // Search -> Replace -> Search - let focus_handle = if self.replace_enabled && self.query_editor_focused { - self.replacement_editor.focus_handle(cx) - } else if self.replacement_editor_focused { - self.query_editor.focus_handle(cx) - } else { - return; + self.cycle_field(Direction::Prev, window, cx); + } + fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context) { + let mut handles = vec![self.query_editor.focus_handle(cx)]; + if self.replace_enabled { + handles.push(self.replacement_editor.focus_handle(cx)); + } + if let Some(item) = self.active_searchable_item.as_ref() { + handles.push(item.item_focus_handle(cx)); + } + let current_index = match handles.iter().position(|focus| focus.is_focused(window)) { + Some(index) => index, + None => return, }; - self.focus(&focus_handle, window, cx); + + let new_index = match direction { + Direction::Next => (current_index + 1) % handles.len(), + Direction::Prev if current_index == 0 => handles.len() - 1, + Direction::Prev => (current_index - 1) % handles.len(), + }; + let next_focus_handle = &handles[new_index]; + self.focus(next_focus_handle, window); cx.stop_propagation(); } @@ -1469,10 +1368,8 @@ impl BufferSearchBar { } } - fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut Context) { - cx.on_next_frame(window, |_, window, _| { - window.invalidate_character_coordinates(); - }); + fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window) { + window.invalidate_character_coordinates(); window.focus(handle); } @@ -1484,7 +1381,7 @@ impl BufferSearchBar { } else { self.query_editor.focus_handle(cx) }; - self.focus(&handle, window, cx); + self.focus(&handle, window); cx.notify(); } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 96194cdad2d5f42146c28dfdd2730f2c848cd9a2..9e8afa439269b1681e6b99d23ee95cc0e080fc33 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,20 +1,25 @@ use crate::{ BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleIncludeIgnored, - ToggleRegex, ToggleReplace, ToggleWholeWord, buffer_search::Deploy, + ToggleRegex, ToggleReplace, ToggleWholeWord, + buffer_search::Deploy, + search_bar::{ + input_base_styles, render_action_button, render_text_input, toggle_replace_button, + }, }; use anyhow::Context as _; -use collections::{HashMap, HashSet}; +use collections::HashMap; use editor::{ - Anchor, Editor, EditorElement, EditorEvent, EditorSettings, EditorStyle, MAX_TAB_TITLE_LEN, - MultiBuffer, SelectionEffects, actions::SelectAll, items::active_match_index, + Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, SelectionEffects, + actions::{Backtab, SelectAll, Tab}, + items::active_match_index, }; use futures::{StreamExt, stream::FuturesOrdered}; use gpui::{ Action, AnyElement, AnyView, App, Axis, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Global, Hsla, InteractiveElement, IntoElement, KeyContext, ParentElement, Point, - Render, SharedString, Styled, Subscription, Task, TextStyle, UpdateGlobal, WeakEntity, Window, - actions, div, + Render, SharedString, Styled, Subscription, Task, UpdateGlobal, WeakEntity, Window, actions, + div, }; use language::{Buffer, Language}; use menu::Confirm; @@ -32,7 +37,6 @@ use std::{ pin::pin, sync::Arc, }; -use theme::ThemeSettings; use ui::{ Icon, IconButton, IconButtonShape, IconName, KeyBinding, Label, LabelCommon, LabelSize, Toggleable, Tooltip, h_flex, prelude::*, utils::SearchInputWidth, v_flex, @@ -208,7 +212,7 @@ pub struct ProjectSearchView { replacement_editor: Entity, results_editor: Entity, search_options: SearchOptions, - panels_with_errors: HashSet, + panels_with_errors: HashMap, active_match_index: Option, search_id: usize, included_files_editor: Entity, @@ -218,7 +222,6 @@ pub struct ProjectSearchView { included_opened_only: bool, regex_language: Option>, _subscriptions: Vec, - query_error: Option, } #[derive(Debug, Clone)] @@ -879,7 +882,7 @@ impl ProjectSearchView { query_editor, results_editor, search_options: options, - panels_with_errors: HashSet::default(), + panels_with_errors: HashMap::default(), active_match_index: None, included_files_editor, excluded_files_editor, @@ -888,7 +891,6 @@ impl ProjectSearchView { included_opened_only: false, regex_language: None, _subscriptions: subscriptions, - query_error: None, }; this.entity_changed(window, cx); this @@ -1152,14 +1154,16 @@ impl ProjectSearchView { Ok(included_files) => { let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Include); - if should_unmark_error { + if should_unmark_error.is_some() { cx.notify(); } included_files } - Err(_e) => { - let should_mark_error = self.panels_with_errors.insert(InputPanel::Include); - if should_mark_error { + Err(e) => { + let should_mark_error = self + .panels_with_errors + .insert(InputPanel::Include, e.to_string()); + if should_mark_error.is_none() { cx.notify(); } PathMatcher::default() @@ -1174,15 +1178,17 @@ impl ProjectSearchView { Ok(excluded_files) => { let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Exclude); - if should_unmark_error { + if should_unmark_error.is_some() { cx.notify(); } excluded_files } - Err(_e) => { - let should_mark_error = self.panels_with_errors.insert(InputPanel::Exclude); - if should_mark_error { + Err(e) => { + let should_mark_error = self + .panels_with_errors + .insert(InputPanel::Exclude, e.to_string()); + if should_mark_error.is_none() { cx.notify(); } PathMatcher::default() @@ -1219,19 +1225,19 @@ impl ProjectSearchView { ) { Ok(query) => { let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query); - if should_unmark_error { + if should_unmark_error.is_some() { cx.notify(); } - self.query_error = None; Some(query) } Err(e) => { - let should_mark_error = self.panels_with_errors.insert(InputPanel::Query); - if should_mark_error { + let should_mark_error = self + .panels_with_errors + .insert(InputPanel::Query, e.to_string()); + if should_mark_error.is_none() { cx.notify(); } - self.query_error = Some(e.to_string()); None } @@ -1249,15 +1255,17 @@ impl ProjectSearchView { ) { Ok(query) => { let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query); - if should_unmark_error { + if should_unmark_error.is_some() { cx.notify(); } Some(query) } - Err(_e) => { - let should_mark_error = self.panels_with_errors.insert(InputPanel::Query); - if should_mark_error { + Err(e) => { + let should_mark_error = self + .panels_with_errors + .insert(InputPanel::Query, e.to_string()); + if should_mark_error.is_none() { cx.notify(); } @@ -1512,7 +1520,7 @@ impl ProjectSearchView { } fn border_color_for(&self, panel: InputPanel, cx: &App) -> Hsla { - if self.panels_with_errors.contains(&panel) { + if self.panels_with_errors.contains_key(&panel) { Color::Error.color(cx) } else { cx.theme().colors().border @@ -1610,16 +1618,11 @@ impl ProjectSearchBar { } } - fn tab(&mut self, _: &editor::actions::Tab, window: &mut Window, cx: &mut Context) { + fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { self.cycle_field(Direction::Next, window, cx); } - fn backtab( - &mut self, - _: &editor::actions::Backtab, - window: &mut Window, - cx: &mut Context, - ) { + fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context) { self.cycle_field(Direction::Prev, window, cx); } @@ -1634,29 +1637,22 @@ impl ProjectSearchBar { fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context) { let active_project_search = match &self.active_project_search { Some(active_project_search) => active_project_search, - - None => { - return; - } + None => return, }; active_project_search.update(cx, |project_view, cx| { - let mut views = vec![&project_view.query_editor]; + let mut views = vec![project_view.query_editor.focus_handle(cx)]; if project_view.replace_enabled { - views.push(&project_view.replacement_editor); + views.push(project_view.replacement_editor.focus_handle(cx)); } if project_view.filters_enabled { views.extend([ - &project_view.included_files_editor, - &project_view.excluded_files_editor, + project_view.included_files_editor.focus_handle(cx), + project_view.excluded_files_editor.focus_handle(cx), ]); } - let current_index = match views - .iter() - .enumerate() - .find(|(_, editor)| editor.focus_handle(cx).is_focused(window)) - { - Some((index, _)) => index, + let current_index = match views.iter().position(|focus| focus.is_focused(window)) { + Some(index) => index, None => return, }; @@ -1665,8 +1661,8 @@ impl ProjectSearchBar { Direction::Prev if current_index == 0 => views.len() - 1, Direction::Prev => (current_index - 1) % views.len(), }; - let next_focus_handle = views[new_index].focus_handle(cx); - window.focus(&next_focus_handle); + let next_focus_handle = &views[new_index]; + window.focus(next_focus_handle); cx.stop_propagation(); }); } @@ -1915,37 +1911,6 @@ impl ProjectSearchBar { }) } } - - fn render_text_input(&self, editor: &Entity, cx: &Context) -> impl IntoElement { - let (color, use_syntax) = if editor.read(cx).read_only(cx) { - (cx.theme().colors().text_disabled, false) - } else { - (cx.theme().colors().text, true) - }; - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: rems(0.875).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(1.3), - ..TextStyle::default() - }; - - let mut editor_style = EditorStyle { - background: cx.theme().colors().toolbar_background, - local_player: cx.theme().players().local(), - text: text_style, - ..EditorStyle::default() - }; - if use_syntax { - editor_style.syntax = cx.theme().syntax().clone(); - } - - EditorElement::new(editor, editor_style) - } } impl Render for ProjectSearchBar { @@ -1959,28 +1924,43 @@ impl Render for ProjectSearchBar { let container_width = window.viewport_size().width; let input_width = SearchInputWidth::calc_width(container_width); - enum BaseStyle { - SingleInput, - MultipleInputs, - } - - let input_base_styles = |base_style: BaseStyle, panel: InputPanel| { - h_flex() - .min_w_32() - .map(|div| match base_style { - BaseStyle::SingleInput => div.w(input_width), - BaseStyle::MultipleInputs => div.flex_grow(), - }) - .h_8() - .pl_2() - .pr_1() - .py_1() - .border_1() - .border_color(search.border_color_for(panel, cx)) - .rounded_lg() + let input_base_styles = |panel: InputPanel| { + input_base_styles(search.border_color_for(panel, cx), |div| match panel { + InputPanel::Query | InputPanel::Replacement => div.w(input_width), + InputPanel::Include | InputPanel::Exclude => div.flex_grow(), + }) + }; + let theme_colors = cx.theme().colors(); + let project_search = search.entity.read(cx); + let limit_reached = project_search.limit_reached; + + let color_override = match ( + project_search.no_results, + &project_search.active_query, + &project_search.last_search_query_text, + ) { + (Some(true), Some(q), Some(p)) if q.as_str() == p => Some(Color::Error), + _ => None, }; + let match_text = search + .active_match_index + .and_then(|index| { + let index = index + 1; + let match_quantity = project_search.match_ranges.len(); + if match_quantity > 0 { + debug_assert!(match_quantity >= index); + if limit_reached { + Some(format!("{index}/{match_quantity}+")) + } else { + Some(format!("{index}/{match_quantity}")) + } + } else { + None + } + }) + .unwrap_or_else(|| "0/0".to_string()); - let query_column = input_base_styles(BaseStyle::SingleInput, InputPanel::Query) + let query_column = input_base_styles(InputPanel::Query) .on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx))) .on_action(cx.listener(|this, action, window, cx| { this.previous_history_query(action, window, cx) @@ -1988,7 +1968,7 @@ impl Render for ProjectSearchBar { .on_action( cx.listener(|this, action, window, cx| this.next_history_query(action, window, cx)), ) - .child(self.render_text_input(&search.query_editor, cx)) + .child(render_text_input(&search.query_editor, color_override, cx)) .child( h_flex() .gap_1() @@ -2017,6 +1997,7 @@ impl Render for ProjectSearchBar { let mode_column = h_flex() .gap_1() + .min_w_64() .child( IconButton::new("project-search-filter-button", IconName::Filter) .shape(IconButtonShape::Square) @@ -2045,109 +2026,46 @@ impl Render for ProjectSearchBar { } }), ) - .child( - IconButton::new("project-search-toggle-replace", IconName::Replace) - .shape(IconButtonShape::Square) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_replace(&ToggleReplace, window, cx); - })) - .toggle_state( - self.active_project_search - .as_ref() - .map(|search| search.read(cx).replace_enabled) - .unwrap_or_default(), - ) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Toggle Replace", - &ToggleReplace, - &focus_handle, - window, - cx, - ) - } - }), - ); - - let limit_reached = search.entity.read(cx).limit_reached; - - let match_text = search - .active_match_index - .and_then(|index| { - let index = index + 1; - let match_quantity = search.entity.read(cx).match_ranges.len(); - if match_quantity > 0 { - debug_assert!(match_quantity >= index); - if limit_reached { - Some(format!("{index}/{match_quantity}+")) - } else { - Some(format!("{index}/{match_quantity}")) - } - } else { - None - } - }) - .unwrap_or_else(|| "0/0".to_string()); + .child(toggle_replace_button( + "project-search-toggle-replace", + focus_handle.clone(), + self.active_project_search + .as_ref() + .map(|search| search.read(cx).replace_enabled) + .unwrap_or_default(), + cx.listener(|this, _, window, cx| { + this.toggle_replace(&ToggleReplace, window, cx); + }), + )); + + let query_focus = search.query_editor.focus_handle(cx); let matches_column = h_flex() .pl_2() .ml_2() .border_l_1() - .border_color(cx.theme().colors().border_variant) - .child( - IconButton::new("project-search-prev-match", IconName::ChevronLeft) - .shape(IconButtonShape::Square) - .disabled(search.active_match_index.is_none()) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.select_match(Direction::Prev, window, cx); - }) - } - })) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Go To Previous Match", - &SelectPreviousMatch, - &focus_handle, - window, - cx, - ) - } - }), - ) - .child( - IconButton::new("project-search-next-match", IconName::ChevronRight) - .shape(IconButtonShape::Square) - .disabled(search.active_match_index.is_none()) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.select_match(Direction::Next, window, cx); - }) - } - })) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Go To Next Match", - &SelectNextMatch, - &focus_handle, - window, - cx, - ) - } - }), - ) + .border_color(theme_colors.border_variant) + .child(render_action_button( + "project-search-nav-button", + IconName::ChevronLeft, + search.active_match_index.is_some(), + "Select Previous Match", + &SelectPreviousMatch, + query_focus.clone(), + )) + .child(render_action_button( + "project-search-nav-button", + IconName::ChevronRight, + search.active_match_index.is_some(), + "Select Next Match", + &SelectNextMatch, + query_focus, + )) .child( div() .id("matches") - .ml_1() + .ml_2() + .min_w(rems_from_px(40.)) .child(Label::new(match_text).size(LabelSize::Small).color( if search.active_match_index.is_some() { Color::Default @@ -2169,63 +2087,30 @@ impl Render for ProjectSearchBar { .child(h_flex().min_w_64().child(mode_column).child(matches_column)); let replace_line = search.replace_enabled.then(|| { - let replace_column = input_base_styles(BaseStyle::SingleInput, InputPanel::Replacement) - .child(self.render_text_input(&search.replacement_editor, cx)); + let replace_column = input_base_styles(InputPanel::Replacement) + .child(render_text_input(&search.replacement_editor, None, cx)); let focus_handle = search.replacement_editor.read(cx).focus_handle(cx); - let replace_actions = - h_flex() - .min_w_64() - .gap_1() - .when(search.replace_enabled, |this| { - this.child( - IconButton::new("project-search-replace-next", IconName::ReplaceNext) - .shape(IconButtonShape::Square) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_next(&ReplaceNext, window, cx); - }) - } - })) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Replace Next Match", - &ReplaceNext, - &focus_handle, - window, - cx, - ) - } - }), - ) - .child( - IconButton::new("project-search-replace-all", IconName::ReplaceAll) - .shape(IconButtonShape::Square) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_all(&ReplaceAll, window, cx); - }) - } - })) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Replace All Matches", - &ReplaceAll, - &focus_handle, - window, - cx, - ) - } - }), - ) - }); + let replace_actions = h_flex() + .min_w_64() + .gap_1() + .child(render_action_button( + "project-search-replace-button", + IconName::ReplaceNext, + true, + "Replace Next Match", + &ReplaceNext, + focus_handle.clone(), + )) + .child(render_action_button( + "project-search-replace-button", + IconName::ReplaceAll, + true, + "Replace All Matches", + &ReplaceAll, + focus_handle, + )); h_flex() .w_full() @@ -2235,6 +2120,45 @@ impl Render for ProjectSearchBar { }); let filter_line = search.filters_enabled.then(|| { + let include = input_base_styles(InputPanel::Include) + .on_action(cx.listener(|this, action, window, cx| { + this.previous_history_query(action, window, cx) + })) + .on_action(cx.listener(|this, action, window, cx| { + this.next_history_query(action, window, cx) + })) + .child(render_text_input(&search.included_files_editor, None, cx)); + let exclude = input_base_styles(InputPanel::Exclude) + .on_action(cx.listener(|this, action, window, cx| { + this.previous_history_query(action, window, cx) + })) + .on_action(cx.listener(|this, action, window, cx| { + this.next_history_query(action, window, cx) + })) + .child(render_text_input(&search.excluded_files_editor, None, cx)); + let mode_column = h_flex() + .gap_1() + .min_w_64() + .child( + IconButton::new("project-search-opened-only", IconName::FolderSearch) + .shape(IconButtonShape::Square) + .toggle_state(self.is_opened_only_enabled(cx)) + .tooltip(Tooltip::text("Only Search Open Files")) + .on_click(cx.listener(|this, _, window, cx| { + this.toggle_opened_only(window, cx); + })), + ) + .child( + SearchOptions::INCLUDE_IGNORED.as_button( + search + .search_options + .contains(SearchOptions::INCLUDE_IGNORED), + focus_handle.clone(), + cx.listener(|this, _, window, cx| { + this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, window, cx); + }), + ), + ); h_flex() .w_full() .gap_2() @@ -2242,62 +2166,14 @@ impl Render for ProjectSearchBar { h_flex() .gap_2() .w(input_width) - .child( - input_base_styles(BaseStyle::MultipleInputs, InputPanel::Include) - .on_action(cx.listener(|this, action, window, cx| { - this.previous_history_query(action, window, cx) - })) - .on_action(cx.listener(|this, action, window, cx| { - this.next_history_query(action, window, cx) - })) - .child(self.render_text_input(&search.included_files_editor, cx)), - ) - .child( - input_base_styles(BaseStyle::MultipleInputs, InputPanel::Exclude) - .on_action(cx.listener(|this, action, window, cx| { - this.previous_history_query(action, window, cx) - })) - .on_action(cx.listener(|this, action, window, cx| { - this.next_history_query(action, window, cx) - })) - .child(self.render_text_input(&search.excluded_files_editor, cx)), - ), - ) - .child( - h_flex() - .min_w_64() - .gap_1() - .child( - IconButton::new("project-search-opened-only", IconName::FolderSearch) - .shape(IconButtonShape::Square) - .toggle_state(self.is_opened_only_enabled(cx)) - .tooltip(Tooltip::text("Only Search Open Files")) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_opened_only(window, cx); - })), - ) - .child( - SearchOptions::INCLUDE_IGNORED.as_button( - search - .search_options - .contains(SearchOptions::INCLUDE_IGNORED), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_search_option( - SearchOptions::INCLUDE_IGNORED, - window, - cx, - ); - }), - ), - ), + .child(include) + .child(exclude), ) + .child(mode_column) }); let mut key_context = KeyContext::default(); - key_context.add("ProjectSearchBar"); - if search .replacement_editor .focus_handle(cx) @@ -2306,16 +2182,33 @@ impl Render for ProjectSearchBar { key_context.add("in_replace"); } - let query_error_line = search.query_error.as_ref().map(|error| { - Label::new(error) - .size(LabelSize::Small) - .color(Color::Error) - .mt_neg_1() - .ml_2() - }); + let query_error_line = search + .panels_with_errors + .get(&InputPanel::Query) + .map(|error| { + Label::new(error) + .size(LabelSize::Small) + .color(Color::Error) + .mt_neg_1() + .ml_2() + }); + + let filter_error_line = search + .panels_with_errors + .get(&InputPanel::Include) + .or_else(|| search.panels_with_errors.get(&InputPanel::Exclude)) + .map(|error| { + Label::new(error) + .size(LabelSize::Small) + .color(Color::Error) + .mt_neg_1() + .ml_2() + }); v_flex() + .gap_2() .py(px(1.0)) + .w_full() .key_context(key_context) .on_action(cx.listener(|this, _: &ToggleFocus, window, cx| { this.move_focus_to_results(window, cx) @@ -2323,14 +2216,8 @@ impl Render for ProjectSearchBar { .on_action(cx.listener(|this, _: &ToggleFilters, window, cx| { this.toggle_filters(window, cx); })) - .capture_action(cx.listener(|this, action, window, cx| { - this.tab(action, window, cx); - cx.stop_propagation(); - })) - .capture_action(cx.listener(|this, action, window, cx| { - this.backtab(action, window, cx); - cx.stop_propagation(); - })) + .capture_action(cx.listener(Self::tab)) + .capture_action(cx.listener(Self::backtab)) .on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx))) .on_action(cx.listener(|this, action, window, cx| { this.toggle_replace(action, window, cx); @@ -2362,12 +2249,11 @@ impl Render for ProjectSearchBar { }) .on_action(cx.listener(Self::select_next_match)) .on_action(cx.listener(Self::select_prev_match)) - .gap_2() - .w_full() .child(search_line) .children(query_error_line) .children(replace_line) .children(filter_line) + .children(filter_error_line) } } diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 805664c7942470e4acab1f3364902df0aa7619c5..2805b0c62d48ff7180d3076bc984ef597f96f28a 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -1,8 +1,14 @@ -use gpui::{Action, FocusHandle, IntoElement}; +use editor::{Editor, EditorElement, EditorStyle}; +use gpui::{Action, Entity, FocusHandle, Hsla, IntoElement, TextStyle}; +use settings::Settings; +use theme::ThemeSettings; use ui::{IconButton, IconButtonShape}; use ui::{Tooltip, prelude::*}; -pub(super) fn render_nav_button( +use crate::ToggleReplace; + +pub(super) fn render_action_button( + id_prefix: &'static str, icon: ui::IconName, active: bool, tooltip: &'static str, @@ -10,7 +16,7 @@ pub(super) fn render_nav_button( focus_handle: FocusHandle, ) -> impl IntoElement { IconButton::new( - SharedString::from(format!("search-nav-button-{}", action.name())), + SharedString::from(format!("{id_prefix}-{}", action.name())), icon, ) .shape(IconButtonShape::Square) @@ -26,3 +32,74 @@ pub(super) fn render_nav_button( .tooltip(move |window, cx| Tooltip::for_action_in(tooltip, action, &focus_handle, window, cx)) .disabled(!active) } + +pub(crate) fn input_base_styles(border_color: Hsla, map: impl FnOnce(Div) -> Div) -> Div { + h_flex() + .min_w_32() + .map(map) + .h_8() + .pl_2() + .pr_1() + .py_1() + .border_1() + .border_color(border_color) + .rounded_lg() +} + +pub(crate) fn toggle_replace_button( + id: &'static str, + focus_handle: FocusHandle, + replace_enabled: bool, + on_click: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static, +) -> IconButton { + IconButton::new(id, IconName::Replace) + .shape(IconButtonShape::Square) + .style(ButtonStyle::Subtle) + .when(replace_enabled, |button| button.style(ButtonStyle::Filled)) + .on_click(on_click) + .toggle_state(replace_enabled) + .tooltip({ + move |window, cx| { + Tooltip::for_action_in("Toggle Replace", &ToggleReplace, &focus_handle, window, cx) + } + }) +} + +pub(crate) fn render_text_input( + editor: &Entity, + color_override: Option, + app: &App, +) -> impl IntoElement { + let (color, use_syntax) = if editor.read(app).read_only(app) { + (app.theme().colors().text_disabled, false) + } else { + match color_override { + Some(color_override) => (color_override.color(app), false), + None => (app.theme().colors().text, true), + } + }; + + let settings = ThemeSettings::get_global(app); + let text_style = TextStyle { + color, + font_family: settings.buffer_font.family.clone(), + font_features: settings.buffer_font.features.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), + font_size: rems(0.875).into(), + font_weight: settings.buffer_font.weight, + line_height: relative(1.3), + ..TextStyle::default() + }; + + let mut editor_style = EditorStyle { + background: app.theme().colors().toolbar_background, + local_player: app.theme().players().local(), + text: text_style, + ..EditorStyle::default() + }; + if use_syntax { + editor_style.syntax = app.theme().syntax().clone(); + } + + EditorElement::new(editor, editor_style) +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ba9e3bbb8a973a604d549269faa6d259cac681c7..ca9840419469f0da398e332f6e7c49105a662cc1 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3878,9 +3878,7 @@ impl Workspace { local, focus_changed, } => { - cx.on_next_frame(window, |_, window, _| { - window.invalidate_character_coordinates(); - }); + window.invalidate_character_coordinates(); pane.update(cx, |pane, _| { pane.track_alternate_file_items(); @@ -3921,9 +3919,7 @@ impl Workspace { } } pane::Event::Focus => { - cx.on_next_frame(window, |_, window, _| { - window.invalidate_character_coordinates(); - }); + window.invalidate_character_coordinates(); self.handle_pane_focused(pane.clone(), window, cx); } pane::Event::ZoomIn => { From a3dcc7668756f4ab6aae6d3d5b2ba9a309303723 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Fri, 15 Aug 2025 12:12:18 +0300 Subject: [PATCH 021/744] openai: Don't send reasoning_effort if it's not set (#36228) Release Notes: - N/A --- crates/open_ai/src/open_ai.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 5801f29623ce8d9c515ef3d1756a2375bfdfcf4b..8bbe8589950c29f2f364e90b83c2d8b657e1f8f7 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -257,6 +257,7 @@ pub struct Request { pub tools: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub prompt_cache_key: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, } From 4f0b00b0d9cd25798a3e20a789cf93835251d8c3 Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Fri, 15 Aug 2025 12:10:52 +0200 Subject: [PATCH 022/744] Add component NotificationFrame & CaptureAudio parts for testing (#36081) Adds component NotificationFrame. It implements a subset of MessageNotification as a Component and refactors MessageNotification to use NotificationFrame. Having some notification UI Component is nice as it allows us to easily build new types of notifications. Uses the new NotificationFrame component for CaptureAudioNotification. Adds a CaptureAudio action in the dev namespace (not meant for end-users). It records 10 seconds of audio and saves that to a wav file. Release Notes: - N/A --------- Co-authored-by: Mikayla --- .config/hakari.toml | 2 + Cargo.lock | 9 + Cargo.toml | 2 +- crates/audio/Cargo.toml | 2 +- crates/livekit_client/Cargo.toml | 2 + crates/livekit_client/src/lib.rs | 64 +++++ crates/livekit_client/src/livekit_client.rs | 2 + .../src/livekit_client/playback.rs | 57 +---- crates/livekit_client/src/record.rs | 91 ++++++++ crates/workspace/src/notifications.rs | 218 ++++++++++++------ crates/workspace/src/workspace.rs | 3 +- crates/zed/Cargo.toml | 1 + crates/zed/src/zed.rs | 122 +++++++++- 13 files changed, 448 insertions(+), 127 deletions(-) create mode 100644 crates/livekit_client/src/record.rs diff --git a/.config/hakari.toml b/.config/hakari.toml index 2050065cc2d6be2a27ec012dcd125af992793eeb..f71e97b45c8399e52c6d793e59b3fdfcc47c87f7 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -25,6 +25,8 @@ third-party = [ { name = "reqwest", version = "0.11.27" }, # build of remote_server should not include scap / its x11 dependency { name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" }, + # build of remote_server should not need to include on libalsa through rodio + { name = "rodio" }, ] [final-excludes] diff --git a/Cargo.lock b/Cargo.lock index 0bafc3c386ad8443a3e02a1e39a4cb2e2fa0e7c6..2353733dc02f46117bb28f33073a135343e306e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7883,6 +7883,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + [[package]] name = "html5ever" version = "0.27.0" @@ -9711,6 +9717,7 @@ dependencies = [ "objc", "parking_lot", "postage", + "rodio", "scap", "serde", "serde_json", @@ -13972,6 +13979,7 @@ checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183" dependencies = [ "cpal", "dasp_sample", + "hound", "num-rational", "symphonia", "tracing", @@ -20576,6 +20584,7 @@ dependencies = [ "language_tools", "languages", "libc", + "livekit_client", "log", "markdown", "markdown_preview", diff --git a/Cargo.toml b/Cargo.toml index a872cadd3966ac35857347c1f9b26b6d1eafa790..baa4ee7f4ec97995630398fa98d192a31a6210dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -363,6 +363,7 @@ remote_server = { path = "crates/remote_server" } repl = { path = "crates/repl" } reqwest_client = { path = "crates/reqwest_client" } rich_text = { path = "crates/rich_text" } +rodio = { version = "0.21.1", default-features = false } rope = { path = "crates/rope" } rpc = { path = "crates/rpc" } rules_library = { path = "crates/rules_library" } @@ -564,7 +565,6 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77 "socks", "stream", ] } -rodio = { version = "0.21.1", default-features = false } rsa = "0.9.6" runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [ "async-dispatcher-runtime", diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index f1f40ad6540eea847313efdb4dceedbd4b27f6df..5146396b92266e74aaa771c3c789894b33666874 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -18,6 +18,6 @@ collections.workspace = true derive_more.workspace = true gpui.workspace = true parking_lot.workspace = true -rodio = { workspace = true, features = ["wav", "playback", "tracing"] } +rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] } util.workspace = true workspace-hack.workspace = true diff --git a/crates/livekit_client/Cargo.toml b/crates/livekit_client/Cargo.toml index 821fd5d39006b517d264687d7fb9a25fb570d0c2..58059967b7ab509fd91209a4a0f9873bbbb6b87d 100644 --- a/crates/livekit_client/Cargo.toml +++ b/crates/livekit_client/Cargo.toml @@ -39,6 +39,8 @@ tokio-tungstenite.workspace = true util.workspace = true workspace-hack.workspace = true +rodio = { workspace = true, features = ["wav_output"] } + [target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies] libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" } livekit = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks", features = [ diff --git a/crates/livekit_client/src/lib.rs b/crates/livekit_client/src/lib.rs index 149859fdc8ecd8533332c9462a090adb5496f100..e3934410e1e59a110d634585003a97c587f80912 100644 --- a/crates/livekit_client/src/lib.rs +++ b/crates/livekit_client/src/lib.rs @@ -1,7 +1,13 @@ +use anyhow::Context as _; use collections::HashMap; mod remote_video_track_view; +use cpal::traits::HostTrait as _; pub use remote_video_track_view::{RemoteVideoTrackView, RemoteVideoTrackViewEvent}; +use rodio::DeviceTrait as _; + +mod record; +pub use record::CaptureInput; #[cfg(not(any( test, @@ -18,6 +24,8 @@ mod livekit_client; )))] pub use livekit_client::*; +// If you need proper LSP in livekit_client you've got to comment out +// the mocks and test #[cfg(any( test, feature = "test-support", @@ -168,3 +176,59 @@ pub enum RoomEvent { Reconnecting, Reconnected, } + +pub(crate) fn default_device( + input: bool, +) -> anyhow::Result<(cpal::Device, cpal::SupportedStreamConfig)> { + let device; + let config; + if input { + device = cpal::default_host() + .default_input_device() + .context("no audio input device available")?; + config = device + .default_input_config() + .context("failed to get default input config")?; + } else { + device = cpal::default_host() + .default_output_device() + .context("no audio output device available")?; + config = device + .default_output_config() + .context("failed to get default output config")?; + } + Ok((device, config)) +} + +pub(crate) fn get_sample_data( + sample_format: cpal::SampleFormat, + data: &cpal::Data, +) -> anyhow::Result> { + match sample_format { + cpal::SampleFormat::I8 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::I16 => Ok(data.as_slice::().unwrap().to_vec()), + cpal::SampleFormat::I24 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::I32 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::I64 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::U8 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::U16 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::U32 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::U64 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::F32 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::F64 => Ok(convert_sample_data::(data)), + _ => anyhow::bail!("Unsupported sample format"), + } +} + +pub(crate) fn convert_sample_data< + TSource: cpal::SizedSample, + TDest: cpal::SizedSample + cpal::FromSample, +>( + data: &cpal::Data, +) -> Vec { + data.as_slice::() + .unwrap() + .iter() + .map(|e| e.to_sample::()) + .collect() +} diff --git a/crates/livekit_client/src/livekit_client.rs b/crates/livekit_client/src/livekit_client.rs index 8f0ac1a456aea1ac32879e121961e87930035dba..adeea4f51279ece93160d604672eab3962c7a6d7 100644 --- a/crates/livekit_client/src/livekit_client.rs +++ b/crates/livekit_client/src/livekit_client.rs @@ -8,6 +8,8 @@ use gpui_tokio::Tokio; use playback::capture_local_video_track; mod playback; +#[cfg(feature = "record-microphone")] +mod record; use crate::{LocalTrack, Participant, RemoteTrack, RoomEvent, TrackPublication}; pub use playback::AudioStream; diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index f14e156125f6da815fe24aabd798e53c6c3e82b8..d1eec42f8f0df92e78ef63244c7cde400a1f19a6 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -1,7 +1,6 @@ use anyhow::{Context as _, Result}; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait as _}; -use cpal::{Data, FromSample, I24, SampleFormat, SizedSample}; +use cpal::traits::{DeviceTrait, StreamTrait as _}; use futures::channel::mpsc::UnboundedSender; use futures::{Stream, StreamExt as _}; use gpui::{ @@ -166,7 +165,7 @@ impl AudioStack { ) -> Result<()> { loop { let mut device_change_listener = DeviceChangeListener::new(false)?; - let (output_device, output_config) = default_device(false)?; + let (output_device, output_config) = crate::default_device(false)?; let (end_on_drop_tx, end_on_drop_rx) = std::sync::mpsc::channel::<()>(); let mixer = mixer.clone(); let apm = apm.clone(); @@ -238,7 +237,7 @@ impl AudioStack { ) -> Result<()> { loop { let mut device_change_listener = DeviceChangeListener::new(true)?; - let (device, config) = default_device(true)?; + let (device, config) = crate::default_device(true)?; let (end_on_drop_tx, end_on_drop_rx) = std::sync::mpsc::channel::<()>(); let apm = apm.clone(); let frame_tx = frame_tx.clone(); @@ -262,7 +261,7 @@ impl AudioStack { config.sample_format(), move |data, _: &_| { let data = - Self::get_sample_data(config.sample_format(), data).log_err(); + crate::get_sample_data(config.sample_format(), data).log_err(); let Some(data) = data else { return; }; @@ -320,33 +319,6 @@ 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; @@ -393,27 +365,6 @@ pub(crate) async fn capture_local_video_track( )) } -fn default_device(input: bool) -> Result<(cpal::Device, cpal::SupportedStreamConfig)> { - let device; - let config; - if input { - device = cpal::default_host() - .default_input_device() - .context("no audio input device available")?; - config = device - .default_input_config() - .context("failed to get default input config")?; - } else { - device = cpal::default_host() - .default_output_device() - .context("no audio output device available")?; - config = device - .default_output_config() - .context("failed to get default output config")?; - } - Ok((device, config)) -} - #[derive(Clone)] struct AudioMixerSource { ssrc: i32, diff --git a/crates/livekit_client/src/record.rs b/crates/livekit_client/src/record.rs new file mode 100644 index 0000000000000000000000000000000000000000..925c0d4c67f91bcb147a9fb8d0d99b0aa1ab1810 --- /dev/null +++ b/crates/livekit_client/src/record.rs @@ -0,0 +1,91 @@ +use std::{ + env, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, + time::Duration, +}; + +use anyhow::{Context, Result}; +use cpal::traits::{DeviceTrait, StreamTrait}; +use rodio::{buffer::SamplesBuffer, conversions::SampleTypeConverter}; +use util::ResultExt; + +pub struct CaptureInput { + pub name: String, + config: cpal::SupportedStreamConfig, + samples: Arc>>, + _stream: cpal::Stream, +} + +impl CaptureInput { + pub fn start() -> anyhow::Result { + let (device, config) = crate::default_device(true)?; + let name = device.name().unwrap_or("".to_string()); + log::info!("Using microphone: {}", name); + + let samples = Arc::new(Mutex::new(Vec::new())); + let stream = start_capture(device, config.clone(), samples.clone())?; + + Ok(Self { + name, + _stream: stream, + config, + samples, + }) + } + + pub fn finish(self) -> Result { + let name = self.name; + let mut path = env::current_dir().context("Could not get current dir")?; + path.push(&format!("test_recording_{name}.wav")); + log::info!("Test recording written to: {}", path.display()); + write_out(self.samples, self.config, &path)?; + Ok(path) + } +} + +fn start_capture( + device: cpal::Device, + config: cpal::SupportedStreamConfig, + samples: Arc>>, +) -> Result { + let stream = device + .build_input_stream_raw( + &config.config(), + config.sample_format(), + move |data, _: &_| { + let data = crate::get_sample_data(config.sample_format(), data).log_err(); + let Some(data) = data else { + return; + }; + samples + .try_lock() + .expect("Only locked after stream ends") + .extend_from_slice(&data); + }, + |err| log::error!("error capturing audio track: {:?}", err), + Some(Duration::from_millis(100)), + ) + .context("failed to build input stream")?; + + stream.play()?; + Ok(stream) +} + +fn write_out( + samples: Arc>>, + config: cpal::SupportedStreamConfig, + path: &Path, +) -> Result<()> { + let samples = std::mem::take( + &mut *samples + .try_lock() + .expect("Stream has ended, callback cant hold the lock"), + ); + let samples: Vec = SampleTypeConverter::<_, f32>::new(samples.into_iter()).collect(); + let mut samples = SamplesBuffer::new(config.channels(), config.sample_rate().0, samples); + match rodio::output_to_wav(&mut samples, path) { + Ok(_) => Ok(()), + Err(e) => Err(anyhow::anyhow!("Failed to write wav file: {}", e)), + } +} diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 96966435e1bd2630379a1d2344261be26f317be5..7d8a28b0f1fd8a07e9a47fbebeae7097c6fd3aa0 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -6,6 +6,7 @@ use gpui::{ Task, svg, }; use parking_lot::Mutex; + use std::ops::Deref; use std::sync::{Arc, LazyLock}; use std::{any::TypeId, time::Duration}; @@ -189,6 +190,7 @@ impl Workspace { cx.notify(); } + /// Hide all notifications matching the given ID pub fn suppress_notification(&mut self, id: &NotificationId, cx: &mut Context) { self.dismiss_notification(id, cx); self.suppressed_notifications.insert(id.clone()); @@ -462,16 +464,144 @@ impl EventEmitter for ErrorMessagePrompt {} impl Notification for ErrorMessagePrompt {} +#[derive(IntoElement, RegisterComponent)] +pub struct NotificationFrame { + title: Option, + show_suppress_button: bool, + show_close_button: bool, + close: Option>, + contents: Option, + suffix: Option, +} + +impl NotificationFrame { + pub fn new() -> Self { + Self { + title: None, + contents: None, + suffix: None, + show_suppress_button: true, + show_close_button: true, + close: None, + } + } + + pub fn with_title(mut self, title: Option>) -> Self { + self.title = title.map(Into::into); + self + } + + pub fn with_content(self, content: impl IntoElement) -> Self { + Self { + contents: Some(content.into_any_element()), + ..self + } + } + + /// Determines whether the given notification ID should be suppressible + /// Suppressed motifications will not be shown anymore + pub fn show_suppress_button(mut self, show: bool) -> Self { + self.show_suppress_button = show; + self + } + + pub fn show_close_button(mut self, show: bool) -> Self { + self.show_close_button = show; + self + } + + pub fn on_close(self, on_close: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { + Self { + close: Some(Box::new(on_close)), + ..self + } + } + + pub fn with_suffix(mut self, suffix: impl IntoElement) -> Self { + self.suffix = Some(suffix.into_any_element()); + self + } +} + +impl RenderOnce for NotificationFrame { + fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let entity = window.current_view(); + let show_suppress_button = self.show_suppress_button; + let suppress = show_suppress_button && window.modifiers().shift; + let (close_id, close_icon) = if suppress { + ("suppress", IconName::Minimize) + } else { + ("close", IconName::Close) + }; + + v_flex() + .occlude() + .p_3() + .gap_2() + .elevation_3(cx) + .child( + h_flex() + .gap_4() + .justify_between() + .items_start() + .child( + v_flex() + .gap_0p5() + .when_some(self.title.clone(), |div, title| { + div.child(Label::new(title)) + }) + .child(div().max_w_96().children(self.contents)), + ) + .when(self.show_close_button, |this| { + this.on_modifiers_changed(move |_, _, cx| cx.notify(entity)) + .child( + IconButton::new(close_id, close_icon) + .tooltip(move |window, cx| { + if suppress { + Tooltip::for_action( + "Suppress.\nClose with click.", + &SuppressNotification, + window, + cx, + ) + } else if show_suppress_button { + Tooltip::for_action( + "Close.\nSuppress with shift-click.", + &menu::Cancel, + window, + cx, + ) + } else { + Tooltip::for_action("Close", &menu::Cancel, window, cx) + } + }) + .on_click({ + let close = self.close.take(); + move |_, window, cx| { + if let Some(close) = &close { + close(&suppress, window, cx) + } + } + }), + ) + }), + ) + .children(self.suffix) + } +} + +impl Component for NotificationFrame {} + pub mod simple_message_notification { use std::sync::Arc; use gpui::{ - AnyElement, ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, - Render, SharedString, Styled, div, + AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render, + SharedString, Styled, }; - use ui::{Tooltip, prelude::*}; + use ui::prelude::*; - use crate::SuppressNotification; + use crate::notifications::NotificationFrame; use super::{Notification, SuppressEvent}; @@ -631,6 +761,8 @@ pub mod simple_message_notification { self } + /// Determines whether the given notification ID should be supressable + /// Suppressed motifications will not be shown anymor pub fn show_suppress_button(mut self, show: bool) -> Self { self.show_suppress_button = show; self @@ -647,71 +779,19 @@ pub mod simple_message_notification { impl Render for MessageNotification { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let show_suppress_button = self.show_suppress_button; - let suppress = show_suppress_button && window.modifiers().shift; - let (close_id, close_icon) = if suppress { - ("suppress", IconName::Minimize) - } else { - ("close", IconName::Close) - }; - - v_flex() - .occlude() - .p_3() - .gap_2() - .elevation_3(cx) - .child( - h_flex() - .gap_4() - .justify_between() - .items_start() - .child( - v_flex() - .gap_0p5() - .when_some(self.title.clone(), |element, title| { - element.child(Label::new(title)) - }) - .child(div().max_w_96().child((self.build_content)(window, cx))), - ) - .when(self.show_close_button, |this| { - this.on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify())) - .child( - IconButton::new(close_id, close_icon) - .tooltip(move |window, cx| { - if suppress { - Tooltip::for_action( - "Suppress.\nClose with click.", - &SuppressNotification, - window, - cx, - ) - } else if show_suppress_button { - Tooltip::for_action( - "Close.\nSuppress with shift-click.", - &menu::Cancel, - window, - cx, - ) - } else { - Tooltip::for_action( - "Close", - &menu::Cancel, - window, - cx, - ) - } - }) - .on_click(cx.listener(move |_, _: &ClickEvent, _, cx| { - if suppress { - cx.emit(SuppressEvent); - } else { - cx.emit(DismissEvent); - } - })), - ) - }), - ) - .child( + NotificationFrame::new() + .with_title(self.title.clone()) + .with_content((self.build_content)(window, cx)) + .show_close_button(self.show_close_button) + .show_suppress_button(self.show_suppress_button) + .on_close(cx.listener(|_, suppress, _, cx| { + if *suppress { + cx.emit(SuppressEvent); + } else { + cx.emit(DismissEvent); + } + })) + .with_suffix( h_flex() .gap_1() .children(self.primary_message.iter().map(|message| { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ca9840419469f0da398e332f6e7c49105a662cc1..3129c12dbfe93dea3270b083323c8ebada235d98 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -15,6 +15,8 @@ mod toast_layer; mod toolbar; mod workspace_settings; +pub use crate::notifications::NotificationFrame; +pub use dock::Panel; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; use anyhow::{Context as _, Result, anyhow}; @@ -24,7 +26,6 @@ use client::{ proto::{self, ErrorCode, PanelId, PeerId}, }; use collections::{HashMap, HashSet, hash_map}; -pub use dock::Panel; use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; use futures::{ Future, FutureExt, StreamExt, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 4335f2d5a1326d0a538b27c2f9adbad2354c38f4..d69efaf6c0034dc0d3091fc68e074e86454a334f 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -82,6 +82,7 @@ inspector_ui.workspace = true install_cli.workspace = true jj_ui.workspace = true journal.workspace = true +livekit_client.workspace = true language.workspace = true language_extension.workspace = true language_model.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ceda403fdd204848cde573f3fb6dec461f1c0458..84145a1be437eb7ae6f4928ac4d2087a7ce54e86 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -56,6 +56,7 @@ use settings::{ initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content, update_settings_file, }; +use std::time::{Duration, Instant}; use std::{ borrow::Cow, path::{Path, PathBuf}, @@ -69,13 +70,17 @@ use util::markdown::MarkdownString; use util::{ResultExt, asset_str}; use uuid::Uuid; use vim_mode_setting::VimModeSetting; -use workspace::notifications::{NotificationId, dismiss_app_notification, show_app_notification}; +use workspace::notifications::{ + NotificationId, SuppressEvent, dismiss_app_notification, show_app_notification, +}; use workspace::{ AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, create_and_open_local_file, notifications::simple_message_notification::MessageNotification, open_new, }; -use workspace::{CloseIntent, CloseWindow, RestoreBanner, with_active_or_new_workspace}; +use workspace::{ + CloseIntent, CloseWindow, NotificationFrame, RestoreBanner, with_active_or_new_workspace, +}; use workspace::{Pane, notifications::DetachAndPromptErr}; use zed_actions::{ OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettings, OpenZedUrl, Quit, @@ -117,6 +122,14 @@ actions!( ] ); +actions!( + dev, + [ + /// Record 10s of audio from your current microphone + CaptureAudio + ] +); + pub fn init(cx: &mut App) { #[cfg(target_os = "macos")] cx.on_action(|_: &Hide, cx| cx.hide()); @@ -897,7 +910,11 @@ fn register_actions( .detach(); } } + }) + .register_action(|workspace, _: &CaptureAudio, window, cx| { + capture_audio(workspace, window, cx); }); + if workspace.project().read(cx).is_via_ssh() { workspace.register_action({ move |workspace, _: &OpenServerSettings, window, cx| { @@ -1806,6 +1823,107 @@ fn open_settings_file( .detach_and_log_err(cx); } +fn capture_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context) { + #[derive(Default)] + enum State { + Recording(livekit_client::CaptureInput), + Failed(String), + Finished(PathBuf), + // Used during state switch. Should never occur naturally. + #[default] + Invalid, + } + + struct CaptureAudioNotification { + focus_handle: gpui::FocusHandle, + start_time: Instant, + state: State, + } + + impl gpui::EventEmitter for CaptureAudioNotification {} + impl gpui::EventEmitter for CaptureAudioNotification {} + impl gpui::Focusable for CaptureAudioNotification { + fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { + self.focus_handle.clone() + } + } + impl workspace::notifications::Notification for CaptureAudioNotification {} + + const AUDIO_RECORDING_TIME_SECS: u64 = 10; + + impl Render for CaptureAudioNotification { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let elapsed = self.start_time.elapsed().as_secs(); + let message = match &self.state { + State::Recording(capture) => format!( + "Recording {} seconds of audio from input: '{}'", + AUDIO_RECORDING_TIME_SECS - elapsed, + capture.name, + ), + State::Failed(e) => format!("Error capturing audio: {e}"), + State::Finished(path) => format!("Audio recorded to {}", path.display()), + State::Invalid => "Error invalid state".to_string(), + }; + + NotificationFrame::new() + .with_title(Some("Recording Audio")) + .show_suppress_button(false) + .on_close(cx.listener(|_, _, _, cx| { + cx.emit(DismissEvent); + })) + .with_content(message) + } + } + + impl CaptureAudioNotification { + fn finish(&mut self) { + let state = std::mem::take(&mut self.state); + self.state = if let State::Recording(capture) = state { + match capture.finish() { + Ok(path) => State::Finished(path), + Err(e) => State::Failed(e.to_string()), + } + } else { + state + }; + } + + fn new(cx: &mut Context) -> Self { + cx.spawn(async move |this, cx| { + for _ in 0..10 { + cx.background_executor().timer(Duration::from_secs(1)).await; + this.update(cx, |_, cx| { + cx.notify(); + })?; + } + + this.update(cx, |this, cx| { + this.finish(); + cx.notify(); + })?; + + anyhow::Ok(()) + }) + .detach(); + + let state = match livekit_client::CaptureInput::start() { + Ok(capture_input) => State::Recording(capture_input), + Err(err) => State::Failed(format!("Error starting audio capture: {}", err)), + }; + + Self { + focus_handle: cx.focus_handle(), + start_time: Instant::now(), + state, + } + } + } + + workspace.show_notification(NotificationId::unique::(), cx, |cx| { + cx.new(CaptureAudioNotification::new) + }); +} + #[cfg(test)] mod tests { use super::*; From d891348442f2196b248f992ef9067b3eee534f7c Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 15 Aug 2025 12:34:54 +0200 Subject: [PATCH 023/744] search: Simplify search options handling (#36233) Release Notes: - N/A --- crates/search/src/buffer_search.rs | 55 +++++------ crates/search/src/mode.rs | 36 ------- crates/search/src/project_search.rs | 71 +++++--------- crates/search/src/search.rs | 111 +++++++++++++--------- crates/search/src/search_bar.rs | 21 ---- crates/search/src/search_status_button.rs | 4 +- crates/zed/src/zed/quick_action_bar.rs | 2 +- 7 files changed, 115 insertions(+), 185 deletions(-) delete mode 100644 crates/search/src/mode.rs diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index ccef198f042de631b4a9b9e6a3550716dcd679b4..da2d35d74cae3a8bc3c183fe034af20e07826f84 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,12 +1,10 @@ mod registrar; use crate::{ - FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, - SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex, - ToggleReplace, ToggleSelection, ToggleWholeWord, - search_bar::{ - input_base_styles, render_action_button, render_text_input, toggle_replace_button, - }, + FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption, + SearchOptions, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, + ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord, + search_bar::{input_base_styles, render_action_button, render_text_input}, }; use any_vec::AnyVec; use anyhow::Context as _; @@ -215,31 +213,22 @@ impl Render for BufferSearchBar { h_flex() .gap_1() .when(case, |div| { - div.child(SearchOptions::CASE_SENSITIVE.as_button( - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_case_sensitive(&ToggleCaseSensitive, window, cx) - }), - )) + div.child( + SearchOption::CaseSensitive + .as_button(self.search_options, focus_handle.clone()), + ) }) .when(word, |div| { - div.child(SearchOptions::WHOLE_WORD.as_button( - self.search_options.contains(SearchOptions::WHOLE_WORD), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_whole_word(&ToggleWholeWord, window, cx) - }), - )) + div.child( + SearchOption::WholeWord + .as_button(self.search_options, focus_handle.clone()), + ) }) .when(regex, |div| { - div.child(SearchOptions::REGEX.as_button( - self.search_options.contains(SearchOptions::REGEX), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_regex(&ToggleRegex, window, cx) - }), - )) + div.child( + SearchOption::Regex + .as_button(self.search_options, focus_handle.clone()), + ) }), ) }); @@ -248,13 +237,13 @@ impl Render for BufferSearchBar { .gap_1() .min_w_64() .when(replacement, |this| { - this.child(toggle_replace_button( - "buffer-search-bar-toggle-replace-button", - focus_handle.clone(), + this.child(render_action_button( + "buffer-search-bar-toggle", + IconName::Replace, self.replace_enabled, - cx.listener(|this, _: &ClickEvent, window, cx| { - this.toggle_replace(&ToggleReplace, window, cx); - }), + "Toggle Replace", + &ToggleReplace, + focus_handle.clone(), )) }) .when(selection, |this| { diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs deleted file mode 100644 index 957eb707a5262b9a9ce2a3b73cd6c3336288b737..0000000000000000000000000000000000000000 --- a/crates/search/src/mode.rs +++ /dev/null @@ -1,36 +0,0 @@ -use gpui::{Action, SharedString}; - -use crate::{ActivateRegexMode, ActivateTextMode}; - -// TODO: Update the default search mode to get from config -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub enum SearchMode { - #[default] - Text, - Regex, -} - -impl SearchMode { - pub(crate) fn label(&self) -> &'static str { - match self { - SearchMode::Text => "Text", - SearchMode::Regex => "Regex", - } - } - pub(crate) fn tooltip(&self) -> SharedString { - format!("Activate {} Mode", self.label()).into() - } - pub(crate) fn action(&self) -> Box { - match self { - SearchMode::Text => ActivateTextMode.boxed_clone(), - SearchMode::Regex => ActivateRegexMode.boxed_clone(), - } - } -} - -pub(crate) fn next_mode(mode: &SearchMode) -> SearchMode { - match mode { - SearchMode::Text => SearchMode::Regex, - SearchMode::Regex => SearchMode::Text, - } -} diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 9e8afa439269b1681e6b99d23ee95cc0e080fc33..6b9777906af1791605644c64e3a72f3ddde920d6 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,11 +1,9 @@ use crate::{ BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, - SearchOptions, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleIncludeIgnored, - ToggleRegex, ToggleReplace, ToggleWholeWord, + SearchOption, SearchOptions, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, + ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord, buffer_search::Deploy, - search_bar::{ - input_base_styles, render_action_button, render_text_input, toggle_replace_button, - }, + search_bar::{input_base_styles, render_action_button, render_text_input}, }; use anyhow::Context as _; use collections::HashMap; @@ -1784,14 +1782,6 @@ impl ProjectSearchBar { } } - fn is_option_enabled(&self, option: SearchOptions, cx: &App) -> bool { - if let Some(search) = self.active_project_search.as_ref() { - search.read(cx).search_options.contains(option) - } else { - false - } - } - fn next_history_query( &mut self, _: &NextHistoryQuery, @@ -1972,27 +1962,17 @@ impl Render for ProjectSearchBar { .child( h_flex() .gap_1() - .child(SearchOptions::CASE_SENSITIVE.as_button( - self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); - }), - )) - .child(SearchOptions::WHOLE_WORD.as_button( - self.is_option_enabled(SearchOptions::WHOLE_WORD, cx), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); - }), - )) - .child(SearchOptions::REGEX.as_button( - self.is_option_enabled(SearchOptions::REGEX, cx), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_search_option(SearchOptions::REGEX, window, cx); - }), - )), + .child( + SearchOption::CaseSensitive + .as_button(search.search_options, focus_handle.clone()), + ) + .child( + SearchOption::WholeWord + .as_button(search.search_options, focus_handle.clone()), + ) + .child( + SearchOption::Regex.as_button(search.search_options, focus_handle.clone()), + ), ); let mode_column = h_flex() @@ -2026,16 +2006,16 @@ impl Render for ProjectSearchBar { } }), ) - .child(toggle_replace_button( - "project-search-toggle-replace", - focus_handle.clone(), + .child(render_action_button( + "project-search", + IconName::Replace, self.active_project_search .as_ref() .map(|search| search.read(cx).replace_enabled) .unwrap_or_default(), - cx.listener(|this, _, window, cx| { - this.toggle_replace(&ToggleReplace, window, cx); - }), + "Toggle Replace", + &ToggleReplace, + focus_handle.clone(), )); let query_focus = search.query_editor.focus_handle(cx); @@ -2149,15 +2129,8 @@ impl Render for ProjectSearchBar { })), ) .child( - SearchOptions::INCLUDE_IGNORED.as_button( - search - .search_options - .contains(SearchOptions::INCLUDE_IGNORED), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, window, cx); - }), - ), + SearchOption::IncludeIgnored + .as_button(search.search_options, focus_handle.clone()), ); h_flex() .w_full() diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 5f57bfb4b1c11c22e34f22fd029d917673a45522..89064e0a27b64e0144dce0bc1f9d8a5a06031949 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -9,6 +9,8 @@ use ui::{Tooltip, prelude::*}; use workspace::notifications::NotificationId; use workspace::{Toast, Workspace}; +pub use search_status_button::SEARCH_ICON; + pub mod buffer_search; pub mod project_search; pub(crate) mod search_bar; @@ -59,48 +61,87 @@ actions!( bitflags! { #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] pub struct SearchOptions: u8 { - const NONE = 0b000; - const WHOLE_WORD = 0b001; - const CASE_SENSITIVE = 0b010; - const INCLUDE_IGNORED = 0b100; - const REGEX = 0b1000; - const ONE_MATCH_PER_LINE = 0b100000; + const NONE = 0; + const WHOLE_WORD = 1 << SearchOption::WholeWord as u8; + const CASE_SENSITIVE = 1 << SearchOption::CaseSensitive as u8; + const INCLUDE_IGNORED = 1 << SearchOption::IncludeIgnored as u8; + const REGEX = 1 << SearchOption::Regex as u8; + const ONE_MATCH_PER_LINE = 1 << SearchOption::OneMatchPerLine as u8; /// If set, reverse direction when finding the active match - const BACKWARDS = 0b10000; + const BACKWARDS = 1 << SearchOption::Backwards as u8; } } -impl SearchOptions { +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum SearchOption { + WholeWord = 0, + CaseSensitive, + IncludeIgnored, + Regex, + OneMatchPerLine, + Backwards, +} + +impl SearchOption { + pub fn as_options(self) -> SearchOptions { + SearchOptions::from_bits(1 << self as u8).unwrap() + } + pub fn label(&self) -> &'static str { - match *self { - SearchOptions::WHOLE_WORD => "Match Whole Words", - SearchOptions::CASE_SENSITIVE => "Match Case Sensitively", - SearchOptions::INCLUDE_IGNORED => "Also search files ignored by configuration", - SearchOptions::REGEX => "Use Regular Expressions", - _ => panic!("{:?} is not a named SearchOption", self), + match self { + SearchOption::WholeWord => "Match Whole Words", + SearchOption::CaseSensitive => "Match Case Sensitively", + SearchOption::IncludeIgnored => "Also search files ignored by configuration", + SearchOption::Regex => "Use Regular Expressions", + SearchOption::OneMatchPerLine => "One Match Per Line", + SearchOption::Backwards => "Search Backwards", } } pub fn icon(&self) -> ui::IconName { - match *self { - SearchOptions::WHOLE_WORD => ui::IconName::WholeWord, - SearchOptions::CASE_SENSITIVE => ui::IconName::CaseSensitive, - SearchOptions::INCLUDE_IGNORED => ui::IconName::Sliders, - SearchOptions::REGEX => ui::IconName::Regex, - _ => panic!("{:?} is not a named SearchOption", self), + match self { + SearchOption::WholeWord => ui::IconName::WholeWord, + SearchOption::CaseSensitive => ui::IconName::CaseSensitive, + SearchOption::IncludeIgnored => ui::IconName::Sliders, + SearchOption::Regex => ui::IconName::Regex, + _ => panic!("{self:?} is not a named SearchOption"), } } - pub fn to_toggle_action(&self) -> Box { + pub fn to_toggle_action(&self) -> &'static dyn Action { match *self { - SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord), - SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive), - SearchOptions::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored), - SearchOptions::REGEX => Box::new(ToggleRegex), - _ => panic!("{:?} is not a named SearchOption", self), + SearchOption::WholeWord => &ToggleWholeWord, + SearchOption::CaseSensitive => &ToggleCaseSensitive, + SearchOption::IncludeIgnored => &ToggleIncludeIgnored, + SearchOption::Regex => &ToggleRegex, + _ => panic!("{self:?} is not a toggle action"), } } + pub fn as_button(&self, active: SearchOptions, focus_handle: FocusHandle) -> impl IntoElement { + let action = self.to_toggle_action(); + let label = self.label(); + IconButton::new(label, self.icon()) + .on_click({ + let focus_handle = focus_handle.clone(); + move |_, window, cx| { + if !focus_handle.is_focused(&window) { + window.focus(&focus_handle); + } + window.dispatch_action(action.boxed_clone(), cx) + } + }) + .style(ButtonStyle::Subtle) + .shape(IconButtonShape::Square) + .toggle_state(active.contains(self.as_options())) + .tooltip({ + move |window, cx| Tooltip::for_action_in(label, action, &focus_handle, window, cx) + }) + } +} + +impl SearchOptions { pub fn none() -> SearchOptions { SearchOptions::NONE } @@ -122,24 +163,6 @@ impl SearchOptions { options.set(SearchOptions::REGEX, settings.regex); options } - - pub fn as_button( - &self, - active: bool, - focus_handle: FocusHandle, - action: Action, - ) -> impl IntoElement + use { - IconButton::new(self.label(), self.icon()) - .on_click(action) - .style(ButtonStyle::Subtle) - .shape(IconButtonShape::Square) - .toggle_state(active) - .tooltip({ - let action = self.to_toggle_action(); - let label = self.label(); - move |window, cx| Tooltip::for_action_in(label, &*action, &focus_handle, window, cx) - }) - } } pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) { diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 2805b0c62d48ff7180d3076bc984ef597f96f28a..094ce3638ed539431a51ee0aac802453c9b79f70 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -5,8 +5,6 @@ use theme::ThemeSettings; use ui::{IconButton, IconButtonShape}; use ui::{Tooltip, prelude::*}; -use crate::ToggleReplace; - pub(super) fn render_action_button( id_prefix: &'static str, icon: ui::IconName, @@ -46,25 +44,6 @@ pub(crate) fn input_base_styles(border_color: Hsla, map: impl FnOnce(Div) -> Div .rounded_lg() } -pub(crate) fn toggle_replace_button( - id: &'static str, - focus_handle: FocusHandle, - replace_enabled: bool, - on_click: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static, -) -> IconButton { - IconButton::new(id, IconName::Replace) - .shape(IconButtonShape::Square) - .style(ButtonStyle::Subtle) - .when(replace_enabled, |button| button.style(ButtonStyle::Filled)) - .on_click(on_click) - .toggle_state(replace_enabled) - .tooltip({ - move |window, cx| { - Tooltip::for_action_in("Toggle Replace", &ToggleReplace, &focus_handle, window, cx) - } - }) -} - pub(crate) fn render_text_input( editor: &Entity, color_override: Option, diff --git a/crates/search/src/search_status_button.rs b/crates/search/src/search_status_button.rs index ff2ee1641d07a68c52e88a9686e90b2f3f40c4c5..fcf36e86fa84a96117fa9b1f257d422d0bc50978 100644 --- a/crates/search/src/search_status_button.rs +++ b/crates/search/src/search_status_button.rs @@ -3,6 +3,8 @@ use settings::Settings as _; use ui::{ButtonCommon, Clickable, Context, Render, Tooltip, Window, prelude::*}; use workspace::{ItemHandle, StatusItemView}; +pub const SEARCH_ICON: IconName = IconName::MagnifyingGlass; + pub struct SearchButton; impl SearchButton { @@ -20,7 +22,7 @@ impl Render for SearchButton { } button.child( - IconButton::new("project-search-indicator", IconName::MagnifyingGlass) + IconButton::new("project-search-indicator", SEARCH_ICON) .icon_size(IconSize::Small) .tooltip(|window, cx| { Tooltip::for_action( diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index e76bef59a38004d42cc769e574ecfc4ac4621037..2b7c38f997db4eb5d5210f5d0887dc4443be549d 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -140,7 +140,7 @@ impl Render for QuickActionBar { let search_button = editor.is_singleton(cx).then(|| { QuickActionBarButton::new( "toggle buffer search", - IconName::MagnifyingGlass, + search::SEARCH_ICON, !self.buffer_search_bar.read(cx).is_dismissed(), Box::new(buffer_search::Deploy::find()), focus_handle.clone(), From 2a57b160b03c8e8543fdae12a0c191ed1a985e54 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Fri, 15 Aug 2025 13:54:24 +0300 Subject: [PATCH 024/744] openai: Don't send prompt_cache_key for OpenAI-compatible models (#36231) Some APIs fail when they get this parameter Closes #36215 Release Notes: - Fixed OpenAI-compatible providers that don't support prompt caching and/or reasoning --- crates/language_models/src/provider/cloud.rs | 1 + crates/language_models/src/provider/open_ai.rs | 8 +++++++- crates/language_models/src/provider/open_ai_compatible.rs | 5 ++++- crates/language_models/src/provider/vercel.rs | 1 + crates/language_models/src/provider/x_ai.rs | 1 + crates/open_ai/src/open_ai.rs | 7 +++++++ crates/vercel/src/vercel.rs | 4 ++++ crates/x_ai/src/x_ai.rs | 4 ++++ 8 files changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index ff8048040e6c7967d52df79b5837a502844998a8..c1337399f993a5a9be247cec31c5b048cedbf731 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -941,6 +941,7 @@ impl LanguageModel for CloudLanguageModel { request, model.id(), model.supports_parallel_tool_calls(), + model.supports_prompt_cache_key(), None, None, ); diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 725027b2a73d4b54303b17f795d0e93526169575..eaf8d885b304ea0d6526c8e61d3a71a467f41376 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -370,6 +370,7 @@ impl LanguageModel for OpenAiLanguageModel { request, self.model.id(), self.model.supports_parallel_tool_calls(), + self.model.supports_prompt_cache_key(), self.max_output_tokens(), self.model.reasoning_effort(), ); @@ -386,6 +387,7 @@ pub fn into_open_ai( request: LanguageModelRequest, model_id: &str, supports_parallel_tool_calls: bool, + supports_prompt_cache_key: bool, max_output_tokens: Option, reasoning_effort: Option, ) -> open_ai::Request { @@ -477,7 +479,11 @@ pub fn into_open_ai( } else { None }, - prompt_cache_key: request.thread_id, + prompt_cache_key: if supports_prompt_cache_key { + request.thread_id + } else { + None + }, tools: request .tools .into_iter() diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index 6e912765cdabeadbaab743904c723b556502703f..5f546f52194d37a4ee97e59ed38e681e0ac26440 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -355,10 +355,13 @@ impl LanguageModel for OpenAiCompatibleLanguageModel { LanguageModelCompletionError, >, > { + let supports_parallel_tool_call = true; + let supports_prompt_cache_key = false; let request = into_open_ai( request, &self.model.name, - true, + supports_parallel_tool_call, + supports_prompt_cache_key, self.max_output_tokens(), None, ); diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 57a89ba4aabc63e981949ceb22e3f91de9ec3957..9f447cb68b96739b0ab2997ed25e6307c6db5ba0 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -355,6 +355,7 @@ impl LanguageModel for VercelLanguageModel { request, self.model.id(), self.model.supports_parallel_tool_calls(), + self.model.supports_prompt_cache_key(), self.max_output_tokens(), None, ); diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index 5e7190ea961d1ec4781e31d9d1a19d9673afc9c0..fed6fe92bfaac45a810f2531e934310f248f17b6 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -359,6 +359,7 @@ impl LanguageModel for XAiLanguageModel { request, self.model.id(), self.model.supports_parallel_tool_calls(), + self.model.supports_prompt_cache_key(), self.max_output_tokens(), None, ); diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 8bbe8589950c29f2f364e90b83c2d8b657e1f8f7..604e8fe6221e80661d515e6e865914dabcc2d170 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -236,6 +236,13 @@ impl Model { Self::O1 | Self::O3 | Self::O3Mini | Self::O4Mini | Model::Custom { .. } => false, } } + + /// Returns whether the given model supports the `prompt_cache_key` parameter. + /// + /// If the model does not support the parameter, do not pass it up. + pub fn supports_prompt_cache_key(&self) -> bool { + return true; + } } #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/vercel/src/vercel.rs b/crates/vercel/src/vercel.rs index 1ae22c5fefa742979eb01f57703e75f5d4546a5c..8686fda53fbb1d19090f14ff944ec0641ac16c07 100644 --- a/crates/vercel/src/vercel.rs +++ b/crates/vercel/src/vercel.rs @@ -71,4 +71,8 @@ impl Model { Model::Custom { .. } => false, } } + + pub fn supports_prompt_cache_key(&self) -> bool { + false + } } diff --git a/crates/x_ai/src/x_ai.rs b/crates/x_ai/src/x_ai.rs index ac116b2f8f610614b4d1efd380169739bbdbc9f2..23cd5b9320fafe290fe7d4f509f2aef7ef93e6ba 100644 --- a/crates/x_ai/src/x_ai.rs +++ b/crates/x_ai/src/x_ai.rs @@ -105,6 +105,10 @@ impl Model { } } + pub fn supports_prompt_cache_key(&self) -> bool { + false + } + pub fn supports_tool(&self) -> bool { match self { Self::Grok2Vision From f8b01052583d3e27fbbbf5f46eb4f5bd5ec279aa Mon Sep 17 00:00:00 2001 From: smit Date: Fri, 15 Aug 2025 16:24:54 +0530 Subject: [PATCH 025/744] project: Fix LSP TextDocumentSyncCapability dynamic registration (#36234) Closes #36213 Use `textDocument/didChange` ([docs](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_synchronization)) instead of `textDocument/synchronization`. Release Notes: - Fixed an issue where Dart projects were being formatted incorrectly by the language server. --- crates/project/src/lsp_store.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 60d847023fbcf1d0839dfd53f688a7a11eb156bb..196f55171a5949866222164e221686bbeb3598f8 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11817,14 +11817,16 @@ impl LspStore { notify_server_capabilities_updated(&server, cx); } } - "textDocument/synchronization" => { - if let Some(caps) = reg + "textDocument/didChange" => { + if let Some(sync_kind) = reg .register_options - .map(serde_json::from_value) + .and_then(|opts| opts.get("syncKind").cloned()) + .map(serde_json::from_value::) .transpose()? { server.update_capabilities(|capabilities| { - capabilities.text_document_sync = Some(caps); + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)); }); notify_server_capabilities_updated(&server, cx); } @@ -11974,7 +11976,7 @@ impl LspStore { }); notify_server_capabilities_updated(&server, cx); } - "textDocument/synchronization" => { + "textDocument/didChange" => { server.update_capabilities(|capabilities| { capabilities.text_document_sync = None; }); From 6f3cd42411c64879848d0bc96d838d3bef8c374c Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 15 Aug 2025 13:17:17 +0200 Subject: [PATCH 026/744] agent2: Port Zed AI features (#36172) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/acp_thread/src/acp_thread.rs | 173 +++++--- crates/acp_thread/src/connection.rs | 26 +- crates/agent2/src/agent.rs | 282 +++++++------ crates/agent2/src/tests/mod.rs | 202 +++++++++- crates/agent2/src/tests/test_tools.rs | 2 +- crates/agent2/src/thread.rs | 186 ++++++--- crates/agent2/src/tools/edit_file_tool.rs | 2 +- crates/agent_servers/src/acp/v0.rs | 6 +- crates/agent_servers/src/acp/v1.rs | 6 +- crates/agent_servers/src/claude.rs | 5 + crates/agent_ui/src/acp/thread_view.rs | 376 ++++++++++++++++-- crates/agent_ui/src/agent_ui.rs | 1 - crates/agent_ui/src/burn_mode_tooltip.rs | 61 --- crates/agent_ui/src/message_editor.rs | 4 +- crates/agent_ui/src/text_thread_editor.rs | 2 +- crates/agent_ui/src/ui/burn_mode_tooltip.rs | 6 +- .../language_model/src/model/cloud_model.rs | 12 + 17 files changed, 994 insertions(+), 358 deletions(-) delete mode 100644 crates/agent_ui/src/burn_mode_tooltip.rs diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 4005f27a0c9a7ca3d13ec0ae91b0170838ddeb62..4995ddb9dfbf17c50cca66244bf1f28098894dc8 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -33,13 +33,23 @@ pub struct UserMessage { pub id: Option, pub content: ContentBlock, pub chunks: Vec, - pub checkpoint: Option, + pub checkpoint: Option, +} + +#[derive(Debug)] +pub struct Checkpoint { + git_checkpoint: GitStoreCheckpoint, + pub show: bool, } impl UserMessage { fn to_markdown(&self, cx: &App) -> String { let mut markdown = String::new(); - if let Some(_) = self.checkpoint { + if self + .checkpoint + .as_ref() + .map_or(false, |checkpoint| checkpoint.show) + { writeln!(markdown, "## User (checkpoint)").unwrap(); } else { writeln!(markdown, "## User").unwrap(); @@ -1145,9 +1155,12 @@ impl AcpThread { self.project.read(cx).languages().clone(), cx, ); + let request = acp::PromptRequest { + prompt: message.clone(), + session_id: self.session_id.clone(), + }; let git_store = self.project.read(cx).git_store().clone(); - let old_checkpoint = git_store.update(cx, |git, cx| git.checkpoint(cx)); let message_id = if self .connection .session_editor(&self.session_id, cx) @@ -1161,68 +1174,63 @@ impl AcpThread { AgentThreadEntry::UserMessage(UserMessage { id: message_id.clone(), content: block, - chunks: message.clone(), + chunks: message, checkpoint: None, }), cx, ); + + self.run_turn(cx, async move |this, cx| { + let old_checkpoint = git_store + .update(cx, |git, cx| git.checkpoint(cx))? + .await + .context("failed to get old checkpoint") + .log_err(); + this.update(cx, |this, cx| { + if let Some((_ix, message)) = this.last_user_message() { + message.checkpoint = old_checkpoint.map(|git_checkpoint| Checkpoint { + git_checkpoint, + show: false, + }); + } + this.connection.prompt(message_id, request, cx) + })? + .await + }) + } + + pub fn resume(&mut self, cx: &mut Context) -> BoxFuture<'static, Result<()>> { + self.run_turn(cx, async move |this, cx| { + this.update(cx, |this, cx| { + this.connection + .resume(&this.session_id, cx) + .map(|resume| resume.run(cx)) + })? + .context("resuming a session is not supported")? + .await + }) + } + + fn run_turn( + &mut self, + cx: &mut Context, + f: impl 'static + AsyncFnOnce(WeakEntity, &mut AsyncApp) -> Result, + ) -> BoxFuture<'static, Result<()>> { self.clear_completed_plan_entries(cx); - let (old_checkpoint_tx, old_checkpoint_rx) = oneshot::channel(); let (tx, rx) = oneshot::channel(); let cancel_task = self.cancel(cx); - let request = acp::PromptRequest { - prompt: message, - session_id: self.session_id.clone(), - }; - - self.send_task = Some(cx.spawn({ - let message_id = message_id.clone(); - async move |this, cx| { - cancel_task.await; - old_checkpoint_tx.send(old_checkpoint.await).ok(); - if let Ok(result) = this.update(cx, |this, cx| { - this.connection.prompt(message_id, request, cx) - }) { - tx.send(result.await).log_err(); - } - } + self.send_task = Some(cx.spawn(async move |this, cx| { + cancel_task.await; + tx.send(f(this, cx).await).ok(); })); cx.spawn(async move |this, cx| { - let old_checkpoint = old_checkpoint_rx - .await - .map_err(|_| anyhow!("send canceled")) - .flatten() - .context("failed to get old checkpoint") - .log_err(); - let response = rx.await; - if let Some((old_checkpoint, message_id)) = old_checkpoint.zip(message_id) { - let new_checkpoint = git_store - .update(cx, |git, cx| git.checkpoint(cx))? - .await - .context("failed to get new checkpoint") - .log_err(); - if let Some(new_checkpoint) = new_checkpoint { - let equal = git_store - .update(cx, |git, cx| { - git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx) - })? - .await - .unwrap_or(true); - if !equal { - this.update(cx, |this, cx| { - if let Some((ix, message)) = this.user_message_mut(&message_id) { - message.checkpoint = Some(old_checkpoint); - cx.emit(AcpThreadEvent::EntryUpdated(ix)); - } - })?; - } - } - } + this.update(cx, |this, cx| this.update_last_checkpoint(cx))? + .await?; this.update(cx, |this, cx| { match response { @@ -1294,7 +1302,10 @@ impl AcpThread { return Task::ready(Err(anyhow!("message not found"))); }; - let checkpoint = message.checkpoint.clone(); + let checkpoint = message + .checkpoint + .as_ref() + .map(|c| c.git_checkpoint.clone()); let git_store = self.project.read(cx).git_store().clone(); cx.spawn(async move |this, cx| { @@ -1316,6 +1327,59 @@ impl AcpThread { }) } + fn update_last_checkpoint(&mut self, cx: &mut Context) -> Task> { + let git_store = self.project.read(cx).git_store().clone(); + + let old_checkpoint = if let Some((_, message)) = self.last_user_message() { + if let Some(checkpoint) = message.checkpoint.as_ref() { + checkpoint.git_checkpoint.clone() + } else { + return Task::ready(Ok(())); + } + } else { + return Task::ready(Ok(())); + }; + + let new_checkpoint = git_store.update(cx, |git, cx| git.checkpoint(cx)); + cx.spawn(async move |this, cx| { + let new_checkpoint = new_checkpoint + .await + .context("failed to get new checkpoint") + .log_err(); + if let Some(new_checkpoint) = new_checkpoint { + let equal = git_store + .update(cx, |git, cx| { + git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx) + })? + .await + .unwrap_or(true); + this.update(cx, |this, cx| { + let (ix, message) = this.last_user_message().context("no user message")?; + let checkpoint = message.checkpoint.as_mut().context("no checkpoint")?; + checkpoint.show = !equal; + cx.emit(AcpThreadEvent::EntryUpdated(ix)); + anyhow::Ok(()) + })??; + } + + Ok(()) + }) + } + + fn last_user_message(&mut self) -> Option<(usize, &mut UserMessage)> { + self.entries + .iter_mut() + .enumerate() + .rev() + .find_map(|(ix, entry)| { + if let AgentThreadEntry::UserMessage(message) = entry { + Some((ix, message)) + } else { + None + } + }) + } + fn user_message(&self, id: &UserMessageId) -> Option<&UserMessage> { self.entries.iter().find_map(|entry| { if let AgentThreadEntry::UserMessage(message) = entry { @@ -1552,6 +1616,7 @@ mod tests { use settings::SettingsStore; use smol::stream::StreamExt as _; use std::{ + any::Any, cell::RefCell, path::Path, rc::Rc, @@ -2284,6 +2349,10 @@ mod tests { _session_id: session_id.clone(), })) } + + fn into_any(self: Rc) -> Rc { + self + } } struct FakeAgentSessionEditor { diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 0f531acbde7edc30d88c7e20608aeb3b1949baf4..b2116020fb963ae6f2789ef9b37b1c997714aa63 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -4,7 +4,7 @@ use anyhow::Result; use collections::IndexMap; use gpui::{Entity, SharedString, Task}; use project::Project; -use std::{error::Error, fmt, path::Path, rc::Rc, sync::Arc}; +use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc}; use ui::{App, IconName}; use uuid::Uuid; @@ -36,6 +36,14 @@ pub trait AgentConnection { cx: &mut App, ) -> Task>; + fn resume( + &self, + _session_id: &acp::SessionId, + _cx: &mut App, + ) -> Option> { + None + } + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); fn session_editor( @@ -53,12 +61,24 @@ pub trait AgentConnection { fn model_selector(&self) -> Option> { None } + + fn into_any(self: Rc) -> Rc; +} + +impl dyn AgentConnection { + pub fn downcast(self: Rc) -> Option> { + self.into_any().downcast().ok() + } } pub trait AgentSessionEditor { fn truncate(&self, message_id: UserMessageId, cx: &mut App) -> Task>; } +pub trait AgentSessionResume { + fn run(&self, cx: &mut App) -> Task>; +} + #[derive(Debug)] pub struct AuthRequired; @@ -299,6 +319,10 @@ mod test_support { ) -> Option> { Some(Rc::new(StubAgentSessionEditor)) } + + fn into_any(self: Rc) -> Rc { + self + } } struct StubAgentSessionEditor; diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 9ac3c2d0e517b55e038b9853594d834e1b4f7ec4..358365d11faebb11cc91b67616de0fc5ff3a3ee4 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,9 +1,8 @@ -use crate::{AgentResponseEvent, Thread, templates::Templates}; use crate::{ - ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool, DiagnosticsTool, - EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, - OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, UserMessageContent, - WebSearchTool, + AgentResponseEvent, ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool, + DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, + MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool, ThinkingTool, Thread, + ToolCallAuthorization, UserMessageContent, WebSearchTool, templates::Templates, }; use acp_thread::AgentModelSelector; use agent_client_protocol as acp; @@ -11,6 +10,7 @@ use agent_settings::AgentSettings; use anyhow::{Context as _, Result, anyhow}; use collections::{HashSet, IndexMap}; use fs::Fs; +use futures::channel::mpsc; use futures::{StreamExt, future}; use gpui::{ App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, @@ -21,6 +21,7 @@ use prompt_store::{ ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext, }; use settings::update_settings_file; +use std::any::Any; use std::cell::RefCell; use std::collections::HashMap; use std::path::Path; @@ -426,9 +427,9 @@ impl NativeAgent { self.models.refresh_list(cx); for session in self.sessions.values_mut() { session.thread.update(cx, |thread, _| { - let model_id = LanguageModels::model_id(&thread.selected_model); + let model_id = LanguageModels::model_id(&thread.model()); if let Some(model) = self.models.model_from_id(&model_id) { - thread.selected_model = model.clone(); + thread.set_model(model.clone()); } }); } @@ -439,6 +440,124 @@ impl NativeAgent { #[derive(Clone)] pub struct NativeAgentConnection(pub Entity); +impl NativeAgentConnection { + pub fn thread(&self, session_id: &acp::SessionId, cx: &App) -> Option> { + self.0 + .read(cx) + .sessions + .get(session_id) + .map(|session| session.thread.clone()) + } + + fn run_turn( + &self, + session_id: acp::SessionId, + cx: &mut App, + f: impl 'static + + FnOnce( + Entity, + &mut App, + ) -> Result>>, + ) -> Task> { + let Some((thread, acp_thread)) = self.0.update(cx, |agent, _cx| { + agent + .sessions + .get_mut(&session_id) + .map(|s| (s.thread.clone(), s.acp_thread.clone())) + }) else { + return Task::ready(Err(anyhow!("Session not found"))); + }; + log::debug!("Found session for: {}", session_id); + + let mut response_stream = match f(thread, cx) { + Ok(stream) => stream, + Err(err) => return Task::ready(Err(err)), + }; + cx.spawn(async move |cx| { + // Handle response stream and forward to session.acp_thread + while let Some(result) = response_stream.next().await { + match result { + Ok(event) => { + log::trace!("Received completion event: {:?}", event); + + match event { + AgentResponseEvent::Text(text) => { + acp_thread.update(cx, |thread, cx| { + thread.push_assistant_content_block( + acp::ContentBlock::Text(acp::TextContent { + text, + annotations: None, + }), + false, + cx, + ) + })?; + } + AgentResponseEvent::Thinking(text) => { + acp_thread.update(cx, |thread, cx| { + thread.push_assistant_content_block( + acp::ContentBlock::Text(acp::TextContent { + text, + annotations: None, + }), + true, + cx, + ) + })?; + } + AgentResponseEvent::ToolCallAuthorization(ToolCallAuthorization { + tool_call, + options, + response, + }) => { + let recv = acp_thread.update(cx, |thread, cx| { + thread.request_tool_call_authorization(tool_call, options, cx) + })?; + cx.background_spawn(async move { + if let Some(option) = recv + .await + .context("authorization sender was dropped") + .log_err() + { + response + .send(option) + .map(|_| anyhow!("authorization receiver was dropped")) + .log_err(); + } + }) + .detach(); + } + AgentResponseEvent::ToolCall(tool_call) => { + acp_thread.update(cx, |thread, cx| { + thread.upsert_tool_call(tool_call, cx) + })?; + } + AgentResponseEvent::ToolCallUpdate(update) => { + acp_thread.update(cx, |thread, cx| { + thread.update_tool_call(update, cx) + })??; + } + AgentResponseEvent::Stop(stop_reason) => { + log::debug!("Assistant message complete: {:?}", stop_reason); + return Ok(acp::PromptResponse { stop_reason }); + } + } + } + Err(e) => { + log::error!("Error in model response stream: {:?}", e); + return Err(e); + } + } + } + + log::info!("Response stream completed"); + anyhow::Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + }) + } +} + impl AgentModelSelector for NativeAgentConnection { fn list_models(&self, cx: &mut App) -> Task> { log::debug!("NativeAgentConnection::list_models called"); @@ -472,7 +591,7 @@ impl AgentModelSelector for NativeAgentConnection { }; thread.update(cx, |thread, _cx| { - thread.selected_model = model.clone(); + thread.set_model(model.clone()); }); update_settings_file::( @@ -502,7 +621,7 @@ impl AgentModelSelector for NativeAgentConnection { else { return Task::ready(Err(anyhow!("Session not found"))); }; - let model = thread.read(cx).selected_model.clone(); + let model = thread.read(cx).model().clone(); let Some(provider) = LanguageModelRegistry::read_global(cx).provider(&model.provider_id()) else { return Task::ready(Err(anyhow!("Provider not found"))); @@ -644,25 +763,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection { ) -> Task> { let id = id.expect("UserMessageId is required"); let session_id = params.session_id.clone(); - let agent = self.0.clone(); log::info!("Received prompt request for session: {}", session_id); log::debug!("Prompt blocks count: {}", params.prompt.len()); - cx.spawn(async move |cx| { - // Get session - let (thread, acp_thread) = agent - .update(cx, |agent, _| { - agent - .sessions - .get_mut(&session_id) - .map(|s| (s.thread.clone(), s.acp_thread.clone())) - })? - .ok_or_else(|| { - log::error!("Session not found: {}", session_id); - anyhow::anyhow!("Session not found") - })?; - log::debug!("Found session for: {}", session_id); - + self.run_turn(session_id, cx, |thread, cx| { let content: Vec = params .prompt .into_iter() @@ -672,99 +776,27 @@ impl acp_thread::AgentConnection for NativeAgentConnection { log::debug!("Message id: {:?}", id); log::debug!("Message content: {:?}", content); - // Get model using the ModelSelector capability (always available for agent2) - // Get the selected model from the thread directly - let model = thread.read_with(cx, |thread, _| thread.selected_model.clone())?; - - // Send to thread - log::info!("Sending message to thread with model: {:?}", model.name()); - let mut response_stream = - thread.update(cx, |thread, cx| thread.send(id, content, cx))?; - - // Handle response stream and forward to session.acp_thread - while let Some(result) = response_stream.next().await { - match result { - Ok(event) => { - log::trace!("Received completion event: {:?}", event); - - match event { - AgentResponseEvent::Text(text) => { - acp_thread.update(cx, |thread, cx| { - thread.push_assistant_content_block( - acp::ContentBlock::Text(acp::TextContent { - text, - annotations: None, - }), - false, - cx, - ) - })?; - } - AgentResponseEvent::Thinking(text) => { - acp_thread.update(cx, |thread, cx| { - thread.push_assistant_content_block( - acp::ContentBlock::Text(acp::TextContent { - text, - annotations: None, - }), - true, - cx, - ) - })?; - } - AgentResponseEvent::ToolCallAuthorization(ToolCallAuthorization { - tool_call, - options, - response, - }) => { - let recv = acp_thread.update(cx, |thread, cx| { - thread.request_tool_call_authorization(tool_call, options, cx) - })?; - cx.background_spawn(async move { - if let Some(option) = recv - .await - .context("authorization sender was dropped") - .log_err() - { - response - .send(option) - .map(|_| anyhow!("authorization receiver was dropped")) - .log_err(); - } - }) - .detach(); - } - AgentResponseEvent::ToolCall(tool_call) => { - acp_thread.update(cx, |thread, cx| { - thread.upsert_tool_call(tool_call, cx) - })?; - } - AgentResponseEvent::ToolCallUpdate(update) => { - acp_thread.update(cx, |thread, cx| { - thread.update_tool_call(update, cx) - })??; - } - AgentResponseEvent::Stop(stop_reason) => { - log::debug!("Assistant message complete: {:?}", stop_reason); - return Ok(acp::PromptResponse { stop_reason }); - } - } - } - Err(e) => { - log::error!("Error in model response stream: {:?}", e); - // TODO: Consider sending an error message to the UI - break; - } - } - } - - log::info!("Response stream completed"); - anyhow::Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - }) + Ok(thread.update(cx, |thread, cx| { + log::info!( + "Sending message to thread with model: {:?}", + thread.model().name() + ); + thread.send(id, content, cx) + })) }) } + fn resume( + &self, + session_id: &acp::SessionId, + _cx: &mut App, + ) -> Option> { + Some(Rc::new(NativeAgentSessionResume { + connection: self.clone(), + session_id: session_id.clone(), + }) as _) + } + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { log::info!("Cancelling on session: {}", session_id); self.0.update(cx, |agent, cx| { @@ -786,6 +818,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .map(|session| Rc::new(NativeAgentSessionEditor(session.thread.clone())) as _) }) } + + fn into_any(self: Rc) -> Rc { + self + } } struct NativeAgentSessionEditor(Entity); @@ -796,6 +832,20 @@ impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor { } } +struct NativeAgentSessionResume { + connection: NativeAgentConnection, + session_id: acp::SessionId, +} + +impl acp_thread::AgentSessionResume for NativeAgentSessionResume { + fn run(&self, cx: &mut App) -> Task> { + self.connection + .run_turn(self.session_id.clone(), cx, |thread, cx| { + thread.update(cx, |thread, cx| thread.resume(cx)) + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -957,7 +1007,7 @@ mod tests { agent.read_with(cx, |agent, _| { let session = agent.sessions.get(&session_id).unwrap(); session.thread.read_with(cx, |thread, _| { - assert_eq!(thread.selected_model.id().0, "fake"); + assert_eq!(thread.model().id().0, "fake"); }); }); diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 1df664c0296aa3314bd4a430d0ac69660ba4887f..cf90c8f650b6b4fab8fb7321f9d966ee623c2eff 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -12,9 +12,9 @@ use gpui::{ }; use indoc::indoc; use language_model::{ - LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, - LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, Role, StopReason, - fake_provider::FakeLanguageModel, + LanguageModel, LanguageModelCompletionEvent, LanguageModelId, LanguageModelRegistry, + LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse, MessageContent, + Role, StopReason, fake_provider::FakeLanguageModel, }; use project::Project; use prompt_store::ProjectContext; @@ -394,8 +394,194 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) { assert_eq!(update.fields.status, Some(acp::ToolCallStatus::Failed)); } +#[gpui::test] +async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let events = thread.update(cx, |thread, cx| { + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["abc"], cx) + }); + cx.run_until_parked(); + let tool_use = LanguageModelToolUse { + id: "tool_id_1".into(), + name: EchoTool.name().into(), + raw_input: "{}".into(), + input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), + is_input_complete: true, + }; + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone())); + fake_model.end_last_completion_stream(); + + cx.run_until_parked(); + let completion = fake_model.pending_completions().pop().unwrap(); + let tool_result = LanguageModelToolResult { + tool_use_id: "tool_id_1".into(), + tool_name: EchoTool.name().into(), + is_error: false, + content: "def".into(), + output: Some("def".into()), + }; + assert_eq!( + completion.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["abc".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec![MessageContent::ToolUse(tool_use.clone())], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec![MessageContent::ToolResult(tool_result.clone())], + cache: false + }, + ] + ); + + // Simulate reaching tool use limit. + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate( + cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached, + )); + fake_model.end_last_completion_stream(); + let last_event = events.collect::>().await.pop().unwrap(); + assert!( + last_event + .unwrap_err() + .is::() + ); + + let events = thread.update(cx, |thread, cx| thread.resume(cx)).unwrap(); + cx.run_until_parked(); + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + completion.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["abc".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec![MessageContent::ToolUse(tool_use)], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec![MessageContent::ToolResult(tool_result)], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Continue where you left off".into()], + cache: false + } + ] + ); + + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text("Done".into())); + fake_model.end_last_completion_stream(); + events.collect::>().await; + thread.read_with(cx, |thread, _cx| { + assert_eq!( + thread.last_message().unwrap().to_markdown(), + indoc! {" + ## Assistant + + Done + "} + ) + }); + + // Ensure we error if calling resume when tool use limit was *not* reached. + let error = thread + .update(cx, |thread, cx| thread.resume(cx)) + .unwrap_err(); + assert_eq!( + error.to_string(), + "can only resume after tool use limit is reached" + ) +} + +#[gpui::test] +async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let events = thread.update(cx, |thread, cx| { + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["abc"], cx) + }); + cx.run_until_parked(); + + let tool_use = LanguageModelToolUse { + id: "tool_id_1".into(), + name: EchoTool.name().into(), + raw_input: "{}".into(), + input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), + is_input_complete: true, + }; + let tool_result = LanguageModelToolResult { + tool_use_id: "tool_id_1".into(), + tool_name: EchoTool.name().into(), + is_error: false, + content: "def".into(), + output: Some("def".into()), + }; + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone())); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate( + cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached, + )); + fake_model.end_last_completion_stream(); + let last_event = events.collect::>().await.pop().unwrap(); + assert!( + last_event + .unwrap_err() + .is::() + ); + + thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), vec!["ghi"], cx) + }); + cx.run_until_parked(); + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + completion.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["abc".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec![MessageContent::ToolUse(tool_use)], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec![MessageContent::ToolResult(tool_result)], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec!["ghi".into()], + cache: false + } + ] + ); +} + async fn expect_tool_call( - events: &mut UnboundedReceiver>, + events: &mut UnboundedReceiver>, ) -> acp::ToolCall { let event = events .next() @@ -411,7 +597,7 @@ async fn expect_tool_call( } async fn expect_tool_call_update_fields( - events: &mut UnboundedReceiver>, + events: &mut UnboundedReceiver>, ) -> acp::ToolCallUpdate { let event = events .next() @@ -429,7 +615,7 @@ async fn expect_tool_call_update_fields( } async fn next_tool_call_authorization( - events: &mut UnboundedReceiver>, + events: &mut UnboundedReceiver>, ) -> ToolCallAuthorization { loop { let event = events @@ -1007,9 +1193,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { } /// Filters out the stop events for asserting against in tests -fn stop_events( - result_events: Vec>, -) -> Vec { +fn stop_events(result_events: Vec>) -> Vec { result_events .into_iter() .filter_map(|event| match event.unwrap() { diff --git a/crates/agent2/src/tests/test_tools.rs b/crates/agent2/src/tests/test_tools.rs index 7c7b81f52fce95c9af181cd7fa03579160021518..cbff44cedfc28a0d24ea4fa12e3ac71c9135c0d8 100644 --- a/crates/agent2/src/tests/test_tools.rs +++ b/crates/agent2/src/tests/test_tools.rs @@ -7,7 +7,7 @@ use std::future; #[derive(JsonSchema, Serialize, Deserialize)] pub struct EchoToolInput { /// The text to echo. - text: String, + pub text: String, } pub struct EchoTool; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 260aaaf550c89823be40ce2c48b5a0affdf64b72..231ee92dda0525272a7729c9593443b4627cd9e3 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -2,10 +2,10 @@ use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates}; use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; use agent_client_protocol as acp; -use agent_settings::{AgentProfileId, AgentSettings}; +use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; -use cloud_llm_client::{CompletionIntent, CompletionMode}; +use cloud_llm_client::{CompletionIntent, CompletionRequestStatus}; use collections::IndexMap; use fs::Fs; use futures::{ @@ -14,10 +14,10 @@ use futures::{ }; use gpui::{App, Context, Entity, SharedString, Task}; use language_model::{ - LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, - LanguageModelProviderId, LanguageModelRequest, LanguageModelRequestMessage, - LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, - LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, + LanguageModel, LanguageModelCompletionEvent, LanguageModelImage, LanguageModelProviderId, + LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, + LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, + LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, }; use project::Project; use prompt_store::ProjectContext; @@ -33,6 +33,7 @@ use util::{ResultExt, markdown::MarkdownCodeBlock}; pub enum Message { User(UserMessage), Agent(AgentMessage), + Resume, } impl Message { @@ -47,6 +48,7 @@ impl Message { match self { Message::User(message) => message.to_markdown(), Message::Agent(message) => message.to_markdown(), + Message::Resume => "[resumed after tool use limit was reached]".into(), } } } @@ -320,7 +322,11 @@ impl AgentMessage { } pub fn to_request(&self) -> Vec { - let mut content = Vec::with_capacity(self.content.len()); + let mut assistant_message = LanguageModelRequestMessage { + role: Role::Assistant, + content: Vec::with_capacity(self.content.len()), + cache: false, + }; for chunk in &self.content { let chunk = match chunk { AgentMessageContent::Text(text) => { @@ -342,29 +348,30 @@ impl AgentMessage { language_model::MessageContent::Image(value.clone()) } }; - content.push(chunk); + assistant_message.content.push(chunk); } - let mut messages = vec![LanguageModelRequestMessage { - role: Role::Assistant, - content, + let mut user_message = LanguageModelRequestMessage { + role: Role::User, + content: Vec::new(), cache: false, - }]; + }; - if !self.tool_results.is_empty() { - let mut tool_results = Vec::with_capacity(self.tool_results.len()); - for tool_result in self.tool_results.values() { - tool_results.push(language_model::MessageContent::ToolResult( + for tool_result in self.tool_results.values() { + user_message + .content + .push(language_model::MessageContent::ToolResult( tool_result.clone(), )); - } - messages.push(LanguageModelRequestMessage { - role: Role::User, - content: tool_results, - cache: false, - }); } + let mut messages = Vec::new(); + if !assistant_message.content.is_empty() { + messages.push(assistant_message); + } + if !user_message.content.is_empty() { + messages.push(user_message); + } messages } } @@ -413,11 +420,12 @@ pub struct Thread { running_turn: Option>, pending_message: Option, tools: BTreeMap>, + tool_use_limit_reached: bool, context_server_registry: Entity, profile_id: AgentProfileId, project_context: Rc>, templates: Arc, - pub selected_model: Arc, + model: Arc, project: Entity, action_log: Entity, } @@ -429,7 +437,7 @@ impl Thread { context_server_registry: Entity, action_log: Entity, templates: Arc, - default_model: Arc, + model: Arc, cx: &mut Context, ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); @@ -439,11 +447,12 @@ impl Thread { running_turn: None, pending_message: None, tools: BTreeMap::default(), + tool_use_limit_reached: false, context_server_registry, profile_id, project_context, templates, - selected_model: default_model, + model, project, action_log, } @@ -457,7 +466,19 @@ impl Thread { &self.action_log } - pub fn set_mode(&mut self, mode: CompletionMode) { + pub fn model(&self) -> &Arc { + &self.model + } + + pub fn set_model(&mut self, model: Arc) { + self.model = model; + } + + pub fn completion_mode(&self) -> CompletionMode { + self.completion_mode + } + + pub fn set_completion_mode(&mut self, mode: CompletionMode) { self.completion_mode = mode; } @@ -499,36 +520,59 @@ impl Thread { Ok(()) } + pub fn resume( + &mut self, + cx: &mut Context, + ) -> Result>> { + anyhow::ensure!( + self.tool_use_limit_reached, + "can only resume after tool use limit is reached" + ); + + self.messages.push(Message::Resume); + cx.notify(); + + log::info!("Total messages in thread: {}", self.messages.len()); + Ok(self.run_turn(cx)) + } + /// Sending a message results in the model streaming a response, which could include tool calls. /// After calling tools, the model will stops and waits for any outstanding tool calls to be completed and their results sent. /// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn. pub fn send( &mut self, - message_id: UserMessageId, + id: UserMessageId, content: impl IntoIterator, cx: &mut Context, - ) -> mpsc::UnboundedReceiver> + ) -> mpsc::UnboundedReceiver> where T: Into, { - let model = self.selected_model.clone(); + log::info!("Thread::send called with model: {:?}", self.model.name()); + let content = content.into_iter().map(Into::into).collect::>(); - log::info!("Thread::send called with model: {:?}", model.name()); log::debug!("Thread::send content: {:?}", content); + self.messages + .push(Message::User(UserMessage { id, content })); cx.notify(); - let (events_tx, events_rx) = - mpsc::unbounded::>(); - let event_stream = AgentResponseEventStream(events_tx); - self.messages.push(Message::User(UserMessage { - id: message_id.clone(), - content, - })); log::info!("Total messages in thread: {}", self.messages.len()); + self.run_turn(cx) + } + + fn run_turn( + &mut self, + cx: &mut Context, + ) -> mpsc::UnboundedReceiver> { + let model = self.model.clone(); + let (events_tx, events_rx) = mpsc::unbounded::>(); + let event_stream = AgentResponseEventStream(events_tx); + let message_ix = self.messages.len().saturating_sub(1); + self.tool_use_limit_reached = false; self.running_turn = Some(cx.spawn(async move |this, cx| { log::info!("Starting agent turn execution"); - let turn_result = async { + let turn_result: Result<()> = async { let mut completion_intent = CompletionIntent::UserPrompt; loop { log::debug!( @@ -543,13 +587,22 @@ impl Thread { let mut events = model.stream_completion(request, cx).await?; log::debug!("Stream completion started successfully"); + let mut tool_use_limit_reached = false; let mut tool_uses = FuturesUnordered::new(); while let Some(event) = events.next().await { match event? { + LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::ToolUseLimitReached, + ) => { + tool_use_limit_reached = true; + } LanguageModelCompletionEvent::Stop(reason) => { event_stream.send_stop(reason); if reason == StopReason::Refusal { - this.update(cx, |this, _cx| this.truncate(message_id))??; + this.update(cx, |this, _cx| { + this.flush_pending_message(); + this.messages.truncate(message_ix); + })?; return Ok(()); } } @@ -567,12 +620,7 @@ impl Thread { } } - if tool_uses.is_empty() { - log::info!("No tool uses found, completing turn"); - return Ok(()); - } - log::info!("Found {} tool uses to execute", tool_uses.len()); - + let used_tools = tool_uses.is_empty(); while let Some(tool_result) = tool_uses.next().await { log::info!("Tool finished {:?}", tool_result); @@ -596,8 +644,17 @@ impl Thread { .ok(); } - this.update(cx, |this, _| this.flush_pending_message())?; - completion_intent = CompletionIntent::ToolResults; + if tool_use_limit_reached { + log::info!("Tool use limit reached, completing turn"); + this.update(cx, |this, _cx| this.tool_use_limit_reached = true)?; + return Err(language_model::ToolUseLimitReachedError.into()); + } else if used_tools { + log::info!("No tool uses found, completing turn"); + return Ok(()); + } else { + this.update(cx, |this, _| this.flush_pending_message())?; + completion_intent = CompletionIntent::ToolResults; + } } } .await; @@ -678,10 +735,10 @@ impl Thread { fn handle_text_event( &mut self, new_text: String, - events_stream: &AgentResponseEventStream, + event_stream: &AgentResponseEventStream, cx: &mut Context, ) { - events_stream.send_text(&new_text); + event_stream.send_text(&new_text); let last_message = self.pending_message(); if let Some(AgentMessageContent::Text(text)) = last_message.content.last_mut() { @@ -798,8 +855,9 @@ impl Thread { status: Some(acp::ToolCallStatus::InProgress), ..Default::default() }); - let supports_images = self.selected_model.supports_images(); + let supports_images = self.model.supports_images(); let tool_result = tool.run(tool_use.input, tool_event_stream, cx); + log::info!("Running tool {}", tool_use.name); Some(cx.foreground_executor().spawn(async move { let tool_result = tool_result.await.and_then(|output| { if let LanguageModelToolResultContent::Image(_) = &output.llm_output { @@ -902,7 +960,7 @@ impl Thread { name: tool_name, description: tool.description().to_string(), input_schema: tool - .input_schema(self.selected_model.tool_input_format()) + .input_schema(self.model.tool_input_format()) .log_err()?, }) }) @@ -917,7 +975,7 @@ impl Thread { thread_id: None, prompt_id: None, intent: Some(completion_intent), - mode: Some(self.completion_mode), + mode: Some(self.completion_mode.into()), messages, tools, tool_choice: None, @@ -935,7 +993,7 @@ impl Thread { .profiles .get(&self.profile_id) .context("profile not found")?; - let provider_id = self.selected_model.provider_id(); + let provider_id = self.model.provider_id(); Ok(self .tools @@ -971,6 +1029,11 @@ impl Thread { match message { Message::User(message) => messages.push(message.to_request()), Message::Agent(message) => messages.extend(message.to_request()), + Message::Resume => messages.push(LanguageModelRequestMessage { + role: Role::User, + content: vec!["Continue where you left off".into()], + cache: false, + }), } } @@ -1123,9 +1186,7 @@ where } #[derive(Clone)] -struct AgentResponseEventStream( - mpsc::UnboundedSender>, -); +struct AgentResponseEventStream(mpsc::UnboundedSender>); impl AgentResponseEventStream { fn send_text(&self, text: &str) { @@ -1212,8 +1273,8 @@ impl AgentResponseEventStream { } } - fn send_error(&self, error: LanguageModelCompletionError) { - self.0.unbounded_send(Err(error)).ok(); + fn send_error(&self, error: impl Into) { + self.0.unbounded_send(Err(error.into())).ok(); } } @@ -1229,8 +1290,7 @@ pub struct ToolCallEventStream { impl ToolCallEventStream { #[cfg(test)] pub fn test() -> (Self, ToolCallEventStreamReceiver) { - let (events_tx, events_rx) = - mpsc::unbounded::>(); + let (events_tx, events_rx) = mpsc::unbounded::>(); let stream = ToolCallEventStream::new( &LanguageModelToolUse { @@ -1351,9 +1411,7 @@ impl ToolCallEventStream { } #[cfg(test)] -pub struct ToolCallEventStreamReceiver( - mpsc::UnboundedReceiver>, -); +pub struct ToolCallEventStreamReceiver(mpsc::UnboundedReceiver>); #[cfg(test)] impl ToolCallEventStreamReceiver { @@ -1381,7 +1439,7 @@ impl ToolCallEventStreamReceiver { #[cfg(test)] impl std::ops::Deref for ToolCallEventStreamReceiver { - type Target = mpsc::UnboundedReceiver>; + type Target = mpsc::UnboundedReceiver>; fn deref(&self) -> &Self::Target { &self.0 diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 405afb585f5e9acfabfd632f0374729aa7e5c5af..c77b9f6a69bededaa632333b40c85d73bf4e8a92 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -241,7 +241,7 @@ impl AgentTool for EditFileTool { thread.build_completion_request(CompletionIntent::ToolResults, cx) }); let thread = self.thread.read(cx); - let model = thread.selected_model.clone(); + let model = thread.model().clone(); let action_log = thread.action_log().clone(); let authorize = self.authorize(&input, &event_stream, cx); diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 15f8635cdef7a2d8431eee3169ae11eb3d817fc1..e936c87643f652c92eda84dc67240612ba3b837f 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -5,7 +5,7 @@ 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, path::Path, rc::Rc}; +use std::{any::Any, cell::RefCell, path::Path, rc::Rc}; use ui::App; use util::ResultExt as _; @@ -507,4 +507,8 @@ impl AgentConnection for AcpConnection { }) .detach_and_log_err(cx) } + + fn into_any(self: Rc) -> Rc { + self + } } diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index d93e3d023e336cd27cd8a98286413ed099b16e60..36511e4644603de957efd1dd939d8915a5fc873d 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -3,9 +3,9 @@ use anyhow::anyhow; use collections::HashMap; use futures::channel::oneshot; use project::Project; -use std::cell::RefCell; use std::path::Path; use std::rc::Rc; +use std::{any::Any, cell::RefCell}; use anyhow::{Context as _, Result}; use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; @@ -191,6 +191,10 @@ impl AgentConnection for AcpConnection { .spawn(async move { conn.cancel(params).await }) .detach(); } + + fn into_any(self: Rc) -> Rc { + self + } } struct ClientDelegate { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index dbcda00e488c3737abcefde4f037749662f1f489..e1cc70928960d6da54f3f3d577128c1d44facf62 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -6,6 +6,7 @@ use context_server::listener::McpServerTool; use project::Project; use settings::SettingsStore; use smol::process::Child; +use std::any::Any; use std::cell::RefCell; use std::fmt::Display; use std::path::Path; @@ -289,6 +290,10 @@ impl AgentConnection for ClaudeAgentConnection { }) .log_err(); } + + fn into_any(self: Rc) -> Rc { + self + } } #[derive(Clone, Copy)] diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index ee016b750357df2f3dea96a3810f6a607cea1925..87af75f04660eb20de6460a1993479b0f477f12b 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -7,20 +7,21 @@ use action_log::ActionLog; use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::{self as acp}; use agent_servers::AgentServer; -use agent_settings::{AgentSettings, NotifyWhenAgentWaiting}; +use agent_settings::{AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; use anyhow::bail; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; +use client::zed_urls; use collections::{HashMap, HashSet}; use editor::scroll::Autoscroll; use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; use gpui::{ - Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, EdgesRefinement, Empty, Entity, - FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay, - SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, - Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, - linear_gradient, list, percentage, point, prelude::*, pulsating_between, + Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, ClipboardItem, EdgesRefinement, + Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, + PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, + TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, + linear_color_stop, linear_gradient, list, percentage, point, prelude::*, pulsating_between, }; use language::Buffer; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; @@ -32,8 +33,8 @@ use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration}; use text::Anchor; use theme::ThemeSettings; use ui::{ - Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, - Tooltip, prelude::*, + Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, + Scrollbar, ScrollbarState, Tooltip, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -44,16 +45,39 @@ use super::entry_view_state::EntryViewState; use crate::acp::AcpModelSelectorPopover; use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; -use crate::ui::{AgentNotification, AgentNotificationEvent}; +use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip}; use crate::{ - AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll, + AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow, + KeepAll, OpenAgentDiff, RejectAll, ToggleBurnMode, }; const RESPONSE_PADDING_X: Pixels = px(19.); - pub const MIN_EDITOR_LINES: usize = 4; pub const MAX_EDITOR_LINES: usize = 8; +enum ThreadError { + PaymentRequired, + ModelRequestLimitReached(cloud_llm_client::Plan), + ToolUseLimitReached, + Other(SharedString), +} + +impl ThreadError { + fn from_err(error: anyhow::Error) -> Self { + if error.is::() { + Self::PaymentRequired + } else if error.is::() { + Self::ToolUseLimitReached + } else if let Some(error) = + error.downcast_ref::() + { + Self::ModelRequestLimitReached(error.plan) + } else { + Self::Other(error.to_string().into()) + } + } +} + pub struct AcpThreadView { agent: Rc, workspace: WeakEntity, @@ -66,7 +90,7 @@ pub struct AcpThreadView { model_selector: Option>, notifications: Vec>, notification_subscriptions: HashMap, Vec>, - last_error: Option>, + thread_error: Option, list_state: ListState, scrollbar_state: ScrollbarState, auth_task: Option>, @@ -151,7 +175,7 @@ impl AcpThreadView { entry_view_state: EntryViewState::default(), list_state: list_state.clone(), scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), - last_error: None, + thread_error: None, auth_task: None, expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), @@ -316,7 +340,7 @@ impl AcpThreadView { } pub fn cancel_generation(&mut self, cx: &mut Context) { - self.last_error.take(); + self.thread_error.take(); if let Some(thread) = self.thread() { self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx))); @@ -371,6 +395,25 @@ impl AcpThreadView { } } + fn resume_chat(&mut self, cx: &mut Context) { + self.thread_error.take(); + let Some(thread) = self.thread() else { + return; + }; + + let task = thread.update(cx, |thread, cx| thread.resume(cx)); + cx.spawn(async move |this, cx| { + let result = task.await; + + this.update(cx, |this, cx| { + if let Err(err) = result { + this.handle_thread_error(err, cx); + } + }) + }) + .detach(); + } + fn send(&mut self, window: &mut Window, cx: &mut Context) { let contents = self .message_editor @@ -384,7 +427,7 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) { - self.last_error.take(); + self.thread_error.take(); self.editing_message.take(); let Some(thread) = self.thread().cloned() else { @@ -409,11 +452,9 @@ impl AcpThreadView { }); cx.spawn(async move |this, cx| { - if let Err(e) = task.await { + if let Err(err) = task.await { this.update(cx, |this, cx| { - this.last_error = - Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx))); - cx.notify() + this.handle_thread_error(err, cx); }) .ok(); } @@ -476,6 +517,16 @@ impl AcpThreadView { }) } + fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context) { + self.thread_error = Some(ThreadError::from_err(error)); + cx.notify(); + } + + fn clear_thread_error(&mut self, cx: &mut Context) { + self.thread_error = None; + cx.notify(); + } + fn handle_thread_event( &mut self, thread: &Entity, @@ -551,7 +602,7 @@ impl AcpThreadView { return; }; - self.last_error.take(); + self.thread_error.take(); let authenticate = connection.authenticate(method, cx); self.auth_task = Some(cx.spawn_in(window, { let project = self.project.clone(); @@ -561,9 +612,7 @@ impl AcpThreadView { this.update_in(cx, |this, window, cx| { if let Err(err) = result { - this.last_error = Some(cx.new(|cx| { - Markdown::new(format!("Error: {err}").into(), None, None, cx) - })) + this.handle_thread_error(err, cx); } else { this.thread_state = Self::initial_state( agent, @@ -620,9 +669,7 @@ impl AcpThreadView { .py_4() .px_2() .children(message.id.clone().and_then(|message_id| { - message.checkpoint.as_ref()?; - - Some( + message.checkpoint.as_ref()?.show.then(|| { Button::new("restore-checkpoint", "Restore Checkpoint") .icon(IconName::Undo) .icon_size(IconSize::XSmall) @@ -630,8 +677,8 @@ impl AcpThreadView { .label_size(LabelSize::XSmall) .on_click(cx.listener(move |this, _, _window, cx| { this.rewind(&message_id, cx); - })), - ) + })) + }) })) .child( v_flex() @@ -2322,7 +2369,12 @@ impl AcpThreadView { h_flex() .flex_none() .justify_between() - .child(self.render_follow_toggle(cx)) + .child( + h_flex() + .gap_1() + .child(self.render_follow_toggle(cx)) + .children(self.render_burn_mode_toggle(cx)), + ) .child( h_flex() .gap_1() @@ -2333,6 +2385,68 @@ impl AcpThreadView { .into_any() } + fn as_native_connection(&self, cx: &App) -> Option> { + let acp_thread = self.thread()?.read(cx); + acp_thread.connection().clone().downcast() + } + + fn as_native_thread(&self, cx: &App) -> Option> { + let acp_thread = self.thread()?.read(cx); + self.as_native_connection(cx)? + .thread(acp_thread.session_id(), cx) + } + + fn toggle_burn_mode( + &mut self, + _: &ToggleBurnMode, + _window: &mut Window, + cx: &mut Context, + ) { + let Some(thread) = self.as_native_thread(cx) else { + return; + }; + + thread.update(cx, |thread, _cx| { + let current_mode = thread.completion_mode(); + thread.set_completion_mode(match current_mode { + CompletionMode::Burn => CompletionMode::Normal, + CompletionMode::Normal => CompletionMode::Burn, + }); + }); + } + + fn render_burn_mode_toggle(&self, cx: &mut Context) -> Option { + let thread = self.as_native_thread(cx)?.read(cx); + + if !thread.model().supports_burn_mode() { + return None; + } + + let active_completion_mode = thread.completion_mode(); + let burn_mode_enabled = active_completion_mode == CompletionMode::Burn; + let icon = if burn_mode_enabled { + IconName::ZedBurnModeOn + } else { + IconName::ZedBurnMode + }; + + Some( + IconButton::new("burn-mode", icon) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .toggle_state(burn_mode_enabled) + .selected_icon_color(Color::Error) + .on_click(cx.listener(|this, _event, window, cx| { + this.toggle_burn_mode(&ToggleBurnMode, window, cx); + })) + .tooltip(move |_window, cx| { + cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled)) + .into() + }) + .into_any_element(), + ) + } + fn start_editing_message(&mut self, index: usize, window: &mut Window, cx: &mut Context) { let Some(thread) = self.thread() else { return; @@ -3002,6 +3116,187 @@ impl AcpThreadView { } } +impl AcpThreadView { + fn render_thread_error(&self, window: &mut Window, cx: &mut Context<'_, Self>) -> Option
{ + let content = match self.thread_error.as_ref()? { + ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx), + ThreadError::PaymentRequired => self.render_payment_required_error(cx), + ThreadError::ModelRequestLimitReached(plan) => { + self.render_model_request_limit_reached_error(*plan, cx) + } + ThreadError::ToolUseLimitReached => { + self.render_tool_use_limit_reached_error(window, cx)? + } + }; + + Some( + div() + .border_t_1() + .border_color(cx.theme().colors().border) + .child(content), + ) + } + + fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout { + let icon = Icon::new(IconName::XCircle) + .size(IconSize::Small) + .color(Color::Error); + + Callout::new() + .icon(icon) + .title("Error") + .description(error.clone()) + .secondary_action(self.create_copy_button(error.to_string())) + .primary_action(self.dismiss_error_button(cx)) + .bg_color(self.error_callout_bg(cx)) + } + + fn render_payment_required_error(&self, cx: &mut Context) -> Callout { + const ERROR_MESSAGE: &str = + "You reached your free usage limit. Upgrade to Zed Pro for more prompts."; + + let icon = Icon::new(IconName::XCircle) + .size(IconSize::Small) + .color(Color::Error); + + Callout::new() + .icon(icon) + .title("Free Usage Exceeded") + .description(ERROR_MESSAGE) + .tertiary_action(self.upgrade_button(cx)) + .secondary_action(self.create_copy_button(ERROR_MESSAGE)) + .primary_action(self.dismiss_error_button(cx)) + .bg_color(self.error_callout_bg(cx)) + } + + fn render_model_request_limit_reached_error( + &self, + plan: cloud_llm_client::Plan, + cx: &mut Context, + ) -> Callout { + let error_message = match plan { + cloud_llm_client::Plan::ZedPro => "Upgrade to usage-based billing for more prompts.", + cloud_llm_client::Plan::ZedProTrial | cloud_llm_client::Plan::ZedFree => { + "Upgrade to Zed Pro for more prompts." + } + }; + + let icon = Icon::new(IconName::XCircle) + .size(IconSize::Small) + .color(Color::Error); + + Callout::new() + .icon(icon) + .title("Model Prompt Limit Reached") + .description(error_message) + .tertiary_action(self.upgrade_button(cx)) + .secondary_action(self.create_copy_button(error_message)) + .primary_action(self.dismiss_error_button(cx)) + .bg_color(self.error_callout_bg(cx)) + } + + fn render_tool_use_limit_reached_error( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Option { + let thread = self.as_native_thread(cx)?; + let supports_burn_mode = thread.read(cx).model().supports_burn_mode(); + + let focus_handle = self.focus_handle(cx); + + let icon = Icon::new(IconName::Info) + .size(IconSize::Small) + .color(Color::Info); + + Some( + Callout::new() + .icon(icon) + .title("Consecutive tool use limit reached.") + .when(supports_burn_mode, |this| { + this.secondary_action( + Button::new("continue-burn-mode", "Continue with Burn Mode") + .style(ButtonStyle::Filled) + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .layer(ElevationIndex::ModalSurface) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &ContinueWithBurnMode, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use.")) + .on_click({ + cx.listener(move |this, _, _window, cx| { + thread.update(cx, |thread, _cx| { + thread.set_completion_mode(CompletionMode::Burn); + }); + this.resume_chat(cx); + }) + }), + ) + }) + .primary_action( + Button::new("continue-conversation", "Continue") + .layer(ElevationIndex::ModalSurface) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in(&ContinueThread, &focus_handle, window, cx) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .on_click(cx.listener(|this, _, _window, cx| { + this.resume_chat(cx); + })), + ), + ) + } + + fn create_copy_button(&self, message: impl Into) -> impl IntoElement { + let message = message.into(); + + IconButton::new("copy", IconName::Copy) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .tooltip(Tooltip::text("Copy Error Message")) + .on_click(move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new_string(message.clone())) + }) + } + + fn dismiss_error_button(&self, cx: &mut Context) -> impl IntoElement { + IconButton::new("dismiss", IconName::Close) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .tooltip(Tooltip::text("Dismiss Error")) + .on_click(cx.listener({ + move |this, _, _, cx| { + this.clear_thread_error(cx); + cx.notify(); + } + })) + } + + fn upgrade_button(&self, cx: &mut Context) -> impl IntoElement { + Button::new("upgrade", "Upgrade") + .label_size(LabelSize::Small) + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click(cx.listener({ + move |this, _, _, cx| { + this.clear_thread_error(cx); + cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)); + } + })) + } + + fn error_callout_bg(&self, cx: &Context) -> Hsla { + cx.theme().status().error.opacity(0.08) + } +} + impl Focusable for AcpThreadView { fn focus_handle(&self, cx: &App) -> FocusHandle { self.message_editor.focus_handle(cx) @@ -3016,6 +3311,7 @@ impl Render for AcpThreadView { .size_full() .key_context("AcpThread") .on_action(cx.listener(Self::open_agent_diff)) + .on_action(cx.listener(Self::toggle_burn_mode)) .bg(cx.theme().colors().panel_background) .child(match &self.thread_state { ThreadState::Unauthenticated { connection } => v_flex() @@ -3100,19 +3396,7 @@ impl Render for AcpThreadView { } _ => this, }) - .when_some(self.last_error.clone(), |el, error| { - el.child( - div() - .p_2() - .text_xs() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().status().error_background) - .child( - self.render_markdown(error, default_markdown_style(false, window, cx)), - ), - ) - }) + .children(self.render_thread_error(window, cx)) .child(self.render_message_editor(window, cx)) } } @@ -3299,8 +3583,6 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { #[cfg(test)] pub(crate) mod tests { - use std::path::Path; - use acp_thread::StubAgentConnection; use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::SessionId; @@ -3310,6 +3592,8 @@ pub(crate) mod tests { use project::Project; use serde_json::json; use settings::SettingsStore; + use std::any::Any; + use std::path::Path; use super::*; @@ -3547,6 +3831,10 @@ pub(crate) mod tests { fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { unimplemented!() } + + fn into_any(self: Rc) -> Rc { + self + } } pub(crate) fn init_test(cx: &mut TestAppContext) { diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 231b9cfb38a8c6e8ce4dc102fd14e06703b3e1c5..4f5f02259372bf095a6bb7d2329b69815ccb1184 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -5,7 +5,6 @@ mod agent_diff; mod agent_model_selector; mod agent_panel; mod buffer_codegen; -mod burn_mode_tooltip; mod context_picker; mod context_server_configuration; mod context_strip; diff --git a/crates/agent_ui/src/burn_mode_tooltip.rs b/crates/agent_ui/src/burn_mode_tooltip.rs deleted file mode 100644 index 6354c07760f5aa0261b69e8dd08ce1f1b1be6023..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/burn_mode_tooltip.rs +++ /dev/null @@ -1,61 +0,0 @@ -use gpui::{Context, FontWeight, IntoElement, Render, Window}; -use ui::{prelude::*, tooltip_container}; - -pub struct BurnModeTooltip { - selected: bool, -} - -impl BurnModeTooltip { - pub fn new() -> Self { - Self { selected: false } - } - - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl Render for BurnModeTooltip { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let (icon, color) = if self.selected { - (IconName::ZedBurnModeOn, Color::Error) - } else { - (IconName::ZedBurnMode, Color::Default) - }; - - let turned_on = h_flex() - .h_4() - .px_1() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().text_accent.opacity(0.1)) - .rounded_sm() - .child( - Label::new("ON") - .size(LabelSize::XSmall) - .weight(FontWeight::SEMIBOLD) - .color(Color::Accent), - ); - - let title = h_flex() - .gap_1p5() - .child(Icon::new(icon).size(IconSize::Small).color(color)) - .child(Label::new("Burn Mode")) - .when(self.selected, |title| title.child(turned_on)); - - tooltip_container(window, cx, |this, _, _| { - this - .child(title) - .child( - div() - .max_w_64() - .child( - Label::new("Enables models to use large context windows, unlimited tool calls, and other capabilities for expanded reasoning.") - .size(LabelSize::Small) - .color(Color::Muted) - ) - ) - }) - } -} diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 4b6d51c4c13d92b309d7bb10a6a753076344e4fa..5d094811f139e885fde7d198cdf0867c65cbeaf5 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -6,7 +6,7 @@ use crate::agent_diff::AgentDiffThread; use crate::agent_model_selector::AgentModelSelector; use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip}; use crate::ui::{ - MaxModeTooltip, + BurnModeTooltip, preview::{AgentPreview, UsageCallout}, }; use agent::history_store::HistoryStore; @@ -605,7 +605,7 @@ impl MessageEditor { this.toggle_burn_mode(&ToggleBurnMode, window, cx); })) .tooltip(move |_window, cx| { - cx.new(|_| MaxModeTooltip::new().selected(burn_mode_enabled)) + cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled)) .into() }) .into_any_element(), diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 49a37002f76873aaebbd3bcf32a3e7f7608ffa35..2e3b4ed890b05fd564bcdec5706b33443e2aa7e7 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1,6 +1,6 @@ use crate::{ - burn_mode_tooltip::BurnModeTooltip, language_model_selector::{LanguageModelSelector, language_model_selector}, + ui::BurnModeTooltip, }; use agent_settings::{AgentSettings, CompletionMode}; use anyhow::Result; diff --git a/crates/agent_ui/src/ui/burn_mode_tooltip.rs b/crates/agent_ui/src/ui/burn_mode_tooltip.rs index 97f7853a61bc2bc2766492e077e34c3f1b534abe..72faaa614d0d531365fef9ba5ff0e62a6fbcf145 100644 --- a/crates/agent_ui/src/ui/burn_mode_tooltip.rs +++ b/crates/agent_ui/src/ui/burn_mode_tooltip.rs @@ -2,11 +2,11 @@ use crate::ToggleBurnMode; use gpui::{Context, FontWeight, IntoElement, Render, Window}; use ui::{KeyBinding, prelude::*, tooltip_container}; -pub struct MaxModeTooltip { +pub struct BurnModeTooltip { selected: bool, } -impl MaxModeTooltip { +impl BurnModeTooltip { pub fn new() -> Self { Self { selected: false } } @@ -17,7 +17,7 @@ impl MaxModeTooltip { } } -impl Render for MaxModeTooltip { +impl Render for BurnModeTooltip { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let (icon, color) = if self.selected { (IconName::ZedBurnModeOn, Color::Error) diff --git a/crates/language_model/src/model/cloud_model.rs b/crates/language_model/src/model/cloud_model.rs index 3b4c1fa269020d1bf17d98cbb67251902536dafc..0e10050dae92dcdbfcb3138e7cd3981d773c5aeb 100644 --- a/crates/language_model/src/model/cloud_model.rs +++ b/crates/language_model/src/model/cloud_model.rs @@ -42,6 +42,18 @@ impl fmt::Display for ModelRequestLimitReachedError { } } +#[derive(Error, Debug)] +pub struct ToolUseLimitReachedError; + +impl fmt::Display for ToolUseLimitReachedError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Consecutive tool use limit reached. Enable Burn Mode for unlimited tool use." + ) + } +} + #[derive(Clone, Default)] pub struct LlmApiToken(Arc>>); From 708c434bd4c21614a282fc31ae35d3e9414ec2c9 Mon Sep 17 00:00:00 2001 From: Daniel Sauble Date: Fri, 15 Aug 2025 04:43:29 -0700 Subject: [PATCH 027/744] workspace: Highlight where dragged tab will be dropped (#34740) Closes #18565 I could use some advice on the color palette / theming. A couple options: 1. The `drop_target_background` color could be used for the border if we didn't use it for the background of the tab. In VSCode, the background color of tabs doesn't change as you're dragging, there's just a border between tabs. My only concern with this option is that the current `drop_target_background` color is a bit subtle when used for a small area like a border. 2. Another option could be to add a `drop_target_border` theme color, but I don't know how much complexity this adds to implementation (presumably all existing themes would need to be updated?). Demo: https://github.com/user-attachments/assets/0b7c04ea-5ec5-4b45-adad-156dfbf552db Release Notes: - Highlight where a dragged tab will be dropped between two other tabs --------- Co-authored-by: Smit Barmase --- crates/theme/src/default_colors.rs | 2 ++ crates/theme/src/fallback_themes.rs | 1 + crates/theme/src/schema.rs | 8 ++++++++ crates/theme/src/styles/colors.rs | 4 ++++ crates/workspace/src/pane.rs | 15 +++++++++++++-- 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index 1c3f48b548d3fdd4a2a554b476afaa08dcbae150..051b7acf102597b6f11581afdd45611b9a4b76e3 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -54,6 +54,7 @@ impl ThemeColors { element_disabled: neutral().light_alpha().step_3(), element_selection_background: blue().light().step_3().alpha(0.25), drop_target_background: blue().light_alpha().step_2(), + drop_target_border: neutral().light().step_12(), ghost_element_background: system.transparent, ghost_element_hover: neutral().light_alpha().step_3(), ghost_element_active: neutral().light_alpha().step_4(), @@ -179,6 +180,7 @@ impl ThemeColors { element_disabled: neutral().dark_alpha().step_3(), element_selection_background: blue().dark().step_3().alpha(0.25), drop_target_background: blue().dark_alpha().step_2(), + drop_target_border: neutral().dark().step_12(), ghost_element_background: system.transparent, ghost_element_hover: neutral().dark_alpha().step_4(), ghost_element_active: neutral().dark_alpha().step_5(), diff --git a/crates/theme/src/fallback_themes.rs b/crates/theme/src/fallback_themes.rs index 4d77dd5d81dfc45427bda4034ff7a2085dbcb489..e9e8e2d0db9320a4ec6bf95c53a11c84d1887777 100644 --- a/crates/theme/src/fallback_themes.rs +++ b/crates/theme/src/fallback_themes.rs @@ -115,6 +115,7 @@ pub(crate) fn zed_default_dark() -> Theme { element_disabled: SystemColors::default().transparent, element_selection_background: player.local().selection.alpha(0.25), drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0), + drop_target_border: hsla(221. / 360., 11. / 100., 86. / 100., 1.0), ghost_element_background: SystemColors::default().transparent, ghost_element_hover: hover, ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index bfa2adcedf73ec9d51c25d30785b1e81cd83173e..425fedbc717bb65ce3ce0872e1ed56fef3b79bb9 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -225,6 +225,10 @@ pub struct ThemeColorsContent { #[serde(rename = "drop_target.background")] pub drop_target_background: Option, + /// Border Color. Used for the border that shows where a dragged element will be dropped. + #[serde(rename = "drop_target.border")] + pub drop_target_border: Option, + /// Used for the background of a ghost element that should have the same background as the surface it's on. /// /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... @@ -747,6 +751,10 @@ impl ThemeColorsContent { .drop_target_background .as_ref() .and_then(|color| try_parse_color(color).ok()), + drop_target_border: self + .drop_target_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), ghost_element_background: self .ghost_element_background .as_ref() diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index aab11803f4d810453f5bfc286624ea8e4efb4a61..198ad97adb5d964a1d8f62c5bde99d1d5be5adf7 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -59,6 +59,8 @@ pub struct ThemeColors { pub element_disabled: Hsla, /// Background Color. Used for the area that shows where a dragged element will be dropped. pub drop_target_background: Hsla, + /// Border Color. Used for the border that shows where a dragged element will be dropped. + pub drop_target_border: Hsla, /// Used for the background of a ghost element that should have the same background as the surface it's on. /// /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... @@ -304,6 +306,7 @@ pub enum ThemeColorField { ElementSelected, ElementDisabled, DropTargetBackground, + DropTargetBorder, GhostElementBackground, GhostElementHover, GhostElementActive, @@ -418,6 +421,7 @@ impl ThemeColors { ThemeColorField::ElementSelected => self.element_selected, ThemeColorField::ElementDisabled => self.element_disabled, ThemeColorField::DropTargetBackground => self.drop_target_background, + ThemeColorField::DropTargetBorder => self.drop_target_border, ThemeColorField::GhostElementBackground => self.ghost_element_background, ThemeColorField::GhostElementHover => self.ghost_element_hover, ThemeColorField::GhostElementActive => self.ghost_element_active, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index cffeea0a8d4452ebff011ca3dd03cd3b10357143..45bd497705c05cf9750e0549fb56116262d78826 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2478,8 +2478,19 @@ impl Pane { }, |tab, _, _, cx| cx.new(|_| tab.clone()), ) - .drag_over::(|tab, _, _, cx| { - tab.bg(cx.theme().colors().drop_target_background) + .drag_over::(move |tab, dragged_tab: &DraggedTab, _, cx| { + let mut styled_tab = tab + .bg(cx.theme().colors().drop_target_background) + .border_color(cx.theme().colors().drop_target_border) + .border_0(); + + if ix < dragged_tab.ix { + styled_tab = styled_tab.border_l_2(); + } else if ix > dragged_tab.ix { + styled_tab = styled_tab.border_r_2(); + } + + styled_tab }) .drag_over::(|tab, _, _, cx| { tab.bg(cx.theme().colors().drop_target_background) From 846ed6adf91fc63f585c921da0101802b031c855 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 15 Aug 2025 14:54:05 +0200 Subject: [PATCH 028/744] search: Fix project search not rendering matches count (#36238) Follow up to https://github.com/zed-industries/zed/pull/36103/ Release Notes: - N/A --- crates/search/src/project_search.rs | 87 +++++++++++++++-------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6b9777906af1791605644c64e3a72f3ddde920d6..b791f748adfcd4714b7853e5701b051f26f09d60 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1975,6 +1975,48 @@ impl Render for ProjectSearchBar { ), ); + let query_focus = search.query_editor.focus_handle(cx); + + let matches_column = h_flex() + .pl_2() + .ml_2() + .border_l_1() + .border_color(theme_colors.border_variant) + .child(render_action_button( + "project-search-nav-button", + IconName::ChevronLeft, + search.active_match_index.is_some(), + "Select Previous Match", + &SelectPreviousMatch, + query_focus.clone(), + )) + .child(render_action_button( + "project-search-nav-button", + IconName::ChevronRight, + search.active_match_index.is_some(), + "Select Next Match", + &SelectNextMatch, + query_focus, + )) + .child( + div() + .id("matches") + .ml_2() + .min_w(rems_from_px(40.)) + .child(Label::new(match_text).size(LabelSize::Small).color( + if search.active_match_index.is_some() { + Color::Default + } else { + Color::Disabled + }, + )) + .when(limit_reached, |el| { + el.tooltip(Tooltip::text( + "Search limits reached.\nTry narrowing your search.", + )) + }), + ); + let mode_column = h_flex() .gap_1() .min_w_64() @@ -2016,55 +2058,14 @@ impl Render for ProjectSearchBar { "Toggle Replace", &ToggleReplace, focus_handle.clone(), - )); - - let query_focus = search.query_editor.focus_handle(cx); - - let matches_column = h_flex() - .pl_2() - .ml_2() - .border_l_1() - .border_color(theme_colors.border_variant) - .child(render_action_button( - "project-search-nav-button", - IconName::ChevronLeft, - search.active_match_index.is_some(), - "Select Previous Match", - &SelectPreviousMatch, - query_focus.clone(), )) - .child(render_action_button( - "project-search-nav-button", - IconName::ChevronRight, - search.active_match_index.is_some(), - "Select Next Match", - &SelectNextMatch, - query_focus, - )) - .child( - div() - .id("matches") - .ml_2() - .min_w(rems_from_px(40.)) - .child(Label::new(match_text).size(LabelSize::Small).color( - if search.active_match_index.is_some() { - Color::Default - } else { - Color::Disabled - }, - )) - .when(limit_reached, |el| { - el.tooltip(Tooltip::text( - "Search limits reached.\nTry narrowing your search.", - )) - }), - ); + .child(matches_column); let search_line = h_flex() .w_full() .gap_2() .child(query_column) - .child(h_flex().min_w_64().child(mode_column).child(matches_column)); + .child(mode_column); let replace_line = search.replace_enabled.then(|| { let replace_column = input_base_styles(InputPanel::Replacement) From f63036548c2229a4dfe1cd7576bf6cee5cd3f1ca Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 15 Aug 2025 15:17:56 +0200 Subject: [PATCH 029/744] agent2: Implement prompt caching (#36236) Release Notes: - N/A --- crates/agent2/src/tests/mod.rs | 135 ++++++++++++++++++++++++++++++++- crates/agent2/src/thread.rs | 8 ++ 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index cf90c8f650b6b4fab8fb7321f9d966ee623c2eff..cc8bd483bbe26ac12c092d6743b43086fd5edfd4 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -16,6 +16,7 @@ use language_model::{ LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason, fake_provider::FakeLanguageModel, }; +use pretty_assertions::assert_eq; use project::Project; use prompt_store::ProjectContext; use reqwest_client::ReqwestClient; @@ -129,6 +130,134 @@ async fn test_system_prompt(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_prompt_caching(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + // Send initial user message and verify it's cached + thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Message 1"], cx) + }); + cx.run_until_parked(); + + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + completion.messages[1..], + vec![LanguageModelRequestMessage { + role: Role::User, + content: vec!["Message 1".into()], + cache: true + }] + ); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text( + "Response to Message 1".into(), + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + // Send another user message and verify only the latest is cached + thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Message 2"], cx) + }); + cx.run_until_parked(); + + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + completion.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Message 1".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec!["Response to Message 1".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Message 2".into()], + cache: true + } + ] + ); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text( + "Response to Message 2".into(), + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + // Simulate a tool call and verify that the latest tool result is cached + thread.update(cx, |thread, _| thread.add_tool(EchoTool)); + thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Use the echo tool"], cx) + }); + cx.run_until_parked(); + + let tool_use = LanguageModelToolUse { + id: "tool_1".into(), + name: EchoTool.name().into(), + raw_input: json!({"text": "test"}).to_string(), + input: json!({"text": "test"}), + is_input_complete: true, + }; + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone())); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + let completion = fake_model.pending_completions().pop().unwrap(); + let tool_result = LanguageModelToolResult { + tool_use_id: "tool_1".into(), + tool_name: EchoTool.name().into(), + is_error: false, + content: "test".into(), + output: Some("test".into()), + }; + assert_eq!( + completion.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Message 1".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec!["Response to Message 1".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Message 2".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec!["Response to Message 2".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Use the echo tool".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec![MessageContent::ToolUse(tool_use)], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec![MessageContent::ToolResult(tool_result)], + cache: true + } + ] + ); +} + #[gpui::test] #[ignore = "can't run on CI yet"] async fn test_basic_tool_calls(cx: &mut TestAppContext) { @@ -440,7 +569,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { LanguageModelRequestMessage { role: Role::User, content: vec![MessageContent::ToolResult(tool_result.clone())], - cache: false + cache: true }, ] ); @@ -481,7 +610,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { LanguageModelRequestMessage { role: Role::User, content: vec!["Continue where you left off".into()], - cache: false + cache: true } ] ); @@ -574,7 +703,7 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) { LanguageModelRequestMessage { role: Role::User, content: vec!["ghi".into()], - cache: false + cache: true } ] ); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 231ee92dda0525272a7729c9593443b4627cd9e3..2fe2dc20bb5a7d7dc75808837f233b4526f6ee76 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1041,6 +1041,14 @@ impl Thread { messages.extend(message.to_request()); } + if let Some(last_user_message) = messages + .iter_mut() + .rev() + .find(|message| message.role == Role::User) + { + last_user_message.cache = true; + } + messages } From 91e6b382852fde4e880bc4aba7a15f7bb08c11aa Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Fri, 15 Aug 2025 10:58:57 -0300 Subject: [PATCH 030/744] Log agent servers stderr (#36243) Release Notes: - N/A --- crates/agent_servers/src/acp/v1.rs | 21 ++++++++++++++++++--- crates/agent_servers/src/claude.rs | 21 +++++++++++++++++---- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 36511e4644603de957efd1dd939d8915a5fc873d..6cf9801d064b20addf4e6d976fa856bea8a3a27d 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -1,7 +1,9 @@ use agent_client_protocol::{self as acp, Agent as _}; use anyhow::anyhow; use collections::HashMap; +use futures::AsyncBufReadExt as _; use futures::channel::oneshot; +use futures::io::BufReader; use project::Project; use std::path::Path; use std::rc::Rc; @@ -40,12 +42,13 @@ impl AcpConnection { .current_dir(root_dir) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::piped()) .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 stdout = child.stdout.take().context("Failed to take stdout")?; + let stdin = child.stdin.take().context("Failed to take stdin")?; + let stderr = child.stderr.take().context("Failed to take stderr")?; log::trace!("Spawned (pid: {})", child.id()); let sessions = Rc::new(RefCell::new(HashMap::default())); @@ -63,6 +66,18 @@ impl AcpConnection { let io_task = cx.background_spawn(io_task); + cx.background_spawn(async move { + let mut stderr = BufReader::new(stderr); + let mut line = String::new(); + while let Ok(n) = stderr.read_line(&mut line).await + && n > 0 + { + log::warn!("agent stderr: {}", &line); + line.clear(); + } + }) + .detach(); + cx.spawn({ let sessions = sessions.clone(); async move |cx| { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index e1cc70928960d6da54f3f3d577128c1d44facf62..14a179ba3dbe85d8fbacbfe5f7ee97fbef0045bf 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -14,7 +14,7 @@ use std::rc::Rc; use uuid::Uuid; use agent_client_protocol as acp; -use anyhow::{Result, anyhow}; +use anyhow::{Context as _, Result, anyhow}; use futures::channel::oneshot; use futures::{AsyncBufReadExt, AsyncWriteExt}; use futures::{ @@ -130,12 +130,25 @@ impl AgentConnection for ClaudeAgentConnection { &cwd, )?; - let stdin = child.stdin.take().unwrap(); - let stdout = child.stdout.take().unwrap(); + let stdout = child.stdout.take().context("Failed to take stdout")?; + let stdin = child.stdin.take().context("Failed to take stdin")?; + let stderr = child.stderr.take().context("Failed to take stderr")?; let pid = child.id(); log::trace!("Spawned (pid: {})", pid); + cx.background_spawn(async move { + let mut stderr = BufReader::new(stderr); + let mut line = String::new(); + while let Ok(n) = stderr.read_line(&mut line).await + && n > 0 + { + log::warn!("agent stderr: {}", &line); + line.clear(); + } + }) + .detach(); + cx.background_spawn(async move { let mut outgoing_rx = Some(outgoing_rx); @@ -345,7 +358,7 @@ fn spawn_claude( .current_dir(root_dir) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::piped()) .kill_on_drop(true) .spawn()?; From 10a2426a58e913e2715eb5eab760d40385c839f2 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 15 Aug 2025 16:06:56 +0200 Subject: [PATCH 031/744] agent2: Port profile selector (#36244) Release Notes: - N/A --- crates/agent2/src/thread.rs | 4 ++ crates/agent_ui/src/acp/thread_view.rs | 42 ++++++++++++++++++++- crates/agent_ui/src/message_editor.rs | 27 +++++++++++-- crates/agent_ui/src/profile_selector.rs | 50 ++++++++++++------------- 4 files changed, 91 insertions(+), 32 deletions(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 2fe2dc20bb5a7d7dc75808837f233b4526f6ee76..3f152c79cdefdc57b472c840a3825e68cf87a313 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -499,6 +499,10 @@ impl Thread { self.tools.remove(name).is_some() } + pub fn profile(&self) -> &AgentProfileId { + &self.profile_id + } + pub fn set_profile(&mut self, profile_id: AgentProfileId) { self.profile_id = profile_id; } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 87af75f04660eb20de6460a1993479b0f477f12b..cb1a62fd11a97f56a5147a289bd8e1a349ce0286 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -7,7 +7,7 @@ use action_log::ActionLog; use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::{self as acp}; use agent_servers::AgentServer; -use agent_settings::{AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; +use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; use anyhow::bail; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; @@ -16,6 +16,7 @@ use collections::{HashMap, HashSet}; use editor::scroll::Autoscroll; use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; +use fs::Fs; use gpui::{ Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, ClipboardItem, EdgesRefinement, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, @@ -29,6 +30,7 @@ use project::Project; use prompt_store::PromptId; use rope::Point; use settings::{Settings as _, SettingsStore}; +use std::sync::Arc; use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration}; use text::Anchor; use theme::ThemeSettings; @@ -45,10 +47,11 @@ use super::entry_view_state::EntryViewState; use crate::acp::AcpModelSelectorPopover; use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; +use crate::profile_selector::{ProfileProvider, ProfileSelector}; use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip}; use crate::{ AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow, - KeepAll, OpenAgentDiff, RejectAll, ToggleBurnMode, + KeepAll, OpenAgentDiff, RejectAll, ToggleBurnMode, ToggleProfileSelector, }; const RESPONSE_PADDING_X: Pixels = px(19.); @@ -78,6 +81,22 @@ impl ThreadError { } } +impl ProfileProvider for Entity { + fn profile_id(&self, cx: &App) -> AgentProfileId { + self.read(cx).profile().clone() + } + + fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) { + self.update(cx, |thread, _cx| { + thread.set_profile(profile_id); + }); + } + + fn profiles_supported(&self, cx: &App) -> bool { + self.read(cx).model().supports_tools() + } +} + pub struct AcpThreadView { agent: Rc, workspace: WeakEntity, @@ -88,6 +107,7 @@ pub struct AcpThreadView { entry_view_state: EntryViewState, message_editor: Entity, model_selector: Option>, + profile_selector: Option>, notifications: Vec>, notification_subscriptions: HashMap, Vec>, thread_error: Option, @@ -170,6 +190,7 @@ impl AcpThreadView { thread_state: Self::initial_state(agent, workspace, project, window, cx), message_editor, model_selector: None, + profile_selector: None, notifications: Vec::new(), notification_subscriptions: HashMap::default(), entry_view_state: EntryViewState::default(), @@ -297,6 +318,17 @@ impl AcpThreadView { _subscription: [thread_subscription, action_log_subscription], }; + this.profile_selector = this.as_native_thread(cx).map(|thread| { + cx.new(|cx| { + ProfileSelector::new( + ::global(cx), + Arc::new(thread.clone()), + this.focus_handle(cx), + cx, + ) + }) + }); + cx.notify(); } Err(err) => { @@ -2315,6 +2347,11 @@ impl AcpThreadView { v_flex() .on_action(cx.listener(Self::expand_message_editor)) + .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| { + if let Some(profile_selector) = this.profile_selector.as_ref() { + profile_selector.read(cx).menu_handle().toggle(window, cx); + } + })) .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| { if let Some(model_selector) = this.model_selector.as_ref() { model_selector @@ -2378,6 +2415,7 @@ impl AcpThreadView { .child( h_flex() .gap_1() + .children(self.profile_selector.clone()) .children(self.model_selector.clone()) .child(self.render_send_button(cx)), ), diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 5d094811f139e885fde7d198cdf0867c65cbeaf5..127e9256be04f1b2c3f1168368d7734b5471c2eb 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -14,7 +14,7 @@ use agent::{ context::{AgentContextKey, ContextLoadResult, load_context}, context_store::ContextStoreEvent, }; -use agent_settings::{AgentSettings, CompletionMode}; +use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; use ai_onboarding::ApiKeysWithProviders; use buffer_diff::BufferDiff; use cloud_llm_client::CompletionIntent; @@ -55,7 +55,7 @@ use zed_actions::agent::ToggleModelSelector; use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention}; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; -use crate::profile_selector::ProfileSelector; +use crate::profile_selector::{ProfileProvider, ProfileSelector}; use crate::{ ActiveThread, AgentDiffPane, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll, ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode, @@ -152,6 +152,24 @@ pub(crate) fn create_editor( editor } +impl ProfileProvider for Entity { + fn profiles_supported(&self, cx: &App) -> bool { + self.read(cx) + .configured_model() + .map_or(false, |model| model.model.supports_tools()) + } + + fn profile_id(&self, cx: &App) -> AgentProfileId { + self.read(cx).profile().id().clone() + } + + fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) { + self.update(cx, |this, cx| { + this.set_profile(profile_id, cx); + }); + } +} + impl MessageEditor { pub fn new( fs: Arc, @@ -221,8 +239,9 @@ impl MessageEditor { ) }); - let profile_selector = - cx.new(|cx| ProfileSelector::new(fs, thread.clone(), editor.focus_handle(cx), cx)); + let profile_selector = cx.new(|cx| { + ProfileSelector::new(fs, Arc::new(thread.clone()), editor.focus_handle(cx), cx) + }); Self { editor: editor.clone(), diff --git a/crates/agent_ui/src/profile_selector.rs b/crates/agent_ui/src/profile_selector.rs index ddcb44d46b800f257314a8802ad01abc98560ce0..27ca69590fb20cd5a058f375b7acf8fffadeac31 100644 --- a/crates/agent_ui/src/profile_selector.rs +++ b/crates/agent_ui/src/profile_selector.rs @@ -1,12 +1,8 @@ use crate::{ManageProfiles, ToggleProfileSelector}; -use agent::{ - Thread, - agent_profile::{AgentProfile, AvailableProfiles}, -}; +use agent::agent_profile::{AgentProfile, AvailableProfiles}; use agent_settings::{AgentDockPosition, AgentProfileId, AgentSettings, builtin_profiles}; use fs::Fs; -use gpui::{Action, Empty, Entity, FocusHandle, Subscription, prelude::*}; -use language_model::LanguageModelRegistry; +use gpui::{Action, Entity, FocusHandle, Subscription, prelude::*}; use settings::{Settings as _, SettingsStore, update_settings_file}; use std::sync::Arc; use ui::{ @@ -14,10 +10,22 @@ use ui::{ prelude::*, }; +/// Trait for types that can provide and manage agent profiles +pub trait ProfileProvider { + /// Get the current profile ID + fn profile_id(&self, cx: &App) -> AgentProfileId; + + /// Set the profile ID + fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App); + + /// Check if profiles are supported in the current context (e.g. if the model that is selected has tool support) + fn profiles_supported(&self, cx: &App) -> bool; +} + pub struct ProfileSelector { profiles: AvailableProfiles, fs: Arc, - thread: Entity, + provider: Arc, menu_handle: PopoverMenuHandle, focus_handle: FocusHandle, _subscriptions: Vec, @@ -26,7 +34,7 @@ pub struct ProfileSelector { impl ProfileSelector { pub fn new( fs: Arc, - thread: Entity, + provider: Arc, focus_handle: FocusHandle, cx: &mut Context, ) -> Self { @@ -37,7 +45,7 @@ impl ProfileSelector { Self { profiles: AgentProfile::available_profiles(cx), fs, - thread, + provider, menu_handle: PopoverMenuHandle::default(), focus_handle, _subscriptions: vec![settings_subscription], @@ -113,10 +121,10 @@ impl ProfileSelector { builtin_profiles::MINIMAL => Some("Chat about anything with no tools."), _ => None, }; - let thread_profile_id = self.thread.read(cx).profile().id(); + let thread_profile_id = self.provider.profile_id(cx); let entry = ContextMenuEntry::new(profile_name.clone()) - .toggleable(IconPosition::End, &profile_id == thread_profile_id); + .toggleable(IconPosition::End, profile_id == thread_profile_id); let entry = if let Some(doc_text) = documentation { entry.documentation_aside(documentation_side(settings.dock), move |_| { @@ -128,7 +136,7 @@ impl ProfileSelector { entry.handler({ let fs = self.fs.clone(); - let thread = self.thread.clone(); + let provider = self.provider.clone(); let profile_id = profile_id.clone(); move |_window, cx| { update_settings_file::(fs.clone(), cx, { @@ -138,9 +146,7 @@ impl ProfileSelector { } }); - thread.update(cx, |this, cx| { - this.set_profile(profile_id.clone(), cx); - }); + provider.set_profile(profile_id.clone(), cx); } }) } @@ -149,22 +155,14 @@ impl ProfileSelector { impl Render for ProfileSelector { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let settings = AgentSettings::get_global(cx); - let profile_id = self.thread.read(cx).profile().id(); - let profile = settings.profiles.get(profile_id); + let profile_id = self.provider.profile_id(cx); + let profile = settings.profiles.get(&profile_id); let selected_profile = profile .map(|profile| profile.name.clone()) .unwrap_or_else(|| "Unknown".into()); - let configured_model = self.thread.read(cx).configured_model().or_else(|| { - let model_registry = LanguageModelRegistry::read_global(cx); - model_registry.default_model() - }); - let Some(configured_model) = configured_model else { - return Empty.into_any_element(); - }; - - if configured_model.model.supports_tools() { + if self.provider.profiles_supported(cx) { let this = cx.entity().clone(); let focus_handle = self.focus_handle.clone(); let trigger_button = Button::new("profile-selector-model", selected_profile) From 1e41d86b31b2225173c201cc00770bd485e044ce Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 15 Aug 2025 16:23:55 +0200 Subject: [PATCH 032/744] agent2: Set thread_id, prompt_id, temperature on request (#36246) Release Notes: - N/A --- crates/agent2/src/thread.rs | 57 +++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 3f152c79cdefdc57b472c840a3825e68cf87a313..cfd67f4b05400d281e512a262026989ba4db2675 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -28,6 +28,48 @@ use smol::stream::StreamExt; use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc}; use std::{fmt::Write, ops::Range}; use util::{ResultExt, markdown::MarkdownCodeBlock}; +use uuid::Uuid; + +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema, +)] +pub struct ThreadId(Arc); + +impl ThreadId { + pub fn new() -> Self { + Self(Uuid::new_v4().to_string().into()) + } +} + +impl std::fmt::Display for ThreadId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From<&str> for ThreadId { + fn from(value: &str) -> Self { + Self(value.into()) + } +} + +/// The ID of the user prompt that initiated a request. +/// +/// This equates to the user physically submitting a message to the model (e.g., by pressing the Enter key). +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] +pub struct PromptId(Arc); + +impl PromptId { + pub fn new() -> Self { + Self(Uuid::new_v4().to_string().into()) + } +} + +impl std::fmt::Display for PromptId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} #[derive(Debug, Clone, PartialEq, Eq)] pub enum Message { @@ -412,6 +454,8 @@ pub struct ToolCallAuthorization { } pub struct Thread { + id: ThreadId, + prompt_id: PromptId, messages: Vec, completion_mode: CompletionMode, /// Holds the task that handles agent interaction until the end of the turn. @@ -442,6 +486,8 @@ impl Thread { ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); Self { + id: ThreadId::new(), + prompt_id: PromptId::new(), messages: Vec::new(), completion_mode: CompletionMode::Normal, running_turn: None, @@ -553,6 +599,7 @@ impl Thread { T: Into, { log::info!("Thread::send called with model: {:?}", self.model.name()); + self.advance_prompt_id(); let content = content.into_iter().map(Into::into).collect::>(); log::debug!("Thread::send content: {:?}", content); @@ -976,15 +1023,15 @@ impl Thread { log::info!("Request includes {} tools", tools.len()); let request = LanguageModelRequest { - thread_id: None, - prompt_id: None, + thread_id: Some(self.id.to_string()), + prompt_id: Some(self.prompt_id.to_string()), intent: Some(completion_intent), mode: Some(self.completion_mode.into()), messages, tools, tool_choice: None, stop: Vec::new(), - temperature: None, + temperature: AgentSettings::temperature_for_model(self.model(), cx), thinking_allowed: true, }; @@ -1072,6 +1119,10 @@ impl Thread { markdown } + + fn advance_prompt_id(&mut self) { + self.prompt_id = PromptId::new(); + } } pub trait AgentTool From 485802b9e5226cb00c14bf9d94211cabfd42a51b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 10:46:06 -0400 Subject: [PATCH 033/744] collab: Remove endpoints for issuing notifications from Cloud (#36249) This PR removes the `POST /users/:id/refresh_llm_tokens` and `POST /users/:id/update_plan` endpoints from Collab. These endpoints were added to be called by Cloud in order to push down notifications over the Collab RPC connection. Cloud now sends down notifications to clients directly, so we no longer need these endpoints. All calls to these endpoints have already been removed in production. Release Notes: - N/A --- crates/collab/src/api.rs | 92 ---------------------------------------- crates/collab/src/rpc.rs | 47 -------------------- 2 files changed, 139 deletions(-) diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 6cf3f68f54eda75ac19950c53cf535ff30a107a9..078a4469ae713de4c79929db09ed3522a52790a3 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -11,9 +11,7 @@ use crate::{ db::{User, UserId}, rpc, }; -use ::rpc::proto; use anyhow::Context as _; -use axum::extract; use axum::{ Extension, Json, Router, body::Body, @@ -25,7 +23,6 @@ use axum::{ routing::{get, post}, }; use axum_extra::response::ErasedJson; -use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::sync::{Arc, OnceLock}; use tower::ServiceBuilder; @@ -102,8 +99,6 @@ pub fn routes(rpc_server: Arc) -> Router<(), Body> { Router::new() .route("/users/look_up", get(look_up_user)) .route("/users/:id/access_tokens", post(create_access_token)) - .route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens)) - .route("/users/:id/update_plan", post(update_plan)) .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) .merge(contributors::router()) .layer( @@ -295,90 +290,3 @@ async fn create_access_token( encrypted_access_token, })) } - -#[derive(Serialize)] -struct RefreshLlmTokensResponse {} - -async fn refresh_llm_tokens( - Path(user_id): Path, - Extension(rpc_server): Extension>, -) -> Result> { - rpc_server.refresh_llm_tokens_for_user(user_id).await; - - Ok(Json(RefreshLlmTokensResponse {})) -} - -#[derive(Debug, Serialize, Deserialize)] -struct UpdatePlanBody { - pub plan: cloud_llm_client::Plan, - pub subscription_period: SubscriptionPeriod, - pub usage: cloud_llm_client::CurrentUsage, - pub trial_started_at: Option>, - pub is_usage_based_billing_enabled: bool, - pub is_account_too_young: bool, - pub has_overdue_invoices: bool, -} - -#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] -struct SubscriptionPeriod { - pub started_at: DateTime, - pub ended_at: DateTime, -} - -#[derive(Serialize)] -struct UpdatePlanResponse {} - -async fn update_plan( - Path(user_id): Path, - Extension(rpc_server): Extension>, - extract::Json(body): extract::Json, -) -> Result> { - let plan = match body.plan { - cloud_llm_client::Plan::ZedFree => proto::Plan::Free, - cloud_llm_client::Plan::ZedPro => proto::Plan::ZedPro, - cloud_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial, - }; - - let update_user_plan = proto::UpdateUserPlan { - plan: plan.into(), - trial_started_at: body - .trial_started_at - .map(|trial_started_at| trial_started_at.timestamp() as u64), - is_usage_based_billing_enabled: Some(body.is_usage_based_billing_enabled), - usage: Some(proto::SubscriptionUsage { - model_requests_usage_amount: body.usage.model_requests.used, - model_requests_usage_limit: Some(usage_limit_to_proto(body.usage.model_requests.limit)), - edit_predictions_usage_amount: body.usage.edit_predictions.used, - edit_predictions_usage_limit: Some(usage_limit_to_proto( - body.usage.edit_predictions.limit, - )), - }), - subscription_period: Some(proto::SubscriptionPeriod { - started_at: body.subscription_period.started_at.timestamp() as u64, - ended_at: body.subscription_period.ended_at.timestamp() as u64, - }), - account_too_young: Some(body.is_account_too_young), - has_overdue_invoices: Some(body.has_overdue_invoices), - }; - - rpc_server - .update_plan_for_user(user_id, update_user_plan) - .await?; - - Ok(Json(UpdatePlanResponse {})) -} - -fn usage_limit_to_proto(limit: cloud_llm_client::UsageLimit) -> proto::UsageLimit { - proto::UsageLimit { - variant: Some(match limit { - cloud_llm_client::UsageLimit::Limited(limit) => { - proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { - limit: limit as u32, - }) - } - cloud_llm_client::UsageLimit::Unlimited => { - proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) - } - }), - } -} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 18eb1457dc336c5e2bf32a3d8430514b29bb6966..584970a4c678c3f0f3db2a2516010dec7a30c1ad 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1081,53 +1081,6 @@ impl Server { Ok(()) } - pub async fn update_plan_for_user( - self: &Arc, - user_id: UserId, - update_user_plan: proto::UpdateUserPlan, - ) -> Result<()> { - let pool = self.connection_pool.lock(); - for connection_id in pool.user_connection_ids(user_id) { - self.peer - .send(connection_id, update_user_plan.clone()) - .trace_err(); - } - - Ok(()) - } - - /// This is the legacy way of updating the user's plan, where we fetch the data to construct the `UpdateUserPlan` - /// message on the Collab server. - /// - /// The new way is to receive the data from Cloud via the `POST /users/:id/update_plan` endpoint. - pub async fn update_plan_for_user_legacy(self: &Arc, user_id: UserId) -> Result<()> { - let user = self - .app_state - .db - .get_user_by_id(user_id) - .await? - .context("user not found")?; - - let update_user_plan = make_update_user_plan_message( - &user, - user.admin, - &self.app_state.db, - self.app_state.llm_db.clone(), - ) - .await?; - - self.update_plan_for_user(user_id, update_user_plan).await - } - - pub async fn refresh_llm_tokens_for_user(self: &Arc, user_id: UserId) { - let pool = self.connection_pool.lock(); - for connection_id in pool.user_connection_ids(user_id) { - self.peer - .send(connection_id, proto::RefreshLlmToken {}) - .trace_err(); - } - } - pub async fn snapshot(self: &Arc) -> ServerSnapshot<'_> { ServerSnapshot { connection_pool: ConnectionPoolGuard { From 7993ee9c07a56e61ead665ca95343c038ea2765a Mon Sep 17 00:00:00 2001 From: Igal Tabachnik Date: Fri, 15 Aug 2025 18:26:38 +0300 Subject: [PATCH 034/744] Suggest unsaved buffer content text as the default filename (#35707) Closes #24672 This PR complements a feature added earlier by @JosephTLyons (in https://github.com/zed-industries/zed/pull/32353) where the text is considered as the tab title in a new buffer. It piggybacks off that change and sets the title as the suggested filename in the save dialog (completely mirroring the same functionality in VSCode): ![2025-08-05 11 50 28](https://github.com/user-attachments/assets/49ad9e4a-5559-44b0-a4b0-ae19890e478e) Release Notes: - Text entered in a new untitled buffer is considered as the default filename when saving --- crates/editor/src/items.rs | 4 +++ crates/gpui/src/app.rs | 3 +- crates/gpui/src/platform.rs | 6 +++- crates/gpui/src/platform/linux/platform.rs | 29 +++++++++++++------- crates/gpui/src/platform/mac/platform.rs | 12 +++++++- crates/gpui/src/platform/test/platform.rs | 1 + crates/gpui/src/platform/windows/platform.rs | 20 ++++++++++++-- crates/workspace/src/item.rs | 11 ++++++++ crates/workspace/src/pane.rs | 4 ++- crates/workspace/src/workspace.rs | 3 +- 10 files changed, 75 insertions(+), 18 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 480757a4911ed9b2ecb5b2ae09af736edf0a2b45..45a4f7365c931f241a1143b4d40a80ceb2fad5ab 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -654,6 +654,10 @@ impl Item for Editor { } } + fn suggested_filename(&self, cx: &App) -> SharedString { + self.buffer.read(cx).title(cx).to_string().into() + } + fn tab_icon(&self, _: &Window, cx: &App) -> Option { ItemSettings::get_global(cx) .file_icons diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 5f6d25250375561ebb41adc9191ebb5c37480ba3..e1df6d0be4d7122b6ac8108e4a59774bcfc3d016 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -816,8 +816,9 @@ impl App { pub fn prompt_for_new_path( &self, directory: &Path, + suggested_name: Option<&str>, ) -> oneshot::Receiver>> { - self.platform.prompt_for_new_path(directory) + self.platform.prompt_for_new_path(directory, suggested_name) } /// Reveals the specified path at the platform level, such as in Finder on macOS. diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index b495d70dfdd3594a27ed3c1793e7e0ac4e7e0b4a..bf6ce6870363d432cd49392292b8dda7bb51834d 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -220,7 +220,11 @@ pub(crate) trait Platform: 'static { &self, options: PathPromptOptions, ) -> oneshot::Receiver>>>; - fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver>>; + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>>; fn can_select_mixed_files_and_dirs(&self) -> bool; fn reveal_path(&self, path: &Path); fn open_with_system(&self, path: &Path); diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index fe6a36baa854856eb961a020ab35a7bd0195d465..31d445be5274309f2e84a0e8df0a446cdb79736b 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -327,26 +327,35 @@ impl Platform for P { done_rx } - fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver>> { + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { let (done_tx, done_rx) = oneshot::channel(); #[cfg(not(any(feature = "wayland", feature = "x11")))] - let _ = (done_tx.send(Ok(None)), directory); + let _ = (done_tx.send(Ok(None)), directory, suggested_name); #[cfg(any(feature = "wayland", feature = "x11"))] self.foreground_executor() .spawn({ let directory = directory.to_owned(); + let suggested_name = suggested_name.map(|s| s.to_owned()); async move { - let request = match ashpd::desktop::file_chooser::SaveFileRequest::default() - .modal(true) - .title("Save File") - .current_folder(directory) - .expect("pathbuf should not be nul terminated") - .send() - .await - { + let mut request_builder = + ashpd::desktop::file_chooser::SaveFileRequest::default() + .modal(true) + .title("Save File") + .current_folder(directory) + .expect("pathbuf should not be nul terminated"); + + if let Some(suggested_name) = suggested_name { + request_builder = request_builder.current_name(suggested_name.as_str()); + } + + let request = match request_builder.send().await { Ok(request) => request, Err(err) => { let result = match err { diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index c5731317994c2c81e456556815a0e47842a6c642..533423229cf2448ea70cf0140d5e3d6bc77fb32a 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -737,8 +737,13 @@ impl Platform for MacPlatform { done_rx } - fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver>> { + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { let directory = directory.to_owned(); + let suggested_name = suggested_name.map(|s| s.to_owned()); let (done_tx, done_rx) = oneshot::channel(); self.foreground_executor() .spawn(async move { @@ -748,6 +753,11 @@ impl Platform for MacPlatform { let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc()); panel.setDirectoryURL(url); + if let Some(suggested_name) = suggested_name { + let name_string = ns_string(&suggested_name); + let _: () = msg_send![panel, setNameFieldStringValue: name_string]; + } + let done_tx = Cell::new(Some(done_tx)); let block = ConcreteBlock::new(move |response: NSModalResponse| { let mut result = None; diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index a26b65576cc49e290494762eed597d5bd8d0af26..69371bc8c4aae38c48e1f14ae223fd9c8b1fb75e 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -336,6 +336,7 @@ impl Platform for TestPlatform { fn prompt_for_new_path( &self, directory: &std::path::Path, + _suggested_name: Option<&str>, ) -> oneshot::Receiver>> { let (tx, rx) = oneshot::channel(); self.background_executor() diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index bbde655b80517198e4f604edde2e560e19b57ff2..c1fb0cabc4fcf5759aedbdc8c045fdaa354fd2b3 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -490,13 +490,18 @@ impl Platform for WindowsPlatform { rx } - fn prompt_for_new_path(&self, directory: &Path) -> Receiver>> { + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> Receiver>> { let directory = directory.to_owned(); + let suggested_name = suggested_name.map(|s| s.to_owned()); let (tx, rx) = oneshot::channel(); let window = self.find_current_active_window(); self.foreground_executor() .spawn(async move { - let _ = tx.send(file_save_dialog(directory, window)); + let _ = tx.send(file_save_dialog(directory, suggested_name, window)); }) .detach(); @@ -804,7 +809,11 @@ fn file_open_dialog( Ok(Some(paths)) } -fn file_save_dialog(directory: PathBuf, window: Option) -> Result> { +fn file_save_dialog( + directory: PathBuf, + suggested_name: Option, + window: Option, +) -> Result> { let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? }; if !directory.to_string_lossy().is_empty() { if let Some(full_path) = directory.canonicalize().log_err() { @@ -815,6 +824,11 @@ fn file_save_dialog(directory: PathBuf, window: Option) -> Result + Render + Sized { /// Returns the textual contents of the tab. fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString; + /// Returns the suggested filename for saving this item. + /// By default, returns the tab content text. + fn suggested_filename(&self, cx: &App) -> SharedString { + self.tab_content_text(0, cx) + } + fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { None } @@ -497,6 +503,7 @@ pub trait ItemHandle: 'static + Send { ) -> gpui::Subscription; fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement; fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString; + fn suggested_filename(&self, cx: &App) -> SharedString; fn tab_icon(&self, window: &Window, cx: &App) -> Option; fn tab_tooltip_text(&self, cx: &App) -> Option; fn tab_tooltip_content(&self, cx: &App) -> Option; @@ -631,6 +638,10 @@ impl ItemHandle for Entity { self.read(cx).tab_content_text(detail, cx) } + fn suggested_filename(&self, cx: &App) -> SharedString { + self.read(cx).suggested_filename(cx) + } + fn tab_icon(&self, window: &Window, cx: &App) -> Option { self.read(cx).tab_icon(window, cx) } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 45bd497705c05cf9750e0549fb56116262d78826..759e91f758cea190d8c9e17102475a145e398641 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2062,6 +2062,8 @@ impl Pane { })? .await?; } else if can_save_as && is_singleton { + let suggested_name = + cx.update(|_window, cx| item.suggested_filename(cx).to_string())?; let new_path = pane.update_in(cx, |pane, window, cx| { pane.activate_item(item_ix, true, true, window, cx); pane.workspace.update(cx, |workspace, cx| { @@ -2073,7 +2075,7 @@ impl Pane { } else { DirectoryLister::Project(workspace.project().clone()) }; - workspace.prompt_for_new_path(lister, window, cx) + workspace.prompt_for_new_path(lister, Some(suggested_name), window, cx) }) })??; let Some(new_path) = new_path.await.ok().flatten().into_iter().flatten().next() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3129c12dbfe93dea3270b083323c8ebada235d98..ade6838fad893e3dbbdc0c43f1fa62be4253af5c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2067,6 +2067,7 @@ impl Workspace { pub fn prompt_for_new_path( &mut self, lister: DirectoryLister, + suggested_name: Option, window: &mut Window, cx: &mut Context, ) -> oneshot::Receiver>> { @@ -2094,7 +2095,7 @@ impl Workspace { }) .or_else(std::env::home_dir) .unwrap_or_else(|| PathBuf::from("")); - cx.prompt_for_new_path(&relative_to) + cx.prompt_for_new_path(&relative_to, suggested_name.as_deref()) })?; let abs_path = match abs_path.await? { Ok(path) => path, From 7671f34f88aefbaf75a313cf4b1fc0523cb7a43a Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Fri, 15 Aug 2025 18:37:24 +0300 Subject: [PATCH 035/744] agent: Create checkpoint before/after every edit operation (#36253) 1. Previously, checkpoints only appeared when an agent's edit happened immediately after a user message. This is rare (agent usually collects some context first), so they were almost never shown. This is now fixed. 2. After this change, a checkpoint is created after every edit operation. So when the agent edits files five times in a single dialog turn, we will now display five checkpoints. As a bonus, it's now possible to undo only a part of a long agent response. Closes #36092, #32917 Release Notes: - Create agent checkpoints more frequently (before every edit) --- crates/agent/src/thread.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 1d417efbbae009ef1b08c240fd195534192c28f6..f3f10884830c5c87a3a9e8e34b99c1197feb7756 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -844,11 +844,17 @@ impl Thread { .await .unwrap_or(false); - if !equal { - this.update(cx, |this, cx| { - this.insert_checkpoint(pending_checkpoint, cx) - })?; - } + this.update(cx, |this, cx| { + this.pending_checkpoint = if equal { + Some(pending_checkpoint) + } else { + this.insert_checkpoint(pending_checkpoint, cx); + Some(ThreadCheckpoint { + message_id: this.next_message_id, + git_checkpoint: final_checkpoint, + }) + } + })?; Ok(()) } From c39f294bcbae49e649d5cdd7d5bc774fa7a7190a Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:43:18 +0530 Subject: [PATCH 036/744] remote: Add support for additional SSH arguments in SshSocket (#33243) Closes #29438 Release Notes: - Fix SSH agent forwarding doesn't work when using SSH remote development. --- crates/remote/src/ssh_session.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index df7212d44c478ac68168be213131583ae980d8fa..2f462a86a5a1578993ad6e2893c196ed7daf8c3f 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -400,6 +400,7 @@ impl SshSocket { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) + .args(self.connection_options.additional_args()) .args(["-o", "ControlMaster=no", "-o"]) .arg(format!("ControlPath={}", self.socket_path.display())) } @@ -410,6 +411,7 @@ impl SshSocket { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) + .args(self.connection_options.additional_args()) .envs(self.envs.clone()) } @@ -417,22 +419,26 @@ impl SshSocket { // On Linux, we use the `ControlPath` option to create a socket file that ssh can use to #[cfg(not(target_os = "windows"))] fn ssh_args(&self) -> SshArgs { + let mut arguments = self.connection_options.additional_args(); + arguments.extend(vec![ + "-o".to_string(), + "ControlMaster=no".to_string(), + "-o".to_string(), + format!("ControlPath={}", self.socket_path.display()), + self.connection_options.ssh_url(), + ]); SshArgs { - arguments: vec![ - "-o".to_string(), - "ControlMaster=no".to_string(), - "-o".to_string(), - format!("ControlPath={}", self.socket_path.display()), - self.connection_options.ssh_url(), - ], + arguments, envs: None, } } #[cfg(target_os = "windows")] fn ssh_args(&self) -> SshArgs { + let mut arguments = self.connection_options.additional_args(); + arguments.push(self.connection_options.ssh_url()); SshArgs { - arguments: vec![self.connection_options.ssh_url()], + arguments, envs: Some(self.envs.clone()), } } From 257e0991d8069face34e734ff9ca4e9baa027817 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 12:13:52 -0400 Subject: [PATCH 037/744] collab: Increase minimum required version to connect (#36255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR increases the minimum required version to connect to Collab. Previously this was set at v0.157.0. The new minimum required version is v0.198.4, which is the first version where we no longer connect to Collab automatically. Clients on the v0.199.x minor version will also need to be v0.199.2 or greater in order to connect, due to us hotfixing the connection changes to the Preview branch. We're doing this to force clients to upgrade in order to connect to Collab, as we're going to be removing some of the old RPC usages related to authentication that are no longer used. Therefore, we want users to be on a version of Zed that does not rely on those messages. Users will see a message similar to this one, prompting them to upgrade: Screenshot 2025-08-15 at 11 37
55 AM > Note: In this case I'm simulating the error state, which is why I'm signed in via Cloud while still not being able to connect to Collab. Users on older versions will see the "Please update Zed to Collaborate" message without being signed in. Release Notes: - N/A --- crates/collab/src/rpc/connection_pool.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/rpc/connection_pool.rs b/crates/collab/src/rpc/connection_pool.rs index 35290fa697680140e52a147cc25cd87b6afee31e..729e7c8533460c0789d74040e883d48c8b94af92 100644 --- a/crates/collab/src/rpc/connection_pool.rs +++ b/crates/collab/src/rpc/connection_pool.rs @@ -30,7 +30,19 @@ impl fmt::Display for ZedVersion { impl ZedVersion { pub fn can_collaborate(&self) -> bool { - self.0 >= SemanticVersion::new(0, 157, 0) + // v0.198.4 is the first version where we no longer connect to Collab automatically. + // We reject any clients older than that to prevent them from connecting to Collab just for authentication. + if self.0 < SemanticVersion::new(0, 198, 4) { + return false; + } + + // Since we hotfixed the changes to no longer connect to Collab automatically to Preview, we also need to reject + // versions in the range [v0.199.0, v0.199.1]. + if self.0 >= SemanticVersion::new(0, 199, 0) && self.0 < SemanticVersion::new(0, 199, 2) { + return false; + } + + true } } From 75b832029a7ab35442e030fff05df55dbbd2d6de Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 13:26:21 -0400 Subject: [PATCH 038/744] Remove RPC messages pertaining to the LLM token (#36252) This PR removes the RPC messages pertaining to the LLM token. We now retrieve the LLM token from Cloud. Release Notes: - N/A --- Cargo.lock | 2 - crates/collab/Cargo.toml | 2 - crates/collab/src/llm.rs | 3 - crates/collab/src/llm/token.rs | 146 --------------------------------- crates/collab/src/rpc.rs | 96 +--------------------- crates/proto/proto/ai.proto | 8 -- crates/proto/proto/zed.proto | 6 +- crates/proto/src/proto.rs | 4 - 8 files changed, 4 insertions(+), 263 deletions(-) delete mode 100644 crates/collab/src/llm/token.rs diff --git a/Cargo.lock b/Cargo.lock index 2353733dc02f46117bb28f33073a135343e306e6..bfc797d6cd484f8327d2b6d508e916ef3b482290 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3324,7 +3324,6 @@ dependencies = [ "http_client", "hyper 0.14.32", "indoc", - "jsonwebtoken", "language", "language_model", "livekit_api", @@ -3370,7 +3369,6 @@ dependencies = [ "telemetry_events", "text", "theme", - "thiserror 2.0.12", "time", "tokio", "toml 0.8.20", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 9af95317e60db78fc93b9a1fa01eaee687fac4fc..9a867f9e058cb4a1f4782625571e1184cd7209d3 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -39,7 +39,6 @@ futures.workspace = true gpui.workspace = true hex.workspace = true http_client.workspace = true -jsonwebtoken.workspace = true livekit_api.workspace = true log.workspace = true nanoid.workspace = true @@ -65,7 +64,6 @@ subtle.workspace = true supermaven_api.workspace = true telemetry_events.workspace = true text.workspace = true -thiserror.workspace = true time.workspace = true tokio = { workspace = true, features = ["full"] } toml.workspace = true diff --git a/crates/collab/src/llm.rs b/crates/collab/src/llm.rs index de74858168fd94ab677cee03f721a1e3fbbdfd46..ca8e89bc6d72435b44802e878f86cec324f6cd26 100644 --- a/crates/collab/src/llm.rs +++ b/crates/collab/src/llm.rs @@ -1,7 +1,4 @@ pub mod db; -mod token; - -pub use token::*; pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial"; diff --git a/crates/collab/src/llm/token.rs b/crates/collab/src/llm/token.rs deleted file mode 100644 index da01c7f3bed5cab1e7dbd6cfdef8cd4d7643044c..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/token.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::db::billing_subscription::SubscriptionKind; -use crate::db::{billing_customer, billing_subscription, user}; -use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG}; -use crate::{Config, db::billing_preference}; -use anyhow::{Context as _, Result}; -use chrono::{NaiveDateTime, Utc}; -use cloud_llm_client::Plan; -use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; -use serde::{Deserialize, Serialize}; -use std::time::Duration; -use thiserror::Error; -use uuid::Uuid; - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LlmTokenClaims { - pub iat: u64, - pub exp: u64, - pub jti: String, - pub user_id: u64, - pub system_id: Option, - pub metrics_id: Uuid, - pub github_user_login: String, - pub account_created_at: NaiveDateTime, - pub is_staff: bool, - pub has_llm_closed_beta_feature_flag: bool, - pub bypass_account_age_check: bool, - pub use_llm_request_queue: bool, - pub plan: Plan, - pub has_extended_trial: bool, - pub subscription_period: (NaiveDateTime, NaiveDateTime), - pub enable_model_request_overages: bool, - pub model_request_overages_spend_limit_in_cents: u32, - pub can_use_web_search_tool: bool, - #[serde(default)] - pub has_overdue_invoices: bool, -} - -const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60); - -impl LlmTokenClaims { - pub fn create( - user: &user::Model, - is_staff: bool, - billing_customer: billing_customer::Model, - billing_preferences: Option, - feature_flags: &Vec, - subscription: billing_subscription::Model, - system_id: Option, - config: &Config, - ) -> Result { - let secret = config - .llm_api_secret - .as_ref() - .context("no LLM API secret")?; - - let plan = if is_staff { - Plan::ZedPro - } else { - subscription.kind.map_or(Plan::ZedFree, |kind| match kind { - SubscriptionKind::ZedFree => Plan::ZedFree, - SubscriptionKind::ZedPro => Plan::ZedPro, - SubscriptionKind::ZedProTrial => Plan::ZedProTrial, - }) - }; - let subscription_period = - billing_subscription::Model::current_period(Some(subscription), is_staff) - .map(|(start, end)| (start.naive_utc(), end.naive_utc())) - .context("A plan is required to use Zed's hosted models or edit predictions. Visit https://zed.dev/account to get started.")?; - - let now = Utc::now(); - let claims = Self { - iat: now.timestamp() as u64, - exp: (now + LLM_TOKEN_LIFETIME).timestamp() as u64, - jti: uuid::Uuid::new_v4().to_string(), - user_id: user.id.to_proto(), - system_id, - metrics_id: user.metrics_id, - github_user_login: user.github_login.clone(), - account_created_at: user.account_created_at(), - is_staff, - has_llm_closed_beta_feature_flag: feature_flags - .iter() - .any(|flag| flag == "llm-closed-beta"), - bypass_account_age_check: feature_flags - .iter() - .any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG), - can_use_web_search_tool: true, - use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"), - plan, - has_extended_trial: feature_flags - .iter() - .any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG), - subscription_period, - enable_model_request_overages: billing_preferences - .as_ref() - .map_or(false, |preferences| { - preferences.model_request_overages_enabled - }), - model_request_overages_spend_limit_in_cents: billing_preferences - .as_ref() - .map_or(0, |preferences| { - preferences.model_request_overages_spend_limit_in_cents as u32 - }), - has_overdue_invoices: billing_customer.has_overdue_invoices, - }; - - Ok(jsonwebtoken::encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(secret.as_ref()), - )?) - } - - pub fn validate(token: &str, config: &Config) -> Result { - let secret = config - .llm_api_secret - .as_ref() - .context("no LLM API secret")?; - - match jsonwebtoken::decode::( - token, - &DecodingKey::from_secret(secret.as_ref()), - &Validation::default(), - ) { - Ok(token) => Ok(token.claims), - Err(e) => { - if e.kind() == &jsonwebtoken::errors::ErrorKind::ExpiredSignature { - Err(ValidateLlmTokenError::Expired) - } else { - Err(ValidateLlmTokenError::JwtError(e)) - } - } - } - } -} - -#[derive(Error, Debug)] -pub enum ValidateLlmTokenError { - #[error("access token is expired")] - Expired, - #[error("access token validation error: {0}")] - JwtError(#[from] jsonwebtoken::errors::Error), - #[error("{0}")] - Other(#[from] anyhow::Error), -} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 584970a4c678c3f0f3db2a2516010dec7a30c1ad..715ff4e67d83095079b5ef11659953266f1e8ea1 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1,14 +1,12 @@ mod connection_pool; -use crate::api::billing::find_or_create_billing_customer; use crate::api::{CloudflareIpCountryHeader, SystemIdHeader}; use crate::db::billing_subscription::SubscriptionKind; use crate::llm::db::LlmDatabase; use crate::llm::{ - AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, LlmTokenClaims, + AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, MIN_ACCOUNT_AGE_FOR_LLM_USE, }; -use crate::stripe_client::StripeCustomerId; use crate::{ AppState, Error, Result, auth, db::{ @@ -218,6 +216,7 @@ struct Session { /// The GeoIP country code for the user. #[allow(unused)] geoip_country_code: Option, + #[allow(unused)] system_id: Option, _executor: Executor, } @@ -464,7 +463,6 @@ impl Server { .add_message_handler(unfollow) .add_message_handler(update_followers) .add_request_handler(get_private_user_info) - .add_request_handler(get_llm_api_token) .add_request_handler(accept_terms_of_service) .add_message_handler(acknowledge_channel_message) .add_message_handler(acknowledge_buffer_version) @@ -4251,96 +4249,6 @@ async fn accept_terms_of_service( accepted_tos_at: accepted_tos_at.timestamp() as u64, })?; - // When the user accepts the terms of service, we want to refresh their LLM - // token to grant access. - session - .peer - .send(session.connection_id, proto::RefreshLlmToken {})?; - - Ok(()) -} - -async fn get_llm_api_token( - _request: proto::GetLlmToken, - response: Response, - session: MessageContext, -) -> Result<()> { - let db = session.db().await; - - let flags = db.get_user_flags(session.user_id()).await?; - - let user_id = session.user_id(); - let user = db - .get_user_by_id(user_id) - .await? - .with_context(|| format!("user {user_id} not found"))?; - - if user.accepted_tos_at.is_none() { - Err(anyhow!("terms of service not accepted"))? - } - - let stripe_client = session - .app_state - .stripe_client - .as_ref() - .context("failed to retrieve Stripe client")?; - - let stripe_billing = session - .app_state - .stripe_billing - .as_ref() - .context("failed to retrieve Stripe billing object")?; - - let billing_customer = if let Some(billing_customer) = - db.get_billing_customer_by_user_id(user.id).await? - { - billing_customer - } else { - let customer_id = stripe_billing - .find_or_create_customer_by_email(user.email_address.as_deref()) - .await?; - - find_or_create_billing_customer(&session.app_state, stripe_client.as_ref(), &customer_id) - .await? - .context("billing customer not found")? - }; - - let billing_subscription = - if let Some(billing_subscription) = db.get_active_billing_subscription(user.id).await? { - billing_subscription - } else { - let stripe_customer_id = - StripeCustomerId(billing_customer.stripe_customer_id.clone().into()); - - let stripe_subscription = stripe_billing - .subscribe_to_zed_free(stripe_customer_id) - .await?; - - db.create_billing_subscription(&db::CreateBillingSubscriptionParams { - billing_customer_id: billing_customer.id, - kind: Some(SubscriptionKind::ZedFree), - stripe_subscription_id: stripe_subscription.id.to_string(), - stripe_subscription_status: stripe_subscription.status.into(), - stripe_cancellation_reason: None, - stripe_current_period_start: Some(stripe_subscription.current_period_start), - stripe_current_period_end: Some(stripe_subscription.current_period_end), - }) - .await? - }; - - let billing_preferences = db.get_billing_preferences(user.id).await?; - - let token = LlmTokenClaims::create( - &user, - session.is_staff(), - billing_customer, - billing_preferences, - &flags, - billing_subscription, - session.system_id.clone(), - &session.app_state.config, - )?; - response.send(proto::GetLlmTokenResponse { token })?; Ok(()) } diff --git a/crates/proto/proto/ai.proto b/crates/proto/proto/ai.proto index 67c222438731be456f6b20fc60e525ccec1669e4..1064ed2f8d301a6cc80170ce33fcca33310c2f1d 100644 --- a/crates/proto/proto/ai.proto +++ b/crates/proto/proto/ai.proto @@ -158,14 +158,6 @@ message SynchronizeContextsResponse { repeated ContextVersion contexts = 1; } -message GetLlmToken {} - -message GetLlmTokenResponse { - string token = 1; -} - -message RefreshLlmToken {} - enum LanguageModelRole { LanguageModelUser = 0; LanguageModelAssistant = 1; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 856a793c2ffdb09366947e24c526f40ebb206407..b6c7fc3cac5f204969894460a02e23d49ce4d291 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -250,10 +250,6 @@ message Envelope { AddWorktree add_worktree = 222; AddWorktreeResponse add_worktree_response = 223; - GetLlmToken get_llm_token = 235; - GetLlmTokenResponse get_llm_token_response = 236; - RefreshLlmToken refresh_llm_token = 259; - LspExtSwitchSourceHeader lsp_ext_switch_source_header = 241; LspExtSwitchSourceHeaderResponse lsp_ext_switch_source_header_response = 242; @@ -419,7 +415,9 @@ message Envelope { reserved 221; reserved 224 to 229; reserved 230 to 231; + reserved 235 to 236; reserved 246; + reserved 259; reserved 270; reserved 247 to 254; reserved 255 to 256; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index a5dd97661f5a703742278919a2a4bc1831696fde..8be9fed172cdee4a29f84a51958217697842c002 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -119,8 +119,6 @@ messages!( (GetTypeDefinitionResponse, Background), (GetImplementation, Background), (GetImplementationResponse, Background), - (GetLlmToken, Background), - (GetLlmTokenResponse, Background), (OpenUnstagedDiff, Foreground), (OpenUnstagedDiffResponse, Foreground), (OpenUncommittedDiff, Foreground), @@ -196,7 +194,6 @@ messages!( (PrepareRenameResponse, Background), (ProjectEntryResponse, Foreground), (RefreshInlayHints, Foreground), - (RefreshLlmToken, Background), (RegisterBufferWithLanguageServers, Background), (RejoinChannelBuffers, Foreground), (RejoinChannelBuffersResponse, Foreground), @@ -354,7 +351,6 @@ request_messages!( (GetDocumentHighlights, GetDocumentHighlightsResponse), (GetDocumentSymbols, GetDocumentSymbolsResponse), (GetHover, GetHoverResponse), - (GetLlmToken, GetLlmTokenResponse), (GetNotifications, GetNotificationsResponse), (GetPrivateUserInfo, GetPrivateUserInfoResponse), (GetProjectSymbols, GetProjectSymbolsResponse), From e452aba9da0cd66ec227371a2466f7a97847d5a9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 13:59:08 -0400 Subject: [PATCH 039/744] proto: Order `reserved` fields (#36261) This PR orders the `reserved` fields in the RPC `Envelope`, as they had gotten unsorted. Release Notes: - N/A --- crates/proto/proto/zed.proto | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index b6c7fc3cac5f204969894460a02e23d49ce4d291..7e7bd6b42b80554e1f9201fc1680d1a05cd7fc07 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -417,10 +417,10 @@ message Envelope { reserved 230 to 231; reserved 235 to 236; reserved 246; - reserved 259; - reserved 270; reserved 247 to 254; reserved 255 to 256; + reserved 259; + reserved 270; reserved 280 to 281; reserved 332 to 333; } From bd1fda6782933678be7ed8e39494aba32af871d1 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 14:27:31 -0400 Subject: [PATCH 040/744] proto: Remove `GetPrivateUserInfo` message (#36265) This PR removes the `GetPrivateUserInfo` RPC message. We're no longer using the message after https://github.com/zed-industries/zed/pull/36255. Release Notes: - N/A --- crates/client/src/test.rs | 67 +++++++++++------------------------- crates/collab/src/rpc.rs | 25 -------------- crates/proto/proto/app.proto | 9 ----- crates/proto/proto/zed.proto | 3 +- crates/proto/src/proto.rs | 3 -- 5 files changed, 21 insertions(+), 86 deletions(-) diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index 439fb100d2244499fa59a81495e282673305e00b..3c451fcb015b5efb4fd860cce09cdb29a8f07379 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -1,16 +1,12 @@ use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore}; use anyhow::{Context as _, Result, anyhow}; -use chrono::Duration; use cloud_api_client::{AuthenticatedUser, GetAuthenticatedUserResponse, PlanInfo}; use cloud_llm_client::{CurrentUsage, Plan, UsageData, UsageLimit}; use futures::{StreamExt, stream::BoxStream}; use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext}; use http_client::{AsyncBody, Method, Request, http}; use parking_lot::Mutex; -use rpc::{ - ConnectionId, Peer, Receipt, TypedEnvelope, - proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse}, -}; +use rpc::{ConnectionId, Peer, Receipt, TypedEnvelope, proto}; use std::sync::Arc; pub struct FakeServer { @@ -187,50 +183,27 @@ impl FakeServer { pub async fn receive(&self) -> Result> { self.executor.start_waiting(); - loop { - let message = self - .state - .lock() - .incoming - .as_mut() - .expect("not connected") - .next() - .await - .context("other half hung up")?; - self.executor.finish_waiting(); - let type_name = message.payload_type_name(); - let message = message.into_any(); - - if message.is::>() { - return Ok(*message.downcast().unwrap()); - } - - let accepted_tos_at = chrono::Utc::now() - .checked_sub_signed(Duration::hours(5)) - .expect("failed to build accepted_tos_at") - .timestamp() as u64; - - if message.is::>() { - self.respond( - message - .downcast::>() - .unwrap() - .receipt(), - GetPrivateUserInfoResponse { - metrics_id: "the-metrics-id".into(), - staff: false, - flags: Default::default(), - accepted_tos_at: Some(accepted_tos_at), - }, - ); - continue; - } + let message = self + .state + .lock() + .incoming + .as_mut() + .expect("not connected") + .next() + .await + .context("other half hung up")?; + self.executor.finish_waiting(); + let type_name = message.payload_type_name(); + let message = message.into_any(); - panic!( - "fake server received unexpected message type: {:?}", - type_name - ); + if message.is::>() { + return Ok(*message.downcast().unwrap()); } + + panic!( + "fake server received unexpected message type: {:?}", + type_name + ); } pub fn respond(&self, receipt: Receipt, response: T::Response) { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 715ff4e67d83095079b5ef11659953266f1e8ea1..8366b2cf136c8175efc3b3a8f2b242f5b8db3a93 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -462,7 +462,6 @@ impl Server { .add_request_handler(follow) .add_message_handler(unfollow) .add_message_handler(update_followers) - .add_request_handler(get_private_user_info) .add_request_handler(accept_terms_of_service) .add_message_handler(acknowledge_channel_message) .add_message_handler(acknowledge_buffer_version) @@ -4209,30 +4208,6 @@ async fn mark_notification_as_read( Ok(()) } -/// Get the current users information -async fn get_private_user_info( - _request: proto::GetPrivateUserInfo, - response: Response, - session: MessageContext, -) -> Result<()> { - let db = session.db().await; - - let metrics_id = db.get_user_metrics_id(session.user_id()).await?; - let user = db - .get_user_by_id(session.user_id()) - .await? - .context("user not found")?; - let flags = db.get_user_flags(session.user_id()).await?; - - response.send(proto::GetPrivateUserInfoResponse { - metrics_id, - staff: user.admin, - flags, - accepted_tos_at: user.accepted_tos_at.map(|t| t.and_utc().timestamp() as u64), - })?; - Ok(()) -} - /// Accept the terms of service (tos) on behalf of the current user async fn accept_terms_of_service( _request: proto::AcceptTermsOfService, diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index 353f19adb2e4cee68267bcbd0a4fd033a0ed9b58..66baf968e3c93447658c72336c62310142e76104 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -6,15 +6,6 @@ message UpdateInviteInfo { uint32 count = 2; } -message GetPrivateUserInfo {} - -message GetPrivateUserInfoResponse { - string metrics_id = 1; - bool staff = 2; - repeated string flags = 3; - optional uint64 accepted_tos_at = 4; -} - enum Plan { Free = 0; ZedPro = 1; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 7e7bd6b42b80554e1f9201fc1680d1a05cd7fc07..8984df29443112699afe5895f48e11a541fff817 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -135,8 +135,6 @@ message Envelope { FollowResponse follow_response = 99; UpdateFollowers update_followers = 100; Unfollow unfollow = 101; - GetPrivateUserInfo get_private_user_info = 102; - GetPrivateUserInfoResponse get_private_user_info_response = 103; UpdateUserPlan update_user_plan = 234; UpdateDiffBases update_diff_bases = 104; AcceptTermsOfService accept_terms_of_service = 239; @@ -402,6 +400,7 @@ message Envelope { } reserved 87 to 88; + reserved 102 to 103; reserved 158 to 161; reserved 164; reserved 166 to 169; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 8be9fed172cdee4a29f84a51958217697842c002..82bd1af6dbc8db71d472c6aed50eebcb08bab4d7 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -105,8 +105,6 @@ messages!( (GetPathMetadataResponse, Background), (GetPermalinkToLine, Foreground), (GetPermalinkToLineResponse, Foreground), - (GetPrivateUserInfo, Foreground), - (GetPrivateUserInfoResponse, Foreground), (GetProjectSymbols, Background), (GetProjectSymbolsResponse, Background), (GetReferences, Background), @@ -352,7 +350,6 @@ request_messages!( (GetDocumentSymbols, GetDocumentSymbolsResponse), (GetHover, GetHoverResponse), (GetNotifications, GetNotificationsResponse), - (GetPrivateUserInfo, GetPrivateUserInfoResponse), (GetProjectSymbols, GetProjectSymbolsResponse), (GetReferences, GetReferencesResponse), (GetSignatureHelp, GetSignatureHelpResponse), From 3c5d5a1d57f8569fa2818a0538d0ba950036c710 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 15 Aug 2025 20:34:22 +0200 Subject: [PATCH 041/744] editor: Add access method for `project` (#36266) This resolves a `TODO` that I've stumbled upon too many times whilst looking at the editor code. Release Notes: - N/A --- crates/diagnostics/src/diagnostics_tests.rs | 10 +++--- crates/editor/src/editor.rs | 36 ++++++++++--------- crates/editor/src/editor_tests.rs | 2 +- crates/editor/src/hover_popover.rs | 2 +- crates/editor/src/items.rs | 2 +- crates/editor/src/linked_editing_ranges.rs | 2 +- crates/editor/src/signature_help.rs | 2 +- crates/editor/src/test/editor_test_context.rs | 20 +++++------ crates/git_ui/src/conflict_view.rs | 4 +-- crates/vim/src/command.rs | 4 +-- .../zed/src/zed/edit_prediction_registry.rs | 3 +- 11 files changed, 42 insertions(+), 45 deletions(-) diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 8fb223b2cbfcc7db817059dd92bf1ff869846645..5df1b1389701d28477dc1fa1c435f41bd6079ccb 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -971,7 +971,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) let mut cx = EditorTestContext::new(cx).await; let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); cx.set_state(indoc! {" ˇfn func(abc def: i32) -> u32 { @@ -1065,7 +1065,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { let mut cx = EditorTestContext::new(cx).await; let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); cx.set_state(indoc! {" ˇfn func(abc def: i32) -> u32 { @@ -1239,7 +1239,7 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) { } "}); let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); cx.update(|_, cx| { lsp_store.update(cx, |lsp_store, cx| { @@ -1293,7 +1293,7 @@ async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) fn «test»() { println!(); } "}); let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); cx.update(|_, cx| { lsp_store.update(cx, |lsp_store, cx| { lsp_store.update_diagnostics( @@ -1450,7 +1450,7 @@ async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) { let mut cx = EditorTestContext::new(cx).await; let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); cx.set_state(indoc! {"error warning info hiˇnt"}); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a9780ed6c2711cd65fb4007abeed7795e69d5f57..f77e9ae08cd9326e3276202183970793bb44fa6e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1039,9 +1039,7 @@ pub struct Editor { inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>, soft_wrap_mode_override: Option, hard_wrap: Option, - - // TODO: make this a access method - pub project: Option>, + project: Option>, semantics_provider: Option>, completion_provider: Option>, collaboration_hub: Option>, @@ -2326,7 +2324,7 @@ impl Editor { editor.go_to_active_debug_line(window, cx); if let Some(buffer) = buffer.read(cx).as_singleton() { - if let Some(project) = editor.project.as_ref() { + if let Some(project) = editor.project() { let handle = project.update(cx, |project, cx| { project.register_buffer_with_language_servers(&buffer, cx) }); @@ -2626,6 +2624,10 @@ impl Editor { &self.buffer } + pub fn project(&self) -> Option<&Entity> { + self.project.as_ref() + } + pub fn workspace(&self) -> Option> { self.workspace.as_ref()?.0.upgrade() } @@ -5212,7 +5214,7 @@ impl Editor { restrict_to_languages: Option<&HashSet>>, cx: &mut Context, ) -> HashMap, clock::Global, Range)> { - let Some(project) = self.project.as_ref() else { + let Some(project) = self.project() else { return HashMap::default(); }; let project = project.read(cx); @@ -5294,7 +5296,7 @@ impl Editor { return None; } - let project = self.project.as_ref()?; + let project = self.project()?; let position = self.selections.newest_anchor().head(); let (buffer, buffer_position) = self .buffer @@ -6141,7 +6143,7 @@ impl Editor { cx: &mut App, ) -> Task> { maybe!({ - let project = self.project.as_ref()?; + let project = self.project()?; let dap_store = project.read(cx).dap_store(); let mut scenarios = vec![]; let resolved_tasks = resolved_tasks.as_ref()?; @@ -7907,7 +7909,7 @@ impl Editor { let snapshot = self.snapshot(window, cx); let multi_buffer_snapshot = &snapshot.display_snapshot.buffer_snapshot; - let Some(project) = self.project.as_ref() else { + let Some(project) = self.project() else { return breakpoint_display_points; }; @@ -10501,7 +10503,7 @@ impl Editor { ) { if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| { let project_path = buffer.read(cx).project_path(cx)?; - let project = self.project.as_ref()?.read(cx); + let project = self.project()?.read(cx); let entry = project.entry_for_path(&project_path, cx)?; let parent = match &entry.canonical_path { Some(canonical_path) => canonical_path.to_path_buf(), @@ -14875,7 +14877,7 @@ impl Editor { self.clear_tasks(); return Task::ready(()); } - let project = self.project.as_ref().map(Entity::downgrade); + let project = self.project().map(Entity::downgrade); let task_sources = self.lsp_task_sources(cx); let multi_buffer = self.buffer.downgrade(); cx.spawn_in(window, async move |editor, cx| { @@ -17054,7 +17056,7 @@ impl Editor { if !pull_diagnostics_settings.enabled { return None; } - let project = self.project.as_ref()?.downgrade(); + let project = self.project()?.downgrade(); let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms); let mut buffers = self.buffer.read(cx).all_buffers(); if let Some(buffer_id) = buffer_id { @@ -18018,7 +18020,7 @@ impl Editor { hunks: impl Iterator, cx: &mut App, ) -> Option<()> { - let project = self.project.as_ref()?; + let project = self.project()?; let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?; let diff = self.buffer.read(cx).diff_for(buffer_id)?; let buffer_snapshot = buffer.read(cx).snapshot(); @@ -18678,7 +18680,7 @@ impl Editor { self.active_excerpt(cx).and_then(|(_, buffer, _)| { let buffer = buffer.read(cx); if let Some(project_path) = buffer.project_path(cx) { - let project = self.project.as_ref()?.read(cx); + let project = self.project()?.read(cx); project.absolute_path(&project_path, cx) } else { buffer @@ -18691,7 +18693,7 @@ impl Editor { fn target_file_path(&self, cx: &mut Context) -> Option { self.active_excerpt(cx).and_then(|(_, buffer, _)| { let project_path = buffer.read(cx).project_path(cx)?; - let project = self.project.as_ref()?.read(cx); + let project = self.project()?.read(cx); let entry = project.entry_for_path(&project_path, cx)?; let path = entry.path.to_path_buf(); Some(path) @@ -18912,7 +18914,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if let Some(project) = self.project.as_ref() { + if let Some(project) = self.project() { let Some(buffer) = self.buffer().read(cx).as_singleton() else { return; }; @@ -19028,7 +19030,7 @@ impl Editor { return Task::ready(Err(anyhow!("failed to determine buffer and selection"))); }; - let Some(project) = self.project.as_ref() else { + let Some(project) = self.project() else { return Task::ready(Err(anyhow!("editor does not have project"))); }; @@ -21015,7 +21017,7 @@ impl Editor { cx: &mut Context, ) { let workspace = self.workspace(); - let project = self.project.as_ref(); + let project = self.project(); let save_tasks = self.buffer().update(cx, |multi_buffer, cx| { let mut tasks = Vec::new(); for (buffer_id, changes) in revert_changes { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a5966b3301b4f05aef3465a1c12e957b4c27157f..cf9954bc12d4c7514d9bc5bb9af29547a13f1768 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -15082,7 +15082,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu let mut cx = EditorTestContext::new(cx).await; let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); cx.set_state(indoc! {" ˇfn func(abc def: i32) -> u32 { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index bda229e34669482549182b2c7abbe2c3efb9a751..3fc673bad9197a9142b4027ffe77a1e123a0522a 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -251,7 +251,7 @@ fn show_hover( let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?; - let language_registry = editor.project.as_ref()?.read(cx).languages().clone(); + let language_registry = editor.project()?.read(cx).languages().clone(); let provider = editor.semantics_provider.clone()?; if !ignore_timeout { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 45a4f7365c931f241a1143b4d40a80ceb2fad5ab..34533002ff2cea587c4179d6f0f0770ff53b4b98 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -678,7 +678,7 @@ impl Item for Editor { let buffer = buffer.read(cx); let path = buffer.project_path(cx)?; let buffer_id = buffer.remote_id(); - let project = self.project.as_ref()?.read(cx); + let project = self.project()?.read(cx); let entry = project.entry_for_path(&path, cx)?; let (repo, repo_path) = project .git_store() diff --git a/crates/editor/src/linked_editing_ranges.rs b/crates/editor/src/linked_editing_ranges.rs index a185de33ca3c9522245de50e98ebf6d983acb4e0..aaf9032b04a0b4a6f20482f08917c24951aef4d1 100644 --- a/crates/editor/src/linked_editing_ranges.rs +++ b/crates/editor/src/linked_editing_ranges.rs @@ -51,7 +51,7 @@ pub(super) fn refresh_linked_ranges( if editor.pending_rename.is_some() { return None; } - let project = editor.project.as_ref()?.downgrade(); + let project = editor.project()?.downgrade(); editor.linked_editing_range_task = Some(cx.spawn_in(window, async move |editor, cx| { cx.background_executor().timer(UPDATE_DEBOUNCE).await; diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index e9f8d2dbd33f71e224ae1c868dab80a7c4bb467a..e0736a6e9f1973fba8f34e88fd4b06bfce59e6c2 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -169,7 +169,7 @@ impl Editor { else { return; }; - let Some(lsp_store) = self.project.as_ref().map(|p| p.read(cx).lsp_store()) else { + let Some(lsp_store) = self.project().map(|p| p.read(cx).lsp_store()) else { return; }; let task = lsp_store.update(cx, |lsp_store, cx| { diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index bdf73da5fbfd5d4c29826859790493fbb8494239..dbb519c40e544585b82c3f8aa9b1312fe7078590 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -297,9 +297,8 @@ impl EditorTestContext { pub fn set_head_text(&mut self, diff_base: &str) { self.cx.run_until_parked(); - let fs = self.update_editor(|editor, _, cx| { - editor.project.as_ref().unwrap().read(cx).fs().as_fake() - }); + let fs = + self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone()); fs.set_head_for_repo( &Self::root_path().join(".git"), @@ -311,18 +310,16 @@ impl EditorTestContext { pub fn clear_index_text(&mut self) { self.cx.run_until_parked(); - let fs = self.update_editor(|editor, _, cx| { - editor.project.as_ref().unwrap().read(cx).fs().as_fake() - }); + let fs = + self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); fs.set_index_for_repo(&Self::root_path().join(".git"), &[]); self.cx.run_until_parked(); } pub fn set_index_text(&mut self, diff_base: &str) { self.cx.run_until_parked(); - let fs = self.update_editor(|editor, _, cx| { - editor.project.as_ref().unwrap().read(cx).fs().as_fake() - }); + let fs = + self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone()); fs.set_index_for_repo( &Self::root_path().join(".git"), @@ -333,9 +330,8 @@ impl EditorTestContext { #[track_caller] pub fn assert_index_text(&mut self, expected: Option<&str>) { - let fs = self.update_editor(|editor, _, cx| { - editor.project.as_ref().unwrap().read(cx).fs().as_fake() - }); + let fs = + self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone()); let mut found = None; fs.with_git_state(&Self::root_path().join(".git"), false, |git_state| { diff --git a/crates/git_ui/src/conflict_view.rs b/crates/git_ui/src/conflict_view.rs index 0bbb9411be9ef8a8e6b73d11cc4d01126570741f..6482ebb9f8fa6b7fa5688a7263968319427ac2a4 100644 --- a/crates/git_ui/src/conflict_view.rs +++ b/crates/git_ui/src/conflict_view.rs @@ -112,7 +112,7 @@ fn excerpt_for_buffer_updated( } fn buffer_added(editor: &mut Editor, buffer: Entity, cx: &mut Context) { - let Some(project) = &editor.project else { + let Some(project) = editor.project() else { return; }; let git_store = project.read(cx).git_store().clone(); @@ -469,7 +469,7 @@ pub(crate) fn resolve_conflict( let Some((workspace, project, multibuffer, buffer)) = editor .update(cx, |editor, cx| { let workspace = editor.workspace()?; - let project = editor.project.clone()?; + let project = editor.project()?.clone(); let multibuffer = editor.buffer().clone(); let buffer_id = resolved_conflict.ours.end.buffer_id?; let buffer = multibuffer.read(cx).buffer(buffer_id)?; diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 264fa4bf2f0a85457917279dea1f621ea3bd8a76..ce5e5a0300387743bc9dc84ea6e895941f6885e2 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -299,7 +299,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, action: &VimSave, window, cx| { vim.update_editor(cx, |_, editor, cx| { - let Some(project) = editor.project.clone() else { + let Some(project) = editor.project().cloned() else { return; }; let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else { @@ -436,7 +436,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { let Some(workspace) = vim.workspace(window) else { return; }; - let Some(project) = editor.project.clone() else { + let Some(project) = editor.project().cloned() else { return; }; let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else { diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index da4b6e78c62db447a6d9669dd378ffe9d6fb84d2..5b0826413b1a9771f7daaa719bba96f3820245dc 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -229,8 +229,7 @@ fn assign_edit_prediction_provider( if let Some(file) = buffer.read(cx).file() { let id = file.worktree_id(cx); if let Some(inner_worktree) = editor - .project - .as_ref() + .project() .and_then(|project| project.read(cx).worktree_for_id(id, cx)) { worktree = Some(inner_worktree); From 19318897597071a64282d3bf4e1c4846485e7333 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 15 Aug 2025 14:55:34 -0400 Subject: [PATCH 042/744] thread_view: Move handlers for confirmed completions to the MessageEditor (#36214) Release Notes: - N/A --------- Co-authored-by: Conrad Irwin --- .../agent_ui/src/acp/completion_provider.rs | 435 +++++------------- crates/agent_ui/src/acp/message_editor.rs | 360 ++++++++++++--- crates/agent_ui/src/context_picker.rs | 41 +- crates/editor/src/editor.rs | 28 ++ 4 files changed, 455 insertions(+), 409 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index adcfab85b1e9cbf03a5ce5625ea235e49a73a8b6..4ee1eb69486ca32e2c7882588aa401ce908184d4 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,38 +1,34 @@ use std::ffi::OsStr; use std::ops::Range; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::Arc; use std::sync::atomic::AtomicBool; -use acp_thread::{MentionUri, selection_name}; +use acp_thread::MentionUri; use anyhow::{Context as _, Result, anyhow}; -use collections::{HashMap, HashSet}; +use collections::HashMap; use editor::display_map::CreaseId; -use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _}; +use editor::{CompletionProvider, Editor, ExcerptId}; use futures::future::{Shared, try_join_all}; -use futures::{FutureExt, TryFutureExt}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{App, Entity, ImageFormat, Img, Task, WeakEntity}; use http_client::HttpClientWithUrl; -use itertools::Itertools as _; use language::{Buffer, CodeLabel, HighlightId}; use language_model::LanguageModelImage; use lsp::CompletionContext; -use parking_lot::Mutex; use project::{ Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId, }; use prompt_store::PromptStore; use rope::Point; -use text::{Anchor, OffsetRangeExt as _, ToPoint as _}; +use text::{Anchor, ToPoint as _}; use ui::prelude::*; use url::Url; use workspace::Workspace; -use workspace::notifications::NotifyResultExt; use agent::thread_store::{TextThreadStore, ThreadStore}; -use crate::context_picker::fetch_context_picker::fetch_url_content; +use crate::acp::message_editor::MessageEditor; use crate::context_picker::file_context_picker::{FileMatch, search_files}; use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules}; use crate::context_picker::symbol_context_picker::SymbolMatch; @@ -54,7 +50,7 @@ pub struct MentionImage { #[derive(Default)] pub struct MentionSet { - uri_by_crease_id: HashMap, + pub(crate) uri_by_crease_id: HashMap, fetch_results: HashMap>>>, images: HashMap>>>, } @@ -488,36 +484,31 @@ fn search( } pub struct ContextPickerCompletionProvider { - mention_set: Arc>, workspace: WeakEntity, thread_store: WeakEntity, text_thread_store: WeakEntity, - editor: WeakEntity, + message_editor: WeakEntity, } impl ContextPickerCompletionProvider { pub fn new( - mention_set: Arc>, workspace: WeakEntity, thread_store: WeakEntity, text_thread_store: WeakEntity, - editor: WeakEntity, + message_editor: WeakEntity, ) -> Self { Self { - mention_set, workspace, thread_store, text_thread_store, - editor, + message_editor, } } fn completion_for_entry( entry: ContextPickerEntry, - excerpt_id: ExcerptId, source_range: Range, - editor: Entity, - mention_set: Arc>, + message_editor: WeakEntity, workspace: &Entity, cx: &mut App, ) -> Option { @@ -538,88 +529,39 @@ impl ContextPickerCompletionProvider { ContextPickerEntry::Action(action) => { let (new_text, on_action) = match action { ContextPickerAction::AddSelections => { - let selections = selection_ranges(workspace, cx); - const PLACEHOLDER: &str = "selection "; + let selections = selection_ranges(workspace, cx) + .into_iter() + .enumerate() + .map(|(ix, (buffer, range))| { + ( + buffer, + range, + (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1), + ) + }) + .collect::>(); - let new_text = std::iter::repeat(PLACEHOLDER) - .take(selections.len()) - .chain(std::iter::once("")) - .join(" "); + let new_text: String = PLACEHOLDER.repeat(selections.len()); let callback = Arc::new({ - let mention_set = mention_set.clone(); - let selections = selections.clone(); + let source_range = source_range.clone(); move |_, window: &mut Window, cx: &mut App| { - let editor = editor.clone(); - let mention_set = mention_set.clone(); let selections = selections.clone(); + let message_editor = message_editor.clone(); + let source_range = source_range.clone(); window.defer(cx, move |window, cx| { - let mut current_offset = 0; - - for (buffer, selection_range) in selections { - let snapshot = - editor.read(cx).buffer().read(cx).snapshot(cx); - let Some(start) = snapshot - .anchor_in_excerpt(excerpt_id, source_range.start) - else { - return; - }; - - let offset = start.to_offset(&snapshot) + current_offset; - let text_len = PLACEHOLDER.len() - 1; - - let range = snapshot.anchor_after(offset) - ..snapshot.anchor_after(offset + text_len); - - let path = buffer - .read(cx) - .file() - .map_or(PathBuf::from("untitled"), |file| { - file.path().to_path_buf() - }); - - let point_range = snapshot - .as_singleton() - .map(|(_, _, snapshot)| { - selection_range.to_point(&snapshot) - }) - .unwrap_or_default(); - let line_range = point_range.start.row..point_range.end.row; - - let uri = MentionUri::Selection { - path: path.clone(), - line_range: line_range.clone(), - }; - let crease = crate::context_picker::crease_for_mention( - selection_name(&path, &line_range).into(), - uri.icon_path(cx), - range, - editor.downgrade(), - ); - - let [crease_id]: [_; 1] = - editor.update(cx, |editor, cx| { - let crease_ids = - editor.insert_creases(vec![crease.clone()], cx); - editor.fold_creases( - vec![crease], - false, - window, - cx, - ); - crease_ids.try_into().unwrap() - }); - - mention_set.lock().insert_uri( - crease_id, - MentionUri::Selection { path, line_range }, - ); - - current_offset += text_len + 1; - } + message_editor + .update(cx, |message_editor, cx| { + message_editor.confirm_mention_for_selection( + source_range, + selections, + window, + cx, + ) + }) + .ok(); }); - false } }); @@ -647,11 +589,9 @@ impl ContextPickerCompletionProvider { fn completion_for_thread( thread_entry: ThreadContextEntry, - excerpt_id: ExcerptId, source_range: Range, recent: bool, - editor: Entity, - mention_set: Arc>, + editor: WeakEntity, cx: &mut App, ) -> Completion { let uri = match &thread_entry { @@ -683,13 +623,10 @@ impl ContextPickerCompletionProvider { source: project::CompletionSource::Custom, icon_path: Some(icon_for_completion.clone()), confirm: Some(confirm_completion_callback( - uri.icon_path(cx), thread_entry.title().clone(), - excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), - mention_set, + editor, uri, )), } @@ -697,10 +634,8 @@ impl ContextPickerCompletionProvider { fn completion_for_rules( rule: RulesContextEntry, - excerpt_id: ExcerptId, source_range: Range, - editor: Entity, - mention_set: Arc>, + editor: WeakEntity, cx: &mut App, ) -> Completion { let uri = MentionUri::Rule { @@ -719,13 +654,10 @@ impl ContextPickerCompletionProvider { source: project::CompletionSource::Custom, icon_path: Some(icon_path.clone()), confirm: Some(confirm_completion_callback( - icon_path, rule.title.clone(), - excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), - mention_set, + editor, uri, )), } @@ -736,10 +668,8 @@ impl ContextPickerCompletionProvider { path_prefix: &str, is_recent: bool, is_directory: bool, - excerpt_id: ExcerptId, source_range: Range, - editor: Entity, - mention_set: Arc>, + message_editor: WeakEntity, project: Entity, cx: &mut App, ) -> Option { @@ -777,13 +707,10 @@ impl ContextPickerCompletionProvider { icon_path: Some(completion_icon_path), insert_text_mode: None, confirm: Some(confirm_completion_callback( - crease_icon_path, file_name, - excerpt_id, source_range.start, new_text_len - 1, - editor, - mention_set.clone(), + message_editor, file_uri, )), }) @@ -791,10 +718,8 @@ impl ContextPickerCompletionProvider { fn completion_for_symbol( symbol: Symbol, - excerpt_id: ExcerptId, source_range: Range, - editor: Entity, - mention_set: Arc>, + message_editor: WeakEntity, workspace: Entity, cx: &mut App, ) -> Option { @@ -820,13 +745,10 @@ impl ContextPickerCompletionProvider { icon_path: Some(icon_path.clone()), insert_text_mode: None, confirm: Some(confirm_completion_callback( - icon_path, symbol.name.clone().into(), - excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), - mention_set.clone(), + message_editor, uri, )), }) @@ -835,112 +757,46 @@ impl ContextPickerCompletionProvider { fn completion_for_fetch( source_range: Range, url_to_fetch: SharedString, - excerpt_id: ExcerptId, - editor: Entity, - mention_set: Arc>, + message_editor: WeakEntity, http_client: Arc, cx: &mut App, ) -> Option { let new_text = format!("@fetch {} ", url_to_fetch.clone()); - let new_text_len = new_text.len(); + let url_to_fetch = url::Url::parse(url_to_fetch.as_ref()) + .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) + .ok()?; let mention_uri = MentionUri::Fetch { - url: url::Url::parse(url_to_fetch.as_ref()) - .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) - .ok()?, + url: url_to_fetch.clone(), }; let icon_path = mention_uri.icon_path(cx); Some(Completion { replace_range: source_range.clone(), - new_text, + new_text: new_text.clone(), label: CodeLabel::plain(url_to_fetch.to_string(), None), documentation: None, source: project::CompletionSource::Custom, icon_path: Some(icon_path.clone()), insert_text_mode: None, confirm: Some({ - let start = source_range.start; - let content_len = new_text_len - 1; - let editor = editor.clone(); - let url_to_fetch = url_to_fetch.clone(); - let source_range = source_range.clone(); - let icon_path = icon_path.clone(); - let mention_uri = mention_uri.clone(); Arc::new(move |_, window, cx| { - let Some(url) = url::Url::parse(url_to_fetch.as_ref()) - .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) - .notify_app_err(cx) - else { - return false; - }; - - let editor = editor.clone(); - let mention_set = mention_set.clone(); - let http_client = http_client.clone(); + let url_to_fetch = url_to_fetch.clone(); let source_range = source_range.clone(); - let icon_path = icon_path.clone(); - let mention_uri = mention_uri.clone(); + let message_editor = message_editor.clone(); + let new_text = new_text.clone(); + let http_client = http_client.clone(); window.defer(cx, move |window, cx| { - let url = url.clone(); - - let Some(crease_id) = crate::context_picker::insert_crease_for_mention( - excerpt_id, - start, - content_len, - url.to_string().into(), - icon_path, - editor.clone(), - window, - cx, - ) else { - return; - }; - - let editor = editor.clone(); - let mention_set = mention_set.clone(); - let http_client = http_client.clone(); - let source_range = source_range.clone(); - - let url_string = url.to_string(); - let fetch = cx - .background_executor() - .spawn(async move { - fetch_url_content(http_client, url_string) - .map_err(|e| e.to_string()) - .await + message_editor + .update(cx, |message_editor, cx| { + message_editor.confirm_mention_for_fetch( + new_text, + source_range, + url_to_fetch, + http_client, + window, + cx, + ) }) - .shared(); - mention_set.lock().add_fetch_result(url, fetch.clone()); - - window - .spawn(cx, async move |cx| { - if fetch.await.notify_async_err(cx).is_some() { - mention_set - .lock() - .insert_uri(crease_id, mention_uri.clone()); - } else { - // Remove crease if we failed to fetch - editor - .update(cx, |editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let Some(anchor) = snapshot - .anchor_in_excerpt(excerpt_id, source_range.start) - else { - return; - }; - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting( - vec![anchor..anchor], - true, - cx, - ); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - } - Some(()) - }) - .detach(); + .ok(); }); false }) @@ -968,7 +824,7 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: impl CompletionProvider for ContextPickerCompletionProvider { fn completions( &self, - excerpt_id: ExcerptId, + _excerpt_id: ExcerptId, buffer: &Entity, buffer_position: Anchor, _trigger: CompletionContext, @@ -999,32 +855,18 @@ impl CompletionProvider for ContextPickerCompletionProvider { let thread_store = self.thread_store.clone(); let text_thread_store = self.text_thread_store.clone(); - let editor = self.editor.clone(); + let editor = self.message_editor.clone(); + let Ok((exclude_paths, exclude_threads)) = + self.message_editor.update(cx, |message_editor, cx| { + message_editor.mentioned_path_and_threads(cx) + }) + else { + return Task::ready(Ok(Vec::new())); + }; let MentionCompletion { mode, argument, .. } = state; let query = argument.unwrap_or_else(|| "".to_string()); - let (exclude_paths, exclude_threads) = { - let mention_set = self.mention_set.lock(); - - let mut excluded_paths = HashSet::default(); - let mut excluded_threads = HashSet::default(); - - for uri in mention_set.uri_by_crease_id.values() { - match uri { - MentionUri::File { abs_path, .. } => { - excluded_paths.insert(abs_path.clone()); - } - MentionUri::Thread { id, .. } => { - excluded_threads.insert(id.clone()); - } - _ => {} - } - } - - (excluded_paths, excluded_threads) - }; - let recent_entries = recent_context_picker_entries( Some(thread_store.clone()), Some(text_thread_store.clone()), @@ -1051,13 +893,8 @@ impl CompletionProvider for ContextPickerCompletionProvider { cx, ); - let mention_set = self.mention_set.clone(); - cx.spawn(async move |_, cx| { let matches = search_task.await; - let Some(editor) = editor.upgrade() else { - return Ok(Vec::new()); - }; let completions = cx.update(|cx| { matches @@ -1074,10 +911,8 @@ impl CompletionProvider for ContextPickerCompletionProvider { &mat.path_prefix, is_recent, mat.is_dir, - excerpt_id, source_range.clone(), editor.clone(), - mention_set.clone(), project.clone(), cx, ) @@ -1085,10 +920,8 @@ impl CompletionProvider for ContextPickerCompletionProvider { Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol( symbol, - excerpt_id, source_range.clone(), editor.clone(), - mention_set.clone(), workspace.clone(), cx, ), @@ -1097,39 +930,31 @@ impl CompletionProvider for ContextPickerCompletionProvider { thread, is_recent, .. }) => Some(Self::completion_for_thread( thread, - excerpt_id, source_range.clone(), is_recent, editor.clone(), - mention_set.clone(), cx, )), Match::Rules(user_rules) => Some(Self::completion_for_rules( user_rules, - excerpt_id, source_range.clone(), editor.clone(), - mention_set.clone(), cx, )), Match::Fetch(url) => Self::completion_for_fetch( source_range.clone(), url, - excerpt_id, editor.clone(), - mention_set.clone(), http_client.clone(), cx, ), Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry( entry, - excerpt_id, source_range.clone(), editor.clone(), - mention_set.clone(), &workspace, cx, ), @@ -1182,36 +1007,30 @@ impl CompletionProvider for ContextPickerCompletionProvider { } fn confirm_completion_callback( - crease_icon_path: SharedString, crease_text: SharedString, - excerpt_id: ExcerptId, start: Anchor, content_len: usize, - editor: Entity, - mention_set: Arc>, + message_editor: WeakEntity, mention_uri: MentionUri, ) -> Arc bool + Send + Sync> { Arc::new(move |_, window, cx| { + let message_editor = message_editor.clone(); let crease_text = crease_text.clone(); - let crease_icon_path = crease_icon_path.clone(); - let editor = editor.clone(); - let mention_set = mention_set.clone(); let mention_uri = mention_uri.clone(); window.defer(cx, move |window, cx| { - if let Some(crease_id) = crate::context_picker::insert_crease_for_mention( - excerpt_id, - start, - content_len, - crease_text.clone(), - crease_icon_path, - editor.clone(), - window, - cx, - ) { - mention_set - .lock() - .insert_uri(crease_id, mention_uri.clone()); - } + message_editor + .clone() + .update(cx, |message_editor, cx| { + message_editor.confirm_completion( + crease_text, + start, + content_len, + mention_uri, + window, + cx, + ) + }) + .ok(); }); false }) @@ -1279,13 +1098,13 @@ impl MentionCompletion { #[cfg(test)] mod tests { use super::*; - use editor::AnchorRangeExt; + use editor::{AnchorRangeExt, EditorMode}; use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext}; use project::{Project, ProjectPath}; use serde_json::json; use settings::SettingsStore; use smol::stream::StreamExt as _; - use std::{ops::Deref, path::Path, rc::Rc}; + use std::{ops::Deref, path::Path}; use util::path; use workspace::{AppState, Item}; @@ -1359,9 +1178,9 @@ mod tests { assert_eq!(MentionCompletion::try_parse("test@", 0), None); } - struct AtMentionEditor(Entity); + struct MessageEditorItem(Entity); - impl Item for AtMentionEditor { + impl Item for MessageEditorItem { type Event = (); fn include_in_nav_history() -> bool { @@ -1373,15 +1192,15 @@ mod tests { } } - impl EventEmitter<()> for AtMentionEditor {} + impl EventEmitter<()> for MessageEditorItem {} - impl Focusable for AtMentionEditor { + impl Focusable for MessageEditorItem { fn focus_handle(&self, cx: &App) -> FocusHandle { self.0.read(cx).focus_handle(cx).clone() } } - impl Render for AtMentionEditor { + impl Render for MessageEditorItem { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { self.0.clone().into_any_element() } @@ -1467,19 +1286,28 @@ mod tests { opened_editors.push(buffer); } - let editor = workspace.update_in(&mut cx, |workspace, window, cx| { - let editor = cx.new(|cx| { - Editor::new( - editor::EditorMode::full(), - multi_buffer::MultiBuffer::build_simple("", cx), - None, + let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + + let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { + let workspace_handle = cx.weak_entity(); + let message_editor = cx.new(|cx| { + MessageEditor::new( + workspace_handle, + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + EditorMode::AutoHeight { + max_lines: None, + min_lines: 1, + }, window, cx, ) }); workspace.active_pane().update(cx, |pane, cx| { pane.add_item( - Box::new(cx.new(|_| AtMentionEditor(editor.clone()))), + Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), true, true, None, @@ -1487,24 +1315,9 @@ mod tests { cx, ); }); - editor - }); - - let mention_set = Arc::new(Mutex::new(MentionSet::default())); - - let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - - let editor_entity = editor.downgrade(); - editor.update_in(&mut cx, |editor, window, cx| { - window.focus(&editor.focus_handle(cx)); - editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( - mention_set.clone(), - workspace.downgrade(), - thread_store.downgrade(), - text_thread_store.downgrade(), - editor_entity, - )))); + message_editor.read(cx).focus_handle(cx).focus(window); + let editor = message_editor.read(cx).editor().clone(); + (message_editor, editor) }); cx.simulate_input("Lorem "); @@ -1573,9 +1386,9 @@ mod tests { ); }); - let contents = cx - .update(|window, cx| { - mention_set.lock().contents( + let contents = message_editor + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( project.clone(), thread_store.clone(), text_thread_store.clone(), @@ -1641,9 +1454,9 @@ mod tests { cx.run_until_parked(); - let contents = cx - .update(|window, cx| { - mention_set.lock().contents( + let contents = message_editor + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( project.clone(), thread_store.clone(), text_thread_store.clone(), @@ -1765,9 +1578,9 @@ mod tests { editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); }); - let contents = cx - .update(|window, cx| { - mention_set.lock().contents( + let contents = message_editor + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( project.clone(), thread_store, text_thread_store, diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 8d512948dd6ca4fbe6a05721ed30eab11660c2e8..32c37da519adff9c55b7cccb3c3d4e9a8d0db906 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1,56 +1,55 @@ -use crate::acp::completion_provider::ContextPickerCompletionProvider; -use crate::acp::completion_provider::MentionImage; -use crate::acp::completion_provider::MentionSet; -use acp_thread::MentionUri; -use agent::TextThreadStore; -use agent::ThreadStore; +use crate::{ + acp::completion_provider::{ContextPickerCompletionProvider, MentionImage, MentionSet}, + context_picker::fetch_context_picker::fetch_url_content, +}; +use acp_thread::{MentionUri, selection_name}; +use agent::{TextThreadStore, ThreadId, ThreadStore}; use agent_client_protocol as acp; use anyhow::Result; use collections::HashSet; -use editor::ExcerptId; -use editor::actions::Paste; -use editor::display_map::CreaseId; use editor::{ - AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, - EditorStyle, MultiBuffer, + Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, + EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, ToOffset, + actions::Paste, + display_map::{Crease, CreaseId, FoldId}, }; -use futures::FutureExt as _; -use gpui::ClipboardEntry; -use gpui::Image; -use gpui::ImageFormat; +use futures::{FutureExt as _, TryFutureExt as _}; use gpui::{ - AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, Task, TextStyle, WeakEntity, + AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image, + ImageFormat, Task, TextStyle, WeakEntity, }; -use language::Buffer; -use language::Language; +use http_client::HttpClientWithUrl; +use language::{Buffer, Language}; use language_model::LanguageModelImage; -use parking_lot::Mutex; use project::{CompletionIntent, Project}; use settings::Settings; -use std::fmt::Write; -use std::path::Path; -use std::rc::Rc; -use std::sync::Arc; +use std::{ + fmt::Write, + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, +}; +use text::OffsetRangeExt; use theme::ThemeSettings; -use ui::IconName; -use ui::SharedString; use ui::{ - ActiveTheme, App, InteractiveElement, IntoElement, ParentElement, Render, Styled, TextSize, - Window, div, + ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName, + IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement, + Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div, + h_flex, }; use util::ResultExt; -use workspace::Workspace; -use workspace::notifications::NotifyResultExt as _; +use workspace::{Workspace, notifications::NotifyResultExt as _}; use zed_actions::agent::Chat; use super::completion_provider::Mention; pub struct MessageEditor { + mention_set: MentionSet, editor: Entity, project: Entity, thread_store: Entity, text_thread_store: Entity, - mention_set: Arc>, } pub enum MessageEditorEvent { @@ -77,8 +76,13 @@ impl MessageEditor { }, None, ); - - let mention_set = Arc::new(Mutex::new(MentionSet::default())); + let completion_provider = ContextPickerCompletionProvider::new( + workspace, + thread_store.downgrade(), + text_thread_store.downgrade(), + cx.weak_entity(), + ); + let mention_set = MentionSet::default(); let editor = cx.new(|cx| { let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); @@ -88,13 +92,7 @@ impl MessageEditor { editor.set_show_indent_guides(false, cx); editor.set_soft_wrap(); editor.set_use_modal_editing(true); - editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( - mention_set.clone(), - workspace, - thread_store.downgrade(), - text_thread_store.downgrade(), - cx.weak_entity(), - )))); + editor.set_completion_provider(Some(Rc::new(completion_provider))); editor.set_context_menu_options(ContextMenuOptions { min_entries_visible: 12, max_entries_visible: 12, @@ -112,16 +110,202 @@ impl MessageEditor { } } + #[cfg(test)] + pub(crate) fn editor(&self) -> &Entity { + &self.editor + } + + #[cfg(test)] + pub(crate) fn mention_set(&mut self) -> &mut MentionSet { + &mut self.mention_set + } + pub fn is_empty(&self, cx: &App) -> bool { self.editor.read(cx).is_empty(cx) } + pub fn mentioned_path_and_threads(&self, _: &App) -> (HashSet, HashSet) { + let mut excluded_paths = HashSet::default(); + let mut excluded_threads = HashSet::default(); + + for uri in self.mention_set.uri_by_crease_id.values() { + match uri { + MentionUri::File { abs_path, .. } => { + excluded_paths.insert(abs_path.clone()); + } + MentionUri::Thread { id, .. } => { + excluded_threads.insert(id.clone()); + } + _ => {} + } + } + + (excluded_paths, excluded_threads) + } + + pub fn confirm_completion( + &mut self, + crease_text: SharedString, + start: text::Anchor, + content_len: usize, + mention_uri: MentionUri, + window: &mut Window, + cx: &mut Context, + ) { + let snapshot = self + .editor + .update(cx, |editor, cx| editor.snapshot(window, cx)); + let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { + return; + }; + + if let Some(crease_id) = crate::context_picker::insert_crease_for_mention( + *excerpt_id, + start, + content_len, + crease_text.clone(), + mention_uri.icon_path(cx), + self.editor.clone(), + window, + cx, + ) { + self.mention_set.insert_uri(crease_id, mention_uri.clone()); + } + } + + pub fn confirm_mention_for_fetch( + &mut self, + new_text: String, + source_range: Range, + url: url::Url, + http_client: Arc, + window: &mut Window, + cx: &mut Context, + ) { + let mention_uri = MentionUri::Fetch { url: url.clone() }; + let icon_path = mention_uri.icon_path(cx); + + let start = source_range.start; + let content_len = new_text.len() - 1; + + let snapshot = self + .editor + .update(cx, |editor, cx| editor.snapshot(window, cx)); + let Some((&excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { + return; + }; + + let Some(crease_id) = crate::context_picker::insert_crease_for_mention( + excerpt_id, + start, + content_len, + url.to_string().into(), + icon_path, + self.editor.clone(), + window, + cx, + ) else { + return; + }; + + let http_client = http_client.clone(); + let source_range = source_range.clone(); + + let url_string = url.to_string(); + let fetch = cx + .background_executor() + .spawn(async move { + fetch_url_content(http_client, url_string) + .map_err(|e| e.to_string()) + .await + }) + .shared(); + self.mention_set.add_fetch_result(url, fetch.clone()); + + cx.spawn_in(window, async move |this, cx| { + let fetch = fetch.await.notify_async_err(cx); + this.update(cx, |this, cx| { + if fetch.is_some() { + this.mention_set.insert_uri(crease_id, mention_uri.clone()); + } else { + // Remove crease if we failed to fetch + this.editor.update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let Some(anchor) = + snapshot.anchor_in_excerpt(excerpt_id, source_range.start) + else { + return; + }; + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }); + } + }) + .ok(); + }) + .detach(); + } + + pub fn confirm_mention_for_selection( + &mut self, + source_range: Range, + selections: Vec<(Entity, Range, Range)>, + window: &mut Window, + cx: &mut Context, + ) { + let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx); + let Some((&excerpt_id, _, _)) = snapshot.as_singleton() else { + return; + }; + let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, source_range.start) else { + return; + }; + + let offset = start.to_offset(&snapshot); + + for (buffer, selection_range, range_to_fold) in selections { + let range = snapshot.anchor_after(offset + range_to_fold.start) + ..snapshot.anchor_after(offset + range_to_fold.end); + + let path = buffer + .read(cx) + .file() + .map_or(PathBuf::from("untitled"), |file| file.path().to_path_buf()); + let snapshot = buffer.read(cx).snapshot(); + + let point_range = selection_range.to_point(&snapshot); + let line_range = point_range.start.row..point_range.end.row; + + let uri = MentionUri::Selection { + path: path.clone(), + line_range: line_range.clone(), + }; + let crease = crate::context_picker::crease_for_mention( + selection_name(&path, &line_range).into(), + uri.icon_path(cx), + range, + self.editor.downgrade(), + ); + + let crease_id = self.editor.update(cx, |editor, cx| { + let crease_ids = editor.insert_creases(vec![crease.clone()], cx); + editor.fold_creases(vec![crease], false, window, cx); + crease_ids.first().copied().unwrap() + }); + + self.mention_set + .insert_uri(crease_id, MentionUri::Selection { path, line_range }); + } + } + pub fn contents( &self, window: &mut Window, cx: &mut Context, ) -> Task>> { - let contents = self.mention_set.lock().contents( + let contents = self.mention_set.contents( self.project.clone(), self.thread_store.clone(), self.text_thread_store.clone(), @@ -198,7 +382,7 @@ impl MessageEditor { pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.clear(window, cx); - editor.remove_creases(self.mention_set.lock().drain(), cx) + editor.remove_creases(self.mention_set.drain(), cx) }); } @@ -267,9 +451,6 @@ impl MessageEditor { cx: &mut Context, ) { let buffer = self.editor.read(cx).buffer().clone(); - let Some((&excerpt_id, _, _)) = buffer.read(cx).snapshot(cx).as_singleton() else { - return; - }; let Some(buffer) = buffer.read(cx).as_singleton() else { return; }; @@ -292,10 +473,8 @@ impl MessageEditor { &path_prefix, false, entry.is_dir(), - excerpt_id, anchor..anchor, - self.editor.clone(), - self.mention_set.clone(), + cx.weak_entity(), self.project.clone(), cx, ) else { @@ -331,6 +510,7 @@ impl MessageEditor { excerpt_id, crease_start, content_len, + abs_path.clone(), self.editor.clone(), window, cx, @@ -375,7 +555,7 @@ impl MessageEditor { }) .detach(); - self.mention_set.lock().insert_image(crease_id, task); + self.mention_set.insert_image(crease_id, task); }); } @@ -429,7 +609,7 @@ impl MessageEditor { editor.buffer().read(cx).snapshot(cx) }); - self.mention_set.lock().clear(); + self.mention_set.clear(); for (range, mention_uri) in mentions { let anchor = snapshot.anchor_before(range.start); let crease_id = crate::context_picker::insert_crease_for_mention( @@ -444,7 +624,7 @@ impl MessageEditor { ); if let Some(crease_id) = crease_id { - self.mention_set.lock().insert_uri(crease_id, mention_uri); + self.mention_set.insert_uri(crease_id, mention_uri); } } for (range, content) in images { @@ -479,7 +659,7 @@ impl MessageEditor { let data: SharedString = content.data.to_string().into(); if let Some(crease_id) = crease_id { - self.mention_set.lock().insert_image( + self.mention_set.insert_image( crease_id, Task::ready(Ok(MentionImage { abs_path, @@ -550,20 +730,78 @@ pub(crate) fn insert_crease_for_image( excerpt_id: ExcerptId, anchor: text::Anchor, content_len: usize, + abs_path: Option>, editor: Entity, window: &mut Window, cx: &mut App, ) -> Option { - crate::context_picker::insert_crease_for_mention( - excerpt_id, - anchor, - content_len, - "Image".into(), - IconName::Image.path().into(), - editor, - window, - cx, - ) + let crease_label = abs_path + .as_ref() + .and_then(|path| path.file_name()) + .map(|name| name.to_string_lossy().to_string().into()) + .unwrap_or(SharedString::from("Image")); + + editor.update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + + let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?; + + let start = start.bias_right(&snapshot); + let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len); + + let placeholder = FoldPlaceholder { + render: render_image_fold_icon_button(crease_label, cx.weak_entity()), + merge_adjacent: false, + ..Default::default() + }; + + let crease = Crease::Inline { + range: start..end, + placeholder, + render_toggle: None, + render_trailer: None, + metadata: None, + }; + + let ids = editor.insert_creases(vec![crease.clone()], cx); + editor.fold_creases(vec![crease], false, window, cx); + + Some(ids[0]) + }) +} + +fn render_image_fold_icon_button( + label: SharedString, + editor: WeakEntity, +) -> Arc, &mut App) -> AnyElement> { + Arc::new({ + move |fold_id, fold_range, cx| { + let is_in_text_selection = editor + .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) + .unwrap_or_default(); + + ButtonLike::new(fold_id) + .style(ButtonStyle::Filled) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .toggle_state(is_in_text_selection) + .child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::Image) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child( + Label::new(label.clone()) + .size(LabelSize::Small) + .buffer_font(cx) + .single_line(), + ), + ) + .into_any_element() + } + }) } #[cfg(test)] diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 7dc00bfae2ecd5404b5c3ae3617f6387791f857b..6c5546c6bbb5d07d8e6c260a066ed47922b12083 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -13,7 +13,7 @@ use anyhow::{Result, anyhow}; use collections::HashSet; pub use completion_provider::ContextPickerCompletionProvider; use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId}; -use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset}; +use editor::{Anchor, Editor, ExcerptId, FoldPlaceholder, ToOffset}; use fetch_context_picker::FetchContextPicker; use file_context_picker::FileContextPicker; use file_context_picker::render_file_context_entry; @@ -837,42 +837,9 @@ fn render_fold_icon_button( ) -> Arc, &mut App) -> AnyElement> { Arc::new({ move |fold_id, fold_range, cx| { - let is_in_text_selection = editor.upgrade().is_some_and(|editor| { - editor.update(cx, |editor, cx| { - let snapshot = editor - .buffer() - .update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx)); - - let is_in_pending_selection = || { - editor - .selections - .pending - .as_ref() - .is_some_and(|pending_selection| { - pending_selection - .selection - .range() - .includes(&fold_range, &snapshot) - }) - }; - - let mut is_in_complete_selection = || { - editor - .selections - .disjoint_in_range::(fold_range.clone(), cx) - .into_iter() - .any(|selection| { - // This is needed to cover a corner case, if we just check for an existing - // selection in the fold range, having a cursor at the start of the fold - // marks it as selected. Non-empty selections don't cause this. - let length = selection.end - selection.start; - length > 0 - }) - }; - - is_in_pending_selection() || is_in_complete_selection() - }) - }); + let is_in_text_selection = editor + .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) + .unwrap_or_default(); ButtonLike::new(fold_id) .style(ButtonStyle::Filled) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f77e9ae08cd9326e3276202183970793bb44fa6e..85f2e01ed45697264ac7a677ef9d512c40a0dc6a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2369,6 +2369,34 @@ impl Editor { .is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(window)) } + pub fn is_range_selected(&mut self, range: &Range, cx: &mut Context) -> bool { + if self + .selections + .pending + .as_ref() + .is_some_and(|pending_selection| { + let snapshot = self.buffer().read(cx).snapshot(cx); + pending_selection + .selection + .range() + .includes(&range, &snapshot) + }) + { + return true; + } + + self.selections + .disjoint_in_range::(range.clone(), cx) + .into_iter() + .any(|selection| { + // This is needed to cover a corner case, if we just check for an existing + // selection in the fold range, having a cursor at the start of the fold + // marks it as selected. Non-empty selections don't cause this. + let length = selection.end - selection.start; + length > 0 + }) + } + pub fn key_context(&self, window: &Window, cx: &App) -> KeyContext { self.key_context_internal(self.has_active_edit_prediction(), window, cx) } From b3cad8b527c773c3a541e1a9e3ff23a8fbbae548 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 15:21:04 -0400 Subject: [PATCH 043/744] proto: Remove `UpdateUserPlan` message (#36268) This PR removes the `UpdateUserPlan` RPC message. We're no longer using the message after https://github.com/zed-industries/zed/pull/36255. Release Notes: - N/A --- crates/client/src/user.rs | 21 ---- crates/collab/src/llm.rs | 8 -- crates/collab/src/rpc.rs | 223 ----------------------------------- crates/proto/proto/app.proto | 10 -- crates/proto/proto/zed.proto | 3 +- crates/proto/src/proto.rs | 1 - 6 files changed, 1 insertion(+), 265 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index faf46945d888d3a3da18f69a16cdcc11009e1937..33a240eca17c435f4a1777e36282026f01761403 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -177,7 +177,6 @@ impl UserStore { let (mut current_user_tx, current_user_rx) = watch::channel(); let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded(); let rpc_subscriptions = vec![ - client.add_message_handler(cx.weak_entity(), Self::handle_update_plan), client.add_message_handler(cx.weak_entity(), Self::handle_update_contacts), client.add_message_handler(cx.weak_entity(), Self::handle_update_invite_info), client.add_message_handler(cx.weak_entity(), Self::handle_show_contacts), @@ -343,26 +342,6 @@ impl UserStore { Ok(()) } - async fn handle_update_plan( - this: Entity, - _message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let client = this - .read_with(&cx, |this, _| this.client.upgrade())? - .context("client was dropped")?; - - let response = client - .cloud_client() - .get_authenticated_user() - .await - .context("failed to fetch authenticated user")?; - - this.update(&mut cx, |this, cx| { - this.update_authenticated_user(response, cx); - }) - } - fn update_contacts(&mut self, message: UpdateContacts, cx: &Context) -> Task> { match message { UpdateContacts::Wait(barrier) => { diff --git a/crates/collab/src/llm.rs b/crates/collab/src/llm.rs index ca8e89bc6d72435b44802e878f86cec324f6cd26..dec10232bdb000acef9def25cad519ceb213956b 100644 --- a/crates/collab/src/llm.rs +++ b/crates/collab/src/llm.rs @@ -1,9 +1 @@ pub mod db; - -pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial"; - -/// The name of the feature flag that bypasses the account age check. -pub const BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG: &str = "bypass-account-age-check"; - -/// The minimum account age an account must have in order to use the LLM service. -pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 8366b2cf136c8175efc3b3a8f2b242f5b8db3a93..957cc30fe6b50b49f246bc5dae121b0e0de39fbc 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1,12 +1,6 @@ mod connection_pool; use crate::api::{CloudflareIpCountryHeader, SystemIdHeader}; -use crate::db::billing_subscription::SubscriptionKind; -use crate::llm::db::LlmDatabase; -use crate::llm::{ - AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, - MIN_ACCOUNT_AGE_FOR_LLM_USE, -}; use crate::{ AppState, Error, Result, auth, db::{ @@ -146,13 +140,6 @@ pub enum Principal { } impl Principal { - fn user(&self) -> &User { - match self { - Principal::User(user) => user, - Principal::Impersonated { user, .. } => user, - } - } - fn update_span(&self, span: &tracing::Span) { match &self { Principal::User(user) => { @@ -997,8 +984,6 @@ impl Server { .await?; } - update_user_plan(session).await?; - let contacts = self.app_state.db.get_contacts(user.id).await?; { @@ -2832,214 +2817,6 @@ fn should_auto_subscribe_to_channels(version: ZedVersion) -> bool { version.0.minor() < 139 } -async fn current_plan(db: &Arc, user_id: UserId, is_staff: bool) -> Result { - if is_staff { - return Ok(proto::Plan::ZedPro); - } - - let subscription = db.get_active_billing_subscription(user_id).await?; - let subscription_kind = subscription.and_then(|subscription| subscription.kind); - - let plan = if let Some(subscription_kind) = subscription_kind { - match subscription_kind { - SubscriptionKind::ZedPro => proto::Plan::ZedPro, - SubscriptionKind::ZedProTrial => proto::Plan::ZedProTrial, - SubscriptionKind::ZedFree => proto::Plan::Free, - } - } else { - proto::Plan::Free - }; - - Ok(plan) -} - -async fn make_update_user_plan_message( - user: &User, - is_staff: bool, - db: &Arc, - llm_db: Option>, -) -> Result { - let feature_flags = db.get_user_flags(user.id).await?; - let plan = current_plan(db, user.id, is_staff).await?; - let billing_customer = db.get_billing_customer_by_user_id(user.id).await?; - let billing_preferences = db.get_billing_preferences(user.id).await?; - - let (subscription_period, usage) = if let Some(llm_db) = llm_db { - let subscription = db.get_active_billing_subscription(user.id).await?; - - let subscription_period = - crate::db::billing_subscription::Model::current_period(subscription, is_staff); - - let usage = if let Some((period_start_at, period_end_at)) = subscription_period { - llm_db - .get_subscription_usage_for_period(user.id, period_start_at, period_end_at) - .await? - } else { - None - }; - - (subscription_period, usage) - } else { - (None, None) - }; - - let bypass_account_age_check = feature_flags - .iter() - .any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG); - let account_too_young = !matches!(plan, proto::Plan::ZedPro) - && !bypass_account_age_check - && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE; - - Ok(proto::UpdateUserPlan { - plan: plan.into(), - trial_started_at: billing_customer - .as_ref() - .and_then(|billing_customer| billing_customer.trial_started_at) - .map(|trial_started_at| trial_started_at.and_utc().timestamp() as u64), - is_usage_based_billing_enabled: if is_staff { - Some(true) - } else { - billing_preferences.map(|preferences| preferences.model_request_overages_enabled) - }, - subscription_period: subscription_period.map(|(started_at, ended_at)| { - proto::SubscriptionPeriod { - started_at: started_at.timestamp() as u64, - ended_at: ended_at.timestamp() as u64, - } - }), - account_too_young: Some(account_too_young), - has_overdue_invoices: billing_customer - .map(|billing_customer| billing_customer.has_overdue_invoices), - usage: Some( - usage - .map(|usage| subscription_usage_to_proto(plan, usage, &feature_flags)) - .unwrap_or_else(|| make_default_subscription_usage(plan, &feature_flags)), - ), - }) -} - -fn model_requests_limit( - plan: cloud_llm_client::Plan, - feature_flags: &Vec, -) -> cloud_llm_client::UsageLimit { - match plan.model_requests_limit() { - cloud_llm_client::UsageLimit::Limited(limit) => { - let limit = if plan == cloud_llm_client::Plan::ZedProTrial - && feature_flags - .iter() - .any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG) - { - 1_000 - } else { - limit - }; - - cloud_llm_client::UsageLimit::Limited(limit) - } - cloud_llm_client::UsageLimit::Unlimited => cloud_llm_client::UsageLimit::Unlimited, - } -} - -fn subscription_usage_to_proto( - plan: proto::Plan, - usage: crate::llm::db::subscription_usage::Model, - feature_flags: &Vec, -) -> proto::SubscriptionUsage { - let plan = match plan { - proto::Plan::Free => cloud_llm_client::Plan::ZedFree, - proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro, - proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial, - }; - - proto::SubscriptionUsage { - model_requests_usage_amount: usage.model_requests as u32, - model_requests_usage_limit: Some(proto::UsageLimit { - variant: Some(match model_requests_limit(plan, feature_flags) { - cloud_llm_client::UsageLimit::Limited(limit) => { - proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { - limit: limit as u32, - }) - } - cloud_llm_client::UsageLimit::Unlimited => { - proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) - } - }), - }), - edit_predictions_usage_amount: usage.edit_predictions as u32, - edit_predictions_usage_limit: Some(proto::UsageLimit { - variant: Some(match plan.edit_predictions_limit() { - cloud_llm_client::UsageLimit::Limited(limit) => { - proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { - limit: limit as u32, - }) - } - cloud_llm_client::UsageLimit::Unlimited => { - proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) - } - }), - }), - } -} - -fn make_default_subscription_usage( - plan: proto::Plan, - feature_flags: &Vec, -) -> proto::SubscriptionUsage { - let plan = match plan { - proto::Plan::Free => cloud_llm_client::Plan::ZedFree, - proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro, - proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial, - }; - - proto::SubscriptionUsage { - model_requests_usage_amount: 0, - model_requests_usage_limit: Some(proto::UsageLimit { - variant: Some(match model_requests_limit(plan, feature_flags) { - cloud_llm_client::UsageLimit::Limited(limit) => { - proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { - limit: limit as u32, - }) - } - cloud_llm_client::UsageLimit::Unlimited => { - proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) - } - }), - }), - edit_predictions_usage_amount: 0, - edit_predictions_usage_limit: Some(proto::UsageLimit { - variant: Some(match plan.edit_predictions_limit() { - cloud_llm_client::UsageLimit::Limited(limit) => { - proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { - limit: limit as u32, - }) - } - cloud_llm_client::UsageLimit::Unlimited => { - proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) - } - }), - }), - } -} - -async fn update_user_plan(session: &Session) -> Result<()> { - let db = session.db().await; - - let update_user_plan = make_update_user_plan_message( - session.principal.user(), - session.is_staff(), - &db.0, - session.app_state.llm_db.clone(), - ) - .await?; - - session - .peer - .send(session.connection_id, update_user_plan) - .trace_err(); - - Ok(()) -} - async fn subscribe_to_channels( _: proto::SubscribeToChannels, session: MessageContext, diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index 66baf968e3c93447658c72336c62310142e76104..fe6f7be1b089cf583027151227ca0ebe2465fd5c 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -12,16 +12,6 @@ enum Plan { ZedProTrial = 2; } -message UpdateUserPlan { - Plan plan = 1; - optional uint64 trial_started_at = 2; - optional bool is_usage_based_billing_enabled = 3; - optional SubscriptionUsage usage = 4; - optional SubscriptionPeriod subscription_period = 5; - optional bool account_too_young = 6; - optional bool has_overdue_invoices = 7; -} - message SubscriptionPeriod { uint64 started_at = 1; uint64 ended_at = 2; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 8984df29443112699afe5895f48e11a541fff817..4b023a46bc516e562f0d501624a1dab41c5a1e89 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -135,7 +135,6 @@ message Envelope { FollowResponse follow_response = 99; UpdateFollowers update_followers = 100; Unfollow unfollow = 101; - UpdateUserPlan update_user_plan = 234; UpdateDiffBases update_diff_bases = 104; AcceptTermsOfService accept_terms_of_service = 239; AcceptTermsOfServiceResponse accept_terms_of_service_response = 240; @@ -414,7 +413,7 @@ message Envelope { reserved 221; reserved 224 to 229; reserved 230 to 231; - reserved 235 to 236; + reserved 234 to 236; reserved 246; reserved 247 to 254; reserved 255 to 256; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 82bd1af6dbc8db71d472c6aed50eebcb08bab4d7..18abf31c645c715cb133d9de050e241699596547 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -275,7 +275,6 @@ messages!( (UpdateProject, Foreground), (UpdateProjectCollaborator, Foreground), (UpdateUserChannels, Foreground), - (UpdateUserPlan, Foreground), (UpdateWorktree, Foreground), (UpdateWorktreeSettings, Foreground), (UpdateRepository, Foreground), From 75f85b3aaa202f07185a39d855143851f609ddf7 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 15 Aug 2025 15:37:52 -0400 Subject: [PATCH 044/744] Remove old telemetry events and transformation layer (#36263) Successor to: https://github.com/zed-industries/zed/pull/25179 Release Notes: - N/A --- crates/collab/src/api/events.rs | 166 +----------------- .../telemetry_events/src/telemetry_events.rs | 108 +----------- 2 files changed, 4 insertions(+), 270 deletions(-) diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 2f34a843a860d9d2933a4819788d0f9285473edf..cd1dc42e64460655c59f3cffe022dcc7a2ed431a 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -564,170 +564,10 @@ fn for_snowflake( country_code: Option, checksum_matched: bool, ) -> impl Iterator { - body.events.into_iter().filter_map(move |event| { + body.events.into_iter().map(move |event| { let timestamp = first_event_at + Duration::milliseconds(event.milliseconds_since_first_event); - // We will need to double check, but I believe all of the events that - // are being transformed here are now migrated over to use the - // telemetry::event! macro, as of this commit so this code can go away - // when we feel enough users have upgraded past this point. let (event_type, mut event_properties) = match &event.event { - Event::Editor(e) => ( - match e.operation.as_str() { - "open" => "Editor Opened".to_string(), - "save" => "Editor Saved".to_string(), - _ => format!("Unknown Editor Event: {}", e.operation), - }, - serde_json::to_value(e).unwrap(), - ), - Event::EditPrediction(e) => ( - format!( - "Edit Prediction {}", - if e.suggestion_accepted { - "Accepted" - } else { - "Discarded" - } - ), - serde_json::to_value(e).unwrap(), - ), - Event::EditPredictionRating(e) => ( - "Edit Prediction Rated".to_string(), - serde_json::to_value(e).unwrap(), - ), - Event::Call(e) => { - let event_type = match e.operation.trim() { - "unshare project" => "Project Unshared".to_string(), - "open channel notes" => "Channel Notes Opened".to_string(), - "share project" => "Project Shared".to_string(), - "join channel" => "Channel Joined".to_string(), - "hang up" => "Call Ended".to_string(), - "accept incoming" => "Incoming Call Accepted".to_string(), - "invite" => "Participant Invited".to_string(), - "disable microphone" => "Microphone Disabled".to_string(), - "enable microphone" => "Microphone Enabled".to_string(), - "enable screen share" => "Screen Share Enabled".to_string(), - "disable screen share" => "Screen Share Disabled".to_string(), - "decline incoming" => "Incoming Call Declined".to_string(), - _ => format!("Unknown Call Event: {}", e.operation), - }; - - (event_type, serde_json::to_value(e).unwrap()) - } - Event::Assistant(e) => ( - match e.phase { - telemetry_events::AssistantPhase::Response => "Assistant Responded".to_string(), - telemetry_events::AssistantPhase::Invoked => "Assistant Invoked".to_string(), - telemetry_events::AssistantPhase::Accepted => { - "Assistant Response Accepted".to_string() - } - telemetry_events::AssistantPhase::Rejected => { - "Assistant Response Rejected".to_string() - } - }, - serde_json::to_value(e).unwrap(), - ), - Event::Cpu(_) | Event::Memory(_) => return None, - Event::App(e) => { - let mut properties = json!({}); - let event_type = match e.operation.trim() { - // App - "open" => "App Opened".to_string(), - "first open" => "App First Opened".to_string(), - "first open for release channel" => { - "App First Opened For Release Channel".to_string() - } - "close" => "App Closed".to_string(), - - // Project - "open project" => "Project Opened".to_string(), - "open node project" => { - properties["project_type"] = json!("node"); - "Project Opened".to_string() - } - "open pnpm project" => { - properties["project_type"] = json!("pnpm"); - "Project Opened".to_string() - } - "open yarn project" => { - properties["project_type"] = json!("yarn"); - "Project Opened".to_string() - } - - // SSH - "create ssh server" => "SSH Server Created".to_string(), - "create ssh project" => "SSH Project Created".to_string(), - "open ssh project" => "SSH Project Opened".to_string(), - - // Welcome Page - "welcome page: change keymap" => "Welcome Keymap Changed".to_string(), - "welcome page: change theme" => "Welcome Theme Changed".to_string(), - "welcome page: close" => "Welcome Page Closed".to_string(), - "welcome page: edit settings" => "Welcome Settings Edited".to_string(), - "welcome page: install cli" => "Welcome CLI Installed".to_string(), - "welcome page: open" => "Welcome Page Opened".to_string(), - "welcome page: open extensions" => "Welcome Extensions Page Opened".to_string(), - "welcome page: sign in to copilot" => "Welcome Copilot Signed In".to_string(), - "welcome page: toggle diagnostic telemetry" => { - "Welcome Diagnostic Telemetry Toggled".to_string() - } - "welcome page: toggle metric telemetry" => { - "Welcome Metric Telemetry Toggled".to_string() - } - "welcome page: toggle vim" => "Welcome Vim Mode Toggled".to_string(), - "welcome page: view docs" => "Welcome Documentation Viewed".to_string(), - - // Extensions - "extensions page: open" => "Extensions Page Opened".to_string(), - "extensions: install extension" => "Extension Installed".to_string(), - "extensions: uninstall extension" => "Extension Uninstalled".to_string(), - - // Misc - "markdown preview: open" => "Markdown Preview Opened".to_string(), - "project diagnostics: open" => "Project Diagnostics Opened".to_string(), - "project search: open" => "Project Search Opened".to_string(), - "repl sessions: open" => "REPL Session Started".to_string(), - - // Feature Upsell - "feature upsell: toggle vim" => { - properties["source"] = json!("Feature Upsell"); - "Vim Mode Toggled".to_string() - } - _ => e - .operation - .strip_prefix("feature upsell: viewed docs (") - .and_then(|s| s.strip_suffix(')')) - .map_or_else( - || format!("Unknown App Event: {}", e.operation), - |docs_url| { - properties["url"] = json!(docs_url); - properties["source"] = json!("Feature Upsell"); - "Documentation Viewed".to_string() - }, - ), - }; - (event_type, properties) - } - Event::Setting(e) => ( - "Settings Changed".to_string(), - serde_json::to_value(e).unwrap(), - ), - Event::Extension(e) => ( - "Extension Loaded".to_string(), - serde_json::to_value(e).unwrap(), - ), - Event::Edit(e) => ( - "Editor Edited".to_string(), - serde_json::to_value(e).unwrap(), - ), - Event::Action(e) => ( - "Action Invoked".to_string(), - serde_json::to_value(e).unwrap(), - ), - Event::Repl(e) => ( - "Kernel Status Changed".to_string(), - serde_json::to_value(e).unwrap(), - ), Event::Flexible(e) => ( e.event_type.clone(), serde_json::to_value(&e.event_properties).unwrap(), @@ -759,7 +599,7 @@ fn for_snowflake( }) }); - Some(SnowflakeRow { + SnowflakeRow { time: timestamp, user_id: body.metrics_id.clone(), device_id: body.system_id.clone(), @@ -767,7 +607,7 @@ fn for_snowflake( event_properties, user_properties, insert_id: Some(Uuid::new_v4().to_string()), - }) + } }) } diff --git a/crates/telemetry_events/src/telemetry_events.rs b/crates/telemetry_events/src/telemetry_events.rs index 735a1310ae063befb056563fe8050e8fda153941..12d8d4c04b1da0b2483cc8bc60e1d94b5cbb9193 100644 --- a/crates/telemetry_events/src/telemetry_events.rs +++ b/crates/telemetry_events/src/telemetry_events.rs @@ -2,7 +2,7 @@ use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt::Display, sync::Arc, time::Duration}; +use std::{collections::HashMap, fmt::Display, time::Duration}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct EventRequestBody { @@ -93,19 +93,6 @@ impl Display for AssistantPhase { #[serde(tag = "type")] pub enum Event { Flexible(FlexibleEvent), - Editor(EditorEvent), - EditPrediction(EditPredictionEvent), - EditPredictionRating(EditPredictionRatingEvent), - Call(CallEvent), - Assistant(AssistantEventData), - Cpu(CpuEvent), - Memory(MemoryEvent), - App(AppEvent), - Setting(SettingEvent), - Extension(ExtensionEvent), - Edit(EditEvent), - Action(ActionEvent), - Repl(ReplEvent), } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -114,54 +101,12 @@ pub struct FlexibleEvent { pub event_properties: HashMap, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct EditorEvent { - /// The editor operation performed (open, save) - pub operation: String, - /// The extension of the file that was opened or saved - pub file_extension: Option, - /// Whether the user is in vim mode or not - pub vim_mode: bool, - /// Whether the user has copilot enabled or not - pub copilot_enabled: bool, - /// Whether the user has copilot enabled for the language of the file opened or saved - pub copilot_enabled_for_language: bool, - /// Whether the client is opening/saving a local file or a remote file via SSH - #[serde(default)] - pub is_via_ssh: bool, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct EditPredictionEvent { - /// Provider of the completion suggestion (e.g. copilot, supermaven) - pub provider: String, - pub suggestion_accepted: bool, - pub file_extension: Option, -} - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum EditPredictionRating { Positive, Negative, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct EditPredictionRatingEvent { - pub rating: EditPredictionRating, - pub input_events: Arc, - pub input_excerpt: Arc, - pub output_excerpt: Arc, - pub feedback: String, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct CallEvent { - /// Operation performed: invite/join call; begin/end screenshare; share/unshare project; etc - pub operation: String, - pub room_id: Option, - pub channel_id: Option, -} - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct AssistantEventData { /// Unique random identifier for each assistant tab (None for inline assist) @@ -180,57 +125,6 @@ pub struct AssistantEventData { pub language_name: Option, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct CpuEvent { - pub usage_as_percentage: f32, - pub core_count: u32, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct MemoryEvent { - pub memory_in_bytes: u64, - pub virtual_memory_in_bytes: u64, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct ActionEvent { - pub source: String, - pub action: String, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct EditEvent { - pub duration: i64, - pub environment: String, - /// Whether the edits occurred locally or remotely via SSH - #[serde(default)] - pub is_via_ssh: bool, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct SettingEvent { - pub setting: String, - pub value: String, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct ExtensionEvent { - pub extension_id: Arc, - pub version: Arc, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct AppEvent { - pub operation: String, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct ReplEvent { - pub kernel_language: String, - pub kernel_status: String, - pub repl_session_id: String, -} - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct BacktraceFrame { pub ip: usize, From 2a9d4599cdeb61d5f6cf90f01d7475b14bf5b510 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 15:46:23 -0400 Subject: [PATCH 045/744] proto: Remove unused types (#36269) This PR removes some unused types from the RPC protocol. Release Notes: - N/A --- .../agent_ui/src/language_model_selector.rs | 6 ++-- crates/client/src/user.rs | 13 -------- crates/proto/proto/app.proto | 31 ------------------- 3 files changed, 3 insertions(+), 47 deletions(-) diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 7121624c87f6e44ba73f8380bfdf60227cba5b90..bb8514a224dd1af3c4668be87d8a02e1d3a0e9be 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -1,5 +1,6 @@ use std::{cmp::Reverse, sync::Arc}; +use cloud_llm_client::Plan; use collections::{HashSet, IndexMap}; use feature_flags::ZedProFeatureFlag; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; @@ -10,7 +11,6 @@ use language_model::{ }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; -use proto::Plan; use ui::{ListItem, ListItemSpacing, prelude::*}; const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro"; @@ -536,7 +536,7 @@ impl PickerDelegate for LanguageModelPickerDelegate { ) -> Option { use feature_flags::FeatureFlagAppExt; - let plan = proto::Plan::ZedPro; + let plan = Plan::ZedPro; Some( h_flex() @@ -557,7 +557,7 @@ impl PickerDelegate for LanguageModelPickerDelegate { window .dispatch_action(Box::new(zed_actions::OpenAccountSettings), cx) }), - Plan::Free | Plan::ZedProTrial => Button::new( + Plan::ZedFree | Plan::ZedProTrial => Button::new( "try-pro", if plan == Plan::ZedProTrial { "Upgrade to Pro" diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 33a240eca17c435f4a1777e36282026f01761403..da7f50076b38a7ecf5dbce5a8f229f2912629409 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -998,19 +998,6 @@ impl RequestUsage { } } - pub fn from_proto(amount: u32, limit: proto::UsageLimit) -> Option { - let limit = match limit.variant? { - proto::usage_limit::Variant::Limited(limited) => { - UsageLimit::Limited(limited.limit as i32) - } - proto::usage_limit::Variant::Unlimited(_) => UsageLimit::Unlimited, - }; - Some(RequestUsage { - limit, - amount: amount as i32, - }) - } - fn from_headers( limit_name: &str, amount_name: &str, diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index fe6f7be1b089cf583027151227ca0ebe2465fd5c..9611b607d0021f3138faa2fe8d1e3dbf95614018 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -6,37 +6,6 @@ message UpdateInviteInfo { uint32 count = 2; } -enum Plan { - Free = 0; - ZedPro = 1; - ZedProTrial = 2; -} - -message SubscriptionPeriod { - uint64 started_at = 1; - uint64 ended_at = 2; -} - -message SubscriptionUsage { - uint32 model_requests_usage_amount = 1; - UsageLimit model_requests_usage_limit = 2; - uint32 edit_predictions_usage_amount = 3; - UsageLimit edit_predictions_usage_limit = 4; -} - -message UsageLimit { - oneof variant { - Limited limited = 1; - Unlimited unlimited = 2; - } - - message Limited { - uint32 limit = 1; - } - - message Unlimited {} -} - message AcceptTermsOfService {} message AcceptTermsOfServiceResponse { From 65f64aa5138a4cfcede025648cda973eeae21021 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 15 Aug 2025 22:21:21 +0200 Subject: [PATCH 046/744] search: Fix recently introduced issues with the search bars (#36271) Follow-up to https://github.com/zed-industries/zed/pull/36233 The above PR simplified the handling but introduced some bugs: The replace buttons were no longer clickable, some buttons also lost their toggle states, some buttons shared their element id and, lastly, some buttons were clickable but would not trigger the right action. This PR fixes all that. Release Notes: - N/A --- crates/search/src/buffer_search.rs | 53 +++++++++++++++----------- crates/search/src/project_search.rs | 59 +++++++++++++++++------------ crates/search/src/search.rs | 55 +++++++++++++++++++-------- crates/search/src/search_bar.rs | 12 +++++- 4 files changed, 114 insertions(+), 65 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index da2d35d74cae3a8bc3c183fe034af20e07826f84..189f48e6b6f5d46e442f25b75671136163194872 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -2,9 +2,9 @@ mod registrar; use crate::{ FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption, - SearchOptions, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, - ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord, - search_bar::{input_base_styles, render_action_button, render_text_input}, + SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, + ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord, + search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, }; use any_vec::AnyVec; use anyhow::Context as _; @@ -213,22 +213,25 @@ impl Render for BufferSearchBar { h_flex() .gap_1() .when(case, |div| { - div.child( - SearchOption::CaseSensitive - .as_button(self.search_options, focus_handle.clone()), - ) + div.child(SearchOption::CaseSensitive.as_button( + self.search_options, + SearchSource::Buffer, + focus_handle.clone(), + )) }) .when(word, |div| { - div.child( - SearchOption::WholeWord - .as_button(self.search_options, focus_handle.clone()), - ) + div.child(SearchOption::WholeWord.as_button( + self.search_options, + SearchSource::Buffer, + focus_handle.clone(), + )) }) .when(regex, |div| { - div.child( - SearchOption::Regex - .as_button(self.search_options, focus_handle.clone()), - ) + div.child(SearchOption::Regex.as_button( + self.search_options, + SearchSource::Buffer, + focus_handle.clone(), + )) }), ) }); @@ -240,7 +243,7 @@ impl Render for BufferSearchBar { this.child(render_action_button( "buffer-search-bar-toggle", IconName::Replace, - self.replace_enabled, + self.replace_enabled.then_some(ActionButtonState::Toggled), "Toggle Replace", &ToggleReplace, focus_handle.clone(), @@ -285,7 +288,9 @@ impl Render for BufferSearchBar { .child(render_action_button( "buffer-search-nav-button", ui::IconName::ChevronLeft, - self.active_match_index.is_some(), + self.active_match_index + .is_none() + .then_some(ActionButtonState::Disabled), "Select Previous Match", &SelectPreviousMatch, query_focus.clone(), @@ -293,7 +298,9 @@ impl Render for BufferSearchBar { .child(render_action_button( "buffer-search-nav-button", ui::IconName::ChevronRight, - self.active_match_index.is_some(), + self.active_match_index + .is_none() + .then_some(ActionButtonState::Disabled), "Select Next Match", &SelectNextMatch, query_focus.clone(), @@ -313,7 +320,7 @@ impl Render for BufferSearchBar { el.child(render_action_button( "buffer-search-nav-button", IconName::SelectAll, - true, + Default::default(), "Select All Matches", &SelectAllMatches, query_focus, @@ -324,7 +331,7 @@ impl Render for BufferSearchBar { el.child(render_action_button( "buffer-search", IconName::Close, - true, + Default::default(), "Close Search Bar", &Dismiss, focus_handle.clone(), @@ -352,7 +359,7 @@ impl Render for BufferSearchBar { .child(render_action_button( "buffer-search-replace-button", IconName::ReplaceNext, - true, + Default::default(), "Replace Next Match", &ReplaceNext, focus_handle.clone(), @@ -360,7 +367,7 @@ impl Render for BufferSearchBar { .child(render_action_button( "buffer-search-replace-button", IconName::ReplaceAll, - true, + Default::default(), "Replace All Matches", &ReplaceAll, focus_handle, @@ -394,7 +401,7 @@ impl Render for BufferSearchBar { div.child(h_flex().absolute().right_0().child(render_action_button( "buffer-search", IconName::Close, - true, + Default::default(), "Close Search Bar", &Dismiss, focus_handle.clone(), diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b791f748adfcd4714b7853e5701b051f26f09d60..056c3556ba0442b9a12b2f06192b4f5c2a4e3213 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,9 +1,9 @@ use crate::{ BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, - SearchOption, SearchOptions, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, - ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord, + SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch, + ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord, buffer_search::Deploy, - search_bar::{input_base_styles, render_action_button, render_text_input}, + search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, }; use anyhow::Context as _; use collections::HashMap; @@ -1665,7 +1665,7 @@ impl ProjectSearchBar { }); } - fn toggle_search_option( + pub(crate) fn toggle_search_option( &mut self, option: SearchOptions, window: &mut Window, @@ -1962,17 +1962,21 @@ impl Render for ProjectSearchBar { .child( h_flex() .gap_1() - .child( - SearchOption::CaseSensitive - .as_button(search.search_options, focus_handle.clone()), - ) - .child( - SearchOption::WholeWord - .as_button(search.search_options, focus_handle.clone()), - ) - .child( - SearchOption::Regex.as_button(search.search_options, focus_handle.clone()), - ), + .child(SearchOption::CaseSensitive.as_button( + search.search_options, + SearchSource::Project(cx), + focus_handle.clone(), + )) + .child(SearchOption::WholeWord.as_button( + search.search_options, + SearchSource::Project(cx), + focus_handle.clone(), + )) + .child(SearchOption::Regex.as_button( + search.search_options, + SearchSource::Project(cx), + focus_handle.clone(), + )), ); let query_focus = search.query_editor.focus_handle(cx); @@ -1985,7 +1989,10 @@ impl Render for ProjectSearchBar { .child(render_action_button( "project-search-nav-button", IconName::ChevronLeft, - search.active_match_index.is_some(), + search + .active_match_index + .is_none() + .then_some(ActionButtonState::Disabled), "Select Previous Match", &SelectPreviousMatch, query_focus.clone(), @@ -1993,7 +2000,10 @@ impl Render for ProjectSearchBar { .child(render_action_button( "project-search-nav-button", IconName::ChevronRight, - search.active_match_index.is_some(), + search + .active_match_index + .is_none() + .then_some(ActionButtonState::Disabled), "Select Next Match", &SelectNextMatch, query_focus, @@ -2054,7 +2064,7 @@ impl Render for ProjectSearchBar { self.active_project_search .as_ref() .map(|search| search.read(cx).replace_enabled) - .unwrap_or_default(), + .and_then(|enabled| enabled.then_some(ActionButtonState::Toggled)), "Toggle Replace", &ToggleReplace, focus_handle.clone(), @@ -2079,7 +2089,7 @@ impl Render for ProjectSearchBar { .child(render_action_button( "project-search-replace-button", IconName::ReplaceNext, - true, + Default::default(), "Replace Next Match", &ReplaceNext, focus_handle.clone(), @@ -2087,7 +2097,7 @@ impl Render for ProjectSearchBar { .child(render_action_button( "project-search-replace-button", IconName::ReplaceAll, - true, + Default::default(), "Replace All Matches", &ReplaceAll, focus_handle, @@ -2129,10 +2139,11 @@ impl Render for ProjectSearchBar { this.toggle_opened_only(window, cx); })), ) - .child( - SearchOption::IncludeIgnored - .as_button(search.search_options, focus_handle.clone()), - ); + .child(SearchOption::IncludeIgnored.as_button( + search.search_options, + SearchSource::Project(cx), + focus_handle.clone(), + )); h_flex() .w_full() .gap_2() diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 89064e0a27b64e0144dce0bc1f9d8a5a06031949..904c74d03c9606c2864513026cf88e017aaba74a 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,7 +1,7 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use editor::SearchSettings; -use gpui::{Action, App, FocusHandle, IntoElement, actions}; +use gpui::{Action, App, ClickEvent, FocusHandle, IntoElement, actions}; use project::search::SearchQuery; pub use project_search::ProjectSearchView; use ui::{ButtonStyle, IconButton, IconButtonShape}; @@ -11,6 +11,8 @@ use workspace::{Toast, Workspace}; pub use search_status_button::SEARCH_ICON; +use crate::project_search::ProjectSearchBar; + pub mod buffer_search; pub mod project_search; pub(crate) mod search_bar; @@ -83,9 +85,14 @@ pub enum SearchOption { Backwards, } +pub(crate) enum SearchSource<'a, 'b> { + Buffer, + Project(&'a Context<'b, ProjectSearchBar>), +} + impl SearchOption { - pub fn as_options(self) -> SearchOptions { - SearchOptions::from_bits(1 << self as u8).unwrap() + pub fn as_options(&self) -> SearchOptions { + SearchOptions::from_bits(1 << *self as u8).unwrap() } pub fn label(&self) -> &'static str { @@ -119,25 +126,41 @@ impl SearchOption { } } - pub fn as_button(&self, active: SearchOptions, focus_handle: FocusHandle) -> impl IntoElement { + pub(crate) fn as_button( + &self, + active: SearchOptions, + search_source: SearchSource, + focus_handle: FocusHandle, + ) -> impl IntoElement { let action = self.to_toggle_action(); let label = self.label(); - IconButton::new(label, self.icon()) - .on_click({ + IconButton::new( + (label, matches!(search_source, SearchSource::Buffer) as u32), + self.icon(), + ) + .map(|button| match search_source { + SearchSource::Buffer => { let focus_handle = focus_handle.clone(); - move |_, window, cx| { + button.on_click(move |_: &ClickEvent, window, cx| { if !focus_handle.is_focused(&window) { window.focus(&focus_handle); } - window.dispatch_action(action.boxed_clone(), cx) - } - }) - .style(ButtonStyle::Subtle) - .shape(IconButtonShape::Square) - .toggle_state(active.contains(self.as_options())) - .tooltip({ - move |window, cx| Tooltip::for_action_in(label, action, &focus_handle, window, cx) - }) + window.dispatch_action(action.boxed_clone(), cx); + }) + } + SearchSource::Project(cx) => { + let options = self.as_options(); + button.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { + this.toggle_search_option(options, window, cx); + })) + } + }) + .style(ButtonStyle::Subtle) + .shape(IconButtonShape::Square) + .toggle_state(active.contains(self.as_options())) + .tooltip({ + move |window, cx| Tooltip::for_action_in(label, action, &focus_handle, window, cx) + }) } } diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 094ce3638ed539431a51ee0aac802453c9b79f70..8cc838a8a69e0278af49b7fc28062ebf70f3fc49 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -5,10 +5,15 @@ use theme::ThemeSettings; use ui::{IconButton, IconButtonShape}; use ui::{Tooltip, prelude::*}; +pub(super) enum ActionButtonState { + Disabled, + Toggled, +} + pub(super) fn render_action_button( id_prefix: &'static str, icon: ui::IconName, - active: bool, + button_state: Option, tooltip: &'static str, action: &'static dyn Action, focus_handle: FocusHandle, @@ -28,7 +33,10 @@ pub(super) fn render_action_button( } }) .tooltip(move |window, cx| Tooltip::for_action_in(tooltip, action, &focus_handle, window, cx)) - .disabled(!active) + .when_some(button_state, |this, state| match state { + ActionButtonState::Toggled => this.toggle_state(true), + ActionButtonState::Disabled => this.disabled(true), + }) } pub(crate) fn input_base_styles(border_color: Hsla, map: impl FnOnce(Div) -> Div) -> Div { From 7199c733b252f62f84135e0b9102fab22d5480e5 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 16:21:45 -0400 Subject: [PATCH 047/744] proto: Remove `AcceptTermsOfService` message (#36272) This PR removes the `AcceptTermsOfService` RPC message. We're no longer using the message after https://github.com/zed-industries/zed/pull/36255. Release Notes: - N/A --- crates/collab/src/rpc.rs | 21 --------------------- crates/proto/proto/app.proto | 6 ------ crates/proto/proto/zed.proto | 3 +-- crates/proto/src/proto.rs | 3 --- 4 files changed, 1 insertion(+), 32 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 957cc30fe6b50b49f246bc5dae121b0e0de39fbc..ef749ac9b7de96422a91f20c2b02ec91fab87d3c 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -29,7 +29,6 @@ use axum::{ response::IntoResponse, routing::get, }; -use chrono::Utc; use collections::{HashMap, HashSet}; pub use connection_pool::{ConnectionPool, ZedVersion}; use core::fmt::{self, Debug, Formatter}; @@ -449,7 +448,6 @@ impl Server { .add_request_handler(follow) .add_message_handler(unfollow) .add_message_handler(update_followers) - .add_request_handler(accept_terms_of_service) .add_message_handler(acknowledge_channel_message) .add_message_handler(acknowledge_buffer_version) .add_request_handler(get_supermaven_api_key) @@ -3985,25 +3983,6 @@ async fn mark_notification_as_read( Ok(()) } -/// Accept the terms of service (tos) on behalf of the current user -async fn accept_terms_of_service( - _request: proto::AcceptTermsOfService, - response: Response, - session: MessageContext, -) -> Result<()> { - let db = session.db().await; - - let accepted_tos_at = Utc::now(); - db.set_user_accepted_tos_at(session.user_id(), Some(accepted_tos_at.naive_utc())) - .await?; - - response.send(proto::AcceptTermsOfServiceResponse { - accepted_tos_at: accepted_tos_at.timestamp() as u64, - })?; - - Ok(()) -} - fn to_axum_message(message: TungsteniteMessage) -> anyhow::Result { let message = match message { TungsteniteMessage::Text(payload) => AxumMessage::Text(payload.as_str().to_string()), diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index 9611b607d0021f3138faa2fe8d1e3dbf95614018..1f2ab1f539fb7d31ecd75a9f802e108433a89417 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -6,12 +6,6 @@ message UpdateInviteInfo { uint32 count = 2; } -message AcceptTermsOfService {} - -message AcceptTermsOfServiceResponse { - uint64 accepted_tos_at = 1; -} - message ShutdownRemoteServer {} message Toast { diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 4b023a46bc516e562f0d501624a1dab41c5a1e89..310fcf584e99a82606fdfdf39237b808adc61c9f 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -136,8 +136,6 @@ message Envelope { UpdateFollowers update_followers = 100; Unfollow unfollow = 101; UpdateDiffBases update_diff_bases = 104; - AcceptTermsOfService accept_terms_of_service = 239; - AcceptTermsOfServiceResponse accept_terms_of_service_response = 240; OnTypeFormatting on_type_formatting = 105; OnTypeFormattingResponse on_type_formatting_response = 106; @@ -414,6 +412,7 @@ message Envelope { reserved 224 to 229; reserved 230 to 231; reserved 234 to 236; + reserved 239 to 240; reserved 246; reserved 247 to 254; reserved 255 to 256; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 18abf31c645c715cb133d9de050e241699596547..802db09590a5bb6fc316ce31bd880d394c06c5ca 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -20,8 +20,6 @@ pub const SSH_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 }; pub const SSH_PROJECT_ID: u64 = 0; messages!( - (AcceptTermsOfService, Foreground), - (AcceptTermsOfServiceResponse, Foreground), (Ack, Foreground), (AckBufferOperation, Background), (AckChannelMessage, Background), @@ -315,7 +313,6 @@ messages!( ); request_messages!( - (AcceptTermsOfService, AcceptTermsOfServiceResponse), (ApplyCodeAction, ApplyCodeActionResponse), ( ApplyCompletionAdditionalEdits, From 3e0a755486201a2fe6e77213af68494a784a4895 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 15 Aug 2025 22:27:44 +0200 Subject: [PATCH 048/744] Remove some redundant entity clones (#36274) `cx.entity()` already returns an owned entity, so there is no need for these clones. Release Notes: - N/A --- crates/agent_ui/src/context_picker.rs | 2 +- crates/agent_ui/src/inline_assistant.rs | 2 +- crates/agent_ui/src/profile_selector.rs | 2 +- crates/collab_ui/src/chat_panel.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 8 +- .../src/collab_panel/channel_modal.rs | 2 +- crates/collab_ui/src/notification_panel.rs | 4 +- crates/debugger_ui/src/session/running.rs | 4 +- .../src/edit_prediction_button.rs | 6 +- crates/editor/src/editor_tests.rs | 13 +-- crates/editor/src/element.rs | 2 +- crates/extensions_ui/src/extensions_ui.rs | 2 +- crates/git_ui/src/git_panel.rs | 2 +- crates/gpui/examples/input.rs | 4 +- crates/language_tools/src/lsp_log.rs | 2 +- crates/language_tools/src/lsp_tool.rs | 2 +- crates/language_tools/src/syntax_tree_view.rs | 2 +- crates/outline_panel/src/outline_panel.rs | 82 +++++++++---------- crates/project_panel/src/project_panel.rs | 42 +++++----- crates/recent_projects/src/remote_servers.rs | 4 +- crates/repl/src/session.rs | 2 +- crates/storybook/src/stories/indent_guides.rs | 2 +- crates/terminal_view/src/terminal_panel.rs | 4 +- crates/terminal_view/src/terminal_view.rs | 2 +- crates/vim/src/mode_indicator.rs | 4 +- crates/vim/src/normal/search.rs | 2 +- crates/vim/src/vim.rs | 2 +- crates/workspace/src/dock.rs | 2 +- crates/workspace/src/notifications.rs | 2 +- crates/workspace/src/pane.rs | 16 ++-- crates/workspace/src/workspace.rs | 2 +- crates/zed/src/zed.rs | 2 +- 32 files changed, 107 insertions(+), 124 deletions(-) diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 6c5546c6bbb5d07d8e6c260a066ed47922b12083..131023d249852e54e508cac3165cb482860d005b 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -228,7 +228,7 @@ impl ContextPicker { } fn build_menu(&mut self, window: &mut Window, cx: &mut Context) -> Entity { - let context_picker = cx.entity().clone(); + let context_picker = cx.entity(); let menu = ContextMenu::build(window, cx, move |menu, _window, cx| { let recent = self.recent_entries(cx); diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 4a4a747899ecc310666685de336bacffc4b271e6..bbd35958059346750cb899746823d5e12b2de1b8 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -72,7 +72,7 @@ pub fn init( let Some(window) = window else { return; }; - let workspace = cx.entity().clone(); + let workspace = cx.entity(); InlineAssistant::update_global(cx, |inline_assistant, cx| { inline_assistant.register_workspace(&workspace, window, cx) }); diff --git a/crates/agent_ui/src/profile_selector.rs b/crates/agent_ui/src/profile_selector.rs index 27ca69590fb20cd5a058f375b7acf8fffadeac31..ce25f531e254750fce36628f7d0138b728c9205a 100644 --- a/crates/agent_ui/src/profile_selector.rs +++ b/crates/agent_ui/src/profile_selector.rs @@ -163,7 +163,7 @@ impl Render for ProfileSelector { .unwrap_or_else(|| "Unknown".into()); if self.provider.profiles_supported(cx) { - let this = cx.entity().clone(); + let this = cx.entity(); let focus_handle = self.focus_handle.clone(); let trigger_button = Button::new("profile-selector-model", selected_profile) .label_size(LabelSize::Small) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 51d9f003f813212d40ff8e0716c86b1439fd4de6..2bbaa8446c20e4455504499d468e7c46dff8ced8 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -674,7 +674,7 @@ impl ChatPanel { }) }) .when_some(message_id, |el, message_id| { - let this = cx.entity().clone(); + let this = cx.entity(); el.child( self.render_popover_button( diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 430b447580ed385f5b483f8d9fff8a6492c005d7..c2cc6a7ad5cb9813ec618df5ca45f47aa1075305 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -95,7 +95,7 @@ pub fn init(cx: &mut App) { .and_then(|room| room.read(cx).channel_id()); if let Some(channel_id) = channel_id { - let workspace = cx.entity().clone(); + let workspace = cx.entity(); window.defer(cx, move |window, cx| { ChannelView::open(channel_id, None, workspace, window, cx) .detach_and_log_err(cx) @@ -1142,7 +1142,7 @@ impl CollabPanel { window: &mut Window, cx: &mut Context, ) { - let this = cx.entity().clone(); + let this = cx.entity(); if !(role == proto::ChannelRole::Guest || role == proto::ChannelRole::Talker || role == proto::ChannelRole::Member) @@ -1272,7 +1272,7 @@ impl CollabPanel { .channel_for_id(clipboard.channel_id) .map(|channel| channel.name.clone()) }); - let this = cx.entity().clone(); + let this = cx.entity(); let context_menu = ContextMenu::build(window, cx, |mut context_menu, window, cx| { if self.has_subchannels(ix) { @@ -1439,7 +1439,7 @@ impl CollabPanel { window: &mut Window, cx: &mut Context, ) { - let this = cx.entity().clone(); + let this = cx.entity(); let in_room = ActiveCall::global(cx).read(cx).room().is_some(); let context_menu = ContextMenu::build(window, cx, |mut context_menu, _, _| { diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index c0d3130ee997e3fe2ffffc4b228de9e512f18340..e558835dbaf0e34e2efa1b4f64fd8f6cb96016c5 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -586,7 +586,7 @@ impl ChannelModalDelegate { return; }; let user_id = membership.user.id; - let picker = cx.entity().clone(); + let picker = cx.entity(); let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| { let role = membership.role; diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 3a280ff6677c9a5f9598d5ecaf473af232a8fed1..a3420d603b02b9e9d54d2b5bb441a9ba119840aa 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -321,7 +321,7 @@ impl NotificationPanel { .justify_end() .child(Button::new("decline", "Decline").on_click({ let notification = notification.clone(); - let entity = cx.entity().clone(); + let entity = cx.entity(); move |_, _, cx| { entity.update(cx, |this, cx| { this.respond_to_notification( @@ -334,7 +334,7 @@ impl NotificationPanel { })) .child(Button::new("accept", "Accept").on_click({ let notification = notification.clone(); - let entity = cx.entity().clone(); + let entity = cx.entity(); move |_, _, cx| { entity.update(cx, |this, cx| { this.respond_to_notification( diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index c8bee42039c41a8fd2e393bccd25f082aba488e4..f3117aee0797e2dd183a25a31bbe50ea560f21bc 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -291,7 +291,7 @@ pub(crate) fn new_debugger_pane( let Some(project) = project.upgrade() else { return ControlFlow::Break(()); }; - let this_pane = cx.entity().clone(); + let this_pane = cx.entity(); let item = if tab.pane == this_pane { pane.item_for_index(tab.ix) } else { @@ -502,7 +502,7 @@ pub(crate) fn new_debugger_pane( .on_drag( DraggedTab { item: item.boxed_clone(), - pane: cx.entity().clone(), + pane: cx.entity(), detail: 0, is_active: selected, ix, diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 3d3b43d71bc4a0914ed97dac24a278049f4c52f1..4632a03daf53460cc0f674c0bca425f6bc689f24 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -127,7 +127,7 @@ impl Render for EditPredictionButton { }), ); } - let this = cx.entity().clone(); + let this = cx.entity(); div().child( PopoverMenu::new("copilot") @@ -182,7 +182,7 @@ impl Render for EditPredictionButton { let icon = status.to_icon(); let tooltip_text = status.to_tooltip(); let has_menu = status.has_menu(); - let this = cx.entity().clone(); + let this = cx.entity(); let fs = self.fs.clone(); return div().child( @@ -331,7 +331,7 @@ impl Render for EditPredictionButton { }) }); - let this = cx.entity().clone(); + let this = cx.entity(); let mut popover_menu = PopoverMenu::new("zeta") .menu(move |window, cx| { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index cf9954bc12d4c7514d9bc5bb9af29547a13f1768..ef2bdc5da390e662332a4f0444b4149f3b1debfd 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -74,7 +74,7 @@ fn test_edit_events(cx: &mut TestAppContext) { let editor1 = cx.add_window({ let events = events.clone(); |window, cx| { - let entity = cx.entity().clone(); + let entity = cx.entity(); cx.subscribe_in( &entity, window, @@ -95,7 +95,7 @@ fn test_edit_events(cx: &mut TestAppContext) { let events = events.clone(); |window, cx| { cx.subscribe_in( - &cx.entity().clone(), + &cx.entity(), window, move |_, _, event: &EditorEvent, _, _| match event { EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")), @@ -19634,13 +19634,8 @@ fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) { editor.insert_creases(Some(crease), cx); let snapshot = editor.snapshot(window, cx); - let _div = snapshot.render_crease_toggle( - MultiBufferRow(1), - false, - cx.entity().clone(), - window, - cx, - ); + let _div = + snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx); snapshot }) .unwrap(); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 8a5c65f994fd0c03a59b939a3362f41f0a1bd205..5edfd7df309fb5161ae865abefadda2747589dda 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -7815,7 +7815,7 @@ impl Element for EditorElement { min_lines, max_lines, } => { - let editor_handle = cx.entity().clone(); + let editor_handle = cx.entity(); let max_line_number_width = self.max_line_number_width(&editor.snapshot(window, cx), window); window.request_measured_layout( diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index fe3e94f5c20dc1a78ae01defc24e290c18a1a3e6..49159339205ede0eb2d2db8b16a1c235fcd84303 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -703,7 +703,7 @@ impl ExtensionsPage { extension: &ExtensionMetadata, cx: &mut Context, ) -> ExtensionCard { - let this = cx.entity().clone(); + let this = cx.entity(); let status = Self::extension_status(&extension.id, cx); let has_dev_extension = Self::dev_extension_exists(&extension.id, cx); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index de308b9dde5b22234e67d2038503ff7829b313db..70987dd2128e380c23c64289272e06c24b9b338b 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -3410,7 +3410,7 @@ impl GitPanel { * MAX_PANEL_EDITOR_LINES + gap; - let git_panel = cx.entity().clone(); + let git_panel = cx.entity(); let display_name = SharedString::from(Arc::from( active_repository .read(cx) diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index 52a5b08b967927ef709dffc1e21c4075e4cdc5df..b0f560e38d4896f889a30b5315a265c83065d068 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -595,9 +595,7 @@ impl Render for TextInput { .w_full() .p(px(4.)) .bg(white()) - .child(TextElement { - input: cx.entity().clone(), - }), + .child(TextElement { input: cx.entity() }), ) } } diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 606f3a3f0e5f91b5fb8856cabce240d094f3cf49..823d59ce12ea45bfd5bc45d5889bff5ee7800d2a 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1358,7 +1358,7 @@ impl Render for LspLogToolbarItemView { }) .collect(); - let log_toolbar_view = cx.entity().clone(); + let log_toolbar_view = cx.entity(); let lsp_menu = PopoverMenu::new("LspLogView") .anchor(Corner::TopLeft) diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index 50547253a92b8c23d0530326faf916e56363dcd9..3244350a34e275a33f9b9a5c2d3841c34884d1df 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -1007,7 +1007,7 @@ impl Render for LspTool { (None, "All Servers Operational") }; - let lsp_tool = cx.entity().clone(); + let lsp_tool = cx.entity(); div().child( PopoverMenu::new("lsp-tool") diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index eadba2c1d2f4c96c4f0ad2646c2e9957bbae3bdc..9946442ec88bf5aa2d1c1d5678ab39c08144591f 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -456,7 +456,7 @@ impl SyntaxTreeToolbarItemView { let active_layer = buffer_state.active_layer.clone()?; let active_buffer = buffer_state.buffer.read(cx).snapshot(); - let view = cx.entity().clone(); + let view = cx.entity(); Some( PopoverMenu::new("Syntax Tree") .trigger(Self::render_header(&active_layer)) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 1cda3897ec356c76b8abf4751bad6c35873c1300..004a27b0cf06a2be90969767ddd95b8eb4de47e6 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -4815,51 +4815,45 @@ impl OutlinePanel { .when(show_indent_guides, |list| { list.with_decoration( ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx)) - .with_compute_indents_fn( - cx.entity().clone(), - |outline_panel, range, _, _| { - let entries = outline_panel.cached_entries.get(range); - if let Some(entries) = entries { - entries.into_iter().map(|item| item.depth).collect() - } else { - smallvec::SmallVec::new() - } - }, - ) - .with_render_fn( - cx.entity().clone(), - move |outline_panel, params, _, _| { - const LEFT_OFFSET: Pixels = px(14.); - - let indent_size = params.indent_size; - let item_height = params.item_height; - let active_indent_guide_ix = find_active_indent_guide_ix( - outline_panel, - ¶ms.indent_guides, - ); + .with_compute_indents_fn(cx.entity(), |outline_panel, range, _, _| { + let entries = outline_panel.cached_entries.get(range); + if let Some(entries) = entries { + entries.into_iter().map(|item| item.depth).collect() + } else { + smallvec::SmallVec::new() + } + }) + .with_render_fn(cx.entity(), move |outline_panel, params, _, _| { + const LEFT_OFFSET: Pixels = px(14.); + + let indent_size = params.indent_size; + let item_height = params.item_height; + let active_indent_guide_ix = find_active_indent_guide_ix( + outline_panel, + ¶ms.indent_guides, + ); - params - .indent_guides - .into_iter() - .enumerate() - .map(|(ix, layout)| { - let bounds = Bounds::new( - point( - layout.offset.x * indent_size + LEFT_OFFSET, - layout.offset.y * item_height, - ), - size(px(1.), layout.length * item_height), - ); - ui::RenderedIndentGuide { - bounds, - layout, - is_active: active_indent_guide_ix == Some(ix), - hitbox: None, - } - }) - .collect() - }, - ), + params + .indent_guides + .into_iter() + .enumerate() + .map(|(ix, layout)| { + let bounds = Bounds::new( + point( + layout.offset.x * indent_size + LEFT_OFFSET, + layout.offset.y * item_height, + ), + size(px(1.), layout.length * item_height), + ); + ui::RenderedIndentGuide { + bounds, + layout, + is_active: active_indent_guide_ix == Some(ix), + hitbox: None, + } + }) + .collect() + }), ) }) }; diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 967df41e234c9008f55aa84582729ffe5ec2398b..4d7f2faf62d455adccb69e88cc69a8fc64529fa9 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -5351,26 +5351,22 @@ impl Render for ProjectPanel { .when(show_indent_guides, |list| { list.with_decoration( ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx)) - .with_compute_indents_fn( - cx.entity().clone(), - |this, range, window, cx| { - let mut items = - SmallVec::with_capacity(range.end - range.start); - this.iter_visible_entries( - range, - window, - cx, - |entry, _, entries, _, _| { - let (depth, _) = - Self::calculate_depth_and_difference( - entry, entries, - ); - items.push(depth); - }, - ); - items - }, - ) + .with_compute_indents_fn(cx.entity(), |this, range, window, cx| { + let mut items = + SmallVec::with_capacity(range.end - range.start); + this.iter_visible_entries( + range, + window, + cx, + |entry, _, entries, _, _| { + let (depth, _) = Self::calculate_depth_and_difference( + entry, entries, + ); + items.push(depth); + }, + ); + items + }) .on_click(cx.listener( |this, active_indent_guide: &IndentGuideLayout, window, cx| { if window.modifiers().secondary() { @@ -5394,7 +5390,7 @@ impl Render for ProjectPanel { } }, )) - .with_render_fn(cx.entity().clone(), move |this, params, _, cx| { + .with_render_fn(cx.entity(), move |this, params, _, cx| { const LEFT_OFFSET: Pixels = px(14.); const PADDING_Y: Pixels = px(4.); const HITBOX_OVERDRAW: Pixels = px(3.); @@ -5447,7 +5443,7 @@ impl Render for ProjectPanel { }) .when(show_sticky_entries, |list| { let sticky_items = ui::sticky_items( - cx.entity().clone(), + cx.entity(), |this, range, window, cx| { let mut items = SmallVec::with_capacity(range.end - range.start); this.iter_visible_entries( @@ -5474,7 +5470,7 @@ impl Render for ProjectPanel { list.with_decoration(if show_indent_guides { sticky_items.with_decoration( ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx)) - .with_render_fn(cx.entity().clone(), move |_, params, _, _| { + .with_render_fn(cx.entity(), move |_, params, _, _| { const LEFT_OFFSET: Pixels = px(14.); let indent_size = params.indent_size; diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 354434a7fc9318e58ff7796d061fc2386aae950f..e5e166cb4cc02761c42af42bf36f1c0ed8a7cce0 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1292,7 +1292,7 @@ impl RemoteServerProjects { let connection_string = connection_string.clone(); move |_, _: &menu::Confirm, window, cx| { remove_ssh_server( - cx.entity().clone(), + cx.entity(), server_index, connection_string.clone(), window, @@ -1312,7 +1312,7 @@ impl RemoteServerProjects { .child(Label::new("Remove Server").color(Color::Error)) .on_click(cx.listener(move |_, _, window, cx| { remove_ssh_server( - cx.entity().clone(), + cx.entity(), server_index, connection_string.clone(), window, diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index 729a6161350652a90fcf9687593a2f115481a945..f945e5ed9f52a01856734083e145ff0db9d46080 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -244,7 +244,7 @@ impl Session { repl_session_id = cx.entity_id().to_string(), ); - let session_view = cx.entity().clone(); + let session_view = cx.entity(); let kernel = match self.kernel_specification.clone() { KernelSpecification::Jupyter(kernel_specification) diff --git a/crates/storybook/src/stories/indent_guides.rs b/crates/storybook/src/stories/indent_guides.rs index e4f9669b1fd9172205511a487d20895443305d2e..db23ea79bd43c267e02e4f81b1b0586b0c1d19cd 100644 --- a/crates/storybook/src/stories/indent_guides.rs +++ b/crates/storybook/src/stories/indent_guides.rs @@ -65,7 +65,7 @@ impl Render for IndentGuidesStory { }, ) .with_compute_indents_fn( - cx.entity().clone(), + cx.entity(), |this, range, _cx, _context| { this.depths .iter() diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index c9528c39b97adffdd33683fc4fa7758b9e2d1e84..568dc1db2e840071fa5b18724aaab94da21ddc08 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -947,7 +947,7 @@ pub fn new_terminal_pane( cx: &mut Context, ) -> Entity { let is_local = project.read(cx).is_local(); - let terminal_panel = cx.entity().clone(); + let terminal_panel = cx.entity(); let pane = cx.new(|cx| { let mut pane = Pane::new( workspace.clone(), @@ -1009,7 +1009,7 @@ pub fn new_terminal_pane( return ControlFlow::Break(()); }; if let Some(tab) = dropped_item.downcast_ref::() { - let this_pane = cx.entity().clone(); + let this_pane = cx.entity(); let item = if tab.pane == this_pane { pane.item_for_index(tab.ix) } else { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 219238496cbdee483fafff6692b86ff485d2b390..534c0a805161c946177589ca0fc30f944880fa6f 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1491,7 +1491,7 @@ impl TerminalView { impl Render for TerminalView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let terminal_handle = self.terminal.clone(); - let terminal_view_handle = cx.entity().clone(); + let terminal_view_handle = cx.entity(); let focused = self.focus_handle.is_focused(window); diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index d54b270074f1928b6308fbfef1c6c3a04f1b851b..714b74f239cd26dadf6dc70448b2022781ef398d 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -20,7 +20,7 @@ impl ModeIndicator { }) .detach(); - let handle = cx.entity().clone(); + let handle = cx.entity(); let window_handle = window.window_handle(); cx.observe_new::(move |_, window, cx| { let Some(window) = window else { @@ -29,7 +29,7 @@ impl ModeIndicator { if window.window_handle() != window_handle { return; } - let vim = cx.entity().clone(); + let vim = cx.entity(); handle.update(cx, |_, cx| { cx.subscribe(&vim, |mode_indicator, vim, event, cx| match event { VimEvent::Focused => { diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index e4e95ca48ea69e0304d789419948ccaebaf7e65b..4054c552aeb9eba4f18d708769fc7373201f45ff 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -332,7 +332,7 @@ impl Vim { Vim::take_forced_motion(cx); let prior_selections = self.editor_selections(window, cx); let cursor_word = self.editor_cursor_word(window, cx); - let vim = cx.entity().clone(); + let vim = cx.entity(); let searched = pane.update(cx, |pane, cx| { self.search.direction = direction; diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 51bf2dd131c0183820f310c9e3b5fa4625aeb7f4..44d9b8f4565aff7a83ee998f05b1f09e37b5c8ac 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -402,7 +402,7 @@ impl Vim { const NAMESPACE: &'static str = "vim"; pub fn new(window: &mut Window, cx: &mut Context) -> Entity { - let editor = cx.entity().clone(); + let editor = cx.entity(); let mut initial_mode = VimSettings::get_global(cx).default_mode; if initial_mode == Mode::Normal && HelixModeSetting::get_global(cx).0 { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index ca63d3e5532a393436046e04a3b50a448a0e94f0..ae72df397113904ab2762e182d57bc833b268753 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -253,7 +253,7 @@ impl Dock { cx: &mut Context, ) -> Entity { let focus_handle = cx.focus_handle(); - let workspace = cx.entity().clone(); + let workspace = cx.entity(); let dock = cx.new(|cx| { let focus_subscription = cx.on_focus(&focus_handle, window, |dock: &mut Dock, window, cx| { diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 7d8a28b0f1fd8a07e9a47fbebeae7097c6fd3aa0..1356322a5c4eea864a5fb9c3eca4f9823d56e802 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -346,7 +346,7 @@ impl Render for LanguageServerPrompt { ) .child(Label::new(request.message.to_string()).size(LabelSize::Small)) .children(request.actions.iter().enumerate().map(|(ix, action)| { - let this_handle = cx.entity().clone(); + let this_handle = cx.entity(); Button::new(ix, action.title.clone()) .size(ButtonSize::Large) .on_click(move |_, window, cx| { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 759e91f758cea190d8c9e17102475a145e398641..860a57c21ff3d952d65abd30e1fc7fcc4fef9361 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2198,7 +2198,7 @@ impl Pane { fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context) { let workspace = self.workspace.clone(); - let pane = cx.entity().clone(); + let pane = cx.entity(); window.defer(cx, move |window, cx| { let Ok(status_bar) = @@ -2279,7 +2279,7 @@ impl Pane { cx: &mut Context, ) { maybe!({ - let pane = cx.entity().clone(); + let pane = cx.entity(); let destination_index = match operation { PinOperation::Pin => self.pinned_tab_count.min(ix), @@ -2473,7 +2473,7 @@ impl Pane { .on_drag( DraggedTab { item: item.boxed_clone(), - pane: cx.entity().clone(), + pane: cx.entity(), detail, is_active, ix, @@ -2832,7 +2832,7 @@ impl Pane { let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft) .icon_size(IconSize::Small) .on_click({ - let entity = cx.entity().clone(); + let entity = cx.entity(); move |_, window, cx| { entity.update(cx, |pane, cx| pane.navigate_backward(window, cx)) } @@ -2848,7 +2848,7 @@ impl Pane { let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight) .icon_size(IconSize::Small) .on_click({ - let entity = cx.entity().clone(); + let entity = cx.entity(); move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx)) }) .disabled(!self.can_navigate_forward()) @@ -3054,7 +3054,7 @@ impl Pane { return; } } - let mut to_pane = cx.entity().clone(); + let mut to_pane = cx.entity(); let split_direction = self.drag_split_direction; let item_id = dragged_tab.item.item_id(); if let Some(preview_item_id) = self.preview_item_id { @@ -3163,7 +3163,7 @@ impl Pane { return; } } - let mut to_pane = cx.entity().clone(); + let mut to_pane = cx.entity(); let split_direction = self.drag_split_direction; let project_entry_id = *project_entry_id; self.workspace @@ -3239,7 +3239,7 @@ impl Pane { return; } } - let mut to_pane = cx.entity().clone(); + let mut to_pane = cx.entity(); let mut split_direction = self.drag_split_direction; let paths = paths.paths().to_vec(); let is_remote = self diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ade6838fad893e3dbbdc0c43f1fa62be4253af5c..1eaa125ba5e221f2d86cefb722883b8d165a0df2 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6338,7 +6338,7 @@ impl Render for Workspace { .border_b_1() .border_color(colors.border) .child({ - let this = cx.entity().clone(); + let this = cx.entity(); canvas( move |bounds, window, cx| { this.update(cx, |this, cx| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 84145a1be437eb7ae6f4928ac4d2087a7ce54e86..b06652b2cebc0d7832f958fb4766e08564db172c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -319,7 +319,7 @@ pub fn initialize_workspace( return; }; - let workspace_handle = cx.entity().clone(); + let workspace_handle = cx.entity(); let center_pane = workspace.active_pane().clone(); initialize_pane(workspace, ¢er_pane, window, cx); From 239e479aedebb45cbc2efd7d0417808a3001710c Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 16:49:56 -0400 Subject: [PATCH 049/744] collab: Remove Stripe code (#36275) This PR removes the code for integrating with Stripe from Collab. All of these concerns are now handled by Cloud. Release Notes: - N/A --- Cargo.lock | 159 +---- Cargo.toml | 14 - crates/collab/Cargo.toml | 5 - crates/collab/k8s/collab.template.yml | 6 - crates/collab/src/api.rs | 1 - crates/collab/src/api/billing.rs | 59 -- .../src/db/tables/billing_subscription.rs | 15 - crates/collab/src/lib.rs | 44 -- crates/collab/src/main.rs | 7 - crates/collab/src/stripe_billing.rs | 156 ----- crates/collab/src/stripe_client.rs | 285 -------- .../src/stripe_client/fake_stripe_client.rs | 247 ------- .../src/stripe_client/real_stripe_client.rs | 612 ------------------ crates/collab/src/tests.rs | 2 - .../collab/src/tests/stripe_billing_tests.rs | 123 ---- crates/collab/src/tests/test_server.rs | 5 - 16 files changed, 2 insertions(+), 1738 deletions(-) delete mode 100644 crates/collab/src/api/billing.rs delete mode 100644 crates/collab/src/stripe_billing.rs delete mode 100644 crates/collab/src/stripe_client.rs delete mode 100644 crates/collab/src/stripe_client/fake_stripe_client.rs delete mode 100644 crates/collab/src/stripe_client/real_stripe_client.rs delete mode 100644 crates/collab/src/tests/stripe_billing_tests.rs diff --git a/Cargo.lock b/Cargo.lock index bfc797d6cd484f8327d2b6d508e916ef3b482290..2be16cc22f9a0520b403d530fc79a0f1148431bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1262,26 +1262,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "async-stripe" -version = "0.40.0" -source = "git+https://github.com/zed-industries/async-stripe?rev=3672dd4efb7181aa597bf580bf5a2f5d23db6735#3672dd4efb7181aa597bf580bf5a2f5d23db6735" -dependencies = [ - "chrono", - "futures-util", - "http-types", - "hyper 0.14.32", - "hyper-rustls 0.24.2", - "serde", - "serde_json", - "serde_path_to_error", - "serde_qs 0.10.1", - "smart-default 0.6.0", - "smol_str 0.1.24", - "thiserror 1.0.69", - "tokio", -] - [[package]] name = "async-tar" version = "0.5.0" @@ -2083,12 +2063,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -3281,7 +3255,6 @@ dependencies = [ "anyhow", "assistant_context", "assistant_slash_command", - "async-stripe", "async-trait", "async-tungstenite", "audio", @@ -3308,7 +3281,6 @@ dependencies = [ "dap_adapters", "dashmap 6.1.0", "debugger_ui", - "derive_more 0.99.19", "editor", "envy", "extension", @@ -3870,7 +3842,7 @@ dependencies = [ "rustc-hash 1.1.0", "rustybuzz 0.14.1", "self_cell", - "smol_str 0.2.2", + "smol_str", "swash", "sys-locale", "ttf-parser 0.21.1", @@ -6374,17 +6346,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -7988,27 +7949,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" -[[package]] -name = "http-types" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" -dependencies = [ - "anyhow", - "async-channel 1.9.0", - "base64 0.13.1", - "futures-lite 1.13.0", - "http 0.2.12", - "infer", - "pin-project-lite", - "rand 0.7.3", - "serde", - "serde_json", - "serde_qs 0.8.5", - "serde_urlencoded", - "url", -] - [[package]] name = "http_client" version = "0.1.0" @@ -8487,12 +8427,6 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" -[[package]] -name = "infer" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" - [[package]] name = "inherent" version = "1.0.12" @@ -10269,7 +10203,7 @@ dependencies = [ "num-traits", "range-map", "scroll", - "smart-default 0.7.1", + "smart-default", ] [[package]] @@ -13143,19 +13077,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - [[package]] name = "rand" version = "0.8.5" @@ -13177,16 +13098,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -13207,15 +13118,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -13234,15 +13136,6 @@ dependencies = [ "getrandom 0.3.2", ] -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - [[package]] name = "range-map" version = "0.2.0" @@ -14897,28 +14790,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_qs" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" -dependencies = [ - "percent-encoding", - "serde", - "thiserror 1.0.69", -] - -[[package]] -name = "serde_qs" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa" -dependencies = [ - "percent-encoding", - "serde", - "thiserror 1.0.69", -] - [[package]] name = "serde_repr" version = "0.1.20" @@ -15295,17 +15166,6 @@ dependencies = [ "serde", ] -[[package]] -name = "smart-default" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "smart-default" version = "0.7.1" @@ -15334,15 +15194,6 @@ dependencies = [ "futures-lite 2.6.0", ] -[[package]] -name = "smol_str" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" -dependencies = [ - "serde", -] - [[package]] name = "smol_str" version = "0.2.2" @@ -18191,12 +18042,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index baa4ee7f4ec97995630398fa98d192a31a6210dd..644b6c0f40c403a68dc5bc03842d1f0669eafa38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -667,20 +667,6 @@ workspace-hack = "0.1.0" yawc = { git = "https://github.com/deviant-forks/yawc", rev = "1899688f3e69ace4545aceb97b2a13881cf26142" } zstd = "0.11" -[workspace.dependencies.async-stripe] -git = "https://github.com/zed-industries/async-stripe" -rev = "3672dd4efb7181aa597bf580bf5a2f5d23db6735" -default-features = false -features = [ - "runtime-tokio-hyper-rustls", - "billing", - "checkout", - "events", - # The features below are only enabled to get the `events` feature to build. - "chrono", - "connect", -] - [workspace.dependencies.windows] version = "0.61" features = [ diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 9a867f9e058cb4a1f4782625571e1184cd7209d3..6fc591be133f310f8401ed22589362e2621a949f 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -19,7 +19,6 @@ test-support = ["sqlite"] [dependencies] anyhow.workspace = true -async-stripe.workspace = true async-trait.workspace = true async-tungstenite.workspace = true aws-config = { version = "1.1.5" } @@ -33,7 +32,6 @@ clock.workspace = true cloud_llm_client.workspace = true collections.workspace = true dashmap.workspace = true -derive_more.workspace = true envy = "0.4.2" futures.workspace = true gpui.workspace = true @@ -134,6 +132,3 @@ util.workspace = true workspace = { workspace = true, features = ["test-support"] } worktree = { workspace = true, features = ["test-support"] } zlog.workspace = true - -[package.metadata.cargo-machete] -ignored = ["async-stripe"] diff --git a/crates/collab/k8s/collab.template.yml b/crates/collab/k8s/collab.template.yml index 45fc018a4afbd22180419742ab50197d13ac3a59..214b550ac20499b8b03cfafeefab9b45d51fcc24 100644 --- a/crates/collab/k8s/collab.template.yml +++ b/crates/collab/k8s/collab.template.yml @@ -219,12 +219,6 @@ spec: secretKeyRef: name: slack key: panics_webhook - - name: STRIPE_API_KEY - valueFrom: - secretKeyRef: - name: stripe - key: api_key - optional: true - name: COMPLETE_WITH_LANGUAGE_MODEL_RATE_LIMIT_PER_HOUR value: "1000" - name: SUPERMAVEN_ADMIN_API_KEY diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 078a4469ae713de4c79929db09ed3522a52790a3..143e764eb3ce531c8193a0fd3aa264a0f7c48c06 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -1,4 +1,3 @@ -pub mod billing; pub mod contributors; pub mod events; pub mod extensions; diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs deleted file mode 100644 index a0325d14c4a1b9f4221b17b446983b17f767fcbe..0000000000000000000000000000000000000000 --- a/crates/collab/src/api/billing.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::sync::Arc; -use stripe::SubscriptionStatus; - -use crate::AppState; -use crate::db::billing_subscription::StripeSubscriptionStatus; -use crate::db::{CreateBillingCustomerParams, billing_customer}; -use crate::stripe_client::{StripeClient, StripeCustomerId}; - -impl From for StripeSubscriptionStatus { - fn from(value: SubscriptionStatus) -> Self { - match value { - SubscriptionStatus::Incomplete => Self::Incomplete, - SubscriptionStatus::IncompleteExpired => Self::IncompleteExpired, - SubscriptionStatus::Trialing => Self::Trialing, - SubscriptionStatus::Active => Self::Active, - SubscriptionStatus::PastDue => Self::PastDue, - SubscriptionStatus::Canceled => Self::Canceled, - SubscriptionStatus::Unpaid => Self::Unpaid, - SubscriptionStatus::Paused => Self::Paused, - } - } -} - -/// Finds or creates a billing customer using the provided customer. -pub async fn find_or_create_billing_customer( - app: &Arc, - stripe_client: &dyn StripeClient, - customer_id: &StripeCustomerId, -) -> anyhow::Result> { - // If we already have a billing customer record associated with the Stripe customer, - // there's nothing more we need to do. - if let Some(billing_customer) = app - .db - .get_billing_customer_by_stripe_customer_id(customer_id.0.as_ref()) - .await? - { - return Ok(Some(billing_customer)); - } - - let customer = stripe_client.get_customer(customer_id).await?; - - let Some(email) = customer.email else { - return Ok(None); - }; - - let Some(user) = app.db.get_user_by_email(&email).await? else { - return Ok(None); - }; - - let billing_customer = app - .db - .create_billing_customer(&CreateBillingCustomerParams { - user_id: user.id, - stripe_customer_id: customer.id.to_string(), - }) - .await?; - - Ok(Some(billing_customer)) -} diff --git a/crates/collab/src/db/tables/billing_subscription.rs b/crates/collab/src/db/tables/billing_subscription.rs index 522973dbc970b69947b8e790e370bfc9fa93aa99..f5684aeec32de6178f5f0809c3db5e9974593089 100644 --- a/crates/collab/src/db/tables/billing_subscription.rs +++ b/crates/collab/src/db/tables/billing_subscription.rs @@ -1,5 +1,4 @@ use crate::db::{BillingCustomerId, BillingSubscriptionId}; -use crate::stripe_client; use chrono::{Datelike as _, NaiveDate, Utc}; use sea_orm::entity::prelude::*; use serde::Serialize; @@ -160,17 +159,3 @@ pub enum StripeCancellationReason { #[sea_orm(string_value = "payment_failed")] PaymentFailed, } - -impl From for StripeCancellationReason { - fn from(value: stripe_client::StripeCancellationDetailsReason) -> Self { - match value { - stripe_client::StripeCancellationDetailsReason::CancellationRequested => { - Self::CancellationRequested - } - stripe_client::StripeCancellationDetailsReason::PaymentDisputed => { - Self::PaymentDisputed - } - stripe_client::StripeCancellationDetailsReason::PaymentFailed => Self::PaymentFailed, - } - } -} diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index 905859ca6996c3593e1f13fbcb0e723531595ff6..a68286a5a3891b4e8d43b3c431ce6d19791377a2 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -7,8 +7,6 @@ pub mod llm; pub mod migrations; pub mod rpc; pub mod seed; -pub mod stripe_billing; -pub mod stripe_client; pub mod user_backfiller; #[cfg(test)] @@ -27,16 +25,12 @@ use serde::Deserialize; use std::{path::PathBuf, sync::Arc}; use util::ResultExt; -use crate::stripe_billing::StripeBilling; -use crate::stripe_client::{RealStripeClient, StripeClient}; - pub type Result = std::result::Result; pub enum Error { Http(StatusCode, String, HeaderMap), Database(sea_orm::error::DbErr), Internal(anyhow::Error), - Stripe(stripe::StripeError), } impl From for Error { @@ -51,12 +45,6 @@ impl From for Error { } } -impl From for Error { - fn from(error: stripe::StripeError) -> Self { - Self::Stripe(error) - } -} - impl From for Error { fn from(error: axum::Error) -> Self { Self::Internal(error.into()) @@ -104,14 +92,6 @@ impl IntoResponse for Error { ); (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response() } - Error::Stripe(error) => { - log::error!( - "HTTP error {}: {:?}", - StatusCode::INTERNAL_SERVER_ERROR, - &error - ); - (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response() - } } } } @@ -122,7 +102,6 @@ impl std::fmt::Debug for Error { Error::Http(code, message, _headers) => (code, message).fmt(f), Error::Database(error) => error.fmt(f), Error::Internal(error) => error.fmt(f), - Error::Stripe(error) => error.fmt(f), } } } @@ -133,7 +112,6 @@ impl std::fmt::Display for Error { Error::Http(code, message, _) => write!(f, "{code}: {message}"), Error::Database(error) => error.fmt(f), Error::Internal(error) => error.fmt(f), - Error::Stripe(error) => error.fmt(f), } } } @@ -179,7 +157,6 @@ pub struct Config { pub zed_client_checksum_seed: Option, pub slack_panics_webhook: Option, pub auto_join_channel_id: Option, - pub stripe_api_key: Option, pub supermaven_admin_api_key: Option>, pub user_backfiller_github_access_token: Option>, } @@ -234,7 +211,6 @@ impl Config { auto_join_channel_id: None, migrations_path: None, seed_path: None, - stripe_api_key: None, supermaven_admin_api_key: None, user_backfiller_github_access_token: None, kinesis_region: None, @@ -269,11 +245,6 @@ pub struct AppState { pub llm_db: Option>, pub livekit_client: Option>, pub blob_store_client: Option, - /// This is a real instance of the Stripe client; we're working to replace references to this with the - /// [`StripeClient`] trait. - pub real_stripe_client: Option>, - pub stripe_client: Option>, - pub stripe_billing: Option>, pub executor: Executor, pub kinesis_client: Option<::aws_sdk_kinesis::Client>, pub config: Config, @@ -316,18 +287,11 @@ impl AppState { }; let db = Arc::new(db); - let stripe_client = build_stripe_client(&config).map(Arc::new).log_err(); let this = Self { db: db.clone(), llm_db, livekit_client, blob_store_client: build_blob_store_client(&config).await.log_err(), - stripe_billing: stripe_client - .clone() - .map(|stripe_client| Arc::new(StripeBilling::new(stripe_client))), - real_stripe_client: stripe_client.clone(), - stripe_client: stripe_client - .map(|stripe_client| Arc::new(RealStripeClient::new(stripe_client)) as _), executor, kinesis_client: if config.kinesis_access_key.is_some() { build_kinesis_client(&config).await.log_err() @@ -340,14 +304,6 @@ impl AppState { } } -fn build_stripe_client(config: &Config) -> anyhow::Result { - let api_key = config - .stripe_api_key - .as_ref() - .context("missing stripe_api_key")?; - Ok(stripe::Client::new(api_key)) -} - async fn build_blob_store_client(config: &Config) -> anyhow::Result { let keys = aws_sdk_s3::config::Credentials::new( config diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index 20641cb2322a6aa10372064ca208eef091b2ae5a..177c97f076c219c0389d09a8c6efbd41566f6ac4 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -102,13 +102,6 @@ async fn main() -> Result<()> { let state = AppState::new(config, Executor::Production).await?; - if let Some(stripe_billing) = state.stripe_billing.clone() { - let executor = state.executor.clone(); - executor.spawn_detached(async move { - stripe_billing.initialize().await.trace_err(); - }); - } - if mode.is_collab() { state.db.purge_old_embeddings().await.trace_err(); diff --git a/crates/collab/src/stripe_billing.rs b/crates/collab/src/stripe_billing.rs deleted file mode 100644 index ef5bef3e7e5d6c687e4b963f820d5d484e6c4537..0000000000000000000000000000000000000000 --- a/crates/collab/src/stripe_billing.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::sync::Arc; - -use anyhow::anyhow; -use collections::HashMap; -use stripe::SubscriptionStatus; -use tokio::sync::RwLock; - -use crate::Result; -use crate::stripe_client::{ - RealStripeClient, StripeAutomaticTax, StripeClient, StripeCreateSubscriptionItems, - StripeCreateSubscriptionParams, StripeCustomerId, StripePrice, StripePriceId, - StripeSubscription, -}; - -pub struct StripeBilling { - state: RwLock, - client: Arc, -} - -#[derive(Default)] -struct StripeBillingState { - prices_by_lookup_key: HashMap, -} - -impl StripeBilling { - pub fn new(client: Arc) -> Self { - Self { - client: Arc::new(RealStripeClient::new(client.clone())), - state: RwLock::default(), - } - } - - #[cfg(test)] - pub fn test(client: Arc) -> Self { - Self { - client, - state: RwLock::default(), - } - } - - pub fn client(&self) -> &Arc { - &self.client - } - - pub async fn initialize(&self) -> Result<()> { - log::info!("StripeBilling: initializing"); - - let mut state = self.state.write().await; - - let prices = self.client.list_prices().await?; - - for price in prices { - if let Some(lookup_key) = price.lookup_key.clone() { - state.prices_by_lookup_key.insert(lookup_key, price); - } - } - - log::info!("StripeBilling: initialized"); - - Ok(()) - } - - pub async fn zed_pro_price_id(&self) -> Result { - self.find_price_id_by_lookup_key("zed-pro").await - } - - pub async fn zed_free_price_id(&self) -> Result { - self.find_price_id_by_lookup_key("zed-free").await - } - - pub async fn find_price_id_by_lookup_key(&self, lookup_key: &str) -> Result { - self.state - .read() - .await - .prices_by_lookup_key - .get(lookup_key) - .map(|price| price.id.clone()) - .ok_or_else(|| crate::Error::Internal(anyhow!("no price ID found for {lookup_key:?}"))) - } - - pub async fn find_price_by_lookup_key(&self, lookup_key: &str) -> Result { - self.state - .read() - .await - .prices_by_lookup_key - .get(lookup_key) - .cloned() - .ok_or_else(|| crate::Error::Internal(anyhow!("no price found for {lookup_key:?}"))) - } - - /// Returns the Stripe customer associated with the provided email address, or creates a new customer, if one does - /// not already exist. - /// - /// Always returns a new Stripe customer if the email address is `None`. - pub async fn find_or_create_customer_by_email( - &self, - email_address: Option<&str>, - ) -> Result { - let existing_customer = if let Some(email) = email_address { - let customers = self.client.list_customers_by_email(email).await?; - - customers.first().cloned() - } else { - None - }; - - let customer_id = if let Some(existing_customer) = existing_customer { - existing_customer.id - } else { - let customer = self - .client - .create_customer(crate::stripe_client::CreateCustomerParams { - email: email_address, - }) - .await?; - - customer.id - }; - - Ok(customer_id) - } - - pub async fn subscribe_to_zed_free( - &self, - customer_id: StripeCustomerId, - ) -> Result { - let zed_free_price_id = self.zed_free_price_id().await?; - - let existing_subscriptions = self - .client - .list_subscriptions_for_customer(&customer_id) - .await?; - - let existing_active_subscription = - existing_subscriptions.into_iter().find(|subscription| { - subscription.status == SubscriptionStatus::Active - || subscription.status == SubscriptionStatus::Trialing - }); - if let Some(subscription) = existing_active_subscription { - return Ok(subscription); - } - - let params = StripeCreateSubscriptionParams { - customer: customer_id, - items: vec![StripeCreateSubscriptionItems { - price: Some(zed_free_price_id), - quantity: Some(1), - }], - automatic_tax: Some(StripeAutomaticTax { enabled: true }), - }; - - let subscription = self.client.create_subscription(params).await?; - - Ok(subscription) - } -} diff --git a/crates/collab/src/stripe_client.rs b/crates/collab/src/stripe_client.rs deleted file mode 100644 index 6e75a4d874bf41e7cb4418d4b56cfeb6040e5ff8..0000000000000000000000000000000000000000 --- a/crates/collab/src/stripe_client.rs +++ /dev/null @@ -1,285 +0,0 @@ -#[cfg(test)] -mod fake_stripe_client; -mod real_stripe_client; - -use std::collections::HashMap; -use std::sync::Arc; - -use anyhow::Result; -use async_trait::async_trait; - -#[cfg(test)] -pub use fake_stripe_client::*; -pub use real_stripe_client::*; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display, Serialize)] -pub struct StripeCustomerId(pub Arc); - -#[derive(Debug, Clone)] -pub struct StripeCustomer { - pub id: StripeCustomerId, - pub email: Option, -} - -#[derive(Debug)] -pub struct CreateCustomerParams<'a> { - pub email: Option<&'a str>, -} - -#[derive(Debug)] -pub struct UpdateCustomerParams<'a> { - pub email: Option<&'a str>, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display)] -pub struct StripeSubscriptionId(pub Arc); - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeSubscription { - pub id: StripeSubscriptionId, - pub customer: StripeCustomerId, - // TODO: Create our own version of this enum. - pub status: stripe::SubscriptionStatus, - pub current_period_end: i64, - pub current_period_start: i64, - pub items: Vec, - pub cancel_at: Option, - pub cancellation_details: Option, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display)] -pub struct StripeSubscriptionItemId(pub Arc); - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeSubscriptionItem { - pub id: StripeSubscriptionItemId, - pub price: Option, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct StripeCancellationDetails { - pub reason: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeCancellationDetailsReason { - CancellationRequested, - PaymentDisputed, - PaymentFailed, -} - -#[derive(Debug)] -pub struct StripeCreateSubscriptionParams { - pub customer: StripeCustomerId, - pub items: Vec, - pub automatic_tax: Option, -} - -#[derive(Debug)] -pub struct StripeCreateSubscriptionItems { - pub price: Option, - pub quantity: Option, -} - -#[derive(Debug, Clone)] -pub struct UpdateSubscriptionParams { - pub items: Option>, - pub trial_settings: Option, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct UpdateSubscriptionItems { - pub price: Option, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeSubscriptionTrialSettings { - pub end_behavior: StripeSubscriptionTrialSettingsEndBehavior, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeSubscriptionTrialSettingsEndBehavior { - pub missing_payment_method: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod { - Cancel, - CreateInvoice, - Pause, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display)] -pub struct StripePriceId(pub Arc); - -#[derive(Debug, PartialEq, Clone)] -pub struct StripePrice { - pub id: StripePriceId, - pub unit_amount: Option, - pub lookup_key: Option, - pub recurring: Option, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct StripePriceRecurring { - pub meter: Option, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display, Deserialize)] -pub struct StripeMeterId(pub Arc); - -#[derive(Debug, Clone, Deserialize)] -pub struct StripeMeter { - pub id: StripeMeterId, - pub event_name: String, -} - -#[derive(Debug, Serialize)] -pub struct StripeCreateMeterEventParams<'a> { - pub identifier: &'a str, - pub event_name: &'a str, - pub payload: StripeCreateMeterEventPayload<'a>, - pub timestamp: Option, -} - -#[derive(Debug, Serialize)] -pub struct StripeCreateMeterEventPayload<'a> { - pub value: u64, - pub stripe_customer_id: &'a StripeCustomerId, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeBillingAddressCollection { - Auto, - Required, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeCustomerUpdate { - pub address: Option, - pub name: Option, - pub shipping: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeCustomerUpdateAddress { - Auto, - Never, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeCustomerUpdateName { - Auto, - Never, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeCustomerUpdateShipping { - Auto, - Never, -} - -#[derive(Debug, Default)] -pub struct StripeCreateCheckoutSessionParams<'a> { - pub customer: Option<&'a StripeCustomerId>, - pub client_reference_id: Option<&'a str>, - pub mode: Option, - pub line_items: Option>, - pub payment_method_collection: Option, - pub subscription_data: Option, - pub success_url: Option<&'a str>, - pub billing_address_collection: Option, - pub customer_update: Option, - pub tax_id_collection: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeCheckoutSessionMode { - Payment, - Setup, - Subscription, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeCreateCheckoutSessionLineItems { - pub price: Option, - pub quantity: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeCheckoutSessionPaymentMethodCollection { - Always, - IfRequired, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeCreateCheckoutSessionSubscriptionData { - pub metadata: Option>, - pub trial_period_days: Option, - pub trial_settings: Option, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeTaxIdCollection { - pub enabled: bool, -} - -#[derive(Debug, Clone)] -pub struct StripeAutomaticTax { - pub enabled: bool, -} - -#[derive(Debug)] -pub struct StripeCheckoutSession { - pub url: Option, -} - -#[async_trait] -pub trait StripeClient: Send + Sync { - async fn list_customers_by_email(&self, email: &str) -> Result>; - - async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result; - - async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result; - - async fn update_customer( - &self, - customer_id: &StripeCustomerId, - params: UpdateCustomerParams<'_>, - ) -> Result; - - async fn list_subscriptions_for_customer( - &self, - customer_id: &StripeCustomerId, - ) -> Result>; - - async fn get_subscription( - &self, - subscription_id: &StripeSubscriptionId, - ) -> Result; - - async fn create_subscription( - &self, - params: StripeCreateSubscriptionParams, - ) -> Result; - - async fn update_subscription( - &self, - subscription_id: &StripeSubscriptionId, - params: UpdateSubscriptionParams, - ) -> Result<()>; - - async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()>; - - async fn list_prices(&self) -> Result>; - - async fn list_meters(&self) -> Result>; - - async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()>; - - async fn create_checkout_session( - &self, - params: StripeCreateCheckoutSessionParams<'_>, - ) -> Result; -} diff --git a/crates/collab/src/stripe_client/fake_stripe_client.rs b/crates/collab/src/stripe_client/fake_stripe_client.rs deleted file mode 100644 index 9bb08443ec6a5fd04ad11a8e24b1a71b03e4867b..0000000000000000000000000000000000000000 --- a/crates/collab/src/stripe_client/fake_stripe_client.rs +++ /dev/null @@ -1,247 +0,0 @@ -use std::sync::Arc; - -use anyhow::{Result, anyhow}; -use async_trait::async_trait; -use chrono::{Duration, Utc}; -use collections::HashMap; -use parking_lot::Mutex; -use uuid::Uuid; - -use crate::stripe_client::{ - CreateCustomerParams, StripeBillingAddressCollection, StripeCheckoutSession, - StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient, - StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams, - StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams, - StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate, - StripeMeter, StripeMeterId, StripePrice, StripePriceId, StripeSubscription, - StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, StripeTaxIdCollection, - UpdateCustomerParams, UpdateSubscriptionParams, -}; - -#[derive(Debug, Clone)] -pub struct StripeCreateMeterEventCall { - pub identifier: Arc, - pub event_name: Arc, - pub value: u64, - pub stripe_customer_id: StripeCustomerId, - pub timestamp: Option, -} - -#[derive(Debug, Clone)] -pub struct StripeCreateCheckoutSessionCall { - pub customer: Option, - pub client_reference_id: Option, - pub mode: Option, - pub line_items: Option>, - pub payment_method_collection: Option, - pub subscription_data: Option, - pub success_url: Option, - pub billing_address_collection: Option, - pub customer_update: Option, - pub tax_id_collection: Option, -} - -pub struct FakeStripeClient { - pub customers: Arc>>, - pub subscriptions: Arc>>, - pub update_subscription_calls: - Arc>>, - pub prices: Arc>>, - pub meters: Arc>>, - pub create_meter_event_calls: Arc>>, - pub create_checkout_session_calls: Arc>>, -} - -impl FakeStripeClient { - pub fn new() -> Self { - Self { - customers: Arc::new(Mutex::new(HashMap::default())), - subscriptions: Arc::new(Mutex::new(HashMap::default())), - update_subscription_calls: Arc::new(Mutex::new(Vec::new())), - prices: Arc::new(Mutex::new(HashMap::default())), - meters: Arc::new(Mutex::new(HashMap::default())), - create_meter_event_calls: Arc::new(Mutex::new(Vec::new())), - create_checkout_session_calls: Arc::new(Mutex::new(Vec::new())), - } - } -} - -#[async_trait] -impl StripeClient for FakeStripeClient { - async fn list_customers_by_email(&self, email: &str) -> Result> { - Ok(self - .customers - .lock() - .values() - .filter(|customer| customer.email.as_deref() == Some(email)) - .cloned() - .collect()) - } - - async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result { - self.customers - .lock() - .get(customer_id) - .cloned() - .ok_or_else(|| anyhow!("no customer found for {customer_id:?}")) - } - - async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result { - let customer = StripeCustomer { - id: StripeCustomerId(format!("cus_{}", Uuid::new_v4()).into()), - email: params.email.map(|email| email.to_string()), - }; - - self.customers - .lock() - .insert(customer.id.clone(), customer.clone()); - - Ok(customer) - } - - async fn update_customer( - &self, - customer_id: &StripeCustomerId, - params: UpdateCustomerParams<'_>, - ) -> Result { - let mut customers = self.customers.lock(); - if let Some(customer) = customers.get_mut(customer_id) { - if let Some(email) = params.email { - customer.email = Some(email.to_string()); - } - Ok(customer.clone()) - } else { - Err(anyhow!("no customer found for {customer_id:?}")) - } - } - - async fn list_subscriptions_for_customer( - &self, - customer_id: &StripeCustomerId, - ) -> Result> { - let subscriptions = self - .subscriptions - .lock() - .values() - .filter(|subscription| subscription.customer == *customer_id) - .cloned() - .collect(); - - Ok(subscriptions) - } - - async fn get_subscription( - &self, - subscription_id: &StripeSubscriptionId, - ) -> Result { - self.subscriptions - .lock() - .get(subscription_id) - .cloned() - .ok_or_else(|| anyhow!("no subscription found for {subscription_id:?}")) - } - - async fn create_subscription( - &self, - params: StripeCreateSubscriptionParams, - ) -> Result { - let now = Utc::now(); - - let subscription = StripeSubscription { - id: StripeSubscriptionId(format!("sub_{}", Uuid::new_v4()).into()), - customer: params.customer, - status: stripe::SubscriptionStatus::Active, - current_period_start: now.timestamp(), - current_period_end: (now + Duration::days(30)).timestamp(), - items: params - .items - .into_iter() - .map(|item| StripeSubscriptionItem { - id: StripeSubscriptionItemId(format!("si_{}", Uuid::new_v4()).into()), - price: item - .price - .and_then(|price_id| self.prices.lock().get(&price_id).cloned()), - }) - .collect(), - cancel_at: None, - cancellation_details: None, - }; - - self.subscriptions - .lock() - .insert(subscription.id.clone(), subscription.clone()); - - Ok(subscription) - } - - async fn update_subscription( - &self, - subscription_id: &StripeSubscriptionId, - params: UpdateSubscriptionParams, - ) -> Result<()> { - let subscription = self.get_subscription(subscription_id).await?; - - self.update_subscription_calls - .lock() - .push((subscription.id, params)); - - Ok(()) - } - - async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()> { - // TODO: Implement fake subscription cancellation. - let _ = subscription_id; - - Ok(()) - } - - async fn list_prices(&self) -> Result> { - let prices = self.prices.lock().values().cloned().collect(); - - Ok(prices) - } - - async fn list_meters(&self) -> Result> { - let meters = self.meters.lock().values().cloned().collect(); - - Ok(meters) - } - - async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()> { - self.create_meter_event_calls - .lock() - .push(StripeCreateMeterEventCall { - identifier: params.identifier.into(), - event_name: params.event_name.into(), - value: params.payload.value, - stripe_customer_id: params.payload.stripe_customer_id.clone(), - timestamp: params.timestamp, - }); - - Ok(()) - } - - async fn create_checkout_session( - &self, - params: StripeCreateCheckoutSessionParams<'_>, - ) -> Result { - self.create_checkout_session_calls - .lock() - .push(StripeCreateCheckoutSessionCall { - customer: params.customer.cloned(), - client_reference_id: params.client_reference_id.map(|id| id.to_string()), - mode: params.mode, - line_items: params.line_items, - payment_method_collection: params.payment_method_collection, - subscription_data: params.subscription_data, - success_url: params.success_url.map(|url| url.to_string()), - billing_address_collection: params.billing_address_collection, - customer_update: params.customer_update, - tax_id_collection: params.tax_id_collection, - }); - - Ok(StripeCheckoutSession { - url: Some("https://checkout.stripe.com/c/pay/cs_test_1".to_string()), - }) - } -} diff --git a/crates/collab/src/stripe_client/real_stripe_client.rs b/crates/collab/src/stripe_client/real_stripe_client.rs deleted file mode 100644 index 07c191ff30400ccbf4b73c4c84f09aa47e0fd9aa..0000000000000000000000000000000000000000 --- a/crates/collab/src/stripe_client/real_stripe_client.rs +++ /dev/null @@ -1,612 +0,0 @@ -use std::str::FromStr as _; -use std::sync::Arc; - -use anyhow::{Context as _, Result, anyhow}; -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use stripe::{ - CancellationDetails, CancellationDetailsReason, CheckoutSession, CheckoutSessionMode, - CheckoutSessionPaymentMethodCollection, CreateCheckoutSession, CreateCheckoutSessionLineItems, - CreateCheckoutSessionSubscriptionData, CreateCheckoutSessionSubscriptionDataTrialSettings, - CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior, - CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod, - CreateCustomer, CreateSubscriptionAutomaticTax, Customer, CustomerId, ListCustomers, Price, - PriceId, Recurring, Subscription, SubscriptionId, SubscriptionItem, SubscriptionItemId, - UpdateCustomer, UpdateSubscriptionItems, UpdateSubscriptionTrialSettings, - UpdateSubscriptionTrialSettingsEndBehavior, - UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, -}; - -use crate::stripe_client::{ - CreateCustomerParams, StripeAutomaticTax, StripeBillingAddressCollection, - StripeCancellationDetails, StripeCancellationDetailsReason, StripeCheckoutSession, - StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient, - StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams, - StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams, - StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate, - StripeCustomerUpdateAddress, StripeCustomerUpdateName, StripeCustomerUpdateShipping, - StripeMeter, StripePrice, StripePriceId, StripePriceRecurring, StripeSubscription, - StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, - StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior, - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection, - UpdateCustomerParams, UpdateSubscriptionParams, -}; - -pub struct RealStripeClient { - client: Arc, -} - -impl RealStripeClient { - pub fn new(client: Arc) -> Self { - Self { client } - } -} - -#[async_trait] -impl StripeClient for RealStripeClient { - async fn list_customers_by_email(&self, email: &str) -> Result> { - let response = Customer::list( - &self.client, - &ListCustomers { - email: Some(email), - ..Default::default() - }, - ) - .await?; - - Ok(response - .data - .into_iter() - .map(StripeCustomer::from) - .collect()) - } - - async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result { - let customer_id = customer_id.try_into()?; - - let customer = Customer::retrieve(&self.client, &customer_id, &[]).await?; - - Ok(StripeCustomer::from(customer)) - } - - async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result { - let customer = Customer::create( - &self.client, - CreateCustomer { - email: params.email, - ..Default::default() - }, - ) - .await?; - - Ok(StripeCustomer::from(customer)) - } - - async fn update_customer( - &self, - customer_id: &StripeCustomerId, - params: UpdateCustomerParams<'_>, - ) -> Result { - let customer = Customer::update( - &self.client, - &customer_id.try_into()?, - UpdateCustomer { - email: params.email, - ..Default::default() - }, - ) - .await?; - - Ok(StripeCustomer::from(customer)) - } - - async fn list_subscriptions_for_customer( - &self, - customer_id: &StripeCustomerId, - ) -> Result> { - let customer_id = customer_id.try_into()?; - - let subscriptions = stripe::Subscription::list( - &self.client, - &stripe::ListSubscriptions { - customer: Some(customer_id), - status: None, - ..Default::default() - }, - ) - .await?; - - Ok(subscriptions - .data - .into_iter() - .map(StripeSubscription::from) - .collect()) - } - - async fn get_subscription( - &self, - subscription_id: &StripeSubscriptionId, - ) -> Result { - let subscription_id = subscription_id.try_into()?; - - let subscription = Subscription::retrieve(&self.client, &subscription_id, &[]).await?; - - Ok(StripeSubscription::from(subscription)) - } - - async fn create_subscription( - &self, - params: StripeCreateSubscriptionParams, - ) -> Result { - let customer_id = params.customer.try_into()?; - - let mut create_subscription = stripe::CreateSubscription::new(customer_id); - create_subscription.items = Some( - params - .items - .into_iter() - .map(|item| stripe::CreateSubscriptionItems { - price: item.price.map(|price| price.to_string()), - quantity: item.quantity, - ..Default::default() - }) - .collect(), - ); - create_subscription.automatic_tax = params.automatic_tax.map(Into::into); - - let subscription = Subscription::create(&self.client, create_subscription).await?; - - Ok(StripeSubscription::from(subscription)) - } - - async fn update_subscription( - &self, - subscription_id: &StripeSubscriptionId, - params: UpdateSubscriptionParams, - ) -> Result<()> { - let subscription_id = subscription_id.try_into()?; - - stripe::Subscription::update( - &self.client, - &subscription_id, - stripe::UpdateSubscription { - items: params.items.map(|items| { - items - .into_iter() - .map(|item| UpdateSubscriptionItems { - price: item.price.map(|price| price.to_string()), - ..Default::default() - }) - .collect() - }), - trial_settings: params.trial_settings.map(Into::into), - ..Default::default() - }, - ) - .await?; - - Ok(()) - } - - async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()> { - let subscription_id = subscription_id.try_into()?; - - Subscription::cancel( - &self.client, - &subscription_id, - stripe::CancelSubscription { - invoice_now: None, - ..Default::default() - }, - ) - .await?; - - Ok(()) - } - - async fn list_prices(&self) -> Result> { - let response = stripe::Price::list( - &self.client, - &stripe::ListPrices { - limit: Some(100), - ..Default::default() - }, - ) - .await?; - - Ok(response.data.into_iter().map(StripePrice::from).collect()) - } - - async fn list_meters(&self) -> Result> { - #[derive(Serialize)] - struct Params { - #[serde(skip_serializing_if = "Option::is_none")] - limit: Option, - } - - let response = self - .client - .get_query::, _>( - "/billing/meters", - Params { limit: Some(100) }, - ) - .await?; - - Ok(response.data) - } - - async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()> { - #[derive(Deserialize)] - struct StripeMeterEvent { - pub identifier: String, - } - - let identifier = params.identifier; - match self - .client - .post_form::("/billing/meter_events", params) - .await - { - Ok(_event) => Ok(()), - Err(stripe::StripeError::Stripe(error)) => { - if error.http_status == 400 - && error - .message - .as_ref() - .map_or(false, |message| message.contains(identifier)) - { - Ok(()) - } else { - Err(anyhow!(stripe::StripeError::Stripe(error))) - } - } - Err(error) => Err(anyhow!("failed to create meter event: {error:?}")), - } - } - - async fn create_checkout_session( - &self, - params: StripeCreateCheckoutSessionParams<'_>, - ) -> Result { - let params = params.try_into()?; - let session = CheckoutSession::create(&self.client, params).await?; - - Ok(session.into()) - } -} - -impl From for StripeCustomerId { - fn from(value: CustomerId) -> Self { - Self(value.as_str().into()) - } -} - -impl TryFrom for CustomerId { - type Error = anyhow::Error; - - fn try_from(value: StripeCustomerId) -> Result { - Self::from_str(value.0.as_ref()).context("failed to parse Stripe customer ID") - } -} - -impl TryFrom<&StripeCustomerId> for CustomerId { - type Error = anyhow::Error; - - fn try_from(value: &StripeCustomerId) -> Result { - Self::from_str(value.0.as_ref()).context("failed to parse Stripe customer ID") - } -} - -impl From for StripeCustomer { - fn from(value: Customer) -> Self { - StripeCustomer { - id: value.id.into(), - email: value.email, - } - } -} - -impl From for StripeSubscriptionId { - fn from(value: SubscriptionId) -> Self { - Self(value.as_str().into()) - } -} - -impl TryFrom<&StripeSubscriptionId> for SubscriptionId { - type Error = anyhow::Error; - - fn try_from(value: &StripeSubscriptionId) -> Result { - Self::from_str(value.0.as_ref()).context("failed to parse Stripe subscription ID") - } -} - -impl From for StripeSubscription { - fn from(value: Subscription) -> Self { - Self { - id: value.id.into(), - customer: value.customer.id().into(), - status: value.status, - current_period_start: value.current_period_start, - current_period_end: value.current_period_end, - items: value.items.data.into_iter().map(Into::into).collect(), - cancel_at: value.cancel_at, - cancellation_details: value.cancellation_details.map(Into::into), - } - } -} - -impl From for StripeCancellationDetails { - fn from(value: CancellationDetails) -> Self { - Self { - reason: value.reason.map(Into::into), - } - } -} - -impl From for StripeCancellationDetailsReason { - fn from(value: CancellationDetailsReason) -> Self { - match value { - CancellationDetailsReason::CancellationRequested => Self::CancellationRequested, - CancellationDetailsReason::PaymentDisputed => Self::PaymentDisputed, - CancellationDetailsReason::PaymentFailed => Self::PaymentFailed, - } - } -} - -impl From for StripeSubscriptionItemId { - fn from(value: SubscriptionItemId) -> Self { - Self(value.as_str().into()) - } -} - -impl From for StripeSubscriptionItem { - fn from(value: SubscriptionItem) -> Self { - Self { - id: value.id.into(), - price: value.price.map(Into::into), - } - } -} - -impl From for CreateSubscriptionAutomaticTax { - fn from(value: StripeAutomaticTax) -> Self { - Self { - enabled: value.enabled, - liability: None, - } - } -} - -impl From for UpdateSubscriptionTrialSettings { - fn from(value: StripeSubscriptionTrialSettings) -> Self { - Self { - end_behavior: value.end_behavior.into(), - } - } -} - -impl From - for UpdateSubscriptionTrialSettingsEndBehavior -{ - fn from(value: StripeSubscriptionTrialSettingsEndBehavior) -> Self { - Self { - missing_payment_method: value.missing_payment_method.into(), - } - } -} - -impl From - for UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod -{ - fn from(value: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod) -> Self { - match value { - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel => Self::Cancel, - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::CreateInvoice => { - Self::CreateInvoice - } - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Pause => Self::Pause, - } - } -} - -impl From for StripePriceId { - fn from(value: PriceId) -> Self { - Self(value.as_str().into()) - } -} - -impl TryFrom for PriceId { - type Error = anyhow::Error; - - fn try_from(value: StripePriceId) -> Result { - Self::from_str(value.0.as_ref()).context("failed to parse Stripe price ID") - } -} - -impl From for StripePrice { - fn from(value: Price) -> Self { - Self { - id: value.id.into(), - unit_amount: value.unit_amount, - lookup_key: value.lookup_key, - recurring: value.recurring.map(StripePriceRecurring::from), - } - } -} - -impl From for StripePriceRecurring { - fn from(value: Recurring) -> Self { - Self { meter: value.meter } - } -} - -impl<'a> TryFrom> for CreateCheckoutSession<'a> { - type Error = anyhow::Error; - - fn try_from(value: StripeCreateCheckoutSessionParams<'a>) -> Result { - Ok(Self { - customer: value - .customer - .map(|customer_id| customer_id.try_into()) - .transpose()?, - client_reference_id: value.client_reference_id, - mode: value.mode.map(Into::into), - line_items: value - .line_items - .map(|line_items| line_items.into_iter().map(Into::into).collect()), - payment_method_collection: value.payment_method_collection.map(Into::into), - subscription_data: value.subscription_data.map(Into::into), - success_url: value.success_url, - billing_address_collection: value.billing_address_collection.map(Into::into), - customer_update: value.customer_update.map(Into::into), - tax_id_collection: value.tax_id_collection.map(Into::into), - ..Default::default() - }) - } -} - -impl From for CheckoutSessionMode { - fn from(value: StripeCheckoutSessionMode) -> Self { - match value { - StripeCheckoutSessionMode::Payment => Self::Payment, - StripeCheckoutSessionMode::Setup => Self::Setup, - StripeCheckoutSessionMode::Subscription => Self::Subscription, - } - } -} - -impl From for CreateCheckoutSessionLineItems { - fn from(value: StripeCreateCheckoutSessionLineItems) -> Self { - Self { - price: value.price, - quantity: value.quantity, - ..Default::default() - } - } -} - -impl From for CheckoutSessionPaymentMethodCollection { - fn from(value: StripeCheckoutSessionPaymentMethodCollection) -> Self { - match value { - StripeCheckoutSessionPaymentMethodCollection::Always => Self::Always, - StripeCheckoutSessionPaymentMethodCollection::IfRequired => Self::IfRequired, - } - } -} - -impl From for CreateCheckoutSessionSubscriptionData { - fn from(value: StripeCreateCheckoutSessionSubscriptionData) -> Self { - Self { - trial_period_days: value.trial_period_days, - trial_settings: value.trial_settings.map(Into::into), - metadata: value.metadata, - ..Default::default() - } - } -} - -impl From for CreateCheckoutSessionSubscriptionDataTrialSettings { - fn from(value: StripeSubscriptionTrialSettings) -> Self { - Self { - end_behavior: value.end_behavior.into(), - } - } -} - -impl From - for CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior -{ - fn from(value: StripeSubscriptionTrialSettingsEndBehavior) -> Self { - Self { - missing_payment_method: value.missing_payment_method.into(), - } - } -} - -impl From - for CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod -{ - fn from(value: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod) -> Self { - match value { - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel => Self::Cancel, - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::CreateInvoice => { - Self::CreateInvoice - } - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Pause => Self::Pause, - } - } -} - -impl From for StripeCheckoutSession { - fn from(value: CheckoutSession) -> Self { - Self { url: value.url } - } -} - -impl From for stripe::CheckoutSessionBillingAddressCollection { - fn from(value: StripeBillingAddressCollection) -> Self { - match value { - StripeBillingAddressCollection::Auto => { - stripe::CheckoutSessionBillingAddressCollection::Auto - } - StripeBillingAddressCollection::Required => { - stripe::CheckoutSessionBillingAddressCollection::Required - } - } - } -} - -impl From for stripe::CreateCheckoutSessionCustomerUpdateAddress { - fn from(value: StripeCustomerUpdateAddress) -> Self { - match value { - StripeCustomerUpdateAddress::Auto => { - stripe::CreateCheckoutSessionCustomerUpdateAddress::Auto - } - StripeCustomerUpdateAddress::Never => { - stripe::CreateCheckoutSessionCustomerUpdateAddress::Never - } - } - } -} - -impl From for stripe::CreateCheckoutSessionCustomerUpdateName { - fn from(value: StripeCustomerUpdateName) -> Self { - match value { - StripeCustomerUpdateName::Auto => stripe::CreateCheckoutSessionCustomerUpdateName::Auto, - StripeCustomerUpdateName::Never => { - stripe::CreateCheckoutSessionCustomerUpdateName::Never - } - } - } -} - -impl From for stripe::CreateCheckoutSessionCustomerUpdateShipping { - fn from(value: StripeCustomerUpdateShipping) -> Self { - match value { - StripeCustomerUpdateShipping::Auto => { - stripe::CreateCheckoutSessionCustomerUpdateShipping::Auto - } - StripeCustomerUpdateShipping::Never => { - stripe::CreateCheckoutSessionCustomerUpdateShipping::Never - } - } - } -} - -impl From for stripe::CreateCheckoutSessionCustomerUpdate { - fn from(value: StripeCustomerUpdate) -> Self { - stripe::CreateCheckoutSessionCustomerUpdate { - address: value.address.map(Into::into), - name: value.name.map(Into::into), - shipping: value.shipping.map(Into::into), - } - } -} - -impl From for stripe::CreateCheckoutSessionTaxIdCollection { - fn from(value: StripeTaxIdCollection) -> Self { - stripe::CreateCheckoutSessionTaxIdCollection { - enabled: value.enabled, - } - } -} diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 8d5d076780733406904cd1c0431d56d6ebbc776f..ddf245b06f322b5c62ba83d56f05fbca65e2ba9d 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -8,7 +8,6 @@ mod channel_buffer_tests; mod channel_guest_tests; mod channel_message_tests; mod channel_tests; -// mod debug_panel_tests; mod editor_tests; mod following_tests; mod git_tests; @@ -18,7 +17,6 @@ mod random_channel_buffer_tests; mod random_project_collaboration_tests; mod randomized_test_helpers; mod remote_editing_collaboration_tests; -mod stripe_billing_tests; mod test_server; use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust}; diff --git a/crates/collab/src/tests/stripe_billing_tests.rs b/crates/collab/src/tests/stripe_billing_tests.rs deleted file mode 100644 index bb84bedfcfc1fb4f95724f60bbd80707b12c215a..0000000000000000000000000000000000000000 --- a/crates/collab/src/tests/stripe_billing_tests.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::sync::Arc; - -use pretty_assertions::assert_eq; - -use crate::stripe_billing::StripeBilling; -use crate::stripe_client::{FakeStripeClient, StripePrice, StripePriceId, StripePriceRecurring}; - -fn make_stripe_billing() -> (StripeBilling, Arc) { - let stripe_client = Arc::new(FakeStripeClient::new()); - let stripe_billing = StripeBilling::test(stripe_client.clone()); - - (stripe_billing, stripe_client) -} - -#[gpui::test] -async fn test_initialize() { - let (stripe_billing, stripe_client) = make_stripe_billing(); - - // Add test prices - let price1 = StripePrice { - id: StripePriceId("price_1".into()), - unit_amount: Some(1_000), - lookup_key: Some("zed-pro".to_string()), - recurring: None, - }; - let price2 = StripePrice { - id: StripePriceId("price_2".into()), - unit_amount: Some(0), - lookup_key: Some("zed-free".to_string()), - recurring: None, - }; - let price3 = StripePrice { - id: StripePriceId("price_3".into()), - unit_amount: Some(500), - lookup_key: None, - recurring: Some(StripePriceRecurring { - meter: Some("meter_1".to_string()), - }), - }; - stripe_client - .prices - .lock() - .insert(price1.id.clone(), price1); - stripe_client - .prices - .lock() - .insert(price2.id.clone(), price2); - stripe_client - .prices - .lock() - .insert(price3.id.clone(), price3); - - // Initialize the billing system - stripe_billing.initialize().await.unwrap(); - - // Verify that prices can be found by lookup key - let zed_pro_price_id = stripe_billing.zed_pro_price_id().await.unwrap(); - assert_eq!(zed_pro_price_id.to_string(), "price_1"); - - let zed_free_price_id = stripe_billing.zed_free_price_id().await.unwrap(); - assert_eq!(zed_free_price_id.to_string(), "price_2"); - - // Verify that a price can be found by lookup key - let zed_pro_price = stripe_billing - .find_price_by_lookup_key("zed-pro") - .await - .unwrap(); - assert_eq!(zed_pro_price.id.to_string(), "price_1"); - assert_eq!(zed_pro_price.unit_amount, Some(1_000)); - - // Verify that finding a non-existent lookup key returns an error - let result = stripe_billing - .find_price_by_lookup_key("non-existent") - .await; - assert!(result.is_err()); -} - -#[gpui::test] -async fn test_find_or_create_customer_by_email() { - let (stripe_billing, stripe_client) = make_stripe_billing(); - - // Create a customer with an email that doesn't yet correspond to a customer. - { - let email = "user@example.com"; - - let customer_id = stripe_billing - .find_or_create_customer_by_email(Some(email)) - .await - .unwrap(); - - let customer = stripe_client - .customers - .lock() - .get(&customer_id) - .unwrap() - .clone(); - assert_eq!(customer.email.as_deref(), Some(email)); - } - - // Create a customer with an email that corresponds to an existing customer. - { - let email = "user2@example.com"; - - let existing_customer_id = stripe_billing - .find_or_create_customer_by_email(Some(email)) - .await - .unwrap(); - - let customer_id = stripe_billing - .find_or_create_customer_by_email(Some(email)) - .await - .unwrap(); - assert_eq!(customer_id, existing_customer_id); - - let customer = stripe_client - .customers - .lock() - .get(&customer_id) - .unwrap() - .clone(); - assert_eq!(customer.email.as_deref(), Some(email)); - } -} diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index f5a0e8ea81f0befbb3bae44ab516a7b8f4b04b52..8c545b0670ebc8c95d65da8bd7be6a40ad32aeab 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -1,4 +1,3 @@ -use crate::stripe_client::FakeStripeClient; use crate::{ AppState, Config, db::{NewUserParams, UserId, tests::TestDb}, @@ -569,9 +568,6 @@ impl TestServer { llm_db: None, livekit_client: Some(Arc::new(livekit_test_server.create_api_client())), blob_store_client: None, - real_stripe_client: None, - stripe_client: Some(Arc::new(FakeStripeClient::new())), - stripe_billing: None, executor, kinesis_client: None, config: Config { @@ -608,7 +604,6 @@ impl TestServer { auto_join_channel_id: None, migrations_path: None, seed_path: None, - stripe_api_key: None, supermaven_admin_api_key: None, user_backfiller_github_access_token: None, kinesis_region: None, From 9eb1ff272693a811c8f3f1b251a67c3a97f856e4 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Fri, 15 Aug 2025 18:03:36 -0300 Subject: [PATCH 050/744] acp thread view: Always use editors for user messages (#36256) This means the cursor will be at the position you clicked: https://github.com/user-attachments/assets/0693950d-7513-4d90-88e2-55817df7213a Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 10 +- .../agent_ui/src/acp/completion_provider.rs | 5 - crates/agent_ui/src/acp/entry_view_state.rs | 387 +++++++----- crates/agent_ui/src/acp/message_editor.rs | 28 +- crates/agent_ui/src/acp/thread_view.rs | 591 ++++++++++++------ crates/agent_ui/src/agent_panel.rs | 10 +- 6 files changed, 664 insertions(+), 367 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 4995ddb9dfbf17c50cca66244bf1f28098894dc8..2ef94a3cbe37472d4a8c3485493e06841111fabd 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -109,7 +109,7 @@ pub enum AgentThreadEntry { } impl AgentThreadEntry { - fn to_markdown(&self, cx: &App) -> String { + pub fn to_markdown(&self, cx: &App) -> String { match self { Self::UserMessage(message) => message.to_markdown(cx), Self::AssistantMessage(message) => message.to_markdown(cx), @@ -117,6 +117,14 @@ impl AgentThreadEntry { } } + pub fn user_message(&self) -> Option<&UserMessage> { + if let AgentThreadEntry::UserMessage(message) = self { + Some(message) + } else { + None + } + } + pub fn diffs(&self) -> impl Iterator> { if let AgentThreadEntry::ToolCall(call) = self { itertools::Either::Left(call.diffs()) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 4ee1eb69486ca32e2c7882588aa401ce908184d4..d7d2cd5d0ea2eabfc51225f4191afebab2dd33cc 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -80,11 +80,6 @@ impl MentionSet { .chain(self.images.drain().map(|(id, _)| id)) } - pub fn clear(&mut self) { - self.fetch_results.clear(); - self.uri_by_crease_id.clear(); - } - pub fn contents( &self, project: Entity, diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 2f5f855e90ded81066d8265cda9cf0449121107c..e99d1f6323ef36a8727bc78b69ce76c709324741 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -1,45 +1,141 @@ -use std::{collections::HashMap, ops::Range}; +use std::ops::Range; -use acp_thread::AcpThread; -use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer}; +use acp_thread::{AcpThread, AgentThreadEntry}; +use agent::{TextThreadStore, ThreadStore}; +use collections::HashMap; +use editor::{Editor, EditorMode, MinimapVisibility}; use gpui::{ - AnyEntity, App, AppContext as _, Entity, EntityId, TextStyleRefinement, WeakEntity, Window, + AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, TextStyleRefinement, + WeakEntity, Window, }; use language::language_settings::SoftWrap; +use project::Project; use settings::Settings as _; use terminal_view::TerminalView; use theme::ThemeSettings; -use ui::TextSize; +use ui::{Context, TextSize}; use workspace::Workspace; -#[derive(Default)] +use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; + pub struct EntryViewState { + workspace: WeakEntity, + project: Entity, + thread_store: Entity, + text_thread_store: Entity, entries: Vec, } impl EntryViewState { + pub fn new( + workspace: WeakEntity, + project: Entity, + thread_store: Entity, + text_thread_store: Entity, + ) -> Self { + Self { + workspace, + project, + thread_store, + text_thread_store, + entries: Vec::new(), + } + } + pub fn entry(&self, index: usize) -> Option<&Entry> { self.entries.get(index) } pub fn sync_entry( &mut self, - workspace: WeakEntity, - thread: Entity, index: usize, + thread: &Entity, window: &mut Window, - cx: &mut App, + cx: &mut Context, ) { - debug_assert!(index <= self.entries.len()); - let entry = if let Some(entry) = self.entries.get_mut(index) { - entry - } else { - self.entries.push(Entry::default()); - self.entries.last_mut().unwrap() + let Some(thread_entry) = thread.read(cx).entries().get(index) else { + return; + }; + + match thread_entry { + AgentThreadEntry::UserMessage(message) => { + let has_id = message.id.is_some(); + let chunks = message.chunks.clone(); + let message_editor = cx.new(|cx| { + let mut editor = MessageEditor::new( + self.workspace.clone(), + self.project.clone(), + self.thread_store.clone(), + self.text_thread_store.clone(), + editor::EditorMode::AutoHeight { + min_lines: 1, + max_lines: None, + }, + window, + cx, + ); + if !has_id { + editor.set_read_only(true, cx); + } + editor.set_message(chunks, window, cx); + editor + }); + cx.subscribe(&message_editor, move |_, editor, event, cx| { + cx.emit(EntryViewEvent { + entry_index: index, + view_event: ViewEvent::MessageEditorEvent(editor, *event), + }) + }) + .detach(); + self.set_entry(index, Entry::UserMessage(message_editor)); + } + AgentThreadEntry::ToolCall(tool_call) => { + let terminals = tool_call.terminals().cloned().collect::>(); + let diffs = tool_call.diffs().cloned().collect::>(); + + let views = if let Some(Entry::Content(views)) = self.entries.get_mut(index) { + views + } else { + self.set_entry(index, Entry::empty()); + let Some(Entry::Content(views)) = self.entries.get_mut(index) else { + unreachable!() + }; + views + }; + + for terminal in terminals { + views.entry(terminal.entity_id()).or_insert_with(|| { + create_terminal( + self.workspace.clone(), + self.project.clone(), + terminal.clone(), + window, + cx, + ) + .into_any() + }); + } + + for diff in diffs { + views + .entry(diff.entity_id()) + .or_insert_with(|| create_editor_diff(diff.clone(), window, cx).into_any()); + } + } + AgentThreadEntry::AssistantMessage(_) => { + if index == self.entries.len() { + self.entries.push(Entry::empty()) + } + } }; + } - entry.sync_diff_multibuffers(&thread, index, window, cx); - entry.sync_terminals(&workspace, &thread, index, window, cx); + fn set_entry(&mut self, index: usize, entry: Entry) { + if index == self.entries.len() { + self.entries.push(entry); + } else { + self.entries[index] = entry; + } } pub fn remove(&mut self, range: Range) { @@ -48,26 +144,51 @@ impl EntryViewState { pub fn settings_changed(&mut self, cx: &mut App) { for entry in self.entries.iter() { - for view in entry.views.values() { - if let Ok(diff_editor) = view.clone().downcast::() { - diff_editor.update(cx, |diff_editor, cx| { - diff_editor - .set_text_style_refinement(diff_editor_text_style_refinement(cx)); - cx.notify(); - }) + match entry { + Entry::UserMessage { .. } => {} + Entry::Content(response_views) => { + for view in response_views.values() { + if let Ok(diff_editor) = view.clone().downcast::() { + diff_editor.update(cx, |diff_editor, cx| { + diff_editor.set_text_style_refinement( + diff_editor_text_style_refinement(cx), + ); + cx.notify(); + }) + } + } } } } } } -pub struct Entry { - views: HashMap, +impl EventEmitter for EntryViewState {} + +pub struct EntryViewEvent { + pub entry_index: usize, + pub view_event: ViewEvent, +} + +pub enum ViewEvent { + MessageEditorEvent(Entity, MessageEditorEvent), +} + +pub enum Entry { + UserMessage(Entity), + Content(HashMap), } impl Entry { - pub fn editor_for_diff(&self, diff: &Entity) -> Option> { - self.views + pub fn message_editor(&self) -> Option<&Entity> { + match self { + Self::UserMessage(editor) => Some(editor), + Entry::Content(_) => None, + } + } + + pub fn editor_for_diff(&self, diff: &Entity) -> Option> { + self.content_map()? .get(&diff.entity_id()) .cloned() .map(|entity| entity.downcast::().unwrap()) @@ -77,118 +198,88 @@ impl Entry { &self, terminal: &Entity, ) -> Option> { - self.views + self.content_map()? .get(&terminal.entity_id()) .cloned() .map(|entity| entity.downcast::().unwrap()) } - fn sync_diff_multibuffers( - &mut self, - thread: &Entity, - index: usize, - window: &mut Window, - cx: &mut App, - ) { - let Some(entry) = thread.read(cx).entries().get(index) else { - return; - }; - - let multibuffers = entry - .diffs() - .map(|diff| diff.read(cx).multibuffer().clone()); - - let multibuffers = multibuffers.collect::>(); - - for multibuffer in multibuffers { - if self.views.contains_key(&multibuffer.entity_id()) { - return; - } - - let editor = cx.new(|cx| { - let mut editor = Editor::new( - EditorMode::Full { - scale_ui_elements_with_buffer_font_size: false, - show_active_line_background: false, - sized_by_content: true, - }, - multibuffer.clone(), - None, - window, - cx, - ); - editor.set_show_gutter(false, cx); - editor.disable_inline_diagnostics(); - editor.disable_expand_excerpt_buttons(cx); - editor.set_show_vertical_scrollbar(false, cx); - editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); - editor.set_soft_wrap_mode(SoftWrap::None, cx); - editor.scroll_manager.set_forbid_vertical_scroll(true); - editor.set_show_indent_guides(false, cx); - editor.set_read_only(true); - editor.set_show_breakpoints(false, cx); - editor.set_show_code_actions(false, cx); - editor.set_show_git_diff_gutter(false, cx); - editor.set_expand_all_diff_hunks(cx); - editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); - editor - }); - - let entity_id = multibuffer.entity_id(); - self.views.insert(entity_id, editor.into_any()); + fn content_map(&self) -> Option<&HashMap> { + match self { + Self::Content(map) => Some(map), + _ => None, } } - fn sync_terminals( - &mut self, - workspace: &WeakEntity, - thread: &Entity, - index: usize, - window: &mut Window, - cx: &mut App, - ) { - let Some(entry) = thread.read(cx).entries().get(index) else { - return; - }; - - let terminals = entry - .terminals() - .map(|terminal| terminal.clone()) - .collect::>(); - - for terminal in terminals { - if self.views.contains_key(&terminal.entity_id()) { - return; - } - - let Some(strong_workspace) = workspace.upgrade() else { - return; - }; - - let terminal_view = cx.new(|cx| { - let mut view = TerminalView::new( - terminal.read(cx).inner().clone(), - workspace.clone(), - None, - strong_workspace.read(cx).project().downgrade(), - window, - cx, - ); - view.set_embedded_mode(Some(1000), cx); - view - }); - - let entity_id = terminal.entity_id(); - self.views.insert(entity_id, terminal_view.into_any()); - } + fn empty() -> Self { + Self::Content(HashMap::default()) } #[cfg(test)] - pub fn len(&self) -> usize { - self.views.len() + pub fn has_content(&self) -> bool { + match self { + Self::Content(map) => !map.is_empty(), + Self::UserMessage(_) => false, + } } } +fn create_terminal( + workspace: WeakEntity, + project: Entity, + terminal: Entity, + window: &mut Window, + cx: &mut App, +) -> Entity { + cx.new(|cx| { + let mut view = TerminalView::new( + terminal.read(cx).inner().clone(), + workspace.clone(), + None, + project.downgrade(), + window, + cx, + ); + view.set_embedded_mode(Some(1000), cx); + view + }) +} + +fn create_editor_diff( + diff: Entity, + window: &mut Window, + cx: &mut App, +) -> Entity { + cx.new(|cx| { + let mut editor = Editor::new( + EditorMode::Full { + scale_ui_elements_with_buffer_font_size: false, + show_active_line_background: false, + sized_by_content: true, + }, + diff.read(cx).multibuffer().clone(), + None, + window, + cx, + ); + editor.set_show_gutter(false, cx); + editor.disable_inline_diagnostics(); + editor.disable_expand_excerpt_buttons(cx); + editor.set_show_vertical_scrollbar(false, cx); + editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); + editor.set_soft_wrap_mode(SoftWrap::None, cx); + editor.scroll_manager.set_forbid_vertical_scroll(true); + editor.set_show_indent_guides(false, cx); + editor.set_read_only(true); + editor.set_show_breakpoints(false, cx); + editor.set_show_code_actions(false, cx); + editor.set_show_git_diff_gutter(false, cx); + editor.set_expand_all_diff_hunks(cx); + editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); + editor + }) +} + fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement { TextStyleRefinement { font_size: Some( @@ -201,26 +292,20 @@ fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement { } } -impl Default for Entry { - fn default() -> Self { - Self { - // Avoid allocating in the heap by default - views: HashMap::with_capacity(0), - } - } -} - #[cfg(test)] mod tests { use std::{path::Path, rc::Rc}; use acp_thread::{AgentConnection, StubAgentConnection}; + use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol as acp; use agent_settings::AgentSettings; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use editor::{EditorSettings, RowInfo}; use fs::FakeFs; - use gpui::{SemanticVersion, TestAppContext}; + use gpui::{AppContext as _, SemanticVersion, TestAppContext}; + + use crate::acp::entry_view_state::EntryViewState; use multi_buffer::MultiBufferRow; use pretty_assertions::assert_matches; use project::Project; @@ -230,8 +315,6 @@ mod tests { use util::path; use workspace::Workspace; - use crate::acp::entry_view_state::EntryViewState; - #[gpui::test] async fn test_diff_sync(cx: &mut TestAppContext) { init_test(cx); @@ -269,7 +352,7 @@ mod tests { .update(|_, cx| { connection .clone() - .new_thread(project, Path::new(path!("/project")), cx) + .new_thread(project.clone(), Path::new(path!("/project")), cx) }) .await .unwrap(); @@ -279,12 +362,23 @@ mod tests { connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx) }); - let mut view_state = EntryViewState::default(); - cx.update(|window, cx| { - view_state.sync_entry(workspace.downgrade(), thread.clone(), 0, window, cx); + let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + + let view_state = cx.new(|_cx| { + EntryViewState::new( + workspace.downgrade(), + project.clone(), + thread_store, + text_thread_store, + ) + }); + + view_state.update_in(cx, |view_state, window, cx| { + view_state.sync_entry(0, &thread, window, cx) }); - let multibuffer = thread.read_with(cx, |thread, cx| { + let diff = thread.read_with(cx, |thread, _cx| { thread .entries() .get(0) @@ -292,15 +386,14 @@ mod tests { .diffs() .next() .unwrap() - .read(cx) - .multibuffer() .clone() }); cx.run_until_parked(); - let entry = view_state.entry(0).unwrap(); - let diff_editor = entry.editor_for_diff(&multibuffer).unwrap(); + let diff_editor = view_state.read_with(cx, |view_state, _cx| { + view_state.entry(0).unwrap().editor_for_diff(&diff).unwrap() + }); assert_eq!( diff_editor.read_with(cx, |editor, cx| editor.text(cx)), "hi world\nhello world" diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 32c37da519adff9c55b7cccb3c3d4e9a8d0db906..90827e55145c42f1f5a7f101ef6c8649c489d3c4 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -52,9 +52,11 @@ pub struct MessageEditor { text_thread_store: Entity, } +#[derive(Clone, Copy)] pub enum MessageEditorEvent { Send, Cancel, + Focus, } impl EventEmitter for MessageEditor {} @@ -101,6 +103,11 @@ impl MessageEditor { editor }); + cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| { + cx.emit(MessageEditorEvent::Focus) + }) + .detach(); + Self { editor, project, @@ -386,11 +393,11 @@ impl MessageEditor { }); } - fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context) { + fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context) { cx.emit(MessageEditorEvent::Send) } - fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { + fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context) { cx.emit(MessageEditorEvent::Cancel) } @@ -496,6 +503,13 @@ impl MessageEditor { } } + pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context) { + self.editor.update(cx, |message_editor, cx| { + message_editor.set_read_only(read_only); + cx.notify() + }) + } + fn insert_image( &mut self, excerpt_id: ExcerptId, @@ -572,6 +586,8 @@ impl MessageEditor { window: &mut Window, cx: &mut Context, ) { + self.clear(window, cx); + let mut text = String::new(); let mut mentions = Vec::new(); let mut images = Vec::new(); @@ -609,7 +625,6 @@ impl MessageEditor { editor.buffer().read(cx).snapshot(cx) }); - self.mention_set.clear(); for (range, mention_uri) in mentions { let anchor = snapshot.anchor_before(range.start); let crease_id = crate::context_picker::insert_crease_for_mention( @@ -679,6 +694,11 @@ impl MessageEditor { editor.set_text(text, window, cx); }); } + + #[cfg(test)] + pub fn text(&self, cx: &App) -> String { + self.editor.read(cx).text(cx) + } } impl Focusable for MessageEditor { @@ -691,7 +711,7 @@ impl Render for MessageEditor { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .key_context("MessageEditor") - .on_action(cx.listener(Self::chat)) + .on_action(cx.listener(Self::send)) .on_action(cx.listener(Self::cancel)) .capture_action(cx.listener(Self::paste)) .flex_1() diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index cb1a62fd11a97f56a5147a289bd8e1a349ce0286..17341e4c8ad38aaaa1e8ad2211d9ff3357575097 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -45,6 +45,7 @@ use zed_actions::assistant::OpenRulesLibrary; use super::entry_view_state::EntryViewState; use crate::acp::AcpModelSelectorPopover; +use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent}; use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; use crate::profile_selector::{ProfileProvider, ProfileSelector}; @@ -101,10 +102,8 @@ pub struct AcpThreadView { agent: Rc, workspace: WeakEntity, project: Entity, - thread_store: Entity, - text_thread_store: Entity, thread_state: ThreadState, - entry_view_state: EntryViewState, + entry_view_state: Entity, message_editor: Entity, model_selector: Option>, profile_selector: Option>, @@ -120,16 +119,9 @@ pub struct AcpThreadView { plan_expanded: bool, editor_expanded: bool, terminal_expanded: bool, - editing_message: Option, + editing_message: Option, _cancel_task: Option>, - _subscriptions: [Subscription; 2], -} - -struct EditingMessage { - index: usize, - message_id: UserMessageId, - editor: Entity, - _subscription: Subscription, + _subscriptions: [Subscription; 3], } enum ThreadState { @@ -176,24 +168,32 @@ impl AcpThreadView { let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0)); + let entry_view_state = cx.new(|_| { + EntryViewState::new( + workspace.clone(), + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + ) + }); + let subscriptions = [ cx.observe_global_in::(window, Self::settings_changed), - cx.subscribe_in(&message_editor, window, Self::on_message_editor_event), + cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event), + cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event), ]; Self { agent: agent.clone(), workspace: workspace.clone(), project: project.clone(), - thread_store, - text_thread_store, + entry_view_state, thread_state: Self::initial_state(agent, workspace, project, window, cx), message_editor, model_selector: None, profile_selector: None, notifications: Vec::new(), notification_subscriptions: HashMap::default(), - entry_view_state: EntryViewState::default(), list_state: list_state.clone(), scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), thread_error: None, @@ -414,7 +414,7 @@ impl AcpThreadView { cx.notify(); } - pub fn on_message_editor_event( + pub fn handle_message_editor_event( &mut self, _: &Entity, event: &MessageEditorEvent, @@ -424,6 +424,28 @@ impl AcpThreadView { match event { MessageEditorEvent::Send => self.send(window, cx), MessageEditorEvent::Cancel => self.cancel_generation(cx), + MessageEditorEvent::Focus => {} + } + } + + pub fn handle_entry_view_event( + &mut self, + _: &Entity, + event: &EntryViewEvent, + window: &mut Window, + cx: &mut Context, + ) { + match &event.view_event { + ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => { + self.editing_message = Some(event.entry_index); + cx.notify(); + } + ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => { + self.regenerate(event.entry_index, editor, window, cx); + } + ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => { + self.cancel_editing(&Default::default(), window, cx); + } } } @@ -494,27 +516,56 @@ impl AcpThreadView { .detach(); } - fn cancel_editing(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context) { - self.editing_message.take(); + fn cancel_editing(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { + let Some(thread) = self.thread().cloned() else { + return; + }; + + if let Some(index) = self.editing_message.take() { + if let Some(editor) = self + .entry_view_state + .read(cx) + .entry(index) + .and_then(|e| e.message_editor()) + .cloned() + { + editor.update(cx, |editor, cx| { + if let Some(user_message) = thread + .read(cx) + .entries() + .get(index) + .and_then(|e| e.user_message()) + { + editor.set_message(user_message.chunks.clone(), window, cx); + } + }) + } + }; + self.focus_handle(cx).focus(window); cx.notify(); } - fn regenerate(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - let Some(editing_message) = self.editing_message.take() else { + fn regenerate( + &mut self, + entry_ix: usize, + message_editor: &Entity, + window: &mut Window, + cx: &mut Context, + ) { + let Some(thread) = self.thread().cloned() else { return; }; - let Some(thread) = self.thread().cloned() else { + let Some(rewind) = thread.update(cx, |thread, cx| { + let user_message_id = thread.entries().get(entry_ix)?.user_message()?.id.clone()?; + Some(thread.rewind(user_message_id, cx)) + }) else { return; }; - let rewind = thread.update(cx, |thread, cx| { - thread.rewind(editing_message.message_id, cx) - }); + let contents = + message_editor.update(cx, |message_editor, cx| message_editor.contents(window, cx)); - let contents = editing_message - .editor - .update(cx, |message_editor, cx| message_editor.contents(window, cx)); let task = cx.foreground_executor().spawn(async move { rewind.await?; contents.await @@ -570,27 +621,20 @@ impl AcpThreadView { AcpThreadEvent::NewEntry => { let len = thread.read(cx).entries().len(); let index = len - 1; - self.entry_view_state.sync_entry( - self.workspace.clone(), - thread.clone(), - index, - window, - cx, - ); + self.entry_view_state.update(cx, |view_state, cx| { + view_state.sync_entry(index, &thread, window, cx) + }); self.list_state.splice(index..index, 1); } AcpThreadEvent::EntryUpdated(index) => { - self.entry_view_state.sync_entry( - self.workspace.clone(), - thread.clone(), - *index, - window, - cx, - ); + self.entry_view_state.update(cx, |view_state, cx| { + view_state.sync_entry(*index, &thread, window, cx) + }); self.list_state.splice(*index..index + 1, 1); } AcpThreadEvent::EntriesRemoved(range) => { - self.entry_view_state.remove(range.clone()); + self.entry_view_state + .update(cx, |view_state, _cx| view_state.remove(range.clone())); self.list_state.splice(range.clone(), 0); } AcpThreadEvent::ToolAuthorizationRequired => { @@ -722,29 +766,15 @@ impl AcpThreadView { .border_1() .border_color(cx.theme().colors().border) .text_xs() - .id("message") - .on_click(cx.listener({ - move |this, _, window, cx| { - this.start_editing_message(entry_ix, window, cx) - } - })) .children( - if let Some(editing) = self.editing_message.as_ref() - && Some(&editing.message_id) == message.id.as_ref() - { - Some( - self.render_edit_message_editor(editing, cx) - .into_any_element(), - ) - } else { - message.content.markdown().map(|md| { - self.render_markdown( - md.clone(), - user_message_markdown_style(window, cx), - ) - .into_any_element() - }) - }, + self.entry_view_state + .read(cx) + .entry(entry_ix) + .and_then(|entry| entry.message_editor()) + .map(|editor| { + self.render_sent_message_editor(entry_ix, editor, cx) + .into_any_element() + }), ), ) .into_any(), @@ -819,8 +849,8 @@ impl AcpThreadView { primary }; - if let Some(editing) = self.editing_message.as_ref() - && editing.index < entry_ix + if let Some(editing_index) = self.editing_message.as_ref() + && *editing_index < entry_ix { let backdrop = div() .id(("backdrop", entry_ix)) @@ -834,8 +864,8 @@ impl AcpThreadView { div() .relative() - .child(backdrop) .child(primary) + .child(backdrop) .into_any_element() } else { primary @@ -1256,9 +1286,7 @@ impl AcpThreadView { Empty.into_any_element() } } - ToolCallContent::Diff(diff) => { - self.render_diff_editor(entry_ix, &diff.read(cx).multibuffer(), cx) - } + ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, &diff, cx), ToolCallContent::Terminal(terminal) => { self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx) } @@ -1405,7 +1433,7 @@ impl AcpThreadView { fn render_diff_editor( &self, entry_ix: usize, - multibuffer: &Entity, + diff: &Entity, cx: &Context, ) -> AnyElement { v_flex() @@ -1413,8 +1441,8 @@ impl AcpThreadView { .border_t_1() .border_color(self.tool_card_border_color(cx)) .child( - if let Some(entry) = self.entry_view_state.entry(entry_ix) - && let Some(editor) = entry.editor_for_diff(&multibuffer) + if let Some(entry) = self.entry_view_state.read(cx).entry(entry_ix) + && let Some(editor) = entry.editor_for_diff(&diff) { editor.clone().into_any_element() } else { @@ -1617,6 +1645,7 @@ impl AcpThreadView { let terminal_view = self .entry_view_state + .read(cx) .entry(entry_ix) .and_then(|entry| entry.terminal(&terminal)); let show_output = self.terminal_expanded && terminal_view.is_some(); @@ -2485,82 +2514,38 @@ impl AcpThreadView { ) } - fn start_editing_message(&mut self, index: usize, window: &mut Window, cx: &mut Context) { - let Some(thread) = self.thread() else { - return; - }; - let Some(AgentThreadEntry::UserMessage(message)) = thread.read(cx).entries().get(index) - else { - return; - }; - let Some(message_id) = message.id.clone() else { - return; - }; - - self.list_state.scroll_to_reveal_item(index); - - let chunks = message.chunks.clone(); - let editor = cx.new(|cx| { - let mut editor = MessageEditor::new( - self.workspace.clone(), - self.project.clone(), - self.thread_store.clone(), - self.text_thread_store.clone(), - editor::EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ); - editor.set_message(chunks, window, cx); - editor - }); - let subscription = - cx.subscribe_in(&editor, window, |this, _, event, window, cx| match event { - MessageEditorEvent::Send => { - this.regenerate(&Default::default(), window, cx); - } - MessageEditorEvent::Cancel => { - this.cancel_editing(&Default::default(), window, cx); - } - }); - editor.focus_handle(cx).focus(window); - - self.editing_message.replace(EditingMessage { - index: index, - message_id: message_id.clone(), - editor, - _subscription: subscription, - }); - cx.notify(); - } - - fn render_edit_message_editor(&self, editing: &EditingMessage, cx: &Context) -> Div { - v_flex() - .w_full() - .gap_2() - .child(editing.editor.clone()) - .child( - h_flex() - .gap_1() - .child( - Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::XSmall), - ) - .child( - Label::new("Editing will restart the thread from this point.") - .color(Color::Muted) - .size(LabelSize::XSmall), - ) - .child(self.render_editing_message_editor_buttons(editing, cx)), - ) + fn render_sent_message_editor( + &self, + entry_ix: usize, + editor: &Entity, + cx: &Context, + ) -> Div { + v_flex().w_full().gap_2().child(editor.clone()).when( + self.editing_message == Some(entry_ix), + |el| { + el.child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::Warning) + .color(Color::Warning) + .size(IconSize::XSmall), + ) + .child( + Label::new("Editing will restart the thread from this point.") + .color(Color::Muted) + .size(LabelSize::XSmall), + ) + .child(self.render_sent_message_editor_buttons(entry_ix, editor, cx)), + ) + }, + ) } - fn render_editing_message_editor_buttons( + fn render_sent_message_editor_buttons( &self, - editing: &EditingMessage, + entry_ix: usize, + editor: &Entity, cx: &Context, ) -> Div { h_flex() @@ -2573,7 +2558,7 @@ impl AcpThreadView { .icon_color(Color::Error) .icon_size(IconSize::Small) .tooltip({ - let focus_handle = editing.editor.focus_handle(cx); + let focus_handle = editor.focus_handle(cx); move |window, cx| { Tooltip::for_action_in( "Cancel Edit", @@ -2588,12 +2573,12 @@ impl AcpThreadView { ) .child( IconButton::new("confirm-edit-message", IconName::Return) - .disabled(editing.editor.read(cx).is_empty(cx)) + .disabled(editor.read(cx).is_empty(cx)) .shape(ui::IconButtonShape::Square) .icon_color(Color::Muted) .icon_size(IconSize::Small) .tooltip({ - let focus_handle = editing.editor.focus_handle(cx); + let focus_handle = editor.focus_handle(cx); move |window, cx| { Tooltip::for_action_in( "Regenerate", @@ -2604,7 +2589,12 @@ impl AcpThreadView { ) } }) - .on_click(cx.listener(Self::regenerate)), + .on_click(cx.listener({ + let editor = editor.clone(); + move |this, _, window, cx| { + this.regenerate(entry_ix, &editor, window, cx); + } + })), ) } @@ -3137,7 +3127,9 @@ impl AcpThreadView { } fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context) { - self.entry_view_state.settings_changed(cx); + self.entry_view_state.update(cx, |entry_view_state, cx| { + entry_view_state.settings_changed(cx); + }); } pub(crate) fn insert_dragged_files( @@ -3152,9 +3144,7 @@ impl AcpThreadView { drop(added_worktrees); }) } -} -impl AcpThreadView { fn render_thread_error(&self, window: &mut Window, cx: &mut Context<'_, Self>) -> Option
{ let content = match self.thread_error.as_ref()? { ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx), @@ -3439,35 +3429,6 @@ impl Render for AcpThreadView { } } -fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { - let mut style = default_markdown_style(false, window, cx); - let mut text_style = window.text_style(); - let theme_settings = ThemeSettings::get_global(cx); - - let buffer_font = theme_settings.buffer_font.family.clone(); - let buffer_font_size = TextSize::Small.rems(cx); - - text_style.refine(&TextStyleRefinement { - font_family: Some(buffer_font), - font_size: Some(buffer_font_size.into()), - ..Default::default() - }); - - style.base_text_style = text_style; - style.link_callback = Some(Rc::new(move |url, cx| { - if MentionUri::parse(url).is_ok() { - let colors = cx.theme().colors(); - Some(TextStyleRefinement { - background_color: Some(colors.element_background), - ..Default::default() - }) - } else { - None - } - })); - style -} - fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle { let theme_settings = ThemeSettings::get_global(cx); let colors = cx.theme().colors(); @@ -3626,12 +3587,13 @@ pub(crate) mod tests { use agent_client_protocol::SessionId; use editor::EditorSettings; use fs::FakeFs; - use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; + use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext}; use project::Project; use serde_json::json; use settings::SettingsStore; use std::any::Any; use std::path::Path; + use workspace::Item; use super::*; @@ -3778,6 +3740,50 @@ pub(crate) mod tests { (thread_view, cx) } + fn add_to_workspace(thread_view: Entity, cx: &mut VisualTestContext) { + let workspace = thread_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone()); + + workspace + .update_in(cx, |workspace, window, cx| { + workspace.add_item_to_active_pane( + Box::new(cx.new(|_| ThreadViewItem(thread_view.clone()))), + None, + true, + window, + cx, + ); + }) + .unwrap(); + } + + struct ThreadViewItem(Entity); + + impl Item for ThreadViewItem { + type Event = (); + + fn include_in_nav_history() -> bool { + false + } + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Test".into() + } + } + + impl EventEmitter<()> for ThreadViewItem {} + + impl Focusable for ThreadViewItem { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.0.read(cx).focus_handle(cx).clone() + } + } + + impl Render for ThreadViewItem { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + self.0.clone().into_any_element() + } + } + struct StubAgentServer { connection: C, } @@ -3799,19 +3805,19 @@ pub(crate) mod tests { C: 'static + AgentConnection + Send + Clone, { fn logo(&self) -> ui::IconName { - unimplemented!() + ui::IconName::Ai } fn name(&self) -> &'static str { - unimplemented!() + "Test" } fn empty_state_headline(&self) -> &'static str { - unimplemented!() + "Test" } fn empty_state_message(&self) -> &'static str { - unimplemented!() + "Test" } fn connect( @@ -3960,9 +3966,17 @@ pub(crate) mod tests { assert_eq!(thread.entries().len(), 2); }); - thread_view.read_with(cx, |view, _| { - assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0); - assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1); + thread_view.read_with(cx, |view, cx| { + view.entry_view_state.read_with(cx, |entry_view_state, _| { + assert!( + entry_view_state + .entry(0) + .unwrap() + .message_editor() + .is_some() + ); + assert!(entry_view_state.entry(1).unwrap().has_content()); + }); }); // Second user message @@ -3991,18 +4005,31 @@ pub(crate) mod tests { let second_user_message_id = thread.read_with(cx, |thread, _| { assert_eq!(thread.entries().len(), 4); - let AgentThreadEntry::UserMessage(user_message) = thread.entries().get(2).unwrap() - else { + let AgentThreadEntry::UserMessage(user_message) = &thread.entries()[2] else { panic!(); }; user_message.id.clone().unwrap() }); - thread_view.read_with(cx, |view, _| { - assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0); - assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1); - assert_eq!(view.entry_view_state.entry(2).unwrap().len(), 0); - assert_eq!(view.entry_view_state.entry(3).unwrap().len(), 1); + thread_view.read_with(cx, |view, cx| { + view.entry_view_state.read_with(cx, |entry_view_state, _| { + assert!( + entry_view_state + .entry(0) + .unwrap() + .message_editor() + .is_some() + ); + assert!(entry_view_state.entry(1).unwrap().has_content()); + assert!( + entry_view_state + .entry(2) + .unwrap() + .message_editor() + .is_some() + ); + assert!(entry_view_state.entry(3).unwrap().has_content()); + }); }); // Rewind to first message @@ -4017,13 +4044,169 @@ pub(crate) mod tests { assert_eq!(thread.entries().len(), 2); }); - thread_view.read_with(cx, |view, _| { - assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0); - assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1); + thread_view.read_with(cx, |view, cx| { + view.entry_view_state.read_with(cx, |entry_view_state, _| { + assert!( + entry_view_state + .entry(0) + .unwrap() + .message_editor() + .is_some() + ); + assert!(entry_view_state.entry(1).unwrap().has_content()); - // Old views should be dropped - assert!(view.entry_view_state.entry(2).is_none()); - assert!(view.entry_view_state.entry(3).is_none()); + // Old views should be dropped + assert!(entry_view_state.entry(2).is_none()); + assert!(entry_view_state.entry(3).is_none()); + }); }); } + + #[gpui::test] + async fn test_message_editing_cancel(cx: &mut TestAppContext) { + init_test(cx); + + let connection = StubAgentConnection::new(); + + connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk { + content: acp::ContentBlock::Text(acp::TextContent { + text: "Response".into(), + annotations: None, + }), + }]); + + let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; + add_to_workspace(thread_view.clone(), cx); + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Original message to edit", window, cx); + }); + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.send(window, cx); + }); + + cx.run_until_parked(); + + let user_message_editor = thread_view.read_with(cx, |view, cx| { + assert_eq!(view.editing_message, None); + + view.entry_view_state + .read(cx) + .entry(0) + .unwrap() + .message_editor() + .unwrap() + .clone() + }); + + // Focus + cx.focus(&user_message_editor); + thread_view.read_with(cx, |view, _cx| { + assert_eq!(view.editing_message, Some(0)); + }); + + // Edit + user_message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Edited message content", window, cx); + }); + + // Cancel + user_message_editor.update_in(cx, |_editor, window, cx| { + window.dispatch_action(Box::new(editor::actions::Cancel), cx); + }); + + thread_view.read_with(cx, |view, _cx| { + assert_eq!(view.editing_message, None); + }); + + user_message_editor.read_with(cx, |editor, cx| { + assert_eq!(editor.text(cx), "Original message to edit"); + }); + } + + #[gpui::test] + async fn test_message_editing_regenerate(cx: &mut TestAppContext) { + init_test(cx); + + let connection = StubAgentConnection::new(); + + connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk { + content: acp::ContentBlock::Text(acp::TextContent { + text: "Response".into(), + annotations: None, + }), + }]); + + let (thread_view, cx) = + setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; + add_to_workspace(thread_view.clone(), cx); + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Original message to edit", window, cx); + }); + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.send(window, cx); + }); + + cx.run_until_parked(); + + let user_message_editor = thread_view.read_with(cx, |view, cx| { + assert_eq!(view.editing_message, None); + assert_eq!(view.thread().unwrap().read(cx).entries().len(), 2); + + view.entry_view_state + .read(cx) + .entry(0) + .unwrap() + .message_editor() + .unwrap() + .clone() + }); + + // Focus + cx.focus(&user_message_editor); + + // Edit + user_message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Edited message content", window, cx); + }); + + // Send + connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk { + content: acp::ContentBlock::Text(acp::TextContent { + text: "New Response".into(), + annotations: None, + }), + }]); + + user_message_editor.update_in(cx, |_editor, window, cx| { + window.dispatch_action(Box::new(Chat), cx); + }); + + cx.run_until_parked(); + + thread_view.read_with(cx, |view, cx| { + assert_eq!(view.editing_message, None); + + let entries = view.thread().unwrap().read(cx).entries(); + assert_eq!(entries.len(), 2); + assert_eq!( + entries[0].to_markdown(cx), + "## User\n\nEdited message content\n\n" + ); + assert_eq!( + entries[1].to_markdown(cx), + "## Assistant\n\nNew Response\n\n" + ); + + let new_editor = view.entry_view_state.read_with(cx, |state, _cx| { + assert!(!state.entry(1).unwrap().has_content()); + state.entry(0).unwrap().message_editor().unwrap().clone() + }); + + assert_eq!(new_editor.read(cx).text(cx), "Edited message content"); + }) + } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 73915195f5f483ef16b825d5b0b81d2d64c6dced..519f7980ff684cabaaa1d8d138e476009e2d8ed9 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -818,12 +818,10 @@ impl AgentPanel { ActiveView::Thread { thread, .. } => { thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx)); } - ActiveView::ExternalAgentThread { thread_view, .. } => { - thread_view.update(cx, |thread_element, cx| { - thread_element.cancel_generation(cx) - }); - } - ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} + ActiveView::ExternalAgentThread { .. } + | ActiveView::TextThread { .. } + | ActiveView::History + | ActiveView::Configuration => {} } } From f3654036189ba5ca414f9827aee52c0a9f7e95d9 Mon Sep 17 00:00:00 2001 From: Yang Gang Date: Sat, 16 Aug 2025 05:03:50 +0800 Subject: [PATCH 051/744] agent: Update use_modifier_to_send behavior description for Windows (#36230) Release Notes: - N/A Signed-off-by: Yang Gang --- crates/agent_settings/src/agent_settings.rs | 2 +- crates/agent_ui/src/agent_configuration.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index d9557c5d008bc902acae7e512c1b8532092f4c34..fd38ba1f7f0df640fbc9dd50976112092acc2db2 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -309,7 +309,7 @@ pub struct AgentSettingsContent { /// /// Default: true expand_terminal_card: Option, - /// Whether to always use cmd-enter (or ctrl-enter on Linux) to send messages in the agent panel. + /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel. /// /// Default: false use_modifier_to_send: Option, diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 5f72fa58c8489a21a7f386cea3b2678af37ba44f..96558f1beac5df0c4e7193e9468002bd12fae3a2 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -465,7 +465,7 @@ impl AgentConfiguration { "modifier-send", "Use modifier to submit a message", Some( - "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.".into(), + "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(), ), use_modifier_to_send, move |state, _window, cx| { From 3d77ad7e1a8a7afe068aac600d2ab56225fe1fed Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 15 Aug 2025 17:39:33 -0400 Subject: [PATCH 052/744] thread_view: Start loading images as soon as they're added (#36276) Release Notes: - N/A --- .../agent_ui/src/acp/completion_provider.rs | 129 ++++------- crates/agent_ui/src/acp/message_editor.rs | 215 +++++++++++------- 2 files changed, 169 insertions(+), 175 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index d7d2cd5d0ea2eabfc51225f4191afebab2dd33cc..1a9861d13a4acdf8bb1d418f617d38e115d93b0f 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,20 +1,17 @@ -use std::ffi::OsStr; use std::ops::Range; -use std::path::Path; +use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use acp_thread::MentionUri; use anyhow::{Context as _, Result, anyhow}; -use collections::HashMap; +use collections::{HashMap, HashSet}; use editor::display_map::CreaseId; use editor::{CompletionProvider, Editor, ExcerptId}; use futures::future::{Shared, try_join_all}; use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{App, Entity, ImageFormat, Img, Task, WeakEntity}; -use http_client::HttpClientWithUrl; +use gpui::{App, Entity, ImageFormat, Task, WeakEntity}; use language::{Buffer, CodeLabel, HighlightId}; -use language_model::LanguageModelImage; use lsp::CompletionContext; use project::{ Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId, @@ -43,7 +40,7 @@ use crate::context_picker::{ #[derive(Clone, Debug, Eq, PartialEq)] pub struct MentionImage { - pub abs_path: Option>, + pub abs_path: Option, pub data: SharedString, pub format: ImageFormat, } @@ -88,6 +85,8 @@ impl MentionSet { window: &mut Window, cx: &mut App, ) -> Task>> { + let mut processed_image_creases = HashSet::default(); + let mut contents = self .uri_by_crease_id .iter() @@ -97,59 +96,27 @@ impl MentionSet { // TODO directories let uri = uri.clone(); let abs_path = abs_path.to_path_buf(); - let extension = abs_path.extension().and_then(OsStr::to_str).unwrap_or(""); - - if Img::extensions().contains(&extension) && !extension.contains("svg") { - let open_image_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(&abs_path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_image(path, cx)) - }); - - cx.spawn(async move |cx| { - let image_item = open_image_task?.await?; - let (data, format) = image_item.update(cx, |image_item, cx| { - let format = image_item.image.format; - ( - LanguageModelImage::from_image( - image_item.image.clone(), - cx, - ), - format, - ) - })?; - let data = cx.spawn(async move |_| { - if let Some(data) = data.await { - Ok(data.source) - } else { - anyhow::bail!("Failed to convert image") - } - }); - anyhow::Ok(( - crease_id, - Mention::Image(MentionImage { - abs_path: Some(abs_path.as_path().into()), - data: data.await?, - format, - }), - )) - }) - } else { - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(abs_path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) + if let Some(task) = self.images.get(&crease_id).cloned() { + processed_image_creases.insert(crease_id); + return cx.spawn(async move |_| { + let image = task.await.map_err(|e| anyhow!("{e}"))?; + anyhow::Ok((crease_id, Mention::Image(image))) }); - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; - - anyhow::Ok((crease_id, Mention::Text { uri, content })) - }) } + + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(abs_path, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; + + anyhow::Ok((crease_id, Mention::Text { uri, content })) + }) } MentionUri::Symbol { path, line_range, .. @@ -243,15 +210,19 @@ impl MentionSet { }) .collect::>(); - contents.extend(self.images.iter().map(|(crease_id, image)| { + // Handle images that didn't have a mention URI (because they were added by the paste handler). + contents.extend(self.images.iter().filter_map(|(crease_id, image)| { + if processed_image_creases.contains(crease_id) { + return None; + } let crease_id = *crease_id; let image = image.clone(); - cx.spawn(async move |_| { + Some(cx.spawn(async move |_| { Ok(( crease_id, Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?), )) - }) + })) })); cx.spawn(async move |_cx| { @@ -753,7 +724,6 @@ impl ContextPickerCompletionProvider { source_range: Range, url_to_fetch: SharedString, message_editor: WeakEntity, - http_client: Arc, cx: &mut App, ) -> Option { let new_text = format!("@fetch {} ", url_to_fetch.clone()); @@ -772,30 +742,13 @@ impl ContextPickerCompletionProvider { source: project::CompletionSource::Custom, icon_path: Some(icon_path.clone()), insert_text_mode: None, - confirm: Some({ - Arc::new(move |_, window, cx| { - let url_to_fetch = url_to_fetch.clone(); - let source_range = source_range.clone(); - let message_editor = message_editor.clone(); - let new_text = new_text.clone(); - let http_client = http_client.clone(); - window.defer(cx, move |window, cx| { - message_editor - .update(cx, |message_editor, cx| { - message_editor.confirm_mention_for_fetch( - new_text, - source_range, - url_to_fetch, - http_client, - window, - cx, - ) - }) - .ok(); - }); - false - }) - }), + confirm: Some(confirm_completion_callback( + url_to_fetch.to_string().into(), + source_range.start, + new_text.len() - 1, + message_editor, + mention_uri, + )), }) } } @@ -843,7 +796,6 @@ impl CompletionProvider for ContextPickerCompletionProvider { }; let project = workspace.read(cx).project().clone(); - let http_client = workspace.read(cx).client().http_client(); let snapshot = buffer.read(cx).snapshot(); let source_range = snapshot.anchor_before(state.source_range.start) ..snapshot.anchor_after(state.source_range.end); @@ -852,8 +804,8 @@ impl CompletionProvider for ContextPickerCompletionProvider { let text_thread_store = self.text_thread_store.clone(); let editor = self.message_editor.clone(); let Ok((exclude_paths, exclude_threads)) = - self.message_editor.update(cx, |message_editor, cx| { - message_editor.mentioned_path_and_threads(cx) + self.message_editor.update(cx, |message_editor, _cx| { + message_editor.mentioned_path_and_threads() }) else { return Task::ready(Ok(Vec::new())); @@ -942,7 +894,6 @@ impl CompletionProvider for ContextPickerCompletionProvider { source_range.clone(), url, editor.clone(), - http_client.clone(), cx, ), diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 90827e55145c42f1f5a7f101ef6c8649c489d3c4..a4d74db2666e79ab3d13ea0aa920908d5d8b1145 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -16,14 +16,14 @@ use editor::{ use futures::{FutureExt as _, TryFutureExt as _}; use gpui::{ AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image, - ImageFormat, Task, TextStyle, WeakEntity, + ImageFormat, Img, Task, TextStyle, WeakEntity, }; -use http_client::HttpClientWithUrl; use language::{Buffer, Language}; use language_model::LanguageModelImage; use project::{CompletionIntent, Project}; use settings::Settings; use std::{ + ffi::OsStr, fmt::Write, ops::Range, path::{Path, PathBuf}, @@ -48,6 +48,7 @@ pub struct MessageEditor { mention_set: MentionSet, editor: Entity, project: Entity, + workspace: WeakEntity, thread_store: Entity, text_thread_store: Entity, } @@ -79,7 +80,7 @@ impl MessageEditor { None, ); let completion_provider = ContextPickerCompletionProvider::new( - workspace, + workspace.clone(), thread_store.downgrade(), text_thread_store.downgrade(), cx.weak_entity(), @@ -114,6 +115,7 @@ impl MessageEditor { mention_set, thread_store, text_thread_store, + workspace, } } @@ -131,7 +133,7 @@ impl MessageEditor { self.editor.read(cx).is_empty(cx) } - pub fn mentioned_path_and_threads(&self, _: &App) -> (HashSet, HashSet) { + pub fn mentioned_path_and_threads(&self) -> (HashSet, HashSet) { let mut excluded_paths = HashSet::default(); let mut excluded_threads = HashSet::default(); @@ -165,8 +167,14 @@ impl MessageEditor { let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { return; }; + let Some(anchor) = snapshot + .buffer_snapshot + .anchor_in_excerpt(*excerpt_id, start) + else { + return; + }; - if let Some(crease_id) = crate::context_picker::insert_crease_for_mention( + let Some(crease_id) = crate::context_picker::insert_crease_for_mention( *excerpt_id, start, content_len, @@ -175,49 +183,84 @@ impl MessageEditor { self.editor.clone(), window, cx, - ) { - self.mention_set.insert_uri(crease_id, mention_uri.clone()); + ) else { + return; + }; + self.mention_set.insert_uri(crease_id, mention_uri.clone()); + + match mention_uri { + MentionUri::Fetch { url } => { + self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx); + } + MentionUri::File { + abs_path, + is_directory, + } => { + self.confirm_mention_for_file( + crease_id, + anchor, + abs_path, + is_directory, + window, + cx, + ); + } + MentionUri::Symbol { .. } + | MentionUri::Thread { .. } + | MentionUri::TextThread { .. } + | MentionUri::Rule { .. } + | MentionUri::Selection { .. } => {} } } - pub fn confirm_mention_for_fetch( + fn confirm_mention_for_file( &mut self, - new_text: String, - source_range: Range, - url: url::Url, - http_client: Arc, + crease_id: CreaseId, + anchor: Anchor, + abs_path: PathBuf, + _is_directory: bool, window: &mut Window, cx: &mut Context, ) { - let mention_uri = MentionUri::Fetch { url: url.clone() }; - let icon_path = mention_uri.icon_path(cx); - - let start = source_range.start; - let content_len = new_text.len() - 1; - - let snapshot = self - .editor - .update(cx, |editor, cx| editor.snapshot(window, cx)); - let Some((&excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { + let extension = abs_path + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + let project = self.project.clone(); + let Some(project_path) = project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { return; }; - let Some(crease_id) = crate::context_picker::insert_crease_for_mention( - excerpt_id, - start, - content_len, - url.to_string().into(), - icon_path, - self.editor.clone(), - window, - cx, - ) else { + if Img::extensions().contains(&extension) && !extension.contains("svg") { + let image = cx.spawn(async move |_, cx| { + let image = project + .update(cx, |project, cx| project.open_image(project_path, cx))? + .await?; + image.read_with(cx, |image, _cx| image.image.clone()) + }); + self.confirm_mention_for_image(crease_id, anchor, Some(abs_path), image, window, cx); + } + } + + fn confirm_mention_for_fetch( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + url: url::Url, + window: &mut Window, + cx: &mut Context, + ) { + let Some(http_client) = self + .workspace + .update(cx, |workspace, _cx| workspace.client().http_client()) + .ok() + else { return; }; - let http_client = http_client.clone(); - let source_range = source_range.clone(); - let url_string = url.to_string(); let fetch = cx .background_executor() @@ -227,22 +270,18 @@ impl MessageEditor { .await }) .shared(); - self.mention_set.add_fetch_result(url, fetch.clone()); + self.mention_set + .add_fetch_result(url.clone(), fetch.clone()); cx.spawn_in(window, async move |this, cx| { let fetch = fetch.await.notify_async_err(cx); this.update(cx, |this, cx| { + let mention_uri = MentionUri::Fetch { url }; if fetch.is_some() { this.mention_set.insert_uri(crease_id, mention_uri.clone()); } else { // Remove crease if we failed to fetch this.editor.update(cx, |editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let Some(anchor) = - snapshot.anchor_in_excerpt(excerpt_id, source_range.start) - else { - return; - }; editor.display_map.update(cx, |display_map, cx| { display_map.unfold_intersecting(vec![anchor..anchor], true, cx); }); @@ -424,27 +463,46 @@ impl MessageEditor { let replacement_text = "image"; for image in images { - let (excerpt_id, anchor) = self.editor.update(cx, |message_editor, cx| { - let snapshot = message_editor.snapshot(window, cx); - let (excerpt_id, _, snapshot) = snapshot.buffer_snapshot.as_singleton().unwrap(); - - let anchor = snapshot.anchor_before(snapshot.len()); - message_editor.edit( - [( - multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), - format!("{replacement_text} "), - )], - cx, - ); - (*excerpt_id, anchor) - }); + let (excerpt_id, text_anchor, multibuffer_anchor) = + self.editor.update(cx, |message_editor, cx| { + let snapshot = message_editor.snapshot(window, cx); + let (excerpt_id, _, buffer_snapshot) = + snapshot.buffer_snapshot.as_singleton().unwrap(); + + let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len()); + let multibuffer_anchor = snapshot + .buffer_snapshot + .anchor_in_excerpt(*excerpt_id, text_anchor); + message_editor.edit( + [( + multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), + format!("{replacement_text} "), + )], + cx, + ); + (*excerpt_id, text_anchor, multibuffer_anchor) + }); - self.insert_image( + let content_len = replacement_text.len(); + let Some(anchor) = multibuffer_anchor else { + return; + }; + let Some(crease_id) = insert_crease_for_image( excerpt_id, + text_anchor, + content_len, + None.clone(), + self.editor.clone(), + window, + cx, + ) else { + return; + }; + self.confirm_mention_for_image( + crease_id, anchor, - replacement_text.len(), - Arc::new(image), None, + Task::ready(Ok(Arc::new(image))), window, cx, ); @@ -510,34 +568,25 @@ impl MessageEditor { }) } - fn insert_image( + fn confirm_mention_for_image( &mut self, - excerpt_id: ExcerptId, - crease_start: text::Anchor, - content_len: usize, - image: Arc, - abs_path: Option>, + crease_id: CreaseId, + anchor: Anchor, + abs_path: Option, + image: Task>>, window: &mut Window, cx: &mut Context, ) { - let Some(crease_id) = insert_crease_for_image( - excerpt_id, - crease_start, - content_len, - abs_path.clone(), - self.editor.clone(), - window, - cx, - ) else { - return; - }; self.editor.update(cx, |_editor, cx| { - let format = image.format; - let convert = LanguageModelImage::from_image(image, cx); - let task = cx .spawn_in(window, async move |editor, cx| { - if let Some(image) = convert.await { + let image = image.await.map_err(|e| e.to_string())?; + let format = image.format; + let image = cx + .update(|_, cx| LanguageModelImage::from_image(image, cx)) + .map_err(|e| e.to_string())? + .await; + if let Some(image) = image { Ok(MentionImage { abs_path, data: image.source, @@ -546,12 +595,6 @@ impl MessageEditor { } else { editor .update(cx, |editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let Some(anchor) = - snapshot.anchor_in_excerpt(excerpt_id, crease_start) - else { - return; - }; editor.display_map.update(cx, |display_map, cx| { display_map.unfold_intersecting(vec![anchor..anchor], true, cx); }); From f642f7615f876f56b1cb5bad90c9ee2bbf574bf0 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Fri, 15 Aug 2025 16:59:57 -0500 Subject: [PATCH 053/744] keymap_ui: Don't try to parse empty action arguments as JSON (#36278) Closes #ISSUE Release Notes: - Keymap Editor: Fixed an issue where leaving the arguments field empty would result in an error even if arguments were optional --- crates/settings_ui/src/keybindings.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 1aaab211aa4efb039d934aa5970d503640c72d5e..b4e871c617461ce7d760c4d9374b6ad3dacb2f23 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -2181,6 +2181,7 @@ impl KeybindingEditorModal { let value = action_arguments .as_ref() + .filter(|args| !args.is_empty()) .map(|args| { serde_json::from_str(args).context("Failed to parse action arguments as JSON") }) From b9c110e63e02eea44cde2c1e24d6d332e2a6f0ee Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 18:01:41 -0400 Subject: [PATCH 054/744] collab: Remove `GET /users/look_up` endpoint (#36279) This PR removes the `GET /users/look_up` endpoint from Collab, as it has been moved to Cloud. Release Notes: - N/A --- crates/collab/src/api.rs | 101 +-------------------------------------- 1 file changed, 1 insertion(+), 100 deletions(-) diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 143e764eb3ce531c8193a0fd3aa264a0f7c48c06..0cc7e2b2e93969ba7b8942838e4afcee251a20d9 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -4,12 +4,7 @@ pub mod extensions; pub mod ips_file; pub mod slack; -use crate::db::Database; -use crate::{ - AppState, Error, Result, auth, - db::{User, UserId}, - rpc, -}; +use crate::{AppState, Error, Result, auth, db::UserId, rpc}; use anyhow::Context as _; use axum::{ Extension, Json, Router, @@ -96,7 +91,6 @@ impl std::fmt::Display for SystemIdHeader { pub fn routes(rpc_server: Arc) -> Router<(), Body> { Router::new() - .route("/users/look_up", get(look_up_user)) .route("/users/:id/access_tokens", post(create_access_token)) .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) .merge(contributors::router()) @@ -138,99 +132,6 @@ pub async fn validate_api_token(req: Request, next: Next) -> impl IntoR Ok::<_, Error>(next.run(req).await) } -#[derive(Debug, Deserialize)] -struct LookUpUserParams { - identifier: String, -} - -#[derive(Debug, Serialize)] -struct LookUpUserResponse { - user: Option, -} - -async fn look_up_user( - Query(params): Query, - Extension(app): Extension>, -) -> Result> { - let user = resolve_identifier_to_user(&app.db, ¶ms.identifier).await?; - let user = if let Some(user) = user { - match user { - UserOrId::User(user) => Some(user), - UserOrId::Id(id) => app.db.get_user_by_id(id).await?, - } - } else { - None - }; - - Ok(Json(LookUpUserResponse { user })) -} - -enum UserOrId { - User(User), - Id(UserId), -} - -async fn resolve_identifier_to_user( - db: &Arc, - identifier: &str, -) -> Result> { - if let Some(identifier) = identifier.parse::().ok() { - let user = db.get_user_by_id(UserId(identifier)).await?; - - return Ok(user.map(UserOrId::User)); - } - - if identifier.starts_with("cus_") { - let billing_customer = db - .get_billing_customer_by_stripe_customer_id(&identifier) - .await?; - - return Ok(billing_customer.map(|billing_customer| UserOrId::Id(billing_customer.user_id))); - } - - if identifier.starts_with("sub_") { - let billing_subscription = db - .get_billing_subscription_by_stripe_subscription_id(&identifier) - .await?; - - if let Some(billing_subscription) = billing_subscription { - let billing_customer = db - .get_billing_customer_by_id(billing_subscription.billing_customer_id) - .await?; - - return Ok( - billing_customer.map(|billing_customer| UserOrId::Id(billing_customer.user_id)) - ); - } else { - return Ok(None); - } - } - - if identifier.contains('@') { - let user = db.get_user_by_email(identifier).await?; - - return Ok(user.map(UserOrId::User)); - } - - if let Some(user) = db.get_user_by_github_login(identifier).await? { - return Ok(Some(UserOrId::User(user))); - } - - Ok(None) -} - -#[derive(Deserialize, Debug)] -struct CreateUserParams { - github_user_id: i32, - github_login: String, - email_address: String, - email_confirmation_code: Option, - #[serde(default)] - admin: bool, - #[serde(default)] - invite_count: i32, -} - async fn get_rpc_server_snapshot( Extension(rpc_server): Extension>, ) -> Result { From bf34e185d518f02f032a420f5ed1a59f115b1a9f Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 15 Aug 2025 18:47:36 -0400 Subject: [PATCH 055/744] Move MentionSet to message_editor module (#36281) This is a more natural place for it than its current home next to the completion provider. Release Notes: - N/A --- .../agent_ui/src/acp/completion_provider.rs | 686 +-------------- crates/agent_ui/src/acp/message_editor.rs | 796 ++++++++++++++++-- 2 files changed, 743 insertions(+), 739 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 1a9861d13a4acdf8bb1d418f617d38e115d93b0f..8a413fc91ec1ad3d10a2bfcb7fb5e83cbc12ef92 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,16 +1,12 @@ use std::ops::Range; -use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use acp_thread::MentionUri; -use anyhow::{Context as _, Result, anyhow}; -use collections::{HashMap, HashSet}; -use editor::display_map::CreaseId; +use anyhow::Result; use editor::{CompletionProvider, Editor, ExcerptId}; -use futures::future::{Shared, try_join_all}; use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{App, Entity, ImageFormat, Task, WeakEntity}; +use gpui::{App, Entity, Task, WeakEntity}; use language::{Buffer, CodeLabel, HighlightId}; use lsp::CompletionContext; use project::{ @@ -20,7 +16,6 @@ use prompt_store::PromptStore; use rope::Point; use text::{Anchor, ToPoint as _}; use ui::prelude::*; -use url::Url; use workspace::Workspace; use agent::thread_store::{TextThreadStore, ThreadStore}; @@ -38,206 +33,6 @@ use crate::context_picker::{ available_context_picker_entries, recent_context_picker_entries, selection_ranges, }; -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct MentionImage { - pub abs_path: Option, - pub data: SharedString, - pub format: ImageFormat, -} - -#[derive(Default)] -pub struct MentionSet { - pub(crate) uri_by_crease_id: HashMap, - fetch_results: HashMap>>>, - images: HashMap>>>, -} - -impl MentionSet { - pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) { - self.uri_by_crease_id.insert(crease_id, uri); - } - - pub fn add_fetch_result(&mut self, url: Url, content: Shared>>) { - self.fetch_results.insert(url, content); - } - - pub fn insert_image( - &mut self, - crease_id: CreaseId, - task: Shared>>, - ) { - self.images.insert(crease_id, task); - } - - pub fn drain(&mut self) -> impl Iterator { - self.fetch_results.clear(); - self.uri_by_crease_id - .drain() - .map(|(id, _)| id) - .chain(self.images.drain().map(|(id, _)| id)) - } - - pub fn contents( - &self, - project: Entity, - thread_store: Entity, - text_thread_store: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - let mut processed_image_creases = HashSet::default(); - - let mut contents = self - .uri_by_crease_id - .iter() - .map(|(&crease_id, uri)| { - match uri { - MentionUri::File { abs_path, .. } => { - // TODO directories - let uri = uri.clone(); - let abs_path = abs_path.to_path_buf(); - - if let Some(task) = self.images.get(&crease_id).cloned() { - processed_image_creases.insert(crease_id); - return cx.spawn(async move |_| { - let image = task.await.map_err(|e| anyhow!("{e}"))?; - anyhow::Ok((crease_id, Mention::Image(image))) - }); - } - - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(abs_path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; - - anyhow::Ok((crease_id, Mention::Text { uri, content })) - }) - } - MentionUri::Symbol { - path, line_range, .. - } - | MentionUri::Selection { - path, line_range, .. - } => { - let uri = uri.clone(); - let path_buf = path.clone(); - let line_range = line_range.clone(); - - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(&path_buf, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| { - buffer - .text_for_range( - Point::new(line_range.start, 0) - ..Point::new( - line_range.end, - buffer.line_len(line_range.end), - ), - ) - .collect() - })?; - - anyhow::Ok((crease_id, Mention::Text { uri, content })) - }) - } - MentionUri::Thread { id: thread_id, .. } => { - let open_task = thread_store.update(cx, |thread_store, cx| { - thread_store.open_thread(&thread_id, window, cx) - }); - - let uri = uri.clone(); - cx.spawn(async move |cx| { - let thread = open_task.await?; - let content = thread.read_with(cx, |thread, _cx| { - thread.latest_detailed_summary_or_text().to_string() - })?; - - anyhow::Ok((crease_id, Mention::Text { uri, content })) - }) - } - MentionUri::TextThread { path, .. } => { - let context = text_thread_store.update(cx, |text_thread_store, cx| { - text_thread_store.open_local_context(path.as_path().into(), cx) - }); - let uri = uri.clone(); - cx.spawn(async move |cx| { - let context = context.await?; - let xml = context.update(cx, |context, cx| context.to_xml(cx))?; - anyhow::Ok((crease_id, Mention::Text { uri, content: xml })) - }) - } - MentionUri::Rule { id: prompt_id, .. } => { - let Some(prompt_store) = thread_store.read(cx).prompt_store().clone() - else { - return Task::ready(Err(anyhow!("missing prompt store"))); - }; - let text_task = prompt_store.read(cx).load(*prompt_id, cx); - let uri = uri.clone(); - cx.spawn(async move |_| { - // TODO: report load errors instead of just logging - let text = text_task.await?; - anyhow::Ok((crease_id, Mention::Text { uri, content: text })) - }) - } - MentionUri::Fetch { url } => { - let Some(content) = self.fetch_results.get(&url).cloned() else { - return Task::ready(Err(anyhow!("missing fetch result"))); - }; - let uri = uri.clone(); - cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Text { - uri, - content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, - }, - )) - }) - } - } - }) - .collect::>(); - - // Handle images that didn't have a mention URI (because they were added by the paste handler). - contents.extend(self.images.iter().filter_map(|(crease_id, image)| { - if processed_image_creases.contains(crease_id) { - return None; - } - let crease_id = *crease_id; - let image = image.clone(); - Some(cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?), - )) - })) - })); - - cx.spawn(async move |_cx| { - let contents = try_join_all(contents).await?.into_iter().collect(); - anyhow::Ok(contents) - }) - } -} - -#[derive(Debug, Eq, PartialEq)] -pub enum Mention { - Text { uri: MentionUri, content: String }, - Image(MentionImage), -} - pub(crate) enum Match { File(FileMatch), Symbol(SymbolMatch), @@ -1044,15 +839,6 @@ impl MentionCompletion { #[cfg(test)] mod tests { use super::*; - use editor::{AnchorRangeExt, EditorMode}; - use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext}; - use project::{Project, ProjectPath}; - use serde_json::json; - use settings::SettingsStore; - use smol::stream::StreamExt as _; - use std::{ops::Deref, path::Path}; - use util::path; - use workspace::{AppState, Item}; #[test] fn test_mention_completion_parse() { @@ -1123,472 +909,4 @@ mod tests { assert_eq!(MentionCompletion::try_parse("test@", 0), None); } - - struct MessageEditorItem(Entity); - - impl Item for MessageEditorItem { - type Event = (); - - fn include_in_nav_history() -> bool { - false - } - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Test".into() - } - } - - impl EventEmitter<()> for MessageEditorItem {} - - impl Focusable for MessageEditorItem { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx).clone() - } - } - - impl Render for MessageEditorItem { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - self.0.clone().into_any_element() - } - } - - #[gpui::test] - async fn test_context_completion_provider(cx: &mut TestAppContext) { - init_test(cx); - - let app_state = cx.update(AppState::test); - - cx.update(|cx| { - language::init(cx); - editor::init(cx); - workspace::init(app_state.clone(), cx); - Project::init_settings(cx); - }); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/dir"), - json!({ - "editor": "", - "a": { - "one.txt": "1", - "two.txt": "2", - "three.txt": "3", - "four.txt": "4" - }, - "b": { - "five.txt": "5", - "six.txt": "6", - "seven.txt": "7", - "eight.txt": "8", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - - let worktree = project.update(cx, |project, cx| { - let mut worktrees = project.worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - worktrees.pop().unwrap() - }); - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); - - let mut cx = VisualTestContext::from_window(*window.deref(), cx); - - let paths = vec![ - path!("a/one.txt"), - path!("a/two.txt"), - path!("a/three.txt"), - path!("a/four.txt"), - path!("b/five.txt"), - path!("b/six.txt"), - path!("b/seven.txt"), - path!("b/eight.txt"), - ]; - - let mut opened_editors = Vec::new(); - for path in paths { - let buffer = workspace - .update_in(&mut cx, |workspace, window, cx| { - workspace.open_path( - ProjectPath { - worktree_id, - path: Path::new(path).into(), - }, - None, - false, - window, - cx, - ) - }) - .await - .unwrap(); - opened_editors.push(buffer); - } - - let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - - let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { - let workspace_handle = cx.weak_entity(); - let message_editor = cx.new(|cx| { - MessageEditor::new( - workspace_handle, - project.clone(), - thread_store.clone(), - text_thread_store.clone(), - EditorMode::AutoHeight { - max_lines: None, - min_lines: 1, - }, - window, - cx, - ) - }); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item( - Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), - true, - true, - None, - window, - cx, - ); - }); - message_editor.read(cx).focus_handle(cx).focus(window); - let editor = message_editor.read(cx).editor().clone(); - (message_editor, editor) - }); - - cx.simulate_input("Lorem "); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem "); - assert!(!editor.has_visible_completions_menu()); - }); - - cx.simulate_input("@"); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem @"); - assert!(editor.has_visible_completions_menu()); - assert_eq!( - current_completion_labels(editor), - &[ - "eight.txt dir/b/", - "seven.txt dir/b/", - "six.txt dir/b/", - "five.txt dir/b/", - "Files & Directories", - "Symbols", - "Threads", - "Fetch" - ] - ); - }); - - // Select and confirm "File" - editor.update_in(&mut cx, |editor, window, cx| { - assert!(editor.has_visible_completions_menu()); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - cx.run_until_parked(); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem @file "); - assert!(editor.has_visible_completions_menu()); - }); - - cx.simulate_input("one"); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem @file one"); - assert!(editor.has_visible_completions_menu()); - assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - assert!(editor.has_visible_completions_menu()); - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) "); - assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); - }); - - let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - project.clone(), - thread_store.clone(), - text_thread_store.clone(), - window, - cx, - ) - }) - .await - .unwrap() - .into_values() - .collect::>(); - - pretty_assertions::assert_eq!( - contents, - [Mention::Text { - content: "1".into(), - uri: "file:///dir/a/one.txt".parse().unwrap() - }] - ); - - cx.simulate_input(" "); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) "); - assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); - }); - - cx.simulate_input("Ipsum "); - - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum ", - ); - assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); - }); - - cx.simulate_input("@file "); - - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum @file ", - ); - assert!(editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - cx.run_until_parked(); - - let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - project.clone(), - thread_store.clone(), - text_thread_store.clone(), - window, - cx, - ) - }) - .await - .unwrap() - .into_values() - .collect::>(); - - assert_eq!(contents.len(), 2); - pretty_assertions::assert_eq!( - contents[1], - Mention::Text { - content: "8".to_string(), - uri: "file:///dir/b/eight.txt".parse().unwrap(), - } - ); - - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) " - ); - assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![ - Point::new(0, 6)..Point::new(0, 39), - Point::new(0, 47)..Point::new(0, 84) - ] - ); - }); - - let plain_text_language = Arc::new(language::Language::new( - language::LanguageConfig { - name: "Plain Text".into(), - matcher: language::LanguageMatcher { - path_suffixes: vec!["txt".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - )); - - // Register the language and fake LSP - let language_registry = project.read_with(&cx, |project, _| project.languages().clone()); - language_registry.add(plain_text_language); - - let mut fake_language_servers = language_registry.register_fake_lsp( - "Plain Text", - language::FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - workspace_symbol_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - }, - ); - - // Open the buffer to trigger LSP initialization - let buffer = project - .update(&mut cx, |project, cx| { - project.open_local_buffer(path!("/dir/a/one.txt"), cx) - }) - .await - .unwrap(); - - // Register the buffer with language servers - let _handle = project.update(&mut cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - - cx.run_until_parked(); - - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.set_request_handler::( - |_, _| async move { - Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![ - #[allow(deprecated)] - lsp::SymbolInformation { - name: "MySymbol".into(), - location: lsp::Location { - uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(), - range: lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 1), - ), - }, - kind: lsp::SymbolKind::CONSTANT, - tags: None, - container_name: None, - deprecated: None, - }, - ]))) - }, - ); - - cx.simulate_input("@symbol "); - - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) @symbol " - ); - assert!(editor.has_visible_completions_menu()); - assert_eq!( - current_completion_labels(editor), - &[ - "MySymbol", - ] - ); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - project.clone(), - thread_store, - text_thread_store, - window, - cx, - ) - }) - .await - .unwrap() - .into_values() - .collect::>(); - - assert_eq!(contents.len(), 3); - pretty_assertions::assert_eq!( - contents[2], - Mention::Text { - content: "1".into(), - uri: "file:///dir/a/one.txt?symbol=MySymbol#L1:1" - .parse() - .unwrap(), - } - ); - - cx.run_until_parked(); - - editor.read_with(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) [@MySymbol](file:///dir/a/one.txt?symbol=MySymbol#L1:1) " - ); - }); - } - - fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec> { - let snapshot = editor.buffer().read(cx).snapshot(cx); - editor.display_map.update(cx, |display_map, cx| { - display_map - .snapshot(cx) - .folds_in_range(0..snapshot.len()) - .map(|fold| fold.range.to_point(&snapshot)) - .collect() - }) - } - - fn current_completion_labels(editor: &Editor) -> Vec { - let completions = editor.current_completions().expect("Missing completions"); - completions - .into_iter() - .map(|completion| completion.label.text.to_string()) - .collect::>() - } - - pub(crate) fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let store = SettingsStore::test(cx); - cx.set_global(store); - theme::init(theme::LoadThemes::JustBase, cx); - client::init_settings(cx); - language::init(cx); - Project::init_settings(cx); - workspace::init_settings(cx); - editor::init_settings(cx); - }); - } } diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index a4d74db2666e79ab3d13ea0aa920908d5d8b1145..f6fee3b87e2375cc6429391683999b522b602828 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1,19 +1,22 @@ use crate::{ - acp::completion_provider::{ContextPickerCompletionProvider, MentionImage, MentionSet}, + acp::completion_provider::ContextPickerCompletionProvider, context_picker::fetch_context_picker::fetch_url_content, }; use acp_thread::{MentionUri, selection_name}; use agent::{TextThreadStore, ThreadId, ThreadStore}; use agent_client_protocol as acp; -use anyhow::Result; -use collections::HashSet; +use anyhow::{Context as _, Result, anyhow}; +use collections::{HashMap, HashSet}; use editor::{ Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, ToOffset, actions::Paste, display_map::{Crease, CreaseId, FoldId}, }; -use futures::{FutureExt as _, TryFutureExt as _}; +use futures::{ + FutureExt as _, TryFutureExt as _, + future::{Shared, try_join_all}, +}; use gpui::{ AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, Task, TextStyle, WeakEntity, @@ -21,6 +24,7 @@ use gpui::{ use language::{Buffer, Language}; use language_model::LanguageModelImage; use project::{CompletionIntent, Project}; +use rope::Point; use settings::Settings; use std::{ ffi::OsStr, @@ -38,12 +42,11 @@ use ui::{ Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div, h_flex, }; +use url::Url; use util::ResultExt; use workspace::{Workspace, notifications::NotifyResultExt as _}; use zed_actions::agent::Chat; -use super::completion_provider::Mention; - pub struct MessageEditor { mention_set: MentionSet, editor: Entity, @@ -186,7 +189,6 @@ impl MessageEditor { ) else { return; }; - self.mention_set.insert_uri(crease_id, mention_uri.clone()); match mention_uri { MentionUri::Fetch { url } => { @@ -209,7 +211,9 @@ impl MessageEditor { | MentionUri::Thread { .. } | MentionUri::TextThread { .. } | MentionUri::Rule { .. } - | MentionUri::Selection { .. } => {} + | MentionUri::Selection { .. } => { + self.mention_set.insert_uri(crease_id, mention_uri.clone()); + } } } @@ -218,7 +222,7 @@ impl MessageEditor { crease_id: CreaseId, anchor: Anchor, abs_path: PathBuf, - _is_directory: bool, + is_directory: bool, window: &mut Window, cx: &mut Context, ) { @@ -226,15 +230,15 @@ impl MessageEditor { .extension() .and_then(OsStr::to_str) .unwrap_or_default(); - let project = self.project.clone(); - let Some(project_path) = project - .read(cx) - .project_path_for_absolute_path(&abs_path, cx) - else { - return; - }; if Img::extensions().contains(&extension) && !extension.contains("svg") { + let project = self.project.clone(); + let Some(project_path) = project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return; + }; let image = cx.spawn(async move |_, cx| { let image = project .update(cx, |project, cx| project.open_image(project_path, cx))? @@ -242,6 +246,14 @@ impl MessageEditor { image.read_with(cx, |image, _cx| image.image.clone()) }); self.confirm_mention_for_image(crease_id, anchor, Some(abs_path), image, window, cx); + } else { + self.mention_set.insert_uri( + crease_id, + MentionUri::File { + abs_path, + is_directory, + }, + ); } } @@ -577,43 +589,54 @@ impl MessageEditor { window: &mut Window, cx: &mut Context, ) { - self.editor.update(cx, |_editor, cx| { - let task = cx - .spawn_in(window, async move |editor, cx| { - let image = image.await.map_err(|e| e.to_string())?; - let format = image.format; - let image = cx - .update(|_, cx| LanguageModelImage::from_image(image, cx)) - .map_err(|e| e.to_string())? - .await; - if let Some(image) = image { - Ok(MentionImage { - abs_path, - data: image.source, - format, + let editor = self.editor.clone(); + let task = cx + .spawn_in(window, async move |this, cx| { + let image = image.await.map_err(|e| e.to_string())?; + let format = image.format; + let image = cx + .update(|_, cx| LanguageModelImage::from_image(image, cx)) + .map_err(|e| e.to_string())? + .await; + if let Some(image) = image { + if let Some(abs_path) = abs_path.clone() { + this.update(cx, |this, _cx| { + this.mention_set.insert_uri( + crease_id, + MentionUri::File { + abs_path, + is_directory: false, + }, + ); }) - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - Err("Failed to convert image".to_string()) + .map_err(|e| e.to_string())?; } - }) - .shared(); - - cx.spawn_in(window, { - let task = task.clone(); - async move |_, cx| task.clone().await.notify_async_err(cx) + Ok(MentionImage { + abs_path, + data: image.source, + format, + }) + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + Err("Failed to convert image".to_string()) + } }) - .detach(); + .shared(); - self.mention_set.insert_image(crease_id, task); - }); + cx.spawn_in(window, { + let task = task.clone(); + async move |_, cx| task.clone().await.notify_async_err(cx) + }) + .detach(); + + self.mention_set.insert_image(crease_id, task); } pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context) { @@ -867,22 +890,230 @@ fn render_image_fold_icon_button( }) } +#[derive(Debug, Eq, PartialEq)] +pub enum Mention { + Text { uri: MentionUri, content: String }, + Image(MentionImage), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MentionImage { + pub abs_path: Option, + pub data: SharedString, + pub format: ImageFormat, +} + +#[derive(Default)] +pub struct MentionSet { + pub(crate) uri_by_crease_id: HashMap, + fetch_results: HashMap>>>, + images: HashMap>>>, +} + +impl MentionSet { + pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) { + self.uri_by_crease_id.insert(crease_id, uri); + } + + pub fn add_fetch_result(&mut self, url: Url, content: Shared>>) { + self.fetch_results.insert(url, content); + } + + pub fn insert_image( + &mut self, + crease_id: CreaseId, + task: Shared>>, + ) { + self.images.insert(crease_id, task); + } + + pub fn drain(&mut self) -> impl Iterator { + self.fetch_results.clear(); + self.uri_by_crease_id + .drain() + .map(|(id, _)| id) + .chain(self.images.drain().map(|(id, _)| id)) + } + + pub fn contents( + &self, + project: Entity, + thread_store: Entity, + text_thread_store: Entity, + window: &mut Window, + cx: &mut App, + ) -> Task>> { + let mut processed_image_creases = HashSet::default(); + + let mut contents = self + .uri_by_crease_id + .iter() + .map(|(&crease_id, uri)| { + match uri { + MentionUri::File { abs_path, .. } => { + // TODO directories + let uri = uri.clone(); + let abs_path = abs_path.to_path_buf(); + + if let Some(task) = self.images.get(&crease_id).cloned() { + processed_image_creases.insert(crease_id); + return cx.spawn(async move |_| { + let image = task.await.map_err(|e| anyhow!("{e}"))?; + anyhow::Ok((crease_id, Mention::Image(image))) + }); + } + + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(abs_path, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; + + anyhow::Ok((crease_id, Mention::Text { uri, content })) + }) + } + MentionUri::Symbol { + path, line_range, .. + } + | MentionUri::Selection { + path, line_range, .. + } => { + let uri = uri.clone(); + let path_buf = path.clone(); + let line_range = line_range.clone(); + + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(&path_buf, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| { + buffer + .text_for_range( + Point::new(line_range.start, 0) + ..Point::new( + line_range.end, + buffer.line_len(line_range.end), + ), + ) + .collect() + })?; + + anyhow::Ok((crease_id, Mention::Text { uri, content })) + }) + } + MentionUri::Thread { id: thread_id, .. } => { + let open_task = thread_store.update(cx, |thread_store, cx| { + thread_store.open_thread(&thread_id, window, cx) + }); + + let uri = uri.clone(); + cx.spawn(async move |cx| { + let thread = open_task.await?; + let content = thread.read_with(cx, |thread, _cx| { + thread.latest_detailed_summary_or_text().to_string() + })?; + + anyhow::Ok((crease_id, Mention::Text { uri, content })) + }) + } + MentionUri::TextThread { path, .. } => { + let context = text_thread_store.update(cx, |text_thread_store, cx| { + text_thread_store.open_local_context(path.as_path().into(), cx) + }); + let uri = uri.clone(); + cx.spawn(async move |cx| { + let context = context.await?; + let xml = context.update(cx, |context, cx| context.to_xml(cx))?; + anyhow::Ok((crease_id, Mention::Text { uri, content: xml })) + }) + } + MentionUri::Rule { id: prompt_id, .. } => { + let Some(prompt_store) = thread_store.read(cx).prompt_store().clone() + else { + return Task::ready(Err(anyhow!("missing prompt store"))); + }; + let text_task = prompt_store.read(cx).load(*prompt_id, cx); + let uri = uri.clone(); + cx.spawn(async move |_| { + // TODO: report load errors instead of just logging + let text = text_task.await?; + anyhow::Ok((crease_id, Mention::Text { uri, content: text })) + }) + } + MentionUri::Fetch { url } => { + let Some(content) = self.fetch_results.get(&url).cloned() else { + return Task::ready(Err(anyhow!("missing fetch result"))); + }; + let uri = uri.clone(); + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Text { + uri, + content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, + }, + )) + }) + } + } + }) + .collect::>(); + + // Handle images that didn't have a mention URI (because they were added by the paste handler). + contents.extend(self.images.iter().filter_map(|(crease_id, image)| { + if processed_image_creases.contains(crease_id) { + return None; + } + let crease_id = *crease_id; + let image = image.clone(); + Some(cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?), + )) + })) + })); + + cx.spawn(async move |_cx| { + let contents = try_join_all(contents).await?.into_iter().collect(); + anyhow::Ok(contents) + }) + } +} + #[cfg(test)] mod tests { - use std::path::Path; + use std::{ops::Range, path::Path, sync::Arc}; use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol as acp; - use editor::EditorMode; + use editor::{AnchorRangeExt as _, Editor, EditorMode}; use fs::FakeFs; - use gpui::{AppContext, TestAppContext}; + use futures::StreamExt as _; + use gpui::{ + AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext, + }; use lsp::{CompletionContext, CompletionTriggerKind}; - use project::{CompletionIntent, Project}; + use project::{CompletionIntent, Project, ProjectPath}; use serde_json::json; + use text::Point; + use ui::{App, Context, IntoElement, Render, SharedString, Window}; use util::path; - use workspace::Workspace; + use workspace::{AppState, Item, Workspace}; - use crate::acp::{message_editor::MessageEditor, thread_view::tests::init_test}; + use crate::acp::{ + message_editor::{Mention, MessageEditor}, + thread_view::tests::init_test, + }; #[gpui::test] async fn test_at_mention_removal(cx: &mut TestAppContext) { @@ -982,4 +1213,459 @@ mod tests { // We don't send a resource link for the deleted crease. pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]); } + + struct MessageEditorItem(Entity); + + impl Item for MessageEditorItem { + type Event = (); + + fn include_in_nav_history() -> bool { + false + } + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Test".into() + } + } + + impl EventEmitter<()> for MessageEditorItem {} + + impl Focusable for MessageEditorItem { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.0.read(cx).focus_handle(cx).clone() + } + } + + impl Render for MessageEditorItem { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + self.0.clone().into_any_element() + } + } + + #[gpui::test] + async fn test_context_completion_provider(cx: &mut TestAppContext) { + init_test(cx); + + let app_state = cx.update(AppState::test); + + cx.update(|cx| { + language::init(cx); + editor::init(cx); + workspace::init(app_state.clone(), cx); + Project::init_settings(cx); + }); + + app_state + .fs + .as_fake() + .insert_tree( + path!("/dir"), + json!({ + "editor": "", + "a": { + "one.txt": "1", + "two.txt": "2", + "three.txt": "3", + "four.txt": "4" + }, + "b": { + "five.txt": "5", + "six.txt": "6", + "seven.txt": "7", + "eight.txt": "8", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; + let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let workspace = window.root(cx).unwrap(); + + let worktree = project.update(cx, |project, cx| { + let mut worktrees = project.worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + worktrees.pop().unwrap() + }); + let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); + + let mut cx = VisualTestContext::from_window(*window, cx); + + let paths = vec![ + path!("a/one.txt"), + path!("a/two.txt"), + path!("a/three.txt"), + path!("a/four.txt"), + path!("b/five.txt"), + path!("b/six.txt"), + path!("b/seven.txt"), + path!("b/eight.txt"), + ]; + + let mut opened_editors = Vec::new(); + for path in paths { + let buffer = workspace + .update_in(&mut cx, |workspace, window, cx| { + workspace.open_path( + ProjectPath { + worktree_id, + path: Path::new(path).into(), + }, + None, + false, + window, + cx, + ) + }) + .await + .unwrap(); + opened_editors.push(buffer); + } + + let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + + let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { + let workspace_handle = cx.weak_entity(); + let message_editor = cx.new(|cx| { + MessageEditor::new( + workspace_handle, + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + EditorMode::AutoHeight { + max_lines: None, + min_lines: 1, + }, + window, + cx, + ) + }); + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item( + Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), + true, + true, + None, + window, + cx, + ); + }); + message_editor.read(cx).focus_handle(cx).focus(window); + let editor = message_editor.read(cx).editor().clone(); + (message_editor, editor) + }); + + cx.simulate_input("Lorem "); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem "); + assert!(!editor.has_visible_completions_menu()); + }); + + cx.simulate_input("@"); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem @"); + assert!(editor.has_visible_completions_menu()); + assert_eq!( + current_completion_labels(editor), + &[ + "eight.txt dir/b/", + "seven.txt dir/b/", + "six.txt dir/b/", + "five.txt dir/b/", + "Files & Directories", + "Symbols", + "Threads", + "Fetch" + ] + ); + }); + + // Select and confirm "File" + editor.update_in(&mut cx, |editor, window, cx| { + assert!(editor.has_visible_completions_menu()); + editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); + editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); + editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); + editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + cx.run_until_parked(); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem @file "); + assert!(editor.has_visible_completions_menu()); + }); + + cx.simulate_input("one"); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem @file one"); + assert!(editor.has_visible_completions_menu()); + assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + assert!(editor.has_visible_completions_menu()); + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) "); + assert!(!editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![Point::new(0, 6)..Point::new(0, 39)] + ); + }); + + let contents = message_editor + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + window, + cx, + ) + }) + .await + .unwrap() + .into_values() + .collect::>(); + + pretty_assertions::assert_eq!( + contents, + [Mention::Text { + content: "1".into(), + uri: "file:///dir/a/one.txt".parse().unwrap() + }] + ); + + cx.simulate_input(" "); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) "); + assert!(!editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![Point::new(0, 6)..Point::new(0, 39)] + ); + }); + + cx.simulate_input("Ipsum "); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum ", + ); + assert!(!editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![Point::new(0, 6)..Point::new(0, 39)] + ); + }); + + cx.simulate_input("@file "); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum @file ", + ); + assert!(editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![Point::new(0, 6)..Point::new(0, 39)] + ); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + cx.run_until_parked(); + + let contents = message_editor + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + window, + cx, + ) + }) + .await + .unwrap() + .into_values() + .collect::>(); + + assert_eq!(contents.len(), 2); + pretty_assertions::assert_eq!( + contents[1], + Mention::Text { + content: "8".to_string(), + uri: "file:///dir/b/eight.txt".parse().unwrap(), + } + ); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) " + ); + assert!(!editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![ + Point::new(0, 6)..Point::new(0, 39), + Point::new(0, 47)..Point::new(0, 84) + ] + ); + }); + + let plain_text_language = Arc::new(language::Language::new( + language::LanguageConfig { + name: "Plain Text".into(), + matcher: language::LanguageMatcher { + path_suffixes: vec!["txt".to_string()], + ..Default::default() + }, + ..Default::default() + }, + None, + )); + + // Register the language and fake LSP + let language_registry = project.read_with(&cx, |project, _| project.languages().clone()); + language_registry.add(plain_text_language); + + let mut fake_language_servers = language_registry.register_fake_lsp( + "Plain Text", + language::FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + workspace_symbol_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + }, + ); + + // Open the buffer to trigger LSP initialization + let buffer = project + .update(&mut cx, |project, cx| { + project.open_local_buffer(path!("/dir/a/one.txt"), cx) + }) + .await + .unwrap(); + + // Register the buffer with language servers + let _handle = project.update(&mut cx, |project, cx| { + project.register_buffer_with_language_servers(&buffer, cx) + }); + + cx.run_until_parked(); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + fake_language_server.set_request_handler::( + |_, _| async move { + Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![ + #[allow(deprecated)] + lsp::SymbolInformation { + name: "MySymbol".into(), + location: lsp::Location { + uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(), + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 1), + ), + }, + kind: lsp::SymbolKind::CONSTANT, + tags: None, + container_name: None, + deprecated: None, + }, + ]))) + }, + ); + + cx.simulate_input("@symbol "); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) @symbol " + ); + assert!(editor.has_visible_completions_menu()); + assert_eq!( + current_completion_labels(editor), + &[ + "MySymbol", + ] + ); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + let contents = message_editor + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + project.clone(), + thread_store, + text_thread_store, + window, + cx, + ) + }) + .await + .unwrap() + .into_values() + .collect::>(); + + assert_eq!(contents.len(), 3); + pretty_assertions::assert_eq!( + contents[2], + Mention::Text { + content: "1".into(), + uri: "file:///dir/a/one.txt?symbol=MySymbol#L1:1" + .parse() + .unwrap(), + } + ); + + cx.run_until_parked(); + + editor.read_with(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) [@MySymbol](file:///dir/a/one.txt?symbol=MySymbol#L1:1) " + ); + }); + } + + fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec> { + let snapshot = editor.buffer().read(cx).snapshot(cx); + editor.display_map.update(cx, |display_map, cx| { + display_map + .snapshot(cx) + .folds_in_range(0..snapshot.len()) + .map(|fold| fold.range.to_point(&snapshot)) + .collect() + }) + } + + fn current_completion_labels(editor: &Editor) -> Vec { + let completions = editor.current_completions().expect("Missing completions"); + completions + .into_iter() + .map(|completion| completion.label.text.to_string()) + .collect::>() + } } From e664a9bc48dcc0e74d02772acd295ce6356e850b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 18:58:10 -0400 Subject: [PATCH 056/744] collab: Remove unused billing-related database code (#36282) This PR removes a bunch of unused database code related to billing, as we no longer need it. Release Notes: - N/A --- Cargo.lock | 1 - crates/collab/Cargo.toml | 1 - crates/collab/src/db.rs | 5 - crates/collab/src/db/ids.rs | 3 - crates/collab/src/db/queries.rs | 4 - .../src/db/queries/billing_customers.rs | 100 ----------- .../src/db/queries/billing_preferences.rs | 17 -- .../src/db/queries/billing_subscriptions.rs | 158 ----------------- .../src/db/queries/processed_stripe_events.rs | 69 -------- crates/collab/src/db/tables.rs | 4 - .../collab/src/db/tables/billing_customer.rs | 41 ----- .../src/db/tables/billing_preference.rs | 32 ---- .../src/db/tables/billing_subscription.rs | 161 ------------------ .../src/db/tables/processed_stripe_event.rs | 16 -- crates/collab/src/db/tables/user.rs | 8 - crates/collab/src/db/tests.rs | 1 - .../db/tests/processed_stripe_event_tests.rs | 38 ----- crates/collab/src/lib.rs | 17 -- crates/collab/src/llm/db.rs | 74 +------- crates/collab/src/llm/db/ids.rs | 11 -- crates/collab/src/llm/db/queries.rs | 5 - crates/collab/src/llm/db/queries/providers.rs | 134 --------------- .../src/llm/db/queries/subscription_usages.rs | 38 ----- crates/collab/src/llm/db/queries/usages.rs | 44 ----- crates/collab/src/llm/db/seed.rs | 45 ----- crates/collab/src/llm/db/tables.rs | 6 - crates/collab/src/llm/db/tables/model.rs | 48 ------ crates/collab/src/llm/db/tables/provider.rs | 25 --- .../src/llm/db/tables/subscription_usage.rs | 22 --- .../llm/db/tables/subscription_usage_meter.rs | 55 ------ crates/collab/src/llm/db/tables/usage.rs | 52 ------ .../collab/src/llm/db/tables/usage_measure.rs | 36 ---- crates/collab/src/llm/db/tests.rs | 107 ------------ .../collab/src/llm/db/tests/provider_tests.rs | 31 ---- crates/collab/src/main.rs | 10 -- crates/collab/src/tests/test_server.rs | 1 - 36 files changed, 1 insertion(+), 1419 deletions(-) delete mode 100644 crates/collab/src/db/queries/billing_customers.rs delete mode 100644 crates/collab/src/db/queries/billing_preferences.rs delete mode 100644 crates/collab/src/db/queries/billing_subscriptions.rs delete mode 100644 crates/collab/src/db/queries/processed_stripe_events.rs delete mode 100644 crates/collab/src/db/tables/billing_customer.rs delete mode 100644 crates/collab/src/db/tables/billing_preference.rs delete mode 100644 crates/collab/src/db/tables/billing_subscription.rs delete mode 100644 crates/collab/src/db/tables/processed_stripe_event.rs delete mode 100644 crates/collab/src/db/tests/processed_stripe_event_tests.rs delete mode 100644 crates/collab/src/llm/db/ids.rs delete mode 100644 crates/collab/src/llm/db/queries.rs delete mode 100644 crates/collab/src/llm/db/queries/providers.rs delete mode 100644 crates/collab/src/llm/db/queries/subscription_usages.rs delete mode 100644 crates/collab/src/llm/db/queries/usages.rs delete mode 100644 crates/collab/src/llm/db/seed.rs delete mode 100644 crates/collab/src/llm/db/tables.rs delete mode 100644 crates/collab/src/llm/db/tables/model.rs delete mode 100644 crates/collab/src/llm/db/tables/provider.rs delete mode 100644 crates/collab/src/llm/db/tables/subscription_usage.rs delete mode 100644 crates/collab/src/llm/db/tables/subscription_usage_meter.rs delete mode 100644 crates/collab/src/llm/db/tables/usage.rs delete mode 100644 crates/collab/src/llm/db/tables/usage_measure.rs delete mode 100644 crates/collab/src/llm/db/tests.rs delete mode 100644 crates/collab/src/llm/db/tests/provider_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 2be16cc22f9a0520b403d530fc79a0f1148431bd..3d72eed42e47646c1a65601ca6fb07ee5e64ee80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3270,7 +3270,6 @@ dependencies = [ "chrono", "client", "clock", - "cloud_llm_client", "collab_ui", "collections", "command_palette_hooks", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 6fc591be133f310f8401ed22589362e2621a949f..4fccd3be7ff8b4d44daf5f761695bdba81bd2ad8 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -29,7 +29,6 @@ axum-extra = { version = "0.4", features = ["erased-json"] } base64.workspace = true chrono.workspace = true clock.workspace = true -cloud_llm_client.workspace = true collections.workspace = true dashmap.workspace = true envy = "0.4.2" diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 2c22ca206945eb02752680b6149d7796643ee938..774eec5d2c553ae0014921a1df76bbf74cfabead 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -41,12 +41,7 @@ use worktree_settings_file::LocalSettingsKind; pub use tests::TestDb; pub use ids::*; -pub use queries::billing_customers::{CreateBillingCustomerParams, UpdateBillingCustomerParams}; -pub use queries::billing_subscriptions::{ - CreateBillingSubscriptionParams, UpdateBillingSubscriptionParams, -}; pub use queries::contributors::ContributorSelector; -pub use queries::processed_stripe_events::CreateProcessedStripeEventParams; pub use sea_orm::ConnectOptions; pub use tables::user::Model as User; pub use tables::*; diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 2ba7ec10514d8b0bbf4b26eab0b9384b3911204e..8f116cfd633749b21ff197a723f9e779a750b561 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -70,9 +70,6 @@ macro_rules! id_type { } id_type!(AccessTokenId); -id_type!(BillingCustomerId); -id_type!(BillingSubscriptionId); -id_type!(BillingPreferencesId); id_type!(BufferId); id_type!(ChannelBufferCollaboratorId); id_type!(ChannelChatParticipantId); diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index 64b627e47518e0c18eedd8c625a0c98b678a96cc..95e45dc00451dae27a98fd68c492f1047dea9804 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -1,9 +1,6 @@ use super::*; pub mod access_tokens; -pub mod billing_customers; -pub mod billing_preferences; -pub mod billing_subscriptions; pub mod buffers; pub mod channels; pub mod contacts; @@ -12,7 +9,6 @@ pub mod embeddings; pub mod extensions; pub mod messages; pub mod notifications; -pub mod processed_stripe_events; pub mod projects; pub mod rooms; pub mod servers; diff --git a/crates/collab/src/db/queries/billing_customers.rs b/crates/collab/src/db/queries/billing_customers.rs deleted file mode 100644 index ead9e6cd32dc4e52a5c0e2438e9e8ff97735a255..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/queries/billing_customers.rs +++ /dev/null @@ -1,100 +0,0 @@ -use super::*; - -#[derive(Debug)] -pub struct CreateBillingCustomerParams { - pub user_id: UserId, - pub stripe_customer_id: String, -} - -#[derive(Debug, Default)] -pub struct UpdateBillingCustomerParams { - pub user_id: ActiveValue, - pub stripe_customer_id: ActiveValue, - pub has_overdue_invoices: ActiveValue, - pub trial_started_at: ActiveValue>, -} - -impl Database { - /// Creates a new billing customer. - pub async fn create_billing_customer( - &self, - params: &CreateBillingCustomerParams, - ) -> Result { - self.transaction(|tx| async move { - let customer = billing_customer::Entity::insert(billing_customer::ActiveModel { - user_id: ActiveValue::set(params.user_id), - stripe_customer_id: ActiveValue::set(params.stripe_customer_id.clone()), - ..Default::default() - }) - .exec_with_returning(&*tx) - .await?; - - Ok(customer) - }) - .await - } - - /// Updates the specified billing customer. - pub async fn update_billing_customer( - &self, - id: BillingCustomerId, - params: &UpdateBillingCustomerParams, - ) -> Result<()> { - self.transaction(|tx| async move { - billing_customer::Entity::update(billing_customer::ActiveModel { - id: ActiveValue::set(id), - user_id: params.user_id.clone(), - stripe_customer_id: params.stripe_customer_id.clone(), - has_overdue_invoices: params.has_overdue_invoices.clone(), - trial_started_at: params.trial_started_at.clone(), - created_at: ActiveValue::not_set(), - }) - .exec(&*tx) - .await?; - - Ok(()) - }) - .await - } - - pub async fn get_billing_customer_by_id( - &self, - id: BillingCustomerId, - ) -> Result> { - self.transaction(|tx| async move { - Ok(billing_customer::Entity::find() - .filter(billing_customer::Column::Id.eq(id)) - .one(&*tx) - .await?) - }) - .await - } - - /// Returns the billing customer for the user with the specified ID. - pub async fn get_billing_customer_by_user_id( - &self, - user_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - Ok(billing_customer::Entity::find() - .filter(billing_customer::Column::UserId.eq(user_id)) - .one(&*tx) - .await?) - }) - .await - } - - /// Returns the billing customer for the user with the specified Stripe customer ID. - pub async fn get_billing_customer_by_stripe_customer_id( - &self, - stripe_customer_id: &str, - ) -> Result> { - self.transaction(|tx| async move { - Ok(billing_customer::Entity::find() - .filter(billing_customer::Column::StripeCustomerId.eq(stripe_customer_id)) - .one(&*tx) - .await?) - }) - .await - } -} diff --git a/crates/collab/src/db/queries/billing_preferences.rs b/crates/collab/src/db/queries/billing_preferences.rs deleted file mode 100644 index f370964ecd7d5c762c88e5fb572fde84ce81935d..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/queries/billing_preferences.rs +++ /dev/null @@ -1,17 +0,0 @@ -use super::*; - -impl Database { - /// Returns the billing preferences for the given user, if they exist. - pub async fn get_billing_preferences( - &self, - user_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - Ok(billing_preference::Entity::find() - .filter(billing_preference::Column::UserId.eq(user_id)) - .one(&*tx) - .await?) - }) - .await - } -} diff --git a/crates/collab/src/db/queries/billing_subscriptions.rs b/crates/collab/src/db/queries/billing_subscriptions.rs deleted file mode 100644 index 8361d6b4d07f8e6b59f9c7b39b18057e6f62b3c0..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/queries/billing_subscriptions.rs +++ /dev/null @@ -1,158 +0,0 @@ -use anyhow::Context as _; - -use crate::db::billing_subscription::{ - StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind, -}; - -use super::*; - -#[derive(Debug)] -pub struct CreateBillingSubscriptionParams { - pub billing_customer_id: BillingCustomerId, - pub kind: Option, - pub stripe_subscription_id: String, - pub stripe_subscription_status: StripeSubscriptionStatus, - pub stripe_cancellation_reason: Option, - pub stripe_current_period_start: Option, - pub stripe_current_period_end: Option, -} - -#[derive(Debug, Default)] -pub struct UpdateBillingSubscriptionParams { - pub billing_customer_id: ActiveValue, - pub kind: ActiveValue>, - pub stripe_subscription_id: ActiveValue, - pub stripe_subscription_status: ActiveValue, - pub stripe_cancel_at: ActiveValue>, - pub stripe_cancellation_reason: ActiveValue>, - pub stripe_current_period_start: ActiveValue>, - pub stripe_current_period_end: ActiveValue>, -} - -impl Database { - /// Creates a new billing subscription. - pub async fn create_billing_subscription( - &self, - params: &CreateBillingSubscriptionParams, - ) -> Result { - self.transaction(|tx| async move { - let id = billing_subscription::Entity::insert(billing_subscription::ActiveModel { - billing_customer_id: ActiveValue::set(params.billing_customer_id), - kind: ActiveValue::set(params.kind), - stripe_subscription_id: ActiveValue::set(params.stripe_subscription_id.clone()), - stripe_subscription_status: ActiveValue::set(params.stripe_subscription_status), - stripe_cancellation_reason: ActiveValue::set(params.stripe_cancellation_reason), - stripe_current_period_start: ActiveValue::set(params.stripe_current_period_start), - stripe_current_period_end: ActiveValue::set(params.stripe_current_period_end), - ..Default::default() - }) - .exec(&*tx) - .await? - .last_insert_id; - - Ok(billing_subscription::Entity::find_by_id(id) - .one(&*tx) - .await? - .context("failed to retrieve inserted billing subscription")?) - }) - .await - } - - /// Updates the specified billing subscription. - pub async fn update_billing_subscription( - &self, - id: BillingSubscriptionId, - params: &UpdateBillingSubscriptionParams, - ) -> Result<()> { - self.transaction(|tx| async move { - billing_subscription::Entity::update(billing_subscription::ActiveModel { - id: ActiveValue::set(id), - billing_customer_id: params.billing_customer_id.clone(), - kind: params.kind.clone(), - stripe_subscription_id: params.stripe_subscription_id.clone(), - stripe_subscription_status: params.stripe_subscription_status.clone(), - stripe_cancel_at: params.stripe_cancel_at.clone(), - stripe_cancellation_reason: params.stripe_cancellation_reason.clone(), - stripe_current_period_start: params.stripe_current_period_start.clone(), - stripe_current_period_end: params.stripe_current_period_end.clone(), - created_at: ActiveValue::not_set(), - }) - .exec(&*tx) - .await?; - - Ok(()) - }) - .await - } - - /// Returns the billing subscription with the specified Stripe subscription ID. - pub async fn get_billing_subscription_by_stripe_subscription_id( - &self, - stripe_subscription_id: &str, - ) -> Result> { - self.transaction(|tx| async move { - Ok(billing_subscription::Entity::find() - .filter( - billing_subscription::Column::StripeSubscriptionId.eq(stripe_subscription_id), - ) - .one(&*tx) - .await?) - }) - .await - } - - pub async fn get_active_billing_subscription( - &self, - user_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - Ok(billing_subscription::Entity::find() - .inner_join(billing_customer::Entity) - .filter(billing_customer::Column::UserId.eq(user_id)) - .filter( - Condition::all() - .add( - Condition::any() - .add( - billing_subscription::Column::StripeSubscriptionStatus - .eq(StripeSubscriptionStatus::Active), - ) - .add( - billing_subscription::Column::StripeSubscriptionStatus - .eq(StripeSubscriptionStatus::Trialing), - ), - ) - .add(billing_subscription::Column::Kind.is_not_null()), - ) - .one(&*tx) - .await?) - }) - .await - } - - /// Returns whether the user has an active billing subscription. - pub async fn has_active_billing_subscription(&self, user_id: UserId) -> Result { - Ok(self.count_active_billing_subscriptions(user_id).await? > 0) - } - - /// Returns the count of the active billing subscriptions for the user with the specified ID. - pub async fn count_active_billing_subscriptions(&self, user_id: UserId) -> Result { - self.transaction(|tx| async move { - let count = billing_subscription::Entity::find() - .inner_join(billing_customer::Entity) - .filter( - billing_customer::Column::UserId.eq(user_id).and( - billing_subscription::Column::StripeSubscriptionStatus - .eq(StripeSubscriptionStatus::Active) - .or(billing_subscription::Column::StripeSubscriptionStatus - .eq(StripeSubscriptionStatus::Trialing)), - ), - ) - .count(&*tx) - .await?; - - Ok(count as usize) - }) - .await - } -} diff --git a/crates/collab/src/db/queries/processed_stripe_events.rs b/crates/collab/src/db/queries/processed_stripe_events.rs deleted file mode 100644 index f14ad480e09fb4c0d6d43569b03e7888e9929cf4..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/queries/processed_stripe_events.rs +++ /dev/null @@ -1,69 +0,0 @@ -use super::*; - -#[derive(Debug)] -pub struct CreateProcessedStripeEventParams { - pub stripe_event_id: String, - pub stripe_event_type: String, - pub stripe_event_created_timestamp: i64, -} - -impl Database { - /// Creates a new processed Stripe event. - pub async fn create_processed_stripe_event( - &self, - params: &CreateProcessedStripeEventParams, - ) -> Result<()> { - self.transaction(|tx| async move { - processed_stripe_event::Entity::insert(processed_stripe_event::ActiveModel { - stripe_event_id: ActiveValue::set(params.stripe_event_id.clone()), - stripe_event_type: ActiveValue::set(params.stripe_event_type.clone()), - stripe_event_created_timestamp: ActiveValue::set( - params.stripe_event_created_timestamp, - ), - ..Default::default() - }) - .exec_without_returning(&*tx) - .await?; - - Ok(()) - }) - .await - } - - /// Returns the processed Stripe event with the specified event ID. - pub async fn get_processed_stripe_event_by_event_id( - &self, - event_id: &str, - ) -> Result> { - self.transaction(|tx| async move { - Ok(processed_stripe_event::Entity::find_by_id(event_id) - .one(&*tx) - .await?) - }) - .await - } - - /// Returns the processed Stripe events with the specified event IDs. - pub async fn get_processed_stripe_events_by_event_ids( - &self, - event_ids: &[&str], - ) -> Result> { - self.transaction(|tx| async move { - Ok(processed_stripe_event::Entity::find() - .filter( - processed_stripe_event::Column::StripeEventId.is_in(event_ids.iter().copied()), - ) - .all(&*tx) - .await?) - }) - .await - } - - /// Returns whether the Stripe event with the specified ID has already been processed. - pub async fn already_processed_stripe_event(&self, event_id: &str) -> Result { - Ok(self - .get_processed_stripe_event_by_event_id(event_id) - .await? - .is_some()) - } -} diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index d87ab174bd7a70be7ad57fd1871853018fc25763..0082a9fb030a27e4be13af725f08ea9c82217377 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -1,7 +1,4 @@ pub mod access_token; -pub mod billing_customer; -pub mod billing_preference; -pub mod billing_subscription; pub mod buffer; pub mod buffer_operation; pub mod buffer_snapshot; @@ -23,7 +20,6 @@ pub mod notification; pub mod notification_kind; pub mod observed_buffer_edits; pub mod observed_channel_messages; -pub mod processed_stripe_event; pub mod project; pub mod project_collaborator; pub mod project_repository; diff --git a/crates/collab/src/db/tables/billing_customer.rs b/crates/collab/src/db/tables/billing_customer.rs deleted file mode 100644 index e7d4a216e348a74b0cc79a308626fc1a80c508f6..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/tables/billing_customer.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::db::{BillingCustomerId, UserId}; -use sea_orm::entity::prelude::*; - -/// A billing customer. -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "billing_customers")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: BillingCustomerId, - pub user_id: UserId, - pub stripe_customer_id: String, - pub has_overdue_invoices: bool, - pub trial_started_at: Option, - pub created_at: DateTime, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::UserId", - to = "super::user::Column::Id" - )] - User, - #[sea_orm(has_many = "super::billing_subscription::Entity")] - BillingSubscription, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::BillingSubscription.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/billing_preference.rs b/crates/collab/src/db/tables/billing_preference.rs deleted file mode 100644 index c1888d3b2f9c954f0b9bcd38376f191ed383b973..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/tables/billing_preference.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::db::{BillingPreferencesId, UserId}; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "billing_preferences")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: BillingPreferencesId, - pub created_at: DateTime, - pub user_id: UserId, - pub max_monthly_llm_usage_spending_in_cents: i32, - pub model_request_overages_enabled: bool, - pub model_request_overages_spend_limit_in_cents: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::UserId", - to = "super::user::Column::Id" - )] - User, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/billing_subscription.rs b/crates/collab/src/db/tables/billing_subscription.rs deleted file mode 100644 index f5684aeec32de6178f5f0809c3db5e9974593089..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/tables/billing_subscription.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::db::{BillingCustomerId, BillingSubscriptionId}; -use chrono::{Datelike as _, NaiveDate, Utc}; -use sea_orm::entity::prelude::*; -use serde::Serialize; - -/// A billing subscription. -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "billing_subscriptions")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: BillingSubscriptionId, - pub billing_customer_id: BillingCustomerId, - pub kind: Option, - pub stripe_subscription_id: String, - pub stripe_subscription_status: StripeSubscriptionStatus, - pub stripe_cancel_at: Option, - pub stripe_cancellation_reason: Option, - pub stripe_current_period_start: Option, - pub stripe_current_period_end: Option, - pub created_at: DateTime, -} - -impl Model { - pub fn current_period_start_at(&self) -> Option { - let period_start = self.stripe_current_period_start?; - chrono::DateTime::from_timestamp(period_start, 0) - } - - pub fn current_period_end_at(&self) -> Option { - let period_end = self.stripe_current_period_end?; - chrono::DateTime::from_timestamp(period_end, 0) - } - - pub fn current_period( - subscription: Option, - is_staff: bool, - ) -> Option<(DateTimeUtc, DateTimeUtc)> { - if is_staff { - let now = Utc::now(); - let year = now.year(); - let month = now.month(); - - let first_day_of_this_month = - NaiveDate::from_ymd_opt(year, month, 1)?.and_hms_opt(0, 0, 0)?; - - let next_month = if month == 12 { 1 } else { month + 1 }; - let next_month_year = if month == 12 { year + 1 } else { year }; - let first_day_of_next_month = - NaiveDate::from_ymd_opt(next_month_year, next_month, 1)?.and_hms_opt(23, 59, 59)?; - - let last_day_of_this_month = first_day_of_next_month - chrono::Days::new(1); - - Some(( - first_day_of_this_month.and_utc(), - last_day_of_this_month.and_utc(), - )) - } else { - let subscription = subscription?; - let period_start_at = subscription.current_period_start_at()?; - let period_end_at = subscription.current_period_end_at()?; - - Some((period_start_at, period_end_at)) - } - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::billing_customer::Entity", - from = "Column::BillingCustomerId", - to = "super::billing_customer::Column::Id" - )] - BillingCustomer, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::BillingCustomer.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} - -#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] -#[serde(rename_all = "snake_case")] -pub enum SubscriptionKind { - #[sea_orm(string_value = "zed_pro")] - ZedPro, - #[sea_orm(string_value = "zed_pro_trial")] - ZedProTrial, - #[sea_orm(string_value = "zed_free")] - ZedFree, -} - -impl From for cloud_llm_client::Plan { - fn from(value: SubscriptionKind) -> Self { - match value { - SubscriptionKind::ZedPro => Self::ZedPro, - SubscriptionKind::ZedProTrial => Self::ZedProTrial, - SubscriptionKind::ZedFree => Self::ZedFree, - } - } -} - -/// The status of a Stripe subscription. -/// -/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-status) -#[derive( - Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash, Serialize, -)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] -#[serde(rename_all = "snake_case")] -pub enum StripeSubscriptionStatus { - #[default] - #[sea_orm(string_value = "incomplete")] - Incomplete, - #[sea_orm(string_value = "incomplete_expired")] - IncompleteExpired, - #[sea_orm(string_value = "trialing")] - Trialing, - #[sea_orm(string_value = "active")] - Active, - #[sea_orm(string_value = "past_due")] - PastDue, - #[sea_orm(string_value = "canceled")] - Canceled, - #[sea_orm(string_value = "unpaid")] - Unpaid, - #[sea_orm(string_value = "paused")] - Paused, -} - -impl StripeSubscriptionStatus { - pub fn is_cancelable(&self) -> bool { - match self { - Self::Trialing | Self::Active | Self::PastDue => true, - Self::Incomplete - | Self::IncompleteExpired - | Self::Canceled - | Self::Unpaid - | Self::Paused => false, - } - } -} - -/// The cancellation reason for a Stripe subscription. -/// -/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-cancellation_details-reason) -#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] -#[serde(rename_all = "snake_case")] -pub enum StripeCancellationReason { - #[sea_orm(string_value = "cancellation_requested")] - CancellationRequested, - #[sea_orm(string_value = "payment_disputed")] - PaymentDisputed, - #[sea_orm(string_value = "payment_failed")] - PaymentFailed, -} diff --git a/crates/collab/src/db/tables/processed_stripe_event.rs b/crates/collab/src/db/tables/processed_stripe_event.rs deleted file mode 100644 index 7b6f0cdc31d951caee57dc45c357178d375af9c8..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/tables/processed_stripe_event.rs +++ /dev/null @@ -1,16 +0,0 @@ -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "processed_stripe_events")] -pub struct Model { - #[sea_orm(primary_key)] - pub stripe_event_id: String, - pub stripe_event_type: String, - pub stripe_event_created_timestamp: i64, - pub processed_at: DateTime, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/user.rs b/crates/collab/src/db/tables/user.rs index 49fe3eb58f3ee149d9cfee88fd9c4b175854373b..af43fe300a6cc1224487541ca72af9d887a6fae3 100644 --- a/crates/collab/src/db/tables/user.rs +++ b/crates/collab/src/db/tables/user.rs @@ -29,8 +29,6 @@ pub struct Model { pub enum Relation { #[sea_orm(has_many = "super::access_token::Entity")] AccessToken, - #[sea_orm(has_one = "super::billing_customer::Entity")] - BillingCustomer, #[sea_orm(has_one = "super::room_participant::Entity")] RoomParticipant, #[sea_orm(has_many = "super::project::Entity")] @@ -68,12 +66,6 @@ impl Related for Entity { } } -impl Related for Entity { - fn to() -> RelationDef { - Relation::BillingCustomer.def() - } -} - impl Related for Entity { fn to() -> RelationDef { Relation::RoomParticipant.def() diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 6c2f9dc82a88c159df1111d01a213259ab3a6c76..2eb8d377acaba9f8fe5ea558a29cc028c2aa11fd 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -8,7 +8,6 @@ mod embedding_tests; mod extension_tests; mod feature_flag_tests; mod message_tests; -mod processed_stripe_event_tests; mod user_tests; use crate::migrations::run_database_migrations; diff --git a/crates/collab/src/db/tests/processed_stripe_event_tests.rs b/crates/collab/src/db/tests/processed_stripe_event_tests.rs deleted file mode 100644 index ad93b5a6589dd3a413bd5738dfc2e7debb9228d0..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/tests/processed_stripe_event_tests.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::sync::Arc; - -use crate::test_both_dbs; - -use super::{CreateProcessedStripeEventParams, Database}; - -test_both_dbs!( - test_already_processed_stripe_event, - test_already_processed_stripe_event_postgres, - test_already_processed_stripe_event_sqlite -); - -async fn test_already_processed_stripe_event(db: &Arc) { - let unprocessed_event_id = "evt_1PiJOuRxOf7d5PNaw2zzWiyO".to_string(); - let processed_event_id = "evt_1PiIfMRxOf7d5PNakHrAUe8P".to_string(); - - db.create_processed_stripe_event(&CreateProcessedStripeEventParams { - stripe_event_id: processed_event_id.clone(), - stripe_event_type: "customer.created".into(), - stripe_event_created_timestamp: 1722355968, - }) - .await - .unwrap(); - - assert!( - db.already_processed_stripe_event(&processed_event_id) - .await - .unwrap(), - "Expected {processed_event_id} to already be processed" - ); - - assert!( - !db.already_processed_stripe_event(&unprocessed_event_id) - .await - .unwrap(), - "Expected {unprocessed_event_id} to be unprocessed" - ); -} diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index a68286a5a3891b4e8d43b3c431ce6d19791377a2..191025df3770db78df3a12bc16d5c8f32d54571c 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -20,7 +20,6 @@ use axum::{ }; use db::{ChannelId, Database}; use executor::Executor; -use llm::db::LlmDatabase; use serde::Deserialize; use std::{path::PathBuf, sync::Arc}; use util::ResultExt; @@ -242,7 +241,6 @@ impl ServiceMode { pub struct AppState { pub db: Arc, - pub llm_db: Option>, pub livekit_client: Option>, pub blob_store_client: Option, pub executor: Executor, @@ -257,20 +255,6 @@ impl AppState { let mut db = Database::new(db_options).await?; db.initialize_notification_kinds().await?; - let llm_db = if let Some((llm_database_url, llm_database_max_connections)) = config - .llm_database_url - .clone() - .zip(config.llm_database_max_connections) - { - let mut llm_db_options = db::ConnectOptions::new(llm_database_url); - llm_db_options.max_connections(llm_database_max_connections); - let mut llm_db = LlmDatabase::new(llm_db_options, executor.clone()).await?; - llm_db.initialize().await?; - Some(Arc::new(llm_db)) - } else { - None - }; - let livekit_client = if let Some(((server, key), secret)) = config .livekit_server .as_ref() @@ -289,7 +273,6 @@ impl AppState { let db = Arc::new(db); let this = Self { db: db.clone(), - llm_db, livekit_client, blob_store_client: build_blob_store_client(&config).await.log_err(), executor, diff --git a/crates/collab/src/llm/db.rs b/crates/collab/src/llm/db.rs index 18ad624dab840c47df766a55c2f59cf9a17c55e6..b15d5a42b5f183831b34552beba3f616d3a7c3f0 100644 --- a/crates/collab/src/llm/db.rs +++ b/crates/collab/src/llm/db.rs @@ -1,30 +1,9 @@ -mod ids; -mod queries; -mod seed; -mod tables; - -#[cfg(test)] -mod tests; - -use cloud_llm_client::LanguageModelProvider; -use collections::HashMap; -pub use ids::*; -pub use seed::*; -pub use tables::*; - -#[cfg(test)] -pub use tests::TestLlmDb; -use usage_measure::UsageMeasure; - use std::future::Future; use std::sync::Arc; use anyhow::Context; pub use sea_orm::ConnectOptions; -use sea_orm::prelude::*; -use sea_orm::{ - ActiveValue, DatabaseConnection, DatabaseTransaction, IsolationLevel, TransactionTrait, -}; +use sea_orm::{DatabaseConnection, DatabaseTransaction, IsolationLevel, TransactionTrait}; use crate::Result; use crate::db::TransactionHandle; @@ -36,9 +15,6 @@ pub struct LlmDatabase { pool: DatabaseConnection, #[allow(unused)] executor: Executor, - provider_ids: HashMap, - models: HashMap<(LanguageModelProvider, String), model::Model>, - usage_measure_ids: HashMap, #[cfg(test)] runtime: Option, } @@ -51,59 +27,11 @@ impl LlmDatabase { options: options.clone(), pool: sea_orm::Database::connect(options).await?, executor, - provider_ids: HashMap::default(), - models: HashMap::default(), - usage_measure_ids: HashMap::default(), #[cfg(test)] runtime: None, }) } - pub async fn initialize(&mut self) -> Result<()> { - self.initialize_providers().await?; - self.initialize_models().await?; - self.initialize_usage_measures().await?; - Ok(()) - } - - /// Returns the list of all known models, with their [`LanguageModelProvider`]. - pub fn all_models(&self) -> Vec<(LanguageModelProvider, model::Model)> { - self.models - .iter() - .map(|((model_provider, _model_name), model)| (*model_provider, model.clone())) - .collect::>() - } - - /// Returns the names of the known models for the given [`LanguageModelProvider`]. - pub fn model_names_for_provider(&self, provider: LanguageModelProvider) -> Vec { - self.models - .keys() - .filter_map(|(model_provider, model_name)| { - if model_provider == &provider { - Some(model_name) - } else { - None - } - }) - .cloned() - .collect::>() - } - - pub fn model(&self, provider: LanguageModelProvider, name: &str) -> Result<&model::Model> { - Ok(self - .models - .get(&(provider, name.to_string())) - .with_context(|| format!("unknown model {provider:?}:{name}"))?) - } - - pub fn model_by_id(&self, id: ModelId) -> Result<&model::Model> { - Ok(self - .models - .values() - .find(|model| model.id == id) - .with_context(|| format!("no model for ID {id:?}"))?) - } - pub fn options(&self) -> &ConnectOptions { &self.options } diff --git a/crates/collab/src/llm/db/ids.rs b/crates/collab/src/llm/db/ids.rs deleted file mode 100644 index 03cab6cee0b9e7a07f2d4d43aa7e556615e34494..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/ids.rs +++ /dev/null @@ -1,11 +0,0 @@ -use sea_orm::{DbErr, entity::prelude::*}; -use serde::{Deserialize, Serialize}; - -use crate::id_type; - -id_type!(BillingEventId); -id_type!(ModelId); -id_type!(ProviderId); -id_type!(RevokedAccessTokenId); -id_type!(UsageId); -id_type!(UsageMeasureId); diff --git a/crates/collab/src/llm/db/queries.rs b/crates/collab/src/llm/db/queries.rs deleted file mode 100644 index 0087218b3ff9fe81850870bc8022bd81fe0ee48d..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/queries.rs +++ /dev/null @@ -1,5 +0,0 @@ -use super::*; - -pub mod providers; -pub mod subscription_usages; -pub mod usages; diff --git a/crates/collab/src/llm/db/queries/providers.rs b/crates/collab/src/llm/db/queries/providers.rs deleted file mode 100644 index 9c7dbdd1847ea1d087582ffd959497bc41757b75..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/queries/providers.rs +++ /dev/null @@ -1,134 +0,0 @@ -use super::*; -use sea_orm::{QueryOrder, sea_query::OnConflict}; -use std::str::FromStr; -use strum::IntoEnumIterator as _; - -pub struct ModelParams { - pub provider: LanguageModelProvider, - pub name: String, - pub max_requests_per_minute: i64, - pub max_tokens_per_minute: i64, - pub max_tokens_per_day: i64, - pub price_per_million_input_tokens: i32, - pub price_per_million_output_tokens: i32, -} - -impl LlmDatabase { - pub async fn initialize_providers(&mut self) -> Result<()> { - self.provider_ids = self - .transaction(|tx| async move { - let existing_providers = provider::Entity::find().all(&*tx).await?; - - let mut new_providers = LanguageModelProvider::iter() - .filter(|provider| { - !existing_providers - .iter() - .any(|p| p.name == provider.to_string()) - }) - .map(|provider| provider::ActiveModel { - name: ActiveValue::set(provider.to_string()), - ..Default::default() - }) - .peekable(); - - if new_providers.peek().is_some() { - provider::Entity::insert_many(new_providers) - .exec(&*tx) - .await?; - } - - let all_providers: HashMap<_, _> = provider::Entity::find() - .all(&*tx) - .await? - .iter() - .filter_map(|provider| { - LanguageModelProvider::from_str(&provider.name) - .ok() - .map(|p| (p, provider.id)) - }) - .collect(); - - Ok(all_providers) - }) - .await?; - Ok(()) - } - - pub async fn initialize_models(&mut self) -> Result<()> { - let all_provider_ids = &self.provider_ids; - self.models = self - .transaction(|tx| async move { - let all_models: HashMap<_, _> = model::Entity::find() - .all(&*tx) - .await? - .into_iter() - .filter_map(|model| { - let provider = all_provider_ids.iter().find_map(|(provider, id)| { - if *id == model.provider_id { - Some(provider) - } else { - None - } - })?; - Some(((*provider, model.name.clone()), model)) - }) - .collect(); - Ok(all_models) - }) - .await?; - Ok(()) - } - - pub async fn insert_models(&mut self, models: &[ModelParams]) -> Result<()> { - let all_provider_ids = &self.provider_ids; - self.transaction(|tx| async move { - model::Entity::insert_many(models.iter().map(|model_params| { - let provider_id = all_provider_ids[&model_params.provider]; - model::ActiveModel { - provider_id: ActiveValue::set(provider_id), - name: ActiveValue::set(model_params.name.clone()), - max_requests_per_minute: ActiveValue::set(model_params.max_requests_per_minute), - max_tokens_per_minute: ActiveValue::set(model_params.max_tokens_per_minute), - max_tokens_per_day: ActiveValue::set(model_params.max_tokens_per_day), - price_per_million_input_tokens: ActiveValue::set( - model_params.price_per_million_input_tokens, - ), - price_per_million_output_tokens: ActiveValue::set( - model_params.price_per_million_output_tokens, - ), - ..Default::default() - } - })) - .on_conflict( - OnConflict::columns([model::Column::ProviderId, model::Column::Name]) - .update_columns([ - model::Column::MaxRequestsPerMinute, - model::Column::MaxTokensPerMinute, - model::Column::MaxTokensPerDay, - model::Column::PricePerMillionInputTokens, - model::Column::PricePerMillionOutputTokens, - ]) - .to_owned(), - ) - .exec_without_returning(&*tx) - .await?; - Ok(()) - }) - .await?; - self.initialize_models().await - } - - /// Returns the list of LLM providers. - pub async fn list_providers(&self) -> Result> { - self.transaction(|tx| async move { - Ok(provider::Entity::find() - .order_by_asc(provider::Column::Name) - .all(&*tx) - .await? - .into_iter() - .filter_map(|p| LanguageModelProvider::from_str(&p.name).ok()) - .collect()) - }) - .await - } -} diff --git a/crates/collab/src/llm/db/queries/subscription_usages.rs b/crates/collab/src/llm/db/queries/subscription_usages.rs deleted file mode 100644 index 8a519790753099be62868e94e8b068958095d320..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/queries/subscription_usages.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::db::UserId; - -use super::*; - -impl LlmDatabase { - pub async fn get_subscription_usage_for_period( - &self, - user_id: UserId, - period_start_at: DateTimeUtc, - period_end_at: DateTimeUtc, - ) -> Result> { - self.transaction(|tx| async move { - self.get_subscription_usage_for_period_in_tx( - user_id, - period_start_at, - period_end_at, - &tx, - ) - .await - }) - .await - } - - async fn get_subscription_usage_for_period_in_tx( - &self, - user_id: UserId, - period_start_at: DateTimeUtc, - period_end_at: DateTimeUtc, - tx: &DatabaseTransaction, - ) -> Result> { - Ok(subscription_usage::Entity::find() - .filter(subscription_usage::Column::UserId.eq(user_id)) - .filter(subscription_usage::Column::PeriodStartAt.eq(period_start_at)) - .filter(subscription_usage::Column::PeriodEndAt.eq(period_end_at)) - .one(tx) - .await?) - } -} diff --git a/crates/collab/src/llm/db/queries/usages.rs b/crates/collab/src/llm/db/queries/usages.rs deleted file mode 100644 index a917703f960e657f3ebe345a59558525c7aaa4bb..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/queries/usages.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::str::FromStr; -use strum::IntoEnumIterator as _; - -use super::*; - -impl LlmDatabase { - pub async fn initialize_usage_measures(&mut self) -> Result<()> { - let all_measures = self - .transaction(|tx| async move { - let existing_measures = usage_measure::Entity::find().all(&*tx).await?; - - let new_measures = UsageMeasure::iter() - .filter(|measure| { - !existing_measures - .iter() - .any(|m| m.name == measure.to_string()) - }) - .map(|measure| usage_measure::ActiveModel { - name: ActiveValue::set(measure.to_string()), - ..Default::default() - }) - .collect::>(); - - if !new_measures.is_empty() { - usage_measure::Entity::insert_many(new_measures) - .exec(&*tx) - .await?; - } - - Ok(usage_measure::Entity::find().all(&*tx).await?) - }) - .await?; - - self.usage_measure_ids = all_measures - .into_iter() - .filter_map(|measure| { - UsageMeasure::from_str(&measure.name) - .ok() - .map(|um| (um, measure.id)) - }) - .collect(); - Ok(()) - } -} diff --git a/crates/collab/src/llm/db/seed.rs b/crates/collab/src/llm/db/seed.rs deleted file mode 100644 index 55c6c30cd5d8bf3c6755c3f9b9faaa6fc689370e..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/seed.rs +++ /dev/null @@ -1,45 +0,0 @@ -use super::*; -use crate::{Config, Result}; -use queries::providers::ModelParams; - -pub async fn seed_database(_config: &Config, db: &mut LlmDatabase, _force: bool) -> Result<()> { - db.insert_models(&[ - ModelParams { - provider: LanguageModelProvider::Anthropic, - name: "claude-3-5-sonnet".into(), - max_requests_per_minute: 5, - max_tokens_per_minute: 20_000, - max_tokens_per_day: 300_000, - price_per_million_input_tokens: 300, // $3.00/MTok - price_per_million_output_tokens: 1500, // $15.00/MTok - }, - ModelParams { - provider: LanguageModelProvider::Anthropic, - name: "claude-3-opus".into(), - max_requests_per_minute: 5, - max_tokens_per_minute: 10_000, - max_tokens_per_day: 300_000, - price_per_million_input_tokens: 1500, // $15.00/MTok - price_per_million_output_tokens: 7500, // $75.00/MTok - }, - ModelParams { - provider: LanguageModelProvider::Anthropic, - name: "claude-3-sonnet".into(), - max_requests_per_minute: 5, - max_tokens_per_minute: 20_000, - max_tokens_per_day: 300_000, - price_per_million_input_tokens: 1500, // $15.00/MTok - price_per_million_output_tokens: 7500, // $75.00/MTok - }, - ModelParams { - provider: LanguageModelProvider::Anthropic, - name: "claude-3-haiku".into(), - max_requests_per_minute: 5, - max_tokens_per_minute: 25_000, - max_tokens_per_day: 300_000, - price_per_million_input_tokens: 25, // $0.25/MTok - price_per_million_output_tokens: 125, // $1.25/MTok - }, - ]) - .await -} diff --git a/crates/collab/src/llm/db/tables.rs b/crates/collab/src/llm/db/tables.rs deleted file mode 100644 index 75ea8f51409ec28ec546db5a360b935ef04fb7f9..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/tables.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod model; -pub mod provider; -pub mod subscription_usage; -pub mod subscription_usage_meter; -pub mod usage; -pub mod usage_measure; diff --git a/crates/collab/src/llm/db/tables/model.rs b/crates/collab/src/llm/db/tables/model.rs deleted file mode 100644 index f0a858b4a681ce930f9e8d57f5289950a5476ef1..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/tables/model.rs +++ /dev/null @@ -1,48 +0,0 @@ -use sea_orm::entity::prelude::*; - -use crate::llm::db::{ModelId, ProviderId}; - -/// An LLM model. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "models")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ModelId, - pub provider_id: ProviderId, - pub name: String, - pub max_requests_per_minute: i64, - pub max_tokens_per_minute: i64, - pub max_input_tokens_per_minute: i64, - pub max_output_tokens_per_minute: i64, - pub max_tokens_per_day: i64, - pub price_per_million_input_tokens: i32, - pub price_per_million_cache_creation_input_tokens: i32, - pub price_per_million_cache_read_input_tokens: i32, - pub price_per_million_output_tokens: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::provider::Entity", - from = "Column::ProviderId", - to = "super::provider::Column::Id" - )] - Provider, - #[sea_orm(has_many = "super::usage::Entity")] - Usages, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Provider.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Usages.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/llm/db/tables/provider.rs b/crates/collab/src/llm/db/tables/provider.rs deleted file mode 100644 index 90838f7c65511e83cd7192676e0dafefdd05896a..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/tables/provider.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::llm::db::ProviderId; -use sea_orm::entity::prelude::*; - -/// An LLM provider. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "providers")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ProviderId, - pub name: String, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::model::Entity")] - Models, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Models.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/llm/db/tables/subscription_usage.rs b/crates/collab/src/llm/db/tables/subscription_usage.rs deleted file mode 100644 index dd93b03d051ef9752b1c777d24205085fca4487e..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/tables/subscription_usage.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::db::UserId; -use crate::db::billing_subscription::SubscriptionKind; -use sea_orm::entity::prelude::*; -use time::PrimitiveDateTime; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "subscription_usages_v2")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub user_id: UserId, - pub period_start_at: PrimitiveDateTime, - pub period_end_at: PrimitiveDateTime, - pub plan: SubscriptionKind, - pub model_requests: i32, - pub edit_predictions: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/llm/db/tables/subscription_usage_meter.rs b/crates/collab/src/llm/db/tables/subscription_usage_meter.rs deleted file mode 100644 index c082cf3bc132aa4df2c3c7b5422a0c53ec235579..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/tables/subscription_usage_meter.rs +++ /dev/null @@ -1,55 +0,0 @@ -use sea_orm::entity::prelude::*; -use serde::Serialize; - -use crate::llm::db::ModelId; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "subscription_usage_meters_v2")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub subscription_usage_id: Uuid, - pub model_id: ModelId, - pub mode: CompletionMode, - pub requests: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::subscription_usage::Entity", - from = "Column::SubscriptionUsageId", - to = "super::subscription_usage::Column::Id" - )] - SubscriptionUsage, - #[sea_orm( - belongs_to = "super::model::Entity", - from = "Column::ModelId", - to = "super::model::Column::Id" - )] - Model, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::SubscriptionUsage.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Model.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} - -#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] -#[serde(rename_all = "snake_case")] -pub enum CompletionMode { - #[sea_orm(string_value = "normal")] - Normal, - #[sea_orm(string_value = "max")] - Max, -} diff --git a/crates/collab/src/llm/db/tables/usage.rs b/crates/collab/src/llm/db/tables/usage.rs deleted file mode 100644 index 331c94a8a90df2e38601603a746f97ebbf703461..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/tables/usage.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::{ - db::UserId, - llm::db::{ModelId, UsageId, UsageMeasureId}, -}; -use sea_orm::entity::prelude::*; - -/// An LLM usage record. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "usages")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: UsageId, - /// The ID of the Zed user. - /// - /// Corresponds to the `users` table in the primary collab database. - pub user_id: UserId, - pub model_id: ModelId, - pub measure_id: UsageMeasureId, - pub timestamp: DateTime, - pub buckets: Vec, - pub is_staff: bool, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::model::Entity", - from = "Column::ModelId", - to = "super::model::Column::Id" - )] - Model, - #[sea_orm( - belongs_to = "super::usage_measure::Entity", - from = "Column::MeasureId", - to = "super::usage_measure::Column::Id" - )] - UsageMeasure, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Model.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::UsageMeasure.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/llm/db/tables/usage_measure.rs b/crates/collab/src/llm/db/tables/usage_measure.rs deleted file mode 100644 index 4f75577ed4684ff73b98389eaa08aefbabc08a16..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/tables/usage_measure.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::llm::db::UsageMeasureId; -use sea_orm::entity::prelude::*; - -#[derive( - Copy, Clone, Debug, PartialEq, Eq, Hash, strum::EnumString, strum::Display, strum::EnumIter, -)] -#[strum(serialize_all = "snake_case")] -pub enum UsageMeasure { - RequestsPerMinute, - TokensPerMinute, - InputTokensPerMinute, - OutputTokensPerMinute, - TokensPerDay, -} - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "usage_measures")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: UsageMeasureId, - pub name: String, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::usage::Entity")] - Usages, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Usages.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/llm/db/tests.rs b/crates/collab/src/llm/db/tests.rs deleted file mode 100644 index 43a1b8b0d457817d1e94d72d0cad094011424c83..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/tests.rs +++ /dev/null @@ -1,107 +0,0 @@ -mod provider_tests; - -use gpui::BackgroundExecutor; -use parking_lot::Mutex; -use rand::prelude::*; -use sea_orm::ConnectionTrait; -use sqlx::migrate::MigrateDatabase; -use std::time::Duration; - -use crate::migrations::run_database_migrations; - -use super::*; - -pub struct TestLlmDb { - pub db: Option, - pub connection: Option, -} - -impl TestLlmDb { - pub fn postgres(background: BackgroundExecutor) -> Self { - static LOCK: Mutex<()> = Mutex::new(()); - - let _guard = LOCK.lock(); - let mut rng = StdRng::from_entropy(); - let url = format!( - "postgres://postgres@localhost/zed-llm-test-{}", - rng.r#gen::() - ); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build() - .unwrap(); - - let mut db = runtime.block_on(async { - sqlx::Postgres::create_database(&url) - .await - .expect("failed to create test db"); - let mut options = ConnectOptions::new(url); - options - .max_connections(5) - .idle_timeout(Duration::from_secs(0)); - let db = LlmDatabase::new(options, Executor::Deterministic(background)) - .await - .unwrap(); - let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations_llm"); - run_database_migrations(db.options(), migrations_path) - .await - .unwrap(); - db - }); - - db.runtime = Some(runtime); - - Self { - db: Some(db), - connection: None, - } - } - - pub fn db(&mut self) -> &mut LlmDatabase { - self.db.as_mut().unwrap() - } -} - -#[macro_export] -macro_rules! test_llm_db { - ($test_name:ident, $postgres_test_name:ident) => { - #[gpui::test] - async fn $postgres_test_name(cx: &mut gpui::TestAppContext) { - if !cfg!(target_os = "macos") { - return; - } - - let mut test_db = $crate::llm::db::TestLlmDb::postgres(cx.executor().clone()); - $test_name(test_db.db()).await; - } - }; -} - -impl Drop for TestLlmDb { - fn drop(&mut self) { - let db = self.db.take().unwrap(); - if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() { - db.runtime.as_ref().unwrap().block_on(async { - use util::ResultExt; - let query = " - SELECT pg_terminate_backend(pg_stat_activity.pid) - FROM pg_stat_activity - WHERE - pg_stat_activity.datname = current_database() AND - pid <> pg_backend_pid(); - "; - db.pool - .execute(sea_orm::Statement::from_string( - db.pool.get_database_backend(), - query, - )) - .await - .log_err(); - sqlx::Postgres::drop_database(db.options.get_url()) - .await - .log_err(); - }) - } - } -} diff --git a/crates/collab/src/llm/db/tests/provider_tests.rs b/crates/collab/src/llm/db/tests/provider_tests.rs deleted file mode 100644 index f4e1de40ec10705ed9b740619754fcf9ec5f3e1e..0000000000000000000000000000000000000000 --- a/crates/collab/src/llm/db/tests/provider_tests.rs +++ /dev/null @@ -1,31 +0,0 @@ -use cloud_llm_client::LanguageModelProvider; -use pretty_assertions::assert_eq; - -use crate::llm::db::LlmDatabase; -use crate::test_llm_db; - -test_llm_db!( - test_initialize_providers, - test_initialize_providers_postgres -); - -async fn test_initialize_providers(db: &mut LlmDatabase) { - let initial_providers = db.list_providers().await.unwrap(); - assert_eq!(initial_providers, vec![]); - - db.initialize_providers().await.unwrap(); - - // Do it twice, to make sure the operation is idempotent. - db.initialize_providers().await.unwrap(); - - let providers = db.list_providers().await.unwrap(); - - assert_eq!( - providers, - &[ - LanguageModelProvider::Anthropic, - LanguageModelProvider::Google, - LanguageModelProvider::OpenAi, - ] - ) -} diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index 177c97f076c219c0389d09a8c6efbd41566f6ac4..cb6f6cad1dd483c463bcda5d8a4ff914f4bf10aa 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -62,13 +62,6 @@ async fn main() -> Result<()> { db.initialize_notification_kinds().await?; collab::seed::seed(&config, &db, false).await?; - - if let Some(llm_database_url) = config.llm_database_url.clone() { - let db_options = db::ConnectOptions::new(llm_database_url); - let mut db = LlmDatabase::new(db_options.clone(), Executor::Production).await?; - db.initialize().await?; - collab::llm::db::seed_database(&config, &mut db, true).await?; - } } Some("serve") => { let mode = match args.next().as_deref() { @@ -263,9 +256,6 @@ async fn setup_llm_database(config: &Config) -> Result<()> { .llm_database_migrations_path .as_deref() .unwrap_or_else(|| { - #[cfg(feature = "sqlite")] - let default_migrations = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations_llm.sqlite"); - #[cfg(not(feature = "sqlite"))] let default_migrations = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations_llm"); Path::new(default_migrations) diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 8c545b0670ebc8c95d65da8bd7be6a40ad32aeab..07ea1efc9dada31b7ce6aa82dbcdcfd9a31e1c0f 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -565,7 +565,6 @@ impl TestServer { ) -> Arc { Arc::new(AppState { db: test_db.db().clone(), - llm_db: None, livekit_client: Some(Arc::new(livekit_test_server.create_api_client())), blob_store_client: None, executor, From f5f14111ef3203a7e28df531b808f47c2a6a79f0 Mon Sep 17 00:00:00 2001 From: zumbalogy Date: Sat, 16 Aug 2025 08:19:38 +0200 Subject: [PATCH 057/744] Add setting for hiding the status_bar.cursor_position_button (#36288) Release Notes: - Added an option for the status_bar.cursor_position_button. Setting to `false` will hide the button. It defaults to `true`. This builds off the recent work to hide the language selection button (https://github.com/zed-industries/zed/pull/33977). I tried to follow that pattern, and to pick a clear name for the option, but any feedback/change is welcome. --------- Co-authored-by: zumbalogy <3770982+zumbalogy@users.noreply.github.com> --- assets/settings/default.json | 4 +++- crates/editor/src/editor_settings.rs | 8 ++++++++ crates/go_to_line/src/cursor_position.rs | 9 ++++++++- docs/src/configuring-zed.md | 1 + docs/src/visual-customization.md | 4 ++++ 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 2c3bf6930d04a2668d65c20a191de17033de4aac..1b485a8b284ce7b0064f43a7117ecc49f9924019 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1256,7 +1256,9 @@ // Status bar-related settings. "status_bar": { // Whether to show the active language button in the status bar. - "active_language_button": true + "active_language_button": true, + // Whether to show the cursor position button in the status bar. + "cursor_position_button": true }, // Settings specific to the terminal "terminal": { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 3d132651b846c3654b022b4a3e0aa6f60fa25d04..d3a21c7642e2d5eb11a75e06c5466210dd68f63c 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -132,6 +132,10 @@ pub struct StatusBar { /// /// Default: true pub active_language_button: bool, + /// Whether to show the cursor position button in the status bar. + /// + /// Default: true + pub cursor_position_button: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -585,6 +589,10 @@ pub struct StatusBarContent { /// /// Default: true pub active_language_button: Option, + /// Whether to show the cursor position button in the status bar. + /// + /// Default: true + pub cursor_position_button: Option, } // Toolbar related settings diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 29064eb29cb986187b9d86046fd3d78cd2f63451..af92621378bdd1635af147d845ab809fe3326828 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -1,4 +1,4 @@ -use editor::{Editor, MultiBufferSnapshot}; +use editor::{Editor, EditorSettings, MultiBufferSnapshot}; use gpui::{App, Entity, FocusHandle, Focusable, Subscription, Task, WeakEntity}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -209,6 +209,13 @@ impl CursorPosition { impl Render for CursorPosition { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + if !EditorSettings::get_global(cx) + .status_bar + .cursor_position_button + { + return div(); + } + div().when_some(self.position, |el, position| { let mut text = format!( "{}{FILE_ROW_COLUMN_DELIMITER}{}", diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index b4cb1fcb9b99e3a23c31922cf50f7155992868e5..9d561302566d855e093cfb1993ab466d0c3dfe72 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1284,6 +1284,7 @@ Each option controls displaying of a particular toolbar element. If all elements ```json "status_bar": { "active_language_button": true, + "cursor_position_button": true }, ``` diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 7e75f6287d9c9aec741074cc5936641c178efd74..6e598f44361556d9e2325086c992f11c3d202a28 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -316,6 +316,10 @@ TBD: Centered layout related settings // Clicking the button brings up the language selector. // Defaults to true. "active_language_button": true, + // Show/hide a button that displays the cursor's position. + // Clicking the button brings up an input for jumping to a line and column. + // Defaults to true. + "cursor_position_button": true, }, ``` From 7784fac288b89b5ffc5edbe634ecbc907325faa6 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Sat, 16 Aug 2025 01:33:32 -0500 Subject: [PATCH 058/744] Separate minidump crashes from panics (#36267) The minidump-based crash reporting is now entirely separate from our legacy panic_hook-based reporting. This should improve the association of minidumps with their metadata and give us more consistent crash reports. Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld --- Cargo.lock | 2 + crates/crashes/Cargo.toml | 2 + crates/crashes/src/crashes.rs | 157 +++++++++++++----- crates/proto/proto/app.proto | 6 +- crates/remote/src/ssh_session.rs | 30 ++-- crates/remote_server/src/unix.rs | 93 +++++------ crates/zed/src/main.rs | 11 +- crates/zed/src/reliability.rs | 264 ++++++++++++++----------------- 8 files changed, 316 insertions(+), 249 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d72eed42e47646c1a65601ca6fb07ee5e64ee80..1bce72b3a138a79e5344a080b83db8abc4ef202a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4038,6 +4038,8 @@ dependencies = [ "minidumper", "paths", "release_channel", + "serde", + "serde_json", "smol", "workspace-hack", ] diff --git a/crates/crashes/Cargo.toml b/crates/crashes/Cargo.toml index afb4936b6370791b133395b6205fb5cffaa17284..2420b499f8fecb3d66f2cabbce57bbd39fd19a7c 100644 --- a/crates/crashes/Cargo.toml +++ b/crates/crashes/Cargo.toml @@ -12,6 +12,8 @@ minidumper.workspace = true paths.workspace = true release_channel.workspace = true smol.workspace = true +serde.workspace = true +serde_json.workspace = true workspace-hack.workspace = true [lints] diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index 5b9ae0b54606c7ee9bf3034a097d230e7570f572..ddf6468be817638d40cea3bfdd2a00e8a83e998f 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -2,15 +2,17 @@ use crash_handler::CrashHandler; use log::info; use minidumper::{Client, LoopAction, MinidumpBinary}; use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; +use serde::{Deserialize, Serialize}; use std::{ env, - fs::File, + fs::{self, File}, io, + panic::Location, path::{Path, PathBuf}, process::{self, Command}, sync::{ - LazyLock, OnceLock, + Arc, OnceLock, atomic::{AtomicBool, Ordering}, }, thread, @@ -18,19 +20,17 @@ use std::{ }; // set once the crash handler has initialized and the client has connected to it -pub static CRASH_HANDLER: AtomicBool = AtomicBool::new(false); +pub static CRASH_HANDLER: OnceLock> = OnceLock::new(); // 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); +const CRASH_HANDLER_PING_TIMEOUT: Duration = Duration::from_secs(60); +const CRASH_HANDLER_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); -pub static GENERATE_MINIDUMPS: LazyLock = LazyLock::new(|| { - *RELEASE_CHANNEL != ReleaseChannel::Dev || env::var("ZED_GENERATE_MINIDUMPS").is_ok() -}); - -pub async fn init(id: String) { - if !*GENERATE_MINIDUMPS { +pub async fn init(crash_init: InitCrashHandler) { + if *RELEASE_CHANNEL == ReleaseChannel::Dev && env::var("ZED_GENERATE_MINIDUMPS").is_err() { return; } + 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, @@ -61,9 +61,11 @@ pub async fn init(id: String) { smol::Timer::after(retry_frequency).await; } let client = maybe_client.unwrap(); - client.send_message(1, id).unwrap(); // set session id on the server + client + .send_message(1, serde_json::to_vec(&crash_init).unwrap()) + .unwrap(); - let client = std::sync::Arc::new(client); + let client = 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| { @@ -72,7 +74,6 @@ pub async fn init(id: String) { .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 { @@ -87,7 +88,7 @@ pub async fn init(id: String) { { handler.set_ptracer(Some(server_pid)); } - CRASH_HANDLER.store(true, Ordering::Release); + CRASH_HANDLER.set(client.clone()).ok(); std::mem::forget(handler); info!("crash handler registered"); @@ -98,14 +99,43 @@ pub async fn init(id: String) { } pub struct CrashServer { - session_id: OnceLock, + initialization_params: OnceLock, + panic_info: OnceLock, + has_connection: Arc, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct CrashInfo { + pub init: InitCrashHandler, + pub panic: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct InitCrashHandler { + pub session_id: String, + pub zed_version: String, + pub release_channel: String, + pub commit_sha: String, + // pub gpu: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct CrashPanic { + pub message: String, + pub span: String, } 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 err_message = "Missing initialization data"; let dump_path = paths::logs_dir() - .join(self.session_id.get().expect(err_message)) + .join( + &self + .initialization_params + .get() + .expect(err_message) + .session_id, + ) .with_extension("dmp"); let file = File::create(&dump_path)?; Ok((file, dump_path)) @@ -122,38 +152,71 @@ impl minidumper::ServerHandler for CrashServer { info!("failed to write minidump: {:#}", e); } } + + let crash_info = CrashInfo { + init: self + .initialization_params + .get() + .expect("not initialized") + .clone(), + panic: self.panic_info.get().cloned(), + }; + + let crash_data_path = paths::logs_dir() + .join(&crash_info.init.session_id) + .with_extension("json"); + + fs::write(crash_data_path, serde_json::to_vec(&crash_info).unwrap()).ok(); + 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"); + match kind { + 1 => { + let init_data = + serde_json::from_slice::(&buffer).expect("invalid init data"); + self.initialization_params + .set(init_data) + .expect("already initialized"); + } + 2 => { + let panic_data = + serde_json::from_slice::(&buffer).expect("invalid panic data"); + self.panic_info.set(panic_data).expect("already panicked"); + } + _ => { + panic!("invalid message kind"); + } } } - fn on_client_disconnected(&self, clients: usize) -> LoopAction { - info!("client disconnected, {clients} remaining"); - if clients == 0 { - LoopAction::Exit - } else { - LoopAction::Continue - } + fn on_client_disconnected(&self, _clients: usize) -> LoopAction { + LoopAction::Exit } -} -pub fn handle_panic() { - if !*GENERATE_MINIDUMPS { - return; + fn on_client_connected(&self, _clients: usize) -> LoopAction { + self.has_connection.store(true, Ordering::SeqCst); + LoopAction::Continue } +} + +pub fn handle_panic(message: String, span: Option<&Location>) { + let span = span + .map(|loc| format!("{}:{}", loc.file(), loc.line())) + .unwrap_or_default(); + // 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) { + if let Some(client) = CRASH_HANDLER.get() { + client + .send_message( + 2, + serde_json::to_vec(&CrashPanic { message, span }).unwrap(), + ) + .ok(); log::error!("triggering a crash to generate a minidump..."); #[cfg(target_os = "linux")] CrashHandler.simulate_signal(crash_handler::Signal::Trap as u32); @@ -170,14 +233,30 @@ pub fn crash_server(socket: &Path) { log::info!("Couldn't create socket, there may already be a running crash server"); return; }; - let ab = AtomicBool::new(false); + + let shutdown = Arc::new(AtomicBool::new(false)); + let has_connection = Arc::new(AtomicBool::new(false)); + + std::thread::spawn({ + let shutdown = shutdown.clone(); + let has_connection = has_connection.clone(); + move || { + std::thread::sleep(CRASH_HANDLER_CONNECT_TIMEOUT); + if !has_connection.load(Ordering::SeqCst) { + shutdown.store(true, Ordering::SeqCst); + } + } + }); + server .run( Box::new(CrashServer { - session_id: OnceLock::new(), + initialization_params: OnceLock::new(), + panic_info: OnceLock::new(), + has_connection, }), - &ab, - Some(CRASH_HANDLER_TIMEOUT), + &shutdown, + Some(CRASH_HANDLER_PING_TIMEOUT), ) .expect("failed to run server"); } diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index 1f2ab1f539fb7d31ecd75a9f802e108433a89417..66f8da44f2220266834e0535d3a4e2291c079f1c 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -28,11 +28,13 @@ message GetCrashFiles { message GetCrashFilesResponse { repeated CrashReport crashes = 1; + repeated string legacy_panics = 2; } message CrashReport { - optional string panic_contents = 1; - optional bytes minidump_contents = 2; + reserved 1, 2; + string metadata = 3; + bytes minidump_contents = 4; } message Extension { diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 2f462a86a5a1578993ad6e2893c196ed7daf8c3f..ea383ac26403a2dd5ca1bffb579044e7ffa1a530 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -1490,20 +1490,17 @@ impl RemoteConnection for SshRemoteConnection { identifier = &unique_identifier, ); - if let Some(rust_log) = std::env::var("RUST_LOG").ok() { - start_proxy_command = format!( - "RUST_LOG={} {}", - shlex::try_quote(&rust_log).unwrap(), - start_proxy_command - ) - } - if let Some(rust_backtrace) = std::env::var("RUST_BACKTRACE").ok() { - start_proxy_command = format!( - "RUST_BACKTRACE={} {}", - shlex::try_quote(&rust_backtrace).unwrap(), - start_proxy_command - ) + for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { + if let Some(value) = std::env::var(env_var).ok() { + start_proxy_command = format!( + "{}={} {} ", + env_var, + shlex::try_quote(&value).unwrap(), + start_proxy_command, + ); + } } + if reconnect { start_proxy_command.push_str(" --reconnect"); } @@ -2241,8 +2238,7 @@ impl SshRemoteConnection { #[cfg(not(target_os = "windows"))] { - run_cmd(Command::new("gzip").args(["-9", "-f", &bin_path.to_string_lossy()])) - .await?; + run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?; } #[cfg(target_os = "windows")] { @@ -2474,7 +2470,7 @@ impl ChannelClient { }, async { smol::Timer::after(timeout).await; - anyhow::bail!("Timeout detected") + anyhow::bail!("Timed out resyncing remote client") }, ) .await @@ -2488,7 +2484,7 @@ impl ChannelClient { }, async { smol::Timer::after(timeout).await; - anyhow::bail!("Timeout detected") + anyhow::bail!("Timed out pinging remote client") }, ) .await diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 9bb5645dc7b80ee25cbb2f75f66b4aba73cb7784..dc7fab8c3cda7416de2788cb421984a11203f769 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -34,10 +34,10 @@ 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; +use std::sync::LazyLock; use std::{env, thread}; use std::{ io::Write, @@ -48,6 +48,13 @@ use std::{ use telemetry_events::LocationData; use util::ResultExt; +pub static VERSION: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL { + ReleaseChannel::Stable | ReleaseChannel::Preview => env!("ZED_PKG_VERSION"), + ReleaseChannel::Nightly | ReleaseChannel::Dev => { + option_env!("ZED_COMMIT_SHA").unwrap_or("missing-zed-commit-sha") + } +}); + fn init_logging_proxy() { env_logger::builder() .format(|buf, record| { @@ -113,7 +120,6 @@ fn init_logging_server(log_file_path: PathBuf) -> Result>> { 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>() @@ -121,6 +127,8 @@ fn init_panic_hook(session_id: String) { .or_else(|| info.payload().downcast_ref::().cloned()) .unwrap_or_else(|| "Box".to_string()); + crashes::handle_panic(payload.clone(), info.location()); + let backtrace = backtrace::Backtrace::new(); let mut backtrace = backtrace .frames() @@ -150,14 +158,6 @@ fn init_panic_hook(session_id: String) { (&backtrace).join("\n") ); - let release_channel = *RELEASE_CHANNEL; - let version = match release_channel { - ReleaseChannel::Stable | ReleaseChannel::Preview => env!("ZED_PKG_VERSION"), - ReleaseChannel::Nightly | ReleaseChannel::Dev => { - option_env!("ZED_COMMIT_SHA").unwrap_or("missing-zed-commit-sha") - } - }; - let panic_data = telemetry_events::Panic { thread: thread_name.into(), payload: payload.clone(), @@ -165,9 +165,9 @@ fn init_panic_hook(session_id: String) { file: location.file().into(), line: location.line(), }), - app_version: format!("remote-server-{version}"), + app_version: format!("remote-server-{}", *VERSION), app_commit_sha: option_env!("ZED_COMMIT_SHA").map(|sha| sha.into()), - release_channel: release_channel.dev_name().into(), + release_channel: RELEASE_CHANNEL.dev_name().into(), target: env!("TARGET").to_owned().into(), os_name: telemetry::os_name(), os_version: Some(telemetry::os_version()), @@ -204,8 +204,8 @@ fn handle_crash_files_requests(project: &Entity, client: &Arc, _cx| async move { + let mut legacy_panics = Vec::new(); let mut crashes = Vec::new(); - let mut minidumps_by_session_id = HashMap::new(); let mut children = smol::fs::read_dir(paths::logs_dir()).await?; while let Some(child) = children.next().await { let child = child?; @@ -227,41 +227,31 @@ fn handle_crash_files_requests(project: &Entity, client: &Arc Result<()> { let server_paths = ServerPaths::new(&identifier)?; let id = std::process::id().to_string(); - smol::spawn(crashes::init(id.clone())).detach(); + smol::spawn(crashes::init(crashes::InitCrashHandler { + session_id: id.clone(), + zed_version: VERSION.to_owned(), + release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(), + commit_sha: option_env!("ZED_COMMIT_SHA").unwrap_or("no_sha").to_owned(), + })) + .detach(); init_panic_hook(id); log::info!("starting proxy process. PID: {}", std::process::id()); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index fd987ef6c53797181fe6aca3404000c314cc62b7..2a82f81b5b314b83bbe552747d385049dc61585d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -8,6 +8,7 @@ use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::{Client, ProxySettings, UserStore, parse_zed_link}; use collab_ui::channel_view::ChannelView; use collections::HashMap; +use crashes::InitCrashHandler; use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE}; use editor::Editor; use extension::ExtensionHostProxy; @@ -269,7 +270,15 @@ pub fn main() { let session = app.background_executor().block(Session::new()); app.background_executor() - .spawn(crashes::init(session_id.clone())) + .spawn(crashes::init(InitCrashHandler { + session_id: session_id.clone(), + zed_version: app_version.to_string(), + release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(), + commit_sha: app_commit_sha + .as_ref() + .map(|sha| sha.full()) + .unwrap_or_else(|| "no sha".to_owned()), + })) .detach(); reliability::init_panic_hook( app_version, diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index fde44344b14b436e284391642dde4616f54f02e0..c27f4cb0a86ff3f9e6f68f22e99512b801950f90 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -12,6 +12,7 @@ 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 proto::{CrashReport, GetCrashFilesResponse}; use release_channel::{AppCommitSha, RELEASE_CHANNEL, ReleaseChannel}; use reqwest::multipart::{Form, Part}; use settings::Settings; @@ -51,10 +52,6 @@ pub fn init_panic_hook( thread::yield_now(); } } - crashes::handle_panic(); - - let thread = thread::current(); - let thread_name = thread.name().unwrap_or(""); let payload = info .payload() @@ -63,6 +60,11 @@ pub fn init_panic_hook( .or_else(|| info.payload().downcast_ref::().cloned()) .unwrap_or_else(|| "Box".to_string()); + crashes::handle_panic(payload.clone(), info.location()); + + let thread = thread::current(); + let thread_name = thread.name().unwrap_or(""); + if *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev { let location = info.location().unwrap(); let backtrace = Backtrace::new(); @@ -214,45 +216,53 @@ pub fn init( let installation_id = installation_id.clone(); let system_id = system_id.clone(); - 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::GetCrashFiles {}); - cx.background_spawn(async move { - 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(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?; - } - } + let Some(ssh_client) = project.ssh_client() else { + return; + }; + ssh_client.update(cx, |client, cx| { + if !TelemetrySettings::get_global(cx).diagnostics { + return; + } + let request = client.proto_client().request(proto::GetCrashFiles {}); + cx.background_spawn(async move { + let GetCrashFilesResponse { + legacy_panics, + crashes, + } = request.await?; + + for panic in legacy_panics { + if let Some(mut panic) = serde_json::from_str::(&panic).log_err() { + panic.session_id = session_id.clone(); + panic.system_id = system_id.clone(); + panic.installation_id = installation_id.clone(); + upload_panic(&http_client, &panic_report_url, panic, &mut None).await?; + } + } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + let Some(endpoint) = MINIDUMP_ENDPOINT.as_ref() else { + return Ok(()); + }; + for CrashReport { + metadata, + minidump_contents, + } in crashes + { + if let Some(metadata) = serde_json::from_str(&metadata).log_err() { + upload_minidump( + http_client.clone(), + endpoint, + minidump_contents, + &metadata, + ) + .await + .log_err(); + } } + + anyhow::Ok(()) }) - } + .detach_and_log_err(cx); + }) }) .detach(); } @@ -466,16 +476,18 @@ fn upload_panics_and_crashes( installation_id: Option, cx: &App, ) { - let telemetry_settings = *client::TelemetrySettings::get_global(cx); + if !client::TelemetrySettings::get_global(cx).diagnostics { + return; + } cx.background_spawn(async move { - let most_recent_panic = - upload_previous_panics(http.clone(), &panic_report_url, telemetry_settings) - .await - .log_err() - .flatten(); - upload_previous_crashes(http, most_recent_panic, installation_id, telemetry_settings) + upload_previous_minidumps(http.clone()).await.warn_on_err(); + let most_recent_panic = upload_previous_panics(http.clone(), &panic_report_url) .await .log_err() + .flatten(); + upload_previous_crashes(http, most_recent_panic, installation_id) + .await + .log_err(); }) .detach() } @@ -484,7 +496,6 @@ fn upload_panics_and_crashes( async fn upload_previous_panics( http: Arc, panic_report_url: &Url, - telemetry_settings: client::TelemetrySettings, ) -> anyhow::Result> { let mut children = smol::fs::read_dir(paths::logs_dir()).await?; @@ -507,58 +518,41 @@ async fn upload_previous_panics( continue; } - if telemetry_settings.diagnostics { - let panic_file_content = smol::fs::read_to_string(&child_path) - .await - .context("error reading panic file")?; - - let panic: Option = serde_json::from_str(&panic_file_content) - .log_err() - .or_else(|| { - panic_file_content - .lines() - .next() - .and_then(|line| serde_json::from_str(line).ok()) - }) - .unwrap_or_else(|| { - log::error!("failed to deserialize panic file {:?}", panic_file_content); - None - }); - - 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(); - } - } + let panic_file_content = smol::fs::read_to_string(&child_path) + .await + .context("error reading panic file")?; - if !upload_panic(&http, &panic_report_url, panic, &mut most_recent_panic).await? { - continue; - } - } - } + let panic: Option = serde_json::from_str(&panic_file_content) + .log_err() + .or_else(|| { + panic_file_content + .lines() + .next() + .and_then(|line| serde_json::from_str(line).ok()) + }) + .unwrap_or_else(|| { + log::error!("failed to deserialize panic file {:?}", panic_file_content); + None + }); - // We've done what we can, delete the file - fs::remove_file(child_path) - .context("error removing panic") - .log_err(); + if let Some(panic) = panic + && upload_panic(&http, &panic_report_url, panic, &mut most_recent_panic).await? + { + // We've done what we can, delete the file + fs::remove_file(child_path) + .context("error removing panic") + .log_err(); + } } - if MINIDUMP_ENDPOINT.is_none() { - return Ok(most_recent_panic); - } + Ok(most_recent_panic) +} + +pub async fn upload_previous_minidumps(http: Arc) -> anyhow::Result<()> { + let Some(minidump_endpoint) = MINIDUMP_ENDPOINT.as_ref() else { + return Err(anyhow::anyhow!("Minidump endpoint not set")); + }; - // 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?; @@ -566,33 +560,35 @@ async fn upload_previous_panics( 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(); + let mut json_path = child_path.clone(); + json_path.set_extension("json"); + if let Ok(metadata) = serde_json::from_slice(&smol::fs::read(&json_path).await?) { + if upload_minidump( + http.clone(), + &minidump_endpoint, + smol::fs::read(&child_path) + .await + .context("Failed to read minidump")?, + &metadata, + ) + .await + .log_err() + .is_some() + { + fs::remove_file(child_path).ok(); + fs::remove_file(json_path).ok(); + } } } - - Ok(most_recent_panic) + Ok(()) } async fn upload_minidump( http: Arc, + endpoint: &str, minidump: Vec, - panic: Option<&Panic>, + metadata: &crashes::CrashInfo, ) -> Result<()> { - let minidump_endpoint = MINIDUMP_ENDPOINT - .to_owned() - .ok_or_else(|| anyhow::anyhow!("Minidump endpoint not set"))?; - let mut form = Form::new() .part( "upload_file_minidump", @@ -600,38 +596,22 @@ async fn upload_minidump( .file_name("minidump.dmp") .mime_str("application/octet-stream")?, ) + .text( + "sentry[tags][channel]", + metadata.init.release_channel.clone(), + ) + .text("sentry[tags][version]", metadata.init.zed_version.clone()) + .text("sentry[release]", metadata.init.commit_sha.clone()) .text("platform", "rust"); - if let Some(panic) = panic { - form = form - .text("sentry[tags][channel]", panic.release_channel.clone()) - .text("sentry[tags][version]", panic.app_version.clone()) - .text("sentry[context][os][name]", panic.os_name.clone()) - .text( - "sentry[context][device][architecture]", - panic.architecture.clone(), - ) - .text("sentry[logentry][formatted]", panic.payload.clone()); - - if let Some(sha) = panic.app_commit_sha.clone() { - form = form.text("sentry[release]", sha) - } else { - form = form.text( - "sentry[release]", - format!("{}-{}", panic.release_channel, panic.app_version), - ) - } - if let Some(v) = panic.os_version.clone() { - form = form.text("sentry[context][os][release]", v); - } - if let Some(location) = panic.location_data.as_ref() { - form = form.text("span", format!("{}:{}", location.file, location.line)) - } + if let Some(panic_info) = metadata.panic.as_ref() { + form = form.text("sentry[logentry][formatted]", panic_info.message.clone()); + form = form.text("span", panic_info.span.clone()); // TODO: add gpu-context, feature-flag-context, and more of device-context like gpu // name, screen resolution, available ram, device model, etc } let mut response_text = String::new(); - let mut response = http.send_multipart_form(&minidump_endpoint, form).await?; + let mut response = http.send_multipart_form(endpoint, form).await?; response .body_mut() .read_to_string(&mut response_text) @@ -681,11 +661,7 @@ async fn upload_previous_crashes( http: Arc, most_recent_panic: Option<(i64, String)>, installation_id: Option, - telemetry_settings: client::TelemetrySettings, ) -> Result<()> { - if !telemetry_settings.diagnostics { - return Ok(()); - } let last_uploaded = KEY_VALUE_STORE .read_kvp(LAST_CRASH_UPLOADED)? .unwrap_or("zed-2024-01-17-221900.ips".to_string()); // don't upload old crash reports from before we had this. From 864d4bc1d133e5beb24c64bc0bf7336fc274ed1c Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sat, 16 Aug 2025 09:55:46 +0200 Subject: [PATCH 059/744] editor: Drop multiline targets in navigation buffers (#36291) Release Notes: - N/A --- crates/editor/src/editor.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 85f2e01ed45697264ac7a677ef9d512c40a0dc6a..0111e913471649beabd972db3033816aa41fd858 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -15909,10 +15909,15 @@ impl Editor { .text_for_range(location.range.clone()) .collect::() }) + .filter(|text| !text.contains('\n')) .unique() .take(3) .join(", "); - format!("{tab_kind} for {target}") + if target.is_empty() { + tab_kind.to_owned() + } else { + format!("{tab_kind} for {target}") + } }) .context("buffer title")?; @@ -16117,10 +16122,15 @@ impl Editor { .text_for_range(location.range.clone()) .collect::() }) + .filter(|text| !text.contains('\n')) .unique() .take(3) .join(", "); - let title = format!("References to {target}"); + let title = if target.is_empty() { + "References".to_owned() + } else { + format!("References to {target}") + }; Self::open_locations_in_multibuffer( workspace, locations, From 6f2e7c355ec4d2b68285047258af7e5d72596b33 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Sat, 16 Aug 2025 13:36:17 +0200 Subject: [PATCH 060/744] Ensure bundled files are opened as read-only (#36299) Closes #36297 While we set the editor as read-only for bundled files, we didn't do this for the underlying buffer. This PR fixes this and adds a test for the corresponding case. Release Notes: - Fixed an issue where bundled files (e.g. the default settings) could be edited in some circumstances --- crates/zed/src/zed.rs | 44 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index b06652b2cebc0d7832f958fb4766e08564db172c..a324ba0932c02d490513b8228ffae9e2026f75d0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -31,6 +31,7 @@ use gpui::{ px, retain_all, }; use image_viewer::ImageInfo; +use language::Capability; use language_tools::lsp_tool::{self, LspTool}; use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType}; use migrator::{migrate_keymap, migrate_settings}; @@ -1764,7 +1765,11 @@ fn open_bundled_file( workspace.with_local_workspace(window, cx, |workspace, window, cx| { let project = workspace.project(); let buffer = project.update(cx, move |project, cx| { - project.create_local_buffer(text.as_ref(), language, cx) + let buffer = project.create_local_buffer(text.as_ref(), language, cx); + buffer.update(cx, |buffer, cx| { + buffer.set_capability(Capability::ReadOnly, cx); + }); + buffer }); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into())); @@ -4543,6 +4548,43 @@ mod tests { assert!(has_default_theme); } + #[gpui::test] + async fn test_bundled_files_editor(cx: &mut TestAppContext) { + let app_state = init_test(cx); + cx.update(init); + + let project = Project::test(app_state.fs.clone(), [], cx).await; + let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); + + cx.update(|cx| { + cx.dispatch_action(&OpenDefaultSettings); + }); + cx.run_until_parked(); + + assert_eq!(cx.read(|cx| cx.windows().len()), 1); + + let workspace = cx.windows()[0].downcast::().unwrap(); + let active_editor = workspace + .update(cx, |workspace, _, cx| { + workspace.active_item_as::(cx) + }) + .unwrap(); + assert!( + active_editor.is_some(), + "Settings action should have opened an editor with the default file contents" + ); + + let active_editor = active_editor.unwrap(); + assert!( + active_editor.read_with(cx, |editor, cx| editor.read_only(cx)), + "Default settings should be readonly" + ); + assert!( + active_editor.read_with(cx, |editor, cx| editor.buffer().read(cx).read_only()), + "The underlying buffer should also be readonly for the shipped default settings" + ); + } + #[gpui::test] async fn test_bundled_languages(cx: &mut TestAppContext) { env_logger::builder().is_test(true).try_init().ok(); From 5620e359af2c96aa420ade68d017c802012dd005 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 16 Aug 2025 09:09:14 -0400 Subject: [PATCH 061/744] collab: Make `admin` column non-nullable on `users` table (#36307) This PR updates the `admin` column on the `users` table to be non-nullable. We were already treating it like this in practice. All rows in the production database already have a value for the `admin` column. Release Notes: - N/A --- .../migrations/20250816124707_make_admin_required_on_users.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 crates/collab/migrations/20250816124707_make_admin_required_on_users.sql diff --git a/crates/collab/migrations/20250816124707_make_admin_required_on_users.sql b/crates/collab/migrations/20250816124707_make_admin_required_on_users.sql new file mode 100644 index 0000000000000000000000000000000000000000..e372723d6d5f5e822a2e437cfac4b95bc2023998 --- /dev/null +++ b/crates/collab/migrations/20250816124707_make_admin_required_on_users.sql @@ -0,0 +1,2 @@ +alter table users +alter column admin set not null; From d1958aa43913889390c171e46d6e59259f7be2c0 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 16 Aug 2025 09:48:38 -0400 Subject: [PATCH 062/744] collab: Add `orb_customer_id` to `billing_customers` (#36310) This PR adds an `orb_customer_id` column to the `billing_customers` table. Release Notes: - N/A --- .../20250816133027_add_orb_customer_id_to_billing_customers.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 crates/collab/migrations/20250816133027_add_orb_customer_id_to_billing_customers.sql diff --git a/crates/collab/migrations/20250816133027_add_orb_customer_id_to_billing_customers.sql b/crates/collab/migrations/20250816133027_add_orb_customer_id_to_billing_customers.sql new file mode 100644 index 0000000000000000000000000000000000000000..ea5e4de52a829413030bb5e206f5c7401381adcf --- /dev/null +++ b/crates/collab/migrations/20250816133027_add_orb_customer_id_to_billing_customers.sql @@ -0,0 +1,2 @@ +alter table billing_customers + add column orb_customer_id text; From ea7bc96c051371f93d7247492a91975608e4e1f7 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 16 Aug 2025 09:52:14 -0400 Subject: [PATCH 063/744] collab: Remove billing-related tables from SQLite schema (#36312) This PR removes the billing-related tables from the SQLite schema, as we don't actually reference these tables anywhere in the Collab codebase anymore. Release Notes: - N/A --- .../20221109000000_test_schema.sql | 50 ------------------- 1 file changed, 50 deletions(-) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 73d473ab767e633ae2cefc309d87074523811851..63f999b3a7ca41e0b4c843791676aef74d3c5c67 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -485,56 +485,6 @@ CREATE TABLE rate_buckets ( CREATE INDEX idx_user_id_rate_limit ON rate_buckets (user_id, rate_limit_name); -CREATE TABLE IF NOT EXISTS billing_preferences ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - user_id INTEGER NOT NULL REFERENCES users (id), - max_monthly_llm_usage_spending_in_cents INTEGER NOT NULL, - model_request_overages_enabled bool NOT NULL DEFAULT FALSE, - model_request_overages_spend_limit_in_cents integer NOT NULL DEFAULT 0 -); - -CREATE UNIQUE INDEX "uix_billing_preferences_on_user_id" ON billing_preferences (user_id); - -CREATE TABLE IF NOT EXISTS billing_customers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - user_id INTEGER NOT NULL REFERENCES users (id), - has_overdue_invoices BOOLEAN NOT NULL DEFAULT FALSE, - stripe_customer_id TEXT NOT NULL, - trial_started_at TIMESTAMP -); - -CREATE UNIQUE INDEX "uix_billing_customers_on_user_id" ON billing_customers (user_id); - -CREATE UNIQUE INDEX "uix_billing_customers_on_stripe_customer_id" ON billing_customers (stripe_customer_id); - -CREATE TABLE IF NOT EXISTS billing_subscriptions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - billing_customer_id INTEGER NOT NULL REFERENCES billing_customers (id), - stripe_subscription_id TEXT NOT NULL, - stripe_subscription_status TEXT NOT NULL, - stripe_cancel_at TIMESTAMP, - stripe_cancellation_reason TEXT, - kind TEXT, - stripe_current_period_start BIGINT, - stripe_current_period_end BIGINT -); - -CREATE INDEX "ix_billing_subscriptions_on_billing_customer_id" ON billing_subscriptions (billing_customer_id); - -CREATE UNIQUE INDEX "uix_billing_subscriptions_on_stripe_subscription_id" ON billing_subscriptions (stripe_subscription_id); - -CREATE TABLE IF NOT EXISTS processed_stripe_events ( - stripe_event_id TEXT PRIMARY KEY, - stripe_event_type TEXT NOT NULL, - stripe_event_created_timestamp INTEGER NOT NULL, - processed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX "ix_processed_stripe_events_on_stripe_event_created_timestamp" ON processed_stripe_events (stripe_event_created_timestamp); - CREATE TABLE IF NOT EXISTS "breakpoints" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, From 36184a71df8766fec6ceebd3c54c42f871abec84 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 16 Aug 2025 10:11:36 -0400 Subject: [PATCH 064/744] collab: Drop `rate_buckets` table (#36315) This PR drops the `rate_buckets` table, as we're no longer using it. Release Notes: - N/A --- .../migrations.sqlite/20221109000000_test_schema.sql | 11 ----------- .../20250816135346_drop_rate_buckets_table.sql | 1 + 2 files changed, 1 insertion(+), 11 deletions(-) create mode 100644 crates/collab/migrations/20250816135346_drop_rate_buckets_table.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 63f999b3a7ca41e0b4c843791676aef74d3c5c67..170ac7b0a2201996f069d526cd041a5509f3efc5 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -474,17 +474,6 @@ CREATE UNIQUE INDEX "index_extensions_external_id" ON "extensions" ("external_id CREATE INDEX "index_extensions_total_download_count" ON "extensions" ("total_download_count"); -CREATE TABLE rate_buckets ( - user_id INT NOT NULL, - rate_limit_name VARCHAR(255) NOT NULL, - token_count INT NOT NULL, - last_refill TIMESTAMP WITHOUT TIME ZONE NOT NULL, - PRIMARY KEY (user_id, rate_limit_name), - FOREIGN KEY (user_id) REFERENCES users (id) -); - -CREATE INDEX idx_user_id_rate_limit ON rate_buckets (user_id, rate_limit_name); - CREATE TABLE IF NOT EXISTS "breakpoints" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, diff --git a/crates/collab/migrations/20250816135346_drop_rate_buckets_table.sql b/crates/collab/migrations/20250816135346_drop_rate_buckets_table.sql new file mode 100644 index 0000000000000000000000000000000000000000..f51a33ed30d7fb88bc9dc6c82e7217c7e4634b28 --- /dev/null +++ b/crates/collab/migrations/20250816135346_drop_rate_buckets_table.sql @@ -0,0 +1 @@ +drop table rate_buckets; From 7b3fe0a474f5ead24fb9da976dfde745cc6ba936 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Sat, 16 Aug 2025 16:35:06 +0200 Subject: [PATCH 065/744] Make agent font size inherit the UI font size by default (#36306) Ensures issues like #36242 and #36295 do not arise where users are confused that the agent panel does not follow the default UI font size whilst also keeping the possibility of customization. The agent font size was matching the UI font size previously alredy, which makes it easier to change it for most scenarios. Also cleans up some related logic around modifying the font sizes. Release Notes: - The agent panel font size will now inherit the UI font size by default if not set in your settings. --- assets/settings/default.json | 4 +- crates/agent_ui/src/agent_panel.rs | 6 +-- crates/theme/src/settings.rs | 75 ++++++++++++++++-------------- crates/theme/src/theme.rs | 8 ++++ crates/zed/src/zed.rs | 16 ++----- 5 files changed, 55 insertions(+), 54 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 1b485a8b284ce7b0064f43a7117ecc49f9924019..ff000001b567a58bca11bdbbfcd89494470ac91f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -71,8 +71,8 @@ "ui_font_weight": 400, // The default font size for text in the UI "ui_font_size": 16, - // The default font size for text in the agent panel - "agent_font_size": 16, + // The default font size for text in the agent panel. Falls back to the UI font size if unset. + "agent_font_size": null, // How much to fade out unused code. "unnecessary_code_fade": 0.3, // Active pane styling settings. diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 519f7980ff684cabaaa1d8d138e476009e2d8ed9..44d605af57f6e94e0277d1d7645e22029614f192 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1257,13 +1257,11 @@ impl AgentPanel { ThemeSettings::get_global(cx).agent_font_size(cx) + delta; let _ = settings .agent_font_size - .insert(theme::clamp_font_size(agent_font_size).0); + .insert(Some(theme::clamp_font_size(agent_font_size).into())); }, ); } else { - theme::adjust_agent_font_size(cx, |size| { - *size += delta; - }); + theme::adjust_agent_font_size(cx, |size| size + delta); } } WhichFontSize::BufferFont => { diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index f5f1fd55475ca363c677ef623fd7a2cdde44d40c..df147cfe92377962b135fed309ef0a7df68adcd8 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -19,6 +19,7 @@ use util::ResultExt as _; use util::schemars::replace_subschema; const MIN_FONT_SIZE: Pixels = px(6.0); +const MAX_FONT_SIZE: Pixels = px(100.0); const MIN_LINE_HEIGHT: f32 = 1.0; #[derive( @@ -103,8 +104,8 @@ pub struct ThemeSettings { /// /// The terminal font family can be overridden using it's own setting. pub buffer_font: Font, - /// The agent font size. Determines the size of text in the agent panel. - agent_font_size: Pixels, + /// The agent font size. Determines the size of text in the agent panel. Falls back to the UI font size if unset. + agent_font_size: Option, /// The line height for buffers, and the terminal. /// /// Changing this may affect the spacing of some UI elements. @@ -404,9 +405,9 @@ pub struct ThemeSettingsContent { #[serde(default)] #[schemars(default = "default_font_features")] pub buffer_font_features: Option, - /// The font size for the agent panel. + /// The font size for the agent panel. Falls back to the UI font size if unset. #[serde(default)] - pub agent_font_size: Option, + pub agent_font_size: Option>, /// The name of the Zed theme to use. #[serde(default)] pub theme: Option, @@ -599,13 +600,13 @@ impl ThemeSettings { clamp_font_size(font_size) } - /// Returns the UI font size. + /// Returns the agent panel font size. Falls back to the UI font size if unset. pub fn agent_font_size(&self, cx: &App) -> Pixels { - let font_size = cx - .try_global::() + cx.try_global::() .map(|size| size.0) - .unwrap_or(self.agent_font_size); - clamp_font_size(font_size) + .or(self.agent_font_size) + .map(clamp_font_size) + .unwrap_or_else(|| self.ui_font_size(cx)) } /// Returns the buffer font size, read from the settings. @@ -624,6 +625,14 @@ impl ThemeSettings { self.ui_font_size } + /// Returns the agent font size, read from the settings. + /// + /// The real agent font size is stored in-memory, to support temporary font size changes. + /// Use [`Self::agent_font_size`] to get the real font size. + pub fn agent_font_size_settings(&self) -> Option { + self.agent_font_size + } + // TODO: Rename: `line_height` -> `buffer_line_height` /// Returns the buffer's line height. pub fn line_height(&self) -> f32 { @@ -732,14 +741,12 @@ pub fn adjusted_font_size(size: Pixels, cx: &App) -> Pixels { } /// Adjusts the buffer font size. -pub fn adjust_buffer_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) { +pub fn adjust_buffer_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; - let mut adjusted_size = cx + let adjusted_size = cx .try_global::() .map_or(buffer_font_size, |adjusted_size| adjusted_size.0); - - f(&mut adjusted_size); - cx.set_global(BufferFontSize(clamp_font_size(adjusted_size))); + cx.set_global(BufferFontSize(clamp_font_size(f(adjusted_size)))); cx.refresh_windows(); } @@ -765,14 +772,12 @@ pub fn setup_ui_font(window: &mut Window, cx: &mut App) -> gpui::Font { } /// Sets the adjusted UI font size. -pub fn adjust_ui_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) { +pub fn adjust_ui_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); - let mut adjusted_size = cx + let adjusted_size = cx .try_global::() .map_or(ui_font_size, |adjusted_size| adjusted_size.0); - - f(&mut adjusted_size); - cx.set_global(UiFontSize(clamp_font_size(adjusted_size))); + cx.set_global(UiFontSize(clamp_font_size(f(adjusted_size)))); cx.refresh_windows(); } @@ -784,19 +789,17 @@ pub fn reset_ui_font_size(cx: &mut App) { } } -/// Sets the adjusted UI font size. -pub fn adjust_agent_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) { +/// Sets the adjusted agent panel font size. +pub fn adjust_agent_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { let agent_font_size = ThemeSettings::get_global(cx).agent_font_size(cx); - let mut adjusted_size = cx + let adjusted_size = cx .try_global::() .map_or(agent_font_size, |adjusted_size| adjusted_size.0); - - f(&mut adjusted_size); - cx.set_global(AgentFontSize(clamp_font_size(adjusted_size))); + cx.set_global(AgentFontSize(clamp_font_size(f(adjusted_size)))); cx.refresh_windows(); } -/// Resets the UI font size to the default value. +/// Resets the agent panel font size to the default value. pub fn reset_agent_font_size(cx: &mut App) { if cx.has_global::() { cx.remove_global::(); @@ -806,7 +809,7 @@ pub fn reset_agent_font_size(cx: &mut App) { /// Ensures font size is within the valid range. pub fn clamp_font_size(size: Pixels) -> Pixels { - size.max(MIN_FONT_SIZE) + size.clamp(MIN_FONT_SIZE, MAX_FONT_SIZE) } fn clamp_font_weight(weight: f32) -> FontWeight { @@ -860,7 +863,7 @@ impl settings::Settings for ThemeSettings { }, buffer_font_size: defaults.buffer_font_size.unwrap().into(), buffer_line_height: defaults.buffer_line_height.unwrap(), - agent_font_size: defaults.agent_font_size.unwrap().into(), + agent_font_size: defaults.agent_font_size.flatten().map(Into::into), theme_selection: defaults.theme.clone(), active_theme: themes .get(defaults.theme.as_ref().unwrap().theme(*system_appearance)) @@ -959,20 +962,20 @@ impl settings::Settings for ThemeSettings { } } - merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into)); - this.ui_font_size = this.ui_font_size.clamp(px(6.), px(100.)); - + merge( + &mut this.ui_font_size, + value.ui_font_size.map(Into::into).map(clamp_font_size), + ); merge( &mut this.buffer_font_size, - value.buffer_font_size.map(Into::into), + value.buffer_font_size.map(Into::into).map(clamp_font_size), ); - this.buffer_font_size = this.buffer_font_size.clamp(px(6.), px(100.)); - merge( &mut this.agent_font_size, - value.agent_font_size.map(Into::into), + value + .agent_font_size + .map(|value| value.map(Into::into).map(clamp_font_size)), ); - this.agent_font_size = this.agent_font_size.clamp(px(6.), px(100.)); merge(&mut this.buffer_line_height, value.buffer_line_height); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f04eeade73a55210143ff9f96b865d90af20f6f5..e02324a14241a89ad9004b6fcff0644c3099d945 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -107,6 +107,8 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut App) { let mut prev_buffer_font_size_settings = ThemeSettings::get_global(cx).buffer_font_size_settings(); let mut prev_ui_font_size_settings = ThemeSettings::get_global(cx).ui_font_size_settings(); + let mut prev_agent_font_size_settings = + ThemeSettings::get_global(cx).agent_font_size_settings(); cx.observe_global::(move |cx| { let buffer_font_size_settings = ThemeSettings::get_global(cx).buffer_font_size_settings(); if buffer_font_size_settings != prev_buffer_font_size_settings { @@ -119,6 +121,12 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut App) { prev_ui_font_size_settings = ui_font_size_settings; reset_ui_font_size(cx); } + + let agent_font_size_settings = ThemeSettings::get_global(cx).agent_font_size_settings(); + if agent_font_size_settings != prev_agent_font_size_settings { + prev_agent_font_size_settings = agent_font_size_settings; + reset_agent_font_size(cx); + } }) .detach(); } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a324ba0932c02d490513b8228ffae9e2026f75d0..cfafbb70f05b63220841b27712d3d9ea1b30f5f8 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -716,9 +716,7 @@ fn register_actions( .insert(theme::clamp_font_size(ui_font_size).0); }); } else { - theme::adjust_ui_font_size(cx, |size| { - *size += px(1.0); - }); + theme::adjust_ui_font_size(cx, |size| size + px(1.0)); } } }) @@ -733,9 +731,7 @@ fn register_actions( .insert(theme::clamp_font_size(ui_font_size).0); }); } else { - theme::adjust_ui_font_size(cx, |size| { - *size -= px(1.0); - }); + theme::adjust_ui_font_size(cx, |size| size - px(1.0)); } } }) @@ -763,9 +759,7 @@ fn register_actions( .insert(theme::clamp_font_size(buffer_font_size).0); }); } else { - theme::adjust_buffer_font_size(cx, |size| { - *size += px(1.0); - }); + theme::adjust_buffer_font_size(cx, |size| size + px(1.0)); } } }) @@ -781,9 +775,7 @@ fn register_actions( .insert(theme::clamp_font_size(buffer_font_size).0); }); } else { - theme::adjust_buffer_font_size(cx, |size| { - *size -= px(1.0); - }); + theme::adjust_buffer_font_size(cx, |size| size - px(1.0)); } } }) From 332626e5825564e97afc969292c90d9b0fb40b6d Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Sat, 16 Aug 2025 17:04:09 +0200 Subject: [PATCH 066/744] Allow Permission Request to only require a ToolCallUpdate instead of a full tool call (#36319) Release Notes: - N/A --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 36 ++++++++++------- crates/acp_thread/src/connection.rs | 4 +- crates/agent2/src/agent.rs | 11 ++--- crates/agent2/src/thread.rs | 40 ++++++------------- crates/agent2/src/tools/edit_file_tool.rs | 12 ++++-- crates/agent_servers/src/acp/v0.rs | 6 +-- crates/agent_servers/src/acp/v1.rs | 2 +- crates/agent_servers/src/claude.rs | 3 +- crates/agent_servers/src/claude/mcp_server.rs | 4 +- 11 files changed, 63 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1bce72b3a138a79e5344a080b83db8abc4ef202a..f59d92739b2b46315f09585bb32dece302329930 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.24" +version = "0.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd68bbbef8e424fb8a605c5f0b00c360f682c4528b0a5feb5ec928aaf5ce28e" +checksum = "2ab66add8be8d6a963f5bf4070045c1bbf36472837654c73e2298dd16bda5bf7" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index 644b6c0f40c403a68dc5bc03842d1f0669eafa38..b467e8743e37d149812ec0deb84782d4d351b1e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -426,7 +426,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.24" +agent-client-protocol = "0.0.25" 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 2ef94a3cbe37472d4a8c3485493e06841111fabd..3bb1b99ba101f32becab7f1c85c5be1e0d870a55 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -792,7 +792,7 @@ impl AcpThread { &mut self, update: acp::SessionUpdate, cx: &mut Context, - ) -> Result<()> { + ) -> Result<(), acp::Error> { match update { acp::SessionUpdate::UserMessageChunk { content } => { self.push_user_content_block(None, content, cx); @@ -804,7 +804,7 @@ impl AcpThread { self.push_assistant_content_block(content, true, cx); } acp::SessionUpdate::ToolCall(tool_call) => { - self.upsert_tool_call(tool_call, cx); + self.upsert_tool_call(tool_call, cx)?; } acp::SessionUpdate::ToolCallUpdate(tool_call_update) => { self.update_tool_call(tool_call_update, cx)?; @@ -940,32 +940,40 @@ impl AcpThread { } /// Updates a tool call if id matches an existing entry, otherwise inserts a new one. - pub fn upsert_tool_call(&mut self, tool_call: acp::ToolCall, cx: &mut Context) { + pub fn upsert_tool_call( + &mut self, + tool_call: acp::ToolCall, + cx: &mut Context, + ) -> Result<(), acp::Error> { let status = ToolCallStatus::Allowed { status: tool_call.status, }; - self.upsert_tool_call_inner(tool_call, status, cx) + self.upsert_tool_call_inner(tool_call.into(), status, cx) } + /// Fails if id does not match an existing entry. pub fn upsert_tool_call_inner( &mut self, - tool_call: acp::ToolCall, + tool_call_update: acp::ToolCallUpdate, status: ToolCallStatus, cx: &mut Context, - ) { + ) -> Result<(), acp::Error> { let language_registry = self.project.read(cx).languages().clone(); - let call = ToolCall::from_acp(tool_call, status, language_registry, cx); - let id = call.id.clone(); + let id = tool_call_update.id.clone(); - if let Some((ix, current_call)) = self.tool_call_mut(&call.id) { - *current_call = call; + if let Some((ix, current_call)) = self.tool_call_mut(&id) { + current_call.update_fields(tool_call_update.fields, language_registry, cx); + current_call.status = status; cx.emit(AcpThreadEvent::EntryUpdated(ix)); } else { + let call = + ToolCall::from_acp(tool_call_update.try_into()?, status, language_registry, cx); self.push_entry(AgentThreadEntry::ToolCall(call), cx); }; self.resolve_locations(id, cx); + Ok(()) } fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> { @@ -1034,10 +1042,10 @@ impl AcpThread { pub fn request_tool_call_authorization( &mut self, - tool_call: acp::ToolCall, + tool_call: acp::ToolCallUpdate, options: Vec, cx: &mut Context, - ) -> oneshot::Receiver { + ) -> Result, acp::Error> { let (tx, rx) = oneshot::channel(); let status = ToolCallStatus::WaitingForConfirmation { @@ -1045,9 +1053,9 @@ impl AcpThread { respond_tx: tx, }; - self.upsert_tool_call_inner(tool_call, status, cx); + self.upsert_tool_call_inner(tool_call, status, cx)?; cx.emit(AcpThreadEvent::ToolAuthorizationRequired); - rx + Ok(rx) } pub fn authorize_tool_call( diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index b2116020fb963ae6f2789ef9b37b1c997714aa63..7497d2309f1de72186c23773429cc6e8c57de2d2 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -286,12 +286,12 @@ mod test_support { if let Some((tool_call, options)) = permission_request { let permission = thread.update(cx, |thread, cx| { thread.request_tool_call_authorization( - tool_call.clone(), + tool_call.clone().into(), options.clone(), cx, ) })?; - permission.await?; + permission?.await?; } thread.update(cx, |thread, cx| { thread.handle_session_update(update.clone(), cx).unwrap(); diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 358365d11faebb11cc91b67616de0fc5ff3a3ee4..d63e3f81345e690b2ab7ea0e5644b62da740fa20 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -514,10 +514,11 @@ impl NativeAgentConnection { thread.request_tool_call_authorization(tool_call, options, cx) })?; cx.background_spawn(async move { - if let Some(option) = recv - .await - .context("authorization sender was dropped") - .log_err() + if let Some(recv) = recv.log_err() + && let Some(option) = recv + .await + .context("authorization sender was dropped") + .log_err() { response .send(option) @@ -530,7 +531,7 @@ impl NativeAgentConnection { AgentResponseEvent::ToolCall(tool_call) => { acp_thread.update(cx, |thread, cx| { thread.upsert_tool_call(tool_call, cx) - })?; + })??; } AgentResponseEvent::ToolCallUpdate(update) => { acp_thread.update(cx, |thread, cx| { diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index cfd67f4b05400d281e512a262026989ba4db2675..0741bb9e081ca4c17536f96623ad0d2830243051 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -448,7 +448,7 @@ pub enum AgentResponseEvent { #[derive(Debug)] pub struct ToolCallAuthorization { - pub tool_call: acp::ToolCall, + pub tool_call: acp::ToolCallUpdate, pub options: Vec, pub response: oneshot::Sender, } @@ -901,7 +901,7 @@ impl Thread { let fs = self.project.read(cx).fs().clone(); let tool_event_stream = - ToolCallEventStream::new(&tool_use, tool.kind(), event_stream.clone(), Some(fs)); + ToolCallEventStream::new(tool_use.id.clone(), event_stream.clone(), Some(fs)); tool_event_stream.update_fields(acp::ToolCallUpdateFields { status: Some(acp::ToolCallStatus::InProgress), ..Default::default() @@ -1344,8 +1344,6 @@ impl AgentResponseEventStream { #[derive(Clone)] pub struct ToolCallEventStream { tool_use_id: LanguageModelToolUseId, - kind: acp::ToolKind, - input: serde_json::Value, stream: AgentResponseEventStream, fs: Option>, } @@ -1355,32 +1353,19 @@ impl ToolCallEventStream { pub fn test() -> (Self, ToolCallEventStreamReceiver) { let (events_tx, events_rx) = mpsc::unbounded::>(); - let stream = ToolCallEventStream::new( - &LanguageModelToolUse { - id: "test_id".into(), - name: "test_tool".into(), - raw_input: String::new(), - input: serde_json::Value::Null, - is_input_complete: true, - }, - acp::ToolKind::Other, - AgentResponseEventStream(events_tx), - None, - ); + let stream = + ToolCallEventStream::new("test_id".into(), AgentResponseEventStream(events_tx), None); (stream, ToolCallEventStreamReceiver(events_rx)) } fn new( - tool_use: &LanguageModelToolUse, - kind: acp::ToolKind, + tool_use_id: LanguageModelToolUseId, stream: AgentResponseEventStream, fs: Option>, ) -> Self { Self { - tool_use_id: tool_use.id.clone(), - kind, - input: tool_use.input.clone(), + tool_use_id, stream, fs, } @@ -1427,12 +1412,13 @@ impl ToolCallEventStream { .0 .unbounded_send(Ok(AgentResponseEvent::ToolCallAuthorization( ToolCallAuthorization { - tool_call: AgentResponseEventStream::initial_tool_call( - &self.tool_use_id, - title.into(), - self.kind.clone(), - self.input.clone(), - ), + tool_call: acp::ToolCallUpdate { + id: acp::ToolCallId(self.tool_use_id.to_string().into()), + fields: acp::ToolCallUpdateFields { + title: Some(title.into()), + ..Default::default() + }, + }, options: vec![ acp::PermissionOption { id: acp::PermissionOptionId("always_allow".into()), diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index c77b9f6a69bededaa632333b40c85d73bf4e8a92..4b4f98daecb90593aa642d41e1becf325aa4c699 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -1001,7 +1001,10 @@ mod tests { }); let event = stream_rx.expect_authorization().await; - assert_eq!(event.tool_call.title, "test 1 (local settings)"); + assert_eq!( + event.tool_call.fields.title, + Some("test 1 (local settings)".into()) + ); // Test 2: Path outside project should require confirmation let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); @@ -1018,7 +1021,7 @@ mod tests { }); let event = stream_rx.expect_authorization().await; - assert_eq!(event.tool_call.title, "test 2"); + assert_eq!(event.tool_call.fields.title, Some("test 2".into())); // Test 3: Relative path without .zed should not require confirmation let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); @@ -1051,7 +1054,10 @@ mod tests { ) }); let event = stream_rx.expect_authorization().await; - assert_eq!(event.tool_call.title, "test 4 (local settings)"); + assert_eq!( + event.tool_call.fields.title, + Some("test 4 (local settings)".into()) + ); // Test 5: When always_allow_tool_actions is enabled, no confirmation needed cx.update(|cx| { diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index e936c87643f652c92eda84dc67240612ba3b837f..74647f73133f23681f18da1d2bddb02675c55a22 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -135,9 +135,9 @@ impl acp_old::Client for OldAcpClientDelegate { let response = cx .update(|cx| { self.thread.borrow().update(cx, |thread, cx| { - thread.request_tool_call_authorization(tool_call, acp_options, cx) + thread.request_tool_call_authorization(tool_call.into(), acp_options, cx) }) - })? + })?? .context("Failed to update thread")? .await; @@ -168,7 +168,7 @@ impl acp_old::Client for OldAcpClientDelegate { cx, ) }) - })? + })?? .context("Failed to update thread")?; Ok(acp_old::PushToolCallResponse { diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 6cf9801d064b20addf4e6d976fa856bea8a3a27d..506ae80886c9d66acdbaebb197a6f17748b2a46a 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -233,7 +233,7 @@ impl acp::Client for ClientDelegate { thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx) })?; - let result = rx.await; + let result = rx?.await; let outcome = match result { Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 14a179ba3dbe85d8fbacbfe5f7ee97fbef0045bf..4b3a1733491262b8d26979c35d20af6a78b16a8d 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -560,8 +560,9 @@ impl ClaudeAgentSession { thread.upsert_tool_call( claude_tool.as_acp(acp::ToolCallId(id.into())), cx, - ); + )?; } + anyhow::Ok(()) }) .log_err(); } diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index 53a8556e74545bc339936d0b2f9f78444190af0c..22cb2f8f8d7c15608527eb7492220bf9c7fe920c 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -154,7 +154,7 @@ impl McpServerTool for PermissionTool { let chosen_option = thread .update(cx, |thread, cx| { thread.request_tool_call_authorization( - claude_tool.as_acp(tool_call_id), + claude_tool.as_acp(tool_call_id).into(), vec![ acp::PermissionOption { id: allow_option_id.clone(), @@ -169,7 +169,7 @@ impl McpServerTool for PermissionTool { ], cx, ) - })? + })?? .await?; let response = if chosen_option == allow_option_id { From 15a1eb2a2e3e249eae5ee402fc8a7a3d19260bf6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 16 Aug 2025 13:02:51 -0400 Subject: [PATCH 067/744] emmet: Extract to zed-extensions/emmet repository (#36323) This PR extracts the Emmet extension to the [zed-extensions/emmet](https://github.com/zed-extensions/emmet) repository. Release Notes: - N/A --- .config/hakari.toml | 1 - Cargo.lock | 7 --- Cargo.toml | 1 - docs/src/languages/emmet.md | 2 + extensions/emmet/.gitignore | 3 - extensions/emmet/Cargo.toml | 16 ----- extensions/emmet/LICENSE-APACHE | 1 - extensions/emmet/extension.toml | 24 -------- extensions/emmet/src/emmet.rs | 106 -------------------------------- 9 files changed, 2 insertions(+), 159 deletions(-) delete mode 100644 extensions/emmet/.gitignore delete mode 100644 extensions/emmet/Cargo.toml delete mode 120000 extensions/emmet/LICENSE-APACHE delete mode 100644 extensions/emmet/extension.toml delete mode 100644 extensions/emmet/src/emmet.rs diff --git a/.config/hakari.toml b/.config/hakari.toml index f71e97b45c8399e52c6d793e59b3fdfcc47c87f7..8ce0b77490482ab5ff2d781fb78fd86b56959a6a 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -34,7 +34,6 @@ workspace-members = [ "zed_extension_api", # exclude all extensions - "zed_emmet", "zed_glsl", "zed_html", "zed_proto", diff --git a/Cargo.lock b/Cargo.lock index f59d92739b2b46315f09585bb32dece302329930..5100a634770efdd39225414262fdf3d64a00e0f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20520,13 +20520,6 @@ dependencies = [ "workspace-hack", ] -[[package]] -name = "zed_emmet" -version = "0.0.6" -dependencies = [ - "zed_extension_api 0.1.0", -] - [[package]] name = "zed_extension_api" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b467e8743e37d149812ec0deb84782d4d351b1e7..a94db953ab5cc00bbdeaa1c3a5227c57dc1a8194 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -199,7 +199,6 @@ members = [ # Extensions # - "extensions/emmet", "extensions/glsl", "extensions/html", "extensions/proto", diff --git a/docs/src/languages/emmet.md b/docs/src/languages/emmet.md index 1a76291ad4e2612c25cc24fbc1f1122b047c25d8..73e34c209f1248c9d0d5dda96730dec15ab74730 100644 --- a/docs/src/languages/emmet.md +++ b/docs/src/languages/emmet.md @@ -1,5 +1,7 @@ # Emmet +Emmet support is available through the [Emmet extension](https://github.com/zed-extensions/emmet). + [Emmet](https://emmet.io/) is a web-developer’s toolkit that can greatly improve your HTML & CSS workflow. - Language Server: [olrtg/emmet-language-server](https://github.com/olrtg/emmet-language-server) diff --git a/extensions/emmet/.gitignore b/extensions/emmet/.gitignore deleted file mode 100644 index 62c0add260e0ad28057d36f9575ef66e430a5a20..0000000000000000000000000000000000000000 --- a/extensions/emmet/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.wasm -grammars -target diff --git a/extensions/emmet/Cargo.toml b/extensions/emmet/Cargo.toml deleted file mode 100644 index 2fbdf2a7e5930ab2a80b83c925b01d2a7cc98129..0000000000000000000000000000000000000000 --- a/extensions/emmet/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "zed_emmet" -version = "0.0.6" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/emmet.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.1.0" diff --git a/extensions/emmet/LICENSE-APACHE b/extensions/emmet/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3affae83854be02a0afdec3b7a9ec4d..0000000000000000000000000000000000000000 --- a/extensions/emmet/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/emmet/extension.toml b/extensions/emmet/extension.toml deleted file mode 100644 index a1848400b89b00944f18ee3783e8f66a708bb065..0000000000000000000000000000000000000000 --- a/extensions/emmet/extension.toml +++ /dev/null @@ -1,24 +0,0 @@ -id = "emmet" -name = "Emmet" -description = "Emmet support" -version = "0.0.6" -schema_version = 1 -authors = ["Piotr Osiewicz "] -repository = "https://github.com/zed-industries/zed" - -[language_servers.emmet-language-server] -name = "Emmet Language Server" -language = "HTML" -languages = ["HTML", "PHP", "ERB", "HTML/ERB", "JavaScript", "TSX", "CSS", "HEEX", "Elixir", "Vue.js"] - -[language_servers.emmet-language-server.language_ids] -"HTML" = "html" -"PHP" = "php" -"ERB" = "eruby" -"HTML/ERB" = "eruby" -"JavaScript" = "javascriptreact" -"TSX" = "typescriptreact" -"CSS" = "css" -"HEEX" = "heex" -"Elixir" = "heex" -"Vue.js" = "vue" diff --git a/extensions/emmet/src/emmet.rs b/extensions/emmet/src/emmet.rs deleted file mode 100644 index 1434e16e882f2238b8ba0ae21f46ecbbe4b06550..0000000000000000000000000000000000000000 --- a/extensions/emmet/src/emmet.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::{env, fs}; -use zed_extension_api::{self as zed, Result}; - -struct EmmetExtension { - did_find_server: bool, -} - -const SERVER_PATH: &str = "node_modules/@olrtg/emmet-language-server/dist/index.js"; -const PACKAGE_NAME: &str = "@olrtg/emmet-language-server"; - -impl EmmetExtension { - fn server_exists(&self) -> bool { - fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file()) - } - - fn server_script_path(&mut self, language_server_id: &zed::LanguageServerId) -> Result { - let server_exists = self.server_exists(); - if self.did_find_server && server_exists { - return Ok(SERVER_PATH.to_string()); - } - - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::CheckingForUpdate, - ); - let version = zed::npm_package_latest_version(PACKAGE_NAME)?; - - if !server_exists - || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version) - { - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::Downloading, - ); - let result = zed::npm_install_package(PACKAGE_NAME, &version); - match result { - Ok(()) => { - if !self.server_exists() { - Err(format!( - "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'", - ))?; - } - } - Err(error) => { - if !self.server_exists() { - Err(error)?; - } - } - } - } - - self.did_find_server = true; - Ok(SERVER_PATH.to_string()) - } -} - -impl zed::Extension for EmmetExtension { - fn new() -> Self { - Self { - did_find_server: false, - } - } - - fn language_server_command( - &mut self, - language_server_id: &zed::LanguageServerId, - _worktree: &zed::Worktree, - ) -> Result { - let server_path = self.server_script_path(language_server_id)?; - Ok(zed::Command { - command: zed::node_binary_path()?, - args: vec![ - zed_ext::sanitize_windows_path(env::current_dir().unwrap()) - .join(&server_path) - .to_string_lossy() - .to_string(), - "--stdio".to_string(), - ], - env: Default::default(), - }) - } -} - -zed::register_extension!(EmmetExtension); - -/// Extensions to the Zed extension API that have not yet stabilized. -mod zed_ext { - /// Sanitizes the given path to remove the leading `/` on Windows. - /// - /// On macOS and Linux this is a no-op. - /// - /// This is a workaround for https://github.com/bytecodealliance/wasmtime/issues/10415. - pub fn sanitize_windows_path(path: std::path::PathBuf) -> std::path::PathBuf { - use zed_extension_api::{Os, current_platform}; - - let (os, _arch) = current_platform(); - match os { - Os::Mac | Os::Linux => path, - Os::Windows => path - .to_string_lossy() - .to_string() - .trim_start_matches('/') - .into(), - } - } -} From f17f63ec84424f772bfdb7c7998db598829596bf Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 16 Aug 2025 15:00:31 -0400 Subject: [PATCH 068/744] Remove `/docs` slash command (#36325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR removes the `/docs` slash command. We never fully shipped this—with it requiring explicit opt-in via a setting—and it doesn't seem like the feature is needed in an agentic world. Release Notes: - Removed the `/docs` slash command. --- Cargo.lock | 30 - Cargo.toml | 2 - assets/settings/default.json | 5 - crates/agent_ui/Cargo.toml | 1 - crates/agent_ui/src/agent_configuration.rs | 1 - crates/agent_ui/src/agent_ui.rs | 7 - crates/agent_ui/src/slash_command_settings.rs | 11 - crates/agent_ui/src/text_thread_editor.rs | 86 +-- crates/assistant_slash_commands/Cargo.toml | 1 - .../src/assistant_slash_commands.rs | 2 - .../src/docs_command.rs | 543 --------------- crates/extension/src/extension_host_proxy.rs | 34 - crates/extension/src/extension_manifest.rs | 7 - crates/extension_cli/src/main.rs | 4 - .../extension_compilation_benchmark.rs | 1 - .../extension_host/src/capability_granter.rs | 1 - crates/extension_host/src/extension_host.rs | 15 +- .../src/extension_store_test.rs | 3 - crates/indexed_docs/Cargo.toml | 38 -- crates/indexed_docs/LICENSE-GPL | 1 - .../src/extension_indexed_docs_provider.rs | 81 --- crates/indexed_docs/src/indexed_docs.rs | 16 - crates/indexed_docs/src/providers.rs | 1 - crates/indexed_docs/src/providers/rustdoc.rs | 291 --------- .../src/providers/rustdoc/item.rs | 82 --- .../src/providers/rustdoc/popular_crates.txt | 252 ------- .../src/providers/rustdoc/to_markdown.rs | 618 ------------------ crates/indexed_docs/src/registry.rs | 62 -- crates/indexed_docs/src/store.rs | 346 ---------- typos.toml | 3 - 30 files changed, 6 insertions(+), 2539 deletions(-) delete mode 100644 crates/assistant_slash_commands/src/docs_command.rs delete mode 100644 crates/indexed_docs/Cargo.toml delete mode 120000 crates/indexed_docs/LICENSE-GPL delete mode 100644 crates/indexed_docs/src/extension_indexed_docs_provider.rs delete mode 100644 crates/indexed_docs/src/indexed_docs.rs delete mode 100644 crates/indexed_docs/src/providers.rs delete mode 100644 crates/indexed_docs/src/providers/rustdoc.rs delete mode 100644 crates/indexed_docs/src/providers/rustdoc/item.rs delete mode 100644 crates/indexed_docs/src/providers/rustdoc/popular_crates.txt delete mode 100644 crates/indexed_docs/src/providers/rustdoc/to_markdown.rs delete mode 100644 crates/indexed_docs/src/registry.rs delete mode 100644 crates/indexed_docs/src/store.rs diff --git a/Cargo.lock b/Cargo.lock index 5100a634770efdd39225414262fdf3d64a00e0f0..b4bf705eb9aa27be9ae6637ce3e3f8c34b18151e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -347,7 +347,6 @@ dependencies = [ "gpui", "html_to_markdown", "http_client", - "indexed_docs", "indoc", "inventory", "itertools 0.14.0", @@ -872,7 +871,6 @@ dependencies = [ "gpui", "html_to_markdown", "http_client", - "indexed_docs", "language", "pretty_assertions", "project", @@ -8383,34 +8381,6 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" -[[package]] -name = "indexed_docs" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "cargo_metadata", - "collections", - "derive_more 0.99.19", - "extension", - "fs", - "futures 0.3.31", - "fuzzy", - "gpui", - "heed", - "html_to_markdown", - "http_client", - "indexmap", - "indoc", - "parking_lot", - "paths", - "pretty_assertions", - "serde", - "strum 0.27.1", - "util", - "workspace-hack", -] - [[package]] name = "indexmap" version = "2.9.0" diff --git a/Cargo.toml b/Cargo.toml index a94db953ab5cc00bbdeaa1c3a5227c57dc1a8194..b3105bd97c9eb59e43fda3bf714defef4aee0b09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,6 @@ members = [ "crates/http_client_tls", "crates/icons", "crates/image_viewer", - "crates/indexed_docs", "crates/edit_prediction", "crates/edit_prediction_button", "crates/inspector_ui", @@ -305,7 +304,6 @@ http_client = { path = "crates/http_client" } http_client_tls = { path = "crates/http_client_tls" } icons = { path = "crates/icons" } image_viewer = { path = "crates/image_viewer" } -indexed_docs = { path = "crates/indexed_docs" } edit_prediction = { path = "crates/edit_prediction" } edit_prediction_button = { path = "crates/edit_prediction_button" } inspector_ui = { path = "crates/inspector_ui" } diff --git a/assets/settings/default.json b/assets/settings/default.json index ff000001b567a58bca11bdbbfcd89494470ac91f..6a8b034268d39c14d3f57273f1cb80a025e3cf5e 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -887,11 +887,6 @@ }, // The settings for slash commands. "slash_commands": { - // Settings for the `/docs` slash command. - "docs": { - // Whether `/docs` is enabled. - "enabled": false - }, // Settings for the `/project` slash command. "project": { // Whether `/project` is enabled. diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 13fd9d13c5014d5683a41739fdff335049880de8..fbf8590e681c1d355e7904f171abec8cafff97da 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -50,7 +50,6 @@ fuzzy.workspace = true gpui.workspace = true html_to_markdown.workspace = true http_client.workspace = true -indexed_docs.workspace = true indoc.workspace = true inventory.workspace = true itertools.workspace = true diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 96558f1beac5df0c4e7193e9468002bd12fae3a2..4a2dd88c332b03dd914bcb793fed2b4b2d8c0c07 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -1035,7 +1035,6 @@ fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool && manifest.grammars.is_empty() && manifest.language_servers.is_empty() && manifest.slash_commands.is_empty() - && manifest.indexed_docs_providers.is_empty() && manifest.snippets.is_none() && manifest.debug_locators.is_empty() } diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 4f5f02259372bf095a6bb7d2329b69815ccb1184..f25b576886d83db32ea48dbffac400a8a096a695 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -242,7 +242,6 @@ pub fn init( client.telemetry().clone(), cx, ); - indexed_docs::init(cx); cx.observe_new(move |workspace, window, cx| { ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx) }) @@ -409,12 +408,6 @@ fn update_slash_commands_from_settings(cx: &mut App) { let slash_command_registry = SlashCommandRegistry::global(cx); let settings = SlashCommandSettings::get_global(cx); - if settings.docs.enabled { - slash_command_registry.register_command(assistant_slash_commands::DocsSlashCommand, true); - } else { - slash_command_registry.unregister_command(assistant_slash_commands::DocsSlashCommand); - } - if settings.cargo_workspace.enabled { slash_command_registry .register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true); diff --git a/crates/agent_ui/src/slash_command_settings.rs b/crates/agent_ui/src/slash_command_settings.rs index f254d00ec60b08197237d0f179bc755c16ed7d40..73e5622aa921ccf03a3813717446e830c21079b8 100644 --- a/crates/agent_ui/src/slash_command_settings.rs +++ b/crates/agent_ui/src/slash_command_settings.rs @@ -7,22 +7,11 @@ use settings::{Settings, SettingsSources}; /// Settings for slash commands. #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] pub struct SlashCommandSettings { - /// Settings for the `/docs` slash command. - #[serde(default)] - pub docs: DocsCommandSettings, /// Settings for the `/cargo-workspace` slash command. #[serde(default)] pub cargo_workspace: CargoWorkspaceCommandSettings, } -/// Settings for the `/docs` slash command. -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] -pub struct DocsCommandSettings { - /// Whether `/docs` is enabled. - #[serde(default)] - pub enabled: bool, -} - /// Settings for the `/cargo-workspace` slash command. #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] pub struct CargoWorkspaceCommandSettings { diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 2e3b4ed890b05fd564bcdec5706b33443e2aa7e7..8c1e163ecaf54d305f5f3d440e29d23ea7a0f0f8 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -5,10 +5,7 @@ use crate::{ use agent_settings::{AgentSettings, CompletionMode}; use anyhow::Result; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet}; -use assistant_slash_commands::{ - DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs, FileSlashCommand, - selections_creases, -}; +use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases}; use client::{proto, zed_urls}; use collections::{BTreeSet, HashMap, HashSet, hash_map}; use editor::{ @@ -30,7 +27,6 @@ use gpui::{ StatefulInteractiveElement, Styled, Subscription, Task, Transformation, WeakEntity, actions, div, img, percentage, point, prelude::*, pulsating_between, size, }; -use indexed_docs::IndexedDocsStore; use language::{ BufferSnapshot, LspAdapterDelegate, ToOffset, language_settings::{SoftWrap, all_language_settings}, @@ -77,7 +73,7 @@ use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker} use assistant_context::{ AssistantContext, CacheStatus, Content, ContextEvent, ContextId, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, MessageMetadata, MessageStatus, - ParsedSlashCommand, PendingSlashCommandStatus, ThoughtProcessOutputSection, + PendingSlashCommandStatus, ThoughtProcessOutputSection, }; actions!( @@ -701,19 +697,7 @@ impl TextThreadEditor { } }; let render_trailer = { - let command = command.clone(); - move |row, _unfold, _window: &mut Window, cx: &mut App| { - // TODO: In the future we should investigate how we can expose - // this as a hook on the `SlashCommand` trait so that we don't - // need to special-case it here. - if command.name == DocsSlashCommand::NAME { - return render_docs_slash_command_trailer( - row, - command.clone(), - cx, - ); - } - + move |_row, _unfold, _window: &mut Window, _cx: &mut App| { Empty.into_any() } }; @@ -2398,70 +2382,6 @@ fn render_pending_slash_command_gutter_decoration( icon.into_any_element() } -fn render_docs_slash_command_trailer( - row: MultiBufferRow, - command: ParsedSlashCommand, - cx: &mut App, -) -> AnyElement { - if command.arguments.is_empty() { - return Empty.into_any(); - } - let args = DocsSlashCommandArgs::parse(&command.arguments); - - let Some(store) = args - .provider() - .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok()) - else { - return Empty.into_any(); - }; - - let Some(package) = args.package() else { - return Empty.into_any(); - }; - - let mut children = Vec::new(); - - if store.is_indexing(&package) { - children.push( - div() - .id(("crates-being-indexed", row.0)) - .child(Icon::new(IconName::ArrowCircle).with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(4)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - )) - .tooltip({ - let package = package.clone(); - Tooltip::text(format!("Indexing {package}…")) - }) - .into_any_element(), - ); - } - - if let Some(latest_error) = store.latest_error_for_package(&package) { - children.push( - div() - .id(("latest-error", row.0)) - .child( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .color(Color::Warning), - ) - .tooltip(Tooltip::text(format!("Failed to index: {latest_error}"))) - .into_any_element(), - ) - } - - let is_indexing = store.is_indexing(&package); - let latest_error = store.latest_error_for_package(&package); - - if !is_indexing && latest_error.is_none() { - return Empty.into_any(); - } - - h_flex().gap_2().children(children).into_any_element() -} - #[derive(Debug, Clone, Serialize, Deserialize)] struct CopyMetadata { creases: Vec, diff --git a/crates/assistant_slash_commands/Cargo.toml b/crates/assistant_slash_commands/Cargo.toml index f703a753f5d261f4151d0d6a47eb3753fd18afb8..c054c3ced84825bcd131bdd76644c00595c4c4a9 100644 --- a/crates/assistant_slash_commands/Cargo.toml +++ b/crates/assistant_slash_commands/Cargo.toml @@ -27,7 +27,6 @@ globset.workspace = true gpui.workspace = true html_to_markdown.workspace = true http_client.workspace = true -indexed_docs.workspace = true language.workspace = true project.workspace = true prompt_store.workspace = true diff --git a/crates/assistant_slash_commands/src/assistant_slash_commands.rs b/crates/assistant_slash_commands/src/assistant_slash_commands.rs index fa5dd8b683d4404365db252e27f9e8e30db6ca30..fb00a912197e07942a67ad92418b85c4920ad66b 100644 --- a/crates/assistant_slash_commands/src/assistant_slash_commands.rs +++ b/crates/assistant_slash_commands/src/assistant_slash_commands.rs @@ -3,7 +3,6 @@ mod context_server_command; mod default_command; mod delta_command; mod diagnostics_command; -mod docs_command; mod fetch_command; mod file_command; mod now_command; @@ -18,7 +17,6 @@ pub use crate::context_server_command::*; pub use crate::default_command::*; pub use crate::delta_command::*; pub use crate::diagnostics_command::*; -pub use crate::docs_command::*; pub use crate::fetch_command::*; pub use crate::file_command::*; pub use crate::now_command::*; diff --git a/crates/assistant_slash_commands/src/docs_command.rs b/crates/assistant_slash_commands/src/docs_command.rs deleted file mode 100644 index bd87c72849e1eb54ca782d978f319676c1e8b3fe..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/src/docs_command.rs +++ /dev/null @@ -1,543 +0,0 @@ -use std::path::Path; -use std::sync::Arc; -use std::sync::atomic::AtomicBool; -use std::time::Duration; - -use anyhow::{Context as _, Result, anyhow, bail}; -use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, - SlashCommandResult, -}; -use gpui::{App, BackgroundExecutor, Entity, Task, WeakEntity}; -use indexed_docs::{ - DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName, - ProviderId, -}; -use language::{BufferSnapshot, LspAdapterDelegate}; -use project::{Project, ProjectPath}; -use ui::prelude::*; -use util::{ResultExt, maybe}; -use workspace::Workspace; - -pub struct DocsSlashCommand; - -impl DocsSlashCommand { - pub const NAME: &'static str = "docs"; - - fn path_to_cargo_toml(project: Entity, cx: &mut App) -> Option> { - let worktree = project.read(cx).worktrees(cx).next()?; - let worktree = worktree.read(cx); - let entry = worktree.entry_for_path("Cargo.toml")?; - let path = ProjectPath { - worktree_id: worktree.id(), - path: entry.path.clone(), - }; - Some(Arc::from( - project.read(cx).absolute_path(&path, cx)?.as_path(), - )) - } - - /// Ensures that the indexed doc providers for Rust are registered. - /// - /// Ideally we would do this sooner, but we need to wait until we're able to - /// access the workspace so we can read the project. - fn ensure_rust_doc_providers_are_registered( - &self, - workspace: Option>, - cx: &mut App, - ) { - let indexed_docs_registry = IndexedDocsRegistry::global(cx); - if indexed_docs_registry - .get_provider_store(LocalRustdocProvider::id()) - .is_none() - { - let index_provider_deps = maybe!({ - let workspace = workspace - .as_ref() - .context("no workspace")? - .upgrade() - .context("workspace dropped")?; - let project = workspace.read(cx).project().clone(); - let fs = project.read(cx).fs().clone(); - let cargo_workspace_root = Self::path_to_cargo_toml(project, cx) - .and_then(|path| path.parent().map(|path| path.to_path_buf())) - .context("no Cargo workspace root found")?; - - anyhow::Ok((fs, cargo_workspace_root)) - }); - - if let Some((fs, cargo_workspace_root)) = index_provider_deps.log_err() { - indexed_docs_registry.register_provider(Box::new(LocalRustdocProvider::new( - fs, - cargo_workspace_root, - ))); - } - } - - if indexed_docs_registry - .get_provider_store(DocsDotRsProvider::id()) - .is_none() - { - let http_client = maybe!({ - let workspace = workspace - .as_ref() - .context("no workspace")? - .upgrade() - .context("workspace was dropped")?; - let project = workspace.read(cx).project().clone(); - anyhow::Ok(project.read(cx).client().http_client()) - }); - - if let Some(http_client) = http_client.log_err() { - indexed_docs_registry - .register_provider(Box::new(DocsDotRsProvider::new(http_client))); - } - } - } - - /// Runs just-in-time indexing for a given package, in case the slash command - /// is run without any entries existing in the index. - fn run_just_in_time_indexing( - store: Arc, - key: String, - package: PackageName, - executor: BackgroundExecutor, - ) -> Task<()> { - executor.clone().spawn(async move { - let (prefix, needs_full_index) = if let Some((prefix, _)) = key.split_once('*') { - // If we have a wildcard in the search, we want to wait until - // we've completely finished indexing so we get a full set of - // results for the wildcard. - (prefix.to_string(), true) - } else { - (key, false) - }; - - // If we already have some entries, we assume that we've indexed the package before - // and don't need to do it again. - let has_any_entries = store - .any_with_prefix(prefix.clone()) - .await - .unwrap_or_default(); - if has_any_entries { - return (); - }; - - let index_task = store.clone().index(package.clone()); - - if needs_full_index { - _ = index_task.await; - } else { - loop { - executor.timer(Duration::from_millis(200)).await; - - if store - .any_with_prefix(prefix.clone()) - .await - .unwrap_or_default() - || !store.is_indexing(&package) - { - break; - } - } - } - }) - } -} - -impl SlashCommand for DocsSlashCommand { - fn name(&self) -> String { - Self::NAME.into() - } - - fn description(&self) -> String { - "insert docs".into() - } - - fn menu_text(&self) -> String { - "Insert Documentation".into() - } - - fn requires_argument(&self) -> bool { - true - } - - fn complete_argument( - self: Arc, - arguments: &[String], - _cancel: Arc, - workspace: Option>, - _: &mut Window, - cx: &mut App, - ) -> Task>> { - self.ensure_rust_doc_providers_are_registered(workspace, cx); - - let indexed_docs_registry = IndexedDocsRegistry::global(cx); - let args = DocsSlashCommandArgs::parse(arguments); - let store = args - .provider() - .context("no docs provider specified") - .and_then(|provider| IndexedDocsStore::try_global(provider, cx)); - cx.background_spawn(async move { - fn build_completions(items: Vec) -> Vec { - items - .into_iter() - .map(|item| ArgumentCompletion { - label: item.clone().into(), - new_text: item.to_string(), - after_completion: assistant_slash_command::AfterCompletion::Run, - replace_previous_arguments: false, - }) - .collect() - } - - match args { - DocsSlashCommandArgs::NoProvider => { - let providers = indexed_docs_registry.list_providers(); - if providers.is_empty() { - return Ok(vec![ArgumentCompletion { - label: "No available docs providers.".into(), - new_text: String::new(), - after_completion: false.into(), - replace_previous_arguments: false, - }]); - } - - Ok(providers - .into_iter() - .map(|provider| ArgumentCompletion { - label: provider.to_string().into(), - new_text: provider.to_string(), - after_completion: false.into(), - replace_previous_arguments: false, - }) - .collect()) - } - DocsSlashCommandArgs::SearchPackageDocs { - provider, - package, - index, - } => { - let store = store?; - - if index { - // We don't need to hold onto this task, as the `IndexedDocsStore` will hold it - // until it completes. - drop(store.clone().index(package.as_str().into())); - } - - let suggested_packages = store.clone().suggest_packages().await?; - let search_results = store.search(package).await; - - let mut items = build_completions(search_results); - let workspace_crate_completions = suggested_packages - .into_iter() - .filter(|package_name| { - !items - .iter() - .any(|item| item.label.text() == package_name.as_ref()) - }) - .map(|package_name| ArgumentCompletion { - label: format!("{package_name} (unindexed)").into(), - new_text: format!("{package_name}"), - after_completion: true.into(), - replace_previous_arguments: false, - }) - .collect::>(); - items.extend(workspace_crate_completions); - - if items.is_empty() { - return Ok(vec![ArgumentCompletion { - label: format!( - "Enter a {package_term} name.", - package_term = package_term(&provider) - ) - .into(), - new_text: provider.to_string(), - after_completion: false.into(), - replace_previous_arguments: false, - }]); - } - - Ok(items) - } - DocsSlashCommandArgs::SearchItemDocs { item_path, .. } => { - let store = store?; - let items = store.search(item_path).await; - Ok(build_completions(items)) - } - } - }) - } - - fn run( - self: Arc, - arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - _workspace: WeakEntity, - _delegate: Option>, - _: &mut Window, - cx: &mut App, - ) -> Task { - if arguments.is_empty() { - return Task::ready(Err(anyhow!("missing an argument"))); - }; - - let args = DocsSlashCommandArgs::parse(arguments); - let executor = cx.background_executor().clone(); - let task = cx.background_spawn({ - let store = args - .provider() - .context("no docs provider specified") - .and_then(|provider| IndexedDocsStore::try_global(provider, cx)); - async move { - let (provider, key) = match args.clone() { - DocsSlashCommandArgs::NoProvider => bail!("no docs provider specified"), - DocsSlashCommandArgs::SearchPackageDocs { - provider, package, .. - } => (provider, package), - DocsSlashCommandArgs::SearchItemDocs { - provider, - item_path, - .. - } => (provider, item_path), - }; - - if key.trim().is_empty() { - bail!( - "no {package_term} name provided", - package_term = package_term(&provider) - ); - } - - let store = store?; - - if let Some(package) = args.package() { - Self::run_just_in_time_indexing(store.clone(), key.clone(), package, executor) - .await; - } - - let (text, ranges) = if let Some((prefix, _)) = key.split_once('*') { - let docs = store.load_many_by_prefix(prefix.to_string()).await?; - - let mut text = String::new(); - let mut ranges = Vec::new(); - - for (key, docs) in docs { - let prev_len = text.len(); - - text.push_str(&docs.0); - text.push_str("\n"); - ranges.push((key, prev_len..text.len())); - text.push_str("\n"); - } - - (text, ranges) - } else { - let item_docs = store.load(key.clone()).await?; - let text = item_docs.to_string(); - let range = 0..text.len(); - - (text, vec![(key, range)]) - }; - - anyhow::Ok((provider, text, ranges)) - } - }); - - cx.foreground_executor().spawn(async move { - let (provider, text, ranges) = task.await?; - Ok(SlashCommandOutput { - text, - sections: ranges - .into_iter() - .map(|(key, range)| SlashCommandOutputSection { - range, - icon: IconName::FileDoc, - label: format!("docs ({provider}): {key}",).into(), - metadata: None, - }) - .collect(), - run_commands_in_text: false, - } - .to_event_stream()) - }) - } -} - -fn is_item_path_delimiter(char: char) -> bool { - !char.is_alphanumeric() && char != '-' && char != '_' -} - -#[derive(Debug, PartialEq, Clone)] -pub enum DocsSlashCommandArgs { - NoProvider, - SearchPackageDocs { - provider: ProviderId, - package: String, - index: bool, - }, - SearchItemDocs { - provider: ProviderId, - package: String, - item_path: String, - }, -} - -impl DocsSlashCommandArgs { - pub fn parse(arguments: &[String]) -> Self { - let Some(provider) = arguments - .get(0) - .cloned() - .filter(|arg| !arg.trim().is_empty()) - else { - return Self::NoProvider; - }; - let provider = ProviderId(provider.into()); - let Some(argument) = arguments.get(1) else { - return Self::NoProvider; - }; - - if let Some((package, rest)) = argument.split_once(is_item_path_delimiter) { - if rest.trim().is_empty() { - Self::SearchPackageDocs { - provider, - package: package.to_owned(), - index: true, - } - } else { - Self::SearchItemDocs { - provider, - package: package.to_owned(), - item_path: argument.to_owned(), - } - } - } else { - Self::SearchPackageDocs { - provider, - package: argument.to_owned(), - index: false, - } - } - } - - pub fn provider(&self) -> Option { - match self { - Self::NoProvider => None, - Self::SearchPackageDocs { provider, .. } | Self::SearchItemDocs { provider, .. } => { - Some(provider.clone()) - } - } - } - - pub fn package(&self) -> Option { - match self { - Self::NoProvider => None, - Self::SearchPackageDocs { package, .. } | Self::SearchItemDocs { package, .. } => { - Some(package.as_str().into()) - } - } - } -} - -/// Returns the term used to refer to a package. -fn package_term(provider: &ProviderId) -> &'static str { - if provider == &DocsDotRsProvider::id() || provider == &LocalRustdocProvider::id() { - return "crate"; - } - - "package" -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_docs_slash_command_args() { - assert_eq!( - DocsSlashCommandArgs::parse(&["".to_string()]), - DocsSlashCommandArgs::NoProvider - ); - assert_eq!( - DocsSlashCommandArgs::parse(&["rustdoc".to_string()]), - DocsSlashCommandArgs::NoProvider - ); - - assert_eq!( - DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "".to_string()]), - DocsSlashCommandArgs::SearchPackageDocs { - provider: ProviderId("rustdoc".into()), - package: "".into(), - index: false - } - ); - assert_eq!( - DocsSlashCommandArgs::parse(&["gleam".to_string(), "".to_string()]), - DocsSlashCommandArgs::SearchPackageDocs { - provider: ProviderId("gleam".into()), - package: "".into(), - index: false - } - ); - - assert_eq!( - DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui".to_string()]), - DocsSlashCommandArgs::SearchPackageDocs { - provider: ProviderId("rustdoc".into()), - package: "gpui".into(), - index: false, - } - ); - assert_eq!( - DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib".to_string()]), - DocsSlashCommandArgs::SearchPackageDocs { - provider: ProviderId("gleam".into()), - package: "gleam_stdlib".into(), - index: false - } - ); - - // Adding an item path delimiter indicates we can start indexing. - assert_eq!( - DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui:".to_string()]), - DocsSlashCommandArgs::SearchPackageDocs { - provider: ProviderId("rustdoc".into()), - package: "gpui".into(), - index: true, - } - ); - assert_eq!( - DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib/".to_string()]), - DocsSlashCommandArgs::SearchPackageDocs { - provider: ProviderId("gleam".into()), - package: "gleam_stdlib".into(), - index: true - } - ); - - assert_eq!( - DocsSlashCommandArgs::parse(&[ - "rustdoc".to_string(), - "gpui::foo::bar::Baz".to_string() - ]), - DocsSlashCommandArgs::SearchItemDocs { - provider: ProviderId("rustdoc".into()), - package: "gpui".into(), - item_path: "gpui::foo::bar::Baz".into() - } - ); - assert_eq!( - DocsSlashCommandArgs::parse(&[ - "gleam".to_string(), - "gleam_stdlib/gleam/int".to_string() - ]), - DocsSlashCommandArgs::SearchItemDocs { - provider: ProviderId("gleam".into()), - package: "gleam_stdlib".into(), - item_path: "gleam_stdlib/gleam/int".into() - } - ); - } -} diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index 917739759f2ab0dcbfe012b1d774a8c9f11ca96b..6a24e3ba3f496bd0f0b89d61e9125b29ecae0204 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -28,7 +28,6 @@ pub struct ExtensionHostProxy { snippet_proxy: RwLock>>, slash_command_proxy: RwLock>>, context_server_proxy: RwLock>>, - indexed_docs_provider_proxy: RwLock>>, debug_adapter_provider_proxy: RwLock>>, } @@ -54,7 +53,6 @@ impl ExtensionHostProxy { snippet_proxy: RwLock::default(), slash_command_proxy: RwLock::default(), context_server_proxy: RwLock::default(), - indexed_docs_provider_proxy: RwLock::default(), debug_adapter_provider_proxy: RwLock::default(), } } @@ -87,14 +85,6 @@ impl ExtensionHostProxy { self.context_server_proxy.write().replace(Arc::new(proxy)); } - pub fn register_indexed_docs_provider_proxy( - &self, - proxy: impl ExtensionIndexedDocsProviderProxy, - ) { - self.indexed_docs_provider_proxy - .write() - .replace(Arc::new(proxy)); - } pub fn register_debug_adapter_proxy(&self, proxy: impl ExtensionDebugAdapterProviderProxy) { self.debug_adapter_provider_proxy .write() @@ -408,30 +398,6 @@ impl ExtensionContextServerProxy for ExtensionHostProxy { } } -pub trait ExtensionIndexedDocsProviderProxy: Send + Sync + 'static { - fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc); - - fn unregister_indexed_docs_provider(&self, provider_id: Arc); -} - -impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy { - fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc) { - let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else { - return; - }; - - proxy.register_indexed_docs_provider(extension, provider_id) - } - - fn unregister_indexed_docs_provider(&self, provider_id: Arc) { - let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else { - return; - }; - - proxy.unregister_indexed_docs_provider(provider_id) - } -} - pub trait ExtensionDebugAdapterProviderProxy: Send + Sync + 'static { fn register_debug_adapter( &self, diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 5852b3e3fc32601e8d9527e02d593e02cd66f3c6..f5296198b06ffeeb83dd21be35d27be6b4387294 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -84,8 +84,6 @@ pub struct ExtensionManifest { #[serde(default)] pub slash_commands: BTreeMap, SlashCommandManifestEntry>, #[serde(default)] - pub indexed_docs_providers: BTreeMap, IndexedDocsProviderEntry>, - #[serde(default)] pub snippets: Option, #[serde(default)] pub capabilities: Vec, @@ -195,9 +193,6 @@ pub struct SlashCommandManifestEntry { pub requires_argument: bool, } -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct IndexedDocsProviderEntry {} - #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct DebugAdapterManifestEntry { pub schema_path: Option, @@ -271,7 +266,6 @@ fn manifest_from_old_manifest( language_servers: Default::default(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), - indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: Vec::new(), debug_adapters: Default::default(), @@ -304,7 +298,6 @@ mod tests { language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), - indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: vec![], debug_adapters: Default::default(), diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index ab4a9cddb0fa13421677772d1c07c1a8d9234d76..d6c0501efdacff2a9eaf542695ed44325908ea56 100644 --- a/crates/extension_cli/src/main.rs +++ b/crates/extension_cli/src/main.rs @@ -144,10 +144,6 @@ fn extension_provides(manifest: &ExtensionManifest) -> BTreeSet ExtensionManifest { .collect(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), - indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: vec![ExtensionCapability::ProcessExec( extension::ProcessExecCapability { diff --git a/crates/extension_host/src/capability_granter.rs b/crates/extension_host/src/capability_granter.rs index c77e5ecba15b5e10caa331d3b6ee3976b899ed21..5a2093c1dd02008b9b7ee8f0155b3aa675806a77 100644 --- a/crates/extension_host/src/capability_granter.rs +++ b/crates/extension_host/src/capability_granter.rs @@ -108,7 +108,6 @@ mod tests { language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), - indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: vec![], debug_adapters: Default::default(), diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 67baf4e692a26294cf673d2769b4b647d73811b9..46deacfe69f1e00fca3c4b158f8760276339d46b 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -16,9 +16,9 @@ pub use extension::ExtensionManifest; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; use extension::{ ExtensionContextServerProxy, ExtensionDebugAdapterProviderProxy, ExtensionEvents, - ExtensionGrammarProxy, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy, - ExtensionLanguageProxy, ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, - ExtensionSnippetProxy, ExtensionThemeProxy, + ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy, + ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy, + ExtensionThemeProxy, }; use fs::{Fs, RemoveOptions}; use futures::future::join_all; @@ -1192,10 +1192,6 @@ impl ExtensionStore { for (command_name, _) in &extension.manifest.slash_commands { self.proxy.unregister_slash_command(command_name.clone()); } - for (provider_id, _) in &extension.manifest.indexed_docs_providers { - self.proxy - .unregister_indexed_docs_provider(provider_id.clone()); - } } self.wasm_extensions @@ -1399,11 +1395,6 @@ impl ExtensionStore { .register_context_server(extension.clone(), id.clone(), cx); } - for (provider_id, _provider) in &manifest.indexed_docs_providers { - this.proxy - .register_indexed_docs_provider(extension.clone(), provider_id.clone()); - } - for (debug_adapter, meta) in &manifest.debug_adapters { let mut path = root_dir.clone(); path.push(Path::new(manifest.id.as_ref())); diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index c31774c20d3e94f829e8de5d6ca822228735ca18..347a610439c98ae020a7ebf190dd9e1a603df5a1 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -160,7 +160,6 @@ async fn test_extension_store(cx: &mut TestAppContext) { language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), - indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: Vec::new(), debug_adapters: Default::default(), @@ -191,7 +190,6 @@ async fn test_extension_store(cx: &mut TestAppContext) { language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), - indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: Vec::new(), debug_adapters: Default::default(), @@ -371,7 +369,6 @@ async fn test_extension_store(cx: &mut TestAppContext) { language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), - indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: Vec::new(), debug_adapters: Default::default(), diff --git a/crates/indexed_docs/Cargo.toml b/crates/indexed_docs/Cargo.toml deleted file mode 100644 index eb269ad939b59394f12ccceba941585f6dec3ca7..0000000000000000000000000000000000000000 --- a/crates/indexed_docs/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "indexed_docs" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/indexed_docs.rs" - -[dependencies] -anyhow.workspace = true -async-trait.workspace = true -cargo_metadata.workspace = true -collections.workspace = true -derive_more.workspace = true -extension.workspace = true -fs.workspace = true -futures.workspace = true -fuzzy.workspace = true -gpui.workspace = true -heed.workspace = true -html_to_markdown.workspace = true -http_client.workspace = true -indexmap.workspace = true -parking_lot.workspace = true -paths.workspace = true -serde.workspace = true -strum.workspace = true -util.workspace = true -workspace-hack.workspace = true - -[dev-dependencies] -indoc.workspace = true -pretty_assertions.workspace = true diff --git a/crates/indexed_docs/LICENSE-GPL b/crates/indexed_docs/LICENSE-GPL deleted file mode 120000 index 89e542f750cd3860a0598eff0dc34b56d7336dc4..0000000000000000000000000000000000000000 --- a/crates/indexed_docs/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/indexed_docs/src/extension_indexed_docs_provider.rs b/crates/indexed_docs/src/extension_indexed_docs_provider.rs deleted file mode 100644 index c77ea4066d2b46f0377d7f64c7d514fd3a95872d..0000000000000000000000000000000000000000 --- a/crates/indexed_docs/src/extension_indexed_docs_provider.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; - -use anyhow::Result; -use async_trait::async_trait; -use extension::{Extension, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy}; -use gpui::App; - -use crate::{ - IndexedDocsDatabase, IndexedDocsProvider, IndexedDocsRegistry, PackageName, ProviderId, -}; - -pub fn init(cx: &mut App) { - let proxy = ExtensionHostProxy::default_global(cx); - proxy.register_indexed_docs_provider_proxy(IndexedDocsRegistryProxy { - indexed_docs_registry: IndexedDocsRegistry::global(cx), - }); -} - -struct IndexedDocsRegistryProxy { - indexed_docs_registry: Arc, -} - -impl ExtensionIndexedDocsProviderProxy for IndexedDocsRegistryProxy { - fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc) { - self.indexed_docs_registry - .register_provider(Box::new(ExtensionIndexedDocsProvider::new( - extension, - ProviderId(provider_id), - ))); - } - - fn unregister_indexed_docs_provider(&self, provider_id: Arc) { - self.indexed_docs_registry - .unregister_provider(&ProviderId(provider_id)); - } -} - -pub struct ExtensionIndexedDocsProvider { - extension: Arc, - id: ProviderId, -} - -impl ExtensionIndexedDocsProvider { - pub fn new(extension: Arc, id: ProviderId) -> Self { - Self { extension, id } - } -} - -#[async_trait] -impl IndexedDocsProvider for ExtensionIndexedDocsProvider { - fn id(&self) -> ProviderId { - self.id.clone() - } - - fn database_path(&self) -> PathBuf { - let mut database_path = PathBuf::from(self.extension.work_dir().as_ref()); - database_path.push("docs"); - database_path.push(format!("{}.0.mdb", self.id)); - - database_path - } - - async fn suggest_packages(&self) -> Result> { - let packages = self - .extension - .suggest_docs_packages(self.id.0.clone()) - .await?; - - Ok(packages - .into_iter() - .map(|package| PackageName::from(package.as_str())) - .collect()) - } - - async fn index(&self, package: PackageName, database: Arc) -> Result<()> { - self.extension - .index_docs(self.id.0.clone(), package.as_ref().into(), database) - .await - } -} diff --git a/crates/indexed_docs/src/indexed_docs.rs b/crates/indexed_docs/src/indexed_docs.rs deleted file mode 100644 index 97538329d4d6265c7587209c679f4e3fa041ce35..0000000000000000000000000000000000000000 --- a/crates/indexed_docs/src/indexed_docs.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod extension_indexed_docs_provider; -mod providers; -mod registry; -mod store; - -use gpui::App; - -pub use crate::extension_indexed_docs_provider::ExtensionIndexedDocsProvider; -pub use crate::providers::rustdoc::*; -pub use crate::registry::*; -pub use crate::store::*; - -pub fn init(cx: &mut App) { - IndexedDocsRegistry::init_global(cx); - extension_indexed_docs_provider::init(cx); -} diff --git a/crates/indexed_docs/src/providers.rs b/crates/indexed_docs/src/providers.rs deleted file mode 100644 index c6505a2ab667724c83d3f7bed3c1ca16a1423bc5..0000000000000000000000000000000000000000 --- a/crates/indexed_docs/src/providers.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod rustdoc; diff --git a/crates/indexed_docs/src/providers/rustdoc.rs b/crates/indexed_docs/src/providers/rustdoc.rs deleted file mode 100644 index ac6dc3a10bb3f70f7329b399287124e0417dc0f4..0000000000000000000000000000000000000000 --- a/crates/indexed_docs/src/providers/rustdoc.rs +++ /dev/null @@ -1,291 +0,0 @@ -mod item; -mod to_markdown; - -use cargo_metadata::MetadataCommand; -use futures::future::BoxFuture; -pub use item::*; -use parking_lot::RwLock; -pub use to_markdown::convert_rustdoc_to_markdown; - -use std::collections::BTreeSet; -use std::path::PathBuf; -use std::sync::{Arc, LazyLock}; -use std::time::{Duration, Instant}; - -use anyhow::{Context as _, Result, bail}; -use async_trait::async_trait; -use collections::{HashSet, VecDeque}; -use fs::Fs; -use futures::{AsyncReadExt, FutureExt}; -use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; - -use crate::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId}; - -#[derive(Debug)] -struct RustdocItemWithHistory { - pub item: RustdocItem, - #[cfg(debug_assertions)] - pub history: Vec, -} - -pub struct LocalRustdocProvider { - fs: Arc, - cargo_workspace_root: PathBuf, -} - -impl LocalRustdocProvider { - pub fn id() -> ProviderId { - ProviderId("rustdoc".into()) - } - - pub fn new(fs: Arc, cargo_workspace_root: PathBuf) -> Self { - Self { - fs, - cargo_workspace_root, - } - } -} - -#[async_trait] -impl IndexedDocsProvider for LocalRustdocProvider { - fn id(&self) -> ProviderId { - Self::id() - } - - fn database_path(&self) -> PathBuf { - paths::data_dir().join("docs/rust/rustdoc-db.1.mdb") - } - - async fn suggest_packages(&self) -> Result> { - static WORKSPACE_CRATES: LazyLock, Instant)>>> = - LazyLock::new(|| RwLock::new(None)); - - if let Some((crates, fetched_at)) = &*WORKSPACE_CRATES.read() { - if fetched_at.elapsed() < Duration::from_secs(300) { - return Ok(crates.iter().cloned().collect()); - } - } - - let workspace = MetadataCommand::new() - .manifest_path(self.cargo_workspace_root.join("Cargo.toml")) - .exec() - .context("failed to load cargo metadata")?; - - let workspace_crates = workspace - .packages - .into_iter() - .map(|package| PackageName::from(package.name.as_str())) - .collect::>(); - - *WORKSPACE_CRATES.write() = Some((workspace_crates.clone(), Instant::now())); - - Ok(workspace_crates.into_iter().collect()) - } - - async fn index(&self, package: PackageName, database: Arc) -> Result<()> { - index_rustdoc(package, database, { - move |crate_name, item| { - let fs = self.fs.clone(); - let cargo_workspace_root = self.cargo_workspace_root.clone(); - let crate_name = crate_name.clone(); - let item = item.cloned(); - async move { - let target_doc_path = cargo_workspace_root.join("target/doc"); - let mut local_cargo_doc_path = target_doc_path.join(crate_name.as_ref().replace('-', "_")); - - if !fs.is_dir(&local_cargo_doc_path).await { - let cargo_doc_exists_at_all = fs.is_dir(&target_doc_path).await; - if cargo_doc_exists_at_all { - bail!( - "no docs directory for '{crate_name}'. if this is a valid crate name, try running `cargo doc`" - ); - } else { - bail!("no cargo doc directory. run `cargo doc`"); - } - } - - if let Some(item) = item { - local_cargo_doc_path.push(item.url_path()); - } else { - local_cargo_doc_path.push("index.html"); - } - - let Ok(contents) = fs.load(&local_cargo_doc_path).await else { - return Ok(None); - }; - - Ok(Some(contents)) - } - .boxed() - } - }) - .await - } -} - -pub struct DocsDotRsProvider { - http_client: Arc, -} - -impl DocsDotRsProvider { - pub fn id() -> ProviderId { - ProviderId("docs-rs".into()) - } - - pub fn new(http_client: Arc) -> Self { - Self { http_client } - } -} - -#[async_trait] -impl IndexedDocsProvider for DocsDotRsProvider { - fn id(&self) -> ProviderId { - Self::id() - } - - fn database_path(&self) -> PathBuf { - paths::data_dir().join("docs/rust/docs-rs-db.1.mdb") - } - - async fn suggest_packages(&self) -> Result> { - static POPULAR_CRATES: LazyLock> = LazyLock::new(|| { - include_str!("./rustdoc/popular_crates.txt") - .lines() - .filter(|line| !line.starts_with('#')) - .map(|line| PackageName::from(line.trim())) - .collect() - }); - - Ok(POPULAR_CRATES.clone()) - } - - async fn index(&self, package: PackageName, database: Arc) -> Result<()> { - index_rustdoc(package, database, { - move |crate_name, item| { - let http_client = self.http_client.clone(); - let crate_name = crate_name.clone(); - let item = item.cloned(); - async move { - let version = "latest"; - let path = format!( - "{crate_name}/{version}/{crate_name}{item_path}", - item_path = item - .map(|item| format!("/{}", item.url_path())) - .unwrap_or_default() - ); - - let mut response = http_client - .get( - &format!("https://docs.rs/{path}"), - AsyncBody::default(), - true, - ) - .await?; - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading docs.rs response body")?; - - if response.status().is_client_error() { - let text = String::from_utf8_lossy(body.as_slice()); - bail!( - "status error {}, response: {text:?}", - response.status().as_u16() - ); - } - - Ok(Some(String::from_utf8(body)?)) - } - .boxed() - } - }) - .await - } -} - -async fn index_rustdoc( - package: PackageName, - database: Arc, - fetch_page: impl Fn( - &PackageName, - Option<&RustdocItem>, - ) -> BoxFuture<'static, Result>> - + Send - + Sync, -) -> Result<()> { - let Some(package_root_content) = fetch_page(&package, None).await? else { - return Ok(()); - }; - - let (crate_root_markdown, items) = - convert_rustdoc_to_markdown(package_root_content.as_bytes())?; - - database - .insert(package.to_string(), crate_root_markdown) - .await?; - - let mut seen_items = HashSet::from_iter(items.clone()); - let mut items_to_visit: VecDeque = - VecDeque::from_iter(items.into_iter().map(|item| RustdocItemWithHistory { - item, - #[cfg(debug_assertions)] - history: Vec::new(), - })); - - while let Some(item_with_history) = items_to_visit.pop_front() { - let item = &item_with_history.item; - - let Some(result) = fetch_page(&package, Some(item)).await.with_context(|| { - #[cfg(debug_assertions)] - { - format!( - "failed to fetch {item:?}: {history:?}", - history = item_with_history.history - ) - } - - #[cfg(not(debug_assertions))] - { - format!("failed to fetch {item:?}") - } - })? - else { - continue; - }; - - let (markdown, referenced_items) = convert_rustdoc_to_markdown(result.as_bytes())?; - - database - .insert(format!("{package}::{}", item.display()), markdown) - .await?; - - let parent_item = item; - for mut item in referenced_items { - if seen_items.contains(&item) { - continue; - } - - seen_items.insert(item.clone()); - - item.path.extend(parent_item.path.clone()); - if parent_item.kind == RustdocItemKind::Mod { - item.path.push(parent_item.name.clone()); - } - - items_to_visit.push_back(RustdocItemWithHistory { - #[cfg(debug_assertions)] - history: { - let mut history = item_with_history.history.clone(); - history.push(item.url_path()); - history - }, - item, - }); - } - } - - Ok(()) -} diff --git a/crates/indexed_docs/src/providers/rustdoc/item.rs b/crates/indexed_docs/src/providers/rustdoc/item.rs deleted file mode 100644 index 7d9023ef3e1bcda298e7c1aaabcf9205f333ee23..0000000000000000000000000000000000000000 --- a/crates/indexed_docs/src/providers/rustdoc/item.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::sync::Arc; - -use serde::{Deserialize, Serialize}; -use strum::EnumIter; - -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize, EnumIter, -)] -#[serde(rename_all = "snake_case")] -pub enum RustdocItemKind { - Mod, - Macro, - Struct, - Enum, - Constant, - Trait, - Function, - TypeAlias, - AttributeMacro, - DeriveMacro, -} - -impl RustdocItemKind { - pub(crate) const fn class(&self) -> &'static str { - match self { - Self::Mod => "mod", - Self::Macro => "macro", - Self::Struct => "struct", - Self::Enum => "enum", - Self::Constant => "constant", - Self::Trait => "trait", - Self::Function => "fn", - Self::TypeAlias => "type", - Self::AttributeMacro => "attr", - Self::DeriveMacro => "derive", - } - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] -pub struct RustdocItem { - pub kind: RustdocItemKind, - /// The item path, up until the name of the item. - pub path: Vec>, - /// The name of the item. - pub name: Arc, -} - -impl RustdocItem { - pub fn display(&self) -> String { - let mut path_segments = self.path.clone(); - path_segments.push(self.name.clone()); - - path_segments.join("::") - } - - pub fn url_path(&self) -> String { - let name = &self.name; - let mut path_components = self.path.clone(); - - match self.kind { - RustdocItemKind::Mod => { - path_components.push(name.clone()); - path_components.push("index.html".into()); - } - RustdocItemKind::Macro - | RustdocItemKind::Struct - | RustdocItemKind::Enum - | RustdocItemKind::Constant - | RustdocItemKind::Trait - | RustdocItemKind::Function - | RustdocItemKind::TypeAlias - | RustdocItemKind::AttributeMacro - | RustdocItemKind::DeriveMacro => { - path_components - .push(format!("{kind}.{name}.html", kind = self.kind.class()).into()); - } - } - - path_components.join("/") - } -} diff --git a/crates/indexed_docs/src/providers/rustdoc/popular_crates.txt b/crates/indexed_docs/src/providers/rustdoc/popular_crates.txt deleted file mode 100644 index ce2c3d51d834ecca05ebdde23c697b19e356a478..0000000000000000000000000000000000000000 --- a/crates/indexed_docs/src/providers/rustdoc/popular_crates.txt +++ /dev/null @@ -1,252 +0,0 @@ -# A list of the most popular Rust crates. -# Sourced from https://lib.rs/std. -serde -serde_json -syn -clap -thiserror -rand -log -tokio -anyhow -regex -quote -proc-macro2 -base64 -itertools -chrono -lazy_static -once_cell -libc -reqwest -futures -bitflags -tracing -url -bytes -toml -tempfile -uuid -indexmap -env_logger -num-traits -async-trait -sha2 -hex -tracing-subscriber -http -parking_lot -cfg-if -futures-util -cc -hashbrown -rayon -hyper -getrandom -semver -strum -flate2 -tokio-util -smallvec -criterion -paste -heck -rand_core -nom -rustls -nix -glob -time -byteorder -strum_macros -serde_yaml -wasm-bindgen -ahash -either -num_cpus -rand_chacha -prost -percent-encoding -pin-project-lite -tokio-stream -bincode -walkdir -bindgen -axum -windows-sys -futures-core -ring -digest -num-bigint -rustls-pemfile -serde_with -crossbeam-channel -tokio-rustls -hmac -fastrand -dirs -zeroize -socket2 -pin-project -tower -derive_more -memchr -toml_edit -static_assertions -pretty_assertions -js-sys -convert_case -unicode-width -pkg-config -itoa -colored -rustc-hash -darling -mime -web-sys -image -bytemuck -which -sha1 -dashmap -arrayvec -fnv -tonic -humantime -libloading -winapi -rustc_version -http-body -indoc -num -home -serde_urlencoded -http-body-util -unicode-segmentation -num-integer -webpki-roots -phf -futures-channel -indicatif -petgraph -ordered-float -strsim -zstd -console -encoding_rs -wasm-bindgen-futures -urlencoding -subtle -crc32fast -slab -rustix -predicates -spin -hyper-rustls -backtrace -rustversion -mio -scopeguard -proc-macro-error -hyper-util -ryu -prost-types -textwrap -memmap2 -zip -zerocopy -generic-array -tar -pyo3 -async-stream -quick-xml -memoffset -csv -crossterm -windows -num_enum -tokio-tungstenite -crossbeam-utils -async-channel -lru -aes -futures-lite -tracing-core -prettyplease -httparse -serde_bytes -tracing-log -tower-service -cargo_metadata -pest -mime_guess -tower-http -data-encoding -native-tls -prost-build -proptest -derivative -serial_test -libm -half -futures-io -bitvec -rustls-native-certs -ureq -object -anstyle -tonic-build -form_urlencoded -num-derive -pest_derive -schemars -proc-macro-crate -rstest -futures-executor -assert_cmd -termcolor -serde_repr -ctrlc -sha3 -clap_complete -flume -mockall -ipnet -aho-corasick -atty -signal-hook -async-std -filetime -num-complex -opentelemetry -cmake -arc-swap -derive_builder -async-recursion -dyn-clone -bumpalo -fs_extra -git2 -sysinfo -shlex -instant -approx -rmp-serde -rand_distr -rustls-pki-types -maplit -sqlx -blake3 -hyper-tls -dotenvy -jsonwebtoken -openssl-sys -crossbeam -camino -winreg -config -rsa -bit-vec -chrono-tz -async-lock -bstr diff --git a/crates/indexed_docs/src/providers/rustdoc/to_markdown.rs b/crates/indexed_docs/src/providers/rustdoc/to_markdown.rs deleted file mode 100644 index 87e3863728c5822c19edf4b1dc79283d95b40481..0000000000000000000000000000000000000000 --- a/crates/indexed_docs/src/providers/rustdoc/to_markdown.rs +++ /dev/null @@ -1,618 +0,0 @@ -use std::cell::RefCell; -use std::io::Read; -use std::rc::Rc; - -use anyhow::Result; -use html_to_markdown::markdown::{ - HeadingHandler, ListHandler, ParagraphHandler, StyledTextHandler, TableHandler, -}; -use html_to_markdown::{ - HandleTag, HandlerOutcome, HtmlElement, MarkdownWriter, StartTagOutcome, TagHandler, - convert_html_to_markdown, -}; -use indexmap::IndexSet; -use strum::IntoEnumIterator; - -use crate::{RustdocItem, RustdocItemKind}; - -/// Converts the provided rustdoc HTML to Markdown. -pub fn convert_rustdoc_to_markdown(html: impl Read) -> Result<(String, Vec)> { - let item_collector = Rc::new(RefCell::new(RustdocItemCollector::new())); - - let mut handlers: Vec = vec![ - Rc::new(RefCell::new(ParagraphHandler)), - Rc::new(RefCell::new(HeadingHandler)), - Rc::new(RefCell::new(ListHandler)), - Rc::new(RefCell::new(TableHandler::new())), - Rc::new(RefCell::new(StyledTextHandler)), - Rc::new(RefCell::new(RustdocChromeRemover)), - Rc::new(RefCell::new(RustdocHeadingHandler)), - Rc::new(RefCell::new(RustdocCodeHandler)), - Rc::new(RefCell::new(RustdocItemHandler)), - item_collector.clone(), - ]; - - let markdown = convert_html_to_markdown(html, &mut handlers)?; - - let items = item_collector - .borrow() - .items - .iter() - .cloned() - .collect::>(); - - Ok((markdown, items)) -} - -pub struct RustdocHeadingHandler; - -impl HandleTag for RustdocHeadingHandler { - fn should_handle(&self, _tag: &str) -> bool { - // We're only handling text, so we don't need to visit any tags. - false - } - - fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome { - if writer.is_inside("h1") - || writer.is_inside("h2") - || writer.is_inside("h3") - || writer.is_inside("h4") - || writer.is_inside("h5") - || writer.is_inside("h6") - { - let text = text - .trim_matches(|char| char == '\n' || char == '\r') - .replace('\n', " "); - writer.push_str(&text); - - return HandlerOutcome::Handled; - } - - HandlerOutcome::NoOp - } -} - -pub struct RustdocCodeHandler; - -impl HandleTag for RustdocCodeHandler { - fn should_handle(&self, tag: &str) -> bool { - matches!(tag, "pre" | "code") - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - match tag.tag() { - "code" => { - if !writer.is_inside("pre") { - writer.push_str("`"); - } - } - "pre" => { - let classes = tag.classes(); - let is_rust = classes.iter().any(|class| class == "rust"); - let language = is_rust - .then_some("rs") - .or_else(|| { - classes.iter().find_map(|class| { - if let Some((_, language)) = class.split_once("language-") { - Some(language.trim()) - } else { - None - } - }) - }) - .unwrap_or(""); - - writer.push_str(&format!("\n\n```{language}\n")); - } - _ => {} - } - - StartTagOutcome::Continue - } - - fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) { - match tag.tag() { - "code" => { - if !writer.is_inside("pre") { - writer.push_str("`"); - } - } - "pre" => writer.push_str("\n```\n"), - _ => {} - } - } - - fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome { - if writer.is_inside("pre") { - writer.push_str(text); - return HandlerOutcome::Handled; - } - - HandlerOutcome::NoOp - } -} - -const RUSTDOC_ITEM_NAME_CLASS: &str = "item-name"; - -pub struct RustdocItemHandler; - -impl RustdocItemHandler { - /// Returns whether we're currently inside of an `.item-name` element, which - /// rustdoc uses to display Rust items in a list. - fn is_inside_item_name(writer: &MarkdownWriter) -> bool { - writer - .current_element_stack() - .iter() - .any(|element| element.has_class(RUSTDOC_ITEM_NAME_CLASS)) - } -} - -impl HandleTag for RustdocItemHandler { - fn should_handle(&self, tag: &str) -> bool { - matches!(tag, "div" | "span") - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - match tag.tag() { - "div" | "span" => { - if Self::is_inside_item_name(writer) && tag.has_class("stab") { - writer.push_str(" ["); - } - } - _ => {} - } - - StartTagOutcome::Continue - } - - fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) { - match tag.tag() { - "div" | "span" => { - if tag.has_class(RUSTDOC_ITEM_NAME_CLASS) { - writer.push_str(": "); - } - - if Self::is_inside_item_name(writer) && tag.has_class("stab") { - writer.push_str("]"); - } - } - _ => {} - } - } - - fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome { - if Self::is_inside_item_name(writer) - && !writer.is_inside("span") - && !writer.is_inside("code") - { - writer.push_str(&format!("`{text}`")); - return HandlerOutcome::Handled; - } - - HandlerOutcome::NoOp - } -} - -pub struct RustdocChromeRemover; - -impl HandleTag for RustdocChromeRemover { - fn should_handle(&self, tag: &str) -> bool { - matches!( - tag, - "head" | "script" | "nav" | "summary" | "button" | "a" | "div" | "span" - ) - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - _writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - match tag.tag() { - "head" | "script" | "nav" => return StartTagOutcome::Skip, - "summary" => { - if tag.has_class("hideme") { - return StartTagOutcome::Skip; - } - } - "button" => { - if tag.attr("id").as_deref() == Some("copy-path") { - return StartTagOutcome::Skip; - } - } - "a" => { - if tag.has_any_classes(&["anchor", "doc-anchor", "src"]) { - return StartTagOutcome::Skip; - } - } - "div" | "span" => { - if tag.has_any_classes(&["nav-container", "sidebar-elems", "out-of-band"]) { - return StartTagOutcome::Skip; - } - } - - _ => {} - } - - StartTagOutcome::Continue - } -} - -pub struct RustdocItemCollector { - pub items: IndexSet, -} - -impl RustdocItemCollector { - pub fn new() -> Self { - Self { - items: IndexSet::new(), - } - } - - fn parse_item(tag: &HtmlElement) -> Option { - if tag.tag() != "a" { - return None; - } - - let href = tag.attr("href")?; - if href.starts_with('#') || href.starts_with("https://") || href.starts_with("../") { - return None; - } - - for kind in RustdocItemKind::iter() { - if tag.has_class(kind.class()) { - let mut parts = href.trim_end_matches("/index.html").split('/'); - - if let Some(last_component) = parts.next_back() { - let last_component = match last_component.split_once('#') { - Some((component, _fragment)) => component, - None => last_component, - }; - - let name = last_component - .trim_start_matches(&format!("{}.", kind.class())) - .trim_end_matches(".html"); - - return Some(RustdocItem { - kind, - name: name.into(), - path: parts.map(Into::into).collect(), - }); - } - } - } - - None - } -} - -impl HandleTag for RustdocItemCollector { - fn should_handle(&self, tag: &str) -> bool { - tag == "a" - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - if tag.tag() == "a" { - let is_reexport = writer.current_element_stack().iter().any(|element| { - if let Some(id) = element.attr("id") { - id.starts_with("reexport.") || id.starts_with("method.") - } else { - false - } - }); - - if !is_reexport { - if let Some(item) = Self::parse_item(tag) { - self.items.insert(item); - } - } - } - - StartTagOutcome::Continue - } -} - -#[cfg(test)] -mod tests { - use html_to_markdown::{TagHandler, convert_html_to_markdown}; - use indoc::indoc; - use pretty_assertions::assert_eq; - - use super::*; - - fn rustdoc_handlers() -> Vec { - vec![ - Rc::new(RefCell::new(ParagraphHandler)), - Rc::new(RefCell::new(HeadingHandler)), - Rc::new(RefCell::new(ListHandler)), - Rc::new(RefCell::new(TableHandler::new())), - Rc::new(RefCell::new(StyledTextHandler)), - Rc::new(RefCell::new(RustdocChromeRemover)), - Rc::new(RefCell::new(RustdocHeadingHandler)), - Rc::new(RefCell::new(RustdocCodeHandler)), - Rc::new(RefCell::new(RustdocItemHandler)), - ] - } - - #[test] - fn test_main_heading_buttons_get_removed() { - let html = indoc! {r##" -
-

Crate serde

- - source · - -
- "##}; - let expected = indoc! {" - # Crate serde - "} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } - - #[test] - fn test_single_paragraph() { - let html = indoc! {r#" -

In particular, the last point is what sets axum apart from other frameworks. - axum doesn’t have its own middleware system but instead uses - tower::Service. This means axum gets timeouts, tracing, compression, - authorization, and more, for free. It also enables you to share middleware with - applications written using hyper or tonic.

- "#}; - let expected = indoc! {" - In particular, the last point is what sets `axum` apart from other frameworks. `axum` doesn’t have its own middleware system but instead uses `tower::Service`. This means `axum` gets timeouts, tracing, compression, authorization, and more, for free. It also enables you to share middleware with applications written using `hyper` or `tonic`. - "} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } - - #[test] - fn test_multiple_paragraphs() { - let html = indoc! {r##" -

§Serde

-

Serde is a framework for serializing and deserializing Rust data - structures efficiently and generically.

-

The Serde ecosystem consists of data structures that know how to serialize - and deserialize themselves along with data formats that know how to - serialize and deserialize other things. Serde provides the layer by which - these two groups interact with each other, allowing any supported data - structure to be serialized and deserialized using any supported data format.

-

See the Serde website https://serde.rs/ for additional documentation and - usage examples.

-

§Design

-

Where many other languages rely on runtime reflection for serializing data, - Serde is instead built on Rust’s powerful trait system. A data structure - that knows how to serialize and deserialize itself is one that implements - Serde’s Serialize and Deserialize traits (or uses Serde’s derive - attribute to automatically generate implementations at compile time). This - avoids any overhead of reflection or runtime type information. In fact in - many situations the interaction between data structure and data format can - be completely optimized away by the Rust compiler, leaving Serde - serialization to perform the same speed as a handwritten serializer for the - specific selection of data structure and data format.

- "##}; - let expected = indoc! {" - ## Serde - - Serde is a framework for _**ser**_ializing and _**de**_serializing Rust data structures efficiently and generically. - - The Serde ecosystem consists of data structures that know how to serialize and deserialize themselves along with data formats that know how to serialize and deserialize other things. Serde provides the layer by which these two groups interact with each other, allowing any supported data structure to be serialized and deserialized using any supported data format. - - See the Serde website https://serde.rs/ for additional documentation and usage examples. - - ### Design - - Where many other languages rely on runtime reflection for serializing data, Serde is instead built on Rust’s powerful trait system. A data structure that knows how to serialize and deserialize itself is one that implements Serde’s `Serialize` and `Deserialize` traits (or uses Serde’s derive attribute to automatically generate implementations at compile time). This avoids any overhead of reflection or runtime type information. In fact in many situations the interaction between data structure and data format can be completely optimized away by the Rust compiler, leaving Serde serialization to perform the same speed as a handwritten serializer for the specific selection of data structure and data format. - "} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } - - #[test] - fn test_styled_text() { - let html = indoc! {r#" -

This text is bolded.

-

This text is italicized.

- "#}; - let expected = indoc! {" - This text is **bolded**. - - This text is _italicized_. - "} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } - - #[test] - fn test_rust_code_block() { - let html = indoc! {r#" -
use axum::extract::{Path, Query, Json};
-            use std::collections::HashMap;
-
-            // `Path` gives you the path parameters and deserializes them.
-            async fn path(Path(user_id): Path<u32>) {}
-
-            // `Query` gives you the query parameters and deserializes them.
-            async fn query(Query(params): Query<HashMap<String, String>>) {}
-
-            // Buffer the request body and deserialize it as JSON into a
-            // `serde_json::Value`. `Json` supports any type that implements
-            // `serde::Deserialize`.
-            async fn json(Json(payload): Json<serde_json::Value>) {}
- "#}; - let expected = indoc! {" - ```rs - use axum::extract::{Path, Query, Json}; - use std::collections::HashMap; - - // `Path` gives you the path parameters and deserializes them. - async fn path(Path(user_id): Path) {} - - // `Query` gives you the query parameters and deserializes them. - async fn query(Query(params): Query>) {} - - // Buffer the request body and deserialize it as JSON into a - // `serde_json::Value`. `Json` supports any type that implements - // `serde::Deserialize`. - async fn json(Json(payload): Json) {} - ``` - "} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } - - #[test] - fn test_toml_code_block() { - let html = indoc! {r##" -

§Required dependencies

-

To use axum there are a few dependencies you have to pull in as well:

-
[dependencies]
-            axum = "<latest-version>"
-            tokio = { version = "<latest-version>", features = ["full"] }
-            tower = "<latest-version>"
-            
- "##}; - let expected = indoc! {r#" - ## Required dependencies - - To use axum there are a few dependencies you have to pull in as well: - - ```toml - [dependencies] - axum = "" - tokio = { version = "", features = ["full"] } - tower = "" - - ``` - "#} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } - - #[test] - fn test_item_table() { - let html = indoc! {r##" -

Structs§

-
    -
  • Errors that can happen when using axum.
  • -
  • Extractor and response for extensions.
  • -
  • Formform
    URL encoded extractor and response.
  • -
  • Jsonjson
    JSON Extractor / Response.
  • -
  • The router type for composing handlers and services.
-

Functions§

-
    -
  • servetokio and (http1 or http2)
    Serve the service with the supplied listener.
  • -
- "##}; - let expected = indoc! {r#" - ## Structs - - - `Error`: Errors that can happen when using axum. - - `Extension`: Extractor and response for extensions. - - `Form` [`form`]: URL encoded extractor and response. - - `Json` [`json`]: JSON Extractor / Response. - - `Router`: The router type for composing handlers and services. - - ## Functions - - - `serve` [`tokio` and (`http1` or `http2`)]: Serve the service with the supplied listener. - "#} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } - - #[test] - fn test_table() { - let html = indoc! {r##" -

§Feature flags

-

axum uses a set of feature flags to reduce the amount of compiled and - optional dependencies.

-

The following optional features are available:

-
- - - - - - - - - - - - - -
NameDescriptionDefault?
http1Enables hyper’s http1 featureYes
http2Enables hyper’s http2 featureNo
jsonEnables the Json type and some similar convenience functionalityYes
macrosEnables optional utility macrosNo
matched-pathEnables capturing of every request’s router path and the MatchedPath extractorYes
multipartEnables parsing multipart/form-data requests with MultipartNo
original-uriEnables capturing of every request’s original URI and the OriginalUri extractorYes
tokioEnables tokio as a dependency and axum::serve, SSE and extract::connect_info types.Yes
tower-logEnables tower’s log featureYes
tracingLog rejections from built-in extractorsYes
wsEnables WebSockets support via extract::wsNo
formEnables the Form extractorYes
queryEnables the Query extractorYes
- "##}; - let expected = indoc! {r#" - ## Feature flags - - axum uses a set of feature flags to reduce the amount of compiled and optional dependencies. - - The following optional features are available: - - | Name | Description | Default? | - | --- | --- | --- | - | `http1` | Enables hyper’s `http1` feature | Yes | - | `http2` | Enables hyper’s `http2` feature | No | - | `json` | Enables the `Json` type and some similar convenience functionality | Yes | - | `macros` | Enables optional utility macros | No | - | `matched-path` | Enables capturing of every request’s router path and the `MatchedPath` extractor | Yes | - | `multipart` | Enables parsing `multipart/form-data` requests with `Multipart` | No | - | `original-uri` | Enables capturing of every request’s original URI and the `OriginalUri` extractor | Yes | - | `tokio` | Enables `tokio` as a dependency and `axum::serve`, `SSE` and `extract::connect_info` types. | Yes | - | `tower-log` | Enables `tower`’s `log` feature | Yes | - | `tracing` | Log rejections from built-in extractors | Yes | - | `ws` | Enables WebSockets support via `extract::ws` | No | - | `form` | Enables the `Form` extractor | Yes | - | `query` | Enables the `Query` extractor | Yes | - "#} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } -} diff --git a/crates/indexed_docs/src/registry.rs b/crates/indexed_docs/src/registry.rs deleted file mode 100644 index 6757cd9c1a1a324e43765df618e01397a8b85708..0000000000000000000000000000000000000000 --- a/crates/indexed_docs/src/registry.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::sync::Arc; - -use collections::HashMap; -use gpui::{App, BackgroundExecutor, Global, ReadGlobal, UpdateGlobal}; -use parking_lot::RwLock; - -use crate::{IndexedDocsProvider, IndexedDocsStore, ProviderId}; - -struct GlobalIndexedDocsRegistry(Arc); - -impl Global for GlobalIndexedDocsRegistry {} - -pub struct IndexedDocsRegistry { - executor: BackgroundExecutor, - stores_by_provider: RwLock>>, -} - -impl IndexedDocsRegistry { - pub fn global(cx: &App) -> Arc { - GlobalIndexedDocsRegistry::global(cx).0.clone() - } - - pub(crate) fn init_global(cx: &mut App) { - GlobalIndexedDocsRegistry::set_global( - cx, - GlobalIndexedDocsRegistry(Arc::new(Self::new(cx.background_executor().clone()))), - ); - } - - pub fn new(executor: BackgroundExecutor) -> Self { - Self { - executor, - stores_by_provider: RwLock::new(HashMap::default()), - } - } - - pub fn list_providers(&self) -> Vec { - self.stores_by_provider - .read() - .keys() - .cloned() - .collect::>() - } - - pub fn register_provider( - &self, - provider: Box, - ) { - self.stores_by_provider.write().insert( - provider.id(), - Arc::new(IndexedDocsStore::new(provider, self.executor.clone())), - ); - } - - pub fn unregister_provider(&self, provider_id: &ProviderId) { - self.stores_by_provider.write().remove(provider_id); - } - - pub fn get_provider_store(&self, provider_id: ProviderId) -> Option> { - self.stores_by_provider.read().get(&provider_id).cloned() - } -} diff --git a/crates/indexed_docs/src/store.rs b/crates/indexed_docs/src/store.rs deleted file mode 100644 index 1407078efaf122acfc07e56894d3fd803605a42a..0000000000000000000000000000000000000000 --- a/crates/indexed_docs/src/store.rs +++ /dev/null @@ -1,346 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; -use std::sync::atomic::AtomicBool; - -use anyhow::{Context as _, Result, anyhow}; -use async_trait::async_trait; -use collections::HashMap; -use derive_more::{Deref, Display}; -use futures::FutureExt; -use futures::future::{self, BoxFuture, Shared}; -use fuzzy::StringMatchCandidate; -use gpui::{App, BackgroundExecutor, Task}; -use heed::Database; -use heed::types::SerdeBincode; -use parking_lot::RwLock; -use serde::{Deserialize, Serialize}; -use util::ResultExt; - -use crate::IndexedDocsRegistry; - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deref, Display)] -pub struct ProviderId(pub Arc); - -/// The name of a package. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deref, Display)] -pub struct PackageName(Arc); - -impl From<&str> for PackageName { - fn from(value: &str) -> Self { - Self(value.into()) - } -} - -#[async_trait] -pub trait IndexedDocsProvider { - /// Returns the ID of this provider. - fn id(&self) -> ProviderId; - - /// Returns the path to the database for this provider. - fn database_path(&self) -> PathBuf; - - /// Returns a list of packages as suggestions to be included in the search - /// results. - /// - /// This can be used to provide completions for known packages (e.g., from the - /// local project or a registry) before a package has been indexed. - async fn suggest_packages(&self) -> Result>; - - /// Indexes the package with the given name. - async fn index(&self, package: PackageName, database: Arc) -> Result<()>; -} - -/// A store for indexed docs. -pub struct IndexedDocsStore { - executor: BackgroundExecutor, - database_future: - Shared, Arc>>>, - provider: Box, - indexing_tasks_by_package: - RwLock>>>>>, - latest_errors_by_package: RwLock>>, -} - -impl IndexedDocsStore { - pub fn try_global(provider: ProviderId, cx: &App) -> Result> { - let registry = IndexedDocsRegistry::global(cx); - registry - .get_provider_store(provider.clone()) - .with_context(|| format!("no indexed docs store found for {provider}")) - } - - pub fn new( - provider: Box, - executor: BackgroundExecutor, - ) -> Self { - let database_future = executor - .spawn({ - let executor = executor.clone(); - let database_path = provider.database_path(); - async move { IndexedDocsDatabase::new(database_path, executor) } - }) - .then(|result| future::ready(result.map(Arc::new).map_err(Arc::new))) - .boxed() - .shared(); - - Self { - executor, - database_future, - provider, - indexing_tasks_by_package: RwLock::new(HashMap::default()), - latest_errors_by_package: RwLock::new(HashMap::default()), - } - } - - pub fn latest_error_for_package(&self, package: &PackageName) -> Option> { - self.latest_errors_by_package.read().get(package).cloned() - } - - /// Returns whether the package with the given name is currently being indexed. - pub fn is_indexing(&self, package: &PackageName) -> bool { - self.indexing_tasks_by_package.read().contains_key(package) - } - - pub async fn load(&self, key: String) -> Result { - self.database_future - .clone() - .await - .map_err(|err| anyhow!(err))? - .load(key) - .await - } - - pub async fn load_many_by_prefix(&self, prefix: String) -> Result> { - self.database_future - .clone() - .await - .map_err(|err| anyhow!(err))? - .load_many_by_prefix(prefix) - .await - } - - /// Returns whether any entries exist with the given prefix. - pub async fn any_with_prefix(&self, prefix: String) -> Result { - self.database_future - .clone() - .await - .map_err(|err| anyhow!(err))? - .any_with_prefix(prefix) - .await - } - - pub fn suggest_packages(self: Arc) -> Task>> { - let this = self.clone(); - self.executor - .spawn(async move { this.provider.suggest_packages().await }) - } - - pub fn index( - self: Arc, - package: PackageName, - ) -> Shared>>> { - if let Some(existing_task) = self.indexing_tasks_by_package.read().get(&package) { - return existing_task.clone(); - } - - let indexing_task = self - .executor - .spawn({ - let this = self.clone(); - let package = package.clone(); - async move { - let _finally = util::defer({ - let this = this.clone(); - let package = package.clone(); - move || { - this.indexing_tasks_by_package.write().remove(&package); - } - }); - - let index_task = { - let package = package.clone(); - async { - let database = this - .database_future - .clone() - .await - .map_err(|err| anyhow!(err))?; - this.provider.index(package, database).await - } - }; - - let result = index_task.await.map_err(Arc::new); - match &result { - Ok(_) => { - this.latest_errors_by_package.write().remove(&package); - } - Err(err) => { - this.latest_errors_by_package - .write() - .insert(package, err.to_string().into()); - } - } - - result - } - }) - .shared(); - - self.indexing_tasks_by_package - .write() - .insert(package, indexing_task.clone()); - - indexing_task - } - - pub fn search(&self, query: String) -> Task> { - let executor = self.executor.clone(); - let database_future = self.database_future.clone(); - self.executor.spawn(async move { - let Some(database) = database_future.await.map_err(|err| anyhow!(err)).log_err() else { - return Vec::new(); - }; - - let Some(items) = database.keys().await.log_err() else { - return Vec::new(); - }; - - let candidates = items - .iter() - .enumerate() - .map(|(ix, item_path)| StringMatchCandidate::new(ix, &item_path)) - .collect::>(); - - let matches = fuzzy::match_strings( - &candidates, - &query, - false, - true, - 100, - &AtomicBool::default(), - executor, - ) - .await; - - matches - .into_iter() - .map(|mat| items[mat.candidate_id].clone()) - .collect() - }) - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Display, Serialize, Deserialize)] -pub struct MarkdownDocs(pub String); - -pub struct IndexedDocsDatabase { - executor: BackgroundExecutor, - env: heed::Env, - entries: Database, SerdeBincode>, -} - -impl IndexedDocsDatabase { - pub fn new(path: PathBuf, executor: BackgroundExecutor) -> Result { - std::fs::create_dir_all(&path)?; - - const ONE_GB_IN_BYTES: usize = 1024 * 1024 * 1024; - let env = unsafe { - heed::EnvOpenOptions::new() - .map_size(ONE_GB_IN_BYTES) - .max_dbs(1) - .open(path)? - }; - - let mut txn = env.write_txn()?; - let entries = env.create_database(&mut txn, Some("rustdoc_entries"))?; - txn.commit()?; - - Ok(Self { - executor, - env, - entries, - }) - } - - pub fn keys(&self) -> Task>> { - let env = self.env.clone(); - let entries = self.entries; - - self.executor.spawn(async move { - let txn = env.read_txn()?; - let mut iter = entries.iter(&txn)?; - let mut keys = Vec::new(); - while let Some((key, _value)) = iter.next().transpose()? { - keys.push(key); - } - - Ok(keys) - }) - } - - pub fn load(&self, key: String) -> Task> { - let env = self.env.clone(); - let entries = self.entries; - - self.executor.spawn(async move { - let txn = env.read_txn()?; - entries - .get(&txn, &key)? - .with_context(|| format!("no docs found for {key}")) - }) - } - - pub fn load_many_by_prefix(&self, prefix: String) -> Task>> { - let env = self.env.clone(); - let entries = self.entries; - - self.executor.spawn(async move { - let txn = env.read_txn()?; - let results = entries - .iter(&txn)? - .filter_map(|entry| { - let (key, value) = entry.ok()?; - if key.starts_with(&prefix) { - Some((key, value)) - } else { - None - } - }) - .collect::>(); - - Ok(results) - }) - } - - /// Returns whether any entries exist with the given prefix. - pub fn any_with_prefix(&self, prefix: String) -> Task> { - let env = self.env.clone(); - let entries = self.entries; - - self.executor.spawn(async move { - let txn = env.read_txn()?; - let any = entries - .iter(&txn)? - .any(|entry| entry.map_or(false, |(key, _value)| key.starts_with(&prefix))); - Ok(any) - }) - } - - pub fn insert(&self, key: String, docs: String) -> Task> { - let env = self.env.clone(); - let entries = self.entries; - - self.executor.spawn(async move { - let mut txn = env.write_txn()?; - entries.put(&mut txn, &key, &MarkdownDocs(docs))?; - txn.commit()?; - Ok(()) - }) - } -} - -impl extension::KeyValueStoreDelegate for IndexedDocsDatabase { - fn insert(&self, key: String, docs: String) -> Task> { - IndexedDocsDatabase::insert(&self, key, docs) - } -} diff --git a/typos.toml b/typos.toml index 336a829a44e6ff7a36e7f8f27f8a5ddc6f3a3f87..e5f02b64159faddd165d6d4571b929c82ad5bed0 100644 --- a/typos.toml +++ b/typos.toml @@ -16,9 +16,6 @@ extend-exclude = [ "crates/google_ai/src/supported_countries.rs", "crates/open_ai/src/supported_countries.rs", - # Some crate names are flagged as typos. - "crates/indexed_docs/src/providers/rustdoc/popular_crates.txt", - # Some mock data is flagged as typos. "crates/assistant_tools/src/web_search_tool.rs", From da8a692ec0aab6d050fe9ab20f2f7d4a793b5b00 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sun, 17 Aug 2025 08:52:05 -0400 Subject: [PATCH 069/744] docs: Remove link to openSUSE Tumbleweed (#36355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR removes the link to Zed on openSUSE Tumbleweed, as it has been removed: https://en.opensuse.org/index.php?title=Archive:Zed&action=history Screenshot 2025-08-17 at 8 48 59 AM Release Notes: - N/A --- docs/src/linux.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/linux.md b/docs/src/linux.md index 309354de6d1b6e3c8f0936350708c161132fd803..4a66445b78902cde0d96ca17dd1e22abaa9ee96d 100644 --- a/docs/src/linux.md +++ b/docs/src/linux.md @@ -48,7 +48,6 @@ There are several third-party Zed packages for various Linux distributions and p - Manjaro: [`zed`](https://packages.manjaro.org/?query=zed) - ALT Linux (Sisyphus): [`zed`](https://packages.altlinux.org/en/sisyphus/srpms/zed/) - AOSC OS: [`zed`](https://packages.aosc.io/packages/zed) -- openSUSE Tumbleweed: [`zed`](https://en.opensuse.org/Zed) See [Repology](https://repology.org/project/zed-editor/versions) for a list of Zed packages in various repositories. From 5895fac377b3c9abd3f14fd8e48188451b02d215 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Sun, 17 Aug 2025 15:05:23 +0200 Subject: [PATCH 070/744] Refactor ToolCallStatus enum to flat variants (#36356) Replace nested Allowed variant with distinct statuses for clearer status handling. Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 74 ++++++++++++++------------ crates/agent_servers/src/e2e_tests.rs | 12 +++-- crates/agent_ui/src/acp/thread_view.rs | 56 ++++++++----------- 3 files changed, 72 insertions(+), 70 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 3bb1b99ba101f32becab7f1c85c5be1e0d870a55..c1c634612b47dade0a089de8b35ee862900e41dc 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -223,7 +223,7 @@ impl ToolCall { } if let Some(status) = status { - self.status = ToolCallStatus::Allowed { status }; + self.status = status.into(); } if let Some(title) = title { @@ -344,30 +344,48 @@ impl ToolCall { #[derive(Debug)] pub enum ToolCallStatus { + /// The tool call hasn't started running yet, but we start showing it to + /// the user. + Pending, + /// The tool call is waiting for confirmation from the user. WaitingForConfirmation { options: Vec, respond_tx: oneshot::Sender, }, - Allowed { - status: acp::ToolCallStatus, - }, + /// The tool call is currently running. + InProgress, + /// The tool call completed successfully. + Completed, + /// The tool call failed. + Failed, + /// The user rejected the tool call. Rejected, + /// The user cancelled generation so the tool call was cancelled. Canceled, } +impl From for ToolCallStatus { + fn from(status: acp::ToolCallStatus) -> Self { + match status { + acp::ToolCallStatus::Pending => Self::Pending, + acp::ToolCallStatus::InProgress => Self::InProgress, + acp::ToolCallStatus::Completed => Self::Completed, + acp::ToolCallStatus::Failed => Self::Failed, + } + } +} + impl Display for ToolCallStatus { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { + ToolCallStatus::Pending => "Pending", ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation", - ToolCallStatus::Allowed { status } => match status { - acp::ToolCallStatus::Pending => "Pending", - acp::ToolCallStatus::InProgress => "In Progress", - acp::ToolCallStatus::Completed => "Completed", - acp::ToolCallStatus::Failed => "Failed", - }, + ToolCallStatus::InProgress => "In Progress", + ToolCallStatus::Completed => "Completed", + ToolCallStatus::Failed => "Failed", ToolCallStatus::Rejected => "Rejected", ToolCallStatus::Canceled => "Canceled", } @@ -759,11 +777,7 @@ impl AcpThread { AgentThreadEntry::UserMessage(_) => return false, AgentThreadEntry::ToolCall( call @ ToolCall { - status: - ToolCallStatus::Allowed { - status: - acp::ToolCallStatus::InProgress | acp::ToolCallStatus::Pending, - }, + status: ToolCallStatus::InProgress | ToolCallStatus::Pending, .. }, ) if call.diffs().next().is_some() => { @@ -945,9 +959,7 @@ impl AcpThread { tool_call: acp::ToolCall, cx: &mut Context, ) -> Result<(), acp::Error> { - let status = ToolCallStatus::Allowed { - status: tool_call.status, - }; + let status = tool_call.status.into(); self.upsert_tool_call_inner(tool_call.into(), status, cx) } @@ -1074,9 +1086,7 @@ impl AcpThread { ToolCallStatus::Rejected } acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways => { - ToolCallStatus::Allowed { - status: acp::ToolCallStatus::InProgress, - } + ToolCallStatus::InProgress } }; @@ -1097,7 +1107,10 @@ impl AcpThread { match &entry { AgentThreadEntry::ToolCall(call) => match call.status { ToolCallStatus::WaitingForConfirmation { .. } => return true, - ToolCallStatus::Allowed { .. } + ToolCallStatus::Pending + | ToolCallStatus::InProgress + | ToolCallStatus::Completed + | ToolCallStatus::Failed | ToolCallStatus::Rejected | ToolCallStatus::Canceled => continue, }, @@ -1290,10 +1303,9 @@ impl AcpThread { if let AgentThreadEntry::ToolCall(call) = entry { let cancel = matches!( call.status, - ToolCallStatus::WaitingForConfirmation { .. } - | ToolCallStatus::Allowed { - status: acp::ToolCallStatus::InProgress - } + ToolCallStatus::Pending + | ToolCallStatus::WaitingForConfirmation { .. } + | ToolCallStatus::InProgress ); if cancel { @@ -1939,10 +1951,7 @@ mod tests { assert!(matches!( thread.entries[1], AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Allowed { - status: acp::ToolCallStatus::InProgress, - .. - }, + status: ToolCallStatus::InProgress, .. }) )); @@ -1981,10 +1990,7 @@ mod tests { assert!(matches!( thread.entries[1], AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Completed, - .. - }, + status: ToolCallStatus::Completed, .. }) )); diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 5af7010f26faf31016b15b0625c8b96e384ea7a4..2b32edcd4f54b2ca216e11d37cb2dd7c1ee2243a 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -134,7 +134,9 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp matches!( entry, AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Allowed { .. }, + status: ToolCallStatus::Pending + | ToolCallStatus::InProgress + | ToolCallStatus::Completed, .. }) ) @@ -212,7 +214,9 @@ pub async fn test_tool_call_with_permission( assert!(thread.entries().iter().any(|entry| matches!( entry, AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Allowed { .. }, + status: ToolCallStatus::Pending + | ToolCallStatus::InProgress + | ToolCallStatus::Completed, .. }) ))); @@ -223,7 +227,9 @@ pub async fn test_tool_call_with_permission( thread.read_with(cx, |thread, cx| { let AgentThreadEntry::ToolCall(ToolCall { content, - status: ToolCallStatus::Allowed { .. }, + status: ToolCallStatus::Pending + | ToolCallStatus::InProgress + | ToolCallStatus::Completed, .. }) = thread .entries() diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 17341e4c8ad38aaaa1e8ad2211d9ff3357575097..7c1f3cf4ae51b562cbbe3eb52eac48038221b95c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1053,14 +1053,10 @@ impl AcpThreadView { let card_header_id = SharedString::from("inner-tool-call-header"); let status_icon = match &tool_call.status { - ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Pending, - } - | ToolCallStatus::WaitingForConfirmation { .. } => None, - ToolCallStatus::Allowed { - status: acp::ToolCallStatus::InProgress, - .. - } => Some( + ToolCallStatus::Pending + | ToolCallStatus::WaitingForConfirmation { .. } + | ToolCallStatus::Completed => None, + ToolCallStatus::InProgress => Some( Icon::new(IconName::ArrowCircle) .color(Color::Accent) .size(IconSize::Small) @@ -1071,16 +1067,7 @@ impl AcpThreadView { ) .into_any(), ), - ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Completed, - .. - } => None, - ToolCallStatus::Rejected - | ToolCallStatus::Canceled - | ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Failed, - .. - } => Some( + ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => Some( Icon::new(IconName::Close) .color(Color::Error) .size(IconSize::Small) @@ -1146,15 +1133,23 @@ impl AcpThreadView { tool_call.content.is_empty(), cx, )), - ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => v_flex() - .w_full() - .children(tool_call.content.iter().map(|content| { - div() - .child( - self.render_tool_call_content(entry_ix, content, tool_call, window, cx), - ) - .into_any_element() - })), + ToolCallStatus::Pending + | ToolCallStatus::InProgress + | ToolCallStatus::Completed + | ToolCallStatus::Failed + | ToolCallStatus::Canceled => { + v_flex() + .w_full() + .children(tool_call.content.iter().map(|content| { + div() + .child( + self.render_tool_call_content( + entry_ix, content, tool_call, window, cx, + ), + ) + .into_any_element() + })) + } ToolCallStatus::Rejected => v_flex().size_0(), }; @@ -1467,12 +1462,7 @@ impl AcpThreadView { let tool_failed = matches!( &tool_call.status, - ToolCallStatus::Rejected - | ToolCallStatus::Canceled - | ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Failed, - .. - } + ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed ); let output = terminal_data.output(); From addc4f4a11a816fd6d116be379bf249aa203f535 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sun, 17 Aug 2025 17:19:38 +0200 Subject: [PATCH 071/744] agent_ui: Ensure that all configuration views get rendered with full width (#36362) Closes #36097 Release Notes: - Fixed API key input fields getting shrunk in Agent Panel settings view on low panel widths paired with high UI font sizes. --- crates/agent_ui/src/agent_configuration.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 4a2dd88c332b03dd914bcb793fed2b4b2d8c0c07..b4ebb8206c78a3866b4d04cc9e8f5aa714c2c37a 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -300,6 +300,7 @@ impl AgentConfiguration { ) .child( div() + .w_full() .px_2() .when(is_expanded, |parent| match configuration_view { Some(configuration_view) => parent.child(configuration_view), From faaaf02bf211e71743912b77cd6e7911e73965ff Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sun, 17 Aug 2025 13:25:05 -0300 Subject: [PATCH 072/744] ui: Reduce icons stroke width (#36361) After redesigning all Zed icons (https://github.com/zed-industries/zed/pull/35856), it felt like using 1.5 for stroke width didn't really flow well with the default typeface default font weight. Reducing it to 1.2 makes the UI much sharper, less burry, and more cohesive overall. Release Notes: - N/A --- assets/icons/ai.svg | 2 +- assets/icons/arrow_circle.svg | 8 ++++---- assets/icons/arrow_down.svg | 2 +- assets/icons/arrow_down10.svg | 2 +- assets/icons/arrow_down_right.svg | 4 ++-- assets/icons/arrow_left.svg | 2 +- assets/icons/arrow_right.svg | 2 +- assets/icons/arrow_right_left.svg | 8 ++++---- assets/icons/arrow_up.svg | 2 +- assets/icons/arrow_up_right.svg | 4 ++-- assets/icons/audio_off.svg | 10 +++++----- assets/icons/audio_on.svg | 6 +++--- assets/icons/backspace.svg | 6 +++--- assets/icons/bell.svg | 4 ++-- assets/icons/bell_dot.svg | 4 ++-- assets/icons/bell_off.svg | 6 +++--- assets/icons/bell_ring.svg | 8 ++++---- assets/icons/binary.svg | 2 +- assets/icons/blocks.svg | 2 +- assets/icons/bolt_outlined.svg | 2 +- assets/icons/book.svg | 2 +- assets/icons/book_copy.svg | 2 +- assets/icons/chat.svg | 4 ++-- assets/icons/check.svg | 2 +- assets/icons/check_circle.svg | 4 ++-- assets/icons/check_double.svg | 4 ++-- assets/icons/chevron_down.svg | 2 +- assets/icons/chevron_left.svg | 2 +- assets/icons/chevron_right.svg | 2 +- assets/icons/chevron_up.svg | 2 +- assets/icons/chevron_up_down.svg | 4 ++-- assets/icons/circle_help.svg | 6 +++--- assets/icons/close.svg | 2 +- assets/icons/cloud_download.svg | 2 +- assets/icons/code.svg | 2 +- assets/icons/cog.svg | 2 +- assets/icons/command.svg | 2 +- assets/icons/control.svg | 2 +- assets/icons/copilot.svg | 6 +++--- assets/icons/copy.svg | 2 +- assets/icons/countdown_timer.svg | 2 +- assets/icons/crosshair.svg | 10 +++++----- assets/icons/cursor_i_beam.svg | 4 ++-- assets/icons/dash.svg | 2 +- assets/icons/database_zap.svg | 2 +- assets/icons/debug.svg | 20 +++++++++---------- assets/icons/debug_breakpoint.svg | 2 +- assets/icons/debug_continue.svg | 2 +- assets/icons/debug_detach.svg | 2 +- assets/icons/debug_disabled_breakpoint.svg | 2 +- .../icons/debug_disabled_log_breakpoint.svg | 6 +++--- assets/icons/debug_ignore_breakpoints.svg | 2 +- assets/icons/debug_step_back.svg | 2 +- assets/icons/debug_step_into.svg | 2 +- assets/icons/debug_step_out.svg | 2 +- assets/icons/debug_step_over.svg | 2 +- assets/icons/diff.svg | 2 +- assets/icons/disconnected.svg | 2 +- assets/icons/download.svg | 2 +- assets/icons/envelope.svg | 4 ++-- assets/icons/eraser.svg | 2 +- assets/icons/escape.svg | 2 +- assets/icons/exit.svg | 6 +++--- assets/icons/expand_down.svg | 4 ++-- assets/icons/expand_up.svg | 4 ++-- assets/icons/expand_vertical.svg | 2 +- assets/icons/eye.svg | 4 ++-- assets/icons/file.svg | 2 +- assets/icons/file_code.svg | 2 +- assets/icons/file_diff.svg | 2 +- assets/icons/file_doc.svg | 6 +++--- assets/icons/file_generic.svg | 6 +++--- assets/icons/file_git.svg | 8 ++++---- assets/icons/file_icons/ai.svg | 2 +- assets/icons/file_icons/audio.svg | 12 +++++------ assets/icons/file_icons/book.svg | 6 +++--- assets/icons/file_icons/bun.svg | 2 +- assets/icons/file_icons/chevron_down.svg | 2 +- assets/icons/file_icons/chevron_left.svg | 2 +- assets/icons/file_icons/chevron_right.svg | 2 +- assets/icons/file_icons/chevron_up.svg | 2 +- assets/icons/file_icons/code.svg | 4 ++-- assets/icons/file_icons/coffeescript.svg | 2 +- assets/icons/file_icons/conversations.svg | 2 +- assets/icons/file_icons/dart.svg | 2 +- assets/icons/file_icons/database.svg | 6 +++--- assets/icons/file_icons/diff.svg | 6 +++--- assets/icons/file_icons/eslint.svg | 2 +- assets/icons/file_icons/file.svg | 6 +++--- assets/icons/file_icons/folder.svg | 2 +- assets/icons/file_icons/folder_open.svg | 4 ++-- assets/icons/file_icons/font.svg | 2 +- assets/icons/file_icons/git.svg | 8 ++++---- assets/icons/file_icons/gleam.svg | 4 ++-- assets/icons/file_icons/graphql.svg | 4 ++-- assets/icons/file_icons/hash.svg | 8 ++++---- assets/icons/file_icons/heroku.svg | 2 +- assets/icons/file_icons/html.svg | 6 +++--- assets/icons/file_icons/image.svg | 6 +++--- assets/icons/file_icons/java.svg | 10 +++++----- assets/icons/file_icons/lock.svg | 2 +- assets/icons/file_icons/magnifying_glass.svg | 2 +- assets/icons/file_icons/nix.svg | 12 +++++------ assets/icons/file_icons/notebook.svg | 10 +++++----- assets/icons/file_icons/package.svg | 2 +- assets/icons/file_icons/phoenix.svg | 2 +- assets/icons/file_icons/plus.svg | 2 +- assets/icons/file_icons/prettier.svg | 20 +++++++++---------- assets/icons/file_icons/project.svg | 2 +- assets/icons/file_icons/python.svg | 4 ++-- assets/icons/file_icons/replace.svg | 2 +- assets/icons/file_icons/replace_next.svg | 2 +- assets/icons/file_icons/rust.svg | 2 +- assets/icons/file_icons/scala.svg | 2 +- assets/icons/file_icons/settings.svg | 2 +- assets/icons/file_icons/tcl.svg | 2 +- assets/icons/file_icons/toml.svg | 6 +++--- assets/icons/file_icons/video.svg | 4 ++-- assets/icons/file_icons/vue.svg | 2 +- assets/icons/file_lock.svg | 2 +- assets/icons/file_markdown.svg | 2 +- assets/icons/file_rust.svg | 2 +- assets/icons/file_text_outlined.svg | 8 ++++---- assets/icons/file_toml.svg | 6 +++--- assets/icons/file_tree.svg | 2 +- assets/icons/filter.svg | 2 +- assets/icons/flame.svg | 2 +- assets/icons/folder.svg | 2 +- assets/icons/folder_open.svg | 4 ++-- assets/icons/folder_search.svg | 2 +- assets/icons/font.svg | 2 +- assets/icons/font_size.svg | 2 +- assets/icons/font_weight.svg | 2 +- assets/icons/forward_arrow.svg | 4 ++-- assets/icons/git_branch.svg | 2 +- assets/icons/git_branch_alt.svg | 10 +++++----- assets/icons/github.svg | 2 +- assets/icons/hash.svg | 2 +- assets/icons/history_rerun.svg | 6 +++--- assets/icons/image.svg | 2 +- assets/icons/info.svg | 4 ++-- assets/icons/json.svg | 4 ++-- assets/icons/keyboard.svg | 2 +- assets/icons/knockouts/x_fg.svg | 2 +- assets/icons/library.svg | 8 ++++---- assets/icons/line_height.svg | 2 +- assets/icons/list_collapse.svg | 2 +- assets/icons/list_todo.svg | 2 +- assets/icons/list_tree.svg | 10 +++++----- assets/icons/list_x.svg | 10 +++++----- assets/icons/load_circle.svg | 2 +- assets/icons/location_edit.svg | 2 +- assets/icons/lock_outlined.svg | 6 +++--- assets/icons/magnifying_glass.svg | 2 +- assets/icons/maximize.svg | 8 ++++---- assets/icons/menu.svg | 2 +- assets/icons/menu_alt.svg | 2 +- assets/icons/mic.svg | 6 +++--- assets/icons/mic_mute.svg | 12 +++++------ assets/icons/minimize.svg | 8 ++++---- assets/icons/notepad.svg | 2 +- assets/icons/option.svg | 4 ++-- assets/icons/pencil.svg | 4 ++-- assets/icons/person.svg | 2 +- assets/icons/pin.svg | 4 ++-- assets/icons/play_filled.svg | 2 +- assets/icons/play_outlined.svg | 2 +- assets/icons/plus.svg | 4 ++-- assets/icons/power.svg | 2 +- assets/icons/public.svg | 2 +- assets/icons/pull_request.svg | 2 +- assets/icons/quote.svg | 2 +- assets/icons/reader.svg | 6 +++--- assets/icons/refresh_title.svg | 2 +- assets/icons/regex.svg | 2 +- assets/icons/repl_neutral.svg | 8 ++++---- assets/icons/repl_off.svg | 18 ++++++++--------- assets/icons/repl_pause.svg | 12 +++++------ assets/icons/repl_play.svg | 10 +++++----- assets/icons/replace.svg | 2 +- assets/icons/replace_next.svg | 2 +- assets/icons/rerun.svg | 2 +- assets/icons/return.svg | 4 ++-- assets/icons/rotate_ccw.svg | 2 +- assets/icons/rotate_cw.svg | 2 +- assets/icons/scissors.svg | 2 +- assets/icons/screen.svg | 6 +++--- assets/icons/select_all.svg | 2 +- assets/icons/send.svg | 2 +- assets/icons/server.svg | 8 ++++---- assets/icons/settings.svg | 2 +- assets/icons/shield_check.svg | 4 ++-- assets/icons/shift.svg | 2 +- assets/icons/slash.svg | 2 +- assets/icons/sliders.svg | 12 +++++------ assets/icons/space.svg | 2 +- assets/icons/sparkle.svg | 2 +- assets/icons/split.svg | 4 ++-- assets/icons/split_alt.svg | 2 +- assets/icons/square_dot.svg | 2 +- assets/icons/square_minus.svg | 4 ++-- assets/icons/square_plus.svg | 6 +++--- assets/icons/star.svg | 2 +- assets/icons/star_filled.svg | 2 +- assets/icons/stop.svg | 2 +- assets/icons/swatch_book.svg | 2 +- assets/icons/tab.svg | 6 +++--- assets/icons/terminal_alt.svg | 6 +++--- assets/icons/text_snippet.svg | 2 +- assets/icons/text_thread.svg | 10 +++++----- assets/icons/thread.svg | 2 +- assets/icons/thread_from_summary.svg | 8 ++++---- assets/icons/thumbs_down.svg | 2 +- assets/icons/thumbs_up.svg | 2 +- assets/icons/todo_complete.svg | 2 +- assets/icons/todo_pending.svg | 16 +++++++-------- assets/icons/todo_progress.svg | 18 ++++++++--------- assets/icons/tool_copy.svg | 4 ++-- assets/icons/tool_delete_file.svg | 6 +++--- assets/icons/tool_diagnostics.svg | 6 +++--- assets/icons/tool_folder.svg | 2 +- assets/icons/tool_hammer.svg | 6 +++--- assets/icons/tool_notification.svg | 4 ++-- assets/icons/tool_pencil.svg | 4 ++-- assets/icons/tool_read.svg | 10 +++++----- assets/icons/tool_regex.svg | 2 +- assets/icons/tool_search.svg | 4 ++-- assets/icons/tool_terminal.svg | 6 +++--- assets/icons/tool_think.svg | 2 +- assets/icons/tool_web.svg | 6 +++--- assets/icons/trash.svg | 6 +++--- assets/icons/undo.svg | 2 +- assets/icons/user_check.svg | 2 +- assets/icons/user_group.svg | 6 +++--- assets/icons/user_round_pen.svg | 2 +- assets/icons/warning.svg | 2 +- assets/icons/whole_word.svg | 2 +- assets/icons/x_circle.svg | 2 +- assets/icons/zed_assistant.svg | 2 +- assets/icons/zed_burn_mode.svg | 2 +- assets/icons/zed_burn_mode_on.svg | 2 +- assets/icons/zed_mcp_custom.svg | 2 +- assets/icons/zed_mcp_extension.svg | 2 +- assets/icons/zed_predict.svg | 6 +++--- assets/icons/zed_predict_down.svg | 6 +++--- assets/icons/zed_predict_error.svg | 4 ++-- assets/icons/zed_predict_up.svg | 6 +++--- crates/icons/README.md | 2 +- 248 files changed, 499 insertions(+), 499 deletions(-) diff --git a/assets/icons/ai.svg b/assets/icons/ai.svg index d60396ad47db1a2068207c52f783c08cd2da4e69..4236d50337bef92cb550cdbf71d83843ab35e2f3 100644 --- a/assets/icons/ai.svg +++ b/assets/icons/ai.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/arrow_circle.svg b/assets/icons/arrow_circle.svg index 76363c6270890d51e5946664fa4943e5b16aca0c..cdfa93979505e45a9e876059eddf5a61ac489e1a 100644 --- a/assets/icons/arrow_circle.svg +++ b/assets/icons/arrow_circle.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/arrow_down.svg b/assets/icons/arrow_down.svg index c71e5437f8cd9424be47da102802a47c30575769..60e6584c4568a5e113e225800024e835ea9743e7 100644 --- a/assets/icons/arrow_down.svg +++ b/assets/icons/arrow_down.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_down10.svg b/assets/icons/arrow_down10.svg index 8eed82276cc4bf613d5bc026ad8bd59694760787..5933b758d939bef502495cbffcbddc60a3d42691 100644 --- a/assets/icons/arrow_down10.svg +++ b/assets/icons/arrow_down10.svg @@ -1 +1 @@ - + diff --git a/assets/icons/arrow_down_right.svg b/assets/icons/arrow_down_right.svg index 73f72a2c38c6f6833a3c96f74fddafd8d1fb8730..ebdb06d77b24d5aa0d28615e156135495e8e80c4 100644 --- a/assets/icons/arrow_down_right.svg +++ b/assets/icons/arrow_down_right.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/arrow_left.svg b/assets/icons/arrow_left.svg index ca441497a054f2c6a1769f804d4c8aaf7cbc8ccc..f7eacb2a779c94e3f743fdbc594773de73017e41 100644 --- a/assets/icons/arrow_left.svg +++ b/assets/icons/arrow_left.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_right.svg b/assets/icons/arrow_right.svg index ae148885637563795bec94d85b51f979be4613a4..b9324af5a289ac2b4ae6d7b6374d603587763de0 100644 --- a/assets/icons/arrow_right.svg +++ b/assets/icons/arrow_right.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_right_left.svg b/assets/icons/arrow_right_left.svg index cfeee0cc24b5c988f15d83a29e7fb32b7427ccb0..2c1211056a17eee8644b07b1fb651818f63db3dc 100644 --- a/assets/icons/arrow_right_left.svg +++ b/assets/icons/arrow_right_left.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/arrow_up.svg b/assets/icons/arrow_up.svg index b98c710374fc5c9c5ef8ddc05fcbdbe2eaa30017..ff3ad441234b8d2ae1aeb17c531a9ecb288dc8d2 100644 --- a/assets/icons/arrow_up.svg +++ b/assets/icons/arrow_up.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_up_right.svg b/assets/icons/arrow_up_right.svg index fb065bc9ce7d90d20db4ee45b7bc2d909dede09f..a948ef8f8130b99339130e4400320309ae3afaec 100644 --- a/assets/icons/arrow_up_right.svg +++ b/assets/icons/arrow_up_right.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/audio_off.svg b/assets/icons/audio_off.svg index dfb5a1c45829119ea0dc89bbca3a3f33228ee88f..43d2a04344748feab9496cd528aacba075d9f7e8 100644 --- a/assets/icons/audio_off.svg +++ b/assets/icons/audio_off.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/audio_on.svg b/assets/icons/audio_on.svg index d1bef0d337d6c8a0e79cb0dab8b7d63d5cb2a4d1..6e183bd585461e49418f58af95026590549c950b 100644 --- a/assets/icons/audio_on.svg +++ b/assets/icons/audio_on.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/backspace.svg b/assets/icons/backspace.svg index 679ef1ade19eef8317e0a35547c3b6b212a72499..9ef4432b6f019b1eb71978e214e6ea9a3e680839 100644 --- a/assets/icons/backspace.svg +++ b/assets/icons/backspace.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/bell.svg b/assets/icons/bell.svg index f9b2a97fb34faceb155b5eb6a263ff0752b9e402..70225bb105f24ad42616fb10b4742a2d3176502b 100644 --- a/assets/icons/bell.svg +++ b/assets/icons/bell.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/bell_dot.svg b/assets/icons/bell_dot.svg index 09a17401dabe97c75eb8d8a977aa0ed00d12f1ec..959a7773cf2af4a6520741a40cc6866ffab4bdab 100644 --- a/assets/icons/bell_dot.svg +++ b/assets/icons/bell_dot.svg @@ -1,5 +1,5 @@ - - + + diff --git a/assets/icons/bell_off.svg b/assets/icons/bell_off.svg index 98cbd1eb603c48de6f157b5d4cbcfbf246e05702..5c3c1a0d68680d8d9a7fa42163c40e899259646c 100644 --- a/assets/icons/bell_off.svg +++ b/assets/icons/bell_off.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/bell_ring.svg b/assets/icons/bell_ring.svg index e411e7511b0b10be7efd5d85d1257b325f9d64de..838056cc032aa4c47c75ffa1a1f2e189835ff2da 100644 --- a/assets/icons/bell_ring.svg +++ b/assets/icons/bell_ring.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/binary.svg b/assets/icons/binary.svg index bbc375617f061c9feae2de08bcda683f5192c3b8..3c15e9b5470575c6251fef6fe1ae2f035ef677a6 100644 --- a/assets/icons/binary.svg +++ b/assets/icons/binary.svg @@ -1 +1 @@ - + diff --git a/assets/icons/blocks.svg b/assets/icons/blocks.svg index e1690e2642b60d93893e52008c6d10d96e810d48..84725d789233079e9f3f4138392af6b15a104d9c 100644 --- a/assets/icons/blocks.svg +++ b/assets/icons/blocks.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/bolt_outlined.svg b/assets/icons/bolt_outlined.svg index 58fccf778813d3653f1066f45e5573adbf2d9ec2..ca9c75fbfd64beaac0ed544d2718a5ecb59a8243 100644 --- a/assets/icons/bolt_outlined.svg +++ b/assets/icons/bolt_outlined.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/book.svg b/assets/icons/book.svg index 8b0f89e82d073d857582f8364f1f501b8567cccd..a2ab394be4a74b9fb618dd3a7613b70f277181df 100644 --- a/assets/icons/book.svg +++ b/assets/icons/book.svg @@ -1 +1 @@ - + diff --git a/assets/icons/book_copy.svg b/assets/icons/book_copy.svg index f509beffe6da4af84b72268832af94bd6d3568b1..b7afd1df5c1fbaf51936a7170b3cfa0d0622511d 100644 --- a/assets/icons/book_copy.svg +++ b/assets/icons/book_copy.svg @@ -1 +1 @@ - + diff --git a/assets/icons/chat.svg b/assets/icons/chat.svg index a0548c3d3e6917fbea2bfba825761e01cd215a33..c64f6b5e0efb65a7c2e056d55bee1917960b4d29 100644 --- a/assets/icons/chat.svg +++ b/assets/icons/chat.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/check.svg b/assets/icons/check.svg index 4563505aaaecfdfa300d609709a34ecc4a5f3fb5..21e2137965e01f4d384f1f2aad70629e2d75f313 100644 --- a/assets/icons/check.svg +++ b/assets/icons/check.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/check_circle.svg b/assets/icons/check_circle.svg index e6ec5d11efffcc64721e444b8fef9a5a94481436..f9b88c4ce1451ef24a4084d6b3bb9469be85b571 100644 --- a/assets/icons/check_circle.svg +++ b/assets/icons/check_circle.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/check_double.svg b/assets/icons/check_double.svg index b52bef81a404d96489121985fa8bafdcfe30753c..fabc7005209070087e8d56e14808e68bd1f4c771 100644 --- a/assets/icons/check_double.svg +++ b/assets/icons/check_double.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/chevron_down.svg b/assets/icons/chevron_down.svg index 7894aae76497858d0db923063eed53dc41db991b..e4ca142a91fa18a252dbba72faffd9d403d29c2a 100644 --- a/assets/icons/chevron_down.svg +++ b/assets/icons/chevron_down.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/chevron_left.svg b/assets/icons/chevron_left.svg index 4be4c95dcac3e79116df2836634a8720bb36a2ef..fbe438fd4bfbcfc0bf08c2bbcf1c416c073ddc6d 100644 --- a/assets/icons/chevron_left.svg +++ b/assets/icons/chevron_left.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/chevron_right.svg b/assets/icons/chevron_right.svg index c8ff84717750a076b39cd4a045667ec1942e4167..4f170717c9b185a3204f0078160d34b0dae4aff1 100644 --- a/assets/icons/chevron_right.svg +++ b/assets/icons/chevron_right.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/chevron_up.svg b/assets/icons/chevron_up.svg index 8e575e2e8d2242c29602655ca08a334edc04690b..bbe6b9762d244af38f4fe258f9f334638829e6f6 100644 --- a/assets/icons/chevron_up.svg +++ b/assets/icons/chevron_up.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/chevron_up_down.svg b/assets/icons/chevron_up_down.svg index c7af01d4a36869c9c7b9e44bd8e0bdc42c5baf44..299f6bce5ad1e5e6d89b0d12d4ce9deb2f3ee193 100644 --- a/assets/icons/chevron_up_down.svg +++ b/assets/icons/chevron_up_down.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/circle_help.svg b/assets/icons/circle_help.svg index 4e2890d3e10e7976c648aec48c524771cce80ba8..0e623bd1da3241616b9b6bd8fb7d6243b12b07b7 100644 --- a/assets/icons/circle_help.svg +++ b/assets/icons/circle_help.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/close.svg b/assets/icons/close.svg index ad487e0a4f9fd2d95b26bf5cd84933e7bb817b9e..846b3a703dc6f53f36736b515443830d51205c99 100644 --- a/assets/icons/close.svg +++ b/assets/icons/close.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/cloud_download.svg b/assets/icons/cloud_download.svg index 0efcbe10f13b7b5d79c367824b1dfd88d0de1105..70cda55856cc459674e7641366631a035fcc0251 100644 --- a/assets/icons/cloud_download.svg +++ b/assets/icons/cloud_download.svg @@ -1 +1 @@ - + diff --git a/assets/icons/code.svg b/assets/icons/code.svg index 6a1795b59c9c8fefb9b0df2061e29ac3be2e3e1f..72d145224a16f28184ac0a6bedbb02f608248adf 100644 --- a/assets/icons/code.svg +++ b/assets/icons/code.svg @@ -1 +1 @@ - + diff --git a/assets/icons/cog.svg b/assets/icons/cog.svg index 4f3ada11a632c5d69bb68dda95bfdfa0d2ae6975..7dd3a8befff59b5aaa0506df9b2cd7140725ab81 100644 --- a/assets/icons/cog.svg +++ b/assets/icons/cog.svg @@ -1 +1 @@ - + diff --git a/assets/icons/command.svg b/assets/icons/command.svg index 6602af8e1f1e085e26d4548b93e3fb26964825a6..f361ca2d05f71ec0af5d1e99ac0f0a633a932ebe 100644 --- a/assets/icons/command.svg +++ b/assets/icons/command.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/control.svg b/assets/icons/control.svg index e831968df6d0a6e85d376517add5978c10b56315..f9341b6256143ce250aed35a38cebd2b6ed74207 100644 --- a/assets/icons/control.svg +++ b/assets/icons/control.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/copilot.svg b/assets/icons/copilot.svg index 57c0a5f91ae3641bd35cb41da9dff530d4ae7d51..2584cd631006c10ea9535408657fd881f0748249 100644 --- a/assets/icons/copilot.svg +++ b/assets/icons/copilot.svg @@ -1,9 +1,9 @@ - - - + + + diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg index dfd8d9dbb9d62d09a3d0c7de9da4a9f0a9af3c5f..bca13f8d56a1b644051c5be2f17c0e4cc1cdb43b 100644 --- a/assets/icons/copy.svg +++ b/assets/icons/copy.svg @@ -1 +1 @@ - + diff --git a/assets/icons/countdown_timer.svg b/assets/icons/countdown_timer.svg index 5e69f1bfb4b47a144d0675c2338f210aae953781..5d1e775e68c8bc3871ad8070faeaea36c7395eec 100644 --- a/assets/icons/countdown_timer.svg +++ b/assets/icons/countdown_timer.svg @@ -1 +1 @@ - + diff --git a/assets/icons/crosshair.svg b/assets/icons/crosshair.svg index 1492bf924543c9c64b06021a93dc44967740ea4c..3af6aa9fa35f29a6635d58027d7774ddd43a510c 100644 --- a/assets/icons/crosshair.svg +++ b/assets/icons/crosshair.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/cursor_i_beam.svg b/assets/icons/cursor_i_beam.svg index 3790de6f49d454bc5bb317e64e80a4daffceaa45..2d513181f94d2d2d29ebf1f779925bc09b85b699 100644 --- a/assets/icons/cursor_i_beam.svg +++ b/assets/icons/cursor_i_beam.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/dash.svg b/assets/icons/dash.svg index 9270f80781da095feb8abf5561fa579189574078..3928ee7cfa2b9ce408d4560b6f1aa0c0b298d1fb 100644 --- a/assets/icons/dash.svg +++ b/assets/icons/dash.svg @@ -1 +1 @@ - + diff --git a/assets/icons/database_zap.svg b/assets/icons/database_zap.svg index 160ffa5041957318e0b3f47864c81a50028c62f6..76af0f9251d096d91ee0d054ed867181a288e313 100644 --- a/assets/icons/database_zap.svg +++ b/assets/icons/database_zap.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug.svg b/assets/icons/debug.svg index 900caf4b983f60a2de7424c62965023f88283a18..6423a2b090c1b838b0a4e84c089f5db694777790 100644 --- a/assets/icons/debug.svg +++ b/assets/icons/debug.svg @@ -1,12 +1,12 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/assets/icons/debug_breakpoint.svg b/assets/icons/debug_breakpoint.svg index 9cab42eecd37ba8dff1892ced7dd8de92dae0998..c09a3c159fed6bb2423fc7e50ce7fcae8d425065 100644 --- a/assets/icons/debug_breakpoint.svg +++ b/assets/icons/debug_breakpoint.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/debug_continue.svg b/assets/icons/debug_continue.svg index f663a5a041abc8ddc29938f3f980187c1d3e9f03..f03a8b2364b5fe8555cddb6d0e940480f2d15e75 100644 --- a/assets/icons/debug_continue.svg +++ b/assets/icons/debug_continue.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_detach.svg b/assets/icons/debug_detach.svg index a34a0e817146097fce6e4b95919e2415c665fa43..8b3484557148a3e3e54638be5d6579f9741b7e3e 100644 --- a/assets/icons/debug_detach.svg +++ b/assets/icons/debug_detach.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_disabled_breakpoint.svg b/assets/icons/debug_disabled_breakpoint.svg index 8b80623b025af88df7a5ff4fcf4871a6b6b86cd2..9a7c896f4709c97591196bc0c3d3813bb2a2a62c 100644 --- a/assets/icons/debug_disabled_breakpoint.svg +++ b/assets/icons/debug_disabled_breakpoint.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/debug_disabled_log_breakpoint.svg b/assets/icons/debug_disabled_log_breakpoint.svg index 2ccc37623d9daa4b5f6c79748a2e4bf3f7a03067..f477f4f32d83ee2ea7a364ac2d22bf7ce72a9f5a 100644 --- a/assets/icons/debug_disabled_log_breakpoint.svg +++ b/assets/icons/debug_disabled_log_breakpoint.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/debug_ignore_breakpoints.svg b/assets/icons/debug_ignore_breakpoints.svg index b2a345d314ec599fb53f09988183dff976e38977..bc95329c7ad1b44e075481ef52d98d69f650b176 100644 --- a/assets/icons/debug_ignore_breakpoints.svg +++ b/assets/icons/debug_ignore_breakpoints.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/debug_step_back.svg b/assets/icons/debug_step_back.svg index d1112d6b8e1725f90a5440b862c680bb6272a792..61d45866f61cbabbd9a7ae9975809d342cb76ed5 100644 --- a/assets/icons/debug_step_back.svg +++ b/assets/icons/debug_step_back.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_step_into.svg b/assets/icons/debug_step_into.svg index 02bdd63cb4d0ca5ae7f0e6de167bf8d5c534d560..9a517fc7ca0762b17446a75cd90f39a91e1b51cf 100644 --- a/assets/icons/debug_step_into.svg +++ b/assets/icons/debug_step_into.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_step_out.svg b/assets/icons/debug_step_out.svg index 48190b704b25ba4631b076606eaefd5090e15d24..147a44f930f34f6c3ddce94693a178a932129cb5 100644 --- a/assets/icons/debug_step_out.svg +++ b/assets/icons/debug_step_out.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_step_over.svg b/assets/icons/debug_step_over.svg index 54afac001f3d249af236265da078a92551fb4422..336abc11deb866a128e8418dab47af01b6e4d3f6 100644 --- a/assets/icons/debug_step_over.svg +++ b/assets/icons/debug_step_over.svg @@ -1 +1 @@ - + diff --git a/assets/icons/diff.svg b/assets/icons/diff.svg index 61aa617f5b8ea66c94262f64ca8efb6e6607ae59..9d93b2d5b47f56dd77338c1cf59c912a2cdad294 100644 --- a/assets/icons/diff.svg +++ b/assets/icons/diff.svg @@ -1 +1 @@ - + diff --git a/assets/icons/disconnected.svg b/assets/icons/disconnected.svg index f3069798d0c904fd04313f115a985e70d452a4a2..47bd1db4788825f9475d11a288f5ad715e6de5a5 100644 --- a/assets/icons/disconnected.svg +++ b/assets/icons/disconnected.svg @@ -1 +1 @@ - + diff --git a/assets/icons/download.svg b/assets/icons/download.svg index 6ddcb1e100ec6392ff62c1209f79938cc31f7d8f..6c105d3fd74ac685176f0532e488322c00fb5fef 100644 --- a/assets/icons/download.svg +++ b/assets/icons/download.svg @@ -1 +1 @@ - + diff --git a/assets/icons/envelope.svg b/assets/icons/envelope.svg index 0f5e95f96817aefe0819c4767c7e77b9d1a17638..273cc6de267eeea7b9d893c81df57e806dafe089 100644 --- a/assets/icons/envelope.svg +++ b/assets/icons/envelope.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/eraser.svg b/assets/icons/eraser.svg index 601f2b9b909a8b1c43638ceefdb1a926c842d376..ca6209785fd1a2dd087792c645304dc05d4f4edb 100644 --- a/assets/icons/eraser.svg +++ b/assets/icons/eraser.svg @@ -1 +1 @@ - + diff --git a/assets/icons/escape.svg b/assets/icons/escape.svg index a87f03d2fa07eab2ebaee07cf6800592a0db20c4..1898588a67172f1586c4e40f622b95b8bc971511 100644 --- a/assets/icons/escape.svg +++ b/assets/icons/escape.svg @@ -1 +1 @@ - + diff --git a/assets/icons/exit.svg b/assets/icons/exit.svg index 1ff9d7882441548e9c3534ae5ffe6b6331391b45..3619a55c87083c7e53bc2545d7e243dad7d58eca 100644 --- a/assets/icons/exit.svg +++ b/assets/icons/exit.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/expand_down.svg b/assets/icons/expand_down.svg index 07390aad18525f69f7fa6b39a62bf7d64bfc1503..9f85ee67209ff89d31739219f76b89f24c71afec 100644 --- a/assets/icons/expand_down.svg +++ b/assets/icons/expand_down.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/expand_up.svg b/assets/icons/expand_up.svg index 73c1358b995d4e7dac1d058d66c463ac0616f646..49b084fa8f41df5bc16443081ce1617e7d7e1ef9 100644 --- a/assets/icons/expand_up.svg +++ b/assets/icons/expand_up.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/expand_vertical.svg b/assets/icons/expand_vertical.svg index e2a6dd227e0bdbddf8185b053adaed62ae03115a..5a5fa8ccb52019ccfc1afebd3972ef067634589e 100644 --- a/assets/icons/expand_vertical.svg +++ b/assets/icons/expand_vertical.svg @@ -1 +1 @@ - + diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg index 7f10f738015ab077507266826e32fd88a0024460..327fa751e992167fba8901848fb2a639fda39726 100644 --- a/assets/icons/eye.svg +++ b/assets/icons/eye.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/file.svg b/assets/icons/file.svg index 85f3f543a51c8b8b24dcafa013468b646339740b..60cf2537d9e67321caf6b63f2775c1e6b29e6c32 100644 --- a/assets/icons/file.svg +++ b/assets/icons/file.svg @@ -1 +1 @@ - + diff --git a/assets/icons/file_code.svg b/assets/icons/file_code.svg index b0e632b67f86717cf6588fc293b42ce118345101..548d5a153ba243f7ae6890372ec9aaae765a9124 100644 --- a/assets/icons/file_code.svg +++ b/assets/icons/file_code.svg @@ -1 +1 @@ - + diff --git a/assets/icons/file_diff.svg b/assets/icons/file_diff.svg index d6cb4440eacddda1cc0be91a22f705c758263add..193dd7392ff1ff5cf4281921ffc2eb0b2b4697c9 100644 --- a/assets/icons/file_diff.svg +++ b/assets/icons/file_diff.svg @@ -1 +1 @@ - + diff --git a/assets/icons/file_doc.svg b/assets/icons/file_doc.svg index 3b11995f36759e6928abbc1cfbaa118345f0a21b..ccd5eeea01b01adc8598b0325bbaec935d272ba5 100644 --- a/assets/icons/file_doc.svg +++ b/assets/icons/file_doc.svg @@ -1,6 +1,6 @@ - + - - + + diff --git a/assets/icons/file_generic.svg b/assets/icons/file_generic.svg index 3c72bd3320d9e851641a4eecbc6d7c6bd3e989e3..790a5f18d723939131d2d7100c50020429eb4ff4 100644 --- a/assets/icons/file_generic.svg +++ b/assets/icons/file_generic.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_git.svg b/assets/icons/file_git.svg index 197db2e9e60f260c7a56a6e44c6250c531e0353d..2b36b0ffd3ba1c4389952a35a072027c6dc6de0f 100644 --- a/assets/icons/file_git.svg +++ b/assets/icons/file_git.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/file_icons/ai.svg b/assets/icons/file_icons/ai.svg index d60396ad47db1a2068207c52f783c08cd2da4e69..4236d50337bef92cb550cdbf71d83843ab35e2f3 100644 --- a/assets/icons/file_icons/ai.svg +++ b/assets/icons/file_icons/ai.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/audio.svg b/assets/icons/file_icons/audio.svg index 672f736c958662ca157b165de417076a391ac398..7948b046160e92eb9e0d3cce3e28e3c007ce0a83 100644 --- a/assets/icons/file_icons/audio.svg +++ b/assets/icons/file_icons/audio.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/assets/icons/file_icons/book.svg b/assets/icons/file_icons/book.svg index 3b11995f36759e6928abbc1cfbaa118345f0a21b..ccd5eeea01b01adc8598b0325bbaec935d272ba5 100644 --- a/assets/icons/file_icons/book.svg +++ b/assets/icons/file_icons/book.svg @@ -1,6 +1,6 @@ - + - - + + diff --git a/assets/icons/file_icons/bun.svg b/assets/icons/file_icons/bun.svg index 48af8b3088dd040f6fd5f39d05a9d9e9e8f413ce..ca1ec900bc0a18eb44c9d5e2a810ae2c3730ed8c 100644 --- a/assets/icons/file_icons/bun.svg +++ b/assets/icons/file_icons/bun.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/chevron_down.svg b/assets/icons/file_icons/chevron_down.svg index 9e60e40cf4c6a86f10ed3bc399b068a52208b572..9918f6c9f7188ca0e0de76071649be9a14f36d27 100644 --- a/assets/icons/file_icons/chevron_down.svg +++ b/assets/icons/file_icons/chevron_down.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/chevron_left.svg b/assets/icons/file_icons/chevron_left.svg index a2aa9ad996a432362d2c8382cd7c651ff13151b7..3299ee71684be25aa2a5f8d520c12b5509bbcbca 100644 --- a/assets/icons/file_icons/chevron_left.svg +++ b/assets/icons/file_icons/chevron_left.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/chevron_right.svg b/assets/icons/file_icons/chevron_right.svg index 06608c95ee11ec5fba5b9f5c235d4604001ab440..140f644127da6b3551b03a921c4a29f8daf0077b 100644 --- a/assets/icons/file_icons/chevron_right.svg +++ b/assets/icons/file_icons/chevron_right.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/chevron_up.svg b/assets/icons/file_icons/chevron_up.svg index fd3d5e4470b438119fd5a33245f5583f76dba32a..ae8c12a9899dcc57985c391fc8f382bbba210905 100644 --- a/assets/icons/file_icons/chevron_up.svg +++ b/assets/icons/file_icons/chevron_up.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/code.svg b/assets/icons/file_icons/code.svg index 5f012f883837f689da5c38e905b2eb0b9723945a..af2f6c5dc0e4916dd673c2a8a40f6e9b1cb9aa99 100644 --- a/assets/icons/file_icons/code.svg +++ b/assets/icons/file_icons/code.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/file_icons/coffeescript.svg b/assets/icons/file_icons/coffeescript.svg index fc49df62c0b74c73106cde11fa5766154d31db86..e91d187615b78a8f4cb6c5d2c209b2d772e7344a 100644 --- a/assets/icons/file_icons/coffeescript.svg +++ b/assets/icons/file_icons/coffeescript.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/conversations.svg b/assets/icons/file_icons/conversations.svg index cef764661fed601146b5a659369999d39c3a44d8..e25ed973ef4e47c4bb1a3c434a5398228f98f488 100644 --- a/assets/icons/file_icons/conversations.svg +++ b/assets/icons/file_icons/conversations.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/dart.svg b/assets/icons/file_icons/dart.svg index fd3ab01c93a42d7737a2d5af6aca4e8083372954..c9ec3de51a469fbc68712ce15849c144e48c6616 100644 --- a/assets/icons/file_icons/dart.svg +++ b/assets/icons/file_icons/dart.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/database.svg b/assets/icons/file_icons/database.svg index 10fbdcbff4ccd6b437e0815750a1fa91fe6bf187..a8226110d3775ce2bd784daccfa52633ab0ab597 100644 --- a/assets/icons/file_icons/database.svg +++ b/assets/icons/file_icons/database.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/diff.svg b/assets/icons/file_icons/diff.svg index 07c46f1799604f0ac9581e51760184c142984f3d..ec59a0aabee71abe6fd954e63056425054a4cc60 100644 --- a/assets/icons/file_icons/diff.svg +++ b/assets/icons/file_icons/diff.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/eslint.svg b/assets/icons/file_icons/eslint.svg index 0f42abe691b4ea275b8b74f1bf2e9b9ab2bcc2ca..ba72d9166b29bc5feca600c5111a68ae2357db6b 100644 --- a/assets/icons/file_icons/eslint.svg +++ b/assets/icons/file_icons/eslint.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/file.svg b/assets/icons/file_icons/file.svg index 3c72bd3320d9e851641a4eecbc6d7c6bd3e989e3..790a5f18d723939131d2d7100c50020429eb4ff4 100644 --- a/assets/icons/file_icons/file.svg +++ b/assets/icons/file_icons/file.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/folder.svg b/assets/icons/file_icons/folder.svg index a76dc63d1a663993f02e4b6a88b200e4aea22f0c..e40613000da5ac10282bce1ed74fd6ef07ab566b 100644 --- a/assets/icons/file_icons/folder.svg +++ b/assets/icons/file_icons/folder.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/folder_open.svg b/assets/icons/file_icons/folder_open.svg index ef37f55f83a38f2eb5713ae615407276f76028b7..55231fb6abdb876aa86984fffaf9d3993552ebab 100644 --- a/assets/icons/file_icons/folder_open.svg +++ b/assets/icons/file_icons/folder_open.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/file_icons/font.svg b/assets/icons/file_icons/font.svg index 4cb01a28f27c1a715bad570aa13ef55e6d9a6412..6f2b734b26307eb2ba8584bf6d673bab701a7de2 100644 --- a/assets/icons/file_icons/font.svg +++ b/assets/icons/file_icons/font.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/git.svg b/assets/icons/file_icons/git.svg index 197db2e9e60f260c7a56a6e44c6250c531e0353d..2b36b0ffd3ba1c4389952a35a072027c6dc6de0f 100644 --- a/assets/icons/file_icons/git.svg +++ b/assets/icons/file_icons/git.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/file_icons/gleam.svg b/assets/icons/file_icons/gleam.svg index 6a3dc2c96fe76bee376d6fbd4a72c8d1cc56715d..0399bb4dd2a747845a79bbc78fc1e845768241c0 100644 --- a/assets/icons/file_icons/gleam.svg +++ b/assets/icons/file_icons/gleam.svg @@ -1,7 +1,7 @@ - - + + diff --git a/assets/icons/file_icons/graphql.svg b/assets/icons/file_icons/graphql.svg index 96884725998e29d223b5c7be76d6d1d27cb773c6..e6c0368182e6ed23fb5c36bb1b7d8ad251bc7e53 100644 --- a/assets/icons/file_icons/graphql.svg +++ b/assets/icons/file_icons/graphql.svg @@ -1,6 +1,6 @@ - - + + diff --git a/assets/icons/file_icons/hash.svg b/assets/icons/file_icons/hash.svg index 2241904266fa2f46df1eaeb6956229cf47e553c7..77e6c600725af5387ec1bebb2eb82d4eff6fa756 100644 --- a/assets/icons/file_icons/hash.svg +++ b/assets/icons/file_icons/hash.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/file_icons/heroku.svg b/assets/icons/file_icons/heroku.svg index 826a88646bf3753bd106308b3e211142c1b65280..732adf72cb6097543946d738e0110ccd75115faf 100644 --- a/assets/icons/file_icons/heroku.svg +++ b/assets/icons/file_icons/heroku.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/html.svg b/assets/icons/file_icons/html.svg index 41f254dd681530e29f443c0aa78c36ea440b4a69..8832bcba3a71bf8369b7f39913e9a24857e032e4 100644 --- a/assets/icons/file_icons/html.svg +++ b/assets/icons/file_icons/html.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/image.svg b/assets/icons/file_icons/image.svg index 75e64c0a43a06570f4806944c390f406deeef5a6..c89de1b1285ed22959e39b1b6c6ce21b444b90e9 100644 --- a/assets/icons/file_icons/image.svg +++ b/assets/icons/file_icons/image.svg @@ -1,7 +1,7 @@ - - - + + + diff --git a/assets/icons/file_icons/java.svg b/assets/icons/file_icons/java.svg index 63ce6e768c835007013875c338d52279b8cfe515..70d2d10ed7b8e09d1e6195858d5ec50cb4e7de03 100644 --- a/assets/icons/file_icons/java.svg +++ b/assets/icons/file_icons/java.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/file_icons/lock.svg b/assets/icons/file_icons/lock.svg index 6bfef249b4516f3fbbf7f1a4c220b0fe893367d5..10ae33869a610714a66763683b38ff91ea9fa074 100644 --- a/assets/icons/file_icons/lock.svg +++ b/assets/icons/file_icons/lock.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/magnifying_glass.svg b/assets/icons/file_icons/magnifying_glass.svg index 75c3e76c80b5c1c577881d9fb7a942f162e395d3..d0440d905c35bce2960f4f9691a585c1d91e91fd 100644 --- a/assets/icons/file_icons/magnifying_glass.svg +++ b/assets/icons/file_icons/magnifying_glass.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/nix.svg b/assets/icons/file_icons/nix.svg index 879a4d76aac461739afb97ba6b1d00240c3b4490..215d58a035c2306111665ad7ee2b169950ee59ec 100644 --- a/assets/icons/file_icons/nix.svg +++ b/assets/icons/file_icons/nix.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/assets/icons/file_icons/notebook.svg b/assets/icons/file_icons/notebook.svg index b72ebc3967c8944163a62be92442315b706a8093..968d5c598297c794c7bf5cc86535ac3a3fa67daf 100644 --- a/assets/icons/file_icons/notebook.svg +++ b/assets/icons/file_icons/notebook.svg @@ -1,8 +1,8 @@ - - - - - + + + + + diff --git a/assets/icons/file_icons/package.svg b/assets/icons/file_icons/package.svg index 12889e80845869a6ea2453fb60619fa01a578a0b..16bbccb2e63788c0bf442ccd54f34f63a58c28be 100644 --- a/assets/icons/file_icons/package.svg +++ b/assets/icons/file_icons/package.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/phoenix.svg b/assets/icons/file_icons/phoenix.svg index b61b8beda7ba55e19f47b88e6e5a8bed9ddc02a3..5db68b4e44b0d13aaad3818f6b7635a8b4cd937f 100644 --- a/assets/icons/file_icons/phoenix.svg +++ b/assets/icons/file_icons/phoenix.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/plus.svg b/assets/icons/file_icons/plus.svg index f343d5dd87bf8fe4841de35fa09207906bae9a07..3449da3ecd70868f387937f2f4015c4a63ef2798 100644 --- a/assets/icons/file_icons/plus.svg +++ b/assets/icons/file_icons/plus.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/prettier.svg b/assets/icons/file_icons/prettier.svg index 835bd3a1267886671a2e7a28a2efc596d74bb4bf..f01230c33c4eb0de68731d074c8045b4ca877781 100644 --- a/assets/icons/file_icons/prettier.svg +++ b/assets/icons/file_icons/prettier.svg @@ -1,12 +1,12 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/assets/icons/file_icons/project.svg b/assets/icons/file_icons/project.svg index 86a15d41bc41f3652a82ee5d67fd275ecd8c02fc..509cc5f4d0a4d88f392864ada4c02928c4a9c431 100644 --- a/assets/icons/file_icons/project.svg +++ b/assets/icons/file_icons/project.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/python.svg b/assets/icons/file_icons/python.svg index de904d8e046a143b16283bbbbdfaf7010e22aed1..b44fdc539d4f08bb49e3810eadfff4c3d3abaf08 100644 --- a/assets/icons/file_icons/python.svg +++ b/assets/icons/file_icons/python.svg @@ -1,6 +1,6 @@ - - + + diff --git a/assets/icons/file_icons/replace.svg b/assets/icons/file_icons/replace.svg index 837cb23b669e2aceca4e27b69eb7ebceb08a9ca4..287328e82e7fc91f697ca19b6b006ee78f08ebd4 100644 --- a/assets/icons/file_icons/replace.svg +++ b/assets/icons/file_icons/replace.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/replace_next.svg b/assets/icons/file_icons/replace_next.svg index 72511be70a2567c627e11b398bc95a591569675e..a9a9fc91f5816649aa4312285fb90e856558ee07 100644 --- a/assets/icons/file_icons/replace_next.svg +++ b/assets/icons/file_icons/replace_next.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/rust.svg b/assets/icons/file_icons/rust.svg index 5db753628af10c679f347c863cf9819b3f9afa14..9e4dc57adb4458f7d860182287fa9b766cd6d1d8 100644 --- a/assets/icons/file_icons/rust.svg +++ b/assets/icons/file_icons/rust.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/scala.svg b/assets/icons/file_icons/scala.svg index 9e89d1fa82338b0647f60283ab2bd8cfc8cc9850..0884cc96f4702cdf1ab5d4c7f1bf496752f82834 100644 --- a/assets/icons/file_icons/scala.svg +++ b/assets/icons/file_icons/scala.svg @@ -1,7 +1,7 @@ - + diff --git a/assets/icons/file_icons/settings.svg b/assets/icons/file_icons/settings.svg index 081d25bf482472bc6b1315b012644e8bcf16279f..d308135ff1fdc05166c83e595350facab31f5d30 100644 --- a/assets/icons/file_icons/settings.svg +++ b/assets/icons/file_icons/settings.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/tcl.svg b/assets/icons/file_icons/tcl.svg index bb15b0f8e743c0555a6937cfc1a526ced1e5bb95..1bd7c4a5513dea6e375018d0323e8d4d2f013b8f 100644 --- a/assets/icons/file_icons/tcl.svg +++ b/assets/icons/file_icons/tcl.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/toml.svg b/assets/icons/file_icons/toml.svg index 9ab78af50f9302615ec56535debe3794c2b73503..ae31911d6a659daec785a717926b0e4281683a69 100644 --- a/assets/icons/file_icons/toml.svg +++ b/assets/icons/file_icons/toml.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/video.svg b/assets/icons/file_icons/video.svg index b96e359edbd859a33b4a3d83f93cb1395de60454..c249d4c82b0bbf6ccd17ed6c576ceba34f837a28 100644 --- a/assets/icons/file_icons/video.svg +++ b/assets/icons/file_icons/video.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/file_icons/vue.svg b/assets/icons/file_icons/vue.svg index 1cbe08dff52068688c16e773b283a715c8904ce6..1f993e90ef7f1103ecac0be40bff27566c75e338 100644 --- a/assets/icons/file_icons/vue.svg +++ b/assets/icons/file_icons/vue.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_lock.svg b/assets/icons/file_lock.svg index 6bfef249b4516f3fbbf7f1a4c220b0fe893367d5..10ae33869a610714a66763683b38ff91ea9fa074 100644 --- a/assets/icons/file_lock.svg +++ b/assets/icons/file_lock.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_markdown.svg b/assets/icons/file_markdown.svg index e26d7a532d5bbd2b3a7ff43325ff705d8ab222bd..26688a3db0aa000889436fdf59e63cac2af7b743 100644 --- a/assets/icons/file_markdown.svg +++ b/assets/icons/file_markdown.svg @@ -1 +1 @@ - + diff --git a/assets/icons/file_rust.svg b/assets/icons/file_rust.svg index 5db753628af10c679f347c863cf9819b3f9afa14..9e4dc57adb4458f7d860182287fa9b766cd6d1d8 100644 --- a/assets/icons/file_rust.svg +++ b/assets/icons/file_rust.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_text_outlined.svg b/assets/icons/file_text_outlined.svg index bb9b85d62f42c63b7042231d1eecc6baee8c83cc..d2e8897251e31b5ef10d009bbce7aba3a16521fe 100644 --- a/assets/icons/file_text_outlined.svg +++ b/assets/icons/file_text_outlined.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/file_toml.svg b/assets/icons/file_toml.svg index 9ab78af50f9302615ec56535debe3794c2b73503..ae31911d6a659daec785a717926b0e4281683a69 100644 --- a/assets/icons/file_toml.svg +++ b/assets/icons/file_toml.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_tree.svg b/assets/icons/file_tree.svg index 74acb1fc257a559a5aad1a3718e851159b735953..baf0e26ce6d8e88a18fd0496e708842e9d9394c3 100644 --- a/assets/icons/file_tree.svg +++ b/assets/icons/file_tree.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/filter.svg b/assets/icons/filter.svg index 7391fea132eac0e394cce97f0b1e630e2255f87d..4aa14e93c003d0770e973656c8af81af16d84b89 100644 --- a/assets/icons/filter.svg +++ b/assets/icons/filter.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/flame.svg b/assets/icons/flame.svg index 3215f0d5aee240cc1ae04a896e7e498d513b4aa9..89fc6cab1ef07336d7ae5886c9cbcbec5d419e49 100644 --- a/assets/icons/flame.svg +++ b/assets/icons/flame.svg @@ -1 +1 @@ - + diff --git a/assets/icons/folder.svg b/assets/icons/folder.svg index 0d76b7e3f8bd75aee66f7683a30b01052bc69dfe..35f4c1f8acf6796b14c2fb575e2181b771253706 100644 --- a/assets/icons/folder.svg +++ b/assets/icons/folder.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/folder_open.svg b/assets/icons/folder_open.svg index ef37f55f83a38f2eb5713ae615407276f76028b7..55231fb6abdb876aa86984fffaf9d3993552ebab 100644 --- a/assets/icons/folder_open.svg +++ b/assets/icons/folder_open.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/folder_search.svg b/assets/icons/folder_search.svg index d1bc537c98bb8d4029f2c368297fad51557e966e..207ea5c10e823929cc957e038487cb0f9d2f89ac 100644 --- a/assets/icons/folder_search.svg +++ b/assets/icons/folder_search.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/font.svg b/assets/icons/font.svg index 1cc569ecb7b61a67d4f001b4e41fed663c0173d0..47633a58c93feb5f80da5bf7bd15a382a74ee975 100644 --- a/assets/icons/font.svg +++ b/assets/icons/font.svg @@ -1 +1 @@ - + diff --git a/assets/icons/font_size.svg b/assets/icons/font_size.svg index fd983cb5d3cdf6c69cd81a6845af352937a7b44f..4286277bd900596d861a857a3b1e28b66d53d678 100644 --- a/assets/icons/font_size.svg +++ b/assets/icons/font_size.svg @@ -1 +1 @@ - + diff --git a/assets/icons/font_weight.svg b/assets/icons/font_weight.svg index 73b9852e2fbb674e1bdb1772fd3ea94870c448c5..410f43ec6e983f20e1f697c4a8f47253999ee786 100644 --- a/assets/icons/font_weight.svg +++ b/assets/icons/font_weight.svg @@ -1 +1 @@ - + diff --git a/assets/icons/forward_arrow.svg b/assets/icons/forward_arrow.svg index 503b0b309bfca1c3841de97fc2648bf8a9d26eab..e51796e5546a55b20a31f8bcaa9fbf8da8cb1b25 100644 --- a/assets/icons/forward_arrow.svg +++ b/assets/icons/forward_arrow.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/git_branch.svg b/assets/icons/git_branch.svg index 811bc7476211e61ba3d23bfde1e3fed1d6a419f5..fc6dcfe1b275974e64c292e56e7f962aa67cde06 100644 --- a/assets/icons/git_branch.svg +++ b/assets/icons/git_branch.svg @@ -1 +1 @@ - + diff --git a/assets/icons/git_branch_alt.svg b/assets/icons/git_branch_alt.svg index d18b072512c305c88d9daa5861b2413fbd163481..cf40195d8b2faaea629b04ec2430bd9e8afeff5f 100644 --- a/assets/icons/git_branch_alt.svg +++ b/assets/icons/git_branch_alt.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/github.svg b/assets/icons/github.svg index fe9186872b27c1c748170ed375f1b327967c95c9..0a12c9b656f659b010d2aaa4f1f89290368d1941 100644 --- a/assets/icons/github.svg +++ b/assets/icons/github.svg @@ -1 +1 @@ - + diff --git a/assets/icons/hash.svg b/assets/icons/hash.svg index 9e4dd7c0689f4945f8059884ebf82a9b8eaa3c64..afc1f9c0b50ceaca0bae9e5c6772d5d53665e31b 100644 --- a/assets/icons/hash.svg +++ b/assets/icons/hash.svg @@ -1 +1 @@ - + diff --git a/assets/icons/history_rerun.svg b/assets/icons/history_rerun.svg index 9ade606b31ed0bb646925210ee7c522e77208ca2..e11e754318192b25362ccc5a3a5ef61b25b1a474 100644 --- a/assets/icons/history_rerun.svg +++ b/assets/icons/history_rerun.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/image.svg b/assets/icons/image.svg index 0a26c35182b2aed46fcad8845f3f6d56bc7cfe52..e0d73d76212f38aef9a2a61eb5fb48447db717b6 100644 --- a/assets/icons/image.svg +++ b/assets/icons/image.svg @@ -1 +1 @@ - + diff --git a/assets/icons/info.svg b/assets/icons/info.svg index f3d2e6644ff2d7119965a08a1a1cfd45e2bf6f0b..c000f25867c4092fe08ed01a48a18e6ce07b2784 100644 --- a/assets/icons/info.svg +++ b/assets/icons/info.svg @@ -1,5 +1,5 @@ - - + + diff --git a/assets/icons/json.svg b/assets/icons/json.svg index 5f012f883837f689da5c38e905b2eb0b9723945a..af2f6c5dc0e4916dd673c2a8a40f6e9b1cb9aa99 100644 --- a/assets/icons/json.svg +++ b/assets/icons/json.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/keyboard.svg b/assets/icons/keyboard.svg index de9afd9561a3d332a63f16d8e245b860e3d04130..82791cda3fe31192f3be66fd6f27d2ca3c068bdd 100644 --- a/assets/icons/keyboard.svg +++ b/assets/icons/keyboard.svg @@ -1 +1 @@ - + diff --git a/assets/icons/knockouts/x_fg.svg b/assets/icons/knockouts/x_fg.svg index a3d47f13735e734ca2f801618a4edbee02ea457b..f459954f729f3b80c50b64ec7ad0547d534235e9 100644 --- a/assets/icons/knockouts/x_fg.svg +++ b/assets/icons/knockouts/x_fg.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/library.svg b/assets/icons/library.svg index ed59e1818b4b33f284df570d4faf3c72cf9acf63..fc7f5afcd2fa45626033d8cd7f9963e28b5f8d31 100644 --- a/assets/icons/library.svg +++ b/assets/icons/library.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/line_height.svg b/assets/icons/line_height.svg index 7afa70f767a3e1053129235d06c1423d629fa4e4..3929fc408001022def1e54c60d193058bfa4aabd 100644 --- a/assets/icons/line_height.svg +++ b/assets/icons/line_height.svg @@ -1 +1 @@ - + diff --git a/assets/icons/list_collapse.svg b/assets/icons/list_collapse.svg index 938799b1513fd5625f940cad99839501b4fee837..f18bc550b90228c2f689848b86cfc5bea3d6ff50 100644 --- a/assets/icons/list_collapse.svg +++ b/assets/icons/list_collapse.svg @@ -1 +1 @@ - + diff --git a/assets/icons/list_todo.svg b/assets/icons/list_todo.svg index 019af957347a4a5f2f70c69be0dab084887a6f07..709f26d89dbb5b5e95869bf4daa41fa5ad230ba9 100644 --- a/assets/icons/list_todo.svg +++ b/assets/icons/list_todo.svg @@ -1 +1 @@ - + diff --git a/assets/icons/list_tree.svg b/assets/icons/list_tree.svg index 09872a60f7ed9c85e89f06b7384b083a7f4b5779..de3e0f3a57b0e0edfc38cfa2b8b364529a741cea 100644 --- a/assets/icons/list_tree.svg +++ b/assets/icons/list_tree.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/list_x.svg b/assets/icons/list_x.svg index 206faf2ce45dee9b94333ee747f18262a2c6baa5..0fa3bd68fbf362ebb769f840aecf320f13c219da 100644 --- a/assets/icons/list_x.svg +++ b/assets/icons/list_x.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/load_circle.svg b/assets/icons/load_circle.svg index 825aa335b00961e77d4f615897a5d7914cccced8..eecf099310e17daeb9e953a816dbca389447292a 100644 --- a/assets/icons/load_circle.svg +++ b/assets/icons/load_circle.svg @@ -1 +1 @@ - + diff --git a/assets/icons/location_edit.svg b/assets/icons/location_edit.svg index 02cd6f3389a499a84e37070aa110f6150fce94f0..e342652eb153caaea18cb40b1154e8444e8554ec 100644 --- a/assets/icons/location_edit.svg +++ b/assets/icons/location_edit.svg @@ -1 +1 @@ - + diff --git a/assets/icons/lock_outlined.svg b/assets/icons/lock_outlined.svg index 0bfd2fdc82ad6cfd21e9fd2c901a7604fb6c0ba9..d69a2456031113e6451b046acc71deaf559f3dc7 100644 --- a/assets/icons/lock_outlined.svg +++ b/assets/icons/lock_outlined.svg @@ -1,6 +1,6 @@ - - + + - + diff --git a/assets/icons/magnifying_glass.svg b/assets/icons/magnifying_glass.svg index b7c22e64bd219c63a9cdfbd9b187bbf49d6a2e8f..24f00bb51bccc34a61a55bea0aa2fcdabfc99b60 100644 --- a/assets/icons/magnifying_glass.svg +++ b/assets/icons/magnifying_glass.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg index ee03a2c0210586a0cf0744df051414451e92f2f6..7b6d26fed8fd0a5074def7afe880bb26274afe0a 100644 --- a/assets/icons/maximize.svg +++ b/assets/icons/maximize.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/menu.svg b/assets/icons/menu.svg index 0724fb2816f6dd891779bcb82da35755fb7f521d..f12ce47f7e9d6208accfd97d66ce8b977b36080e 100644 --- a/assets/icons/menu.svg +++ b/assets/icons/menu.svg @@ -1 +1 @@ - + diff --git a/assets/icons/menu_alt.svg b/assets/icons/menu_alt.svg index b605e094e37749f64b9d3df55adce3830a2d7eb3..f73102e286c51e5c52fcec40cb976a3bd6a981cf 100644 --- a/assets/icons/menu_alt.svg +++ b/assets/icons/menu_alt.svg @@ -1 +1 @@ - + diff --git a/assets/icons/mic.svg b/assets/icons/mic.svg index 1d9c5bc9edf2a48b3311965fb57758b3ee2e015e..000d135ea54a539be0d381ac22fff334e1bc24df 100644 --- a/assets/icons/mic.svg +++ b/assets/icons/mic.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/mic_mute.svg b/assets/icons/mic_mute.svg index 8c61ae2f1ccedc1b27244ed80e1a3fdd75cd4120..8bc63be610baf13539957a218c0fd9af3425a12f 100644 --- a/assets/icons/mic_mute.svg +++ b/assets/icons/mic_mute.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg index ea825f054ed1813aff11d2838b2c0e8e2211d717..082ade47dbdab16ea69bc1e00f6e0c7bd5a8d936 100644 --- a/assets/icons/minimize.svg +++ b/assets/icons/minimize.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/notepad.svg b/assets/icons/notepad.svg index 48875eedee635472b19e17afaa49e38813c4d58d..27fd35566eee2141946658d315a155016e5ac345 100644 --- a/assets/icons/notepad.svg +++ b/assets/icons/notepad.svg @@ -1 +1 @@ - + diff --git a/assets/icons/option.svg b/assets/icons/option.svg index 676c10c93b78222ad42656cdf8f35e9443ab482a..47201f7c671e4503cf597252973ebe4c4e3b5c7d 100644 --- a/assets/icons/option.svg +++ b/assets/icons/option.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/pencil.svg b/assets/icons/pencil.svg index b913015c08ae5e7fc2ceb6011bd89925cedc27fe..c4d289e9c06c7fdc6d4e1875f4170c87ef6ea425 100644 --- a/assets/icons/pencil.svg +++ b/assets/icons/pencil.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/person.svg b/assets/icons/person.svg index c64167830378093dce799498d528edfcfd9bc6c4..a1c29e4acb29a52c5fb6f08e75875306026c63f0 100644 --- a/assets/icons/person.svg +++ b/assets/icons/person.svg @@ -1 +1 @@ - + diff --git a/assets/icons/pin.svg b/assets/icons/pin.svg index f3f50cc65953d2dc3b3ce038da708de213c6058a..d23daff8b988a5882f95ef62c3de5d99612d9317 100644 --- a/assets/icons/pin.svg +++ b/assets/icons/pin.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/play_filled.svg b/assets/icons/play_filled.svg index c632434305c6bd25da205ca8cee8203b9d3611b1..8075197ad2ae94fe3ffec1a2685e90f8b57ea513 100644 --- a/assets/icons/play_filled.svg +++ b/assets/icons/play_filled.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/play_outlined.svg b/assets/icons/play_outlined.svg index 7e1cacd5af8795501cc30f4e33927f752a1eba7f..ba1ea2693d61646623a998b7d34ae0ca2d716cef 100644 --- a/assets/icons/play_outlined.svg +++ b/assets/icons/play_outlined.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg index e26d430320eae364d012a2482c39de19fce4ed2a..8ac57d8cdde017ef51d622cf8b63af644b3de332 100644 --- a/assets/icons/plus.svg +++ b/assets/icons/plus.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/power.svg b/assets/icons/power.svg index 23f6f48f30befb236346b06d99fe877862d151e1..29bd2127c58e2e940ad044e18c2767d8826de6d5 100644 --- a/assets/icons/power.svg +++ b/assets/icons/power.svg @@ -1 +1 @@ - + diff --git a/assets/icons/public.svg b/assets/icons/public.svg index 574ee1010db1f27d59843c27632091afefa4cb0a..5659b5419f7d4df12ab45d8a7dfbc954ccd4c131 100644 --- a/assets/icons/public.svg +++ b/assets/icons/public.svg @@ -1 +1 @@ - + diff --git a/assets/icons/pull_request.svg b/assets/icons/pull_request.svg index ccfaaacfdcb28a25a73d278b7d61195eb28fd299..515462ab64406cf22d83717304cc40293ab21230 100644 --- a/assets/icons/pull_request.svg +++ b/assets/icons/pull_request.svg @@ -1 +1 @@ - + diff --git a/assets/icons/quote.svg b/assets/icons/quote.svg index 5564a60f95e34c16c6d4e820f5ea2901c7884707..a958bc67f2a7c04611499b53d4c25bec5ad1f2ff 100644 --- a/assets/icons/quote.svg +++ b/assets/icons/quote.svg @@ -1 +1 @@ - + diff --git a/assets/icons/reader.svg b/assets/icons/reader.svg index 2ccc37623d9daa4b5f6c79748a2e4bf3f7a03067..f477f4f32d83ee2ea7a364ac2d22bf7ce72a9f5a 100644 --- a/assets/icons/reader.svg +++ b/assets/icons/reader.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/refresh_title.svg b/assets/icons/refresh_title.svg index 8a8fdb04f395749c2cdbf509c6bd38f1bcaae623..c9e670bfabe7940d6936d9e767fa23d73d4703b5 100644 --- a/assets/icons/refresh_title.svg +++ b/assets/icons/refresh_title.svg @@ -1 +1 @@ - + diff --git a/assets/icons/regex.svg b/assets/icons/regex.svg index 0432cd570fe2341829c40f5d4e629e3b27e24379..818c2ba360bc5aca3d4a7bf8ab65a03a2efe235e 100644 --- a/assets/icons/regex.svg +++ b/assets/icons/regex.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/repl_neutral.svg b/assets/icons/repl_neutral.svg index d9c8b001df15bc3812084be29460d43733deffb1..2842e2c4210085cb930efee43aa3340a4d628d6a 100644 --- a/assets/icons/repl_neutral.svg +++ b/assets/icons/repl_neutral.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/repl_off.svg b/assets/icons/repl_off.svg index ac249ad5ffa687c2cbf369ff5bb5434f234960fa..3018ceaf8588cd9b545397d6167d5d68d8e33bfa 100644 --- a/assets/icons/repl_off.svg +++ b/assets/icons/repl_off.svg @@ -1,11 +1,11 @@ - - - - - - - - - + + + + + + + + + diff --git a/assets/icons/repl_pause.svg b/assets/icons/repl_pause.svg index 5273ed60bb5126cb8f1331b9b82651a06e4c9157..5a69a576c1152d71a714252d0cc66699eb39b1b3 100644 --- a/assets/icons/repl_pause.svg +++ b/assets/icons/repl_pause.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/assets/icons/repl_play.svg b/assets/icons/repl_play.svg index 76c292a38236fbe63df045c05ec6737a7a207ebe..0c8f4b0832ba2d74ae793751328e9927e45c950f 100644 --- a/assets/icons/repl_play.svg +++ b/assets/icons/repl_play.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/replace.svg b/assets/icons/replace.svg index 837cb23b669e2aceca4e27b69eb7ebceb08a9ca4..287328e82e7fc91f697ca19b6b006ee78f08ebd4 100644 --- a/assets/icons/replace.svg +++ b/assets/icons/replace.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/replace_next.svg b/assets/icons/replace_next.svg index 72511be70a2567c627e11b398bc95a591569675e..a9a9fc91f5816649aa4312285fb90e856558ee07 100644 --- a/assets/icons/replace_next.svg +++ b/assets/icons/replace_next.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/rerun.svg b/assets/icons/rerun.svg index a5daa5de1d748069422a47618076115050c56182..1a03a01ae6403e92565bc85a92eaddcb9edaa601 100644 --- a/assets/icons/rerun.svg +++ b/assets/icons/rerun.svg @@ -1 +1 @@ - + diff --git a/assets/icons/return.svg b/assets/icons/return.svg index aed9242a95bd1f830a5c3722af0dca7412491b07..c605eb6512b3f4314cc1d42936213ad4eef5b041 100644 --- a/assets/icons/return.svg +++ b/assets/icons/return.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/rotate_ccw.svg b/assets/icons/rotate_ccw.svg index 8f6bd6346a067dd1b8dafc46dd71c579ebd61729..cdfa8d0ab4d649e6255061faa54d337f038bc611 100644 --- a/assets/icons/rotate_ccw.svg +++ b/assets/icons/rotate_ccw.svg @@ -1 +1 @@ - + diff --git a/assets/icons/rotate_cw.svg b/assets/icons/rotate_cw.svg index b082096ee4be635dac44bc3308fc64d5ad5ebd0b..2adfa7f972b71b4d7b9194d4dc9488745dc18ce9 100644 --- a/assets/icons/rotate_cw.svg +++ b/assets/icons/rotate_cw.svg @@ -1 +1 @@ - + diff --git a/assets/icons/scissors.svg b/assets/icons/scissors.svg index 430293f9138947b35a96ff04db27db657320f64d..a19580bd89d3b3da56df2089f2de7dbd0cd981fe 100644 --- a/assets/icons/scissors.svg +++ b/assets/icons/scissors.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/screen.svg b/assets/icons/screen.svg index 4b686b58f9de2e4993546ddad1a20af395d50330..4bcdf19528a799b4725f617d56d1f84baa33c904 100644 --- a/assets/icons/screen.svg +++ b/assets/icons/screen.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/select_all.svg b/assets/icons/select_all.svg index c15973c419df4426bc47c8d07736a768ffab9d99..4fa17dcf6371838974f393021a39953afbbdcfdf 100644 --- a/assets/icons/select_all.svg +++ b/assets/icons/select_all.svg @@ -1 +1 @@ - + diff --git a/assets/icons/send.svg b/assets/icons/send.svg index 1403a43ff54b25d4424d5a66b3a00199fa8e1b6d..5ceeef2af4721301bdb3e92e06c019aa4440a39f 100644 --- a/assets/icons/send.svg +++ b/assets/icons/send.svg @@ -1 +1 @@ - + diff --git a/assets/icons/server.svg b/assets/icons/server.svg index bde19efd75bb11015ba48687a471d633257c8440..8d851d1328d60b0ba45f89a70fb97ff49bf5b732 100644 --- a/assets/icons/server.svg +++ b/assets/icons/server.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg index 617b14b3cde91918315801220294224c13b47e2a..33ac74f2300ed4ebad1e9ea4f523a10bb4865eea 100644 --- a/assets/icons/settings.svg +++ b/assets/icons/settings.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/shield_check.svg b/assets/icons/shield_check.svg index 6e58c314682a5e87de9b2ca582262a3110f7006d..43b52f43a8d70beb6e69c2271235090db4dc2c00 100644 --- a/assets/icons/shield_check.svg +++ b/assets/icons/shield_check.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/shift.svg b/assets/icons/shift.svg index 35dc2f144cff68641c37eae6d64bb016e55c498c..c38807d8b0b434ff4dd868decfd1d79ef772f720 100644 --- a/assets/icons/shift.svg +++ b/assets/icons/shift.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/slash.svg b/assets/icons/slash.svg index e2313f0099f9158c18c65c2f928a7301a277bcd6..1ebf01eb9f13af5b449dfdfcd075f759ac4a1d1f 100644 --- a/assets/icons/slash.svg +++ b/assets/icons/slash.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/sliders.svg b/assets/icons/sliders.svg index 8ab83055eef53a07c84cca255aeca505b07f47c2..20a6a367dc4963f3ecdfe6611076fd6462faa764 100644 --- a/assets/icons/sliders.svg +++ b/assets/icons/sliders.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/assets/icons/space.svg b/assets/icons/space.svg index 86bd55cd537bf49c955130fedb65ce148bec092d..0294c9bf1e64d9b0e0521b88981c7e704bba28e6 100644 --- a/assets/icons/space.svg +++ b/assets/icons/space.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/sparkle.svg b/assets/icons/sparkle.svg index e5cce9fafdb699e3e7144cd468f7f4747343c9de..535c447723cee7913c8fc40494c49d24760f7569 100644 --- a/assets/icons/sparkle.svg +++ b/assets/icons/sparkle.svg @@ -1 +1 @@ - + diff --git a/assets/icons/split.svg b/assets/icons/split.svg index eb031ab790cbea7fb12d43b9e28a1b5884ab84f6..b2be46a875b461928715129b560062aacd1fc5f4 100644 --- a/assets/icons/split.svg +++ b/assets/icons/split.svg @@ -1,5 +1,5 @@ - - + + diff --git a/assets/icons/split_alt.svg b/assets/icons/split_alt.svg index 5b99b7a26a44f4a97b05fcbcec02eae41554098e..2f99e1436fb71220f6853d7e56f3e748b53fff12 100644 --- a/assets/icons/split_alt.svg +++ b/assets/icons/split_alt.svg @@ -1 +1 @@ - + diff --git a/assets/icons/square_dot.svg b/assets/icons/square_dot.svg index 4bb684afb296218d9fe61a6f803c0aeaa3cb75af..72b32734399a2d9e47ea520a93b6891f0d4664f6 100644 --- a/assets/icons/square_dot.svg +++ b/assets/icons/square_dot.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/square_minus.svg b/assets/icons/square_minus.svg index 4b8fc4d982500fea1c548b6e01c6b80ba90050ee..5ba458e8b53bf6df71a95bd2326e7b1323bae161 100644 --- a/assets/icons/square_minus.svg +++ b/assets/icons/square_minus.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/square_plus.svg b/assets/icons/square_plus.svg index e0ee106b525196d267640a6173e3faddf1858e0b..063c7dbf8261d98957e3b835f4d8262b155dc396 100644 --- a/assets/icons/square_plus.svg +++ b/assets/icons/square_plus.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/star.svg b/assets/icons/star.svg index fd1502ede8a19ea3781c64430f4b10bc2475a630..b39638e386e9913ab12983ebd0805cc9128a955b 100644 --- a/assets/icons/star.svg +++ b/assets/icons/star.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/star_filled.svg b/assets/icons/star_filled.svg index d7de9939db2a57f19497e91ac7f1420c6c698fef..16f64e5cb33c17f879022341094be59316eb5135 100644 --- a/assets/icons/star_filled.svg +++ b/assets/icons/star_filled.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/stop.svg b/assets/icons/stop.svg index 41e4fd35e913f774a4b3674e8505b90ab787cbc6..cc2bbe9207acf5acd44ff13e93140099d222250b 100644 --- a/assets/icons/stop.svg +++ b/assets/icons/stop.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/swatch_book.svg b/assets/icons/swatch_book.svg index 99a1c88bd5fcede4bf0a7638fcf7c6896eecd025..b37d5df8c1a5f0f6b9fa9cb46b3004a2ba55da4f 100644 --- a/assets/icons/swatch_book.svg +++ b/assets/icons/swatch_book.svg @@ -1 +1 @@ - + diff --git a/assets/icons/tab.svg b/assets/icons/tab.svg index f16d51ccf5ae25bc2670d5ad959f2fb0cdca4e9c..db93be4df53cb01e07f1a66773b9118e68ed6609 100644 --- a/assets/icons/tab.svg +++ b/assets/icons/tab.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/terminal_alt.svg b/assets/icons/terminal_alt.svg index 82d88167b2a50fbadc36151354fed6fd65432a69..d03c05423e24fa8d7c8604050545a8ddd26cc9be 100644 --- a/assets/icons/terminal_alt.svg +++ b/assets/icons/terminal_alt.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/text_snippet.svg b/assets/icons/text_snippet.svg index 12f131fdd526f5d2cc86ac40a80d4fb5fea7983f..b8987546d323de794bf0b7a974162c81a48e5135 100644 --- a/assets/icons/text_snippet.svg +++ b/assets/icons/text_snippet.svg @@ -1 +1 @@ - + diff --git a/assets/icons/text_thread.svg b/assets/icons/text_thread.svg index 75afa934a028f1bddd104effe536db70ad4f241c..aa078c72a2f35d2b82e90f2be64d23fcda3418a5 100644 --- a/assets/icons/text_thread.svg +++ b/assets/icons/text_thread.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/thread.svg b/assets/icons/thread.svg index 8c2596a4c9fca9f75a122dc85225f33696320030..496cf42e3a3ee1439f36b8e2479d05564362e628 100644 --- a/assets/icons/thread.svg +++ b/assets/icons/thread.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/thread_from_summary.svg b/assets/icons/thread_from_summary.svg index 7519935affc03bf50e9a39bcb5792237fba1e44f..94ce9562da15e2abc53912a8069f1d4e3f3dd3d8 100644 --- a/assets/icons/thread_from_summary.svg +++ b/assets/icons/thread_from_summary.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/thumbs_down.svg b/assets/icons/thumbs_down.svg index 334115a014d386b18f800f3f3ad59b9f8cad49de..a396ff14f614158382172ff3a8ed682612db541c 100644 --- a/assets/icons/thumbs_down.svg +++ b/assets/icons/thumbs_down.svg @@ -1 +1 @@ - + diff --git a/assets/icons/thumbs_up.svg b/assets/icons/thumbs_up.svg index b1e435936b3cd46433e58b98fbf586468b288ff4..73c859c3557c17dd6fe962dec147ae3275a9aae9 100644 --- a/assets/icons/thumbs_up.svg +++ b/assets/icons/thumbs_up.svg @@ -1 +1 @@ - + diff --git a/assets/icons/todo_complete.svg b/assets/icons/todo_complete.svg index d50044e4351126305321ab7ce3afdb2814b78244..5bf70841a8f2876ac2955c6731d66cbfa2f8a7dd 100644 --- a/assets/icons/todo_complete.svg +++ b/assets/icons/todo_complete.svg @@ -1 +1 @@ - + diff --git a/assets/icons/todo_pending.svg b/assets/icons/todo_pending.svg index dfb013b52b987a3f99e1b8304418b847ff1ccf2b..e5e9776f11b2ebdaed8ab42039d1a8de80f29ccb 100644 --- a/assets/icons/todo_pending.svg +++ b/assets/icons/todo_pending.svg @@ -1,10 +1,10 @@ - - - - - - - - + + + + + + + + diff --git a/assets/icons/todo_progress.svg b/assets/icons/todo_progress.svg index 9b2ed7375d9807139261a2d81f7f1f168470d0f4..b4a3e8c50e75343435849323d49d3c45cfe3069c 100644 --- a/assets/icons/todo_progress.svg +++ b/assets/icons/todo_progress.svg @@ -1,11 +1,11 @@ - - - - - - - - - + + + + + + + + + diff --git a/assets/icons/tool_copy.svg b/assets/icons/tool_copy.svg index e722d8a022fca603b87fc1859436fcc060355095..a497a5c9cba29861710e3810029f2936aa2f02b1 100644 --- a/assets/icons/tool_copy.svg +++ b/assets/icons/tool_copy.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/tool_delete_file.svg b/assets/icons/tool_delete_file.svg index 3276f3d78e8ca1bb6d79a58845577cb150f545aa..e15c0cb568eae4274a9621ac403aa3393b1d5287 100644 --- a/assets/icons/tool_delete_file.svg +++ b/assets/icons/tool_delete_file.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/tool_diagnostics.svg b/assets/icons/tool_diagnostics.svg index c659d967812727450bc3efb825b6492e6d2eda50..414810628d96cbb6fa662e359d0dec3581afa022 100644 --- a/assets/icons/tool_diagnostics.svg +++ b/assets/icons/tool_diagnostics.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/tool_folder.svg b/assets/icons/tool_folder.svg index 0d76b7e3f8bd75aee66f7683a30b01052bc69dfe..35f4c1f8acf6796b14c2fb575e2181b771253706 100644 --- a/assets/icons/tool_folder.svg +++ b/assets/icons/tool_folder.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/tool_hammer.svg b/assets/icons/tool_hammer.svg index e66173ce70f39416bbfdbfdb97dfa6f99e1ef3b7..f725012cdf211fcdd0f739e2693caac4da300b49 100644 --- a/assets/icons/tool_hammer.svg +++ b/assets/icons/tool_hammer.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/tool_notification.svg b/assets/icons/tool_notification.svg index 7510b3204000d714e8fb120179cbfc521e1abdd8..7903a3369a5a620fdbefc3a7f1cfe72ee0b98c6f 100644 --- a/assets/icons/tool_notification.svg +++ b/assets/icons/tool_notification.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/tool_pencil.svg b/assets/icons/tool_pencil.svg index b913015c08ae5e7fc2ceb6011bd89925cedc27fe..c4d289e9c06c7fdc6d4e1875f4170c87ef6ea425 100644 --- a/assets/icons/tool_pencil.svg +++ b/assets/icons/tool_pencil.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/tool_read.svg b/assets/icons/tool_read.svg index 458cbb36607a308ae4ce5e6a98006f9ff87461e8..d22e9d8c7da9ba04fe194339d787e40637cf5257 100644 --- a/assets/icons/tool_read.svg +++ b/assets/icons/tool_read.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/tool_regex.svg b/assets/icons/tool_regex.svg index 0432cd570fe2341829c40f5d4e629e3b27e24379..818c2ba360bc5aca3d4a7bf8ab65a03a2efe235e 100644 --- a/assets/icons/tool_regex.svg +++ b/assets/icons/tool_regex.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/tool_search.svg b/assets/icons/tool_search.svg index 4f2750cfa2624ff4419c159fda5b62a515b43113..b225a1298eeb627c420cf19c801dded83fffb097 100644 --- a/assets/icons/tool_search.svg +++ b/assets/icons/tool_search.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/tool_terminal.svg b/assets/icons/tool_terminal.svg index 3c4ab42a4dc06f7b2a9aaefff8145764a876d117..24da5e3a10bc47c8a7c73173c21a1ec2c6cf21b6 100644 --- a/assets/icons/tool_terminal.svg +++ b/assets/icons/tool_terminal.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/tool_think.svg b/assets/icons/tool_think.svg index 595f8070d8b6d30ade68b1bff41c141b43050394..efd5908a907b21c573ebc69fc13f5a210ab5d848 100644 --- a/assets/icons/tool_think.svg +++ b/assets/icons/tool_think.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/tool_web.svg b/assets/icons/tool_web.svg index 6250a9f05ab53d2bc364dc7520d10ee319f29f1f..288b54c432dcb4336161d779771c04b045eb6b4f 100644 --- a/assets/icons/tool_web.svg +++ b/assets/icons/tool_web.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/trash.svg b/assets/icons/trash.svg index 1322e90f9fdc1fad9901febff0f71a938621f900..4a9e9add021be23727ec7c8c69a98a593a6bece7 100644 --- a/assets/icons/trash.svg +++ b/assets/icons/trash.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/undo.svg b/assets/icons/undo.svg index b2407456dcf49102c8cbd4d7ad0580d6cff44535..c714b58747e950ab75d3a02be7eebfe7cd83eda1 100644 --- a/assets/icons/undo.svg +++ b/assets/icons/undo.svg @@ -1 +1 @@ - + diff --git a/assets/icons/user_check.svg b/assets/icons/user_check.svg index cd682b5eda44247245efc278babe4657078c50ab..ee32a525909a738ffa21b75a2a9690fa5ee8dcaf 100644 --- a/assets/icons/user_check.svg +++ b/assets/icons/user_check.svg @@ -1 +1 @@ - + diff --git a/assets/icons/user_group.svg b/assets/icons/user_group.svg index ac1f7bdc633190f88b202d9e5ae7430af225aecd..30d2e5a7eac519246fe4ee176d107bb2b8cd6598 100644 --- a/assets/icons/user_group.svg +++ b/assets/icons/user_group.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/user_round_pen.svg b/assets/icons/user_round_pen.svg index eb755173231ee3ed41147e2ef612e9d716496c66..e684fd1a2006e5435e93e2b6db27d5584ce41090 100644 --- a/assets/icons/user_round_pen.svg +++ b/assets/icons/user_round_pen.svg @@ -1 +1 @@ - + diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg index 456799fa5ae761e04fc4c20d4d31bd7afe5479aa..5af37dab9db2c07c4d6ff505d03e62348d078f53 100644 --- a/assets/icons/warning.svg +++ b/assets/icons/warning.svg @@ -1 +1 @@ - + diff --git a/assets/icons/whole_word.svg b/assets/icons/whole_word.svg index 77cecce38c5700a4bc983f005f05de390e45a521..ce0d1606c8552f002d7bf58ad7a778a3be9561af 100644 --- a/assets/icons/whole_word.svg +++ b/assets/icons/whole_word.svg @@ -1 +1 @@ - + diff --git a/assets/icons/x_circle.svg b/assets/icons/x_circle.svg index 69aaa3f6a166be2834f0db5813041ab86b25e758..8807e5fa1fe6912982ab271744f17d13026c13ad 100644 --- a/assets/icons/x_circle.svg +++ b/assets/icons/x_circle.svg @@ -1 +1 @@ - + diff --git a/assets/icons/zed_assistant.svg b/assets/icons/zed_assistant.svg index d21252de8c234611ddd41caff287e3fc0d540ed3..470eb0fedeab7535287db64b601b5dfd99b6c05d 100644 --- a/assets/icons/zed_assistant.svg +++ b/assets/icons/zed_assistant.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/zed_burn_mode.svg b/assets/icons/zed_burn_mode.svg index f6192d16e7d3cd0a081fa745524c08b31875e80c..cad6ed666be5edbb1b2d6dced7d0e8990ac90d68 100644 --- a/assets/icons/zed_burn_mode.svg +++ b/assets/icons/zed_burn_mode.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/zed_burn_mode_on.svg b/assets/icons/zed_burn_mode_on.svg index 29a74a3e636a22a568f45bc40c1397ed033ce10d..10e0e42b1302f5323adee6724c3d43cc59a82d31 100644 --- a/assets/icons/zed_burn_mode_on.svg +++ b/assets/icons/zed_burn_mode_on.svg @@ -1 +1 @@ - + diff --git a/assets/icons/zed_mcp_custom.svg b/assets/icons/zed_mcp_custom.svg index 6410a26fcade9d5be5dd494eb24efa6b5985724a..feff2d7d34fb71d4d9064ae0cf5075216f969f75 100644 --- a/assets/icons/zed_mcp_custom.svg +++ b/assets/icons/zed_mcp_custom.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/zed_mcp_extension.svg b/assets/icons/zed_mcp_extension.svg index 996e0c1920c206f1d4ace11179069f77bc103300..00117efcf4e20cb368824ee248b671caedad0b3b 100644 --- a/assets/icons/zed_mcp_extension.svg +++ b/assets/icons/zed_mcp_extension.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/zed_predict.svg b/assets/icons/zed_predict.svg index 79fd8c8fc132d7a2d7bb966086a3ff62c819c5b0..605a0584d52b3163158610ae9a96fbe96fc60806 100644 --- a/assets/icons/zed_predict.svg +++ b/assets/icons/zed_predict.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/zed_predict_down.svg b/assets/icons/zed_predict_down.svg index 4532ad7e26cab76bc4e52a68ea5c766a1ffdca81..79eef9b0b4ad3bc2678371120bfbf5154cdd32fd 100644 --- a/assets/icons/zed_predict_down.svg +++ b/assets/icons/zed_predict_down.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/zed_predict_error.svg b/assets/icons/zed_predict_error.svg index b2dc339fe954f4bbb146d792bc653e2c402a3818..6f75326179bf3a6663ff28bfaecb80bf29878d42 100644 --- a/assets/icons/zed_predict_error.svg +++ b/assets/icons/zed_predict_error.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/zed_predict_up.svg b/assets/icons/zed_predict_up.svg index 61ec143022b4f785affa5183d549a750bd741ab2..f77001e4bddf3094532708ef0313e8b938434781 100644 --- a/assets/icons/zed_predict_up.svg +++ b/assets/icons/zed_predict_up.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/crates/icons/README.md b/crates/icons/README.md index 71bc5c85459604243207c68686da5662ceabeddc..e340a00277db558b4bb13b212d53188c0c8fbe5a 100644 --- a/crates/icons/README.md +++ b/crates/icons/README.md @@ -6,7 +6,7 @@ Icons are a big part of Zed, and they're how we convey hundreds of actions witho When introducing a new icon, it's important to ensure consistency with the existing set, which follows these guidelines: 1. The SVG view box should be 16x16. -2. For outlined icons, use a 1.5px stroke width. +2. For outlined icons, use a 1.2px stroke width. 3. Not all icons are mathematically aligned; there's quite a bit of optical adjustment. However, try to keep the icon within an internal 12x12 bounding box as much as possible while ensuring proper visibility. 4. Use the `filled` and `outlined` terminology when introducing icons that will have these two variants. 5. Icons that are deeply contextual may have the feature context as their name prefix. For example, `ToolWeb`, `ReplPlay`, `DebugStepInto`, etc. From 9cd13a35de2fb658fee3af30a4863333816828b8 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sun, 17 Aug 2025 13:39:14 -0300 Subject: [PATCH 073/744] agent2: Experiment with new toolbar design (#36366) Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 72 ++++++++++++++------------- crates/agent_ui/src/thread_history.rs | 1 + 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 44d605af57f6e94e0277d1d7645e22029614f192..b01bf39728f672434ea1d875b6426649edde62a4 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -65,8 +65,8 @@ use theme::ThemeSettings; use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ - Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu, - PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, + Banner, Callout, ContextMenu, ContextMenuEntry, Divider, ElevationIndex, KeyBinding, + PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, }; use util::ResultExt as _; use workspace::{ @@ -243,9 +243,9 @@ pub enum AgentType { impl AgentType { fn label(self) -> impl Into { match self { - Self::Zed | Self::TextThread => "Zed", + Self::Zed | Self::TextThread => "Zed Agent", Self::NativeAgent => "Agent 2", - Self::Gemini => "Gemini", + Self::Gemini => "Google Gemini", Self::ClaudeCode => "Claude Code", } } @@ -1784,7 +1784,8 @@ impl AgentPanel { .w_full() .child(change_title_editor.clone()) .child( - ui::IconButton::new("retry-summary-generation", IconName::RotateCcw) + IconButton::new("retry-summary-generation", IconName::RotateCcw) + .icon_size(IconSize::Small) .on_click({ let active_thread = active_thread.clone(); move |_, _window, cx| { @@ -1836,7 +1837,8 @@ impl AgentPanel { .w_full() .child(title_editor.clone()) .child( - ui::IconButton::new("retry-summary-generation", IconName::RotateCcw) + IconButton::new("retry-summary-generation", IconName::RotateCcw) + .icon_size(IconSize::Small) .on_click({ let context_editor = context_editor.clone(); move |_, _window, cx| { @@ -1974,21 +1976,17 @@ impl AgentPanel { }) } - fn render_recent_entries_menu( - &self, - icon: IconName, - cx: &mut Context, - ) -> impl IntoElement { + fn render_recent_entries_menu(&self, cx: &mut Context) -> impl IntoElement { let focus_handle = self.focus_handle(cx); PopoverMenu::new("agent-nav-menu") .trigger_with_tooltip( - IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small), + IconButton::new("agent-nav-menu", IconName::MenuAlt).icon_size(IconSize::Small), { let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( - "Toggle Panel Menu", + "Toggle Recent Threads", &ToggleNavigationMenu, &focus_handle, window, @@ -2124,9 +2122,7 @@ impl AgentPanel { .pl(DynamicSpacing::Base04.rems(cx)) .child(self.render_toolbar_back_button(cx)) .into_any_element(), - _ => self - .render_recent_entries_menu(IconName::MenuAlt, cx) - .into_any_element(), + _ => self.render_recent_entries_menu(cx).into_any_element(), }) .child(self.render_title_view(window, cx)), ) @@ -2364,6 +2360,22 @@ impl AgentPanel { } }); + let selected_agent_label = self.selected_agent.label().into(); + let selected_agent = div() + .id("selected_agent_icon") + .px(DynamicSpacing::Base02.rems(cx)) + .child(Icon::new(self.selected_agent.icon()).color(Color::Muted)) + .tooltip(move |window, cx| { + Tooltip::with_meta( + selected_agent_label.clone(), + None, + "Selected Agent", + window, + cx, + ) + }) + .into_any_element(); + h_flex() .id("agent-panel-toolbar") .h(Tab::container_height(cx)) @@ -2377,26 +2389,17 @@ impl AgentPanel { .child( h_flex() .size_full() - .gap(DynamicSpacing::Base08.rems(cx)) + .gap(DynamicSpacing::Base04.rems(cx)) + .pl(DynamicSpacing::Base04.rems(cx)) .child(match &self.active_view { - ActiveView::History | ActiveView::Configuration => div() - .pl(DynamicSpacing::Base04.rems(cx)) - .child(self.render_toolbar_back_button(cx)) - .into_any_element(), + ActiveView::History | ActiveView::Configuration => { + self.render_toolbar_back_button(cx).into_any_element() + } _ => h_flex() - .h_full() - .px(DynamicSpacing::Base04.rems(cx)) - .border_r_1() - .border_color(cx.theme().colors().border) - .child( - h_flex() - .px_0p5() - .gap_1p5() - .child( - Icon::new(self.selected_agent.icon()).color(Color::Muted), - ) - .child(Label::new(self.selected_agent.label())), - ) + .gap_1() + .child(self.render_recent_entries_menu(cx)) + .child(Divider::vertical()) + .child(selected_agent) .into_any_element(), }) .child(self.render_title_view(window, cx)), @@ -2415,7 +2418,6 @@ impl AgentPanel { .border_l_1() .border_color(cx.theme().colors().border) .child(new_thread_menu) - .child(self.render_recent_entries_menu(IconName::HistoryRerun, cx)) .child(self.render_panel_options_menu(window, cx)), ), ) diff --git a/crates/agent_ui/src/thread_history.rs b/crates/agent_ui/src/thread_history.rs index b8d1db88d6e3164b32ade0f2137ad7ca37a0650a..66afe2c2c5835387f10d095c7ee9649bda177f0b 100644 --- a/crates/agent_ui/src/thread_history.rs +++ b/crates/agent_ui/src/thread_history.rs @@ -541,6 +541,7 @@ impl Render for ThreadHistory { v_flex() .key_context("ThreadHistory") .size_full() + .bg(cx.theme().colors().panel_background) .on_action(cx.listener(Self::select_previous)) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_first)) From 46a2d8d95aad9e0070f683050703bec384f2fec4 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sun, 17 Aug 2025 14:03:58 -0300 Subject: [PATCH 074/744] git: Refine clone repo modal design (#36369) Release Notes: - N/A --- crates/git_ui/src/git_ui.rs | 87 +++++++++----------- crates/recent_projects/src/remote_servers.rs | 7 +- 2 files changed, 41 insertions(+), 53 deletions(-) diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 79aa4a6bd0f828ab28ea89dcd26e5ab9b7ef8c2d..3b4196b8ec3191e5e993552c9b97a200bf711a34 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -3,7 +3,7 @@ use std::any::Any; use ::settings::Settings; use command_palette_hooks::CommandPaletteFilter; use commit_modal::CommitModal; -use editor::{Editor, EditorElement, EditorStyle, actions::DiffClipboardWithSelectionData}; +use editor::{Editor, actions::DiffClipboardWithSelectionData}; mod blame_ui; use git::{ repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus}, @@ -11,12 +11,11 @@ use git::{ }; use git_panel_settings::GitPanelSettings; use gpui::{ - Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, TextStyle, - Window, actions, + Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Window, + actions, }; use onboarding::GitOnboardingModal; use project_diff::ProjectDiff; -use theme::ThemeSettings; use ui::prelude::*; use workspace::{ModalView, Workspace}; use zed_actions; @@ -637,7 +636,7 @@ impl GitCloneModal { pub fn show(panel: Entity, window: &mut Window, cx: &mut Context) -> Self { let repo_input = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Enter repository", cx); + editor.set_placeholder_text("Enter repository URL…", cx); editor }); let focus_handle = repo_input.focus_handle(cx); @@ -650,46 +649,6 @@ impl GitCloneModal { focus_handle, } } - - fn render_editor(&self, window: &Window, cx: &App) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - let theme = cx.theme(); - - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_size: settings.buffer_font_size(cx).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(settings.buffer_line_height.value()), - background_color: Some(theme.colors().editor_background), - ..Default::default() - }; - - let element = EditorElement::new( - &self.repo_input, - EditorStyle { - background: theme.colors().editor_background, - local_player: theme.players().local(), - text: text_style, - ..Default::default() - }, - ); - - div() - .rounded_md() - .p_1() - .border_1() - .border_color(theme.colors().border_variant) - .when( - self.repo_input - .focus_handle(cx) - .contains_focused(window, cx), - |this| this.border_color(theme.colors().border_focused), - ) - .child(element) - .bg(theme.colors().editor_background) - } } impl Focusable for GitCloneModal { @@ -699,12 +658,42 @@ impl Focusable for GitCloneModal { } impl Render for GitCloneModal { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { div() - .size_full() - .w(rems(34.)) .elevation_3(cx) - .child(self.render_editor(window, cx)) + .w(rems(34.)) + .flex_1() + .overflow_hidden() + .child( + div() + .w_full() + .p_2() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child(self.repo_input.clone()), + ) + .child( + h_flex() + .w_full() + .p_2() + .gap_0p5() + .rounded_b_sm() + .bg(cx.theme().colors().editor_background) + .child( + Label::new("Clone a repository from GitHub or other sources.") + .color(Color::Muted) + .size(LabelSize::Small), + ) + .child( + Button::new("learn-more", "Learn More") + .label_size(LabelSize::Small) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .on_click(|_, _, cx| { + cx.open_url("https://github.com/git-guides/git-clone"); + }), + ), + ) .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| { cx.emit(DismissEvent); })) diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index e5e166cb4cc02761c42af42bf36f1c0ed8a7cce0..81259c1aac8bfdb61fe6d7e8d537bfbc6c06a56a 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1094,11 +1094,10 @@ impl RemoteServerProjects { .size(LabelSize::Small), ) .child( - Button::new("learn-more", "Learn more…") + Button::new("learn-more", "Learn More") .label_size(LabelSize::Small) - .size(ButtonSize::None) - .color(Color::Accent) - .style(ButtonStyle::Transparent) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) .on_click(|_, _, cx| { cx.open_url( "https://zed.dev/docs/remote-development", From 8282b9cf000d3636fd69d29a00260edb6edecd63 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sun, 17 Aug 2025 14:27:42 -0300 Subject: [PATCH 075/744] project panel: Add git clone action to empty state (#36371) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds the git clone action to the project panel. It also changes the "open" button to open a folder instead of the recent projects modal, which feels faster to start with, more intuitive, and also consistent with VS Code (which I think is good in this specific case). CleanShot 2025-08-17 at 2  10 01@2x Release Notes: - Improved the project panel empty state by including the git clone action and allowing users to quickly open a local folder. --- crates/onboarding/src/welcome.rs | 2 +- crates/project_panel/src/project_panel.rs | 41 ++++++++++++++++++----- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index ba0053a3b68cf880918dcc60618dc4440e168968..610f6a98e322b24207777aa7f307b848e3a49f3c 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -37,7 +37,7 @@ const CONTENT: (Section<4>, Section<3>) = ( }, SectionEntry { icon: IconName::CloudDownload, - title: "Clone a Repo", + title: "Clone Repository", action: &git::Clone, }, SectionEntry { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 4d7f2faf62d455adccb69e88cc69a8fc64529fa9..d5ddd89419b4955a031d904655c40eaa105d0b3c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -57,9 +57,9 @@ use std::{ }; use theme::ThemeSettings; use ui::{ - Color, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind, IndentGuideColors, - IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, ScrollableHandle, - Scrollbar, ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex, + Color, ContextMenu, DecoratedIcon, Divider, Icon, IconDecoration, IconDecorationKind, + IndentGuideColors, IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, + ScrollableHandle, Scrollbar, ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex, }; use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths}; use workspace::{ @@ -69,7 +69,6 @@ use workspace::{ notifications::{DetachAndPromptErr, NotifyTaskExt}, }; use worktree::CreatedEntry; -use zed_actions::OpenRecent; const PROJECT_PANEL_KEY: &str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; @@ -5521,24 +5520,48 @@ impl Render for ProjectPanel { .with_priority(3) })) } else { + let focus_handle = self.focus_handle(cx).clone(); + v_flex() .id("empty-project_panel") - .size_full() .p_4() + .size_full() + .items_center() + .justify_center() + .gap_1() .track_focus(&self.focus_handle(cx)) .child( - Button::new("open_project", "Open a project") + Button::new("open_project", "Open Project") .full_width() .key_binding(KeyBinding::for_action_in( - &OpenRecent::default(), - &self.focus_handle, + &workspace::Open, + &focus_handle, window, cx, )) .on_click(cx.listener(|this, _, window, cx| { this.workspace .update(cx, |_, cx| { - window.dispatch_action(OpenRecent::default().boxed_clone(), cx); + window.dispatch_action(workspace::Open.boxed_clone(), cx); + }) + .log_err(); + })), + ) + .child( + h_flex() + .w_1_2() + .gap_2() + .child(Divider::horizontal()) + .child(Label::new("or").size(LabelSize::XSmall).color(Color::Muted)) + .child(Divider::horizontal()), + ) + .child( + Button::new("clone_repo", "Clone Repository") + .full_width() + .on_click(cx.listener(|this, _, window, cx| { + this.workspace + .update(cx, |_, cx| { + window.dispatch_action(git::Clone.boxed_clone(), cx); }) .log_err(); })), From 2dbc951058fe0b2325bca2452da330f2bafa34d7 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sun, 17 Aug 2025 16:38:07 -0400 Subject: [PATCH 076/744] agent2: Start loading mentioned threads and text threads as soon as they're added (#36374) Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/agent_ui/src/acp/message_editor.rs | 298 ++++++++++++++++------ 1 file changed, 216 insertions(+), 82 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index f6fee3b87e2375cc6429391683999b522b602828..12766ef458d112d277b9b22c80ffa959fe0e2a16 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -207,11 +207,13 @@ impl MessageEditor { cx, ); } - MentionUri::Symbol { .. } - | MentionUri::Thread { .. } - | MentionUri::TextThread { .. } - | MentionUri::Rule { .. } - | MentionUri::Selection { .. } => { + MentionUri::Thread { id, name } => { + self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx); + } + MentionUri::TextThread { path, name } => { + self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx); + } + MentionUri::Symbol { .. } | MentionUri::Rule { .. } | MentionUri::Selection { .. } => { self.mention_set.insert_uri(crease_id, mention_uri.clone()); } } @@ -363,13 +365,9 @@ impl MessageEditor { window: &mut Window, cx: &mut Context, ) -> Task>> { - let contents = self.mention_set.contents( - self.project.clone(), - self.thread_store.clone(), - self.text_thread_store.clone(), - window, - cx, - ); + let contents = + self.mention_set + .contents(self.project.clone(), self.thread_store.clone(), window, cx); let editor = self.editor.clone(); cx.spawn(async move |_, cx| { @@ -591,52 +589,154 @@ impl MessageEditor { ) { let editor = self.editor.clone(); let task = cx - .spawn_in(window, async move |this, cx| { - let image = image.await.map_err(|e| e.to_string())?; - let format = image.format; - let image = cx - .update(|_, cx| LanguageModelImage::from_image(image, cx)) - .map_err(|e| e.to_string())? - .await; - if let Some(image) = image { - if let Some(abs_path) = abs_path.clone() { - this.update(cx, |this, _cx| { - this.mention_set.insert_uri( - crease_id, - MentionUri::File { - abs_path, - is_directory: false, - }, - ); + .spawn_in(window, { + let abs_path = abs_path.clone(); + async move |_, cx| { + let image = image.await.map_err(|e| e.to_string())?; + let format = image.format; + let image = cx + .update(|_, cx| LanguageModelImage::from_image(image, cx)) + .map_err(|e| e.to_string())? + .await; + if let Some(image) = image { + Ok(MentionImage { + abs_path, + data: image.source, + format, }) - .map_err(|e| e.to_string())?; + } else { + Err("Failed to convert image".into()) } - Ok(MentionImage { - abs_path, - data: image.source, - format, + } + }) + .shared(); + + self.mention_set.insert_image(crease_id, task.clone()); + + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + if let Some(abs_path) = abs_path.clone() { + this.update(cx, |this, _cx| { + this.mention_set.insert_uri( + crease_id, + MentionUri::File { + abs_path, + is_directory: false, + }, + ); }) - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - Err("Failed to convert image".to_string()) + .ok(); } + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + } + }) + .detach(); + } + + fn confirm_mention_for_thread( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + id: ThreadId, + name: String, + window: &mut Window, + cx: &mut Context, + ) { + let uri = MentionUri::Thread { + id: id.clone(), + name, + }; + let open_task = self.thread_store.update(cx, |thread_store, cx| { + thread_store.open_thread(&id, window, cx) + }); + let task = cx + .spawn(async move |_, cx| { + let thread = open_task.await.map_err(|e| e.to_string())?; + let content = thread + .read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text()) + .map_err(|e| e.to_string())?; + Ok(content) }) .shared(); - cx.spawn_in(window, { - let task = task.clone(); - async move |_, cx| task.clone().await.notify_async_err(cx) + self.mention_set.insert_thread(id, task.clone()); + + let editor = self.editor.clone(); + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + this.update(cx, |this, _| { + this.mention_set.insert_uri(crease_id, uri); + }) + .ok(); + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + } }) .detach(); + } - self.mention_set.insert_image(crease_id, task); + fn confirm_mention_for_text_thread( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + path: PathBuf, + name: String, + window: &mut Window, + cx: &mut Context, + ) { + let uri = MentionUri::TextThread { + path: path.clone(), + name, + }; + let context = self.text_thread_store.update(cx, |text_thread_store, cx| { + text_thread_store.open_local_context(path.as_path().into(), cx) + }); + let task = cx + .spawn(async move |_, cx| { + let context = context.await.map_err(|e| e.to_string())?; + let xml = context + .update(cx, |context, cx| context.to_xml(cx)) + .map_err(|e| e.to_string())?; + Ok(xml) + }) + .shared(); + + self.mention_set.insert_text_thread(path, task.clone()); + + let editor = self.editor.clone(); + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + this.update(cx, |this, _| { + this.mention_set.insert_uri(crease_id, uri); + }) + .ok(); + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + } + }) + .detach(); } pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context) { @@ -671,7 +771,7 @@ impl MessageEditor { let start = text.len(); write!(&mut text, "{}", mention_uri.as_link()).ok(); let end = text.len(); - mentions.push((start..end, mention_uri)); + mentions.push((start..end, mention_uri, resource.text)); } } acp::ContentBlock::Image(content) => { @@ -691,7 +791,7 @@ impl MessageEditor { editor.buffer().read(cx).snapshot(cx) }); - for (range, mention_uri) in mentions { + for (range, mention_uri, text) in mentions { let anchor = snapshot.anchor_before(range.start); let crease_id = crate::context_picker::insert_crease_for_mention( anchor.excerpt_id, @@ -705,7 +805,26 @@ impl MessageEditor { ); if let Some(crease_id) = crease_id { - self.mention_set.insert_uri(crease_id, mention_uri); + self.mention_set.insert_uri(crease_id, mention_uri.clone()); + } + + match mention_uri { + MentionUri::Thread { id, .. } => { + self.mention_set + .insert_thread(id, Task::ready(Ok(text.into())).shared()); + } + MentionUri::TextThread { path, .. } => { + self.mention_set + .insert_text_thread(path, Task::ready(Ok(text)).shared()); + } + MentionUri::Fetch { url } => { + self.mention_set + .add_fetch_result(url, Task::ready(Ok(text)).shared()); + } + MentionUri::File { .. } + | MentionUri::Symbol { .. } + | MentionUri::Rule { .. } + | MentionUri::Selection { .. } => {} } } for (range, content) in images { @@ -905,9 +1024,11 @@ pub struct MentionImage { #[derive(Default)] pub struct MentionSet { - pub(crate) uri_by_crease_id: HashMap, + uri_by_crease_id: HashMap, fetch_results: HashMap>>>, images: HashMap>>>, + thread_summaries: HashMap>>>, + text_thread_summaries: HashMap>>>, } impl MentionSet { @@ -927,8 +1048,18 @@ impl MentionSet { self.images.insert(crease_id, task); } + fn insert_thread(&mut self, id: ThreadId, task: Shared>>) { + self.thread_summaries.insert(id, task); + } + + fn insert_text_thread(&mut self, path: PathBuf, task: Shared>>) { + self.text_thread_summaries.insert(path, task); + } + pub fn drain(&mut self) -> impl Iterator { self.fetch_results.clear(); + self.thread_summaries.clear(); + self.text_thread_summaries.clear(); self.uri_by_crease_id .drain() .map(|(id, _)| id) @@ -939,8 +1070,7 @@ impl MentionSet { &self, project: Entity, thread_store: Entity, - text_thread_store: Entity, - window: &mut Window, + _window: &mut Window, cx: &mut App, ) -> Task>> { let mut processed_image_creases = HashSet::default(); @@ -1010,30 +1140,40 @@ impl MentionSet { anyhow::Ok((crease_id, Mention::Text { uri, content })) }) } - MentionUri::Thread { id: thread_id, .. } => { - let open_task = thread_store.update(cx, |thread_store, cx| { - thread_store.open_thread(&thread_id, window, cx) - }); - + MentionUri::Thread { id, .. } => { + let Some(content) = self.thread_summaries.get(id).cloned() else { + return Task::ready(Err(anyhow!("missing thread summary"))); + }; let uri = uri.clone(); - cx.spawn(async move |cx| { - let thread = open_task.await?; - let content = thread.read_with(cx, |thread, _cx| { - thread.latest_detailed_summary_or_text().to_string() - })?; - - anyhow::Ok((crease_id, Mention::Text { uri, content })) + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Text { + uri, + content: content + .await + .map_err(|e| anyhow::anyhow!("{e}"))? + .to_string(), + }, + )) }) } MentionUri::TextThread { path, .. } => { - let context = text_thread_store.update(cx, |text_thread_store, cx| { - text_thread_store.open_local_context(path.as_path().into(), cx) - }); + let Some(content) = self.text_thread_summaries.get(path).cloned() else { + return Task::ready(Err(anyhow!("missing text thread summary"))); + }; let uri = uri.clone(); - cx.spawn(async move |cx| { - let context = context.await?; - let xml = context.update(cx, |context, cx| context.to_xml(cx))?; - anyhow::Ok((crease_id, Mention::Text { uri, content: xml })) + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Text { + uri, + content: content + .await + .map_err(|e| anyhow::anyhow!("{e}"))? + .to_string(), + }, + )) }) } MentionUri::Rule { id: prompt_id, .. } => { @@ -1427,7 +1567,6 @@ mod tests { message_editor.mention_set().contents( project.clone(), thread_store.clone(), - text_thread_store.clone(), window, cx, ) @@ -1495,7 +1634,6 @@ mod tests { message_editor.mention_set().contents( project.clone(), thread_store.clone(), - text_thread_store.clone(), window, cx, ) @@ -1616,13 +1754,9 @@ mod tests { let contents = message_editor .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - project.clone(), - thread_store, - text_thread_store, - window, - cx, - ) + message_editor + .mention_set() + .contents(project.clone(), thread_store, window, cx) }) .await .unwrap() From 7dc4adbd4027b9b3ba80db589f93be25dcaaa64d Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 18 Aug 2025 08:16:17 +0530 Subject: [PATCH 077/744] gpui: Fix crash when starting Zed on macOS during texture creation (#36382) Closes #36229 Fix zero-sized texture creation that triggers a SIGABRT in the Metal renderer. Not sure why this happens yet, but it likely occurs when `native_window.contentView()` returns a zero `NSSize` during initial window creation, before the view size is computed. Release Notes: - Fixed a rare startup crash on macOS. --- crates/gpui/src/platform/mac/metal_renderer.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 629654014d5a15632c5992d9347cab3ee1fd28d9..a686d8c45bf846fe7f36123cb559e5c412bf1783 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -314,6 +314,15 @@ impl MetalRenderer { } fn update_path_intermediate_textures(&mut self, size: Size) { + // We are uncertain when this happens, but sometimes size can be 0 here. Most likely before + // the layout pass on window creation. Zero-sized texture creation causes SIGABRT. + // https://github.com/zed-industries/zed/issues/36229 + if size.width.0 <= 0 || size.height.0 <= 0 { + self.path_intermediate_texture = None; + self.path_intermediate_msaa_texture = None; + return; + } + let texture_descriptor = metal::TextureDescriptor::new(); texture_descriptor.set_width(size.width.0 as u64); texture_descriptor.set_height(size.height.0 as u64); From b3969ed427d44077595a329032969f35dc28c0fb Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 18 Aug 2025 06:07:32 +0200 Subject: [PATCH 078/744] Standardize on canceled instead of cancelled (#36385) Release Notes: - N/A --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 14 +++++++------- crates/agent/src/thread.rs | 4 ++-- crates/agent/src/tool_use.rs | 6 +++--- crates/agent_servers/src/acp/v1.rs | 2 +- crates/agent_servers/src/claude.rs | 17 ++++++++--------- crates/agent_ui/src/active_thread.rs | 2 +- crates/agent_ui/src/message_editor.rs | 6 +++--- 9 files changed, 28 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4bf705eb9aa27be9ae6637ce3e3f8c34b18151e..a4f8c521a1e46b6c312069102bb184e6a5ecbae7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.25" +version = "0.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab66add8be8d6a963f5bf4070045c1bbf36472837654c73e2298dd16bda5bf7" +checksum = "160971bb53ca0b2e70ebc857c21e24eb448745f1396371015f4c59e9a9e51ed0" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index b3105bd97c9eb59e43fda3bf714defef4aee0b09..14691cf8a4f3d723e99710b72807ff931c8b7da2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -423,7 +423,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.25" +agent-client-protocol = "0.0.26" 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 c1c634612b47dade0a089de8b35ee862900e41dc..fb312653265a408f9ab98a06449d572ab5063714 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -360,7 +360,7 @@ pub enum ToolCallStatus { Failed, /// The user rejected the tool call. Rejected, - /// The user cancelled generation so the tool call was cancelled. + /// The user canceled generation so the tool call was canceled. Canceled, } @@ -1269,19 +1269,19 @@ impl AcpThread { Err(e) } result => { - let cancelled = matches!( + let canceled = matches!( result, Ok(Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Cancelled + stop_reason: acp::StopReason::Canceled })) ); - // We only take the task if the current prompt wasn't cancelled. + // We only take the task if the current prompt wasn't canceled. // - // This prompt may have been cancelled because another one was sent + // This prompt may have been canceled because another one was sent // while it was still generating. In these cases, dropping `send_task` - // would cause the next generation to be cancelled. - if !cancelled { + // would cause the next generation to be canceled. + if !canceled { this.send_task.take(); } diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index f3f10884830c5c87a3a9e8e34b99c1197feb7756..549184218517a0b9e4915187dea30174021182b2 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -5337,7 +5337,7 @@ fn main() {{ } #[gpui::test] - async fn test_retry_cancelled_on_stop(cx: &mut TestAppContext) { + async fn test_retry_canceled_on_stop(cx: &mut TestAppContext) { init_test_settings(cx); let project = create_test_project(cx, json!({})).await; @@ -5393,7 +5393,7 @@ fn main() {{ "Should have no pending completions after cancellation" ); - // Verify the retry was cancelled by checking retry state + // Verify the retry was canceled by checking retry state thread.read_with(cx, |thread, _| { if let Some(retry_state) = &thread.retry_state { panic!( diff --git a/crates/agent/src/tool_use.rs b/crates/agent/src/tool_use.rs index 7392c0878d17adf8038292b10a7a8c349d3ec4e8..74dfaf9a85852d151554df9439a53fee90ec5686 100644 --- a/crates/agent/src/tool_use.rs +++ b/crates/agent/src/tool_use.rs @@ -137,7 +137,7 @@ impl ToolUseState { } pub fn cancel_pending(&mut self) -> Vec { - let mut cancelled_tool_uses = Vec::new(); + let mut canceled_tool_uses = Vec::new(); self.pending_tool_uses_by_id .retain(|tool_use_id, tool_use| { if matches!(tool_use.status, PendingToolUseStatus::Error { .. }) { @@ -155,10 +155,10 @@ impl ToolUseState { is_error: true, }, ); - cancelled_tool_uses.push(tool_use.clone()); + canceled_tool_uses.push(tool_use.clone()); false }); - cancelled_tool_uses + canceled_tool_uses } pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> { diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 506ae80886c9d66acdbaebb197a6f17748b2a46a..b77b5ef36d26ebec9bae48cfe5c1a36c003e230b 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -237,7 +237,7 @@ impl acp::Client for ClientDelegate { let outcome = match result { Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, - Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, + Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled, }; Ok(acp::RequestPermissionResponse { outcome }) diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 4b3a1733491262b8d26979c35d20af6a78b16a8d..d15cc1dd89f8547da03704209af586e87ce8455f 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -285,7 +285,7 @@ impl AgentConnection for ClaudeAgentConnection { let turn_state = session.turn_state.take(); let TurnState::InProgress { end_tx } = turn_state else { - // Already cancelled or idle, put it back + // Already canceled or idle, put it back session.turn_state.replace(turn_state); return; }; @@ -389,7 +389,7 @@ enum TurnState { } impl TurnState { - fn is_cancelled(&self) -> bool { + fn is_canceled(&self) -> bool { matches!(self, TurnState::CancelConfirmed { .. }) } @@ -439,7 +439,7 @@ impl ClaudeAgentSession { for chunk in message.content.chunks() { match chunk { ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => { - if !turn_state.borrow().is_cancelled() { + if !turn_state.borrow().is_canceled() { thread .update(cx, |thread, cx| { thread.push_user_content_block(None, text.into(), cx) @@ -458,8 +458,8 @@ impl ClaudeAgentSession { acp::ToolCallUpdate { id: acp::ToolCallId(tool_use_id.into()), fields: acp::ToolCallUpdateFields { - status: if turn_state.borrow().is_cancelled() { - // Do not set to completed if turn was cancelled + status: if turn_state.borrow().is_canceled() { + // Do not set to completed if turn was canceled None } else { Some(acp::ToolCallStatus::Completed) @@ -592,14 +592,13 @@ impl ClaudeAgentSession { .. } => { let turn_state = turn_state.take(); - let was_cancelled = turn_state.is_cancelled(); + let was_canceled = turn_state.is_canceled(); let Some(end_turn_tx) = turn_state.end_tx() else { debug_panic!("Received `SdkMessage::Result` but there wasn't an active turn"); return; }; - if is_error || (!was_cancelled && subtype == ResultErrorType::ErrorDuringExecution) - { + if is_error || (!was_canceled && subtype == ResultErrorType::ErrorDuringExecution) { end_turn_tx .send(Err(anyhow!( "Error: {}", @@ -610,7 +609,7 @@ impl ClaudeAgentSession { let stop_reason = match subtype { ResultErrorType::Success => acp::StopReason::EndTurn, ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests, - ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled, + ResultErrorType::ErrorDuringExecution => acp::StopReason::Canceled, }; end_turn_tx .send(Ok(acp::PromptResponse { stop_reason })) diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index ffed62d41ff6f76c11ca6def63bff6e99df168c3..116c2b901bb5624a387017db22883f5b03d6db4f 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -4020,7 +4020,7 @@ mod tests { cx.run_until_parked(); - // Verify that the previous completion was cancelled + // Verify that the previous completion was canceled assert_eq!(cancellation_events.lock().unwrap().len(), 1); // Verify that a new request was started after cancellation diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 127e9256be04f1b2c3f1168368d7734b5471c2eb..d6c9a778a60a73a57db6aaf4a38fd907f5366615 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -441,11 +441,11 @@ impl MessageEditor { thread.cancel_editing(cx); }); - let cancelled = self.thread.update(cx, |thread, cx| { + let canceled = self.thread.update(cx, |thread, cx| { thread.cancel_last_completion(Some(window.window_handle()), cx) }); - if cancelled { + if canceled { self.set_editor_is_expanded(false, cx); self.send_to_model(window, cx); } @@ -1404,7 +1404,7 @@ impl MessageEditor { }) .ok(); }); - // Replace existing load task, if any, causing it to be cancelled. + // Replace existing load task, if any, causing it to be canceled. let load_task = load_task.shared(); self.load_context_task = Some(load_task.clone()); cx.spawn(async move |this, cx| { From ea828c0c597a00bd84941ca163dc1f063d14ae89 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 18 Aug 2025 09:58:30 +0200 Subject: [PATCH 079/744] agent2: Emit cancellation stop reason on cancel (#36381) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/agent2/src/tests/mod.rs | 66 +++++++++- crates/agent2/src/thread.rs | 218 +++++++++++++++++++-------------- 2 files changed, 191 insertions(+), 93 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index cc8bd483bbe26ac12c092d6743b43086fd5edfd4..48a16bf685575ace9360d4285a63702740a6fc86 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -941,7 +941,15 @@ async fn test_cancellation(cx: &mut TestAppContext) { // Cancel the current send and ensure that the event stream is closed, even // if one of the tools is still running. thread.update(cx, |thread, _cx| thread.cancel()); - events.collect::>().await; + let events = events.collect::>().await; + let last_event = events.last(); + assert!( + matches!( + last_event, + Some(Ok(AgentResponseEvent::Stop(acp::StopReason::Canceled))) + ), + "unexpected event {last_event:?}" + ); // Ensure we can still send a new message after cancellation. let events = thread @@ -965,6 +973,62 @@ async fn test_cancellation(cx: &mut TestAppContext) { assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); } +#[gpui::test] +async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let events_1 = thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 1"], cx) + }); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hey 1!"); + cx.run_until_parked(); + + let events_2 = thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 2"], cx) + }); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hey 2!"); + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); + fake_model.end_last_completion_stream(); + + let events_1 = events_1.collect::>().await; + assert_eq!(stop_events(events_1), vec![acp::StopReason::Canceled]); + let events_2 = events_2.collect::>().await; + assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]); +} + +#[gpui::test] +async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let events_1 = thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 1"], cx) + }); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hey 1!"); + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); + fake_model.end_last_completion_stream(); + let events_1 = events_1.collect::>().await; + + let events_2 = thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 2"], cx) + }); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hey 2!"); + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); + fake_model.end_last_completion_stream(); + let events_2 = events_2.collect::>().await; + + assert_eq!(stop_events(events_1), vec![acp::StopReason::EndTurn]); + assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]); +} + #[gpui::test] async fn test_refusal(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 0741bb9e081ca4c17536f96623ad0d2830243051..d8b6286f607b963d216ac267bfcb4bf06a743117 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -461,7 +461,7 @@ pub struct Thread { /// Holds the task that handles agent interaction until the end of the turn. /// Survives across multiple requests as the model performs tool calls and /// we run tools, report their results. - running_turn: Option>, + running_turn: Option, pending_message: Option, tools: BTreeMap>, tool_use_limit_reached: bool, @@ -554,8 +554,9 @@ impl Thread { } pub fn cancel(&mut self) { - // TODO: do we need to emit a stop::cancel for ACP? - self.running_turn.take(); + if let Some(running_turn) = self.running_turn.take() { + running_turn.cancel(); + } self.flush_pending_message(); } @@ -616,108 +617,118 @@ impl Thread { &mut self, cx: &mut Context, ) -> mpsc::UnboundedReceiver> { + self.cancel(); + let model = self.model.clone(); let (events_tx, events_rx) = mpsc::unbounded::>(); let event_stream = AgentResponseEventStream(events_tx); let message_ix = self.messages.len().saturating_sub(1); self.tool_use_limit_reached = false; - self.running_turn = Some(cx.spawn(async move |this, cx| { - log::info!("Starting agent turn execution"); - let turn_result: Result<()> = async { - let mut completion_intent = CompletionIntent::UserPrompt; - loop { - log::debug!( - "Building completion request with intent: {:?}", - completion_intent - ); - let request = this.update(cx, |this, cx| { - this.build_completion_request(completion_intent, cx) - })?; - - log::info!("Calling model.stream_completion"); - let mut events = model.stream_completion(request, cx).await?; - log::debug!("Stream completion started successfully"); - - let mut tool_use_limit_reached = false; - let mut tool_uses = FuturesUnordered::new(); - while let Some(event) = events.next().await { - match event? { - LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::ToolUseLimitReached, - ) => { - tool_use_limit_reached = true; - } - LanguageModelCompletionEvent::Stop(reason) => { - event_stream.send_stop(reason); - if reason == StopReason::Refusal { - this.update(cx, |this, _cx| { - this.flush_pending_message(); - this.messages.truncate(message_ix); - })?; - return Ok(()); + self.running_turn = Some(RunningTurn { + event_stream: event_stream.clone(), + _task: cx.spawn(async move |this, cx| { + log::info!("Starting agent turn execution"); + let turn_result: Result<()> = async { + let mut completion_intent = CompletionIntent::UserPrompt; + loop { + log::debug!( + "Building completion request with intent: {:?}", + completion_intent + ); + let request = this.update(cx, |this, cx| { + this.build_completion_request(completion_intent, cx) + })?; + + log::info!("Calling model.stream_completion"); + let mut events = model.stream_completion(request, cx).await?; + log::debug!("Stream completion started successfully"); + + let mut tool_use_limit_reached = false; + let mut tool_uses = FuturesUnordered::new(); + while let Some(event) = events.next().await { + match event? { + LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::ToolUseLimitReached, + ) => { + tool_use_limit_reached = true; + } + LanguageModelCompletionEvent::Stop(reason) => { + event_stream.send_stop(reason); + if reason == StopReason::Refusal { + this.update(cx, |this, _cx| { + this.flush_pending_message(); + this.messages.truncate(message_ix); + })?; + return Ok(()); + } + } + event => { + log::trace!("Received completion event: {:?}", event); + this.update(cx, |this, cx| { + tool_uses.extend(this.handle_streamed_completion_event( + event, + &event_stream, + cx, + )); + }) + .ok(); } - } - event => { - log::trace!("Received completion event: {:?}", event); - this.update(cx, |this, cx| { - tool_uses.extend(this.handle_streamed_completion_event( - event, - &event_stream, - cx, - )); - }) - .ok(); } } - } - let used_tools = tool_uses.is_empty(); - while let Some(tool_result) = tool_uses.next().await { - log::info!("Tool finished {:?}", tool_result); - - event_stream.update_tool_call_fields( - &tool_result.tool_use_id, - acp::ToolCallUpdateFields { - status: Some(if tool_result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - }), - raw_output: tool_result.output.clone(), - ..Default::default() - }, - ); - this.update(cx, |this, _cx| { - this.pending_message() - .tool_results - .insert(tool_result.tool_use_id.clone(), tool_result); - }) - .ok(); - } + let used_tools = tool_uses.is_empty(); + while let Some(tool_result) = tool_uses.next().await { + log::info!("Tool finished {:?}", tool_result); + + event_stream.update_tool_call_fields( + &tool_result.tool_use_id, + acp::ToolCallUpdateFields { + status: Some(if tool_result.is_error { + acp::ToolCallStatus::Failed + } else { + acp::ToolCallStatus::Completed + }), + raw_output: tool_result.output.clone(), + ..Default::default() + }, + ); + this.update(cx, |this, _cx| { + this.pending_message() + .tool_results + .insert(tool_result.tool_use_id.clone(), tool_result); + }) + .ok(); + } - if tool_use_limit_reached { - log::info!("Tool use limit reached, completing turn"); - this.update(cx, |this, _cx| this.tool_use_limit_reached = true)?; - return Err(language_model::ToolUseLimitReachedError.into()); - } else if used_tools { - log::info!("No tool uses found, completing turn"); - return Ok(()); - } else { - this.update(cx, |this, _| this.flush_pending_message())?; - completion_intent = CompletionIntent::ToolResults; + if tool_use_limit_reached { + log::info!("Tool use limit reached, completing turn"); + this.update(cx, |this, _cx| this.tool_use_limit_reached = true)?; + return Err(language_model::ToolUseLimitReachedError.into()); + } else if used_tools { + log::info!("No tool uses found, completing turn"); + return Ok(()); + } else { + this.update(cx, |this, _| this.flush_pending_message())?; + completion_intent = CompletionIntent::ToolResults; + } } } - } - .await; + .await; - this.update(cx, |this, _| this.flush_pending_message()).ok(); - if let Err(error) = turn_result { - log::error!("Turn execution failed: {:?}", error); - event_stream.send_error(error); - } else { - log::info!("Turn execution completed successfully"); - } - })); + if let Err(error) = turn_result { + log::error!("Turn execution failed: {:?}", error); + event_stream.send_error(error); + } else { + log::info!("Turn execution completed successfully"); + } + + this.update(cx, |this, _| { + this.flush_pending_message(); + this.running_turn.take(); + }) + .ok(); + }), + }); events_rx } @@ -1125,6 +1136,23 @@ impl Thread { } } +struct RunningTurn { + /// Holds the task that handles agent interaction until the end of the turn. + /// Survives across multiple requests as the model performs tool calls and + /// we run tools, report their results. + _task: Task<()>, + /// The current event stream for the running turn. Used to report a final + /// cancellation event if we cancel the turn. + event_stream: AgentResponseEventStream, +} + +impl RunningTurn { + fn cancel(self) { + log::debug!("Cancelling in progress turn"); + self.event_stream.send_canceled(); + } +} + pub trait AgentTool where Self: 'static + Sized, @@ -1336,6 +1364,12 @@ impl AgentResponseEventStream { } } + fn send_canceled(&self) { + self.0 + .unbounded_send(Ok(AgentResponseEvent::Stop(acp::StopReason::Canceled))) + .ok(); + } + fn send_error(&self, error: impl Into) { self.0.unbounded_send(Err(error.into())).ok(); } From 61ce07a91b06f3edf58d5fb7f76cdc1e79b6ae76 Mon Sep 17 00:00:00 2001 From: Cale Sennett Date: Mon, 18 Aug 2025 03:36:52 -0500 Subject: [PATCH 080/744] Add capabilities to OpenAI-compatible model settings (#36370) ### TL;DR * Adds `capabilities` configuration for OpenAI-compatible models * Relates to https://github.com/zed-industries/zed/issues/36215#issuecomment-3193920491 ### Summary This PR introduces support for configuring model capabilities for OpenAI-compatible language models. The implementation addresses the issue that not all OpenAI-compatible APIs support the same features - for example, Cerebras' API explicitly does not support `parallel_tool_calls` as documented in their [OpenAI compatibility guide](https://inference-docs.cerebras.ai/resources/openai#currently-unsupported-openai-features). ### Changes 1. **Model Capabilities Structure**: - Added `ModelCapabilityToggles` struct for UI representation with boolean toggle states - Implemented proper parsing of capability toggles into `ModelCapabilities` 2. **UI Updates**: - Modified the "Add LLM Provider" modal to include checkboxes for each capability - Each OpenAI-compatible model can now be configured with its specific capabilities through the UI 3. **Configuration File Structure**: - Updated the settings schema to support a `capabilities` object for each `openai_compatible` model - Each capability (`tools`, `images`, `parallel_tool_calls`, `prompt_cache_key`) can be individually specified per model ### Example Configuration ```json { "openai_compatible": { "Cerebras": { "api_url": "https://api.cerebras.ai/v1", "available_models": [ { "name": "gpt-oss-120b", "max_tokens": 131000, "capabilities": { "tools": true, "images": false, "parallel_tool_calls": false, "prompt_cache_key": false } } ] } } } ``` ### Tests Added - Added tests to verify default capability values are correctly applied - Added tests to verify that deselected toggles are properly parsed as `false` - Added tests to verify that mixed capability selections work correctly Thanks to @osyvokon for the desired `capabilities` configuration structure! Release Notes: - OpenAI-compatible models now have configurable capabilities (#36370; thanks @calesennett) --------- Co-authored-by: Oleksiy Syvokon --- .../add_llm_provider_modal.rs | 168 +++++++++++++++++- .../src/provider/open_ai_compatible.rs | 35 +++- docs/src/ai/llm-providers.md | 17 +- 3 files changed, 208 insertions(+), 12 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 401a6334886e18ef2e53bbd5b68392597d0db1e9..c68c9c2730ad9ed894e5e3a2b2a842f281b7f281 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 @@ -7,10 +7,12 @@ use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, T use language_model::LanguageModelRegistry; use language_models::{ AllLanguageModelSettings, OpenAiCompatibleSettingsContent, - provider::open_ai_compatible::AvailableModel, + provider::open_ai_compatible::{AvailableModel, ModelCapabilities}, }; use settings::update_settings_file; -use ui::{Banner, KeyBinding, Modal, ModalFooter, ModalHeader, Section, prelude::*}; +use ui::{ + Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*, +}; use ui_input::SingleLineInput; use workspace::{ModalView, Workspace}; @@ -69,11 +71,19 @@ impl AddLlmProviderInput { } } +struct ModelCapabilityToggles { + pub supports_tools: ToggleState, + pub supports_images: ToggleState, + pub supports_parallel_tool_calls: ToggleState, + pub supports_prompt_cache_key: ToggleState, +} + struct ModelInput { name: Entity, max_completion_tokens: Entity, max_output_tokens: Entity, max_tokens: Entity, + capabilities: ModelCapabilityToggles, } impl ModelInput { @@ -100,11 +110,23 @@ impl ModelInput { cx, ); let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx); + let ModelCapabilities { + tools, + images, + parallel_tool_calls, + prompt_cache_key, + } = ModelCapabilities::default(); Self { name: model_name, max_completion_tokens, max_output_tokens, max_tokens, + capabilities: ModelCapabilityToggles { + supports_tools: tools.into(), + supports_images: images.into(), + supports_parallel_tool_calls: parallel_tool_calls.into(), + supports_prompt_cache_key: prompt_cache_key.into(), + }, } } @@ -136,6 +158,12 @@ impl ModelInput { .text(cx) .parse::() .map_err(|_| SharedString::from("Max Tokens must be a number"))?, + capabilities: ModelCapabilities { + tools: self.capabilities.supports_tools.selected(), + images: self.capabilities.supports_images.selected(), + parallel_tool_calls: self.capabilities.supports_parallel_tool_calls.selected(), + prompt_cache_key: self.capabilities.supports_prompt_cache_key.selected(), + }, }) } } @@ -322,6 +350,55 @@ impl AddLlmProviderModal { .child(model.max_output_tokens.clone()), ) .child(model.max_tokens.clone()) + .child( + v_flex() + .gap_1() + .child( + Checkbox::new(("supports-tools", ix), model.capabilities.supports_tools) + .label("Supports tools") + .on_click(cx.listener(move |this, checked, _window, cx| { + this.input.models[ix].capabilities.supports_tools = *checked; + cx.notify(); + })), + ) + .child( + Checkbox::new(("supports-images", ix), model.capabilities.supports_images) + .label("Supports images") + .on_click(cx.listener(move |this, checked, _window, cx| { + this.input.models[ix].capabilities.supports_images = *checked; + cx.notify(); + })), + ) + .child( + Checkbox::new( + ("supports-parallel-tool-calls", ix), + model.capabilities.supports_parallel_tool_calls, + ) + .label("Supports parallel_tool_calls") + .on_click(cx.listener( + move |this, checked, _window, cx| { + this.input.models[ix] + .capabilities + .supports_parallel_tool_calls = *checked; + cx.notify(); + }, + )), + ) + .child( + Checkbox::new( + ("supports-prompt-cache-key", ix), + model.capabilities.supports_prompt_cache_key, + ) + .label("Supports prompt_cache_key") + .on_click(cx.listener( + move |this, checked, _window, cx| { + this.input.models[ix].capabilities.supports_prompt_cache_key = + *checked; + cx.notify(); + }, + )), + ), + ) .when(has_more_than_one_model, |this| { this.child( Button::new(("remove-model", ix), "Remove Model") @@ -562,6 +639,93 @@ mod tests { ); } + #[gpui::test] + async fn test_model_input_default_capabilities(cx: &mut TestAppContext) { + let cx = setup_test(cx).await; + + cx.update(|window, cx| { + let model_input = ModelInput::new(window, cx); + model_input.name.update(cx, |input, cx| { + input.editor().update(cx, |editor, cx| { + editor.set_text("somemodel", window, cx); + }); + }); + assert_eq!( + model_input.capabilities.supports_tools, + ToggleState::Selected + ); + assert_eq!( + model_input.capabilities.supports_images, + ToggleState::Unselected + ); + assert_eq!( + model_input.capabilities.supports_parallel_tool_calls, + ToggleState::Unselected + ); + assert_eq!( + model_input.capabilities.supports_prompt_cache_key, + ToggleState::Unselected + ); + + let parsed_model = model_input.parse(cx).unwrap(); + assert_eq!(parsed_model.capabilities.tools, true); + assert_eq!(parsed_model.capabilities.images, false); + assert_eq!(parsed_model.capabilities.parallel_tool_calls, false); + assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + }); + } + + #[gpui::test] + async fn test_model_input_deselected_capabilities(cx: &mut TestAppContext) { + let cx = setup_test(cx).await; + + cx.update(|window, cx| { + let mut model_input = ModelInput::new(window, cx); + model_input.name.update(cx, |input, cx| { + input.editor().update(cx, |editor, cx| { + editor.set_text("somemodel", window, cx); + }); + }); + + model_input.capabilities.supports_tools = ToggleState::Unselected; + model_input.capabilities.supports_images = ToggleState::Unselected; + model_input.capabilities.supports_parallel_tool_calls = ToggleState::Unselected; + model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected; + + let parsed_model = model_input.parse(cx).unwrap(); + assert_eq!(parsed_model.capabilities.tools, false); + assert_eq!(parsed_model.capabilities.images, false); + assert_eq!(parsed_model.capabilities.parallel_tool_calls, false); + assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + }); + } + + #[gpui::test] + async fn test_model_input_with_name_and_capabilities(cx: &mut TestAppContext) { + let cx = setup_test(cx).await; + + cx.update(|window, cx| { + let mut model_input = ModelInput::new(window, cx); + model_input.name.update(cx, |input, cx| { + input.editor().update(cx, |editor, cx| { + editor.set_text("somemodel", window, cx); + }); + }); + + model_input.capabilities.supports_tools = ToggleState::Selected; + model_input.capabilities.supports_images = ToggleState::Unselected; + model_input.capabilities.supports_parallel_tool_calls = ToggleState::Selected; + model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected; + + let parsed_model = model_input.parse(cx).unwrap(); + assert_eq!(parsed_model.name, "somemodel"); + assert_eq!(parsed_model.capabilities.tools, true); + assert_eq!(parsed_model.capabilities.images, false); + assert_eq!(parsed_model.capabilities.parallel_tool_calls, true); + assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + }); + } + async fn setup_test(cx: &mut TestAppContext) -> &mut VisualTestContext { cx.update(|cx| { let store = SettingsStore::test(cx); diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index 5f546f52194d37a4ee97e59ed38e681e0ac26440..e2d3adb198a15904baa8ebdd6645ab79a20ed249 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -38,6 +38,27 @@ pub struct AvailableModel { pub max_tokens: u64, pub max_output_tokens: Option, pub max_completion_tokens: Option, + #[serde(default)] + pub capabilities: ModelCapabilities, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct ModelCapabilities { + pub tools: bool, + pub images: bool, + pub parallel_tool_calls: bool, + pub prompt_cache_key: bool, +} + +impl Default for ModelCapabilities { + fn default() -> Self { + Self { + tools: true, + images: false, + parallel_tool_calls: false, + prompt_cache_key: false, + } + } } pub struct OpenAiCompatibleLanguageModelProvider { @@ -293,17 +314,17 @@ impl LanguageModel for OpenAiCompatibleLanguageModel { } fn supports_tools(&self) -> bool { - true + self.model.capabilities.tools } fn supports_images(&self) -> bool { - false + self.model.capabilities.images } fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { match choice { - LanguageModelToolChoice::Auto => true, - LanguageModelToolChoice::Any => true, + LanguageModelToolChoice::Auto => self.model.capabilities.tools, + LanguageModelToolChoice::Any => self.model.capabilities.tools, LanguageModelToolChoice::None => true, } } @@ -355,13 +376,11 @@ impl LanguageModel for OpenAiCompatibleLanguageModel { LanguageModelCompletionError, >, > { - let supports_parallel_tool_call = true; - let supports_prompt_cache_key = false; let request = into_open_ai( request, &self.model.name, - supports_parallel_tool_call, - supports_prompt_cache_key, + self.model.capabilities.parallel_tool_calls, + self.model.capabilities.prompt_cache_key, self.max_output_tokens(), None, ); diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index 58c92307600dcce6cc20ab2da19efd6b05b7211a..5ef6081421240ae13ab53b27fd966aec64ca3b82 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -427,7 +427,7 @@ Custom models will be listed in the model dropdown in the Agent Panel. 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. -You can add a custom, OpenAI-compatible model via either via the UI or by editing your `settings.json`. +You can add a custom, OpenAI-compatible model either via the UI or by editing your `settings.json`. 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. @@ -443,7 +443,13 @@ To do it via your `settings.json`, add the following snippet under `language_mod { "name": "mistralai/Mixtral-8x7B-Instruct-v0.1", "display_name": "Together Mixtral 8x7B", - "max_tokens": 32768 + "max_tokens": 32768, + "capabilities": { + "tools": true, + "images": false, + "parallel_tool_calls": false, + "prompt_cache_key": false + } } ] } @@ -451,6 +457,13 @@ To do it via your `settings.json`, add the following snippet under `language_mod } ``` +By default, OpenAI-compatible models inherit the following capabilities: + +- `tools`: true (supports tool/function calling) +- `images`: false (does not support image inputs) +- `parallel_tool_calls`: false (does not support `parallel_tool_calls` parameter) +- `prompt_cache_key`: false (does not support `prompt_cache_key` parameter) + 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. From 42ffa8900afaa6ec6bd954bdde08f1686d729019 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Mon, 18 Aug 2025 11:54:31 +0300 Subject: [PATCH 081/744] open_ai: Fix error response parsing (#36390) Closes #35925 Release Notes: - Fixed OpenAI error response parsing in some cases --- crates/open_ai/src/open_ai.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 604e8fe6221e80661d515e6e865914dabcc2d170..1fb9a1342cc81a0198c7e3c4258d45521dae32e4 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -432,11 +432,16 @@ pub struct ChoiceDelta { pub finish_reason: Option, } +#[derive(Serialize, Deserialize, Debug)] +pub struct OpenAiError { + message: String, +} + #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] pub enum ResponseStreamResult { Ok(ResponseStreamEvent), - Err { error: String }, + Err { error: OpenAiError }, } #[derive(Serialize, Deserialize, Debug)] @@ -475,7 +480,7 @@ pub async fn stream_completion( match serde_json::from_str(line) { Ok(ResponseStreamResult::Ok(response)) => Some(Ok(response)), Ok(ResponseStreamResult::Err { error }) => { - Some(Err(anyhow!(error))) + Some(Err(anyhow!(error.message))) } Err(error) => { log::error!( @@ -502,11 +507,6 @@ pub async fn stream_completion( error: OpenAiError, } - #[derive(Deserialize)] - struct OpenAiError { - message: String, - } - match serde_json::from_str::(&body) { Ok(response) if !response.error.message.is_empty() => Err(anyhow!( "API request to {} failed: {}", From b8a106632fca78d6f07f88b003464e6573f90702 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 18 Aug 2025 11:43:52 +0200 Subject: [PATCH 082/744] lsp: Identify language servers by their configuration (#35270) - **WIP: reorganize dispositions** - **Introduce a LocalToolchainStore trait and use it for LspAdapter methods** Closes #35782 Closes #27331 Release Notes: - Python: Improved propagation of a selected virtual environment into the LSP configuration. This should the make all language-related features such as Go to definition or Find all references more reliable. --------- Co-authored-by: Cole Miller Co-authored-by: Lukas Wirth --- crates/editor/src/editor.rs | 46 +- crates/extension_host/src/extension_host.rs | 1 + crates/extension_host/src/headless_host.rs | 1 + .../src/wasm_host/wit/since_v0_6_0.rs | 2 +- crates/language/src/buffer.rs | 1 + crates/language/src/language.rs | 40 +- crates/language/src/language_registry.rs | 12 +- crates/language/src/manifest.rs | 6 + crates/language/src/toolchain.rs | 33 +- .../src/extension_lsp_adapter.rs | 10 +- .../src/language_extension.rs | 2 +- crates/languages/src/c.rs | 2 +- crates/languages/src/css.rs | 6 +- crates/languages/src/go.rs | 2 +- crates/languages/src/json.rs | 10 +- crates/languages/src/lib.rs | 11 +- crates/languages/src/python.rs | 70 +- crates/languages/src/rust.rs | 6 +- crates/languages/src/tailwind.rs | 6 +- crates/languages/src/typescript.rs | 6 +- crates/languages/src/vtsls.rs | 6 +- crates/languages/src/yaml.rs | 8 +- crates/project/src/lsp_command.rs | 26 +- crates/project/src/lsp_store.rs | 1184 ++++++++--------- crates/project/src/manifest_tree.rs | 107 +- .../src/manifest_tree/manifest_store.rs | 13 +- .../project/src/manifest_tree/server_tree.rs | 384 +++--- crates/project/src/project.rs | 18 +- crates/project/src/project_settings.rs | 8 +- crates/project/src/project_tests.rs | 7 +- crates/project/src/toolchain_store.rs | 78 +- crates/remote_server/src/headless_project.rs | 6 +- 32 files changed, 1035 insertions(+), 1083 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0111e913471649beabd972db3033816aa41fd858..e645bfee6738540d7a099c1a69718933afeda331 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -16022,38 +16022,24 @@ impl Editor { cx.spawn_in(window, async move |editor, cx| { let location_task = editor.update(cx, |_, cx| { project.update(cx, |project, cx| { - let language_server_name = project - .language_server_statuses(cx) - .find(|(id, _)| server_id == *id) - .map(|(_, status)| status.name.clone()); - language_server_name.map(|language_server_name| { - project.open_local_buffer_via_lsp( - lsp_location.uri.clone(), - server_id, - language_server_name, - cx, - ) - }) + project.open_local_buffer_via_lsp(lsp_location.uri.clone(), server_id, cx) }) })?; - let location = match location_task { - Some(task) => Some({ - let target_buffer_handle = task.await.context("open local buffer")?; - let range = target_buffer_handle.read_with(cx, |target_buffer, _| { - let target_start = target_buffer - .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); - let target_end = target_buffer - .clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left); - target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end) - })?; - Location { - buffer: target_buffer_handle, - range, - } - }), - None => None, - }; + let location = Some({ + let target_buffer_handle = location_task.await.context("open local buffer")?; + let range = target_buffer_handle.read_with(cx, |target_buffer, _| { + let target_start = target_buffer + .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); + let target_end = target_buffer + .clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + })?; + Location { + buffer: target_buffer_handle, + range, + } + }); Ok(location) }) } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 46deacfe69f1e00fca3c4b158f8760276339d46b..e795fa5ac598416ca804d0a01a73fcaf8ed28dc0 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1275,6 +1275,7 @@ impl ExtensionStore { queries, context_provider, toolchain_provider: None, + manifest_name: None, }) }), ); diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs index adc9638c2998eb1f122df5137577ca7e0cf4c975..8ce3847376a4f02c04178cf62554704348c7e0f3 100644 --- a/crates/extension_host/src/headless_host.rs +++ b/crates/extension_host/src/headless_host.rs @@ -163,6 +163,7 @@ impl HeadlessExtensionStore { queries: LanguageQueries::default(), context_provider: None, toolchain_provider: None, + manifest_name: None, }) }), ); diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs index 767b9033ade3c81c6ac149363676513c72996b7e..84794d5386eda1517808d181eb259a3264f7b82d 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs @@ -938,7 +938,7 @@ impl ExtensionImports for WasmState { binary: settings.binary.map(|binary| settings::CommandSettings { path: binary.path, arguments: binary.arguments, - env: binary.env, + env: binary.env.map(|env| env.into_iter().collect()), }), settings: settings.settings, initialization_options: settings.initialization_options, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 83517accc239ecf9d2196f124fc5695a8545ef17..2080513f49e593e0561ff4e28ec2f2ef649cb4f2 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1571,6 +1571,7 @@ impl Buffer { diagnostics: diagnostics.iter().cloned().collect(), lamport_timestamp, }; + self.apply_diagnostic_update(server_id, diagnostics, lamport_timestamp, cx); self.send_operation(op, true, cx); } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index b9933dfcec36f1e8c5cb31271668a25b60020c8a..f299dee345a61d858fb411b5916766eba47dc72e 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -44,6 +44,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use serde_json::Value; use settings::WorktreeId; use smol::future::FutureExt as _; +use std::num::NonZeroU32; use std::{ any::Any, ffi::OsStr, @@ -59,7 +60,6 @@ use std::{ atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst}, }, }; -use std::{num::NonZeroU32, sync::OnceLock}; use syntax_map::{QueryCursorHandle, SyntaxSnapshot}; use task::RunnableTag; pub use task_context::{ContextLocation, ContextProvider, RunnableRange}; @@ -67,7 +67,9 @@ pub use text_diff::{ DiffOptions, apply_diff_patch, line_diff, text_diff, text_diff_with_options, unified_diff, }; use theme::SyntaxTheme; -pub use toolchain::{LanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister}; +pub use toolchain::{ + LanguageToolchainStore, LocalLanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister, +}; use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime}; use util::serde::default_true; @@ -165,7 +167,6 @@ pub struct CachedLspAdapter { pub adapter: Arc, pub reinstall_attempt_count: AtomicU64, cached_binary: futures::lock::Mutex>, - manifest_name: OnceLock>, } impl Debug for CachedLspAdapter { @@ -201,7 +202,6 @@ impl CachedLspAdapter { adapter, cached_binary: Default::default(), reinstall_attempt_count: AtomicU64::new(0), - manifest_name: Default::default(), }) } @@ -212,7 +212,7 @@ impl CachedLspAdapter { pub async fn get_language_server_command( self: Arc, delegate: Arc, - toolchains: Arc, + toolchains: Option, binary_options: LanguageServerBinaryOptions, cx: &mut AsyncApp, ) -> Result { @@ -281,12 +281,6 @@ impl CachedLspAdapter { .cloned() .unwrap_or_else(|| language_name.lsp_id()) } - - pub fn manifest_name(&self) -> Option { - self.manifest_name - .get_or_init(|| self.adapter.manifest_name()) - .clone() - } } /// Determines what gets sent out as a workspace folders content @@ -327,7 +321,7 @@ pub trait LspAdapter: 'static + Send + Sync { fn get_language_server_command<'a>( self: Arc, delegate: Arc, - toolchains: Arc, + toolchains: Option, binary_options: LanguageServerBinaryOptions, mut cached_binary: futures::lock::MutexGuard<'a, Option>, cx: &'a mut AsyncApp, @@ -402,7 +396,7 @@ pub trait LspAdapter: 'static + Send + Sync { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { None @@ -535,7 +529,7 @@ pub trait LspAdapter: 'static + Send + Sync { self: Arc, _: &dyn Fs, _: &Arc, - _: Arc, + _: Option, _cx: &mut AsyncApp, ) -> Result { Ok(serde_json::json!({})) @@ -555,7 +549,6 @@ pub trait LspAdapter: 'static + Send + Sync { _target_language_server_id: LanguageServerName, _: &dyn Fs, _: &Arc, - _: Arc, _cx: &mut AsyncApp, ) -> Result> { Ok(None) @@ -594,10 +587,6 @@ pub trait LspAdapter: 'static + Send + Sync { WorkspaceFoldersContent::SubprojectRoots } - fn manifest_name(&self) -> Option { - None - } - /// Method only implemented by the default JSON language server adapter. /// Used to provide dynamic reloading of the JSON schemas used to /// provide autocompletion and diagnostics in Zed setting and keybind @@ -1108,6 +1097,7 @@ pub struct Language { pub(crate) grammar: Option>, pub(crate) context_provider: Option>, pub(crate) toolchain: Option>, + pub(crate) manifest_name: Option, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] @@ -1318,6 +1308,7 @@ impl Language { }), context_provider: None, toolchain: None, + manifest_name: None, } } @@ -1331,6 +1322,10 @@ impl Language { self } + pub fn with_manifest(mut self, name: Option) -> Self { + self.manifest_name = name; + self + } pub fn with_queries(mut self, queries: LanguageQueries) -> Result { if let Some(query) = queries.highlights { self = self @@ -1764,6 +1759,9 @@ impl Language { pub fn name(&self) -> LanguageName { self.config.name.clone() } + pub fn manifest(&self) -> Option<&ManifestName> { + self.manifest_name.as_ref() + } pub fn code_fence_block_name(&self) -> Arc { self.config @@ -2209,7 +2207,7 @@ impl LspAdapter for FakeLspAdapter { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { Some(self.language_server_binary.clone()) @@ -2218,7 +2216,7 @@ impl LspAdapter for FakeLspAdapter { fn get_language_server_command<'a>( self: Arc, _: Arc, - _: Arc, + _: Option, _: LanguageServerBinaryOptions, _: futures::lock::MutexGuard<'a, Option>, _: &'a mut AsyncApp, diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index ea988e8098ec2a795e8c0a386b4e162ecd5c89ca..6a89b90462dcb832b4f6bf6895c362f057e103b7 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -1,6 +1,6 @@ use crate::{ CachedLspAdapter, File, Language, LanguageConfig, LanguageId, LanguageMatcher, - LanguageServerName, LspAdapter, PLAIN_TEXT, ToolchainLister, + LanguageServerName, LspAdapter, ManifestName, PLAIN_TEXT, ToolchainLister, language_settings::{ AllLanguageSettingsContent, LanguageSettingsContent, all_language_settings, }, @@ -172,6 +172,7 @@ pub struct AvailableLanguage { hidden: bool, load: Arc Result + 'static + Send + Sync>, loaded: bool, + manifest_name: Option, } impl AvailableLanguage { @@ -259,6 +260,7 @@ pub struct LoadedLanguage { pub queries: LanguageQueries, pub context_provider: Option>, pub toolchain_provider: Option>, + pub manifest_name: Option, } impl LanguageRegistry { @@ -349,12 +351,14 @@ impl LanguageRegistry { config.grammar.clone(), config.matcher.clone(), config.hidden, + None, Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), queries: Default::default(), toolchain_provider: None, context_provider: None, + manifest_name: None, }) }), ) @@ -487,6 +491,7 @@ impl LanguageRegistry { grammar_name: Option>, matcher: LanguageMatcher, hidden: bool, + manifest_name: Option, load: Arc Result + 'static + Send + Sync>, ) { let state = &mut *self.state.write(); @@ -496,6 +501,7 @@ impl LanguageRegistry { existing_language.grammar = grammar_name; existing_language.matcher = matcher; existing_language.load = load; + existing_language.manifest_name = manifest_name; return; } } @@ -508,6 +514,7 @@ impl LanguageRegistry { load, hidden, loaded: false, + manifest_name, }); state.version += 1; state.reload_count += 1; @@ -575,6 +582,7 @@ impl LanguageRegistry { grammar: language.config.grammar.clone(), matcher: language.config.matcher.clone(), hidden: language.config.hidden, + manifest_name: None, load: Arc::new(|| Err(anyhow!("already loaded"))), loaded: true, }); @@ -914,10 +922,12 @@ impl LanguageRegistry { Language::new_with_id(id, loaded_language.config, grammar) .with_context_provider(loaded_language.context_provider) .with_toolchain_lister(loaded_language.toolchain_provider) + .with_manifest(loaded_language.manifest_name) .with_queries(loaded_language.queries) } else { Ok(Language::new_with_id(id, loaded_language.config, None) .with_context_provider(loaded_language.context_provider) + .with_manifest(loaded_language.manifest_name) .with_toolchain_lister(loaded_language.toolchain_provider)) } } diff --git a/crates/language/src/manifest.rs b/crates/language/src/manifest.rs index 37505fec3b233c2ecd7e2ac7807a7ade6a9b3d4a..3ca0ddf71da20f69d5d6440189d4a656bfbe7c9d 100644 --- a/crates/language/src/manifest.rs +++ b/crates/language/src/manifest.rs @@ -12,6 +12,12 @@ impl Borrow for ManifestName { } } +impl Borrow for ManifestName { + fn borrow(&self) -> &str { + &self.0 + } +} + impl From for ManifestName { fn from(value: SharedString) -> Self { Self(value) diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 1f4b038f68e5fcf1ed5c499d543fa92ba3c2de94..979513bc96f9660772517a728077614fbd7f5e7a 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -17,7 +17,7 @@ use settings::WorktreeId; use crate::{LanguageName, ManifestName}; /// Represents a single toolchain. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq)] pub struct Toolchain { /// User-facing label pub name: SharedString, @@ -27,6 +27,14 @@ pub struct Toolchain { pub as_json: serde_json::Value, } +impl std::hash::Hash for Toolchain { + fn hash(&self, state: &mut H) { + self.name.hash(state); + self.path.hash(state); + self.language_name.hash(state); + } +} + impl PartialEq for Toolchain { fn eq(&self, other: &Self) -> bool { // Do not use as_json for comparisons; it shouldn't impact equality, as it's not user-surfaced. @@ -64,6 +72,29 @@ pub trait LanguageToolchainStore: Send + Sync + 'static { ) -> Option; } +pub trait LocalLanguageToolchainStore: Send + Sync + 'static { + fn active_toolchain( + self: Arc, + worktree_id: WorktreeId, + relative_path: &Arc, + language_name: LanguageName, + cx: &mut AsyncApp, + ) -> Option; +} + +#[async_trait(?Send )] +impl LanguageToolchainStore for T { + async fn active_toolchain( + self: Arc, + worktree_id: WorktreeId, + relative_path: Arc, + language_name: LanguageName, + cx: &mut AsyncApp, + ) -> Option { + self.active_toolchain(worktree_id, &relative_path, language_name, cx) + } +} + type DefaultIndex = usize; #[derive(Default, Clone)] pub struct ToolchainList { diff --git a/crates/language_extension/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs index 98b6fd4b5a2ef6e7f1b5adbc54dcecd0707b60ff..e465a8dd0a0404e76b19942dd15bf19c4f204fdc 100644 --- a/crates/language_extension/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -12,8 +12,8 @@ use fs::Fs; use futures::{Future, FutureExt, future::join_all}; use gpui::{App, AppContext, AsyncApp, Task}; use language::{ - BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore, - LspAdapter, LspAdapterDelegate, + BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LspAdapter, LspAdapterDelegate, + Toolchain, }; use lsp::{ CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName, @@ -159,7 +159,7 @@ impl LspAdapter for ExtensionLspAdapter { fn get_language_server_command<'a>( self: Arc, delegate: Arc, - _: Arc, + _: Option, _: LanguageServerBinaryOptions, _: futures::lock::MutexGuard<'a, Option>, _: &'a mut AsyncApp, @@ -288,7 +288,7 @@ impl LspAdapter for ExtensionLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, _cx: &mut AsyncApp, ) -> Result { let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; @@ -336,7 +336,7 @@ impl LspAdapter for ExtensionLspAdapter { target_language_server_id: LanguageServerName, _: &dyn Fs, delegate: &Arc, - _: Arc, + _cx: &mut AsyncApp, ) -> Result> { let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; diff --git a/crates/language_extension/src/language_extension.rs b/crates/language_extension/src/language_extension.rs index 1915eae2d18fe5fb96dbb0dcca614f8a4f41bb81..7bca0eb48566b739e68c3efb4f2502cabb80994f 100644 --- a/crates/language_extension/src/language_extension.rs +++ b/crates/language_extension/src/language_extension.rs @@ -52,7 +52,7 @@ impl ExtensionLanguageProxy for LanguageServerRegistryProxy { load: Arc Result + Send + Sync + 'static>, ) { self.language_registry - .register_language(language, grammar, matcher, hidden, load); + .register_language(language, grammar, matcher, hidden, None, load); } fn remove_languages( diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index aee1abee95fa2ea21931084ebe442c2ecd41da3c..999d4a74c30c776e6e0d83b7b0a4466079bff199 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -28,7 +28,7 @@ impl super::LspAdapter for CLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index ffd9006c769a4ad14cc70beb988c7ea96a578872..a1a5418220442f44072b8a8b88b96184778c972b 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -2,7 +2,7 @@ use anyhow::{Context as _, Result}; use async_trait::async_trait; use futures::StreamExt; use gpui::AsyncApp; -use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; +use language::{LspAdapter, LspAdapterDelegate, Toolchain}; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; @@ -43,7 +43,7 @@ impl LspAdapter for CssLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate @@ -144,7 +144,7 @@ impl LspAdapter for CssLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, cx: &mut AsyncApp, ) -> Result { let mut default_config = json!({ diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 14f646133bf22ba7977cb23dca38a4700b527e1b..f739c5c4c696c52db338c6cea3cf036a4f245be7 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -75,7 +75,7 @@ impl super::LspAdapter for GoLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 484631d01f0809334eecaacd2851be540771fe36..4db48c67f05870d10d2ab2313900f86650fe9e6e 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -8,8 +8,8 @@ use futures::StreamExt; use gpui::{App, AsyncApp, Task}; use http_client::github::{GitHubLspBinaryVersion, latest_github_release}; use language::{ - ContextProvider, LanguageName, LanguageRegistry, LanguageToolchainStore, LocalFile as _, - LspAdapter, LspAdapterDelegate, + ContextProvider, LanguageName, LanguageRegistry, LocalFile as _, LspAdapter, + LspAdapterDelegate, Toolchain, }; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; @@ -303,7 +303,7 @@ impl LspAdapter for JsonLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate @@ -404,7 +404,7 @@ impl LspAdapter for JsonLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, cx: &mut AsyncApp, ) -> Result { let mut config = self.get_or_init_workspace_config(cx).await?; @@ -529,7 +529,7 @@ impl LspAdapter for NodeVersionAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 195ba79e1d0e96acea7ac1a53590c1a947334069..e446f22713d860d9574303d91579c305bcc44391 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -1,6 +1,6 @@ use anyhow::Context as _; use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; -use gpui::{App, UpdateGlobal}; +use gpui::{App, SharedString, UpdateGlobal}; use node_runtime::NodeRuntime; use python::PyprojectTomlManifestProvider; use rust::CargoManifestProvider; @@ -177,11 +177,13 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { adapters: vec![python_lsp_adapter.clone(), py_lsp_adapter.clone()], context: Some(python_context_provider), toolchain: Some(python_toolchain_provider), + manifest_name: Some(SharedString::new_static("pyproject.toml").into()), }, LanguageInfo { name: "rust", adapters: vec![rust_lsp_adapter], context: Some(rust_context_provider), + manifest_name: Some(SharedString::new_static("Cargo.toml").into()), ..Default::default() }, LanguageInfo { @@ -234,6 +236,7 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { registration.adapters, registration.context, registration.toolchain, + registration.manifest_name, ); } @@ -340,7 +343,7 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { Arc::from(PyprojectTomlManifestProvider), ]; for provider in manifest_providers { - project::ManifestProviders::global(cx).register(provider); + project::ManifestProvidersStore::global(cx).register(provider); } } @@ -350,6 +353,7 @@ struct LanguageInfo { adapters: Vec>, context: Option>, toolchain: Option>, + manifest_name: Option, } fn register_language( @@ -358,6 +362,7 @@ fn register_language( adapters: Vec>, context: Option>, toolchain: Option>, + manifest_name: Option, ) { let config = load_config(name); for adapter in adapters { @@ -368,12 +373,14 @@ fn register_language( config.grammar.clone(), config.matcher.clone(), config.hidden, + manifest_name.clone(), Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), queries: load_queries(name), context_provider: context.clone(), toolchain_provider: toolchain.clone(), + manifest_name: manifest_name.clone(), }) }), ); diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 40131089d1ccb8bc211df23f2c7def3810006181..b61ad2d36c8ef44a3bb2cd144f49bf6c968e84a8 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -127,7 +127,7 @@ impl LspAdapter for PythonLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { if let Some(pyright_bin) = delegate.which("pyright-langserver".as_ref()).await { @@ -319,17 +319,9 @@ impl LspAdapter for PythonLspAdapter { self: Arc, _: &dyn Fs, adapter: &Arc, - toolchains: Arc, + toolchain: Option, cx: &mut AsyncApp, ) -> Result { - let toolchain = toolchains - .active_toolchain( - adapter.worktree_id(), - Arc::from("".as_ref()), - LanguageName::new("Python"), - cx, - ) - .await; cx.update(move |cx| { let mut user_settings = language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx) @@ -397,9 +389,7 @@ impl LspAdapter for PythonLspAdapter { user_settings }) } - fn manifest_name(&self) -> Option { - Some(SharedString::new_static("pyproject.toml").into()) - } + fn workspace_folders_content(&self) -> WorkspaceFoldersContent { WorkspaceFoldersContent::WorktreeRoot } @@ -1046,8 +1036,8 @@ impl LspAdapter for PyLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - toolchains: Arc, - cx: &AsyncApp, + toolchain: Option, + _: &AsyncApp, ) -> Option { if let Some(pylsp_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await { let env = delegate.shell_env().await; @@ -1057,14 +1047,7 @@ impl LspAdapter for PyLspAdapter { arguments: vec![], }) } else { - let venv = toolchains - .active_toolchain( - delegate.worktree_id(), - Arc::from("".as_ref()), - LanguageName::new("Python"), - &mut cx.clone(), - ) - .await?; + let venv = toolchain?; let pylsp_path = Path::new(venv.path.as_ref()).parent()?.join("pylsp"); pylsp_path.exists().then(|| LanguageServerBinary { path: venv.path.to_string().into(), @@ -1211,17 +1194,9 @@ impl LspAdapter for PyLspAdapter { self: Arc, _: &dyn Fs, adapter: &Arc, - toolchains: Arc, + toolchain: Option, cx: &mut AsyncApp, ) -> Result { - let toolchain = toolchains - .active_toolchain( - adapter.worktree_id(), - Arc::from("".as_ref()), - LanguageName::new("Python"), - cx, - ) - .await; cx.update(move |cx| { let mut user_settings = language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx) @@ -1282,9 +1257,6 @@ impl LspAdapter for PyLspAdapter { user_settings }) } - fn manifest_name(&self) -> Option { - Some(SharedString::new_static("pyproject.toml").into()) - } fn workspace_folders_content(&self) -> WorkspaceFoldersContent { WorkspaceFoldersContent::WorktreeRoot } @@ -1377,8 +1349,8 @@ impl LspAdapter for BasedPyrightLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - toolchains: Arc, - cx: &AsyncApp, + toolchain: Option, + _: &AsyncApp, ) -> Option { if let Some(bin) = delegate.which(Self::BINARY_NAME.as_ref()).await { let env = delegate.shell_env().await; @@ -1388,15 +1360,7 @@ impl LspAdapter for BasedPyrightLspAdapter { arguments: vec!["--stdio".into()], }) } else { - let venv = toolchains - .active_toolchain( - delegate.worktree_id(), - Arc::from("".as_ref()), - LanguageName::new("Python"), - &mut cx.clone(), - ) - .await?; - let path = Path::new(venv.path.as_ref()) + let path = Path::new(toolchain?.path.as_ref()) .parent()? .join(Self::BINARY_NAME); path.exists().then(|| LanguageServerBinary { @@ -1543,17 +1507,9 @@ impl LspAdapter for BasedPyrightLspAdapter { self: Arc, _: &dyn Fs, adapter: &Arc, - toolchains: Arc, + toolchain: Option, cx: &mut AsyncApp, ) -> Result { - let toolchain = toolchains - .active_toolchain( - adapter.worktree_id(), - Arc::from("".as_ref()), - LanguageName::new("Python"), - cx, - ) - .await; cx.update(move |cx| { let mut user_settings = language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx) @@ -1622,10 +1578,6 @@ impl LspAdapter for BasedPyrightLspAdapter { }) } - fn manifest_name(&self) -> Option { - Some(SharedString::new_static("pyproject.toml").into()) - } - fn workspace_folders_content(&self) -> WorkspaceFoldersContent { WorkspaceFoldersContent::WorktreeRoot } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 3baaec18421f10cfd83aff44c348e7635e295acf..3ef7c1ba3442ee9fd953fbeed5ac528feb500a9a 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -109,14 +109,10 @@ impl LspAdapter for RustLspAdapter { SERVER_NAME.clone() } - fn manifest_name(&self) -> Option { - Some(SharedString::new_static("Cargo.toml").into()) - } - async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate.which("rust-analyzer".as_ref()).await?; diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 0d647f07cf0c97969928d5f292a5101127368016..27939c645cdb5a03f842974f849fb057d68e40b1 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use collections::HashMap; use futures::StreamExt; use gpui::AsyncApp; -use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; +use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain}; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; @@ -50,7 +50,7 @@ impl LspAdapter for TailwindLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; @@ -155,7 +155,7 @@ impl LspAdapter for TailwindLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, cx: &mut AsyncApp, ) -> Result { let mut tailwind_user_settings = cx.update(|cx| { diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 1877c86dc5278c7d8b5b2721125d50ae84ebbd01..dec7df4060463886236b7a296756b214b15a7359 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -7,7 +7,7 @@ use gpui::{App, AppContext, AsyncApp, Task}; use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url}; use language::{ ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter, - LspAdapterDelegate, + LspAdapterDelegate, Toolchain, }; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; @@ -722,7 +722,7 @@ impl LspAdapter for TypeScriptLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, cx: &mut AsyncApp, ) -> Result { let override_options = cx.update(|cx| { @@ -822,7 +822,7 @@ impl LspAdapter for EsLintLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, cx: &mut AsyncApp, ) -> Result { let workspace_root = delegate.worktree_root_path(); diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index 90faf883ba8b20016ec5b614d03de39ccb3a94e8..fd227e267dc585f90f35a815bede32dd9b9f78c8 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -2,7 +2,7 @@ use anyhow::Result; use async_trait::async_trait; use collections::HashMap; use gpui::AsyncApp; -use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; +use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain}; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; @@ -86,7 +86,7 @@ impl LspAdapter for VtslsLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let env = delegate.shell_env().await; @@ -211,7 +211,7 @@ impl LspAdapter for VtslsLspAdapter { self: Arc, fs: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, cx: &mut AsyncApp, ) -> Result { let tsdk_path = Self::tsdk_path(fs, delegate).await; diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 15a4d590bc2fcd13f611b8afdbf189b1b76b1eb9..137a9c2282eaf1ed62c042e1ad9581b3f6e3bbf8 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -2,9 +2,7 @@ use anyhow::{Context as _, Result}; use async_trait::async_trait; use futures::StreamExt; use gpui::AsyncApp; -use language::{ - LanguageToolchainStore, LspAdapter, LspAdapterDelegate, language_settings::AllLanguageSettings, -}; +use language::{LspAdapter, LspAdapterDelegate, Toolchain, language_settings::AllLanguageSettings}; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; @@ -57,7 +55,7 @@ impl LspAdapter for YamlLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; @@ -135,7 +133,7 @@ impl LspAdapter for YamlLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, cx: &mut AsyncApp, ) -> Result { let location = SettingsLocation { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index c458b6b300c34ec03d144cf297277faf4a94f5db..fcfeb9c66081624332d587a19b4ba9837aaf37bd 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -500,13 +500,12 @@ impl LspCommand for PerformRename { mut cx: AsyncApp, ) -> Result { if let Some(edit) = message { - let (lsp_adapter, lsp_server) = + let (_, lsp_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; LocalLspStore::deserialize_workspace_edit( lsp_store, edit, self.push_to_history, - lsp_adapter, lsp_server, &mut cx, ) @@ -1116,18 +1115,12 @@ pub async fn location_links_from_lsp( } } - let (lsp_adapter, language_server) = - language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; + let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; let mut definitions = Vec::new(); for (origin_range, target_uri, target_range) in unresolved_links { let target_buffer_handle = lsp_store .update(&mut cx, |this, cx| { - this.open_local_buffer_via_lsp( - target_uri, - language_server.server_id(), - lsp_adapter.name.clone(), - cx, - ) + this.open_local_buffer_via_lsp(target_uri, language_server.server_id(), cx) })? .await?; @@ -1172,8 +1165,7 @@ pub async fn location_link_from_lsp( server_id: LanguageServerId, cx: &mut AsyncApp, ) -> Result { - let (lsp_adapter, language_server) = - language_server_for_buffer(&lsp_store, &buffer, server_id, cx)?; + let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, cx)?; let (origin_range, target_uri, target_range) = ( link.origin_selection_range, @@ -1183,12 +1175,7 @@ pub async fn location_link_from_lsp( let target_buffer_handle = lsp_store .update(cx, |lsp_store, cx| { - lsp_store.open_local_buffer_via_lsp( - target_uri, - language_server.server_id(), - lsp_adapter.name.clone(), - cx, - ) + lsp_store.open_local_buffer_via_lsp(target_uri, language_server.server_id(), cx) })? .await?; @@ -1326,7 +1313,7 @@ impl LspCommand for GetReferences { mut cx: AsyncApp, ) -> Result> { let mut references = Vec::new(); - let (lsp_adapter, language_server) = + let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; if let Some(locations) = locations { @@ -1336,7 +1323,6 @@ impl LspCommand for GetReferences { lsp_store.open_local_buffer_via_lsp( lsp_location.uri, language_server.server_id(), - lsp_adapter.name.clone(), cx, ) })? diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 196f55171a5949866222164e221686bbeb3598f8..8ea41a100b58d0b6d890b1ee20abe2f3b1f3e459 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1,3 +1,14 @@ +//! LSP store provides unified access to the language server protocol. +//! The consumers of LSP store can interact with language servers without knowing exactly which language server they're interacting with. +//! +//! # Local/Remote LSP Stores +//! This module is split up into three distinct parts: +//! - [`LocalLspStore`], which is ran on the host machine (either project host or SSH host), that manages the lifecycle of language servers. +//! - [`RemoteLspStore`], which is ran on the remote machine (project guests) which is mostly about passing through the requests via RPC. +//! The remote stores don't really care about which language server they're running against - they don't usually get to decide which language server is going to responsible for handling their request. +//! - [`LspStore`], which unifies the two under one consistent interface for interacting with language servers. +//! +//! Most of the interesting work happens at the local layer, as bulk of the complexity is with managing the lifecycle of language servers. The actual implementation of the LSP protocol is handled by [`lsp`] crate. pub mod clangd_ext; pub mod json_language_server_ext; pub mod lsp_ext_command; @@ -6,20 +17,20 @@ pub mod rust_analyzer_ext; use crate::{ CodeAction, ColorPresentation, Completion, CompletionResponse, CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, LocationLink, LspAction, LspPullDiagnostics, - ProjectItem, ProjectPath, ProjectTransaction, PulledDiagnostics, ResolveState, Symbol, - ToolchainStore, + ManifestProvidersStore, ProjectItem, ProjectPath, ProjectTransaction, PulledDiagnostics, + ResolveState, Symbol, buffer_store::{BufferStore, BufferStoreEvent}, environment::ProjectEnvironment, lsp_command::{self, *}, lsp_store, manifest_tree::{ - AdapterQuery, LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, - ManifestQueryDelegate, ManifestTree, + LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, ManifestQueryDelegate, + ManifestTree, }, prettier_store::{self, PrettierStore, PrettierStoreEvent}, project_settings::{LspSettings, ProjectSettings}, relativize_path, resolve_path, - toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent}, + toolchain_store::{LocalToolchainStore, ToolchainStoreEvent}, worktree_store::{WorktreeStore, WorktreeStoreEvent}, yarn::YarnPathStore, }; @@ -44,9 +55,9 @@ use itertools::Itertools as _; use language::{ Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName, - LanguageRegistry, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, - PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, - WorkspaceFoldersContent, + LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, ManifestDelegate, ManifestName, + Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Toolchain, Transaction, + Unclipped, WorkspaceFoldersContent, language_settings::{ FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings, }, @@ -140,6 +151,20 @@ impl FormatTrigger { } } +#[derive(Clone)] +struct UnifiedLanguageServer { + id: LanguageServerId, + project_roots: HashSet>, +} + +#[derive(Clone, Hash, PartialEq, Eq)] +struct LanguageServerSeed { + worktree_id: WorktreeId, + name: LanguageServerName, + toolchain: Option, + settings: Arc, +} + #[derive(Debug)] pub struct DocumentDiagnosticsUpdate<'a, D> { pub diagnostics: D, @@ -157,17 +182,18 @@ pub struct DocumentDiagnostics { pub struct LocalLspStore { weak: WeakEntity, worktree_store: Entity, - toolchain_store: Entity, + toolchain_store: Entity, http_client: Arc, environment: Entity, fs: Arc, languages: Arc, - language_server_ids: HashMap<(WorktreeId, LanguageServerName), BTreeSet>, + language_server_ids: HashMap, yarn: Entity, pub language_servers: HashMap, buffers_being_formatted: HashSet, last_workspace_edits_by_language_server: HashMap, language_server_watched_paths: HashMap, + watched_manifest_filenames: HashSet, language_server_paths_watched_for_rename: HashMap, language_server_watcher_registrations: @@ -188,7 +214,7 @@ pub struct LocalLspStore { >, buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots _subscription: gpui::Subscription, - lsp_tree: Entity, + lsp_tree: LanguageServerTree, registered_buffers: HashMap, buffers_opened_in_servers: HashMap>, buffer_pull_diagnostics_result_ids: HashMap>>, @@ -208,19 +234,63 @@ impl LocalLspStore { } } + fn get_or_insert_language_server( + &mut self, + worktree_handle: &Entity, + delegate: Arc, + disposition: &Arc, + language_name: &LanguageName, + cx: &mut App, + ) -> LanguageServerId { + let key = LanguageServerSeed { + worktree_id: worktree_handle.read(cx).id(), + name: disposition.server_name.clone(), + settings: disposition.settings.clone(), + toolchain: disposition.toolchain.clone(), + }; + if let Some(state) = self.language_server_ids.get_mut(&key) { + state.project_roots.insert(disposition.path.path.clone()); + state.id + } else { + let adapter = self + .languages + .lsp_adapters(language_name) + .into_iter() + .find(|adapter| adapter.name() == disposition.server_name) + .expect("To find LSP adapter"); + let new_language_server_id = self.start_language_server( + worktree_handle, + delegate, + adapter, + disposition.settings.clone(), + key.clone(), + cx, + ); + if let Some(state) = self.language_server_ids.get_mut(&key) { + state.project_roots.insert(disposition.path.path.clone()); + } else { + debug_assert!( + false, + "Expected `start_language_server` to ensure that `key` exists in a map" + ); + } + new_language_server_id + } + } + fn start_language_server( &mut self, worktree_handle: &Entity, delegate: Arc, adapter: Arc, settings: Arc, + key: LanguageServerSeed, cx: &mut App, ) -> LanguageServerId { let worktree = worktree_handle.read(cx); - let worktree_id = worktree.id(); - let root_path = worktree.abs_path(); - let key = (worktree_id, adapter.name.clone()); + let root_path = worktree.abs_path(); + let toolchain = key.toolchain.clone(); let override_options = settings.initialization_options.clone(); let stderr_capture = Arc::new(Mutex::new(Some(String::new()))); @@ -231,7 +301,14 @@ impl LocalLspStore { adapter.name.0 ); - let binary = self.get_language_server_binary(adapter.clone(), delegate.clone(), true, cx); + let binary = self.get_language_server_binary( + adapter.clone(), + settings, + toolchain.clone(), + delegate.clone(), + true, + cx, + ); let pending_workspace_folders: Arc>> = Default::default(); let pending_server = cx.spawn({ @@ -290,15 +367,13 @@ impl LocalLspStore { .enabled; cx.spawn(async move |cx| { let result = async { - let toolchains = - lsp_store.update(cx, |lsp_store, cx| lsp_store.toolchain_store(cx))?; let language_server = pending_server.await?; let workspace_config = Self::workspace_configuration_for_adapter( adapter.adapter.clone(), fs.as_ref(), &delegate, - toolchains.clone(), + toolchain, cx, ) .await?; @@ -417,31 +492,26 @@ impl LocalLspStore { self.language_servers.insert(server_id, state); self.language_server_ids .entry(key) - .or_default() - .insert(server_id); + .or_insert(UnifiedLanguageServer { + id: server_id, + project_roots: Default::default(), + }); server_id } fn get_language_server_binary( &self, adapter: Arc, + settings: Arc, + toolchain: Option, delegate: Arc, allow_binary_download: bool, cx: &mut App, ) -> Task> { - let settings = ProjectSettings::get( - Some(SettingsLocation { - worktree_id: delegate.worktree_id(), - path: Path::new(""), - }), - cx, - ) - .lsp - .get(&adapter.name) - .and_then(|s| s.binary.clone()); - - if settings.as_ref().is_some_and(|b| b.path.is_some()) { - let settings = settings.unwrap(); + if let Some(settings) = settings.binary.as_ref() + && settings.path.is_some() + { + let settings = settings.clone(); return cx.background_spawn(async move { let mut env = delegate.shell_env().await; @@ -461,16 +531,17 @@ impl LocalLspStore { } let lsp_binary_options = LanguageServerBinaryOptions { allow_path_lookup: !settings + .binary .as_ref() .and_then(|b| b.ignore_system_version) .unwrap_or_default(), allow_binary_download, }; - let toolchains = self.toolchain_store.read(cx).as_language_toolchain_store(); + cx.spawn(async move |cx| { let binary_result = adapter .clone() - .get_language_server_command(delegate.clone(), toolchains, lsp_binary_options, cx) + .get_language_server_command(delegate.clone(), toolchain, lsp_binary_options, cx) .await; delegate.update_status(adapter.name.clone(), BinaryStatus::None); @@ -480,12 +551,12 @@ impl LocalLspStore { shell_env.extend(binary.env.unwrap_or_default()); - if let Some(settings) = settings { - if let Some(arguments) = settings.arguments { + if let Some(settings) = settings.binary.as_ref() { + if let Some(arguments) = &settings.arguments { binary.arguments = arguments.into_iter().map(Into::into).collect(); } - if let Some(env) = settings.env { - shell_env.extend(env); + if let Some(env) = &settings.env { + shell_env.extend(env.iter().map(|(k, v)| (k.clone(), v.clone()))); } } @@ -559,14 +630,20 @@ impl LocalLspStore { let fs = fs.clone(); let mut cx = cx.clone(); async move { - let toolchains = - this.update(&mut cx, |this, cx| this.toolchain_store(cx))?; - + let toolchain_for_id = this + .update(&mut cx, |this, _| { + this.as_local()?.language_server_ids.iter().find_map( + |(seed, value)| { + (value.id == server_id).then(|| seed.toolchain.clone()) + }, + ) + })? + .context("Expected the LSP store to be in a local mode")?; let workspace_config = Self::workspace_configuration_for_adapter( adapter.clone(), fs.as_ref(), &delegate, - toolchains.clone(), + toolchain_for_id, &mut cx, ) .await?; @@ -700,18 +777,15 @@ impl LocalLspStore { language_server .on_request::({ - let adapter = adapter.clone(); let this = this.clone(); move |params, cx| { let mut cx = cx.clone(); let this = this.clone(); - let adapter = adapter.clone(); async move { LocalLspStore::on_lsp_workspace_edit( this.clone(), params, server_id, - adapter.clone(), &mut cx, ) .await @@ -960,19 +1034,18 @@ impl LocalLspStore { ) -> impl Iterator> { self.language_server_ids .iter() - .flat_map(move |((language_server_path, _), ids)| { - ids.iter().filter_map(move |id| { - if *language_server_path != worktree_id { - return None; - } - if let Some(LanguageServerState::Running { server, .. }) = - self.language_servers.get(id) - { - return Some(server); - } else { - None - } - }) + .filter_map(move |(seed, state)| { + if seed.worktree_id != worktree_id { + return None; + } + + if let Some(LanguageServerState::Running { server, .. }) = + self.language_servers.get(&state.id) + { + return Some(server); + } else { + None + } }) } @@ -989,17 +1062,18 @@ impl LocalLspStore { else { return Vec::new(); }; - let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); - let root = self.lsp_tree.update(cx, |this, cx| { - this.get( + let delegate: Arc = + Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); + let root = self + .lsp_tree + .get( project_path, - AdapterQuery::Language(&language.name()), - delegate, + language.name(), + language.manifest(), + &delegate, cx, ) - .filter_map(|node| node.server_id()) - .collect::>() - }); + .collect::>(); root } @@ -1083,7 +1157,7 @@ impl LocalLspStore { .collect::>() }) })?; - for (lsp_adapter, language_server) in adapters_and_servers.iter() { + for (_, language_server) in adapters_and_servers.iter() { let actions = Self::get_server_code_actions_from_action_kinds( &lsp_store, language_server.server_id(), @@ -1095,7 +1169,6 @@ impl LocalLspStore { Self::execute_code_actions_on_server( &lsp_store, language_server, - lsp_adapter, actions, push_to_history, &mut project_transaction, @@ -2038,13 +2111,14 @@ impl LocalLspStore { let buffer = buffer_handle.read(cx); let file = buffer.file().cloned(); + let Some(file) = File::from_dyn(file.as_ref()) else { return; }; if !file.is_local() { return; } - + let path = ProjectPath::from_file(file, cx); let worktree_id = file.worktree_id(cx); let language = buffer.language().cloned(); @@ -2067,46 +2141,52 @@ impl LocalLspStore { let Some(language) = language else { return; }; - for adapter in self.languages.lsp_adapters(&language.name()) { - let servers = self - .language_server_ids - .get(&(worktree_id, adapter.name.clone())); - if let Some(server_ids) = servers { - for server_id in server_ids { - let server = self - .language_servers - .get(server_id) - .and_then(|server_state| { - if let LanguageServerState::Running { server, .. } = server_state { - Some(server.clone()) - } else { - None - } - }); - let server = match server { - Some(server) => server, - None => continue, - }; + let Some(snapshot) = self + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + .map(|worktree| worktree.read(cx).snapshot()) + else { + return; + }; + let delegate: Arc = Arc::new(ManifestQueryDelegate::new(snapshot)); - buffer_handle.update(cx, |buffer, cx| { - buffer.set_completion_triggers( - server.server_id(), - server - .capabilities() - .completion_provider + for server_id in + self.lsp_tree + .get(path, language.name(), language.manifest(), &delegate, cx) + { + let server = self + .language_servers + .get(&server_id) + .and_then(|server_state| { + if let LanguageServerState::Running { server, .. } = server_state { + Some(server.clone()) + } else { + None + } + }); + let server = match server { + Some(server) => server, + None => continue, + }; + + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + server.server_id(), + server + .capabilities() + .completion_provider + .as_ref() + .and_then(|provider| { + provider + .trigger_characters .as_ref() - .and_then(|provider| { - provider - .trigger_characters - .as_ref() - .map(|characters| characters.iter().cloned().collect()) - }) - .unwrap_or_default(), - cx, - ); - }); - } - } + .map(|characters| characters.iter().cloned().collect()) + }) + .unwrap_or_default(), + cx, + ); + }); } } @@ -2216,6 +2296,31 @@ impl LocalLspStore { Ok(()) } + fn register_language_server_for_invisible_worktree( + &mut self, + worktree: &Entity, + language_server_id: LanguageServerId, + cx: &mut App, + ) { + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + debug_assert!(!worktree.is_visible()); + let Some(mut origin_seed) = self + .language_server_ids + .iter() + .find_map(|(seed, state)| (state.id == language_server_id).then(|| seed.clone())) + else { + return; + }; + origin_seed.worktree_id = worktree_id; + self.language_server_ids + .entry(origin_seed) + .or_insert_with(|| UnifiedLanguageServer { + id: language_server_id, + project_roots: Default::default(), + }); + } + fn register_buffer_with_language_servers( &mut self, buffer_handle: &Entity, @@ -2256,27 +2361,23 @@ impl LocalLspStore { }; let language_name = language.name(); let (reused, delegate, servers) = self - .lsp_tree - .update(cx, |lsp_tree, cx| { - self.reuse_existing_language_server(lsp_tree, &worktree, &language_name, cx) - }) - .map(|(delegate, servers)| (true, delegate, servers)) + .reuse_existing_language_server(&self.lsp_tree, &worktree, &language_name, cx) + .map(|(delegate, apply)| (true, delegate, apply(&mut self.lsp_tree))) .unwrap_or_else(|| { let lsp_delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx); - let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); + let delegate: Arc = + Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); + let servers = self .lsp_tree - .clone() - .update(cx, |language_server_tree, cx| { - language_server_tree - .get( - ProjectPath { worktree_id, path }, - AdapterQuery::Language(&language.name()), - delegate.clone(), - cx, - ) - .collect::>() - }); + .walk( + ProjectPath { worktree_id, path }, + language.name(), + language.manifest(), + &delegate, + cx, + ) + .collect::>(); (false, lsp_delegate, servers) }); let servers_and_adapters = servers @@ -2298,55 +2399,35 @@ impl LocalLspStore { } } - let server_id = server_node.server_id_or_init( - |LaunchDisposition { - server_name, - path, - settings, - }| { - let server_id = - { - let uri = Url::from_file_path( - worktree.read(cx).abs_path().join(&path.path), - ); - let key = (worktree_id, server_name.clone()); - if !self.language_server_ids.contains_key(&key) { - let language_name = language.name(); - let adapter = self.languages - .lsp_adapters(&language_name) - .into_iter() - .find(|adapter| &adapter.name() == server_name) - .expect("To find LSP adapter"); - self.start_language_server( - &worktree, - delegate.clone(), - adapter, - settings, - cx, - ); - } - if let Some(server_ids) = self - .language_server_ids - .get(&key) - { - debug_assert_eq!(server_ids.len(), 1); - let server_id = server_ids.iter().cloned().next().unwrap(); - if let Some(state) = self.language_servers.get(&server_id) { - if let Ok(uri) = uri { - state.add_workspace_folder(uri); - }; - } - server_id - } else { - unreachable!("Language server ID should be available, as it's registered on demand") - } + let server_id = server_node.server_id_or_init(|disposition| { + let path = &disposition.path; + let server_id = { + let uri = + Url::from_file_path(worktree.read(cx).abs_path().join(&path.path)); + + let server_id = self.get_or_insert_language_server( + &worktree, + delegate.clone(), + disposition, + &language_name, + cx, + ); - }; + if let Some(state) = self.language_servers.get(&server_id) { + if let Ok(uri) = uri { + state.add_workspace_folder(uri); + }; + } server_id - }, - )?; + }; + + server_id + })?; let server_state = self.language_servers.get(&server_id)?; - if let LanguageServerState::Running { server, adapter, .. } = server_state { + if let LanguageServerState::Running { + server, adapter, .. + } = server_state + { Some((server.clone(), adapter.clone())) } else { None @@ -2413,13 +2494,16 @@ impl LocalLspStore { } } - fn reuse_existing_language_server( + fn reuse_existing_language_server<'lang_name>( &self, - server_tree: &mut LanguageServerTree, + server_tree: &LanguageServerTree, worktree: &Entity, - language_name: &LanguageName, + language_name: &'lang_name LanguageName, cx: &mut App, - ) -> Option<(Arc, Vec)> { + ) -> Option<( + Arc, + impl FnOnce(&mut LanguageServerTree) -> Vec + use<'lang_name>, + )> { if worktree.read(cx).is_visible() { return None; } @@ -2458,16 +2542,16 @@ impl LocalLspStore { .into_values() .max_by_key(|servers| servers.len())?; - for server_node in &servers { - server_tree.register_reused( - worktree.read(cx).id(), - language_name.clone(), - server_node.clone(), - ); - } + let worktree_id = worktree.read(cx).id(); + let apply = move |tree: &mut LanguageServerTree| { + for server_node in &servers { + tree.register_reused(worktree_id, language_name.clone(), server_node.clone()); + } + servers + }; let delegate = LocalLspAdapterDelegate::from_local_lsp(self, worktree, cx); - Some((delegate, servers)) + Some((delegate, apply)) } pub(crate) fn unregister_old_buffer_from_language_servers( @@ -2568,7 +2652,7 @@ impl LocalLspStore { pub async fn execute_code_actions_on_server( lsp_store: &WeakEntity, language_server: &Arc, - lsp_adapter: &Arc, + actions: Vec, push_to_history: bool, project_transaction: &mut ProjectTransaction, @@ -2588,7 +2672,6 @@ impl LocalLspStore { lsp_store.upgrade().context("project dropped")?, edit.clone(), push_to_history, - lsp_adapter.clone(), language_server.clone(), cx, ) @@ -2769,7 +2852,6 @@ impl LocalLspStore { this: Entity, edit: lsp::WorkspaceEdit, push_to_history: bool, - lsp_adapter: Arc, language_server: Arc, cx: &mut AsyncApp, ) -> Result { @@ -2870,7 +2952,6 @@ impl LocalLspStore { this.open_local_buffer_via_lsp( op.text_document.uri.clone(), language_server.server_id(), - lsp_adapter.name.clone(), cx, ) })? @@ -2995,7 +3076,6 @@ impl LocalLspStore { this: WeakEntity, params: lsp::ApplyWorkspaceEditParams, server_id: LanguageServerId, - adapter: Arc, cx: &mut AsyncApp, ) -> Result { let this = this.upgrade().context("project project closed")?; @@ -3006,7 +3086,6 @@ impl LocalLspStore { this.clone(), params.edit, true, - adapter.clone(), language_server.clone(), cx, ) @@ -3037,23 +3116,19 @@ impl LocalLspStore { prettier_store.remove_worktree(id_to_remove, cx); }); - let mut servers_to_remove = BTreeMap::default(); + let mut servers_to_remove = BTreeSet::default(); let mut servers_to_preserve = HashSet::default(); - for ((path, server_name), ref server_ids) in &self.language_server_ids { - if *path == id_to_remove { - servers_to_remove.extend(server_ids.iter().map(|id| (*id, server_name.clone()))); + for (seed, ref state) in &self.language_server_ids { + if seed.worktree_id == id_to_remove { + servers_to_remove.insert(state.id); } else { - servers_to_preserve.extend(server_ids.iter().cloned()); + servers_to_preserve.insert(state.id); } } - servers_to_remove.retain(|server_id, _| !servers_to_preserve.contains(server_id)); - - for (server_id_to_remove, _) in &servers_to_remove { - self.language_server_ids - .values_mut() - .for_each(|server_ids| { - server_ids.remove(server_id_to_remove); - }); + servers_to_remove.retain(|server_id| !servers_to_preserve.contains(server_id)); + self.language_server_ids + .retain(|_, state| !servers_to_remove.contains(&state.id)); + for server_id_to_remove in &servers_to_remove { self.language_server_watched_paths .remove(server_id_to_remove); self.language_server_paths_watched_for_rename @@ -3068,7 +3143,7 @@ impl LocalLspStore { } cx.emit(LspStoreEvent::LanguageServerRemoved(*server_id_to_remove)); } - servers_to_remove.into_keys().collect() + servers_to_remove.into_iter().collect() } fn rebuild_watched_paths_inner<'a>( @@ -3326,16 +3401,20 @@ impl LocalLspStore { Ok(Some(initialization_config)) } + fn toolchain_store(&self) -> &Entity { + &self.toolchain_store + } + async fn workspace_configuration_for_adapter( adapter: Arc, fs: &dyn Fs, delegate: &Arc, - toolchains: Arc, + toolchain: Option, cx: &mut AsyncApp, ) -> Result { let mut workspace_config = adapter .clone() - .workspace_configuration(fs, delegate, toolchains.clone(), cx) + .workspace_configuration(fs, delegate, toolchain, cx) .await?; for other_adapter in delegate.registered_lsp_adapters() { @@ -3344,13 +3423,7 @@ impl LocalLspStore { } if let Ok(Some(target_config)) = other_adapter .clone() - .additional_workspace_configuration( - adapter.name(), - fs, - delegate, - toolchains.clone(), - cx, - ) + .additional_workspace_configuration(adapter.name(), fs, delegate, cx) .await { merge_json_value_into(target_config.clone(), &mut workspace_config); @@ -3416,7 +3489,6 @@ pub struct LspStore { nonce: u128, buffer_store: Entity, worktree_store: Entity, - toolchain_store: Option>, pub languages: Arc, language_server_statuses: BTreeMap, active_entry: Option, @@ -3607,7 +3679,7 @@ impl LspStore { buffer_store: Entity, worktree_store: Entity, prettier_store: Entity, - toolchain_store: Entity, + toolchain_store: Entity, environment: Entity, manifest_tree: Entity, languages: Arc, @@ -3649,7 +3721,7 @@ impl LspStore { mode: LspStoreMode::Local(LocalLspStore { weak: cx.weak_entity(), worktree_store: worktree_store.clone(), - toolchain_store: toolchain_store.clone(), + supplementary_language_servers: Default::default(), languages: languages.clone(), language_server_ids: Default::default(), @@ -3672,16 +3744,22 @@ impl LspStore { .unwrap() .shutdown_language_servers_on_quit(cx) }), - lsp_tree: LanguageServerTree::new(manifest_tree, languages.clone(), cx), + lsp_tree: LanguageServerTree::new( + manifest_tree, + languages.clone(), + toolchain_store.clone(), + ), + toolchain_store, registered_buffers: HashMap::default(), buffers_opened_in_servers: HashMap::default(), buffer_pull_diagnostics_result_ids: HashMap::default(), + watched_manifest_filenames: ManifestProvidersStore::global(cx) + .manifest_file_names(), }), last_formatting_failure: None, downstream_client: None, buffer_store, worktree_store, - toolchain_store: Some(toolchain_store), languages: languages.clone(), language_server_statuses: Default::default(), nonce: StdRng::from_entropy().r#gen(), @@ -3719,7 +3797,6 @@ impl LspStore { pub(super) fn new_remote( buffer_store: Entity, worktree_store: Entity, - toolchain_store: Option>, languages: Arc, upstream_client: AnyProtoClient, project_id: u64, @@ -3752,7 +3829,7 @@ impl LspStore { lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), active_entry: None, - toolchain_store, + _maintain_workspace_config, _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), } @@ -3851,7 +3928,7 @@ impl LspStore { fn on_toolchain_store_event( &mut self, - _: Entity, + _: Entity, event: &ToolchainStoreEvent, _: &mut Context, ) { @@ -3930,9 +4007,9 @@ impl LspStore { let local = this.as_local()?; let mut servers = Vec::new(); - for ((worktree_id, _), server_ids) in &local.language_server_ids { - for server_id in server_ids { - let Some(states) = local.language_servers.get(server_id) else { + for (seed, state) in &local.language_server_ids { + + let Some(states) = local.language_servers.get(&state.id) else { continue; }; let (json_adapter, json_server) = match states { @@ -3947,7 +4024,7 @@ impl LspStore { let Some(worktree) = this .worktree_store .read(cx) - .worktree_for_id(*worktree_id, cx) + .worktree_for_id(seed.worktree_id, cx) else { continue; }; @@ -3963,7 +4040,7 @@ impl LspStore { ); servers.push((json_adapter, json_server, json_delegate)); - } + } return Some(servers); }) @@ -3974,9 +4051,9 @@ impl LspStore { return; }; - let Ok(Some((fs, toolchain_store))) = this.read_with(cx, |this, cx| { + let Ok(Some((fs, _))) = this.read_with(cx, |this, _| { let local = this.as_local()?; - let toolchain_store = this.toolchain_store(cx); + let toolchain_store = local.toolchain_store().clone(); return Some((local.fs.clone(), toolchain_store)); }) else { return; @@ -3988,7 +4065,7 @@ impl LspStore { adapter, fs.as_ref(), &delegate, - toolchain_store.clone(), + None, cx, ) .await @@ -4533,7 +4610,7 @@ impl LspStore { } } - self.refresh_server_tree(cx); + self.request_workspace_config_refresh(); if let Some(prettier_store) = self.as_local().map(|s| s.prettier_store.clone()) { prettier_store.update(cx, |prettier_store, cx| { @@ -4546,158 +4623,150 @@ impl LspStore { fn refresh_server_tree(&mut self, cx: &mut Context) { let buffer_store = self.buffer_store.clone(); - if let Some(local) = self.as_local_mut() { - let mut adapters = BTreeMap::default(); - let get_adapter = { - let languages = local.languages.clone(); - let environment = local.environment.clone(); - let weak = local.weak.clone(); - let worktree_store = local.worktree_store.clone(); - let http_client = local.http_client.clone(); - let fs = local.fs.clone(); - move |worktree_id, cx: &mut App| { - let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?; - Some(LocalLspAdapterDelegate::new( - languages.clone(), - &environment, - weak.clone(), - &worktree, - http_client.clone(), - fs.clone(), - cx, - )) - } - }; + let Some(local) = self.as_local_mut() else { + return; + }; + let mut adapters = BTreeMap::default(); + let get_adapter = { + let languages = local.languages.clone(); + let environment = local.environment.clone(); + let weak = local.weak.clone(); + let worktree_store = local.worktree_store.clone(); + let http_client = local.http_client.clone(); + let fs = local.fs.clone(); + move |worktree_id, cx: &mut App| { + let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?; + Some(LocalLspAdapterDelegate::new( + languages.clone(), + &environment, + weak.clone(), + &worktree, + http_client.clone(), + fs.clone(), + cx, + )) + } + }; - let mut messages_to_report = Vec::new(); - let to_stop = local.lsp_tree.clone().update(cx, |lsp_tree, cx| { - let mut rebase = lsp_tree.rebase(); - for buffer_handle in buffer_store.read(cx).buffers().sorted_by_key(|buffer| { - Reverse( - File::from_dyn(buffer.read(cx).file()) - .map(|file| file.worktree.read(cx).is_visible()), - ) - }) { - let buffer = buffer_handle.read(cx); - 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()) - .cloned() - .zip(buffer.language().map(|l| l.name())) + let mut messages_to_report = Vec::new(); + let (new_tree, to_stop) = { + let mut rebase = local.lsp_tree.rebase(); + let buffers = buffer_store + .read(cx) + .buffers() + .filter_map(|buffer| { + let raw_buffer = buffer.read(cx); + if !local + .registered_buffers + .contains_key(&raw_buffer.remote_id()) { - let worktree_id = file.worktree_id(cx); - let Some(worktree) = local - .worktree_store - .read(cx) - .worktree_for_id(worktree_id, cx) - else { - continue; - }; + return None; + } + let file = File::from_dyn(raw_buffer.file()).cloned()?; + let language = raw_buffer.language().cloned()?; + Some((file, language, raw_buffer.remote_id())) + }) + .sorted_by_key(|(file, _, _)| Reverse(file.worktree.read(cx).is_visible())); - let Some((reused, delegate, nodes)) = local - .reuse_existing_language_server( - rebase.server_tree(), - &worktree, - &language, - cx, - ) - .map(|(delegate, servers)| (true, delegate, servers)) - .or_else(|| { - let lsp_delegate = adapters - .entry(worktree_id) - .or_insert_with(|| get_adapter(worktree_id, cx)) - .clone()?; - let delegate = Arc::new(ManifestQueryDelegate::new( - worktree.read(cx).snapshot(), - )); - let path = file - .path() - .parent() - .map(Arc::from) - .unwrap_or_else(|| file.path().clone()); - let worktree_path = ProjectPath { worktree_id, path }; - - let nodes = rebase.get( - worktree_path, - AdapterQuery::Language(&language), - delegate.clone(), - cx, - ); + for (file, language, buffer_id) in buffers { + let worktree_id = file.worktree_id(cx); + let Some(worktree) = local + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + else { + continue; + }; - Some((false, lsp_delegate, nodes.collect())) - }) - else { - continue; - }; + if let Some((_, apply)) = local.reuse_existing_language_server( + rebase.server_tree(), + &worktree, + &language.name(), + cx, + ) { + (apply)(rebase.server_tree()); + } else if let Some(lsp_delegate) = adapters + .entry(worktree_id) + .or_insert_with(|| get_adapter(worktree_id, cx)) + .clone() + { + let delegate = + Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); + let path = file + .path() + .parent() + .map(Arc::from) + .unwrap_or_else(|| file.path().clone()); + let worktree_path = ProjectPath { worktree_id, path }; + let abs_path = file.abs_path(cx); + let worktree_root = worktree.read(cx).abs_path(); + let nodes = rebase + .walk( + worktree_path, + language.name(), + language.manifest(), + delegate.clone(), + cx, + ) + .collect::>(); - let abs_path = file.abs_path(cx); - for node in nodes { - if !reused { - let server_id = node.server_id_or_init( - |LaunchDisposition { - server_name, - - path, - settings, - }| - { - let uri = Url::from_file_path( - worktree.read(cx).abs_path().join(&path.path), - ); - let key = (worktree_id, server_name.clone()); - local.language_server_ids.remove(&key); - - let adapter = local - .languages - .lsp_adapters(&language) - .into_iter() - .find(|adapter| &adapter.name() == server_name) - .expect("To find LSP adapter"); - let server_id = local.start_language_server( - &worktree, - delegate.clone(), - adapter, - settings, - cx, - ); - if let Some(state) = - local.language_servers.get(&server_id) - { - if let Ok(uri) = uri { - state.add_workspace_folder(uri); - }; - } - server_id - } - ); + for node in nodes { + let server_id = node.server_id_or_init(|disposition| { + let path = &disposition.path; + let uri = Url::from_file_path(worktree_root.join(&path.path)); + let key = LanguageServerSeed { + worktree_id, + name: disposition.server_name.clone(), + settings: disposition.settings.clone(), + toolchain: local.toolchain_store.read(cx).active_toolchain( + path.worktree_id, + &path.path, + language.name(), + ), + }; + local.language_server_ids.remove(&key); - if let Some(language_server_id) = server_id { - messages_to_report.push(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: node.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(), - }, - ), - }); - } + let server_id = local.get_or_insert_language_server( + &worktree, + lsp_delegate.clone(), + disposition, + &language.name(), + cx, + ); + if let Some(state) = local.language_servers.get(&server_id) { + if let Ok(uri) = uri { + state.add_workspace_folder(uri); + }; } + server_id + }); + + if let Some(language_server_id) = server_id { + messages_to_report.push(LspStoreEvent::LanguageServerUpdate { + language_server_id, + name: node.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(), + }, + ), + }); } } + } else { + continue; } - rebase.finish() - }); - for message in messages_to_report { - cx.emit(message); - } - for (id, _) in to_stop { - self.stop_local_language_server(id, cx).detach(); } + rebase.finish() + }; + for message in messages_to_report { + cx.emit(message); + } + local.lsp_tree = new_tree; + for (id, _) in to_stop { + self.stop_local_language_server(id, cx).detach(); } } @@ -4729,7 +4798,7 @@ impl LspStore { .await }) } else if self.mode.is_local() { - let Some((lsp_adapter, lang_server)) = buffer_handle.update(cx, |buffer, cx| { + let Some((_, lang_server)) = buffer_handle.update(cx, |buffer, cx| { self.language_server_for_local_buffer(buffer, action.server_id, cx) .map(|(adapter, server)| (adapter.clone(), server.clone())) }) else { @@ -4745,7 +4814,7 @@ impl LspStore { this.upgrade().context("no app present")?, edit.clone(), push_to_history, - lsp_adapter.clone(), + lang_server.clone(), cx, ) @@ -7073,11 +7142,11 @@ impl LspStore { let mut requests = Vec::new(); let mut requested_servers = BTreeSet::new(); - 'next_server: for ((worktree_id, _), server_ids) in local.language_server_ids.iter() { + for (seed, state) in local.language_server_ids.iter() { let Some(worktree_handle) = self .worktree_store .read(cx) - .worktree_for_id(*worktree_id, cx) + .worktree_for_id(seed.worktree_id, cx) else { continue; }; @@ -7086,31 +7155,30 @@ impl LspStore { continue; } - let mut servers_to_query = server_ids - .difference(&requested_servers) - .cloned() - .collect::>(); - for server_id in &servers_to_query { - let (lsp_adapter, server) = match local.language_servers.get(server_id) { - Some(LanguageServerState::Running { - adapter, server, .. - }) => (adapter.clone(), server), - - _ => continue 'next_server, + if !requested_servers.insert(state.id) { + continue; + } + + let (lsp_adapter, server) = match local.language_servers.get(&state.id) { + Some(LanguageServerState::Running { + adapter, server, .. + }) => (adapter.clone(), server), + + _ => continue, + }; + let supports_workspace_symbol_request = + match server.capabilities().workspace_symbol_provider { + Some(OneOf::Left(supported)) => supported, + Some(OneOf::Right(_)) => true, + None => false, }; - let supports_workspace_symbol_request = - match server.capabilities().workspace_symbol_provider { - Some(OneOf::Left(supported)) => supported, - Some(OneOf::Right(_)) => true, - None => false, - }; - if !supports_workspace_symbol_request { - continue 'next_server; - } - let worktree_abs_path = worktree.abs_path().clone(); - let worktree_handle = worktree_handle.clone(); - let server_id = server.server_id(); - requests.push( + if !supports_workspace_symbol_request { + continue; + } + let worktree_abs_path = worktree.abs_path().clone(); + let worktree_handle = worktree_handle.clone(); + let server_id = server.server_id(); + requests.push( server .request::( lsp::WorkspaceSymbolParams { @@ -7152,8 +7220,6 @@ impl LspStore { } }), ); - } - requested_servers.append(&mut servers_to_query); } cx.spawn(async move |this, cx| { @@ -7416,7 +7482,7 @@ impl LspStore { None } - pub(crate) async fn refresh_workspace_configurations( + async fn refresh_workspace_configurations( lsp_store: &WeakEntity, fs: Arc, cx: &mut AsyncApp, @@ -7425,71 +7491,70 @@ impl LspStore { let mut refreshed_servers = HashSet::default(); let servers = lsp_store .update(cx, |lsp_store, cx| { - let toolchain_store = lsp_store.toolchain_store(cx); - let Some(local) = lsp_store.as_local() else { - return Vec::default(); - }; - local + let local = lsp_store.as_local()?; + + let servers = local .language_server_ids .iter() - .flat_map(|((worktree_id, _), server_ids)| { + .filter_map(|(seed, state)| { let worktree = lsp_store .worktree_store .read(cx) - .worktree_for_id(*worktree_id, cx); - let delegate = worktree.map(|worktree| { - LocalLspAdapterDelegate::new( - local.languages.clone(), - &local.environment, - cx.weak_entity(), - &worktree, - local.http_client.clone(), - local.fs.clone(), - cx, - ) - }); + .worktree_for_id(seed.worktree_id, cx); + let delegate: Arc = + worktree.map(|worktree| { + LocalLspAdapterDelegate::new( + local.languages.clone(), + &local.environment, + cx.weak_entity(), + &worktree, + local.http_client.clone(), + local.fs.clone(), + cx, + ) + })?; + let server_id = state.id; - let fs = fs.clone(); - let toolchain_store = toolchain_store.clone(); - server_ids.iter().filter_map(|server_id| { - let delegate = delegate.clone()? as Arc; - let states = local.language_servers.get(server_id)?; - - match states { - LanguageServerState::Starting { .. } => None, - LanguageServerState::Running { - adapter, server, .. - } => { - let fs = fs.clone(); - let toolchain_store = toolchain_store.clone(); - let adapter = adapter.clone(); - let server = server.clone(); - refreshed_servers.insert(server.name()); - Some(cx.spawn(async move |_, cx| { - let settings = - LocalLspStore::workspace_configuration_for_adapter( - adapter.adapter.clone(), - fs.as_ref(), - &delegate, - toolchain_store, - cx, - ) - .await - .ok()?; - server - .notify::( - &lsp::DidChangeConfigurationParams { settings }, - ) - .ok()?; - Some(()) - })) - } + let states = local.language_servers.get(&server_id)?; + + match states { + LanguageServerState::Starting { .. } => None, + LanguageServerState::Running { + adapter, server, .. + } => { + let fs = fs.clone(); + + let adapter = adapter.clone(); + let server = server.clone(); + refreshed_servers.insert(server.name()); + let toolchain = seed.toolchain.clone(); + Some(cx.spawn(async move |_, cx| { + let settings = + LocalLspStore::workspace_configuration_for_adapter( + adapter.adapter.clone(), + fs.as_ref(), + &delegate, + toolchain, + cx, + ) + .await + .ok()?; + server + .notify::( + &lsp::DidChangeConfigurationParams { settings }, + ) + .ok()?; + Some(()) + })) } - }).collect::>() + } }) - .collect::>() + .collect::>(); + + Some(servers) }) - .ok()?; + .ok() + .flatten()?; log::info!("Refreshing workspace configurations for servers {refreshed_servers:?}"); // TODO this asynchronous job runs concurrently with extension (de)registration and may take enough time for a certain extension @@ -7497,18 +7562,12 @@ impl LspStore { // This is racy : an extension might have already removed all `local.language_servers` state, but here we `.clone()` and hold onto it anyway. // This now causes errors in the logs, we should find a way to remove such servers from the processing everywhere. let _: Vec> = join_all(servers).await; + Some(()) }) .await; } - fn toolchain_store(&self, cx: &App) -> Arc { - if let Some(toolchain_store) = self.toolchain_store.as_ref() { - toolchain_store.read(cx).as_language_toolchain_store() - } else { - Arc::new(EmptyToolchainStore) - } - } fn maintain_workspace_config( fs: Arc, external_refresh_requests: watch::Receiver<()>, @@ -7523,8 +7582,19 @@ impl LspStore { let mut joint_future = futures::stream::select(settings_changed_rx, external_refresh_requests); + // Multiple things can happen when a workspace environment (selected toolchain + settings) change: + // - We might shut down a language server if it's no longer enabled for a given language (and there are no buffers using it otherwise). + // - We might also shut it down when the workspace configuration of all of the users of a given language server converges onto that of the other. + // - In the same vein, we might also decide to start a new language server if the workspace configuration *diverges* from the other. + // - In the easiest case (where we're not wrangling the lifetime of a language server anyhow), if none of the roots of a single language server diverge in their configuration, + // but it is still different to what we had before, we're gonna send out a workspace configuration update. cx.spawn(async move |this, cx| { while let Some(()) = joint_future.next().await { + this.update(cx, |this, cx| { + this.refresh_server_tree(cx); + }) + .ok(); + Self::refresh_workspace_configurations(&this, fs.clone(), cx).await; } @@ -7642,47 +7712,6 @@ impl LspStore { .collect(); } - fn register_local_language_server( - &mut self, - worktree: Entity, - language_server_name: LanguageServerName, - language_server_id: LanguageServerId, - cx: &mut App, - ) { - let Some(local) = self.as_local_mut() else { - return; - }; - - let worktree_id = worktree.read(cx).id(); - if worktree.read(cx).is_visible() { - let path = ProjectPath { - worktree_id, - path: Arc::from("".as_ref()), - }; - let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); - local.lsp_tree.update(cx, |language_server_tree, cx| { - for node in language_server_tree.get( - path, - AdapterQuery::Adapter(&language_server_name), - delegate, - cx, - ) { - node.server_id_or_init(|disposition| { - assert_eq!(disposition.server_name, &language_server_name); - - language_server_id - }); - } - }); - } - - local - .language_server_ids - .entry((worktree_id, language_server_name)) - .or_default() - .insert(language_server_id); - } - #[cfg(test)] pub fn update_diagnostic_entries( &mut self, @@ -7912,17 +7941,12 @@ impl LspStore { .await }) } else if let Some(local) = self.as_local() { - let Some(language_server_id) = local - .language_server_ids - .get(&( - symbol.source_worktree_id, - symbol.language_server_name.clone(), - )) - .and_then(|ids| { - ids.contains(&symbol.source_language_server_id) - .then_some(symbol.source_language_server_id) - }) - else { + let is_valid = local.language_server_ids.iter().any(|(seed, state)| { + seed.worktree_id == symbol.source_worktree_id + && state.id == symbol.source_language_server_id + && symbol.language_server_name == seed.name + }); + if !is_valid { return Task::ready(Err(anyhow!( "language server for worktree and language not found" ))); @@ -7946,22 +7970,16 @@ impl LspStore { return Task::ready(Err(anyhow!("invalid symbol path"))); }; - self.open_local_buffer_via_lsp( - symbol_uri, - language_server_id, - symbol.language_server_name.clone(), - cx, - ) + self.open_local_buffer_via_lsp(symbol_uri, symbol.source_language_server_id, cx) } else { Task::ready(Err(anyhow!("no upstream client or local store"))) } } - pub fn open_local_buffer_via_lsp( + pub(crate) fn open_local_buffer_via_lsp( &mut self, mut abs_path: lsp::Url, language_server_id: LanguageServerId, - language_server_name: LanguageServerName, cx: &mut Context, ) -> Task>> { cx.spawn(async move |lsp_store, cx| { @@ -8012,12 +8030,13 @@ impl LspStore { if worktree.read_with(cx, |worktree, _| worktree.is_local())? { lsp_store .update(cx, |lsp_store, cx| { - lsp_store.register_local_language_server( - worktree.clone(), - language_server_name, - language_server_id, - cx, - ) + if let Some(local) = lsp_store.as_local_mut() { + local.register_language_server_for_invisible_worktree( + &worktree, + language_server_id, + cx, + ) + } }) .ok(); } @@ -9202,11 +9221,7 @@ impl LspStore { else { continue; }; - let Some(adapter) = - this.language_server_adapter_for_id(language_server.server_id()) - else { - continue; - }; + if filter.should_send_will_rename(&old_uri, is_dir) { let apply_edit = cx.spawn({ let old_uri = old_uri.clone(); @@ -9227,7 +9242,6 @@ impl LspStore { this.upgrade()?, edit, false, - adapter.clone(), language_server.clone(), cx, ) @@ -10290,28 +10304,18 @@ impl LspStore { &mut self, server_id: LanguageServerId, cx: &mut Context, - ) -> Task> { + ) -> Task<()> { let local = match &mut self.mode { LspStoreMode::Local(local) => local, _ => { - return Task::ready(Vec::new()); + return Task::ready(()); } }; - let mut orphaned_worktrees = Vec::new(); // Remove this server ID from all entries in the given worktree. - local.language_server_ids.retain(|(worktree, _), ids| { - if !ids.remove(&server_id) { - return true; - } - - if ids.is_empty() { - orphaned_worktrees.push(*worktree); - false - } else { - true - } - }); + local + .language_server_ids + .retain(|_, state| state.id != server_id); self.buffer_store.update(cx, |buffer_store, cx| { for buffer in buffer_store.buffers() { buffer.update(cx, |buffer, cx| { @@ -10390,14 +10394,13 @@ impl LspStore { cx.notify(); }) .ok(); - orphaned_worktrees }); } if server_state.is_some() { cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); } - Task::ready(orphaned_worktrees) + Task::ready(()) } pub fn stop_all_language_servers(&mut self, cx: &mut Context) { @@ -10416,12 +10419,9 @@ impl LspStore { let language_servers_to_stop = local .language_server_ids .values() - .flatten() - .copied() + .map(|state| state.id) .collect(); - local.lsp_tree.update(cx, |this, _| { - this.remove_nodes(&language_servers_to_stop); - }); + local.lsp_tree.remove_nodes(&language_servers_to_stop); let tasks = language_servers_to_stop .into_iter() .map(|server| self.stop_local_language_server(server, cx)) @@ -10571,34 +10571,28 @@ impl LspStore { if let Some(worktree_id) = buffer.file().map(|f| f.worktree_id(cx)) { if covered_worktrees.insert(worktree_id) { language_server_names_to_stop.retain(|name| { - match local.language_server_ids.get(&(worktree_id, name.clone())) { - Some(server_ids) => { - language_servers_to_stop - .extend(server_ids.into_iter().copied()); - false - } - None => true, - } + let old_ids_count = language_servers_to_stop.len(); + let all_language_servers_with_this_name = local + .language_server_ids + .iter() + .filter_map(|(seed, state)| seed.name.eq(name).then(|| state.id)); + language_servers_to_stop.extend(all_language_servers_with_this_name); + old_ids_count == language_servers_to_stop.len() }); } } }); } for name in language_server_names_to_stop { - if let Some(server_ids) = local - .language_server_ids - .iter() - .filter(|((_, server_name), _)| server_name == &name) - .map(|((_, _), server_ids)| server_ids) - .max_by_key(|server_ids| server_ids.len()) - { - language_servers_to_stop.extend(server_ids.into_iter().copied()); - } + language_servers_to_stop.extend( + local + .language_server_ids + .iter() + .filter_map(|(seed, v)| seed.name.eq(&name).then(|| v.id)), + ); } - local.lsp_tree.update(cx, |this, _| { - this.remove_nodes(&language_servers_to_stop); - }); + local.lsp_tree.remove_nodes(&language_servers_to_stop); let tasks = language_servers_to_stop .into_iter() .map(|server| self.stop_local_language_server(server, cx)) @@ -10821,7 +10815,7 @@ impl LspStore { adapter: Arc, language_server: Arc, server_id: LanguageServerId, - key: (WorktreeId, LanguageServerName), + key: LanguageServerSeed, workspace_folders: Arc>>, cx: &mut Context, ) { @@ -10833,7 +10827,7 @@ impl LspStore { if local .language_server_ids .get(&key) - .map(|ids| !ids.contains(&server_id)) + .map(|state| state.id != server_id) .unwrap_or(false) { return; @@ -10890,7 +10884,7 @@ impl LspStore { cx.emit(LspStoreEvent::LanguageServerAdded( server_id, language_server.name(), - Some(key.0), + Some(key.worktree_id), )); cx.emit(LspStoreEvent::RefreshInlayHints); @@ -10902,7 +10896,7 @@ impl LspStore { server: Some(proto::LanguageServer { id: server_id.to_proto(), name: language_server.name().to_string(), - worktree_id: Some(key.0.to_proto()), + worktree_id: Some(key.worktree_id.to_proto()), }), capabilities: serde_json::to_string(&server_capabilities) .expect("serializing server LSP capabilities"), @@ -10914,13 +10908,13 @@ impl LspStore { // 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 - let mut worktrees_using_server = vec![key.0]; + let mut worktrees_using_server = vec![key.worktree_id]; if let Some(local) = self.as_local() { // Find all worktrees that have this server in their language server tree - for (worktree_id, servers) in &local.lsp_tree.read(cx).instances { - if *worktree_id != key.0 { + for (worktree_id, servers) in &local.lsp_tree.instances { + if *worktree_id != key.worktree_id { for (_, server_map) in &servers.roots { - if server_map.contains_key(&key.1) { + if server_map.contains_key(&key.name) { worktrees_using_server.push(*worktree_id); } } @@ -10946,7 +10940,7 @@ impl LspStore { .languages .lsp_adapters(&language.name()) .iter() - .any(|a| a.name == key.1) + .any(|a| a.name == key.name) { continue; } @@ -11191,11 +11185,7 @@ impl LspStore { let mut language_server_ids = local .language_server_ids .iter() - .flat_map(|((server_worktree, _), server_ids)| { - server_ids - .iter() - .filter_map(|server_id| server_worktree.eq(&worktree_id).then(|| *server_id)) - }) + .filter_map(|(seed, v)| seed.worktree_id.eq(&worktree_id).then(|| v.id)) .collect::>(); language_server_ids.sort(); language_server_ids.dedup(); @@ -11239,6 +11229,14 @@ impl LspStore { } } } + for (path, _, _) in changes { + if let Some(file_name) = path.file_name().and_then(|file_name| file_name.to_str()) + && local.watched_manifest_filenames.contains(file_name) + { + self.request_workspace_config_refresh(); + break; + } + } } pub fn wait_for_remote_buffer( @@ -12785,7 +12783,7 @@ impl LspAdapter for SshLspAdapter { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { Some(self.binary.clone()) diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index 7266acb5b4a29b68d8863feb760334de46260424..8621d24d0631229d9f424e7ef7cc9040a58a6780 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -7,18 +7,12 @@ mod manifest_store; mod path_trie; mod server_tree; -use std::{ - borrow::Borrow, - collections::{BTreeMap, hash_map::Entry}, - ops::ControlFlow, - path::Path, - sync::Arc, -}; +use std::{borrow::Borrow, collections::hash_map::Entry, ops::ControlFlow, path::Path, sync::Arc}; use collections::HashMap; -use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription}; +use gpui::{App, AppContext as _, Context, Entity, Subscription}; use language::{ManifestDelegate, ManifestName, ManifestQuery}; -pub use manifest_store::ManifestProviders; +pub use manifest_store::ManifestProvidersStore; use path_trie::{LabelPresence, RootPathTrie, TriePath}; use settings::{SettingsStore, WorktreeId}; use worktree::{Event as WorktreeEvent, Snapshot, Worktree}; @@ -28,9 +22,7 @@ use crate::{ worktree_store::{WorktreeStore, WorktreeStoreEvent}, }; -pub(crate) use server_tree::{ - AdapterQuery, LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, -}; +pub(crate) use server_tree::{LanguageServerTree, LanguageServerTreeNode, LaunchDisposition}; struct WorktreeRoots { roots: RootPathTrie, @@ -81,14 +73,6 @@ pub struct ManifestTree { _subscriptions: [Subscription; 2], } -#[derive(PartialEq)] -pub(crate) enum ManifestTreeEvent { - WorktreeRemoved(WorktreeId), - Cleared, -} - -impl EventEmitter for ManifestTree {} - impl ManifestTree { pub fn new(worktree_store: Entity, cx: &mut App) -> Entity { cx.new(|cx| Self { @@ -101,30 +85,28 @@ impl ManifestTree { worktree_roots.roots = RootPathTrie::new(); }) } - cx.emit(ManifestTreeEvent::Cleared); }), ], worktree_store, }) } + pub(crate) fn root_for_path( &mut self, - ProjectPath { worktree_id, path }: ProjectPath, - manifests: &mut dyn Iterator, - delegate: Arc, + ProjectPath { worktree_id, path }: &ProjectPath, + manifest_name: &ManifestName, + delegate: &Arc, cx: &mut App, - ) -> BTreeMap { - debug_assert_eq!(delegate.worktree_id(), worktree_id); - let mut roots = BTreeMap::from_iter( - manifests.map(|manifest| (manifest, (None, LabelPresence::KnownAbsent))), - ); - let worktree_roots = match self.root_points.entry(worktree_id) { + ) -> Option { + debug_assert_eq!(delegate.worktree_id(), *worktree_id); + let (mut marked_path, mut current_presence) = (None, LabelPresence::KnownAbsent); + let worktree_roots = match self.root_points.entry(*worktree_id) { Entry::Occupied(occupied_entry) => occupied_entry.get().clone(), Entry::Vacant(vacant_entry) => { let Some(worktree) = self .worktree_store .read(cx) - .worktree_for_id(worktree_id, cx) + .worktree_for_id(*worktree_id, cx) else { return Default::default(); }; @@ -133,16 +115,16 @@ impl ManifestTree { } }; - let key = TriePath::from(&*path); + let key = TriePath::from(&**path); worktree_roots.read_with(cx, |this, _| { this.roots.walk(&key, &mut |path, labels| { for (label, presence) in labels { - if let Some((marked_path, current_presence)) = roots.get_mut(label) { - if *current_presence > *presence { + if label == manifest_name { + if current_presence > *presence { debug_assert!(false, "RootPathTrie precondition violation; while walking the tree label presence is only allowed to increase"); } - *marked_path = Some(ProjectPath {worktree_id, path: path.clone()}); - *current_presence = *presence; + marked_path = Some(ProjectPath {worktree_id: *worktree_id, path: path.clone()}); + current_presence = *presence; } } @@ -150,12 +132,9 @@ impl ManifestTree { }); }); - for (manifest_name, (root_path, presence)) in &mut roots { - if *presence == LabelPresence::Present { - continue; - } - - let depth = root_path + if current_presence == LabelPresence::KnownAbsent { + // Some part of the path is unexplored. + let depth = marked_path .as_ref() .map(|root_path| { path.strip_prefix(&root_path.path) @@ -165,13 +144,10 @@ impl ManifestTree { }) .unwrap_or_else(|| path.components().count() + 1); - if depth > 0 { - let Some(provider) = ManifestProviders::global(cx).get(manifest_name.borrow()) - else { - log::warn!("Manifest provider `{}` not found", manifest_name.as_ref()); - continue; - }; - + if depth > 0 + && let Some(provider) = + ManifestProvidersStore::global(cx).get(manifest_name.borrow()) + { let root = provider.search(ManifestQuery { path: path.clone(), depth, @@ -182,9 +158,9 @@ impl ManifestTree { let root = TriePath::from(&*known_root); this.roots .insert(&root, manifest_name.clone(), LabelPresence::Present); - *presence = LabelPresence::Present; - *root_path = Some(ProjectPath { - worktree_id, + current_presence = LabelPresence::Present; + marked_path = Some(ProjectPath { + worktree_id: *worktree_id, path: known_root, }); }), @@ -195,25 +171,35 @@ impl ManifestTree { } } } + marked_path.filter(|_| current_presence.eq(&LabelPresence::Present)) + } - roots - .into_iter() - .filter_map(|(k, (path, presence))| { - let path = path?; - presence.eq(&LabelPresence::Present).then(|| (k, path)) + pub(crate) fn root_for_path_or_worktree_root( + &mut self, + project_path: &ProjectPath, + manifest_name: Option<&ManifestName>, + delegate: &Arc, + cx: &mut App, + ) -> ProjectPath { + let worktree_id = project_path.worktree_id; + // Backwards-compat: Fill in any adapters for which we did not detect the root as having the project root at the root of a worktree. + manifest_name + .and_then(|manifest_name| self.root_for_path(project_path, manifest_name, delegate, cx)) + .unwrap_or_else(|| ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), }) - .collect() } + fn on_worktree_store_event( &mut self, _: Entity, evt: &WorktreeStoreEvent, - cx: &mut Context, + _: &mut Context, ) { match evt { WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => { self.root_points.remove(&worktree_id); - cx.emit(ManifestTreeEvent::WorktreeRemoved(*worktree_id)); } _ => {} } @@ -223,6 +209,7 @@ impl ManifestTree { pub(crate) struct ManifestQueryDelegate { worktree: Snapshot, } + impl ManifestQueryDelegate { pub fn new(worktree: Snapshot) -> Self { Self { worktree } diff --git a/crates/project/src/manifest_tree/manifest_store.rs b/crates/project/src/manifest_tree/manifest_store.rs index 0462b257985c6ec554519c565f1e935853654e59..cf9f81aee470646d5800ca4a1a4ed7aff4cbd03d 100644 --- a/crates/project/src/manifest_tree/manifest_store.rs +++ b/crates/project/src/manifest_tree/manifest_store.rs @@ -1,4 +1,4 @@ -use collections::HashMap; +use collections::{HashMap, HashSet}; use gpui::{App, Global, SharedString}; use parking_lot::RwLock; use std::{ops::Deref, sync::Arc}; @@ -11,13 +11,13 @@ struct ManifestProvidersState { } #[derive(Clone, Default)] -pub struct ManifestProviders(Arc>); +pub struct ManifestProvidersStore(Arc>); #[derive(Default)] -struct GlobalManifestProvider(ManifestProviders); +struct GlobalManifestProvider(ManifestProvidersStore); impl Deref for GlobalManifestProvider { - type Target = ManifestProviders; + type Target = ManifestProvidersStore; fn deref(&self) -> &Self::Target { &self.0 @@ -26,7 +26,7 @@ impl Deref for GlobalManifestProvider { impl Global for GlobalManifestProvider {} -impl ManifestProviders { +impl ManifestProvidersStore { /// Returns the global [`ManifestStore`]. /// /// Inserts a default [`ManifestStore`] if one does not yet exist. @@ -45,4 +45,7 @@ impl ManifestProviders { pub(super) fn get(&self, name: &SharedString) -> Option> { self.0.read().providers.get(name).cloned() } + pub(crate) fn manifest_file_names(&self) -> HashSet { + self.0.read().providers.keys().cloned().collect() + } } diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index 81cb1c450c4626bfa691c98e88d26536705dfb3d..49c0cff7305da38872c18195287cc2612b516608 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -4,8 +4,7 @@ //! //! ## RPC //! LSP Tree is transparent to RPC peers; when clients ask host to spawn a new language server, the host will perform LSP Tree lookup for provided path; it may decide -//! to reuse existing language server. The client maintains it's own LSP Tree that is a subset of host LSP Tree. Done this way, the client does not need to -//! ask about suitable language server for each path it interacts with; it can resolve most of the queries locally. +//! to reuse existing language server. use std::{ collections::{BTreeMap, BTreeSet}, @@ -14,20 +13,23 @@ use std::{ }; use collections::IndexMap; -use gpui::{App, AppContext as _, Entity, Subscription}; +use gpui::{App, Entity}; use language::{ - CachedLspAdapter, LanguageName, LanguageRegistry, ManifestDelegate, + CachedLspAdapter, LanguageName, LanguageRegistry, ManifestDelegate, ManifestName, Toolchain, language_settings::AllLanguageSettings, }; use lsp::LanguageServerName; use settings::{Settings, SettingsLocation, WorktreeId}; use std::sync::OnceLock; -use crate::{LanguageServerId, ProjectPath, project_settings::LspSettings}; +use crate::{ + LanguageServerId, ProjectPath, project_settings::LspSettings, + toolchain_store::LocalToolchainStore, +}; -use super::{ManifestTree, ManifestTreeEvent}; +use super::ManifestTree; -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub(crate) struct ServersForWorktree { pub(crate) roots: BTreeMap< Arc, @@ -39,7 +41,7 @@ pub struct LanguageServerTree { manifest_tree: Entity, pub(crate) instances: BTreeMap, languages: Arc, - _subscriptions: Subscription, + toolchains: Entity, } /// A node in language server tree represents either: @@ -49,22 +51,15 @@ pub struct LanguageServerTree { pub struct LanguageServerTreeNode(Weak); /// Describes a request to launch a language server. -#[derive(Debug)] -pub(crate) struct LaunchDisposition<'a> { - pub(crate) server_name: &'a LanguageServerName, +#[derive(Clone, Debug)] +pub(crate) struct LaunchDisposition { + pub(crate) server_name: LanguageServerName, + /// Path to the root directory of a subproject. pub(crate) path: ProjectPath, pub(crate) settings: Arc, + pub(crate) toolchain: Option, } -impl<'a> From<&'a InnerTreeNode> for LaunchDisposition<'a> { - fn from(value: &'a InnerTreeNode) -> Self { - LaunchDisposition { - server_name: &value.name, - path: value.path.clone(), - settings: value.settings.clone(), - } - } -} impl LanguageServerTreeNode { /// Returns a language server ID for this node if there is one. /// Returns None if this node has not been initialized yet or it is no longer in the tree. @@ -76,19 +71,17 @@ impl LanguageServerTreeNode { /// May return None if the node no longer belongs to the server tree it was created in. pub(crate) fn server_id_or_init( &self, - init: impl FnOnce(LaunchDisposition) -> LanguageServerId, + init: impl FnOnce(&Arc) -> LanguageServerId, ) -> Option { let this = self.0.upgrade()?; - Some( - *this - .id - .get_or_init(|| init(LaunchDisposition::from(&*this))), - ) + Some(*this.id.get_or_init(|| init(&this.disposition))) } /// Returns a language server name as the language server adapter would return. pub fn name(&self) -> Option { - self.0.upgrade().map(|node| node.name.clone()) + self.0 + .upgrade() + .map(|node| node.disposition.server_name.clone()) } } @@ -101,160 +94,149 @@ impl From> for LanguageServerTreeNode { #[derive(Debug)] pub struct InnerTreeNode { id: OnceLock, - name: LanguageServerName, - path: ProjectPath, - settings: Arc, + disposition: Arc, } impl InnerTreeNode { fn new( - name: LanguageServerName, + server_name: LanguageServerName, path: ProjectPath, - settings: impl Into>, + settings: LspSettings, + toolchain: Option, ) -> Self { InnerTreeNode { id: Default::default(), - name, - path, - settings: settings.into(), + disposition: Arc::new(LaunchDisposition { + server_name, + path, + settings: settings.into(), + toolchain, + }), } } } -/// Determines how the list of adapters to query should be constructed. -pub(crate) enum AdapterQuery<'a> { - /// Search for roots of all adapters associated with a given language name. - /// Layman: Look for all project roots along the queried path that have any - /// language server associated with this language running. - Language(&'a LanguageName), - /// Search for roots of adapter with a given name. - /// Layman: Look for all project roots along the queried path that have this server running. - Adapter(&'a LanguageServerName), -} - impl LanguageServerTree { pub(crate) fn new( manifest_tree: Entity, languages: Arc, - cx: &mut App, - ) -> Entity { - cx.new(|cx| Self { - _subscriptions: cx.subscribe(&manifest_tree, |_: &mut Self, _, event, _| { - if event == &ManifestTreeEvent::Cleared {} - }), + toolchains: Entity, + ) -> Self { + Self { manifest_tree, instances: Default::default(), - languages, - }) + toolchains, + } } - /// Get all language server root points for a given path and language; the language servers might already be initialized at a given path. + /// Get all initialized language server IDs for a given path. pub(crate) fn get<'a>( - &'a mut self, + &'a self, path: ProjectPath, - query: AdapterQuery<'_>, - delegate: Arc, + language_name: LanguageName, + manifest_name: Option<&ManifestName>, + delegate: &Arc, cx: &mut App, - ) -> impl Iterator + 'a { - let settings_location = SettingsLocation { - worktree_id: path.worktree_id, - path: &path.path, - }; - let adapters = match query { - AdapterQuery::Language(language_name) => { - self.adapters_for_language(settings_location, language_name, cx) - } - AdapterQuery::Adapter(language_server_name) => { - IndexMap::from_iter(self.adapter_for_name(language_server_name).map(|adapter| { - ( - adapter.name(), - (LspSettings::default(), BTreeSet::new(), adapter), - ) - })) - } - }; - self.get_with_adapters(path, adapters, delegate, cx) + ) -> impl Iterator + 'a { + let manifest_location = self.manifest_location_for_path(&path, manifest_name, delegate, cx); + let adapters = self.adapters_for_language(&manifest_location, &language_name, cx); + self.get_with_adapters(manifest_location, adapters) } - fn get_with_adapters<'a>( + /// Get all language server root points for a given path and language; the language servers might already be initialized at a given path. + pub(crate) fn walk<'a>( &'a mut self, path: ProjectPath, - adapters: IndexMap< - LanguageServerName, - (LspSettings, BTreeSet, Arc), - >, - delegate: Arc, - cx: &mut App, + language_name: LanguageName, + manifest_name: Option<&ManifestName>, + delegate: &Arc, + cx: &'a mut App, ) -> impl Iterator + 'a { - let worktree_id = path.worktree_id; - - let mut manifest_to_adapters = BTreeMap::default(); - for (_, _, adapter) in adapters.values() { - if let Some(manifest_name) = adapter.manifest_name() { - manifest_to_adapters - .entry(manifest_name) - .or_insert_with(Vec::default) - .push(adapter.clone()); - } - } + let manifest_location = self.manifest_location_for_path(&path, manifest_name, delegate, cx); + let adapters = self.adapters_for_language(&manifest_location, &language_name, cx); + self.init_with_adapters(manifest_location, language_name, adapters, cx) + } - let roots = self.manifest_tree.update(cx, |this, cx| { - this.root_for_path( - path, - &mut manifest_to_adapters.keys().cloned(), - delegate, - cx, - ) - }); - let root_path = std::cell::LazyCell::new(move || ProjectPath { - worktree_id, - path: Arc::from("".as_ref()), - }); - adapters - .into_iter() - .map(move |(_, (settings, new_languages, adapter))| { - // Backwards-compat: Fill in any adapters for which we did not detect the root as having the project root at the root of a worktree. - let root_path = adapter - .manifest_name() - .and_then(|name| roots.get(&name)) - .cloned() - .unwrap_or_else(|| root_path.clone()); - - let inner_node = self - .instances - .entry(root_path.worktree_id) - .or_default() - .roots - .entry(root_path.path.clone()) - .or_default() - .entry(adapter.name()); - let (node, languages) = inner_node.or_insert_with(|| { - ( - Arc::new(InnerTreeNode::new( - adapter.name(), - root_path.clone(), - settings.clone(), - )), - Default::default(), - ) - }); - languages.extend(new_languages.iter().cloned()); - Arc::downgrade(&node).into() - }) + fn init_with_adapters<'a>( + &'a mut self, + root_path: ProjectPath, + language_name: LanguageName, + adapters: IndexMap)>, + cx: &'a App, + ) -> impl Iterator + 'a { + adapters.into_iter().map(move |(_, (settings, adapter))| { + let root_path = root_path.clone(); + let inner_node = self + .instances + .entry(root_path.worktree_id) + .or_default() + .roots + .entry(root_path.path.clone()) + .or_default() + .entry(adapter.name()); + let (node, languages) = inner_node.or_insert_with(|| { + let toolchain = self.toolchains.read(cx).active_toolchain( + root_path.worktree_id, + &root_path.path, + language_name.clone(), + ); + ( + Arc::new(InnerTreeNode::new( + adapter.name(), + root_path.clone(), + settings.clone(), + toolchain, + )), + Default::default(), + ) + }); + languages.insert(language_name.clone()); + Arc::downgrade(&node).into() + }) } - fn adapter_for_name(&self, name: &LanguageServerName) -> Option> { - self.languages.adapter_for_name(name) + fn get_with_adapters<'a>( + &'a self, + root_path: ProjectPath, + adapters: IndexMap)>, + ) -> impl Iterator + 'a { + adapters.into_iter().filter_map(move |(_, (_, adapter))| { + let root_path = root_path.clone(); + let inner_node = self + .instances + .get(&root_path.worktree_id)? + .roots + .get(&root_path.path)? + .get(&adapter.name())?; + inner_node.0.id.get().copied() + }) + } + + fn manifest_location_for_path( + &self, + path: &ProjectPath, + manifest_name: Option<&ManifestName>, + delegate: &Arc, + cx: &mut App, + ) -> ProjectPath { + // Find out what the root location of our subproject is. + // That's where we'll look for language settings (that include a set of language servers). + self.manifest_tree.update(cx, |this, cx| { + this.root_for_path_or_worktree_root(path, manifest_name, delegate, cx) + }) } fn adapters_for_language( &self, - settings_location: SettingsLocation, + manifest_location: &ProjectPath, language_name: &LanguageName, cx: &App, - ) -> IndexMap, Arc)> - { + ) -> IndexMap)> { + let settings_location = SettingsLocation { + worktree_id: manifest_location.worktree_id, + path: &manifest_location.path, + }; let settings = AllLanguageSettings::get(Some(settings_location), cx).language( Some(settings_location), Some(language_name), @@ -295,14 +277,7 @@ impl LanguageServerTree { ) .cloned() .unwrap_or_default(); - Some(( - adapter.name(), - ( - adapter_settings, - BTreeSet::from_iter([language_name.clone()]), - adapter, - ), - )) + Some((adapter.name(), (adapter_settings, adapter))) }) .collect::>(); // After starting all the language servers, reorder them to reflect the desired order @@ -315,17 +290,23 @@ impl LanguageServerTree { &language_name, adapters_with_settings .values() - .map(|(_, _, adapter)| adapter.clone()) + .map(|(_, adapter)| adapter.clone()) .collect(), ); adapters_with_settings } - // Rebasing a tree: - // - Clears it out - // - Provides you with the indirect access to the old tree while you're reinitializing a new one (by querying it). - pub(crate) fn rebase(&mut self) -> ServerTreeRebase<'_> { + /// Server Tree is built up incrementally via queries for distinct paths of the worktree. + /// Results of these queries have to be invalidated when data used to build the tree changes. + /// + /// The environment of a server tree is a set of all user settings. + /// Rebasing a tree means invalidating it and building up a new one while reusing the old tree where applicable. + /// We want to reuse the old tree in order to preserve as many of the running language servers as possible. + /// E.g. if the user disables one of their language servers for Python, we don't want to shut down any language servers unaffected by this settings change. + /// + /// Thus, [`ServerTreeRebase`] mimics the interface of a [`ServerTree`], except that it tries to find a matching language server in the old tree before handing out an uninitialized node. + pub(crate) fn rebase(&mut self) -> ServerTreeRebase { ServerTreeRebase::new(self) } @@ -354,16 +335,16 @@ impl LanguageServerTree { .roots .entry(Arc::from(Path::new(""))) .or_default() - .entry(node.name.clone()) + .entry(node.disposition.server_name.clone()) .or_insert_with(|| (node, BTreeSet::new())) .1 .insert(language_name); } } -pub(crate) struct ServerTreeRebase<'a> { +pub(crate) struct ServerTreeRebase { old_contents: BTreeMap, - new_tree: &'a mut LanguageServerTree, + new_tree: LanguageServerTree, /// All server IDs seen in the old tree. all_server_ids: BTreeMap, /// Server IDs we've preserved for a new iteration of the tree. `all_server_ids - rebased_server_ids` is the @@ -371,9 +352,9 @@ pub(crate) struct ServerTreeRebase<'a> { rebased_server_ids: BTreeSet, } -impl<'tree> ServerTreeRebase<'tree> { - fn new(new_tree: &'tree mut LanguageServerTree) -> Self { - let old_contents = std::mem::take(&mut new_tree.instances); +impl ServerTreeRebase { + fn new(old_tree: &LanguageServerTree) -> Self { + let old_contents = old_tree.instances.clone(); let all_server_ids = old_contents .values() .flat_map(|nodes| { @@ -384,69 +365,68 @@ impl<'tree> ServerTreeRebase<'tree> { .id .get() .copied() - .map(|id| (id, server.0.name.clone())) + .map(|id| (id, server.0.disposition.server_name.clone())) }) }) }) .collect(); + let new_tree = LanguageServerTree::new( + old_tree.manifest_tree.clone(), + old_tree.languages.clone(), + old_tree.toolchains.clone(), + ); Self { old_contents, - new_tree, all_server_ids, + new_tree, rebased_server_ids: BTreeSet::new(), } } - pub(crate) fn get<'a>( + pub(crate) fn walk<'a>( &'a mut self, path: ProjectPath, - query: AdapterQuery<'_>, + language_name: LanguageName, + manifest_name: Option<&ManifestName>, delegate: Arc, - cx: &mut App, + cx: &'a mut App, ) -> impl Iterator + 'a { - let settings_location = SettingsLocation { - worktree_id: path.worktree_id, - path: &path.path, - }; - let adapters = match query { - AdapterQuery::Language(language_name) => { - self.new_tree - .adapters_for_language(settings_location, language_name, cx) - } - AdapterQuery::Adapter(language_server_name) => { - IndexMap::from_iter(self.new_tree.adapter_for_name(language_server_name).map( - |adapter| { - ( - adapter.name(), - (LspSettings::default(), BTreeSet::new(), adapter), - ) - }, - )) - } - }; + let manifest = + self.new_tree + .manifest_location_for_path(&path, manifest_name, &delegate, cx); + let adapters = self + .new_tree + .adapters_for_language(&manifest, &language_name, cx); self.new_tree - .get_with_adapters(path, adapters, delegate, cx) + .init_with_adapters(manifest, language_name, adapters, cx) .filter_map(|node| { // Inspect result of the query and initialize it ourselves before // handing it off to the caller. - let disposition = node.0.upgrade()?; + let live_node = node.0.upgrade()?; - if disposition.id.get().is_some() { + if live_node.id.get().is_some() { return Some(node); } + let disposition = &live_node.disposition; let Some((existing_node, _)) = self .old_contents .get(&disposition.path.worktree_id) .and_then(|worktree_nodes| worktree_nodes.roots.get(&disposition.path.path)) - .and_then(|roots| roots.get(&disposition.name)) - .filter(|(old_node, _)| disposition.settings == old_node.settings) + .and_then(|roots| roots.get(&disposition.server_name)) + .filter(|(old_node, _)| { + (&disposition.toolchain, &disposition.settings) + == ( + &old_node.disposition.toolchain, + &old_node.disposition.settings, + ) + }) else { return Some(node); }; if let Some(existing_id) = existing_node.id.get() { self.rebased_server_ids.insert(*existing_id); - disposition.id.set(*existing_id).ok(); + live_node.id.set(*existing_id).ok(); } Some(node) @@ -454,11 +434,19 @@ impl<'tree> ServerTreeRebase<'tree> { } /// Returns IDs of servers that are no longer referenced (and can be shut down). - pub(crate) fn finish(self) -> BTreeMap { - self.all_server_ids - .into_iter() - .filter(|(id, _)| !self.rebased_server_ids.contains(id)) - .collect() + pub(crate) fn finish( + self, + ) -> ( + LanguageServerTree, + BTreeMap, + ) { + ( + self.new_tree, + self.all_server_ids + .into_iter() + .filter(|(id, _)| !self.rebased_server_ids.contains(id)) + .collect(), + ) } pub(crate) fn server_tree(&mut self) -> &mut LanguageServerTree { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 27ab55d53eceacccf295c3b0b70d68e367992765..57afaceecabac903f20db1d38e09b3984335cdf1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -84,7 +84,7 @@ use lsp::{ }; use lsp_command::*; use lsp_store::{CompletionDocumentation, LspFormatTarget, OpenLspBufferHandle}; -pub use manifest_tree::ManifestProviders; +pub use manifest_tree::ManifestProvidersStore; use node_runtime::NodeRuntime; use parking_lot::Mutex; pub use prettier_store::PrettierStore; @@ -1115,7 +1115,11 @@ impl Project { buffer_store.clone(), worktree_store.clone(), prettier_store.clone(), - toolchain_store.clone(), + toolchain_store + .read(cx) + .as_local_store() + .expect("Toolchain store to be local") + .clone(), environment.clone(), manifest_tree, languages.clone(), @@ -1260,7 +1264,6 @@ impl Project { LspStore::new_remote( buffer_store.clone(), worktree_store.clone(), - Some(toolchain_store.clone()), languages.clone(), ssh_proto.clone(), SSH_PROJECT_ID, @@ -1485,7 +1488,6 @@ impl Project { let mut lsp_store = LspStore::new_remote( buffer_store.clone(), worktree_store.clone(), - None, languages.clone(), client.clone().into(), remote_id, @@ -3596,16 +3598,10 @@ impl Project { &mut self, abs_path: lsp::Url, language_server_id: LanguageServerId, - language_server_name: LanguageServerName, cx: &mut Context, ) -> Task>> { self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.open_local_buffer_via_lsp( - abs_path, - language_server_id, - language_server_name, - cx, - ) + lsp_store.open_local_buffer_via_lsp(abs_path, language_server_id, cx) }) } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 12e3aa88ad63e0dc78e78c32ad4d5b2e922d2ab2..d78526ddd0432919ae17f9e753c207fc17a09b8b 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -22,6 +22,7 @@ use settings::{ SettingsStore, parse_json_with_comments, watch_config_file, }; use std::{ + collections::BTreeMap, path::{Path, PathBuf}, sync::Arc, time::Duration, @@ -518,16 +519,15 @@ impl Default for InlineBlameSettings { } } -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] pub struct BinarySettings { pub path: Option, pub arguments: Option>, - // this can't be an FxHashMap because the extension APIs require the default SipHash - pub env: Option>, + pub env: Option>, pub ignore_system_version: Option, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] #[serde(rename_all = "snake_case")] pub struct LspSettings { pub binary: Option, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index cb3c9efe60584df3b2353641d4b676d85da51476..5b3827b42baa366272fe7b9a5d58f18459ca29c0 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1099,9 +1099,9 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon let prev_read_dir_count = fs.read_dir_call_count(); 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, status.name.clone()) + let server_id = lsp_store.read_with(cx, |lsp_store, _| { + let (id, _) = lsp_store.language_server_statuses().next().unwrap(); + id }); // Simulate jumping to a definition in a dependency outside of the worktree. @@ -1110,7 +1110,6 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon project.open_local_buffer_via_lsp( lsp::Url::from_file_path(path!("/the-registry/dep1/src/dep1.rs")).unwrap(), server_id, - server_name.clone(), cx, ) }) diff --git a/crates/project/src/toolchain_store.rs b/crates/project/src/toolchain_store.rs index 61a005520dd2c77880a066c157995addd3a8fb0f..05531ebe9ae44435a80e371da20bde6a138e13f7 100644 --- a/crates/project/src/toolchain_store.rs +++ b/crates/project/src/toolchain_store.rs @@ -11,7 +11,10 @@ use collections::BTreeMap; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, }; -use language::{LanguageName, LanguageRegistry, LanguageToolchainStore, Toolchain, ToolchainList}; +use language::{ + LanguageName, LanguageRegistry, LanguageToolchainStore, ManifestDelegate, Toolchain, + ToolchainList, +}; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self, FromProto, ToProto}, @@ -104,9 +107,11 @@ impl ToolchainStore { cx: &App, ) -> Task> { match &self.0 { - ToolchainStoreInner::Local(local, _) => { - local.read(cx).active_toolchain(path, language_name, cx) - } + ToolchainStoreInner::Local(local, _) => Task::ready(local.read(cx).active_toolchain( + path.worktree_id, + &path.path, + language_name, + )), ToolchainStoreInner::Remote(remote) => { remote.read(cx).active_toolchain(path, language_name, cx) } @@ -232,9 +237,15 @@ impl ToolchainStore { ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())), } } + pub fn as_local_store(&self) -> Option<&Entity> { + match &self.0 { + ToolchainStoreInner::Local(local, _) => Some(local), + ToolchainStoreInner::Remote(_) => None, + } + } } -struct LocalToolchainStore { +pub struct LocalToolchainStore { languages: Arc, worktree_store: Entity, project_environment: Entity, @@ -243,20 +254,19 @@ struct LocalToolchainStore { } #[async_trait(?Send)] -impl language::LanguageToolchainStore for LocalStore { - async fn active_toolchain( +impl language::LocalLanguageToolchainStore for LocalStore { + fn active_toolchain( self: Arc, worktree_id: WorktreeId, - path: Arc, + path: &Arc, language_name: LanguageName, cx: &mut AsyncApp, ) -> Option { self.0 - .update(cx, |this, cx| { - this.active_toolchain(ProjectPath { worktree_id, path }, language_name, cx) + .update(cx, |this, _| { + this.active_toolchain(worktree_id, path, language_name) }) .ok()? - .await } } @@ -279,19 +289,18 @@ impl language::LanguageToolchainStore for RemoteStore { } pub struct EmptyToolchainStore; -#[async_trait(?Send)] -impl language::LanguageToolchainStore for EmptyToolchainStore { - async fn active_toolchain( +impl language::LocalLanguageToolchainStore for EmptyToolchainStore { + fn active_toolchain( self: Arc, _: WorktreeId, - _: Arc, + _: &Arc, _: LanguageName, _: &mut AsyncApp, ) -> Option { None } } -struct LocalStore(WeakEntity); +pub(crate) struct LocalStore(WeakEntity); struct RemoteStore(WeakEntity); #[derive(Clone)] @@ -349,17 +358,13 @@ impl LocalToolchainStore { .flatten()?; let worktree_id = snapshot.id(); let worktree_root = snapshot.abs_path().to_path_buf(); + let delegate = + Arc::from(ManifestQueryDelegate::new(snapshot)) as Arc; let relative_path = manifest_tree .update(cx, |this, cx| { - this.root_for_path( - path, - &mut std::iter::once(manifest_name.clone()), - Arc::new(ManifestQueryDelegate::new(snapshot)), - cx, - ) + this.root_for_path(&path, &manifest_name, &delegate, cx) }) .ok()? - .remove(&manifest_name) .unwrap_or_else(|| ProjectPath { path: Arc::from(Path::new("")), worktree_id, @@ -394,21 +399,20 @@ impl LocalToolchainStore { } pub(crate) fn active_toolchain( &self, - path: ProjectPath, + worktree_id: WorktreeId, + relative_path: &Arc, language_name: LanguageName, - _: &App, - ) -> Task> { - let ancestors = path.path.ancestors(); - Task::ready( - self.active_toolchains - .get(&(path.worktree_id, language_name)) - .and_then(|paths| { - ancestors - .into_iter() - .find_map(|root_path| paths.get(root_path)) - }) - .cloned(), - ) + ) -> Option { + let ancestors = relative_path.ancestors(); + + self.active_toolchains + .get(&(worktree_id, language_name)) + .and_then(|paths| { + ancestors + .into_iter() + .find_map(|root_path| paths.get(root_path)) + }) + .cloned() } } struct RemoteToolchainStore { diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index b4d31626413ded37decdfbe5d7d091f6c4b4c962..ac1737ba4bc4e9fe1c8ddcc38d0c28c73a424c10 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -171,7 +171,11 @@ impl HeadlessProject { buffer_store.clone(), worktree_store.clone(), prettier_store.clone(), - toolchain_store.clone(), + toolchain_store + .read(cx) + .as_local_store() + .expect("Toolchain store to be local") + .clone(), environment, manifest_tree, languages.clone(), From 2075627d6c31a6661816335afc69e662ef0b60e2 Mon Sep 17 00:00:00 2001 From: Mahmud Ridwan Date: Mon, 18 Aug 2025 15:54:45 +0600 Subject: [PATCH 083/744] Suggest single tracked commit message only when nothing else is staged (#36347) Closes #36341 image In the case where commit message was suggested based on single tracked entry, this PR adds a clause to the condition to ensure there are no staged entries. Release Notes: - Fixed commit message suggestion when there is one unstaged tracked file, but multiple untracked files are staged. --- crates/git_ui/src/git_panel.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 70987dd2128e380c23c64289272e06c24b9b338b..b346f4d2165a8d19d2ab10decd18c1e6024a9cdf 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1833,7 +1833,9 @@ impl GitPanel { let git_status_entry = if let Some(staged_entry) = &self.single_staged_entry { Some(staged_entry) - } else if let Some(single_tracked_entry) = &self.single_tracked_entry { + } else if self.total_staged_count() == 0 + && let Some(single_tracked_entry) = &self.single_tracked_entry + { Some(single_tracked_entry) } else { None From 2eadd5a3962e250fc14820ef60dbe94804959b41 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 18 Aug 2025 11:56:02 +0200 Subject: [PATCH 084/744] agent2: Make `model` of `Thread` optional (#36395) Related to #36394 Release Notes: - N/A --- crates/agent2/src/agent.rs | 42 ++--- crates/agent2/src/tests/mod.rs | 200 ++++++++++++++-------- crates/agent2/src/thread.rs | 45 ++--- crates/agent2/src/tools/edit_file_tool.rs | 32 ++-- crates/agent_ui/src/acp/thread_view.rs | 14 +- 5 files changed, 195 insertions(+), 138 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index d63e3f81345e690b2ab7ea0e5644b62da740fa20..0ad90753e16a81f8dc15a79c3eb0ce3c7573da5f 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -427,9 +427,11 @@ impl NativeAgent { self.models.refresh_list(cx); for session in self.sessions.values_mut() { session.thread.update(cx, |thread, _| { - let model_id = LanguageModels::model_id(&thread.model()); - if let Some(model) = self.models.model_from_id(&model_id) { - thread.set_model(model.clone()); + if let Some(model) = thread.model() { + let model_id = LanguageModels::model_id(model); + if let Some(model) = self.models.model_from_id(&model_id) { + thread.set_model(model.clone()); + } } }); } @@ -622,13 +624,15 @@ impl AgentModelSelector for NativeAgentConnection { else { return Task::ready(Err(anyhow!("Session not found"))); }; - let model = thread.read(cx).model().clone(); + let Some(model) = thread.read(cx).model() else { + return Task::ready(Err(anyhow!("Model not found"))); + }; let Some(provider) = LanguageModelRegistry::read_global(cx).provider(&model.provider_id()) else { return Task::ready(Err(anyhow!("Provider not found"))); }; Task::ready(Ok(LanguageModels::map_language_model_to_info( - &model, &provider, + model, &provider, ))) } @@ -679,19 +683,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection { let available_count = registry.available_models(cx).count(); log::debug!("Total available models: {}", available_count); - let default_model = registry - .default_model() - .and_then(|default_model| { - agent - .models - .model_from_id(&LanguageModels::model_id(&default_model.model)) - }) - .ok_or_else(|| { - log::warn!("No default model configured in settings"); - anyhow!( - "No default model. Please configure a default model in settings." - ) - })?; + let default_model = registry.default_model().and_then(|default_model| { + agent + .models + .model_from_id(&LanguageModels::model_id(&default_model.model)) + }); let thread = cx.new(|cx| { let mut thread = Thread::new( @@ -777,13 +773,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { log::debug!("Message id: {:?}", id); log::debug!("Message content: {:?}", content); - Ok(thread.update(cx, |thread, cx| { - log::info!( - "Sending message to thread with model: {:?}", - thread.model().name() - ); - thread.send(id, content, cx) - })) + thread.update(cx, |thread, cx| thread.send(id, content, cx)) }) } @@ -1008,7 +998,7 @@ mod tests { agent.read_with(cx, |agent, _| { let session = agent.sessions.get(&session_id).unwrap(); session.thread.read_with(cx, |thread, _| { - assert_eq!(thread.model().id().0, "fake"); + assert_eq!(thread.model().unwrap().id().0, "fake"); }); }); diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 48a16bf685575ace9360d4285a63702740a6fc86..e3e3050d49d688804eeec5fa7c3dc2b883246a06 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -40,6 +40,7 @@ async fn test_echo(cx: &mut TestAppContext) { .update(cx, |thread, cx| { thread.send(UserMessageId::new(), ["Testing: Reply with 'Hello'"], cx) }) + .unwrap() .collect() .await; thread.update(cx, |thread, _cx| { @@ -73,6 +74,7 @@ async fn test_thinking(cx: &mut TestAppContext) { cx, ) }) + .unwrap() .collect() .await; thread.update(cx, |thread, _cx| { @@ -101,9 +103,11 @@ async fn test_system_prompt(cx: &mut TestAppContext) { project_context.borrow_mut().shell = "test-shell".into(); thread.update(cx, |thread, _| thread.add_tool(EchoTool)); - thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["abc"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["abc"], cx) + }) + .unwrap(); cx.run_until_parked(); let mut pending_completions = fake_model.pending_completions(); assert_eq!( @@ -136,9 +140,11 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { let fake_model = model.as_fake(); // Send initial user message and verify it's cached - thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Message 1"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Message 1"], cx) + }) + .unwrap(); cx.run_until_parked(); let completion = fake_model.pending_completions().pop().unwrap(); @@ -157,9 +163,11 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { cx.run_until_parked(); // Send another user message and verify only the latest is cached - thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Message 2"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Message 2"], cx) + }) + .unwrap(); cx.run_until_parked(); let completion = fake_model.pending_completions().pop().unwrap(); @@ -191,9 +199,11 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { // Simulate a tool call and verify that the latest tool result is cached thread.update(cx, |thread, _| thread.add_tool(EchoTool)); - thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Use the echo tool"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Use the echo tool"], cx) + }) + .unwrap(); cx.run_until_parked(); let tool_use = LanguageModelToolUse { @@ -273,6 +283,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { cx, ) }) + .unwrap() .collect() .await; assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); @@ -291,6 +302,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { cx, ) }) + .unwrap() .collect() .await; assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); @@ -322,10 +334,12 @@ async fn test_streaming_tool_calls(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; // Test a tool call that's likely to complete *before* streaming stops. - let mut events = thread.update(cx, |thread, cx| { - thread.add_tool(WordListTool); - thread.send(UserMessageId::new(), ["Test the word_list tool."], cx) - }); + let mut events = thread + .update(cx, |thread, cx| { + thread.add_tool(WordListTool); + thread.send(UserMessageId::new(), ["Test the word_list tool."], cx) + }) + .unwrap(); let mut saw_partial_tool_use = false; while let Some(event) = events.next().await { @@ -371,10 +385,12 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let mut events = thread.update(cx, |thread, cx| { - thread.add_tool(ToolRequiringPermission); - thread.send(UserMessageId::new(), ["abc"], cx) - }); + let mut events = thread + .update(cx, |thread, cx| { + thread.add_tool(ToolRequiringPermission); + thread.send(UserMessageId::new(), ["abc"], cx) + }) + .unwrap(); cx.run_until_parked(); fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { @@ -501,9 +517,11 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let mut events = thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["abc"], cx) - }); + let mut events = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["abc"], cx) + }) + .unwrap(); cx.run_until_parked(); fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { @@ -528,10 +546,12 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let events = thread.update(cx, |thread, cx| { - thread.add_tool(EchoTool); - thread.send(UserMessageId::new(), ["abc"], cx) - }); + let events = thread + .update(cx, |thread, cx| { + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["abc"], cx) + }) + .unwrap(); cx.run_until_parked(); let tool_use = LanguageModelToolUse { id: "tool_id_1".into(), @@ -644,10 +664,12 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let events = thread.update(cx, |thread, cx| { - thread.add_tool(EchoTool); - thread.send(UserMessageId::new(), ["abc"], cx) - }); + let events = thread + .update(cx, |thread, cx| { + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["abc"], cx) + }) + .unwrap(); cx.run_until_parked(); let tool_use = LanguageModelToolUse { @@ -677,9 +699,11 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) { .is::() ); - thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), vec!["ghi"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), vec!["ghi"], cx) + }) + .unwrap(); cx.run_until_parked(); let completion = fake_model.pending_completions().pop().unwrap(); assert_eq!( @@ -790,6 +814,7 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { cx, ) }) + .unwrap() .collect() .await; @@ -857,10 +882,12 @@ async fn test_profiles(cx: &mut TestAppContext) { cx.run_until_parked(); // Test that test-1 profile (default) has echo and delay tools - thread.update(cx, |thread, cx| { - thread.set_profile(AgentProfileId("test-1".into())); - thread.send(UserMessageId::new(), ["test"], cx); - }); + thread + .update(cx, |thread, cx| { + thread.set_profile(AgentProfileId("test-1".into())); + thread.send(UserMessageId::new(), ["test"], cx) + }) + .unwrap(); cx.run_until_parked(); let mut pending_completions = fake_model.pending_completions(); @@ -875,10 +902,12 @@ async fn test_profiles(cx: &mut TestAppContext) { fake_model.end_last_completion_stream(); // Switch to test-2 profile, and verify that it has only the infinite tool. - thread.update(cx, |thread, cx| { - thread.set_profile(AgentProfileId("test-2".into())); - thread.send(UserMessageId::new(), ["test2"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.set_profile(AgentProfileId("test-2".into())); + thread.send(UserMessageId::new(), ["test2"], cx) + }) + .unwrap(); cx.run_until_parked(); let mut pending_completions = fake_model.pending_completions(); assert_eq!(pending_completions.len(), 1); @@ -896,15 +925,17 @@ async fn test_profiles(cx: &mut TestAppContext) { async fn test_cancellation(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; - let mut events = thread.update(cx, |thread, cx| { - thread.add_tool(InfiniteTool); - thread.add_tool(EchoTool); - thread.send( - UserMessageId::new(), - ["Call the echo tool, then call the infinite tool, then explain their output"], - cx, - ) - }); + let mut events = thread + .update(cx, |thread, cx| { + thread.add_tool(InfiniteTool); + thread.add_tool(EchoTool); + thread.send( + UserMessageId::new(), + ["Call the echo tool, then call the infinite tool, then explain their output"], + cx, + ) + }) + .unwrap(); // Wait until both tools are called. let mut expected_tools = vec!["Echo", "Infinite Tool"]; @@ -960,6 +991,7 @@ async fn test_cancellation(cx: &mut TestAppContext) { cx, ) }) + .unwrap() .collect::>() .await; thread.update(cx, |thread, _cx| { @@ -978,16 +1010,20 @@ async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let events_1 = thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 1"], cx) - }); + let events_1 = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 1"], cx) + }) + .unwrap(); cx.run_until_parked(); fake_model.send_last_completion_stream_text_chunk("Hey 1!"); cx.run_until_parked(); - let events_2 = thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 2"], cx) - }); + let events_2 = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 2"], cx) + }) + .unwrap(); cx.run_until_parked(); fake_model.send_last_completion_stream_text_chunk("Hey 2!"); fake_model @@ -1005,9 +1041,11 @@ async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let events_1 = thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 1"], cx) - }); + let events_1 = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 1"], cx) + }) + .unwrap(); cx.run_until_parked(); fake_model.send_last_completion_stream_text_chunk("Hey 1!"); fake_model @@ -1015,9 +1053,11 @@ async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) { fake_model.end_last_completion_stream(); let events_1 = events_1.collect::>().await; - let events_2 = thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 2"], cx) - }); + let events_2 = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 2"], cx) + }) + .unwrap(); cx.run_until_parked(); fake_model.send_last_completion_stream_text_chunk("Hey 2!"); fake_model @@ -1034,9 +1074,11 @@ async fn test_refusal(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let events = thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello"], cx) - }); + let events = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello"], cx) + }) + .unwrap(); cx.run_until_parked(); thread.read_with(cx, |thread, _| { assert_eq!( @@ -1082,9 +1124,11 @@ async fn test_truncate(cx: &mut TestAppContext) { let fake_model = model.as_fake(); let message_id = UserMessageId::new(); - thread.update(cx, |thread, cx| { - thread.send(message_id.clone(), ["Hello"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.send(message_id.clone(), ["Hello"], cx) + }) + .unwrap(); cx.run_until_parked(); thread.read_with(cx, |thread, _| { assert_eq!( @@ -1123,9 +1167,11 @@ async fn test_truncate(cx: &mut TestAppContext) { }); // Ensure we can still send a new message after truncation. - thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hi"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hi"], cx) + }) + .unwrap(); thread.update(cx, |thread, _cx| { assert_eq!( thread.to_markdown(), @@ -1291,9 +1337,11 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { thread.update(cx, |thread, _cx| thread.add_tool(ThinkingTool)); let fake_model = model.as_fake(); - let mut events = thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Think"], cx) - }); + let mut events = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Think"], cx) + }) + .unwrap(); cx.run_until_parked(); // Simulate streaming partial input. @@ -1506,7 +1554,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { context_server_registry, action_log, templates, - model.clone(), + Some(model.clone()), cx, ) }); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index d8b6286f607b963d216ac267bfcb4bf06a743117..c4181a1f4272eb5cbfd399afd71954d02b483f14 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -469,7 +469,7 @@ pub struct Thread { profile_id: AgentProfileId, project_context: Rc>, templates: Arc, - model: Arc, + model: Option>, project: Entity, action_log: Entity, } @@ -481,7 +481,7 @@ impl Thread { context_server_registry: Entity, action_log: Entity, templates: Arc, - model: Arc, + model: Option>, cx: &mut Context, ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); @@ -512,12 +512,12 @@ impl Thread { &self.action_log } - pub fn model(&self) -> &Arc { - &self.model + pub fn model(&self) -> Option<&Arc> { + self.model.as_ref() } pub fn set_model(&mut self, model: Arc) { - self.model = model; + self.model = Some(model); } pub fn completion_mode(&self) -> CompletionMode { @@ -575,6 +575,7 @@ impl Thread { &mut self, cx: &mut Context, ) -> Result>> { + anyhow::ensure!(self.model.is_some(), "Model not set"); anyhow::ensure!( self.tool_use_limit_reached, "can only resume after tool use limit is reached" @@ -584,7 +585,7 @@ impl Thread { cx.notify(); log::info!("Total messages in thread: {}", self.messages.len()); - Ok(self.run_turn(cx)) + self.run_turn(cx) } /// Sending a message results in the model streaming a response, which could include tool calls. @@ -595,11 +596,13 @@ impl Thread { id: UserMessageId, content: impl IntoIterator, cx: &mut Context, - ) -> mpsc::UnboundedReceiver> + ) -> Result>> where T: Into, { - log::info!("Thread::send called with model: {:?}", self.model.name()); + let model = self.model().context("No language model configured")?; + + log::info!("Thread::send called with model: {:?}", model.name()); self.advance_prompt_id(); let content = content.into_iter().map(Into::into).collect::>(); @@ -616,10 +619,10 @@ impl Thread { fn run_turn( &mut self, cx: &mut Context, - ) -> mpsc::UnboundedReceiver> { + ) -> Result>> { self.cancel(); - let model = self.model.clone(); + let model = self.model.clone().context("No language model configured")?; let (events_tx, events_rx) = mpsc::unbounded::>(); let event_stream = AgentResponseEventStream(events_tx); let message_ix = self.messages.len().saturating_sub(1); @@ -637,7 +640,7 @@ impl Thread { ); let request = this.update(cx, |this, cx| { this.build_completion_request(completion_intent, cx) - })?; + })??; log::info!("Calling model.stream_completion"); let mut events = model.stream_completion(request, cx).await?; @@ -729,7 +732,7 @@ impl Thread { .ok(); }), }); - events_rx + Ok(events_rx) } pub fn build_system_message(&self) -> LanguageModelRequestMessage { @@ -917,7 +920,7 @@ impl Thread { status: Some(acp::ToolCallStatus::InProgress), ..Default::default() }); - let supports_images = self.model.supports_images(); + let supports_images = self.model().map_or(false, |model| model.supports_images()); let tool_result = tool.run(tool_use.input, tool_event_stream, cx); log::info!("Running tool {}", tool_use.name); Some(cx.foreground_executor().spawn(async move { @@ -1005,7 +1008,9 @@ impl Thread { &self, completion_intent: CompletionIntent, cx: &mut App, - ) -> LanguageModelRequest { + ) -> Result { + let model = self.model().context("No language model configured")?; + log::debug!("Building completion request"); log::debug!("Completion intent: {:?}", completion_intent); log::debug!("Completion mode: {:?}", self.completion_mode); @@ -1021,9 +1026,7 @@ impl Thread { Some(LanguageModelRequestTool { name: tool_name, description: tool.description().to_string(), - input_schema: tool - .input_schema(self.model.tool_input_format()) - .log_err()?, + input_schema: tool.input_schema(model.tool_input_format()).log_err()?, }) }) .collect() @@ -1042,20 +1045,22 @@ impl Thread { tools, tool_choice: None, stop: Vec::new(), - temperature: AgentSettings::temperature_for_model(self.model(), cx), + temperature: AgentSettings::temperature_for_model(&model, cx), thinking_allowed: true, }; log::debug!("Completion request built successfully"); - request + Ok(request) } fn tools<'a>(&'a self, cx: &'a App) -> Result>> { + let model = self.model().context("No language model configured")?; + let profile = AgentSettings::get_global(cx) .profiles .get(&self.profile_id) .context("profile not found")?; - let provider_id = self.model.provider_id(); + let provider_id = model.provider_id(); Ok(self .tools diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 4b4f98daecb90593aa642d41e1becf325aa4c699..c55e503d766c8b6f21e7ada4bdca07022d5e435a 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -237,11 +237,17 @@ impl AgentTool for EditFileTool { }); } - let request = self.thread.update(cx, |thread, cx| { - thread.build_completion_request(CompletionIntent::ToolResults, cx) - }); + let Some(request) = self.thread.update(cx, |thread, cx| { + thread + .build_completion_request(CompletionIntent::ToolResults, cx) + .ok() + }) else { + return Task::ready(Err(anyhow!("Failed to build completion request"))); + }; let thread = self.thread.read(cx); - let model = thread.model().clone(); + let Some(model) = thread.model().cloned() else { + return Task::ready(Err(anyhow!("No language model configured"))); + }; let action_log = thread.action_log().clone(); let authorize = self.authorize(&input, &event_stream, cx); @@ -520,7 +526,7 @@ mod tests { context_server_registry, action_log, Templates::new(), - model, + Some(model), cx, ) }); @@ -717,7 +723,7 @@ mod tests { context_server_registry, action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); @@ -853,7 +859,7 @@ mod tests { context_server_registry, action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); @@ -979,7 +985,7 @@ mod tests { context_server_registry, action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); @@ -1116,7 +1122,7 @@ mod tests { context_server_registry, action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); @@ -1226,7 +1232,7 @@ mod tests { context_server_registry.clone(), action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); @@ -1307,7 +1313,7 @@ mod tests { context_server_registry.clone(), action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); @@ -1391,7 +1397,7 @@ mod tests { context_server_registry.clone(), action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); @@ -1472,7 +1478,7 @@ mod tests { context_server_registry, action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 7c1f3cf4ae51b562cbbe3eb52eac48038221b95c..f011d72d3c5be225d048632d7ae1ac5b2bf801db 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -94,7 +94,9 @@ impl ProfileProvider for Entity { } fn profiles_supported(&self, cx: &App) -> bool { - self.read(cx).model().supports_tools() + self.read(cx) + .model() + .map_or(false, |model| model.supports_tools()) } } @@ -2475,7 +2477,10 @@ impl AcpThreadView { fn render_burn_mode_toggle(&self, cx: &mut Context) -> Option { let thread = self.as_native_thread(cx)?.read(cx); - if !thread.model().supports_burn_mode() { + if thread + .model() + .map_or(true, |model| !model.supports_burn_mode()) + { return None; } @@ -3219,7 +3224,10 @@ impl AcpThreadView { cx: &mut Context, ) -> Option { let thread = self.as_native_thread(cx)?; - let supports_burn_mode = thread.read(cx).model().supports_burn_mode(); + let supports_burn_mode = thread + .read(cx) + .model() + .map_or(false, |model| model.supports_burn_mode()); let focus_handle = self.focus_handle(cx); From 5591fc810e8c5cf31463bac2127cc89008c0599b Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 18 Aug 2025 12:22:00 +0200 Subject: [PATCH 085/744] agent: Restore last used agent session on startup (#36401) Release Notes: - N/A --- crates/agent2/src/agent.rs | 17 ++++--- crates/agent2/src/thread.rs | 5 ++- crates/agent_ui/src/agent_panel.rs | 71 ++++++++++++++++++------------ crates/agent_ui/src/agent_ui.rs | 2 +- 4 files changed, 60 insertions(+), 35 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 0ad90753e16a81f8dc15a79c3eb0ce3c7573da5f..af740d9901e36320cf956a32620d89a624a7ff5a 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -425,13 +425,18 @@ impl NativeAgent { cx: &mut Context, ) { self.models.refresh_list(cx); + + let default_model = LanguageModelRegistry::read_global(cx) + .default_model() + .map(|m| m.model.clone()); + for session in self.sessions.values_mut() { - session.thread.update(cx, |thread, _| { - if let Some(model) = thread.model() { - let model_id = LanguageModels::model_id(model); - if let Some(model) = self.models.model_from_id(&model_id) { - thread.set_model(model.clone()); - } + session.thread.update(cx, |thread, cx| { + if thread.model().is_none() + && let Some(model) = default_model.clone() + { + thread.set_model(model); + cx.notify(); } }); } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index c4181a1f4272eb5cbfd399afd71954d02b483f14..429832010be50a6fc1bcb84c26318c3e31e17766 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -622,7 +622,10 @@ impl Thread { ) -> Result>> { self.cancel(); - let model = self.model.clone().context("No language model configured")?; + let model = self + .model() + .cloned() + .context("No language model configured")?; let (events_tx, events_rx) = mpsc::unbounded::>(); let event_stream = AgentResponseEventStream(events_tx); let message_ix = self.messages.len().saturating_sub(1); diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index b01bf39728f672434ea1d875b6426649edde62a4..391d6aa6e976622172167c1d9f53c33adc732534 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -573,6 +573,7 @@ impl AgentPanel { panel.width = serialized_panel.width.map(|w| w.round()); if let Some(selected_agent) = serialized_panel.selected_agent { panel.selected_agent = selected_agent; + panel.new_agent_thread(selected_agent, window, cx); } cx.notify(); }); @@ -1631,16 +1632,53 @@ impl AgentPanel { menu } - pub fn set_selected_agent(&mut self, agent: AgentType, cx: &mut Context) { + pub fn set_selected_agent( + &mut self, + agent: AgentType, + window: &mut Window, + cx: &mut Context, + ) { if self.selected_agent != agent { self.selected_agent = agent; self.serialize(cx); + self.new_agent_thread(agent, window, cx); } } pub fn selected_agent(&self) -> AgentType { self.selected_agent } + + pub fn new_agent_thread( + &mut self, + agent: AgentType, + window: &mut Window, + cx: &mut Context, + ) { + match agent { + AgentType::Zed => { + window.dispatch_action( + NewThread { + from_thread_id: None, + } + .boxed_clone(), + cx, + ); + } + AgentType::TextThread => { + window.dispatch_action(NewTextThread.boxed_clone(), cx); + } + AgentType::NativeAgent => { + self.new_external_thread(Some(crate::ExternalAgent::NativeAgent), window, cx) + } + AgentType::Gemini => { + self.new_external_thread(Some(crate::ExternalAgent::Gemini), window, cx) + } + AgentType::ClaudeCode => { + self.new_external_thread(Some(crate::ExternalAgent::ClaudeCode), window, cx) + } + } + } } impl Focusable for AgentPanel { @@ -2221,16 +2259,13 @@ impl AgentPanel { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::Zed, + window, cx, ); }); } }); } - window.dispatch_action( - NewThread::default().boxed_clone(), - cx, - ); } }), ) @@ -2250,13 +2285,13 @@ impl AgentPanel { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::TextThread, + window, cx, ); }); } }); } - window.dispatch_action(NewTextThread.boxed_clone(), cx); } }), ) @@ -2275,19 +2310,13 @@ impl AgentPanel { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::NativeAgent, + window, cx, ); }); } }); } - window.dispatch_action( - NewExternalAgentThread { - agent: Some(crate::ExternalAgent::NativeAgent), - } - .boxed_clone(), - cx, - ); } }), ) @@ -2308,19 +2337,13 @@ impl AgentPanel { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::Gemini, + window, cx, ); }); } }); } - window.dispatch_action( - NewExternalAgentThread { - agent: Some(crate::ExternalAgent::Gemini), - } - .boxed_clone(), - cx, - ); } }), ) @@ -2339,19 +2362,13 @@ impl AgentPanel { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::ClaudeCode, + window, cx, ); }); } }); } - window.dispatch_action( - NewExternalAgentThread { - agent: Some(crate::ExternalAgent::ClaudeCode), - } - .boxed_clone(), - cx, - ); } }), ); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index f25b576886d83db32ea48dbffac400a8a096a695..ce1c2203bf2fcd15cc929505f59c5ee08a2ef392 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -146,7 +146,7 @@ pub struct NewExternalAgentThread { agent: Option, } -#[derive(Default, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] enum ExternalAgent { #[default] From 472f1a8cc21a4754c12f9a0e125a3242e3c9937a Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 18 Aug 2025 12:40:39 +0200 Subject: [PATCH 086/744] editor: Add right click context menu to buffer headers (#36398) This adds a context menu to buffer headers mimicking that of pane tabs, notably being able to copy the relative and absolute paths of the buffer as well as opening a terminal in the parent. Confusingly prior to this right clicking a buffer header used to open the context menu of the underlying editor. Release Notes: - Added context menu for buffer titles --- crates/editor/src/element.rs | 416 ++++++++++++++++++++++------------- 1 file changed, 259 insertions(+), 157 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5edfd7df309fb5161ae865abefadda2747589dda..c15ff3e5094008788b85a62d4bb16d871b793d68 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -40,14 +40,15 @@ use git::{ }; use gpui::{ Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, - Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, - Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, - HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length, - ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent, - ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, TextRun, - TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop, - linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black, + Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, + DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, + GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, + Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, + ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, + TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, + linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background, + transparent_black, }; use itertools::Itertools; use language::language_settings::{ @@ -60,7 +61,7 @@ use multi_buffer::{ }; use project::{ - ProjectPath, + Entry, ProjectPath, debugger::breakpoint_store::{Breakpoint, BreakpointSessionState}, project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings}, }; @@ -80,11 +81,17 @@ use std::{ use sum_tree::Bias; use text::{BufferId, SelectionGoal}; use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor}; -use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*}; +use ui::{ + ButtonLike, ContextMenu, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*, + right_click_menu, +}; use unicode_segmentation::UnicodeSegmentation; use util::post_inc; use util::{RangeExt, ResultExt, debug_panic}; -use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt}; +use workspace::{ + CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item, + notifications::NotifyTaskExt, +}; /// Determines what kinds of highlights should be applied to a lines background. #[derive(Clone, Copy, Default)] @@ -3556,7 +3563,7 @@ impl EditorElement { jump_data: JumpData, window: &mut Window, cx: &mut App, - ) -> Div { + ) -> impl IntoElement { let editor = self.editor.read(cx); let file_status = editor .buffer @@ -3577,126 +3584,125 @@ impl EditorElement { .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) .unwrap_or_default(); let can_open_excerpts = Editor::can_open_excerpts_in_file(for_excerpt.buffer.file()); - let path = for_excerpt.buffer.resolve_file_path(cx, include_root); - let filename = path + let relative_path = for_excerpt.buffer.resolve_file_path(cx, include_root); + let filename = relative_path .as_ref() .and_then(|path| Some(path.file_name()?.to_string_lossy().to_string())); - let parent_path = path.as_ref().and_then(|path| { + let parent_path = relative_path.as_ref().and_then(|path| { Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR) }); let focus_handle = editor.focus_handle(cx); let colors = cx.theme().colors(); - div() - .p_1() - .w_full() - .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) - .child( - h_flex() - .size_full() - .gap_2() - .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) - .pl_0p5() - .pr_5() - .rounded_sm() - .when(is_sticky, |el| el.shadow_md()) - .border_1() - .map(|div| { - let border_color = if is_selected - && is_folded - && focus_handle.contains_focused(window, cx) - { - colors.border_focused - } else { - colors.border - }; - div.border_color(border_color) - }) - .bg(colors.editor_subheader_background) - .hover(|style| style.bg(colors.element_hover)) - .map(|header| { - let editor = self.editor.clone(); - let buffer_id = for_excerpt.buffer_id; - let toggle_chevron_icon = - FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); - header.child( - div() - .hover(|style| style.bg(colors.element_selected)) - .rounded_xs() - .child( - ButtonLike::new("toggle-buffer-fold") - .style(ui::ButtonStyle::Transparent) - .height(px(28.).into()) - .width(px(28.)) - .children(toggle_chevron_icon) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::with_meta_in( - "Toggle Excerpt Fold", - Some(&ToggleFold), - "Alt+click to toggle all", - &focus_handle, - window, - cx, - ) - } - }) - .on_click(move |event, window, cx| { - if event.modifiers().alt { - // Alt+click toggles all buffers - editor.update(cx, |editor, cx| { - editor.toggle_fold_all( - &ToggleFoldAll, + let header = + div() + .p_1() + .w_full() + .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) + .child( + h_flex() + .size_full() + .gap_2() + .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) + .pl_0p5() + .pr_5() + .rounded_sm() + .when(is_sticky, |el| el.shadow_md()) + .border_1() + .map(|div| { + let border_color = if is_selected + && is_folded + && focus_handle.contains_focused(window, cx) + { + colors.border_focused + } else { + colors.border + }; + div.border_color(border_color) + }) + .bg(colors.editor_subheader_background) + .hover(|style| style.bg(colors.element_hover)) + .map(|header| { + let editor = self.editor.clone(); + let buffer_id = for_excerpt.buffer_id; + let toggle_chevron_icon = + FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); + header.child( + div() + .hover(|style| style.bg(colors.element_selected)) + .rounded_xs() + .child( + ButtonLike::new("toggle-buffer-fold") + .style(ui::ButtonStyle::Transparent) + .height(px(28.).into()) + .width(px(28.)) + .children(toggle_chevron_icon) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::with_meta_in( + "Toggle Excerpt Fold", + Some(&ToggleFold), + "Alt+click to toggle all", + &focus_handle, window, cx, - ); - }); - } else { - // Regular click toggles single buffer - if is_folded { + ) + } + }) + .on_click(move |event, window, cx| { + if event.modifiers().alt { + // Alt+click toggles all buffers editor.update(cx, |editor, cx| { - editor.unfold_buffer(buffer_id, cx); + editor.toggle_fold_all( + &ToggleFoldAll, + window, + cx, + ); }); } else { - editor.update(cx, |editor, cx| { - editor.fold_buffer(buffer_id, cx); - }); + // Regular click toggles single buffer + if is_folded { + editor.update(cx, |editor, cx| { + editor.unfold_buffer(buffer_id, cx); + }); + } else { + editor.update(cx, |editor, cx| { + editor.fold_buffer(buffer_id, cx); + }); + } } - } - }), - ), + }), + ), + ) + }) + .children( + editor + .addons + .values() + .filter_map(|addon| { + addon.render_buffer_header_controls(for_excerpt, window, cx) + }) + .take(1), ) - }) - .children( - editor - .addons - .values() - .filter_map(|addon| { - addon.render_buffer_header_controls(for_excerpt, window, cx) - }) - .take(1), - ) - .child( - h_flex() - .cursor_pointer() - .id("path header block") - .size_full() - .justify_between() - .overflow_hidden() - .child( - h_flex() - .gap_2() - .child( - Label::new( - filename - .map(SharedString::from) - .unwrap_or_else(|| "untitled".into()), - ) - .single_line() - .when_some( - file_status, - |el, status| { + .child( + h_flex() + .cursor_pointer() + .id("path header block") + .size_full() + .justify_between() + .overflow_hidden() + .child( + h_flex() + .gap_2() + .child( + Label::new( + filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()), + ) + .single_line() + .when_some(file_status, |el, status| { el.color(if status.is_conflicted() { Color::Conflict } else if status.is_modified() { @@ -3707,49 +3713,145 @@ impl EditorElement { Color::Created }) .when(status.is_deleted(), |el| el.strikethrough()) - }, - ), - ) - .when_some(parent_path, |then, path| { - then.child(div().child(path).text_color( - if file_status.is_some_and(FileStatus::is_deleted) { - colors.text_disabled - } else { - colors.text_muted - }, - )) + }), + ) + .when_some(parent_path, |then, path| { + then.child(div().child(path).text_color( + if file_status.is_some_and(FileStatus::is_deleted) { + colors.text_disabled + } else { + colors.text_muted + }, + )) + }), + ) + .when( + can_open_excerpts && is_selected && relative_path.is_some(), + |el| { + el.child( + h_flex() + .id("jump-to-file-button") + .gap_2p5() + .child(Label::new("Jump To File")) + .children( + KeyBinding::for_action_in( + &OpenExcerpts, + &focus_handle, + window, + cx, + ) + .map(|binding| binding.into_any_element()), + ), + ) + }, + ) + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_click(window.listener_for(&self.editor, { + move |editor, e: &ClickEvent, window, cx| { + editor.open_excerpts_common( + Some(jump_data.clone()), + e.modifiers().secondary(), + window, + cx, + ); + } + })), + ), + ); + + let file = for_excerpt.buffer.file().cloned(); + let editor = self.editor.clone(); + right_click_menu("buffer-header-context-menu") + .trigger(move |_, _, _| header) + .menu(move |window, cx| { + let menu_context = focus_handle.clone(); + let editor = editor.clone(); + let file = file.clone(); + ContextMenu::build(window, cx, move |mut menu, window, cx| { + if let Some(file) = file + && let Some(project) = editor.read(cx).project() + && let Some(worktree) = + project.read(cx).worktree_for_id(file.worktree_id(cx), cx) + { + let relative_path = file.path(); + let entry_for_path = worktree.read(cx).entry_for_path(relative_path); + let abs_path = entry_for_path.and_then(|e| e.canonical_path.as_deref()); + let has_relative_path = + worktree.read(cx).root_entry().is_some_and(Entry::is_dir); + + let parent_abs_path = + abs_path.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); + let relative_path = has_relative_path + .then_some(relative_path) + .map(ToOwned::to_owned); + + let visible_in_project_panel = + relative_path.is_some() && worktree.read(cx).is_visible(); + let reveal_in_project_panel = entry_for_path + .filter(|_| visible_in_project_panel) + .map(|entry| entry.id); + menu = menu + .when_some(abs_path.map(ToOwned::to_owned), |menu, abs_path| { + menu.entry( + "Copy Path", + Some(Box::new(zed_actions::workspace::CopyPath)), + window.handler_for(&editor, move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new_string( + abs_path.to_string_lossy().to_string(), + )); }), + ) + }) + .when_some(relative_path, |menu, relative_path| { + menu.entry( + "Copy Relative Path", + Some(Box::new(zed_actions::workspace::CopyRelativePath)), + window.handler_for(&editor, move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new_string( + relative_path.to_string_lossy().to_string(), + )); + }), + ) + }) + .when( + reveal_in_project_panel.is_some() || parent_abs_path.is_some(), + |menu| menu.separator(), ) - .when(can_open_excerpts && is_selected && path.is_some(), |el| { - el.child( - h_flex() - .id("jump-to-file-button") - .gap_2p5() - .child(Label::new("Jump To File")) - .children( - KeyBinding::for_action_in( - &OpenExcerpts, - &focus_handle, - window, - cx, - ) - .map(|binding| binding.into_any_element()), - ), + .when_some(reveal_in_project_panel, |menu, entry_id| { + menu.entry( + "Reveal In Project Panel", + Some(Box::new(RevealInProjectPanel::default())), + window.handler_for(&editor, move |editor, _, cx| { + if let Some(project) = &mut editor.project { + project.update(cx, |_, cx| { + cx.emit(project::Event::RevealInProjectPanel( + entry_id, + )) + }); + } + }), ) }) - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .on_click(window.listener_for(&self.editor, { - move |editor, e: &ClickEvent, window, cx| { - editor.open_excerpts_common( - Some(jump_data.clone()), - e.modifiers().secondary(), - window, - cx, - ); - } - })), - ), - ) + .when_some(parent_abs_path, |menu, parent_abs_path| { + menu.entry( + "Open in Terminal", + Some(Box::new(OpenInTerminal)), + window.handler_for(&editor, move |_, window, cx| { + window.dispatch_action( + OpenTerminal { + working_directory: parent_abs_path.clone(), + } + .boxed_clone(), + cx, + ); + }), + ) + }); + } + + menu.context(menu_context) + }) + }) } fn render_blocks( From d83f341d273394140c6052dcc404fe8b332570e1 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 18 Aug 2025 13:45:51 +0300 Subject: [PATCH 087/744] Silence "minidump endpoint not set" errors' backtraces in the logs (#36404) bad Release Notes: - N/A --- crates/zed/src/reliability.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index c27f4cb0a86ff3f9e6f68f22e99512b801950f90..0a54572f6bf96b5bb0e979b0178bb5b7846bb3d7 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -550,7 +550,8 @@ async fn upload_previous_panics( pub async fn upload_previous_minidumps(http: Arc) -> anyhow::Result<()> { let Some(minidump_endpoint) = MINIDUMP_ENDPOINT.as_ref() else { - return Err(anyhow::anyhow!("Minidump endpoint not set")); + log::warn!("Minidump endpoint not set"); + return Ok(()); }; let mut children = smol::fs::read_dir(paths::logs_dir()).await?; From 843336970ad65fcb12c73f45f8d23823ed1167d5 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 18 Aug 2025 13:01:32 +0200 Subject: [PATCH 088/744] keymap_ui: Ensure keybind with empty arguments can be saved (#36393) Follow up to #36278 to ensure this bug is actually fixed. Also fixes this on two layers and adds a test for the lower layer, as we cannot properly test it in the UI. Furthermore, this improves the error message to show some more context and ensures the status toast is actually only shown when the keybind was successfully updated: Before, we would show the success toast whilst also showing an error in the editor. Lastly, this also fixes some issues with the status toast (and animations) where no status toast or no animation would show in certain scenarios. Release Notes: - N/A --- crates/settings/src/keymap_file.rs | 24 +++++++- crates/settings_ui/src/keybindings.rs | 84 +++++++++++++-------------- crates/ui/src/styles/animation.rs | 27 +++++---- crates/workspace/src/toast_layer.rs | 32 +++++----- 4 files changed, 93 insertions(+), 74 deletions(-) diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 7802671fecdcafe26a22057b8484ddfcbe7556fd..fb036622907487fd0e42b3e58d28d93acf77b340 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -928,14 +928,14 @@ impl<'a> KeybindUpdateTarget<'a> { } let action_name: Value = self.action_name.into(); let value = match self.action_arguments { - Some(args) => { + Some(args) if !args.is_empty() => { let args = serde_json::from_str::(args) .context("Failed to parse action arguments as JSON")?; serde_json::json!([action_name, args]) } - None => action_name, + _ => action_name, }; - return Ok(value); + Ok(value) } fn keystrokes_unparsed(&self) -> String { @@ -1084,6 +1084,24 @@ mod tests { .unindent(), ); + check_keymap_update( + "[]", + KeybindUpdateOperation::add(KeybindUpdateTarget { + keystrokes: &parse_keystrokes("ctrl-a"), + action_name: "zed::SomeAction", + context: None, + action_arguments: Some(""), + }), + r#"[ + { + "bindings": { + "ctrl-a": "zed::SomeAction" + } + } + ]"# + .unindent(), + ); + check_keymap_update( r#"[ { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index b4e871c617461ce7d760c4d9374b6ad3dacb2f23..5181d86a789680483b642087e7e0df1ff8a5f562 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -2177,11 +2177,11 @@ impl KeybindingEditorModal { let action_arguments = self .action_arguments_editor .as_ref() - .map(|editor| editor.read(cx).editor.read(cx).text(cx)); + .map(|arguments_editor| arguments_editor.read(cx).editor.read(cx).text(cx)) + .filter(|args| !args.is_empty()); let value = action_arguments .as_ref() - .filter(|args| !args.is_empty()) .map(|args| { serde_json::from_str(args).context("Failed to parse action arguments as JSON") }) @@ -2289,29 +2289,11 @@ impl KeybindingEditorModal { let create = self.creating; - let status_toast = StatusToast::new( - format!( - "Saved edits to the {} action.", - &self.editing_keybind.action().humanized_name - ), - cx, - move |this, _cx| { - this.icon(ToastIcon::new(IconName::Check).color(Color::Success)) - .dismiss_button(true) - // .action("Undo", f) todo: wire the undo functionality - }, - ); - - self.workspace - .update(cx, |workspace, cx| { - workspace.toggle_status_toast(status_toast, cx); - }) - .log_err(); - cx.spawn(async move |this, cx| { let action_name = existing_keybind.action().name; + let humanized_action_name = existing_keybind.action().humanized_name.clone(); - if let Err(err) = save_keybinding_update( + match save_keybinding_update( create, existing_keybind, &action_mapping, @@ -2321,25 +2303,43 @@ impl KeybindingEditorModal { ) .await { - this.update(cx, |this, cx| { - this.set_error(InputError::error(err), cx); - }) - .log_err(); - } else { - this.update(cx, |this, cx| { - this.keymap_editor.update(cx, |keymap, cx| { - keymap.previous_edit = Some(PreviousEdit::Keybinding { - action_mapping, - action_name, - fallback: keymap - .table_interaction_state - .read(cx) - .get_scrollbar_offset(Axis::Vertical), - }) - }); - cx.emit(DismissEvent); - }) - .ok(); + Ok(_) => { + this.update(cx, |this, cx| { + this.keymap_editor.update(cx, |keymap, cx| { + keymap.previous_edit = Some(PreviousEdit::Keybinding { + action_mapping, + action_name, + fallback: keymap + .table_interaction_state + .read(cx) + .get_scrollbar_offset(Axis::Vertical), + }); + let status_toast = StatusToast::new( + format!("Saved edits to the {} action.", humanized_action_name), + cx, + move |this, _cx| { + this.icon(ToastIcon::new(IconName::Check).color(Color::Success)) + .dismiss_button(true) + // .action("Undo", f) todo: wire the undo functionality + }, + ); + + this.workspace + .update(cx, |workspace, cx| { + workspace.toggle_status_toast(status_toast, cx); + }) + .log_err(); + }); + cx.emit(DismissEvent); + }) + .ok(); + } + Err(err) => { + this.update(cx, |this, cx| { + this.set_error(InputError::error(err), cx); + }) + .log_err(); + } } }) .detach(); @@ -3011,7 +3011,7 @@ async fn save_keybinding_update( let updated_keymap_contents = settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) - .context("Failed to update keybinding")?; + .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; fs.write( paths::keymap_file().as_path(), updated_keymap_contents.as_bytes(), diff --git a/crates/ui/src/styles/animation.rs b/crates/ui/src/styles/animation.rs index ee5352d45403183555fe8d6c72806a5b90f88ca8..acea834548675b3a896a03da731a7eb4fae777e4 100644 --- a/crates/ui/src/styles/animation.rs +++ b/crates/ui/src/styles/animation.rs @@ -31,7 +31,7 @@ pub enum AnimationDirection { FromTop, } -pub trait DefaultAnimations: Styled + Sized { +pub trait DefaultAnimations: Styled + Sized + Element { fn animate_in( self, animation_type: AnimationDirection, @@ -44,8 +44,13 @@ pub trait DefaultAnimations: Styled + Sized { AnimationDirection::FromTop => "animate_from_top", }; + let animation_id = self.id().map_or_else( + || ElementId::from(animation_name), + |id| (id, animation_name).into(), + ); + self.with_animation( - animation_name, + animation_id, gpui::Animation::new(AnimationDuration::Fast.into()).with_easing(ease_out_quint()), move |mut this, delta| { let start_opacity = 0.4; @@ -91,7 +96,7 @@ pub trait DefaultAnimations: Styled + Sized { } } -impl DefaultAnimations for E {} +impl DefaultAnimations for E {} // Don't use this directly, it only exists to show animation previews #[derive(RegisterComponent)] @@ -132,7 +137,7 @@ impl Component for Animation { .left(px(offset)) .rounded_md() .bg(gpui::red()) - .animate_in(AnimationDirection::FromBottom, false), + .animate_in_from_bottom(false), ) .into_any_element(), ), @@ -151,7 +156,7 @@ impl Component for Animation { .left(px(offset)) .rounded_md() .bg(gpui::blue()) - .animate_in(AnimationDirection::FromTop, false), + .animate_in_from_top(false), ) .into_any_element(), ), @@ -170,7 +175,7 @@ impl Component for Animation { .top(px(offset)) .rounded_md() .bg(gpui::green()) - .animate_in(AnimationDirection::FromLeft, false), + .animate_in_from_left(false), ) .into_any_element(), ), @@ -189,7 +194,7 @@ impl Component for Animation { .top(px(offset)) .rounded_md() .bg(gpui::yellow()) - .animate_in(AnimationDirection::FromRight, false), + .animate_in_from_right(false), ) .into_any_element(), ), @@ -214,7 +219,7 @@ impl Component for Animation { .left(px(offset)) .rounded_md() .bg(gpui::red()) - .animate_in(AnimationDirection::FromBottom, true), + .animate_in_from_bottom(true), ) .into_any_element(), ), @@ -233,7 +238,7 @@ impl Component for Animation { .left(px(offset)) .rounded_md() .bg(gpui::blue()) - .animate_in(AnimationDirection::FromTop, true), + .animate_in_from_top(true), ) .into_any_element(), ), @@ -252,7 +257,7 @@ impl Component for Animation { .top(px(offset)) .rounded_md() .bg(gpui::green()) - .animate_in(AnimationDirection::FromLeft, true), + .animate_in_from_left(true), ) .into_any_element(), ), @@ -271,7 +276,7 @@ impl Component for Animation { .top(px(offset)) .rounded_md() .bg(gpui::yellow()) - .animate_in(AnimationDirection::FromRight, true), + .animate_in_from_right(true), ) .into_any_element(), ), diff --git a/crates/workspace/src/toast_layer.rs b/crates/workspace/src/toast_layer.rs index 28be3e7e47a7d617725ce4a67936bd481baf53db..515794554831dc62bdf8babf717ce1f372f37763 100644 --- a/crates/workspace/src/toast_layer.rs +++ b/crates/workspace/src/toast_layer.rs @@ -3,7 +3,7 @@ use std::{ time::{Duration, Instant}, }; -use gpui::{AnyView, DismissEvent, Entity, FocusHandle, ManagedView, Subscription, Task}; +use gpui::{AnyView, DismissEvent, Entity, EntityId, FocusHandle, ManagedView, Subscription, Task}; use ui::{animation::DefaultAnimations, prelude::*}; use zed_actions::toast; @@ -76,6 +76,7 @@ impl ToastViewHandle for Entity { } pub struct ActiveToast { + id: EntityId, toast: Box, action: Option, _subscriptions: [Subscription; 1], @@ -113,9 +114,9 @@ impl ToastLayer { V: ToastView, { if let Some(active_toast) = &self.active_toast { - let is_close = active_toast.toast.view().downcast::().is_ok(); - let did_close = self.hide_toast(cx); - if is_close || !did_close { + let show_new = active_toast.id != new_toast.entity_id(); + self.hide_toast(cx); + if !show_new { return; } } @@ -130,11 +131,12 @@ impl ToastLayer { let focus_handle = cx.focus_handle(); self.active_toast = Some(ActiveToast { - toast: Box::new(new_toast.clone()), - action, _subscriptions: [cx.subscribe(&new_toast, |this, _, _: &DismissEvent, cx| { this.hide_toast(cx); })], + id: new_toast.entity_id(), + toast: Box::new(new_toast), + action, focus_handle, }); @@ -143,11 +145,9 @@ impl ToastLayer { cx.notify(); } - pub fn hide_toast(&mut self, cx: &mut Context) -> bool { + pub fn hide_toast(&mut self, cx: &mut Context) { self.active_toast.take(); cx.notify(); - - true } pub fn active_toast(&self) -> Option> @@ -218,11 +218,10 @@ impl Render for ToastLayer { let Some(active_toast) = &self.active_toast else { return div(); }; - let handle = cx.weak_entity(); div().absolute().size_full().bottom_0().left_0().child( v_flex() - .id("toast-layer-container") + .id(("toast-layer-container", active_toast.id)) .absolute() .w_full() .bottom(px(0.)) @@ -234,17 +233,14 @@ impl Render for ToastLayer { h_flex() .id("active-toast-container") .occlude() - .on_hover(move |hover_start, _window, cx| { - let Some(this) = handle.upgrade() else { - return; - }; + .on_hover(cx.listener(|this, hover_start, _window, cx| { if *hover_start { - this.update(cx, |this, _| this.pause_dismiss_timer()); + this.pause_dismiss_timer(); } else { - this.update(cx, |this, cx| this.restart_dismiss_timer(cx)); + this.restart_dismiss_timer(cx); } cx.stop_propagation(); - }) + })) .on_click(|_, _, cx| { cx.stop_propagation(); }) From d5711d44a5cda4bd9f76849ca3e4904a1aed7c75 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 18 Aug 2025 16:32:01 +0530 Subject: [PATCH 089/744] editor: Fix panic in inlay hint while padding (#36405) Closes #36247 Fix a panic when padding inlay hints if the last character is a multi-byte character. Regressed in https://github.com/zed-industries/zed/pull/35786. Release Notes: - Fixed a crash that could occur when an inlay hint ended with `...`. --- crates/editor/src/display_map/inlay_map.rs | 25 +++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index b296b3e62a39aa2ec8671676e051e94f5f9622cf..76148af587d8f6ac6e5488a5fe9c6fde7a7043a8 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -48,7 +48,7 @@ pub struct Inlay { impl Inlay { pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self { let mut text = hint.text(); - if hint.padding_right && text.chars_at(text.len().saturating_sub(1)).next() != Some(' ') { + if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') { text.push(" "); } if hint.padding_left && text.chars_at(0).next() != Some(' ') { @@ -1305,6 +1305,29 @@ mod tests { ); } + #[gpui::test] + fn test_inlay_hint_padding_with_multibyte_chars() { + assert_eq!( + Inlay::hint( + 0, + Anchor::min(), + &InlayHint { + label: InlayHintLabel::String("🎨".to_string()), + position: text::Anchor::default(), + padding_left: true, + padding_right: true, + tooltip: None, + kind: None, + resolve_state: ResolveState::Resolved, + }, + ) + .text + .to_string(), + " 🎨 ", + "Should pad single emoji correctly" + ); + } + #[gpui::test] fn test_basic_inlays(cx: &mut App) { let buffer = MultiBuffer::build_simple("abcdefghi", cx); From 57198f33c46f79a8520049ad9de69498e449d533 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 18 Aug 2025 13:12:17 +0200 Subject: [PATCH 090/744] agent2: Show Zed AI onboarding (#36406) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 7 +++++-- crates/agent_ui/src/agent_panel.rs | 11 +++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f011d72d3c5be225d048632d7ae1ac5b2bf801db..271d9e5d4c78d5dd85be5b872dd7a15807383a20 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2444,12 +2444,15 @@ impl AcpThreadView { .into_any() } - fn as_native_connection(&self, cx: &App) -> Option> { + pub(crate) fn as_native_connection( + &self, + cx: &App, + ) -> Option> { let acp_thread = self.thread()?.read(cx); acp_thread.connection().clone().downcast() } - fn as_native_thread(&self, cx: &App) -> Option> { + pub(crate) fn as_native_thread(&self, cx: &App) -> Option> { let acp_thread = self.thread()?.read(cx); self.as_native_connection(cx)? .thread(acp_thread.session_id(), cx) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 391d6aa6e976622172167c1d9f53c33adc732534..4cb231f357f4be98a379a9689b0567ffbdc41ccf 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2619,7 +2619,13 @@ impl AgentPanel { } match &self.active_view { - ActiveView::Thread { .. } | ActiveView::TextThread { .. } => { + ActiveView::History | ActiveView::Configuration => false, + ActiveView::ExternalAgentThread { thread_view, .. } + if thread_view.read(cx).as_native_thread(cx).is_none() => + { + false + } + _ => { let history_is_empty = self .history_store .update(cx, |store, cx| store.recent_entries(1, cx).is_empty()); @@ -2634,9 +2640,6 @@ impl AgentPanel { history_is_empty || !has_configured_non_zed_providers } - ActiveView::ExternalAgentThread { .. } - | ActiveView::History - | ActiveView::Configuration => false, } } From 5225844c9edc5a43c426b04cb05dc59289ba085b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 18 Aug 2025 13:48:21 +0200 Subject: [PATCH 091/744] lsp: Always report innermost workspace_folders (#36407) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/language/src/language.rs | 16 ---------------- crates/languages/src/python.rs | 13 +------------ crates/project/src/lsp_store.rs | 7 ++----- 3 files changed, 3 insertions(+), 33 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index f299dee345a61d858fb411b5916766eba47dc72e..6fa31da860e1e53164c03185f47c1af95952a011 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -283,15 +283,6 @@ impl CachedLspAdapter { } } -/// Determines what gets sent out as a workspace folders content -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum WorkspaceFoldersContent { - /// Send out a single entry with the root of the workspace. - WorktreeRoot, - /// Send out a list of subproject roots. - SubprojectRoots, -} - /// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application // e.g. to display a notification or fetch data from the web. #[async_trait] @@ -580,13 +571,6 @@ pub trait LspAdapter: 'static + Send + Sync { Ok(original) } - /// Determines whether a language server supports workspace folders. - /// - /// And does not trip over itself in the process. - fn workspace_folders_content(&self) -> WorkspaceFoldersContent { - WorkspaceFoldersContent::SubprojectRoots - } - /// Method only implemented by the default JSON language server adapter. /// Used to provide dynamic reloading of the JSON schemas used to /// provide autocompletion and diagnostics in Zed setting and keybind diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index b61ad2d36c8ef44a3bb2cd144f49bf6c968e84a8..222e3f1946968faf654c0ea9b33c4b8da43d5c10 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -4,13 +4,13 @@ use async_trait::async_trait; use collections::HashMap; use gpui::{App, Task}; use gpui::{AsyncApp, SharedString}; +use language::Toolchain; use language::ToolchainList; use language::ToolchainLister; use language::language_settings::language_settings; use language::{ContextLocation, LanguageToolchainStore}; use language::{ContextProvider, LspAdapter, LspAdapterDelegate}; use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery}; -use language::{Toolchain, WorkspaceFoldersContent}; use lsp::LanguageServerBinary; use lsp::LanguageServerName; use node_runtime::{NodeRuntime, VersionStrategy}; @@ -389,10 +389,6 @@ impl LspAdapter for PythonLspAdapter { user_settings }) } - - fn workspace_folders_content(&self) -> WorkspaceFoldersContent { - WorkspaceFoldersContent::WorktreeRoot - } } async fn get_cached_server_binary( @@ -1257,9 +1253,6 @@ impl LspAdapter for PyLspAdapter { user_settings }) } - fn workspace_folders_content(&self) -> WorkspaceFoldersContent { - WorkspaceFoldersContent::WorktreeRoot - } } pub(crate) struct BasedPyrightLspAdapter { @@ -1577,10 +1570,6 @@ impl LspAdapter for BasedPyrightLspAdapter { user_settings }) } - - fn workspace_folders_content(&self) -> WorkspaceFoldersContent { - WorkspaceFoldersContent::WorktreeRoot - } } #[cfg(test)] diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 8ea41a100b58d0b6d890b1ee20abe2f3b1f3e459..802b304e94e7d47616b438f10b1138e39a05b6c7 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -57,7 +57,7 @@ use language::{ DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName, LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, ManifestDelegate, ManifestName, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Toolchain, Transaction, - Unclipped, WorkspaceFoldersContent, + Unclipped, language_settings::{ FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings, }, @@ -344,10 +344,7 @@ impl LocalLspStore { binary, &root_path, code_action_kinds, - Some(pending_workspace_folders).filter(|_| { - adapter.adapter.workspace_folders_content() - == WorkspaceFoldersContent::SubprojectRoots - }), + Some(pending_workspace_folders), cx, ) } From 1add1d042dc59d82ed9089bd792e5192e71b5e0f Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 18 Aug 2025 14:21:33 +0200 Subject: [PATCH 092/744] Add option to disable auto indentation (#36259) Closes https://github.com/zed-industries/zed/issues/11780 While auto indentation is generally nice to have, there are cases where it is currently just not good enough for some languages (e.g. Haskell) or users just straight up do not want their editor to auto indent for them. Hence, this PR adds the possibilty to disable auto indentation for either all language or on a per-language basis. Manual invocation via the `editor: auto indent` action will continue to work. Also takes a similar approach as https://github.com/zed-industries/zed/pull/31569 to ensure performance is fine for larger multicursor edits. Release Notes: - Added the possibility to configure auto indentation for all languages and per language. Add `"auto_indent": false"` to your settings or desired language to disable the feature. --- assets/settings/default.json | 2 + crates/editor/src/editor_tests.rs | 210 +++++++++++++++++++++++ crates/language/src/buffer.rs | 45 +++-- crates/language/src/language_settings.rs | 7 + 4 files changed, 250 insertions(+), 14 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 6a8b034268d39c14d3f57273f1cb80a025e3cf5e..72e4dcbf4f79230d3d906d4b41944ce95a40656d 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -286,6 +286,8 @@ // bracket, brace, single or double quote characters. // For example, when you select text and type (, Zed will surround the text with (). "use_auto_surround": true, + /// Whether indentation should be adjusted based on the context whilst typing. + "auto_indent": true, // Whether indentation of pasted content should be adjusted based on the context. "auto_indent_on_paste": true, // Controls how the editor handles the autoclosed characters. diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ef2bdc5da390e662332a4f0444b4149f3b1debfd..f97dcd712c99959ae4aee22ff0b43fbf59669fd8 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -8214,6 +8214,216 @@ async fn test_autoindent(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_autoindent_disabled(cx: &mut TestAppContext) { + init_test(cx, |settings| settings.defaults.auto_indent = Some(false)); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + surround: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: false, + surround: false, + newline: true, + }, + ], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_indents_query( + r#" + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap(), + ); + + let text = "fn a() {}"; + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([5..5, 8..8, 9..9]) + }); + editor.newline(&Newline, window, cx); + assert_eq!( + editor.text(cx), + indoc!( + " + fn a( + + ) { + + } + " + ) + ); + assert_eq!( + editor.selections.ranges(cx), + &[ + Point::new(1, 0)..Point::new(1, 0), + Point::new(3, 0)..Point::new(3, 0), + Point::new(5, 0)..Point::new(5, 0) + ] + ); + }); +} + +#[gpui::test] +async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.defaults.auto_indent = Some(true); + settings.languages.0.insert( + "python".into(), + LanguageSettingsContent { + auto_indent: Some(false), + ..Default::default() + }, + ); + }); + + let mut cx = EditorTestContext::new(cx).await; + + let injected_language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + surround: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + surround: false, + newline: true, + }, + ], + ..Default::default() + }, + name: "python".into(), + ..Default::default() + }, + Some(tree_sitter_python::LANGUAGE.into()), + ) + .with_indents_query( + r#" + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap(), + ); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + surround: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + surround: false, + newline: true, + }, + ], + ..Default::default() + }, + name: LanguageName::new("rust"), + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_indents_query( + r#" + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap() + .with_injection_query( + r#" + (macro_invocation + macro: (identifier) @_macro_name + (token_tree) @injection.content + (#set! injection.language "python")) + "#, + ) + .unwrap(), + ); + + cx.language_registry().add(injected_language); + cx.language_registry().add(language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language(Some(language), cx); + }); + + cx.set_state(&r#"struct A {ˇ}"#); + + cx.update_editor(|editor, window, cx| { + editor.newline(&Default::default(), window, cx); + }); + + cx.assert_editor_state(indoc!( + "struct A { + ˇ + }" + )); + + cx.set_state(&r#"select_biased!(ˇ)"#); + + cx.update_editor(|editor, window, cx| { + editor.newline(&Default::default(), window, cx); + editor.handle_input("def ", window, cx); + editor.handle_input("(", window, cx); + editor.newline(&Default::default(), window, cx); + editor.handle_input("a", window, cx); + }); + + cx.assert_editor_state(indoc!( + "select_biased!( + def ( + aˇ + ) + )" + )); +} + #[gpui::test] async fn test_autoindent_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 2080513f49e593e0561ff4e28ec2f2ef649cb4f2..e2bcc938faf6be215d4b6298edf83482e4d5d838 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2271,13 +2271,11 @@ impl Buffer { } let new_text = new_text.into(); if !new_text.is_empty() || !range.is_empty() { - if let Some((prev_range, prev_text)) = edits.last_mut() { - if prev_range.end >= range.start { - prev_range.end = cmp::max(prev_range.end, range.end); - *prev_text = format!("{prev_text}{new_text}").into(); - } else { - edits.push((range, new_text)); - } + if let Some((prev_range, prev_text)) = edits.last_mut() + && prev_range.end >= range.start + { + prev_range.end = cmp::max(prev_range.end, range.end); + *prev_text = format!("{prev_text}{new_text}").into(); } else { edits.push((range, new_text)); } @@ -2297,10 +2295,27 @@ impl Buffer { if let Some((before_edit, mode)) = autoindent_request { let mut delta = 0isize; - let entries = edits + let mut previous_setting = None; + let entries: Vec<_> = edits .into_iter() .enumerate() .zip(&edit_operation.as_edit().unwrap().new_text) + .filter(|((_, (range, _)), _)| { + let language = before_edit.language_at(range.start); + let language_id = language.map(|l| l.id()); + if let Some((cached_language_id, auto_indent)) = previous_setting + && cached_language_id == language_id + { + auto_indent + } else { + // The auto-indent setting is not present in editorconfigs, hence + // we can avoid passing the file here. + let auto_indent = + language_settings(language.map(|l| l.name()), None, cx).auto_indent; + previous_setting = Some((language_id, auto_indent)); + auto_indent + } + }) .map(|((ix, (range, _)), new_text)| { let new_text_length = new_text.len(); let old_start = range.start.to_point(&before_edit); @@ -2374,12 +2389,14 @@ impl Buffer { }) .collect(); - self.autoindent_requests.push(Arc::new(AutoindentRequest { - before_edit, - entries, - is_block_mode: matches!(mode, AutoindentMode::Block { .. }), - ignore_empty_lines: false, - })); + if !entries.is_empty() { + self.autoindent_requests.push(Arc::new(AutoindentRequest { + before_edit, + entries, + is_block_mode: matches!(mode, AutoindentMode::Block { .. }), + ignore_empty_lines: false, + })); + } } self.end_transaction(cx); diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 1aae0b2f7e23cc87cdd2f13e55805b566a20b5bb..29669ba2a04c26a78cd69d36679df3e0e109dffa 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -133,6 +133,8 @@ pub struct LanguageSettings { /// Whether to use additional LSP queries to format (and amend) the code after /// every "trigger" symbol input, defined by LSP server capabilities. pub use_on_type_format: bool, + /// Whether indentation should be adjusted based on the context whilst typing. + pub auto_indent: bool, /// Whether indentation of pasted content should be adjusted based on the context. pub auto_indent_on_paste: bool, /// Controls how the editor handles the autoclosed characters. @@ -561,6 +563,10 @@ pub struct LanguageSettingsContent { /// /// Default: true pub linked_edits: Option, + /// Whether indentation should be adjusted based on the context whilst typing. + /// + /// Default: true + pub auto_indent: Option, /// Whether indentation of pasted content should be adjusted based on the context. /// /// Default: true @@ -1517,6 +1523,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent merge(&mut settings.use_autoclose, src.use_autoclose); merge(&mut settings.use_auto_surround, src.use_auto_surround); merge(&mut settings.use_on_type_format, src.use_on_type_format); + merge(&mut settings.auto_indent, src.auto_indent); merge(&mut settings.auto_indent_on_paste, src.auto_indent_on_paste); merge( &mut settings.always_treat_brackets_as_autoclosed, From 58f7006898d2f67f038f6305f08a9fb990f7a771 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 18 Aug 2025 14:35:54 +0200 Subject: [PATCH 093/744] editor: Add tests to ensure no horizontal scrolling is possible in soft wrap mode (#36411) Prior to https://github.com/zed-industries/zed/pull/34564 as well as https://github.com/zed-industries/zed/pull/26893, we would have cases where editors would be scrollable even if `soft_wrap` was set to `editor_width`. This has regressed and improved quite a few times back and forth. The issue was only within the editor code, the code for the wrap map was functioning and tested properly. Hence, this PR adds two tests to the editor rendering code in an effort to ensure that we maintain the current correct behavior. Release Notes: - N/A --- crates/editor/src/element.rs | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c15ff3e5094008788b85a62d4bb16d871b793d68..e56ac45fab86efe15660f73c07e39fd4b0b94de6 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -10187,6 +10187,71 @@ mod tests { use std::num::NonZeroU32; use util::test::sample_text; + #[gpui::test] + async fn test_soft_wrap_editor_width_auto_height_editor(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let window = cx.add_window(|window, cx| { + let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx); + let mut editor = Editor::new( + EditorMode::AutoHeight { + min_lines: 1, + max_lines: None, + }, + buffer, + None, + window, + cx, + ); + editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); + editor + }); + let cx = &mut VisualTestContext::from_window(*window, cx); + let editor = window.root(cx).unwrap(); + let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone()); + + for x in 1..=100 { + let (_, state) = cx.draw( + Default::default(), + size(px(200. + 0.13 * x as f32), px(500.)), + |_, _| EditorElement::new(&editor, style.clone()), + ); + + assert!( + state.position_map.scroll_max.x == 0., + "Soft wrapped editor should have no horizontal scrolling!" + ); + } + } + + #[gpui::test] + async fn test_soft_wrap_editor_width_full_editor(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let window = cx.add_window(|window, cx| { + let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx); + let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx); + editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); + editor + }); + let cx = &mut VisualTestContext::from_window(*window, cx); + let editor = window.root(cx).unwrap(); + let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone()); + + for x in 1..=100 { + let (_, state) = cx.draw( + Default::default(), + size(px(200. + 0.13 * x as f32), px(500.)), + |_, _| EditorElement::new(&editor, style.clone()), + ); + + assert!( + state.position_map.scroll_max.x == 0., + "Soft wrapped editor should have no horizontal scrolling!" + ); + } + } + #[gpui::test] fn test_shape_line_numbers(cx: &mut TestAppContext) { init_test(cx, |_| {}); From e2db434920cc22e9905e84a50ffec2f0f01da67b Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 18 Aug 2025 09:50:29 -0300 Subject: [PATCH 094/744] acp thread view: Floating editing message controls (#36283) Prevents layout shift when focusing the editor Release Notes: - N/A --------- Co-authored-by: Danilo Leal --- crates/agent_ui/src/acp/entry_view_state.rs | 1 + crates/agent_ui/src/acp/message_editor.rs | 5 +- crates/agent_ui/src/acp/thread_view.rs | 235 +++++++++----------- 3 files changed, 105 insertions(+), 136 deletions(-) diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index e99d1f6323ef36a8727bc78b69ce76c709324741..c7ab2353f125aeb02e0f66ef11e4e9677a29c4b8 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -67,6 +67,7 @@ impl EntryViewState { self.project.clone(), self.thread_store.clone(), self.text_thread_store.clone(), + "Edit message - @ to include context", editor::EditorMode::AutoHeight { min_lines: 1, max_lines: None, diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 12766ef458d112d277b9b22c80ffa959fe0e2a16..299f0c30be566e7d17e82180ee29efd0d95386b8 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -71,6 +71,7 @@ impl MessageEditor { project: Entity, thread_store: Entity, text_thread_store: Entity, + placeholder: impl Into>, mode: EditorMode, window: &mut Window, cx: &mut Context, @@ -94,7 +95,7 @@ impl MessageEditor { let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let mut editor = Editor::new(mode, buffer, None, window, cx); - editor.set_placeholder_text("Message the agent - @ to include files", cx); + editor.set_placeholder_text(placeholder, cx); editor.set_show_indent_guides(false, cx); editor.set_soft_wrap(); editor.set_use_modal_editing(true); @@ -1276,6 +1277,7 @@ mod tests { project.clone(), thread_store.clone(), text_thread_store.clone(), + "Test", EditorMode::AutoHeight { min_lines: 1, max_lines: None, @@ -1473,6 +1475,7 @@ mod tests { project.clone(), thread_store.clone(), text_thread_store.clone(), + "Test", EditorMode::AutoHeight { max_lines: None, min_lines: 1, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 271d9e5d4c78d5dd85be5b872dd7a15807383a20..3be6e355a98fab9ea360e8ca0e93f943986c09c4 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -159,6 +159,7 @@ impl AcpThreadView { project.clone(), thread_store.clone(), text_thread_store.clone(), + "Message the agent - @ to include context", editor::EditorMode::AutoHeight { min_lines: MIN_EDITOR_LINES, max_lines: Some(MAX_EDITOR_LINES), @@ -426,7 +427,9 @@ impl AcpThreadView { match event { MessageEditorEvent::Send => self.send(window, cx), MessageEditorEvent::Cancel => self.cancel_generation(cx), - MessageEditorEvent::Focus => {} + MessageEditorEvent::Focus => { + self.cancel_editing(&Default::default(), window, cx); + } } } @@ -742,44 +745,98 @@ impl AcpThreadView { cx: &Context, ) -> AnyElement { let primary = match &entry { - AgentThreadEntry::UserMessage(message) => div() - .id(("user_message", entry_ix)) - .py_4() - .px_2() - .children(message.id.clone().and_then(|message_id| { - message.checkpoint.as_ref()?.show.then(|| { - Button::new("restore-checkpoint", "Restore Checkpoint") - .icon(IconName::Undo) - .icon_size(IconSize::XSmall) - .icon_position(IconPosition::Start) - .label_size(LabelSize::XSmall) - .on_click(cx.listener(move |this, _, _window, cx| { - this.rewind(&message_id, cx); - })) - }) - })) - .child( - v_flex() - .p_3() - .gap_1p5() - .rounded_lg() - .shadow_md() - .bg(cx.theme().colors().editor_background) - .border_1() - .border_color(cx.theme().colors().border) - .text_xs() - .children( - self.entry_view_state - .read(cx) - .entry(entry_ix) - .and_then(|entry| entry.message_editor()) - .map(|editor| { - self.render_sent_message_editor(entry_ix, editor, cx) - .into_any_element() - }), - ), - ) - .into_any(), + AgentThreadEntry::UserMessage(message) => { + let Some(editor) = self + .entry_view_state + .read(cx) + .entry(entry_ix) + .and_then(|entry| entry.message_editor()) + .cloned() + else { + return Empty.into_any_element(); + }; + + let editing = self.editing_message == Some(entry_ix); + let editor_focus = editor.focus_handle(cx).is_focused(window); + let focus_border = cx.theme().colors().border_focused; + + div() + .id(("user_message", entry_ix)) + .py_4() + .px_2() + .children(message.id.clone().and_then(|message_id| { + message.checkpoint.as_ref()?.show.then(|| { + Button::new("restore-checkpoint", "Restore Checkpoint") + .icon(IconName::Undo) + .icon_size(IconSize::XSmall) + .icon_position(IconPosition::Start) + .label_size(LabelSize::XSmall) + .on_click(cx.listener(move |this, _, _window, cx| { + this.rewind(&message_id, cx); + })) + }) + })) + .child( + div() + .relative() + .child( + div() + .p_3() + .rounded_lg() + .shadow_md() + .bg(cx.theme().colors().editor_background) + .border_1() + .when(editing && !editor_focus, |this| this.border_dashed()) + .border_color(cx.theme().colors().border) + .map(|this|{ + if editor_focus { + this.border_color(focus_border) + } else { + this.hover(|s| s.border_color(focus_border.opacity(0.8))) + } + }) + .text_xs() + .child(editor.clone().into_any_element()), + ) + .when(editor_focus, |this| + this.child( + h_flex() + .absolute() + .top_neg_3p5() + .right_3() + .gap_1() + .rounded_sm() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_background) + .overflow_hidden() + .child( + IconButton::new("cancel", IconName::Close) + .icon_color(Color::Error) + .icon_size(IconSize::XSmall) + .on_click(cx.listener(Self::cancel_editing)) + ) + .child( + IconButton::new("regenerate", IconName::Return) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .tooltip(Tooltip::text( + "Editing will restart the thread from this point." + )) + .on_click(cx.listener({ + let editor = editor.clone(); + move |this, _, window, cx| { + this.regenerate( + entry_ix, &editor, window, cx, + ); + } + })), + ) + ) + ), + ) + .into_any() + } AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { let style = default_markdown_style(false, window, cx); let message_body = v_flex() @@ -854,20 +911,12 @@ impl AcpThreadView { if let Some(editing_index) = self.editing_message.as_ref() && *editing_index < entry_ix { - let backdrop = div() - .id(("backdrop", entry_ix)) - .size_full() - .absolute() - .inset_0() - .bg(cx.theme().colors().panel_background) - .opacity(0.8) - .block_mouse_except_scroll() - .on_click(cx.listener(Self::cancel_editing)); - div() - .relative() .child(primary) - .child(backdrop) + .opacity(0.2) + .block_mouse_except_scroll() + .id("overlay") + .on_click(cx.listener(Self::cancel_editing)) .into_any_element() } else { primary @@ -2512,90 +2561,6 @@ impl AcpThreadView { ) } - fn render_sent_message_editor( - &self, - entry_ix: usize, - editor: &Entity, - cx: &Context, - ) -> Div { - v_flex().w_full().gap_2().child(editor.clone()).when( - self.editing_message == Some(entry_ix), - |el| { - el.child( - h_flex() - .gap_1() - .child( - Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::XSmall), - ) - .child( - Label::new("Editing will restart the thread from this point.") - .color(Color::Muted) - .size(LabelSize::XSmall), - ) - .child(self.render_sent_message_editor_buttons(entry_ix, editor, cx)), - ) - }, - ) - } - - fn render_sent_message_editor_buttons( - &self, - entry_ix: usize, - editor: &Entity, - cx: &Context, - ) -> Div { - h_flex() - .gap_0p5() - .flex_1() - .justify_end() - .child( - IconButton::new("cancel-edit-message", IconName::Close) - .shape(ui::IconButtonShape::Square) - .icon_color(Color::Error) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Cancel Edit", - &menu::Cancel, - &focus_handle, - window, - cx, - ) - } - }) - .on_click(cx.listener(Self::cancel_editing)), - ) - .child( - IconButton::new("confirm-edit-message", IconName::Return) - .disabled(editor.read(cx).is_empty(cx)) - .shape(ui::IconButtonShape::Square) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Regenerate", - &menu::Confirm, - &focus_handle, - window, - cx, - ) - } - }) - .on_click(cx.listener({ - let editor = editor.clone(); - move |this, _, window, cx| { - this.regenerate(entry_ix, &editor, window, cx); - } - })), - ) - } - fn render_send_button(&self, cx: &mut Context) -> AnyElement { if self.thread().map_or(true, |thread| { thread.read(cx).status() == ThreadStatus::Idle From 6f56ac50fecf360a2983adc88fc1e164ac8f9dcc Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:45:52 +0530 Subject: [PATCH 095/744] Use upstream version of yawc (#36412) As this was merged in upstream: https://github.com/infinitefield/yawc/pull/16. It's safe to point yawc to upstream instead of fork. cc @maxdeviant Release Notes: - N/A --- Cargo.lock | 5 +++-- Cargo.toml | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4f8c521a1e46b6c312069102bb184e6a5ecbae7..98f10eff419095a1400cf8efdf02ba2fef71b499 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20196,8 +20196,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yawc" -version = "0.2.4" -source = "git+https://github.com/deviant-forks/yawc?rev=1899688f3e69ace4545aceb97b2a13881cf26142#1899688f3e69ace4545aceb97b2a13881cf26142" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a5d82922135b4ae73a079a4ffb5501e9aadb4d785b8c660eaa0a8b899028c5" dependencies = [ "base64 0.22.1", "bytes 1.10.1", diff --git a/Cargo.toml b/Cargo.toml index 14691cf8a4f3d723e99710b72807ff931c8b7da2..83d6da5cd7e7e3c8b2b3b90eec85fe280a1556aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -659,9 +659,7 @@ which = "6.0.0" windows-core = "0.61" wit-component = "0.221" workspace-hack = "0.1.0" -# We can switch back to the published version once https://github.com/infinitefield/yawc/pull/16 is merged and a new -# version is released. -yawc = { git = "https://github.com/deviant-forks/yawc", rev = "1899688f3e69ace4545aceb97b2a13881cf26142" } +yawc = "0.2.5" zstd = "0.11" [workspace.dependencies.windows] From 6bf666958c7a2cf931ae22690c1affa069c5bbd1 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:49:17 -0300 Subject: [PATCH 096/744] agent2: Allow to interrupt and send a new message (#36185) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 77 +++++++++++++++++++------- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 3be6e355a98fab9ea360e8ca0e93f943986c09c4..2fc30e300712c393b3845d307be7c38c73c3472f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -474,12 +474,41 @@ impl AcpThreadView { } fn send(&mut self, window: &mut Window, cx: &mut Context) { + if let Some(thread) = self.thread() { + if thread.read(cx).status() != ThreadStatus::Idle { + self.stop_current_and_send_new_message(window, cx); + return; + } + } + let contents = self .message_editor .update(cx, |message_editor, cx| message_editor.contents(window, cx)); self.send_impl(contents, window, cx) } + fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context) { + let Some(thread) = self.thread().cloned() else { + return; + }; + + let cancelled = thread.update(cx, |thread, cx| thread.cancel(cx)); + + let contents = self + .message_editor + .update(cx, |message_editor, cx| message_editor.contents(window, cx)); + + cx.spawn_in(window, async move |this, cx| { + cancelled.await; + + this.update_in(cx, |this, window, cx| { + this.send_impl(contents, window, cx); + }) + .ok(); + }) + .detach(); + } + fn send_impl( &mut self, contents: Task>>, @@ -2562,25 +2591,12 @@ impl AcpThreadView { } fn render_send_button(&self, cx: &mut Context) -> AnyElement { - if self.thread().map_or(true, |thread| { - thread.read(cx).status() == ThreadStatus::Idle - }) { - let is_editor_empty = self.message_editor.read(cx).is_empty(cx); - IconButton::new("send-message", IconName::Send) - .icon_color(Color::Accent) - .style(ButtonStyle::Filled) - .disabled(self.thread().is_none() || is_editor_empty) - .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.send(window, cx); - })) - .into_any_element() - } else { + let is_editor_empty = self.message_editor.read(cx).is_empty(cx); + let is_generating = self.thread().map_or(false, |thread| { + thread.read(cx).status() != ThreadStatus::Idle + }); + + if is_generating && is_editor_empty { IconButton::new("stop-generation", IconName::Stop) .icon_color(Color::Error) .style(ButtonStyle::Tinted(ui::TintColor::Error)) @@ -2589,6 +2605,29 @@ impl AcpThreadView { }) .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx))) .into_any_element() + } else { + let send_btn_tooltip = if is_editor_empty && !is_generating { + "Type to Send" + } else if is_generating { + "Stop and Send Message" + } else { + "Send" + }; + + IconButton::new("send-message", IconName::Send) + .style(ButtonStyle::Filled) + .map(|this| { + if is_editor_empty && !is_generating { + this.disabled(true).icon_color(Color::Muted) + } else { + this.icon_color(Color::Accent) + } + }) + .tooltip(move |window, cx| Tooltip::for_action(send_btn_tooltip, &Chat, window, cx)) + .on_click(cx.listener(|this, _, window, cx| { + this.send(window, cx); + })) + .into_any_element() } } From db31fa67f301b0b22f029e455ddad86b28b28371 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 18 Aug 2025 11:37:28 -0300 Subject: [PATCH 097/744] acp: Stay in edit mode when current completion ends (#36413) When a turn ends and the checkpoint is updated, `AcpThread` emits `EntryUpdated` with the index of the user message. This was causing the message editor to be recreated and, therefore, lose focus. Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 1 + crates/acp_thread/src/connection.rs | 119 ++++++++++++++------ crates/agent_ui/src/acp/entry_view_state.rs | 66 ++++++----- crates/agent_ui/src/acp/thread_view.rs | 96 +++++++++++++++- 4 files changed, 213 insertions(+), 69 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index fb312653265a408f9ab98a06449d572ab5063714..3762c553ccba03ae9ea2c7feac9fd5e87eda56c7 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -670,6 +670,7 @@ pub struct AcpThread { session_id: acp::SessionId, } +#[derive(Debug)] pub enum AcpThreadEvent { NewEntry, EntryUpdated(usize), diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 7497d2309f1de72186c23773429cc6e8c57de2d2..48310f07ce3cb162111b6c88d7f39f36b39b1f77 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -186,7 +186,7 @@ mod test_support { use std::sync::Arc; use collections::HashMap; - use futures::future::try_join_all; + use futures::{channel::oneshot, future::try_join_all}; use gpui::{AppContext as _, WeakEntity}; use parking_lot::Mutex; @@ -194,11 +194,16 @@ mod test_support { #[derive(Clone, Default)] pub struct StubAgentConnection { - sessions: Arc>>>, + sessions: Arc>>, permission_requests: HashMap>, next_prompt_updates: Arc>>, } + struct Session { + thread: WeakEntity, + response_tx: Option>, + } + impl StubAgentConnection { pub fn new() -> Self { Self { @@ -226,15 +231,33 @@ mod test_support { update: acp::SessionUpdate, cx: &mut App, ) { + assert!( + self.next_prompt_updates.lock().is_empty(), + "Use either send_update or set_next_prompt_updates" + ); + self.sessions .lock() .get(&session_id) .unwrap() + .thread .update(cx, |thread, cx| { thread.handle_session_update(update.clone(), cx).unwrap(); }) .unwrap(); } + + pub fn end_turn(&self, session_id: acp::SessionId) { + self.sessions + .lock() + .get_mut(&session_id) + .unwrap() + .response_tx + .take() + .expect("No pending turn") + .send(()) + .unwrap(); + } } impl AgentConnection for StubAgentConnection { @@ -251,7 +274,13 @@ mod test_support { let session_id = acp::SessionId(self.sessions.lock().len().to_string().into()); let thread = cx.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)); - self.sessions.lock().insert(session_id, thread.downgrade()); + self.sessions.lock().insert( + session_id, + Session { + thread: thread.downgrade(), + response_tx: None, + }, + ); Task::ready(Ok(thread)) } @@ -269,43 +298,59 @@ mod test_support { params: acp::PromptRequest, cx: &mut App, ) -> Task> { - let sessions = self.sessions.lock(); - let thread = sessions.get(¶ms.session_id).unwrap(); + let mut sessions = self.sessions.lock(); + let Session { + thread, + response_tx, + } = sessions.get_mut(¶ms.session_id).unwrap(); let mut tasks = vec![]; - for update in self.next_prompt_updates.lock().drain(..) { - 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_authorization( - tool_call.clone().into(), - options.clone(), - cx, - ) + if self.next_prompt_updates.lock().is_empty() { + let (tx, rx) = oneshot::channel(); + response_tx.replace(tx); + cx.spawn(async move |_| { + rx.await?; + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + }) + } else { + for update in self.next_prompt_updates.lock().drain(..) { + 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_authorization( + tool_call.clone().into(), + options.clone(), + cx, + ) + })?; + permission?.await?; + } + thread.update(cx, |thread, cx| { + thread.handle_session_update(update.clone(), cx).unwrap(); })?; - 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(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, + anyhow::Ok(()) + }); + tasks.push(task); + } + + cx.spawn(async move |_| { + try_join_all(tasks).await?; + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) }) - }) + } } fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index c7ab2353f125aeb02e0f66ef11e4e9677a29c4b8..18ef1ce2abe33783a0fb1b5f44c559e48c667617 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -5,8 +5,8 @@ use agent::{TextThreadStore, ThreadStore}; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; use gpui::{ - AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, TextStyleRefinement, - WeakEntity, Window, + AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, + TextStyleRefinement, WeakEntity, Window, }; use language::language_settings::SoftWrap; use project::Project; @@ -61,34 +61,44 @@ impl EntryViewState { AgentThreadEntry::UserMessage(message) => { let has_id = message.id.is_some(); let chunks = message.chunks.clone(); - let message_editor = cx.new(|cx| { - let mut editor = MessageEditor::new( - self.workspace.clone(), - self.project.clone(), - self.thread_store.clone(), - self.text_thread_store.clone(), - "Edit message - @ to include context", - editor::EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ); - if !has_id { - editor.set_read_only(true, cx); + if let Some(Entry::UserMessage(editor)) = self.entries.get_mut(index) { + if !editor.focus_handle(cx).is_focused(window) { + // Only update if we are not editing. + // If we are, cancelling the edit will set the message to the newest content. + editor.update(cx, |editor, cx| { + editor.set_message(chunks, window, cx); + }); } - editor.set_message(chunks, window, cx); - editor - }); - cx.subscribe(&message_editor, move |_, editor, event, cx| { - cx.emit(EntryViewEvent { - entry_index: index, - view_event: ViewEvent::MessageEditorEvent(editor, *event), + } else { + let message_editor = cx.new(|cx| { + let mut editor = MessageEditor::new( + self.workspace.clone(), + self.project.clone(), + self.thread_store.clone(), + self.text_thread_store.clone(), + "Edit message - @ to include context", + editor::EditorMode::AutoHeight { + min_lines: 1, + max_lines: None, + }, + window, + cx, + ); + if !has_id { + editor.set_read_only(true, cx); + } + editor.set_message(chunks, window, cx); + editor + }); + cx.subscribe(&message_editor, move |_, editor, event, cx| { + cx.emit(EntryViewEvent { + entry_index: index, + view_event: ViewEvent::MessageEditorEvent(editor, *event), + }) }) - }) - .detach(); - self.set_entry(index, Entry::UserMessage(message_editor)); + .detach(); + self.set_entry(index, Entry::UserMessage(message_editor)); + } } AgentThreadEntry::ToolCall(tool_call) => { let terminals = tool_call.terminals().cloned().collect::>(); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2fc30e300712c393b3845d307be7c38c73c3472f..4760677fa1e9ff270f69cdc1adbd49c0e86f799c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -3606,7 +3606,7 @@ pub(crate) mod tests { async fn test_drop(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, _cx) = setup_thread_view(StubAgentServer::default(), cx).await; + let (thread_view, _cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; let weak_view = thread_view.downgrade(); drop(thread_view); assert!(!weak_view.is_upgradable()); @@ -3616,7 +3616,7 @@ pub(crate) mod tests { async fn test_notification_for_stop_event(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, cx) = setup_thread_view(StubAgentServer::default(), cx).await; + let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); message_editor.update_in(cx, |editor, window, cx| { @@ -3800,8 +3800,12 @@ pub(crate) mod tests { } impl StubAgentServer { - fn default() -> Self { - Self::new(StubAgentConnection::default()) + fn default_response() -> Self { + let conn = StubAgentConnection::new(); + conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk { + content: "Default response".into(), + }]); + Self::new(conn) } } @@ -4214,4 +4218,88 @@ pub(crate) mod tests { assert_eq!(new_editor.read(cx).text(cx), "Edited message content"); }) } + + #[gpui::test] + async fn test_message_editing_while_generating(cx: &mut TestAppContext) { + init_test(cx); + + let connection = StubAgentConnection::new(); + + let (thread_view, cx) = + setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; + add_to_workspace(thread_view.clone(), cx); + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Original message to edit", window, cx); + }); + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.send(window, cx); + }); + + cx.run_until_parked(); + + let (user_message_editor, session_id) = thread_view.read_with(cx, |view, cx| { + let thread = view.thread().unwrap().read(cx); + assert_eq!(thread.entries().len(), 1); + + let editor = view + .entry_view_state + .read(cx) + .entry(0) + .unwrap() + .message_editor() + .unwrap() + .clone(); + + (editor, thread.session_id().clone()) + }); + + // Focus + cx.focus(&user_message_editor); + + thread_view.read_with(cx, |view, _cx| { + assert_eq!(view.editing_message, Some(0)); + }); + + // Edit + user_message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Edited message content", window, cx); + }); + + thread_view.read_with(cx, |view, _cx| { + assert_eq!(view.editing_message, Some(0)); + }); + + // Finish streaming response + cx.update(|_, cx| { + connection.send_update( + session_id.clone(), + acp::SessionUpdate::AgentMessageChunk { + content: acp::ContentBlock::Text(acp::TextContent { + text: "Response".into(), + annotations: None, + }), + }, + cx, + ); + connection.end_turn(session_id); + }); + + thread_view.read_with(cx, |view, _cx| { + assert_eq!(view.editing_message, Some(0)); + }); + + cx.run_until_parked(); + + // Should still be editing + cx.update(|window, cx| { + assert!(user_message_editor.focus_handle(cx).is_focused(window)); + assert_eq!(thread_view.read(cx).editing_message, Some(0)); + assert_eq!( + user_message_editor.read(cx).text(cx), + "Edited message content" + ); + }); + } } From 9b78c4690208367444699f1e3a58e96437cdecd1 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 18 Aug 2025 16:48:38 +0200 Subject: [PATCH 098/744] python: Use pip provided by our 'base' venv (#36414) Closes #36218 Release Notes: - Debugger: Python debugger installation no longer assumes that pip is available in global Python installation --- crates/dap_adapters/src/python.rs | 58 +++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index a2bd934311ec21da13d08d23211e62718ec5bbc5..7b90f80fe24145ddfd0371838eaa9e2f7787af84 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -24,6 +24,7 @@ use util::{ResultExt, maybe}; #[derive(Default)] pub(crate) struct PythonDebugAdapter { + base_venv_path: OnceCell, String>>, debugpy_whl_base_path: OnceCell, String>>, } @@ -91,14 +92,12 @@ impl PythonDebugAdapter { }) } - async fn fetch_wheel(delegate: &Arc) -> Result, String> { - let system_python = Self::system_python_name(delegate) - .await - .ok_or_else(|| String::from("Could not find a Python installation"))?; - let command: &OsStr = system_python.as_ref(); + async fn fetch_wheel(&self, delegate: &Arc) -> Result, String> { 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) + let system_python = self.base_venv_path(delegate).await?; + + let installation_succeeded = util::command::new_smol_command(system_python.as_ref()) .args([ "-m", "pip", @@ -114,7 +113,7 @@ impl PythonDebugAdapter { .status .success(); if !installation_succeeded { - return Err("debugpy installation failed".into()); + return Err("debugpy installation failed (could not fetch Debugpy's wheel)".into()); } let wheel_path = std::fs::read_dir(&download_dir) @@ -139,7 +138,7 @@ impl PythonDebugAdapter { Ok(Arc::from(wheel_path.path())) } - async fn maybe_fetch_new_wheel(delegate: &Arc) { + async fn maybe_fetch_new_wheel(&self, delegate: &Arc) { let latest_release = delegate .http_client() .get( @@ -191,7 +190,7 @@ impl PythonDebugAdapter { ) .await .ok()?; - Self::fetch_wheel(delegate).await.ok()?; + self.fetch_wheel(delegate).await.ok()?; } Some(()) }) @@ -204,7 +203,7 @@ impl PythonDebugAdapter { ) -> Result, String> { self.debugpy_whl_base_path .get_or_init(|| async move { - Self::maybe_fetch_new_wheel(delegate).await; + self.maybe_fetch_new_wheel(delegate).await; Ok(Arc::from( debug_adapters_dir() .join(Self::ADAPTER_NAME) @@ -217,6 +216,45 @@ impl PythonDebugAdapter { .clone() } + async fn base_venv_path(&self, delegate: &Arc) -> Result, String> { + self.base_venv_path + .get_or_init(|| async { + let base_python = Self::system_python_name(delegate) + .await + .ok_or_else(|| String::from("Could not find a Python installation"))?; + + let did_succeed = util::command::new_smol_command(base_python) + .args(["-m", "venv", "zed_base_venv"]) + .current_dir( + paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref()), + ) + .spawn() + .map_err(|e| format!("{e:#?}"))? + .status() + .await + .map_err(|e| format!("{e:#?}"))? + .success(); + if !did_succeed { + return Err("Failed to create base virtual environment".into()); + } + + const DIR: &'static str = if cfg!(target_os = "windows") { + "Scripts" + } else { + "bin" + }; + Ok(Arc::from( + paths::debug_adapters_dir() + .join(Self::DEBUG_ADAPTER_NAME.as_ref()) + .join("zed_base_venv") + .join(DIR) + .join("python3") + .as_ref(), + )) + }) + .await + .clone() + } async fn system_python_name(delegate: &Arc) -> Option { const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"]; let mut name = None; From 48fed866e60f1951bd8aa6ccec000670ce839b7f Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 18 Aug 2025 12:34:27 -0300 Subject: [PATCH 099/744] acp: Have `AcpThread` handle all interrupting (#36417) The view was cancelling the generation, but `AcpThread` already handles that, so we removed that extra code and fixed a bug where an update from the first user message would appear after the second one. Release Notes: - N/A Co-authored-by: Danilo --- crates/acp_thread/src/acp_thread.rs | 22 ++-- crates/acp_thread/src/connection.rs | 27 +++-- crates/agent_ui/src/acp/thread_view.rs | 135 ++++++++++++++++++++++++- 3 files changed, 164 insertions(+), 20 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 3762c553ccba03ae9ea2c7feac9fd5e87eda56c7..e104c40bf2bc969b35e94d0b5a1dda5658116ead 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1200,17 +1200,21 @@ impl AcpThread { } else { None }; - self.push_entry( - AgentThreadEntry::UserMessage(UserMessage { - id: message_id.clone(), - content: block, - chunks: message, - checkpoint: None, - }), - cx, - ); self.run_turn(cx, async move |this, cx| { + this.update(cx, |this, cx| { + this.push_entry( + AgentThreadEntry::UserMessage(UserMessage { + id: message_id.clone(), + content: block, + chunks: message, + checkpoint: None, + }), + cx, + ); + }) + .ok(); + let old_checkpoint = git_store .update(cx, |git, cx| git.checkpoint(cx))? .await diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 48310f07ce3cb162111b6c88d7f39f36b39b1f77..a328499bbc1a9ac47aee2d3421c9fdcceb0e11cd 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -201,7 +201,7 @@ mod test_support { struct Session { thread: WeakEntity, - response_tx: Option>, + response_tx: Option>, } impl StubAgentConnection { @@ -242,12 +242,12 @@ mod test_support { .unwrap() .thread .update(cx, |thread, cx| { - thread.handle_session_update(update.clone(), cx).unwrap(); + thread.handle_session_update(update, cx).unwrap(); }) .unwrap(); } - pub fn end_turn(&self, session_id: acp::SessionId) { + pub fn end_turn(&self, session_id: acp::SessionId, stop_reason: acp::StopReason) { self.sessions .lock() .get_mut(&session_id) @@ -255,7 +255,7 @@ mod test_support { .response_tx .take() .expect("No pending turn") - .send(()) + .send(stop_reason) .unwrap(); } } @@ -308,10 +308,8 @@ mod test_support { let (tx, rx) = oneshot::channel(); response_tx.replace(tx); cx.spawn(async move |_| { - rx.await?; - Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - }) + let stop_reason = rx.await?; + Ok(acp::PromptResponse { stop_reason }) }) } else { for update in self.next_prompt_updates.lock().drain(..) { @@ -353,8 +351,17 @@ mod test_support { } } - fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { - unimplemented!() + fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { + if let Some(end_turn_tx) = self + .sessions + .lock() + .get_mut(session_id) + .unwrap() + .response_tx + .take() + { + end_turn_tx.send(acp::StopReason::Canceled).unwrap(); + } } fn session_editor( diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 4760677fa1e9ff270f69cdc1adbd49c0e86f799c..2c02027c4decc596151a77e8f29fca5c1c35e412 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4283,7 +4283,7 @@ pub(crate) mod tests { }, cx, ); - connection.end_turn(session_id); + connection.end_turn(session_id, acp::StopReason::EndTurn); }); thread_view.read_with(cx, |view, _cx| { @@ -4302,4 +4302,137 @@ pub(crate) mod tests { ); }); } + + #[gpui::test] + async fn test_interrupt(cx: &mut TestAppContext) { + init_test(cx); + + let connection = StubAgentConnection::new(); + + let (thread_view, cx) = + setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; + add_to_workspace(thread_view.clone(), cx); + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Message 1", window, cx); + }); + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.send(window, cx); + }); + + let (thread, session_id) = thread_view.read_with(cx, |view, cx| { + let thread = view.thread().unwrap(); + + (thread.clone(), thread.read(cx).session_id().clone()) + }); + + cx.run_until_parked(); + + cx.update(|_, cx| { + connection.send_update( + session_id.clone(), + acp::SessionUpdate::AgentMessageChunk { + content: "Message 1 resp".into(), + }, + cx, + ); + }); + + cx.run_until_parked(); + + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc::indoc! {" + ## User + + Message 1 + + ## Assistant + + Message 1 resp + + "} + ) + }); + + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Message 2", window, cx); + }); + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.send(window, cx); + }); + + cx.update(|_, cx| { + // Simulate a response sent after beginning to cancel + connection.send_update( + session_id.clone(), + acp::SessionUpdate::AgentMessageChunk { + content: "onse".into(), + }, + cx, + ); + }); + + cx.run_until_parked(); + + // Last Message 1 response should appear before Message 2 + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc::indoc! {" + ## User + + Message 1 + + ## Assistant + + Message 1 response + + ## User + + Message 2 + + "} + ) + }); + + cx.update(|_, cx| { + connection.send_update( + session_id.clone(), + acp::SessionUpdate::AgentMessageChunk { + content: "Message 2 response".into(), + }, + cx, + ); + connection.end_turn(session_id.clone(), acp::StopReason::EndTurn); + }); + + cx.run_until_parked(); + + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc::indoc! {" + ## User + + Message 1 + + ## Assistant + + Message 1 response + + ## User + + Message 2 + + ## Assistant + + Message 2 response + + "} + ) + }); + } } From e1d31cfcc3360bf50f6230d6dd5d1aafc3295c4c Mon Sep 17 00:00:00 2001 From: AidanV <84053180+AidanV@users.noreply.github.com> Date: Mon, 18 Aug 2025 08:52:25 -0700 Subject: [PATCH 100/744] vim: Display invisibles in mode indicator (#35760) Release Notes: - Fixes bug where `ctrl-k enter` while in `INSERT` mode would put a newline in the Vim mode indicator #### Old OldVimModeIndicator #### New NewVimModeIndicator --- crates/vim/src/state.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index c4be0348717a31eac5fc5adc1f2f8b75e3526406..423859dadca94f86b673ba95327b22d6879ec710 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -1028,13 +1028,21 @@ impl Operator { } pub fn status(&self) -> String { + fn make_visible(c: &str) -> &str { + match c { + "\n" => "enter", + "\t" => "tab", + " " => "space", + c => c, + } + } match self { Operator::Digraph { first_char: Some(first_char), - } => format!("^K{first_char}"), + } => format!("^K{}", make_visible(&first_char.to_string())), Operator::Literal { prefix: Some(prefix), - } => format!("^V{prefix}"), + } => format!("^V{}", make_visible(&prefix)), Operator::AutoIndent => "=".to_string(), Operator::ShellCommand => "=".to_string(), _ => self.id().to_string(), From 768b2de368697a559a038f65e61aff81dc99f041 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Mon, 18 Aug 2025 12:57:53 -0300 Subject: [PATCH 101/744] vim: Fix `ap` text object selection when there is line wrapping (#35485) In Vim mode, `ap` text object (used in `vap`, `dap`, `cap`) was selecting multiple paragraphs when soft wrap was enabled. The bug was caused by using DisplayRow coordinates for arithmetic instead of buffer row coordinates in the paragraph boundary calculation. Fix by converting to buffer coordinates before arithmetic, then back to display coordinates for the final result. Closes #35085 --------- Co-authored-by: Conrad Irwin --- crates/vim/src/normal/delete.rs | 22 +++++ crates/vim/src/object.rs | 93 ++++++++++++++++++- crates/vim/src/visual.rs | 33 +++++++ ...hange_paragraph_object_with_soft_wrap.json | 72 ++++++++++++++ ...elete_paragraph_object_with_soft_wrap.json | 72 ++++++++++++++ .../test_delete_paragraph_whitespace.json | 5 + ...isual_paragraph_object_with_soft_wrap.json | 72 ++++++++++++++ 7 files changed, 365 insertions(+), 4 deletions(-) create mode 100644 crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json create mode 100644 crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json create mode 100644 crates/vim/test_data/test_delete_paragraph_whitespace.json create mode 100644 crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index 1b7557371a4161a00afe55765114bd887c7010ee..d7a6932baa8e141a7c8ffd64f7c34d75db29fe97 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -2,6 +2,7 @@ use crate::{ Vim, motion::{Motion, MotionKind}, object::Object, + state::Mode, }; use collections::{HashMap, HashSet}; use editor::{ @@ -102,8 +103,20 @@ impl Vim { // Emulates behavior in vim where if we expanded backwards to include a newline // the cursor gets set back to the start of the line let mut should_move_to_start: HashSet<_> = Default::default(); + + // Emulates behavior in vim where after deletion the cursor should try to move + // to the same column it was before deletion if the line is not empty or only + // contains whitespace + let mut column_before_move: HashMap<_, _> = Default::default(); + let target_mode = object.target_visual_mode(vim.mode, around); + editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { + let cursor_point = selection.head().to_point(map); + if target_mode == Mode::VisualLine { + column_before_move.insert(selection.id, cursor_point.column); + } + object.expand_selection(map, selection, around, times); let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range(); let mut move_selection_start_to_previous_line = @@ -164,6 +177,15 @@ impl Vim { let mut cursor = selection.head(); if should_move_to_start.contains(&selection.id) { *cursor.column_mut() = 0; + } else if let Some(column) = column_before_move.get(&selection.id) + && *column > 0 + { + let mut cursor_point = cursor.to_point(map); + cursor_point.column = *column; + cursor = map + .buffer_snapshot + .clip_point(cursor_point, Bias::Left) + .to_display_point(map); } cursor = map.clip_point(cursor, Bias::Left); selection.collapse_to(cursor, selection.goal) diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 63139d7e94cf1a38764a3d692b88b7bb1c235b31..cff23c4bd4afd02fddc1942cb52179a6beb0e60f 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -1444,14 +1444,15 @@ fn paragraph( return None; } - let paragraph_start_row = paragraph_start.row(); - if paragraph_start_row.0 != 0 { + let paragraph_start_buffer_point = paragraph_start.to_point(map); + if paragraph_start_buffer_point.row != 0 { let previous_paragraph_last_line_start = - Point::new(paragraph_start_row.0 - 1, 0).to_display_point(map); + Point::new(paragraph_start_buffer_point.row - 1, 0).to_display_point(map); paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start); } } else { - let mut start_row = paragraph_end_row.0 + 1; + let paragraph_end_buffer_point = paragraph_end.to_point(map); + let mut start_row = paragraph_end_buffer_point.row + 1; if i > 0 { start_row += 1; } @@ -1903,6 +1904,90 @@ mod test { } } + #[gpui::test] + async fn test_change_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + const WRAPPING_EXAMPLE: &str = indoc! {" + ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines. + + ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly. + + ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ + "}; + + cx.set_shared_wrap(20).await; + + cx.simulate_at_each_offset("c i p", WRAPPING_EXAMPLE) + .await + .assert_matches(); + cx.simulate_at_each_offset("c a p", WRAPPING_EXAMPLE) + .await + .assert_matches(); + } + + #[gpui::test] + async fn test_delete_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + const WRAPPING_EXAMPLE: &str = indoc! {" + ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines. + + ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly. + + ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ + "}; + + cx.set_shared_wrap(20).await; + + cx.simulate_at_each_offset("d i p", WRAPPING_EXAMPLE) + .await + .assert_matches(); + cx.simulate_at_each_offset("d a p", WRAPPING_EXAMPLE) + .await + .assert_matches(); + } + + #[gpui::test] + async fn test_delete_paragraph_whitespace(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + a + ˇ• + aaaaaaaaaaaaa + "}) + .await; + + cx.simulate_shared_keystrokes("d i p").await; + cx.shared_state().await.assert_eq(indoc! {" + a + aaaaaaaˇaaaaaa + "}); + } + + #[gpui::test] + async fn test_visual_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + const WRAPPING_EXAMPLE: &str = indoc! {" + ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines. + + ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly. + + ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ + "}; + + cx.set_shared_wrap(20).await; + + cx.simulate_at_each_offset("v i p", WRAPPING_EXAMPLE) + .await + .assert_matches(); + cx.simulate_at_each_offset("v a p", WRAPPING_EXAMPLE) + .await + .assert_matches(); + } + // Test string with "`" for opening surrounders and "'" for closing surrounders const SURROUNDING_MARKER_STRING: &str = indoc! {" ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn` diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 7bfd8dc8befa8f1650f82d9fa3e69c35973a4020..3b789b1f3e0962aded2500cf21466ec47ea6e0c9 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -414,6 +414,8 @@ impl Vim { ); } + let original_point = selection.tail().to_point(&map); + if let Some(range) = object.range(map, mut_selection, around, count) { if !range.is_empty() { let expand_both_ways = object.always_expands_both_ways() @@ -462,6 +464,37 @@ impl Vim { }; selection.end = new_selection_end.to_display_point(map); } + + // To match vim, if the range starts of the same line as it originally + // did, we keep the tail of the selection in the same place instead of + // snapping it to the start of the line + if target_mode == Mode::VisualLine { + let new_start_point = selection.start.to_point(map); + if new_start_point.row == original_point.row { + if selection.end.to_point(map).row > new_start_point.row { + if original_point.column + == map + .buffer_snapshot + .line_len(MultiBufferRow(original_point.row)) + { + selection.start = movement::saturating_left( + map, + original_point.to_display_point(map), + ) + } else { + selection.start = original_point.to_display_point(map) + } + } else { + selection.end = movement::saturating_right( + map, + original_point.to_display_point(map), + ); + if original_point.column > 0 { + selection.reversed = true + } + } + } + } } }); }); diff --git a/crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json b/crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json new file mode 100644 index 0000000000000000000000000000000000000000..47d68e13a630ad6138f8279d3b67c3a7b1ad56de --- /dev/null +++ b/crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json @@ -0,0 +1,72 @@ +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=20"}} +{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"ˇ\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"ˇ\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}} +{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json b/crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json new file mode 100644 index 0000000000000000000000000000000000000000..19dcd175b3593dd0df5b40046f0ea75115ca2cc5 --- /dev/null +++ b/crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json @@ -0,0 +1,72 @@ +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=20"}} +{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}} +{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Second paragraph that is also quite long and will definitely wrap under soft wrap conditions andˇ should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nThird paragraph with additional long text content that will also wrap when line length is constraˇined by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_paragraph_whitespace.json b/crates/vim/test_data/test_delete_paragraph_whitespace.json new file mode 100644 index 0000000000000000000000000000000000000000..e07b18eaa3cc94f9c32e5cda6aa258aaaa1e7826 --- /dev/null +++ b/crates/vim/test_data/test_delete_paragraph_whitespace.json @@ -0,0 +1,5 @@ +{"Put":{"state":"a\n ˇ•\naaaaaaaaaaaaa\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"a\naaaaaaaˇaaaaaa\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json b/crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json new file mode 100644 index 0000000000000000000000000000000000000000..6bfce2f955299b4ce8c351479ee9de2484ce4bfd --- /dev/null +++ b/crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json @@ -0,0 +1,72 @@ +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=20"}} +{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"«Fˇ»irst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"«ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is l»imited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«Sˇ»econd paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and s»hould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«Tˇ»hird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping s»ettings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.»\n","mode":"VisualLine"}} +{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"«First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ»Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is «limited making it span multiple display lines.\n\nˇ»Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ»Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and «should be handled correctly.\n\nˇ»Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\nˇ»","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping «settings.\nˇ»","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings«.\nˇ»","mode":"VisualLine"}} From e1d8e3bf6d74f260f8fc5b8d0ec3aa89fb3f6985 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:58:12 +0300 Subject: [PATCH 102/744] language: Clean up allocations (#36418) - Correctly pre-allocate `Vec` when deserializing regexes - Simplify manual `Vec::with_capacity` calls by using `Iterator::unzip` - Collect directly into `Arc<[T]>` (uses `Vec` internally anyway, but simplifies code) - Remove unnecessary `LazyLock` around Atomics by not using const incompatible `Default` for initialization. Release Notes: - N/A --- crates/language/src/language.rs | 42 +++++++++++++++------------------ crates/language/src/proto.rs | 10 ++++---- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 6fa31da860e1e53164c03185f47c1af95952a011..c377d7440a49ea7a8535e58b85cae0f0f70f7781 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -121,8 +121,8 @@ where func(cursor.deref_mut()) } -static NEXT_LANGUAGE_ID: LazyLock = LazyLock::new(Default::default); -static NEXT_GRAMMAR_ID: LazyLock = LazyLock::new(Default::default); +static NEXT_LANGUAGE_ID: AtomicUsize = AtomicUsize::new(0); +static NEXT_GRAMMAR_ID: AtomicUsize = AtomicUsize::new(0); static WASM_ENGINE: LazyLock = LazyLock::new(|| { wasmtime::Engine::new(&wasmtime::Config::new()).expect("Failed to create Wasmtime engine") }); @@ -964,11 +964,11 @@ where fn deserialize_regex_vec<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { let sources = Vec::::deserialize(d)?; - let mut regexes = Vec::new(); - for source in sources { - regexes.push(regex::Regex::new(&source).map_err(de::Error::custom)?); - } - Ok(regexes) + sources + .into_iter() + .map(|source| regex::Regex::new(&source)) + .collect::>() + .map_err(de::Error::custom) } fn regex_vec_json_schema(_: &mut SchemaGenerator) -> schemars::Schema { @@ -1034,12 +1034,10 @@ impl<'de> Deserialize<'de> for BracketPairConfig { D: Deserializer<'de>, { let result = Vec::::deserialize(deserializer)?; - let mut brackets = Vec::with_capacity(result.len()); - let mut disabled_scopes_by_bracket_ix = Vec::with_capacity(result.len()); - for entry in result { - brackets.push(entry.bracket_pair); - disabled_scopes_by_bracket_ix.push(entry.not_in); - } + let (brackets, disabled_scopes_by_bracket_ix) = result + .into_iter() + .map(|entry| (entry.bracket_pair, entry.not_in)) + .unzip(); Ok(BracketPairConfig { pairs: brackets, @@ -1379,16 +1377,14 @@ impl Language { let grammar = self.grammar_mut().context("cannot mutate grammar")?; let query = Query::new(&grammar.ts_language, source)?; - let mut extra_captures = Vec::with_capacity(query.capture_names().len()); - - for name in query.capture_names().iter() { - let kind = if *name == "run" { - RunnableCapture::Run - } else { - RunnableCapture::Named(name.to_string().into()) - }; - extra_captures.push(kind); - } + let extra_captures: Vec<_> = query + .capture_names() + .iter() + .map(|&name| match name { + "run" => RunnableCapture::Run, + name => RunnableCapture::Named(name.to_string().into()), + }) + .collect(); grammar.runnable_config = Some(RunnableConfig { extra_captures, diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 18f6bb8709c707af9dd19223cac30d6728eda160..acae97019f0cfc73cef6c8fa91da68efa3d51e18 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -385,12 +385,10 @@ pub fn deserialize_undo_map_entry( /// Deserializes selections from the RPC representation. pub fn deserialize_selections(selections: Vec) -> Arc<[Selection]> { - Arc::from( - selections - .into_iter() - .filter_map(deserialize_selection) - .collect::>(), - ) + selections + .into_iter() + .filter_map(deserialize_selection) + .collect() } /// Deserializes a [`Selection`] from the RPC representation. From ed155ceba9e8add2193dc77220bf1a20bf7c5288 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 18 Aug 2025 18:27:26 +0200 Subject: [PATCH 103/744] title_bar: Fix screensharing errors not being shown to the user (#36424) Release Notes: - N/A --- crates/title_bar/src/collab.rs | 95 ++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index 74d60a6d66b774692c2d6128783c9df24f358b93..b458c64b5f32c11ffb9a6840e374c7b6132212ab 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -14,7 +14,6 @@ use ui::{ Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Divider, DividerColor, Facepile, PopoverMenu, SplitButton, SplitButtonStyle, TintColor, Tooltip, prelude::*, }; -use util::maybe; use workspace::notifications::DetachAndPromptErr; use crate::TitleBar; @@ -32,52 +31,59 @@ actions!( ); fn toggle_screen_sharing( - screen: Option>, + screen: anyhow::Result>>, window: &mut Window, cx: &mut App, ) { let call = ActiveCall::global(cx).read(cx); - if let Some(room) = call.room().cloned() { - let toggle_screen_sharing = room.update(cx, |room, cx| { - let clicked_on_currently_shared_screen = - room.shared_screen_id().is_some_and(|screen_id| { - Some(screen_id) - == screen - .as_deref() - .and_then(|s| s.metadata().ok().map(|meta| meta.id)) - }); - let should_unshare_current_screen = room.is_sharing_screen(); - let unshared_current_screen = should_unshare_current_screen.then(|| { - telemetry::event!( - "Screen Share Disabled", - room_id = room.id(), - channel_id = room.channel_id(), - ); - room.unshare_screen(clicked_on_currently_shared_screen || screen.is_none(), cx) - }); - if let Some(screen) = screen { - if !should_unshare_current_screen { + let toggle_screen_sharing = match screen { + Ok(screen) => { + let Some(room) = call.room().cloned() else { + return; + }; + let toggle_screen_sharing = room.update(cx, |room, cx| { + let clicked_on_currently_shared_screen = + room.shared_screen_id().is_some_and(|screen_id| { + Some(screen_id) + == screen + .as_deref() + .and_then(|s| s.metadata().ok().map(|meta| meta.id)) + }); + let should_unshare_current_screen = room.is_sharing_screen(); + let unshared_current_screen = should_unshare_current_screen.then(|| { telemetry::event!( - "Screen Share Enabled", + "Screen Share Disabled", room_id = room.id(), channel_id = room.channel_id(), ); - } - cx.spawn(async move |room, cx| { - unshared_current_screen.transpose()?; - if !clicked_on_currently_shared_screen { - room.update(cx, |room, cx| room.share_screen(screen, cx))? - .await - } else { - Ok(()) + room.unshare_screen(clicked_on_currently_shared_screen || screen.is_none(), cx) + }); + if let Some(screen) = screen { + if !should_unshare_current_screen { + telemetry::event!( + "Screen Share Enabled", + room_id = room.id(), + channel_id = room.channel_id(), + ); } - }) - } else { - Task::ready(Ok(())) - } - }); - toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", window, cx, |e, _, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e))); - } + cx.spawn(async move |room, cx| { + unshared_current_screen.transpose()?; + if !clicked_on_currently_shared_screen { + room.update(cx, |room, cx| room.share_screen(screen, cx))? + .await + } else { + Ok(()) + } + }) + } else { + Task::ready(Ok(())) + } + }); + toggle_screen_sharing + } + Err(e) => Task::ready(Err(e)), + }; + toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", window, cx, |e, _, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e))); } fn toggle_mute(_: &ToggleMute, cx: &mut App) { @@ -483,9 +489,8 @@ impl TitleBar { let screen = if should_share { cx.update(|_, cx| pick_default_screen(cx))?.await } else { - None + Ok(None) }; - cx.update(|window, cx| toggle_screen_sharing(screen, window, cx))?; Result::<_, anyhow::Error>::Ok(()) @@ -571,7 +576,7 @@ impl TitleBar { selectable: true, documentation_aside: None, handler: Rc::new(move |_, window, cx| { - toggle_screen_sharing(Some(screen.clone()), window, cx); + toggle_screen_sharing(Ok(Some(screen.clone())), window, cx); }), }); } @@ -585,11 +590,11 @@ impl TitleBar { } /// Picks the screen to share when clicking on the main screen sharing button. -fn pick_default_screen(cx: &App) -> Task>> { +fn pick_default_screen(cx: &App) -> Task>>> { let source = cx.screen_capture_sources(); cx.spawn(async move |_| { - let available_sources = maybe!(async move { source.await? }).await.ok()?; - available_sources + let available_sources = source.await??; + Ok(available_sources .iter() .find(|it| { it.as_ref() @@ -597,6 +602,6 @@ fn pick_default_screen(cx: &App) -> Task>> { .is_ok_and(|meta| meta.is_main.unwrap_or_default()) }) .or_else(|| available_sources.iter().next()) - .cloned() + .cloned()) }) } From fa61c3e24d8893a8a62ba0e46dba48e9cc4ae8bd Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 18 Aug 2025 13:27:23 -0400 Subject: [PATCH 104/744] gpui: Fix typo in `handle_gpui_events` (#36431) This PR fixes a typo I noticed in the `handle_gpui_events` method name. Release Notes: - N/A --- crates/gpui/src/platform/windows/platform.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index c1fb0cabc4fcf5759aedbdc8c045fdaa354fd2b3..ee0babf7cb65b9b680cd6111bb7b9d4bf06b410a 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -227,7 +227,7 @@ impl WindowsPlatform { | WM_GPUI_CLOSE_ONE_WINDOW | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD | WM_GPUI_DOCK_MENU_ACTION => { - if self.handle_gpui_evnets(msg.message, msg.wParam, msg.lParam, &msg) { + if self.handle_gpui_events(msg.message, msg.wParam, msg.lParam, &msg) { return; } } @@ -240,7 +240,7 @@ impl WindowsPlatform { } // Returns true if the app should quit. - fn handle_gpui_evnets( + fn handle_gpui_events( &self, message: u32, wparam: WPARAM, From 3a3df5c0118e942893dd3f12aa0c2f734ffae0af Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 18 Aug 2025 15:48:02 -0400 Subject: [PATCH 105/744] gpui: Add support for custom prompt text in PathPromptOptions (#36410) This will be used to improve the clarity of the git clone UI ### MacOS image ### Windows image ### Linux Screenshot From 2025-08-18 15-32-06 Release Notes: - N/A --- crates/extensions_ui/src/extensions_ui.rs | 1 + crates/git_ui/src/git_panel.rs | 1 + crates/gpui/src/platform.rs | 4 +++- crates/gpui/src/platform/linux/platform.rs | 1 + crates/gpui/src/platform/mac/platform.rs | 6 ++++++ crates/gpui/src/platform/windows/platform.rs | 6 ++++++ crates/gpui/src/shared_string.rs | 5 +++++ crates/workspace/src/workspace.rs | 3 +++ crates/zed/src/zed.rs | 2 ++ 9 files changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 49159339205ede0eb2d2db8b16a1c235fcd84303..7c7f9e68365dc45445f085ded87e693a83c1f033 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -116,6 +116,7 @@ pub fn init(cx: &mut App) { files: false, directories: true, multiple: false, + prompt: None, }, DirectoryLister::Local( workspace.project().clone(), diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index b346f4d2165a8d19d2ab10decd18c1e6024a9cdf..754812cbdfe9dc95b6e9fdf58813043af5c17e24 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2088,6 +2088,7 @@ impl GitPanel { files: false, directories: true, multiple: false, + prompt: Some("Select as Repository Destination".into()), }); let workspace = self.workspace.clone(); diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index bf6ce6870363d432cd49392292b8dda7bb51834d..ffd68d60e6f5a87443d099ffeeb1d856ab5e910f 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1278,7 +1278,7 @@ pub enum WindowBackgroundAppearance { } /// The options that can be configured for a file dialog prompt -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] pub struct PathPromptOptions { /// Should the prompt allow files to be selected? pub files: bool, @@ -1286,6 +1286,8 @@ pub struct PathPromptOptions { pub directories: bool, /// Should the prompt allow multiple files to be selected? pub multiple: bool, + /// The prompt to show to a user when selecting a path + pub prompt: Option, } /// What kind of prompt styling to show diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 31d445be5274309f2e84a0e8df0a446cdb79736b..86e5a79e8ae50a39841acd1a05df42d84ba369d9 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -294,6 +294,7 @@ impl Platform for P { let request = match ashpd::desktop::file_chooser::OpenFileRequest::default() .modal(true) .title(title) + .accept_label(options.prompt.as_ref().map(crate::SharedString::as_str)) .multiple(options.multiple) .directory(options.directories) .send() diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 533423229cf2448ea70cf0140d5e3d6bc77fb32a..79177fb2c9cf616b7208af6060b31afec03926bd 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -705,6 +705,7 @@ impl Platform for MacPlatform { panel.setCanChooseDirectories_(options.directories.to_objc()); panel.setCanChooseFiles_(options.files.to_objc()); panel.setAllowsMultipleSelection_(options.multiple.to_objc()); + panel.setCanCreateDirectories(true.to_objc()); panel.setResolvesAliases_(false.to_objc()); let done_tx = Cell::new(Some(done_tx)); @@ -730,6 +731,11 @@ impl Platform for MacPlatform { } }); let block = block.copy(); + + if let Some(prompt) = options.prompt { + let _: () = msg_send![panel, setPrompt: ns_string(&prompt)]; + } + let _: () = msg_send![panel, beginWithCompletionHandler: block]; } }) diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index ee0babf7cb65b9b680cd6111bb7b9d4bf06b410a..856187fa5719cfc95364ec89b521074820b046c7 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -787,6 +787,12 @@ fn file_open_dialog( unsafe { folder_dialog.SetOptions(dialog_options)?; + + if let Some(prompt) = options.prompt { + let prompt: &str = &prompt; + folder_dialog.SetOkButtonLabel(&HSTRING::from(prompt))?; + } + if folder_dialog.Show(window).is_err() { // User cancelled return Ok(None); diff --git a/crates/gpui/src/shared_string.rs b/crates/gpui/src/shared_string.rs index c325f98cd243121264875d7a9452308772d49e86..a34b7502f006b3d01323d58b9a1f499bd79ffd69 100644 --- a/crates/gpui/src/shared_string.rs +++ b/crates/gpui/src/shared_string.rs @@ -23,6 +23,11 @@ impl SharedString { pub fn new(str: impl Into>) -> Self { SharedString(ArcCow::Owned(str.into())) } + + /// Get a &str from the underlying string. + pub fn as_str(&self) -> &str { + &self.0 + } } impl JsonSchema for SharedString { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1eaa125ba5e221f2d86cefb722883b8d165a0df2..02eac1665bdac207f2f0d1de00603c276a441d7c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -561,6 +561,7 @@ pub fn init(app_state: Arc, cx: &mut App) { files: true, directories: true, multiple: true, + prompt: None, }, cx, ); @@ -578,6 +579,7 @@ pub fn init(app_state: Arc, cx: &mut App) { files: true, directories, multiple: true, + prompt: None, }, cx, ); @@ -2655,6 +2657,7 @@ impl Workspace { files: false, directories: true, multiple: true, + prompt: None, }, DirectoryLister::Project(self.project.clone()), window, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index cfafbb70f05b63220841b27712d3d9ea1b30f5f8..6d5aecba7035881b6e5e604759de865a01131580 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -645,6 +645,7 @@ fn register_actions( files: true, directories: true, multiple: true, + prompt: None, }, DirectoryLister::Local( workspace.project().clone(), @@ -685,6 +686,7 @@ fn register_actions( files: true, directories: true, multiple: true, + prompt: None, }, DirectoryLister::Project(workspace.project().clone()), window, From 50819a9d208917344d913800e818fe37e71974a8 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 18 Aug 2025 15:57:28 -0400 Subject: [PATCH 106/744] client: Parse auth callback query parameters before showing sign-in success page (#36440) This PR fixes an issue where we would redirect the user's browser to the sign-in success page even if the OAuth callback was malformed. We now parse the OAuth callback parameters from the query string and only redirect to the sign-in success page when they are valid. Release Notes: - Updated the sign-in flow to not show the sign-in success page prematurely. --- Cargo.lock | 1 + Cargo.toml | 1 + crates/client/Cargo.toml | 1 + crates/client/src/client.rs | 24 +++++++++++++----------- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98f10eff419095a1400cf8efdf02ba2fef71b499..3158a61ad8368391fc866871411a985b4b674c23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3070,6 +3070,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "serde_urlencoded", "settings", "sha2", "smol", diff --git a/Cargo.toml b/Cargo.toml index 83d6da5cd7e7e3c8b2b3b90eec85fe280a1556aa..914f9e6837feea4f0c918c9d11a8a0accc0dee94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -582,6 +582,7 @@ serde_json_lenient = { version = "0.2", features = [ "raw_value", ] } serde_repr = "0.1" +serde_urlencoded = "0.7" sha2 = "0.10" shellexpand = "2.1.0" shlex = "1.3.0" diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 365625b44535e474baecf058c98f54aaf05b5e49..5c6d1157fd710de0e1dd160b611c0bd7c6667c4d 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -44,6 +44,7 @@ rpc = { workspace = true, features = ["gpui"] } schemars.workspace = true serde.workspace = true serde_json.workspace = true +serde_urlencoded.workspace = true settings.workspace = true sha2.workspace = true smol.workspace = true diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index f09c012a858e3cf97166dae9dbdbeb3da51b96b6..0f004713566bf0974e880fd41ffcea3b4a331378 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1410,6 +1410,12 @@ impl Client { open_url_tx.send(url).log_err(); + #[derive(Deserialize)] + struct CallbackParams { + pub user_id: String, + pub access_token: String, + } + // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted // access token from the query params. // @@ -1420,17 +1426,13 @@ impl Client { for _ in 0..100 { if let Some(req) = server.recv_timeout(Duration::from_secs(1))? { let path = req.url(); - let mut user_id = None; - let mut access_token = None; let url = Url::parse(&format!("http://example.com{}", path)) .context("failed to parse login notification url")?; - for (key, value) in url.query_pairs() { - if key == "access_token" { - access_token = Some(value.to_string()); - } else if key == "user_id" { - user_id = Some(value.to_string()); - } - } + let callback_params: CallbackParams = + serde_urlencoded::from_str(url.query().unwrap_or_default()) + .context( + "failed to parse sign-in callback query parameters", + )?; let post_auth_url = http.build_url("/native_app_signin_succeeded"); @@ -1445,8 +1447,8 @@ impl Client { ) .context("failed to respond to login http request")?; return Ok(( - user_id.context("missing user_id parameter")?, - access_token.context("missing access_token parameter")?, + callback_params.user_id, + callback_params.access_token, )); } } From 8b89ea1a801af6190b1b6e6557a69fadb08db93f Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 18 Aug 2025 17:40:59 -0300 Subject: [PATCH 107/744] Handle auth for claude (#36442) We'll now use the anthropic provider to get credentials for `claude` and embed its configuration view in the panel when they are not present. Release Notes: - N/A --- Cargo.lock | 3 + crates/acp_thread/Cargo.toml | 1 + crates/acp_thread/src/connection.rs | 27 ++- crates/agent_servers/Cargo.toml | 2 + crates/agent_servers/src/acp/v0.rs | 2 +- crates/agent_servers/src/acp/v1.rs | 8 +- crates/agent_servers/src/claude.rs | 54 ++++-- crates/agent_ui/src/acp/thread_view.rs | 182 +++++++++++++----- crates/agent_ui/src/agent_configuration.rs | 6 +- crates/agent_ui/src/agent_ui.rs | 2 +- .../agent_ui/src/language_model_selector.rs | 2 +- .../src/agent_api_keys_onboarding.rs | 2 +- .../src/agent_panel_onboarding_content.rs | 2 +- crates/gpui/src/subscription.rs | 6 + crates/language_model/src/fake_provider.rs | 15 +- crates/language_model/src/language_model.rs | 14 +- crates/language_model/src/registry.rs | 9 +- .../language_models/src/provider/anthropic.rs | 90 ++++++--- .../language_models/src/provider/bedrock.rs | 7 +- crates/language_models/src/provider/cloud.rs | 7 +- .../src/provider/copilot_chat.rs | 7 +- .../language_models/src/provider/deepseek.rs | 7 +- crates/language_models/src/provider/google.rs | 7 +- .../language_models/src/provider/lmstudio.rs | 7 +- .../language_models/src/provider/mistral.rs | 7 +- crates/language_models/src/provider/ollama.rs | 7 +- .../language_models/src/provider/open_ai.rs | 7 +- .../src/provider/open_ai_compatible.rs | 7 +- .../src/provider/open_router.rs | 7 +- crates/language_models/src/provider/vercel.rs | 7 +- crates/language_models/src/provider/x_ai.rs | 7 +- crates/onboarding/src/ai_setup_page.rs | 6 +- 32 files changed, 400 insertions(+), 124 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3158a61ad8368391fc866871411a985b4b674c23..3bc2b638434a0b8476478809f4f269b4e39b2cbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,7 @@ dependencies = [ "indoc", "itertools 0.14.0", "language", + "language_model", "markdown", "parking_lot", "project", @@ -267,6 +268,8 @@ dependencies = [ "indoc", "itertools 0.14.0", "language", + "language_model", + "language_models", "libc", "log", "nix 0.29.0", diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 2b9a6513c8e91a165bbc51aae3e5b2e831cfb234..173f4c42083bb0fb9a43b7174dad69fb0c6acbe2 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -28,6 +28,7 @@ futures.workspace = true gpui.workspace = true itertools.workspace = true language.workspace = true +language_model.workspace = true markdown.workspace = true parking_lot = { workspace = true, optional = true } project.workspace = true diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index a328499bbc1a9ac47aee2d3421c9fdcceb0e11cd..0d4116321d00d2ac650a8fe2b43d7406a40b520b 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -3,6 +3,7 @@ use agent_client_protocol::{self as acp}; use anyhow::Result; use collections::IndexMap; use gpui::{Entity, SharedString, Task}; +use language_model::LanguageModelProviderId; use project::Project; use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc}; use ui::{App, IconName}; @@ -80,12 +81,34 @@ pub trait AgentSessionResume { } #[derive(Debug)] -pub struct AuthRequired; +pub struct AuthRequired { + pub description: Option, + pub provider_id: Option, +} + +impl AuthRequired { + pub fn new() -> Self { + Self { + description: None, + provider_id: None, + } + } + + pub fn with_description(mut self, description: String) -> Self { + self.description = Some(description); + self + } + + pub fn with_language_model_provider(mut self, provider_id: LanguageModelProviderId) -> Self { + self.provider_id = Some(provider_id); + self + } +} impl Error for AuthRequired {} impl fmt::Display for AuthRequired { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "AuthRequired") + write!(f, "Authentication required") } } diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 81c97c8aa6cc4fa64d017b97ade5ddd535487b81..f894bb15bf7cb66af8aad356ead6443c32763b26 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -27,6 +27,8 @@ futures.workspace = true gpui.workspace = true indoc.workspace = true itertools.workspace = true +language_model.workspace = true +language_models.workspace = true log.workspace = true paths.workspace = true project.workspace = true diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 74647f73133f23681f18da1d2bddb02675c55a22..551e9fa01a6c79c4a8c4d67b9af6996a0235086f 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -437,7 +437,7 @@ impl AgentConnection for AcpConnection { let result = acp_old::InitializeParams::response_from_any(result)?; if !result.is_authenticated { - anyhow::bail!(AuthRequired) + anyhow::bail!(AuthRequired::new()) } cx.update(|cx| { diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index b77b5ef36d26ebec9bae48cfe5c1a36c003e230b..93a5ae757a3fde11660db24e182b453e0fbb9850 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -140,7 +140,13 @@ impl AgentConnection for AcpConnection { .await .map_err(|err| { if err.code == acp::ErrorCode::AUTH_REQUIRED.code { - anyhow!(AuthRequired) + let mut error = AuthRequired::new(); + + if err.message != acp::ErrorCode::AUTH_REQUIRED.message { + error = error.with_description(err.message); + } + + anyhow!(error) } else { anyhow!(err) } diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index d15cc1dd89f8547da03704209af586e87ce8455f..d80d040aad2f989315fabac6e0c9ac1e114a4805 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -3,6 +3,7 @@ pub mod tools; use collections::HashMap; use context_server::listener::McpServerTool; +use language_models::provider::anthropic::AnthropicLanguageModelProvider; use project::Project; use settings::SettingsStore; use smol::process::Child; @@ -30,7 +31,7 @@ use util::{ResultExt, debug_panic}; use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig}; use crate::claude::tools::ClaudeTool; use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings}; -use acp_thread::{AcpThread, AgentConnection}; +use acp_thread::{AcpThread, AgentConnection, AuthRequired}; #[derive(Clone)] pub struct ClaudeCode; @@ -79,6 +80,36 @@ impl AgentConnection for ClaudeAgentConnection { ) -> Task>> { let cwd = cwd.to_owned(); cx.spawn(async move |cx| { + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).claude.clone() + })?; + + let Some(command) = AgentServerCommand::resolve( + "claude", + &[], + Some(&util::paths::home_dir().join(".claude/local/claude")), + settings, + &project, + cx, + ) + .await + else { + anyhow::bail!("Failed to find claude binary"); + }; + + let api_key = + cx.update(AnthropicLanguageModelProvider::api_key)? + .await + .map_err(|err| { + if err.is::() { + anyhow!(AuthRequired::new().with_language_model_provider( + language_model::ANTHROPIC_PROVIDER_ID + )) + } else { + anyhow!(err) + } + })?; + let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid()); let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), cx).await?; @@ -98,23 +129,6 @@ impl AgentConnection for ClaudeAgentConnection { .await?; mcp_config_file.flush().await?; - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).claude.clone() - })?; - - let Some(command) = AgentServerCommand::resolve( - "claude", - &[], - Some(&util::paths::home_dir().join(".claude/local/claude")), - settings, - &project, - cx, - ) - .await - else { - anyhow::bail!("Failed to find claude binary"); - }; - let (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded(); let (outgoing_tx, outgoing_rx) = mpsc::unbounded(); @@ -126,6 +140,7 @@ impl AgentConnection for ClaudeAgentConnection { &command, ClaudeSessionMode::Start, session_id.clone(), + api_key, &mcp_config_path, &cwd, )?; @@ -320,6 +335,7 @@ fn spawn_claude( command: &AgentServerCommand, mode: ClaudeSessionMode, session_id: acp::SessionId, + api_key: language_models::provider::anthropic::ApiKey, mcp_config_path: &Path, root_dir: &Path, ) -> Result { @@ -355,6 +371,8 @@ fn spawn_claude( ClaudeSessionMode::Resume => ["--resume".to_string(), session_id.to_string()], }) .args(command.args.iter().map(|arg| arg.as_str())) + .envs(command.env.iter().flatten()) + .env("ANTHROPIC_API_KEY", api_key.key) .current_dir(root_dir) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2c02027c4decc596151a77e8f29fca5c1c35e412..e2e582081297ea7c54c67b447beb3facb3fc6acf 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,6 +1,7 @@ use acp_thread::{ AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, - LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, UserMessageId, + AuthRequired, LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, + UserMessageId, }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; @@ -18,13 +19,16 @@ use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; use fs::Fs; use gpui::{ - Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, ClipboardItem, EdgesRefinement, - Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, - PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, - TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, - linear_color_stop, linear_gradient, list, percentage, point, prelude::*, pulsating_between, + Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, + EdgesRefinement, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, + MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, + TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, + WindowHandle, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*, + pulsating_between, }; use language::Buffer; + +use language_model::LanguageModelRegistry; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use project::Project; use prompt_store::PromptId; @@ -137,6 +141,9 @@ enum ThreadState { LoadError(LoadError), Unauthenticated { connection: Rc, + description: Option>, + configuration_view: Option, + _subscription: Option, }, ServerExited { status: ExitStatus, @@ -267,19 +274,16 @@ impl AcpThreadView { }; let result = match result.await { - Err(e) => { - let mut cx = cx.clone(); - if e.is::() { - this.update(&mut cx, |this, cx| { - this.thread_state = ThreadState::Unauthenticated { connection }; - cx.notify(); + Err(e) => match e.downcast::() { + Ok(err) => { + cx.update(|window, cx| { + Self::handle_auth_required(this, err, agent, connection, window, cx) }) - .ok(); + .log_err(); return; - } else { - Err(e) } - } + Err(err) => Err(err), + }, Ok(thread) => Ok(thread), }; @@ -345,6 +349,68 @@ impl AcpThreadView { ThreadState::Loading { _task: load_task } } + fn handle_auth_required( + this: WeakEntity, + err: AuthRequired, + agent: Rc, + connection: Rc, + window: &mut Window, + cx: &mut App, + ) { + let agent_name = agent.name(); + let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id { + let registry = LanguageModelRegistry::global(cx); + + let sub = window.subscribe(®istry, cx, { + let provider_id = provider_id.clone(); + let this = this.clone(); + move |_, ev, window, cx| { + if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev { + if &provider_id == updated_provider_id { + this.update(cx, |this, cx| { + this.thread_state = Self::initial_state( + agent.clone(), + this.workspace.clone(), + this.project.clone(), + window, + cx, + ); + cx.notify(); + }) + .ok(); + } + } + } + }); + + let view = registry.read(cx).provider(&provider_id).map(|provider| { + provider.configuration_view( + language_model::ConfigurationViewTargetAgent::Other(agent_name), + window, + cx, + ) + }); + + (view, Some(sub)) + } else { + (None, None) + }; + + this.update(cx, |this, cx| { + this.thread_state = ThreadState::Unauthenticated { + connection, + configuration_view, + description: err + .description + .clone() + .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))), + _subscription: subscription, + }; + cx.notify(); + }) + .ok(); + } + fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context) { if let Some(load_err) = err.downcast_ref::() { self.thread_state = ThreadState::LoadError(load_err.clone()); @@ -369,7 +435,7 @@ impl AcpThreadView { ThreadState::Ready { thread, .. } => thread.read(cx).title(), ThreadState::Loading { .. } => "Loading…".into(), ThreadState::LoadError(_) => "Failed to load".into(), - ThreadState::Unauthenticated { .. } => "Not authenticated".into(), + ThreadState::Unauthenticated { .. } => "Authentication Required".into(), ThreadState::ServerExited { .. } => "Server exited unexpectedly".into(), } } @@ -708,7 +774,7 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) { - let ThreadState::Unauthenticated { ref connection } = self.thread_state else { + let ThreadState::Unauthenticated { ref connection, .. } = self.thread_state else { return; }; @@ -1841,19 +1907,53 @@ impl AcpThreadView { .into_any() } - fn render_pending_auth_state(&self) -> AnyElement { + fn render_auth_required_state( + &self, + connection: &Rc, + description: Option<&Entity>, + configuration_view: Option<&AnyView>, + window: &mut Window, + cx: &Context, + ) -> Div { v_flex() + .p_2() + .gap_2() + .flex_1() .items_center() .justify_center() - .child(self.render_error_agent_logo()) .child( - h_flex() - .mt_4() - .mb_1() + v_flex() + .items_center() .justify_center() - .child(Headline::new("Not Authenticated").size(HeadlineSize::Medium)), + .child(self.render_error_agent_logo()) + .child( + h_flex().mt_4().mb_1().justify_center().child( + Headline::new("Authentication Required").size(HeadlineSize::Medium), + ), + ) + .into_any(), ) - .into_any() + .children(description.map(|desc| { + div().text_ui(cx).text_center().child( + self.render_markdown(desc.clone(), default_markdown_style(false, window, cx)), + ) + })) + .children( + configuration_view + .cloned() + .map(|view| div().px_4().w_full().max_w_128().child(view)), + ) + .child(h_flex().mt_1p5().justify_center().children( + connection.auth_methods().into_iter().map(|method| { + Button::new(SharedString::from(method.id.0.clone()), method.name.clone()) + .on_click({ + let method_id = method.id.clone(); + cx.listener(move |this, _, window, cx| { + this.authenticate(method_id.clone(), window, cx) + }) + }) + }), + )) } fn render_server_exited(&self, status: ExitStatus, _cx: &Context) -> AnyElement { @@ -3347,26 +3447,18 @@ impl Render for AcpThreadView { .on_action(cx.listener(Self::toggle_burn_mode)) .bg(cx.theme().colors().panel_background) .child(match &self.thread_state { - 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.name.clone(), - ) - .on_click({ - let method_id = method.id.clone(); - cx.listener(move |this, _, window, cx| { - this.authenticate(method_id.clone(), window, cx) - }) - }) - }), - )), + ThreadState::Unauthenticated { + connection, + description, + configuration_view, + .. + } => self.render_auth_required_state( + &connection, + description.as_ref(), + configuration_view.as_ref(), + window, + cx, + ), ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)), ThreadState::LoadError(e) => v_flex() .p_2() diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index b4ebb8206c78a3866b4d04cc9e8f5aa714c2c37a..a0584f9e2e67f4c08efe17c499abc9bbaf394eb9 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -137,7 +137,11 @@ impl AgentConfiguration { window: &mut Window, cx: &mut Context, ) { - let configuration_view = provider.configuration_view(window, cx); + let configuration_view = provider.configuration_view( + language_model::ConfigurationViewTargetAgent::ZedAgent, + window, + cx, + ); self.configuration_views_by_provider .insert(provider.id(), configuration_view); } diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index ce1c2203bf2fcd15cc929505f59c5ee08a2ef392..8525d7f9e5d5af9455853256cbf4f345c70f0e0f 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -320,7 +320,7 @@ fn init_language_model_settings(cx: &mut App) { cx.subscribe( &LanguageModelRegistry::global(cx), |_, event: &language_model::Event, cx| match event { - language_model::Event::ProviderStateChanged + language_model::Event::ProviderStateChanged(_) | language_model::Event::AddedProvider(_) | language_model::Event::RemovedProvider(_) => { update_active_language_model_from_settings(cx); diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index bb8514a224dd1af3c4668be87d8a02e1d3a0e9be..fa8ca490d882eab7e5c363bb133a86e831622124 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -104,7 +104,7 @@ impl LanguageModelPickerDelegate { window, |picker, _, event, window, cx| { match event { - language_model::Event::ProviderStateChanged + language_model::Event::ProviderStateChanged(_) | language_model::Event::AddedProvider(_) | language_model::Event::RemovedProvider(_) => { let query = picker.query(cx); diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs index b55ad4c89549a8843fe2d8273da60236400cb565..0a34a290685746671a49a108ff770bde17fd08db 100644 --- a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs +++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs @@ -11,7 +11,7 @@ impl ApiKeysWithProviders { cx.subscribe( &LanguageModelRegistry::global(cx), |this: &mut Self, _registry, event: &language_model::Event, cx| match event { - language_model::Event::ProviderStateChanged + language_model::Event::ProviderStateChanged(_) | language_model::Event::AddedProvider(_) | language_model::Event::RemovedProvider(_) => { this.configured_providers = Self::compute_configured_providers(cx) diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index f1629eeff81ef51bf2ff823eef0db64c1585a669..23810b74f3251629cfb3695ea651a7fc40f4c955 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -25,7 +25,7 @@ impl AgentPanelOnboarding { cx.subscribe( &LanguageModelRegistry::global(cx), |this: &mut Self, _registry, event: &language_model::Event, cx| match event { - language_model::Event::ProviderStateChanged + language_model::Event::ProviderStateChanged(_) | language_model::Event::AddedProvider(_) | language_model::Event::RemovedProvider(_) => { this.configured_providers = Self::compute_available_providers(cx) diff --git a/crates/gpui/src/subscription.rs b/crates/gpui/src/subscription.rs index a584f1a45f82094ce9b867bc5f43805c48f93ebe..bd869f8d32cdfc81917fc2287b7dc62fac7d727d 100644 --- a/crates/gpui/src/subscription.rs +++ b/crates/gpui/src/subscription.rs @@ -201,3 +201,9 @@ impl Drop for Subscription { } } } + +impl std::fmt::Debug for Subscription { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Subscription").finish() + } +} diff --git a/crates/language_model/src/fake_provider.rs b/crates/language_model/src/fake_provider.rs index a9c7d5c0343295ff02d9d693f2cdbe3d92f1e07d..67fba4488700a637b643294fb99aa905b41f7480 100644 --- a/crates/language_model/src/fake_provider.rs +++ b/crates/language_model/src/fake_provider.rs @@ -1,8 +1,8 @@ use crate::{ - AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, - LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, - LanguageModelToolChoice, + AuthenticateError, ConfigurationViewTargetAgent, LanguageModel, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, LanguageModelToolChoice, }; use futures::{FutureExt, StreamExt, channel::mpsc, future::BoxFuture, stream::BoxStream}; use gpui::{AnyView, App, AsyncApp, Entity, Task, Window}; @@ -62,7 +62,12 @@ impl LanguageModelProvider for FakeLanguageModelProvider { Task::ready(Ok(())) } - fn configuration_view(&self, _window: &mut Window, _: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: ConfigurationViewTargetAgent, + _window: &mut Window, + _: &mut App, + ) -> AnyView { unimplemented!() } diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 1637d2de8a3c14b910ea345c03a4eb5db13df28d..70e42cb02d066e7f2224a7133131d286e65b4b0c 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -634,7 +634,12 @@ pub trait LanguageModelProvider: 'static { } fn is_authenticated(&self, cx: &App) -> bool; fn authenticate(&self, cx: &mut App) -> Task>; - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView; + fn configuration_view( + &self, + target_agent: ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView; fn must_accept_terms(&self, _cx: &App) -> bool { false } @@ -648,6 +653,13 @@ pub trait LanguageModelProvider: 'static { fn reset_credentials(&self, cx: &mut App) -> Task>; } +#[derive(Default, Clone, Copy)] +pub enum ConfigurationViewTargetAgent { + #[default] + ZedAgent, + Other(&'static str), +} + #[derive(PartialEq, Eq)] pub enum LanguageModelProviderTosView { /// When there are some past interactions in the Agent Panel. diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 7cf071808a2c0d95bf9aa5a41eaa260cff533d57..078b90a291bb993c443b6948ebce8ac90e5c83ad 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -107,7 +107,7 @@ pub enum Event { InlineAssistantModelChanged, CommitMessageModelChanged, ThreadSummaryModelChanged, - ProviderStateChanged, + ProviderStateChanged(LanguageModelProviderId), AddedProvider(LanguageModelProviderId), RemovedProvider(LanguageModelProviderId), } @@ -148,8 +148,11 @@ impl LanguageModelRegistry { ) { let id = provider.id(); - let subscription = provider.subscribe(cx, |_, cx| { - cx.emit(Event::ProviderStateChanged); + let subscription = provider.subscribe(cx, { + let id = id.clone(); + move |_, cx| { + cx.emit(Event::ProviderStateChanged(id.clone())); + } }); if let Some(subscription) = subscription { subscription.detach(); diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index ef21e85f711e41722d4ac421ba1d0a89b422b6a6..810d4a5f44ec6ed7c2747b0b0580e3e90844828a 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -15,11 +15,11 @@ use gpui::{ }; use http_client::HttpClient; use language_model::{ - AuthenticateError, LanguageModel, LanguageModelCacheConfiguration, - LanguageModelCompletionError, LanguageModelId, LanguageModelName, LanguageModelProvider, - LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, - LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent, MessageContent, - RateLimiter, Role, + AuthenticateError, ConfigurationViewTargetAgent, LanguageModel, + LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelId, + LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, + LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, + LanguageModelToolResultContent, MessageContent, RateLimiter, Role, }; use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason}; use schemars::JsonSchema; @@ -153,29 +153,14 @@ impl State { return Task::ready(Ok(())); } - let credentials_provider = ::global(cx); - let api_url = AllLanguageModelSettings::get_global(cx) - .anthropic - .api_url - .clone(); + let key = AnthropicLanguageModelProvider::api_key(cx); cx.spawn(async move |this, cx| { - let (api_key, from_env) = if let Ok(api_key) = std::env::var(ANTHROPIC_API_KEY_VAR) { - (api_key, true) - } else { - let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) - .await? - .ok_or(AuthenticateError::CredentialsNotFound)?; - ( - String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?, - false, - ) - }; + let key = key.await?; this.update(cx, |this, cx| { - this.api_key = Some(api_key); - this.api_key_from_env = from_env; + this.api_key = Some(key.key); + this.api_key_from_env = key.from_env; cx.notify(); })?; @@ -184,6 +169,11 @@ impl State { } } +pub struct ApiKey { + pub key: String, + pub from_env: bool, +} + impl AnthropicLanguageModelProvider { pub fn new(http_client: Arc, cx: &mut App) -> Self { let state = cx.new(|cx| State { @@ -206,6 +196,33 @@ impl AnthropicLanguageModelProvider { request_limiter: RateLimiter::new(4), }) } + + pub fn api_key(cx: &mut App) -> Task> { + let credentials_provider = ::global(cx); + let api_url = AllLanguageModelSettings::get_global(cx) + .anthropic + .api_url + .clone(); + + if let Ok(key) = std::env::var(ANTHROPIC_API_KEY_VAR) { + Task::ready(Ok(ApiKey { + key, + from_env: true, + })) + } else { + cx.spawn(async move |cx| { + let (_, api_key) = credentials_provider + .read_credentials(&api_url, &cx) + .await? + .ok_or(AuthenticateError::CredentialsNotFound)?; + + Ok(ApiKey { + key: String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?, + from_env: false, + }) + }) + } + } } impl LanguageModelProviderState for AnthropicLanguageModelProvider { @@ -299,8 +316,13 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { - cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) + fn configuration_view( + &self, + target_agent: ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { + cx.new(|cx| ConfigurationView::new(self.state.clone(), target_agent, window, cx)) .into() } @@ -902,12 +924,18 @@ struct ConfigurationView { api_key_editor: Entity, state: gpui::Entity, load_credentials_task: Option>, + target_agent: ConfigurationViewTargetAgent, } impl ConfigurationView { const PLACEHOLDER_TEXT: &'static str = "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; - fn new(state: gpui::Entity, window: &mut Window, cx: &mut Context) -> Self { + fn new( + state: gpui::Entity, + target_agent: ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut Context, + ) -> Self { cx.observe(&state, |_, _, cx| { cx.notify(); }) @@ -939,6 +967,7 @@ impl ConfigurationView { }), state, load_credentials_task, + target_agent, } } @@ -1012,7 +1041,10 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's agent with Anthropic, you need to add an API key. Follow these steps:")) + .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent { + ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic", + ConfigurationViewTargetAgent::Other(agent) => agent, + }))) .child( List::new() .child( @@ -1023,7 +1055,7 @@ impl Render for ConfigurationView { ) ) .child( - InstructionListItem::text_only("Paste your API key below and hit enter to start using the assistant") + InstructionListItem::text_only("Paste your API key below and hit enter to start using the agent") ) ) .child( diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 6df96c5c566aac6f23af837491292cc89a56c74a..4e6744d745df9a282b952e9fb46cac9f54ac149c 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -348,7 +348,12 @@ impl LanguageModelProvider for BedrockLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index c1337399f993a5a9be247cec31c5b048cedbf731..c3f4399832dc9d71cf54bb59f8e825a62a20a56f 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -391,7 +391,12 @@ impl LanguageModelProvider for CloudLanguageModelProvider { Task::ready(Ok(())) } - fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + _: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|_| ConfigurationView::new(self.state.clone())) .into() } diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index 73f73a9a313c764d45adfd14910efd801a472f1c..eb12c0056f871a4d9eb053c51455081572868aef 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -176,7 +176,12 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider { Task::ready(Err(err.into())) } - fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + _: &mut Window, + cx: &mut App, + ) -> AnyView { let state = self.state.clone(); cx.new(|cx| ConfigurationView::new(state, cx)).into() } diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index a568ef4034193b5b1078d2ec4907d18fb0762efa..2b30d456ee3beb3f2a07afbdef5233c0905b70b0 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -229,7 +229,12 @@ impl LanguageModelProvider for DeepSeekLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index b287e8181a2ac5d04650d799a0cd9b23d51749c2..32f8838df75de7fed44dd96a5bb5356cf4a1dbcb 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -277,7 +277,12 @@ impl LanguageModelProvider for GoogleLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/lmstudio.rs b/crates/language_models/src/provider/lmstudio.rs index 36a32ab941ec65eb790a59ba3a7ed4fe3e6eb575..7ac08f2c1529fbffc68e2d49c1e1765181fd51ef 100644 --- a/crates/language_models/src/provider/lmstudio.rs +++ b/crates/language_models/src/provider/lmstudio.rs @@ -226,7 +226,12 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, _window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + _window: &mut Window, + cx: &mut App, + ) -> AnyView { let state = self.state.clone(); cx.new(|cx| ConfigurationView::new(state, cx)).into() } diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index 4a0d740334e38b9f5ea512344161fe5ca3f8db71..e1d55801eb8bdf2703c51b0ca0fb837a6b03462a 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -243,7 +243,12 @@ impl LanguageModelProvider for MistralLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index 0c2b1107b18cf72f70e46c195e7c61bfae607285..93844542ea68f68479d74b427493cd33cb5e5dc5 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -255,7 +255,12 @@ impl LanguageModelProvider for OllamaLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { let state = self.state.clone(); cx.new(|cx| ConfigurationView::new(state, window, cx)) .into() diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index eaf8d885b304ea0d6526c8e61d3a71a467f41376..04d89f2db19f0dee56a6741600f0cb510b7ecadf 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -233,7 +233,12 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index e2d3adb198a15904baa8ebdd6645ab79a20ed249..c6b980c3ec4bef89bf924df3616075450d54c36d 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -243,7 +243,12 @@ impl LanguageModelProvider for OpenAiCompatibleLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 3a492086f16e1f9b53a196b7bb2e9817a3cac0e7..5d8bace6d397d7c36eee29138ea69237014c5604 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -306,7 +306,12 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 9f447cb68b96739b0ab2997ed25e6307c6db5ba0..98e4f60b6bb8c146741058611ec6e55d01a505eb 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -230,7 +230,12 @@ impl LanguageModelProvider for VercelLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index fed6fe92bfaac45a810f2531e934310f248f17b6..2b8238cc5c7db3aadb879912c14f1a6dd814a82c 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -230,7 +230,12 @@ impl LanguageModelProvider for XAiLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index bb1932bdf21ee9c927f085c8d5ad0a7cbd4c7fbd..d700fa08bd75ce658fdca606bbd204132da170cd 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -329,7 +329,11 @@ impl AiConfigurationModal { cx: &mut Context, ) -> Self { let focus_handle = cx.focus_handle(); - let configuration_view = selected_provider.configuration_view(window, cx); + let configuration_view = selected_provider.configuration_view( + language_model::ConfigurationViewTargetAgent::ZedAgent, + window, + cx, + ); Self { focus_handle, From c5991e74bb6f305c299684dc7ac3f6ee9055efcd Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 19 Aug 2025 02:27:40 +0530 Subject: [PATCH 108/744] project: Handle `textDocument/didSave` and `textDocument/didChange` (un)registration and usage correctly (#36441) Follow-up of https://github.com/zed-industries/zed/pull/35306 This PR contains two changes: Both changes are inspired from: https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/textSynchronization.ts 1. Handling `textDocument/didSave` and `textDocument/didChange` registration and unregistration correctly: ```rs #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] #[serde(untagged)] pub enum TextDocumentSyncCapability { Kind(TextDocumentSyncKind), Options(TextDocumentSyncOptions), } ``` - `textDocument/didSave` dynamic registration contains "includeText" - `textDocument/didChange` dynamic registration contains "syncKind" While storing this to Language Server, we use `TextDocumentSyncCapability::Options` instead of `TextDocumentSyncCapability::Kind` since it also include [change](https://github.com/gluon-lang/lsp-types/blob/be7336e92a6ad23f214df19bcdceab17f39531a9/src/lib.rs#L1714-L1717) field as `TextDocumentSyncCapability::Kind` as well as [save](https://github.com/gluon-lang/lsp-types/blob/be7336e92a6ad23f214df19bcdceab17f39531a9/src/lib.rs#L1727-L1729) field as `TextDocumentSyncSaveOptions`. This way while registering or unregistering both of them, we don't accidentaly mess with other data. So, if at intialization we end up getting `TextDocumentSyncCapability::Kind` and we receive any above kind of dynamic registration, we change `TextDocumentSyncCapability::Kind` to `TextDocumentSyncCapability::Options` so we can store more data anyway. 2. Modify `include_text` method to only depend on `TextDocumentSyncSaveOptions`, instead of depending on `TextDocumentSyncKind`. Idea behind this is, `TextDocumentSyncSaveOptions` should be responsible for "textDocument/didSave" notification, and `TextDocumentSyncKind` should be responsible for "textDocument/didChange", which it already is: https://github.com/zed-industries/zed/blob/4b79eade1da2f5f7dfa18208cf882c8e6ca8a97f/crates/project/src/lsp_store.rs#L7324-L7331 Release Notes: - N/A --- crates/project/src/lsp_store.rs | 72 +++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 802b304e94e7d47616b438f10b1138e39a05b6c7..11c78aad8d1080abee0138cacc16889232845f82 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11820,8 +11820,28 @@ impl LspStore { .transpose()? { server.update_capabilities(|capabilities| { + let mut sync_options = + Self::take_text_document_sync_options(capabilities); + sync_options.change = Some(sync_kind); capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)); + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + }); + notify_server_capabilities_updated(&server, cx); + } + } + "textDocument/didSave" => { + if let Some(save_options) = reg + .register_options + .and_then(|opts| opts.get("includeText").cloned()) + .map(serde_json::from_value::) + .transpose()? + { + server.update_capabilities(|capabilities| { + let mut sync_options = + Self::take_text_document_sync_options(capabilities); + sync_options.save = Some(save_options); + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); }); notify_server_capabilities_updated(&server, cx); } @@ -11973,7 +11993,19 @@ impl LspStore { } "textDocument/didChange" => { server.update_capabilities(|capabilities| { - capabilities.text_document_sync = None; + let mut sync_options = Self::take_text_document_sync_options(capabilities); + sync_options.change = None; + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/didSave" => { + server.update_capabilities(|capabilities| { + let mut sync_options = Self::take_text_document_sync_options(capabilities); + sync_options.save = None; + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); }); notify_server_capabilities_updated(&server, cx); } @@ -12001,6 +12033,20 @@ impl LspStore { Ok(()) } + + fn take_text_document_sync_options( + capabilities: &mut lsp::ServerCapabilities, + ) -> lsp::TextDocumentSyncOptions { + match capabilities.text_document_sync.take() { + Some(lsp::TextDocumentSyncCapability::Options(sync_options)) => sync_options, + Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)) => { + let mut sync_options = lsp::TextDocumentSyncOptions::default(); + sync_options.change = Some(sync_kind); + sync_options + } + None => lsp::TextDocumentSyncOptions::default(), + } + } } // Registration with empty capabilities should be ignored. @@ -13103,24 +13149,18 @@ async fn populate_labels_for_symbols( fn include_text(server: &lsp::LanguageServer) -> Option { match server.capabilities().text_document_sync.as_ref()? { - lsp::TextDocumentSyncCapability::Kind(kind) => match *kind { - lsp::TextDocumentSyncKind::NONE => None, - lsp::TextDocumentSyncKind::FULL => Some(true), - lsp::TextDocumentSyncKind::INCREMENTAL => Some(false), - _ => None, - }, - lsp::TextDocumentSyncCapability::Options(options) => match options.save.as_ref()? { - lsp::TextDocumentSyncSaveOptions::Supported(supported) => { - if *supported { - Some(true) - } else { - None - } - } + lsp::TextDocumentSyncCapability::Options(opts) => match opts.save.as_ref()? { + // Server wants didSave but didn't specify includeText. + lsp::TextDocumentSyncSaveOptions::Supported(true) => Some(false), + // Server doesn't want didSave at all. + lsp::TextDocumentSyncSaveOptions::Supported(false) => None, + // Server provided SaveOptions. lsp::TextDocumentSyncSaveOptions::SaveOptions(save_options) => { Some(save_options.include_text.unwrap_or(false)) } }, + // We do not have any save info. Kind affects didChange only. + lsp::TextDocumentSyncCapability::Kind(_) => None, } } From 97f784dedf58a1f1337f6824918d73deb5abab97 Mon Sep 17 00:00:00 2001 From: localcc Date: Mon, 18 Aug 2025 23:30:02 +0200 Subject: [PATCH 109/744] Fix early dispatch crash on windows (#36445) Closes #36384 Release Notes: - N/A --- crates/gpui/src/platform/windows/events.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 4ab257d27a69fc5fed458655150e1c09c3ebbba8..9b25ab360eaa0e9238f231fadfb2e0c7c86573ee 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -100,6 +100,7 @@ impl WindowsWindowInner { 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_SHOWWINDOW => self.handle_window_visibility_changed(handle, wparam), WM_GPUI_CURSOR_STYLE_CHANGED => self.handle_cursor_changed(lparam), WM_GPUI_FORCE_UPDATE_WINDOW => self.draw_window(handle, true), _ => None, @@ -1160,6 +1161,13 @@ impl WindowsWindowInner { Some(0) } + fn handle_window_visibility_changed(&self, handle: HWND, wparam: WPARAM) -> Option { + if wparam.0 == 1 { + self.draw_window(handle, false); + } + None + } + 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. From eecf142f06a1f7d073242946b98e389dd94d0011 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Tue, 19 Aug 2025 00:49:22 +0300 Subject: [PATCH 110/744] Explicitly allow `clippy::new_without_default` style lint (#36434) Discussed in #36432 Release Notes: - N/A --------- Co-authored-by: Marshall Bowers --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 914f9e6837feea4f0c918c9d11a8a0accc0dee94..3edd8d802c6e949b0040a45c7fda61fd340abbf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -832,6 +832,8 @@ new_ret_no_self = { level = "allow" } # compared to Iterator::next. Yet, clippy complains about those. should_implement_trait = { level = "allow" } let_underscore_future = "allow" +# It doesn't make sense to implement `Default` unilaterally. +new_without_default = "allow" # in Rust it can be very tedious to reduce argument count without # running afoul of the borrow checker. From 9e0e233319d06956bb28fb0609bb843e89d1a812 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:54:35 +0200 Subject: [PATCH 111/744] Fix clippy::needless_borrow lint violations (#36444) Release Notes: - N/A --- Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 8 +- crates/acp_thread/src/diff.rs | 10 +-- crates/acp_thread/src/mention.rs | 2 +- crates/action_log/src/action_log.rs | 8 +- .../src/activity_indicator.rs | 6 +- crates/agent/src/thread.rs | 16 ++-- crates/agent2/src/agent.rs | 2 +- crates/agent2/src/templates.rs | 2 +- crates/agent2/src/thread.rs | 10 +-- .../src/tools/context_server_registry.rs | 2 +- crates/agent2/src/tools/edit_file_tool.rs | 2 +- crates/agent2/src/tools/terminal_tool.rs | 2 +- crates/agent_servers/src/acp.rs | 4 +- crates/agent_servers/src/claude.rs | 2 +- .../agent_ui/src/acp/completion_provider.rs | 4 +- crates/agent_ui/src/acp/message_editor.rs | 2 +- crates/agent_ui/src/acp/model_selector.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 16 ++-- crates/agent_ui/src/active_thread.rs | 6 +- crates/agent_ui/src/agent_diff.rs | 20 ++--- crates/agent_ui/src/agent_panel.rs | 6 +- crates/agent_ui/src/buffer_codegen.rs | 6 +- .../src/context_picker/completion_provider.rs | 4 +- .../src/context_picker/file_context_picker.rs | 4 +- .../context_picker/symbol_context_picker.rs | 2 +- .../context_picker/thread_context_picker.rs | 6 +- crates/agent_ui/src/context_strip.rs | 4 +- crates/agent_ui/src/inline_assistant.rs | 8 +- crates/agent_ui/src/inline_prompt_editor.rs | 2 +- .../agent_ui/src/language_model_selector.rs | 2 +- crates/agent_ui/src/message_editor.rs | 11 ++- crates/agent_ui/src/slash_command_picker.rs | 2 +- crates/agent_ui/src/terminal_codegen.rs | 2 +- crates/agent_ui/src/ui/context_pill.rs | 6 +- .../src/assistant_context.rs | 16 ++-- .../src/assistant_context_tests.rs | 2 +- .../src/context_server_command.rs | 2 +- .../src/diagnostics_command.rs | 6 +- crates/assistant_tools/src/edit_file_tool.rs | 8 +- crates/assistant_tools/src/grep_tool.rs | 22 ++--- .../src/project_notifications_tool.rs | 2 +- crates/assistant_tools/src/terminal_tool.rs | 2 +- crates/breadcrumbs/src/breadcrumbs.rs | 2 +- crates/buffer_diff/src/buffer_diff.rs | 26 +++--- crates/cli/src/main.rs | 4 +- crates/client/src/client.rs | 16 ++-- crates/collab/src/db/queries/projects.rs | 6 +- crates/collab/src/db/queries/rooms.rs | 6 +- .../src/chat_panel/message_editor.rs | 4 +- crates/collab_ui/src/collab_panel.rs | 2 +- crates/collab_ui/src/notification_panel.rs | 2 +- crates/context_server/src/listener.rs | 4 +- crates/context_server/src/types.rs | 2 +- crates/copilot/src/copilot_chat.rs | 4 +- crates/dap/src/adapters.rs | 2 +- crates/dap_adapters/src/go.rs | 2 +- crates/dap_adapters/src/javascript.rs | 2 +- crates/dap_adapters/src/python.rs | 6 +- crates/db/src/db.rs | 10 +-- crates/debugger_tools/src/dap_log.rs | 8 +- crates/debugger_ui/src/debugger_panel.rs | 18 ++-- crates/debugger_ui/src/new_process_modal.rs | 13 +-- crates/debugger_ui/src/persistence.rs | 2 +- crates/debugger_ui/src/session/running.rs | 12 +-- .../src/session/running/breakpoint_list.rs | 4 +- .../src/session/running/console.rs | 4 +- .../src/session/running/memory_view.rs | 4 +- .../src/session/running/variable_list.rs | 4 +- crates/debugger_ui/src/tests/attach_modal.rs | 4 +- .../src/tests/new_process_modal.rs | 2 +- crates/diagnostics/src/diagnostic_renderer.rs | 4 +- crates/diagnostics/src/diagnostics.rs | 4 +- crates/docs_preprocessor/src/main.rs | 6 +- crates/editor/src/clangd_ext.rs | 2 +- crates/editor/src/code_completion_tests.rs | 4 +- crates/editor/src/code_context_menus.rs | 2 +- .../src/display_map/custom_highlights.rs | 8 +- crates/editor/src/display_map/invisibles.rs | 6 +- crates/editor/src/display_map/wrap_map.rs | 2 +- crates/editor/src/editor.rs | 67 +++++++-------- crates/editor/src/editor_tests.rs | 42 ++++----- crates/editor/src/element.rs | 22 ++--- crates/editor/src/hover_links.rs | 2 +- crates/editor/src/items.rs | 12 +-- crates/editor/src/jsx_tag_auto_close.rs | 2 +- crates/editor/src/lsp_colors.rs | 2 +- crates/editor/src/lsp_ext.rs | 2 +- crates/editor/src/mouse_context_menu.rs | 4 +- crates/editor/src/movement.rs | 12 +-- crates/editor/src/proposed_changes_editor.rs | 6 +- crates/editor/src/rust_analyzer_ext.rs | 12 +-- crates/editor/src/signature_help.rs | 2 +- crates/editor/src/test.rs | 2 +- crates/eval/src/eval.rs | 2 +- crates/eval/src/example.rs | 4 +- crates/eval/src/instance.rs | 14 +-- crates/extension/src/extension_builder.rs | 2 +- crates/extension_host/src/extension_host.rs | 4 +- crates/extension_host/src/headless_host.rs | 2 +- crates/file_finder/src/file_finder.rs | 10 +-- crates/file_finder/src/file_finder_tests.rs | 4 +- crates/file_finder/src/open_path_prompt.rs | 2 +- crates/fs/src/fs.rs | 6 +- crates/git/src/repository.rs | 8 +- crates/git/src/status.rs | 2 +- .../src/git_hosting_providers.rs | 2 +- .../src/providers/chromium.rs | 2 +- .../src/providers/github.rs | 4 +- crates/git_ui/src/commit_view.rs | 6 +- crates/git_ui/src/conflict_view.rs | 10 +-- crates/git_ui/src/file_diff_view.rs | 8 +- crates/git_ui/src/git_panel.rs | 16 ++-- crates/git_ui/src/picker_prompt.rs | 2 +- crates/git_ui/src/project_diff.rs | 6 +- crates/git_ui/src/text_diff_view.rs | 4 +- crates/gpui/build.rs | 4 +- crates/gpui/examples/input.rs | 4 +- crates/gpui/src/app.rs | 4 +- crates/gpui/src/app/entity_map.rs | 2 +- crates/gpui/src/elements/div.rs | 2 +- crates/gpui/src/inspector.rs | 2 +- crates/gpui/src/key_dispatch.rs | 4 +- crates/gpui/src/keymap.rs | 2 +- crates/gpui/src/path_builder.rs | 2 +- crates/gpui/src/platform.rs | 2 +- crates/gpui/src/platform/linux/platform.rs | 2 +- .../gpui/src/platform/linux/wayland/client.rs | 15 ++-- .../gpui/src/platform/linux/wayland/cursor.rs | 2 +- crates/gpui/src/platform/linux/x11/client.rs | 6 +- crates/gpui/src/platform/linux/x11/event.rs | 4 +- crates/gpui/src/platform/linux/x11/window.rs | 6 +- .../gpui/src/platform/mac/metal_renderer.rs | 16 ++-- crates/gpui/src/platform/mac/platform.rs | 10 +-- crates/gpui/src/platform/mac/window.rs | 8 +- .../gpui/src/platform/windows/direct_write.rs | 4 +- .../src/platform/windows/directx_renderer.rs | 4 +- crates/gpui/src/tab_stop.rs | 2 +- crates/gpui_macros/src/test.rs | 2 +- crates/install_cli/src/install_cli.rs | 2 +- crates/jj/src/jj_store.rs | 2 +- crates/language/src/buffer.rs | 8 +- crates/language/src/language.rs | 2 +- crates/language/src/language_registry.rs | 2 +- crates/language/src/language_settings.rs | 4 +- crates/language/src/syntax_map.rs | 2 +- crates/language/src/text_diff.rs | 8 +- .../src/language_extension.rs | 2 +- crates/language_model/src/request.rs | 6 +- .../language_models/src/provider/anthropic.rs | 6 +- .../language_models/src/provider/bedrock.rs | 8 +- crates/language_models/src/provider/cloud.rs | 2 +- .../language_models/src/provider/deepseek.rs | 6 +- crates/language_models/src/provider/google.rs | 6 +- .../language_models/src/provider/mistral.rs | 6 +- .../language_models/src/provider/open_ai.rs | 6 +- .../src/provider/open_ai_compatible.rs | 6 +- .../src/provider/open_router.rs | 6 +- crates/language_models/src/provider/vercel.rs | 6 +- crates/language_models/src/provider/x_ai.rs | 6 +- crates/language_tools/src/lsp_log.rs | 2 +- crates/languages/src/css.rs | 2 +- crates/languages/src/github_download.rs | 6 +- crates/languages/src/json.rs | 2 +- crates/languages/src/python.rs | 2 +- crates/languages/src/rust.rs | 6 +- crates/languages/src/tailwind.rs | 2 +- crates/languages/src/typescript.rs | 2 +- crates/languages/src/yaml.rs | 2 +- crates/livekit_client/src/test.rs | 4 +- crates/markdown/src/markdown.rs | 2 +- crates/markdown/src/parser.rs | 2 +- .../markdown_preview/src/markdown_renderer.rs | 4 +- crates/migrator/src/migrator.rs | 8 +- crates/multi_buffer/src/anchor.rs | 4 +- crates/multi_buffer/src/multi_buffer.rs | 38 ++++---- crates/multi_buffer/src/multi_buffer_tests.rs | 19 ++-- crates/multi_buffer/src/position.rs | 6 +- crates/onboarding/src/onboarding.rs | 2 +- crates/onboarding/src/welcome.rs | 2 +- crates/outline_panel/src/outline_panel.rs | 86 +++++++++---------- crates/project/src/context_server_store.rs | 6 +- .../project/src/debugger/breakpoint_store.rs | 6 +- crates/project/src/debugger/dap_store.rs | 4 +- crates/project/src/debugger/locators/cargo.rs | 4 +- crates/project/src/debugger/session.rs | 4 +- crates/project/src/environment.rs | 2 +- crates/project/src/git_store.rs | 20 ++--- crates/project/src/git_store/git_traversal.rs | 2 +- crates/project/src/image_store.rs | 2 +- crates/project/src/lsp_command.rs | 2 +- crates/project/src/lsp_store.rs | 26 +++--- crates/project/src/manifest_tree.rs | 2 +- .../project/src/manifest_tree/server_tree.rs | 8 +- crates/project/src/project.rs | 17 ++-- crates/project/src/project_settings.rs | 4 +- crates/project/src/task_inventory.rs | 2 +- crates/project_panel/src/project_panel.rs | 10 +-- crates/project_symbols/src/project_symbols.rs | 2 +- crates/recent_projects/src/remote_servers.rs | 2 +- crates/remote/src/ssh_session.rs | 30 +++---- crates/remote_server/src/headless_project.rs | 4 +- crates/remote_server/src/unix.rs | 9 +- crates/repl/src/notebook/notebook_ui.rs | 2 +- crates/rope/src/chunk.rs | 4 +- crates/search/src/search.rs | 2 +- crates/search/src/search_bar.rs | 2 +- crates/semantic_index/src/summary_index.rs | 4 +- crates/settings/src/keymap_file.rs | 6 +- crates/settings_ui/src/keybindings.rs | 25 +++--- crates/settings_ui/src/ui_components/table.rs | 2 +- crates/streaming_diff/src/streaming_diff.rs | 30 +++---- crates/task/src/vscode_debug_format.rs | 2 +- crates/terminal/src/terminal.rs | 6 +- crates/terminal_view/src/terminal_panel.rs | 6 +- crates/terminal_view/src/terminal_view.rs | 6 +- crates/title_bar/src/title_bar.rs | 4 +- crates/ui/src/components/indent_guides.rs | 4 +- crates/ui/src/components/keybinding.rs | 4 +- crates/vim/src/command.rs | 2 +- crates/vim/src/motion.rs | 10 +-- crates/vim/src/normal/increment.rs | 4 +- crates/vim/src/normal/scroll.rs | 4 +- crates/vim/src/state.rs | 8 +- crates/vim/src/test/neovim_connection.rs | 4 +- crates/vim/src/vim.rs | 6 +- crates/vim/src/visual.rs | 2 +- crates/workspace/src/notifications.rs | 2 +- crates/workspace/src/pane.rs | 5 +- crates/workspace/src/workspace.rs | 22 ++--- crates/worktree/src/worktree.rs | 12 +-- crates/zed/src/main.rs | 32 +++---- crates/zed/src/reliability.rs | 4 +- crates/zed/src/zed.rs | 4 +- crates/zed/src/zed/component_preview.rs | 2 +- .../zed/src/zed/edit_prediction_registry.rs | 4 +- crates/zed/src/zed/open_listener.rs | 4 +- crates/zed/src/zed/quick_action_bar.rs | 2 +- crates/zeta/src/license_detection.rs | 2 +- crates/zeta/src/zeta.rs | 14 +-- crates/zeta_cli/src/main.rs | 6 +- crates/zlog/src/filter.rs | 2 +- 242 files changed, 801 insertions(+), 821 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3edd8d802c6e949b0040a45c7fda61fd340abbf7..3854ebe0109a7162c023992f7089bd2f9bf70947 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -824,6 +824,7 @@ module_inception = { level = "deny" } question_mark = { level = "deny" } redundant_closure = { level = "deny" } declare_interior_mutable_const = { level = "deny" } +needless_borrow = { level = "warn"} # Individual rules that have violations in the codebase: type_complexity = "allow" # We often return trait objects from `new` functions. diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index e104c40bf2bc969b35e94d0b5a1dda5658116ead..8bc06354755b0feb8e0e41061df82e3ac9415a53 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -485,7 +485,7 @@ impl ContentBlock { } fn resource_link_md(uri: &str) -> String { - if let Some(uri) = MentionUri::parse(&uri).log_err() { + if let Some(uri) = MentionUri::parse(uri).log_err() { uri.as_link().to_string() } else { uri.to_string() @@ -1416,7 +1416,7 @@ impl AcpThread { fn user_message(&self, id: &UserMessageId) -> Option<&UserMessage> { self.entries.iter().find_map(|entry| { if let AgentThreadEntry::UserMessage(message) = entry { - if message.id.as_ref() == Some(&id) { + if message.id.as_ref() == Some(id) { Some(message) } else { None @@ -1430,7 +1430,7 @@ impl AcpThread { fn user_message_mut(&mut self, id: &UserMessageId) -> Option<(usize, &mut UserMessage)> { self.entries.iter_mut().enumerate().find_map(|(ix, entry)| { if let AgentThreadEntry::UserMessage(message) = entry { - if message.id.as_ref() == Some(&id) { + if message.id.as_ref() == Some(id) { Some((ix, message)) } else { None @@ -2356,7 +2356,7 @@ mod tests { fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { let sessions = self.sessions.lock(); - let thread = sessions.get(&session_id).unwrap().clone(); + let thread = sessions.get(session_id).unwrap().clone(); cx.spawn(async move |cx| { thread diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index a2c2d6c3229ae96bf45dfc870e8600a5f778a6f0..e5f71d21098b83c149cedf25c0d1f0c76687a1e5 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -71,8 +71,8 @@ impl Diff { let hunk_ranges = { let buffer = new_buffer.read(cx); let diff = buffer_diff.read(cx); - diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer)) + diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) + .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) .collect::>() }; @@ -306,13 +306,13 @@ impl PendingDiff { let buffer = self.buffer.read(cx); let diff = self.diff.read(cx); let mut ranges = diff - .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer)) + .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) + .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) .collect::>(); ranges.extend( self.revealed_ranges .iter() - .map(|range| range.to_point(&buffer)), + .map(|range| range.to_point(buffer)), ); ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end))); diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index b9b021c4ca1f728ba82e7df111456ace656bb3bc..17bc265facbecd8fa8141f58f2fa7397be3f7a55 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -146,7 +146,7 @@ impl MentionUri { FileIcons::get_folder_icon(false, cx) .unwrap_or_else(|| IconName::Folder.path().into()) } else { - FileIcons::get_icon(&abs_path, cx) + FileIcons::get_icon(abs_path, cx) .unwrap_or_else(|| IconName::File.path().into()) } } diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index c4eaffc2281de30cf0274539897d5fd70cda1351..20ba9586ea459b3f1e863fe1936258c843186b7b 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -290,7 +290,7 @@ impl ActionLog { } _ = git_diff_updates_rx.changed().fuse() => { if let Some(git_diff) = git_diff.as_ref() { - Self::keep_committed_edits(&this, &buffer, &git_diff, cx).await?; + Self::keep_committed_edits(&this, &buffer, git_diff, cx).await?; } } } @@ -498,7 +498,7 @@ impl ActionLog { new: new_range, }, &new_diff_base, - &buffer_snapshot.as_rope(), + buffer_snapshot.as_rope(), )); } unreviewed_edits @@ -964,7 +964,7 @@ impl TrackedBuffer { fn has_edits(&self, cx: &App) -> bool { self.diff .read(cx) - .hunks(&self.buffer.read(cx), cx) + .hunks(self.buffer.read(cx), cx) .next() .is_some() } @@ -2268,7 +2268,7 @@ mod tests { log::info!("quiescing..."); cx.run_until_parked(); action_log.update(cx, |log, cx| { - let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap(); + let tracked_buffer = log.tracked_buffers.get(buffer).unwrap(); let mut old_text = tracked_buffer.diff_base.clone(); let new_text = buffer.read(cx).as_rope(); for edit in tracked_buffer.unreviewed_edits.edits() { diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 7c562aaba4f494d044b3efd4c53344365011257f..090252d3389cb636298636933a75a140ff235681 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -702,7 +702,7 @@ impl ActivityIndicator { on_click: Some(Arc::new(|this, window, cx| { this.dismiss_error_message(&DismissErrorMessage, window, cx) })), - tooltip_message: Some(Self::version_tooltip_message(&version)), + tooltip_message: Some(Self::version_tooltip_message(version)), }), AutoUpdateStatus::Installing { version } => Some(Content { icon: Some( @@ -714,13 +714,13 @@ impl ActivityIndicator { on_click: Some(Arc::new(|this, window, cx| { this.dismiss_error_message(&DismissErrorMessage, window, cx) })), - tooltip_message: Some(Self::version_tooltip_message(&version)), + tooltip_message: Some(Self::version_tooltip_message(version)), }), AutoUpdateStatus::Updated { version } => Some(Content { icon: None, message: "Click to restart and update Zed".to_string(), on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))), - tooltip_message: Some(Self::version_tooltip_message(&version)), + tooltip_message: Some(Self::version_tooltip_message(version)), }), AutoUpdateStatus::Errored => Some(Content { icon: Some( diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 549184218517a0b9e4915187dea30174021182b2..469135a967ab12199486c5014dc1ce3676fe7b78 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1692,7 +1692,7 @@ impl Thread { self.last_received_chunk_at = Some(Instant::now()); let task = cx.spawn(async move |thread, cx| { - let stream_completion_future = model.stream_completion(request, &cx); + let stream_completion_future = model.stream_completion(request, cx); let initial_token_usage = thread.read_with(cx, |thread, _cx| thread.cumulative_token_usage); let stream_completion = async { @@ -1824,7 +1824,7 @@ impl Thread { let streamed_input = if tool_use.is_input_complete { None } else { - Some((&tool_use.input).clone()) + Some(tool_use.input.clone()) }; let ui_text = thread.tool_use.request_tool_use( @@ -2051,7 +2051,7 @@ impl Thread { retry_scheduled = thread .handle_retryable_error_with_delay( - &completion_error, + completion_error, Some(retry_strategy), model.clone(), intent, @@ -2130,7 +2130,7 @@ impl Thread { self.pending_summary = cx.spawn(async move |this, cx| { let result = async { - let mut messages = model.model.stream_completion(request, &cx).await?; + let mut messages = model.model.stream_completion(request, cx).await?; let mut new_summary = String::new(); while let Some(event) = messages.next().await { @@ -2456,7 +2456,7 @@ impl Thread { // which result to prefer (the old task could complete after the new one, resulting in a // stale summary). self.detailed_summary_task = cx.spawn(async move |thread, cx| { - let stream = model.stream_completion_text(request, &cx); + let stream = model.stream_completion_text(request, cx); let Some(mut messages) = stream.await.log_err() else { thread .update(cx, |thread, _cx| { @@ -4043,7 +4043,7 @@ fn main() {{ }); let fake_model = model.as_fake(); - simulate_successful_response(&fake_model, cx); + simulate_successful_response(fake_model, cx); // Should start generating summary when there are >= 2 messages thread.read_with(cx, |thread, _| { @@ -4138,7 +4138,7 @@ fn main() {{ }); let fake_model = model.as_fake(); - simulate_successful_response(&fake_model, cx); + simulate_successful_response(fake_model, cx); thread.read_with(cx, |thread, _| { // State is still Error, not Generating @@ -5420,7 +5420,7 @@ fn main() {{ }); let fake_model = model.as_fake(); - simulate_successful_response(&fake_model, cx); + simulate_successful_response(fake_model, cx); thread.read_with(cx, |thread, _| { assert!(matches!(thread.summary(), ThreadSummary::Generating)); diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index af740d9901e36320cf956a32620d89a624a7ff5a..985de4d123b0dffc3e9b1fac8fd35b767738b71d 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -91,7 +91,7 @@ impl LanguageModels { for provider in &providers { for model in provider.recommended_models(cx) { recommended_models.insert(model.id()); - recommended.push(Self::map_language_model_to_info(&model, &provider)); + recommended.push(Self::map_language_model_to_info(&model, provider)); } } if !recommended.is_empty() { diff --git a/crates/agent2/src/templates.rs b/crates/agent2/src/templates.rs index a63f0ad206308130712b9481cfd7231eb0fd2696..72a8f6633cb7bb926580dbb4f9e65ec032162d93 100644 --- a/crates/agent2/src/templates.rs +++ b/crates/agent2/src/templates.rs @@ -62,7 +62,7 @@ fn contains( handlebars::RenderError::new("contains: missing or invalid query parameter") })?; - if list.contains(&query) { + if list.contains(query) { out.write("true")?; } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 429832010be50a6fc1bcb84c26318c3e31e17766..eed374e3969076aa135029ab91107954179d8224 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -173,7 +173,7 @@ impl UserMessage { &mut symbol_context, "\n{}", MarkdownCodeBlock { - tag: &codeblock_tag(&abs_path, None), + tag: &codeblock_tag(abs_path, None), text: &content.to_string(), } ) @@ -189,8 +189,8 @@ impl UserMessage { &mut rules_context, "\n{}", MarkdownCodeBlock { - tag: &codeblock_tag(&path, Some(line_range)), - text: &content + tag: &codeblock_tag(path, Some(line_range)), + text: content } ) .ok(); @@ -207,7 +207,7 @@ impl UserMessage { "\n{}", MarkdownCodeBlock { tag: "", - text: &content + text: content } ) .ok(); @@ -1048,7 +1048,7 @@ impl Thread { tools, tool_choice: None, stop: Vec::new(), - temperature: AgentSettings::temperature_for_model(&model, cx), + temperature: AgentSettings::temperature_for_model(model, cx), thinking_allowed: true, }; diff --git a/crates/agent2/src/tools/context_server_registry.rs b/crates/agent2/src/tools/context_server_registry.rs index db39e9278c250865d63922cd802e1ce9fb1d003f..ddeb08a046124e698bc4931018f26dbe54efa0bc 100644 --- a/crates/agent2/src/tools/context_server_registry.rs +++ b/crates/agent2/src/tools/context_server_registry.rs @@ -103,7 +103,7 @@ impl ContextServerRegistry { self.reload_tools_for_server(server_id.clone(), cx); } ContextServerStatus::Stopped | ContextServerStatus::Error(_) => { - self.registered_servers.remove(&server_id); + self.registered_servers.remove(server_id); cx.notify(); } } diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index c55e503d766c8b6f21e7ada4bdca07022d5e435a..e70e5e8a141726826e1b5e6aa95fb2654c1ee96d 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -471,7 +471,7 @@ fn resolve_path( let parent_entry = parent_project_path .as_ref() - .and_then(|path| project.entry_for_path(&path, cx)) + .and_then(|path| project.entry_for_path(path, cx)) .context("Can't create file: parent directory doesn't exist")?; anyhow::ensure!( diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index ecb855ac34d655caefab5ed0bd4f33d60be547f8..ac79874c365eb42d3aaead5cb705ee4127389cd1 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -80,7 +80,7 @@ impl AgentTool for TerminalTool { let first_line = lines.next().unwrap_or_default(); let remaining_line_count = lines.count(); match remaining_line_count { - 0 => MarkdownInlineCode(&first_line).to_string().into(), + 0 => MarkdownInlineCode(first_line).to_string().into(), 1 => MarkdownInlineCode(&format!( "{} - {} more line", first_line, remaining_line_count diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 00e3e3df5093c6f1acef32665ab0d3d8846fc39f..1cfb1fcabf3a0a4cc009ab0912188437aef17c2c 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -19,14 +19,14 @@ pub async fn connect( root_dir: &Path, cx: &mut AsyncApp, ) -> Result> { - let conn = v1::AcpConnection::stdio(server_name, command.clone(), &root_dir, cx).await; + 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?); + Rc::new(v0::AcpConnection::stdio(server_name, command, root_dir, cx).await?); Ok(conn) } Err(err) => Err(err), diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index d80d040aad2f989315fabac6e0c9ac1e114a4805..354bda494d70b4994b8f0bdd98309abb2e665a9b 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -291,7 +291,7 @@ impl AgentConnection for ClaudeAgentConnection { fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { let sessions = self.sessions.borrow(); - let Some(session) = sessions.get(&session_id) else { + let Some(session) = sessions.get(session_id) else { log::warn!("Attempted to cancel nonexistent session {}", session_id); return; }; diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 8a413fc91ec1ad3d10a2bfcb7fb5e83cbc12ef92..e2ddd03f2700283442bc1f803490465ab05f59cb 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -552,11 +552,11 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId); let mut label = CodeLabel::default(); - label.push_str(&file_name, None); + label.push_str(file_name, None); label.push_str(" ", None); if let Some(directory) = directory { - label.push_str(&directory, comment_id); + label.push_str(directory, comment_id); } label.filter_range = 0..label.text().len(); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 299f0c30be566e7d17e82180ee29efd0d95386b8..d5922317263fe7e1ae55be08f3bf3f2c1e4b4754 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1191,7 +1191,7 @@ impl MentionSet { }) } MentionUri::Fetch { url } => { - let Some(content) = self.fetch_results.get(&url).cloned() else { + let Some(content) = self.fetch_results.get(url).cloned() else { return Task::ready(Err(anyhow!("missing fetch result"))); }; let uri = uri.clone(); diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs index 563afee65f0168232c0461092272f3af4bbb77dd..77c88c461d6e6fadcefd8eb7319bbbe2ff05fef4 100644 --- a/crates/agent_ui/src/acp/model_selector.rs +++ b/crates/agent_ui/src/acp/model_selector.rs @@ -330,7 +330,7 @@ async fn fuzzy_search( .collect::>(); let mut matches = match_strings( &candidates, - &query, + query, false, true, 100, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index e2e582081297ea7c54c67b447beb3facb3fc6acf..4a8f9bf209be2c16f83cb745d2265d950becb96a 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -696,7 +696,7 @@ impl AcpThreadView { }; diff.update(cx, |diff, cx| { - diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx) + diff.move_to_path(PathKey::for_buffer(buffer, cx), window, cx) }) } @@ -722,13 +722,13 @@ impl AcpThreadView { let len = thread.read(cx).entries().len(); let index = len - 1; self.entry_view_state.update(cx, |view_state, cx| { - view_state.sync_entry(index, &thread, window, cx) + view_state.sync_entry(index, thread, window, cx) }); self.list_state.splice(index..index, 1); } AcpThreadEvent::EntryUpdated(index) => { self.entry_view_state.update(cx, |view_state, cx| { - view_state.sync_entry(*index, &thread, window, cx) + view_state.sync_entry(*index, thread, window, cx) }); self.list_state.splice(*index..index + 1, 1); } @@ -1427,7 +1427,7 @@ impl AcpThreadView { Empty.into_any_element() } } - ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, &diff, cx), + ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, diff, cx), ToolCallContent::Terminal(terminal) => { self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx) } @@ -1583,7 +1583,7 @@ impl AcpThreadView { .border_color(self.tool_card_border_color(cx)) .child( if let Some(entry) = self.entry_view_state.read(cx).entry(entry_ix) - && let Some(editor) = entry.editor_for_diff(&diff) + && let Some(editor) = entry.editor_for_diff(diff) { editor.clone().into_any_element() } else { @@ -1783,7 +1783,7 @@ impl AcpThreadView { .entry_view_state .read(cx) .entry(entry_ix) - .and_then(|entry| entry.terminal(&terminal)); + .and_then(|entry| entry.terminal(terminal)); let show_output = self.terminal_expanded && terminal_view.is_some(); v_flex() @@ -2420,7 +2420,7 @@ impl AcpThreadView { .buffer_font(cx) }); - let file_icon = FileIcons::get_icon(&path, cx) + let file_icon = FileIcons::get_icon(path, cx) .map(Icon::from_path) .map(|icon| icon.color(Color::Muted).size(IconSize::Small)) .unwrap_or_else(|| { @@ -3453,7 +3453,7 @@ impl Render for AcpThreadView { configuration_view, .. } => self.render_auth_required_state( - &connection, + connection, description.as_ref(), configuration_view.as_ref(), window, diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 116c2b901bb5624a387017db22883f5b03d6db4f..38be2b193cab0e86f01b7ec88819d431e0bc7d0d 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1044,12 +1044,12 @@ impl ActiveThread { ); } ThreadEvent::StreamedAssistantText(message_id, text) => { - if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) { + if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(message_id) { rendered_message.append_text(text, cx); } } ThreadEvent::StreamedAssistantThinking(message_id, text) => { - if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) { + if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(message_id) { rendered_message.append_thinking(text, cx); } } @@ -2473,7 +2473,7 @@ impl ActiveThread { message_id, index, content.clone(), - &scroll_handle, + scroll_handle, Some(index) == pending_thinking_segment_index, window, cx, diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index b9e1ea5d0a26262fc24dc58d05d54ed970371ccd..85e729781027238e5c106f30fc26ee14d811763c 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -207,7 +207,7 @@ impl AgentDiffPane { ), match &thread { AgentDiffThread::Native(thread) => { - Some(cx.subscribe(&thread, |this, _thread, event, cx| { + Some(cx.subscribe(thread, |this, _thread, event, cx| { this.handle_thread_event(event, cx) })) } @@ -398,7 +398,7 @@ fn keep_edits_in_selection( .disjoint_anchor_ranges() .collect::>(); - keep_edits_in_ranges(editor, buffer_snapshot, &thread, ranges, window, cx) + keep_edits_in_ranges(editor, buffer_snapshot, thread, ranges, window, cx) } fn reject_edits_in_selection( @@ -412,7 +412,7 @@ fn reject_edits_in_selection( .selections .disjoint_anchor_ranges() .collect::>(); - reject_edits_in_ranges(editor, buffer_snapshot, &thread, ranges, window, cx) + reject_edits_in_ranges(editor, buffer_snapshot, thread, ranges, window, cx) } fn keep_edits_in_ranges( @@ -1001,7 +1001,7 @@ impl AgentDiffToolbar { return; }; - *state = agent_diff.read(cx).editor_state(&editor); + *state = agent_diff.read(cx).editor_state(editor); self.update_location(cx); cx.notify(); } @@ -1343,13 +1343,13 @@ impl AgentDiff { }); let thread_subscription = match &thread { - AgentDiffThread::Native(thread) => cx.subscribe_in(&thread, window, { + AgentDiffThread::Native(thread) => cx.subscribe_in(thread, window, { let workspace = workspace.clone(); move |this, _thread, event, window, cx| { this.handle_native_thread_event(&workspace, event, window, cx) } }), - AgentDiffThread::AcpThread(thread) => cx.subscribe_in(&thread, window, { + AgentDiffThread::AcpThread(thread) => cx.subscribe_in(thread, window, { let workspace = workspace.clone(); move |this, thread, event, window, cx| { this.handle_acp_thread_event(&workspace, thread, event, window, cx) @@ -1357,11 +1357,11 @@ impl AgentDiff { }), }; - if let Some(workspace_thread) = self.workspace_threads.get_mut(&workspace) { + if let Some(workspace_thread) = self.workspace_threads.get_mut(workspace) { // replace thread and action log subscription, but keep editors workspace_thread.thread = thread.downgrade(); workspace_thread._thread_subscriptions = (action_log_subscription, thread_subscription); - self.update_reviewing_editors(&workspace, window, cx); + self.update_reviewing_editors(workspace, window, cx); return; } @@ -1677,7 +1677,7 @@ impl AgentDiff { editor.register_addon(EditorAgentDiffAddon); }); } else { - unaffected.remove(&weak_editor); + unaffected.remove(weak_editor); } if new_state == EditorState::Reviewing && previous_state != Some(new_state) { @@ -1730,7 +1730,7 @@ impl AgentDiff { fn editor_state(&self, editor: &WeakEntity) -> EditorState { self.reviewing_editors - .get(&editor) + .get(editor) .cloned() .unwrap_or(EditorState::Idle) } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 4cb231f357f4be98a379a9689b0567ffbdc41ccf..e1174a41917c4cce035a16bc05309dc5f07c9895 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2923,7 +2923,7 @@ impl AgentPanel { .style(ButtonStyle::Tinted(ui::TintColor::Warning)) .label_size(LabelSize::Small) .key_binding( - KeyBinding::for_action_in(&OpenSettings, &focus_handle, window, cx) + KeyBinding::for_action_in(&OpenSettings, focus_handle, window, cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(|_event, window, cx| { @@ -3329,7 +3329,7 @@ impl AgentPanel { .paths() .into_iter() .map(|path| { - Workspace::project_path_for_path(this.project.clone(), &path, false, cx) + Workspace::project_path_for_path(this.project.clone(), path, false, cx) }) .collect::>(); cx.spawn_in(window, async move |this, cx| { @@ -3599,7 +3599,7 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist { let text_thread_store = None; let context_store = cx.new(|_| ContextStore::new(project.clone(), None)); assistant.assist( - &prompt_editor, + prompt_editor, self.workspace.clone(), context_store, project, diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index 615142b73dfd6eed59f635af780310290e3f6f25..23e04266db73c654ff0ecc97fe69852e6f531b19 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -388,7 +388,7 @@ impl CodegenAlternative { } else { let request = self.build_request(&model, user_prompt, cx)?; cx.spawn(async move |_, cx| { - Ok(model.stream_completion_text(request.await, &cx).await?) + Ok(model.stream_completion_text(request.await, cx).await?) }) .boxed_local() }; @@ -447,7 +447,7 @@ impl CodegenAlternative { } }); - let temperature = AgentSettings::temperature_for_model(&model, cx); + let temperature = AgentSettings::temperature_for_model(model, cx); Ok(cx.spawn(async move |_cx| { let mut request_message = LanguageModelRequestMessage { @@ -1028,7 +1028,7 @@ where chunk.push('\n'); } - chunk.push_str(&line); + chunk.push_str(line); } consumed += line.len(); diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index 962c0df03db99ba8739df2c0eb8713d0e25f7f75..79e56acacf1ce9a1ad541de9404aed44c711ff8b 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -728,11 +728,11 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId); let mut label = CodeLabel::default(); - label.push_str(&file_name, None); + label.push_str(file_name, None); label.push_str(" ", None); if let Some(directory) = directory { - label.push_str(&directory, comment_id); + label.push_str(directory, comment_id); } label.filter_range = 0..label.text().len(); diff --git a/crates/agent_ui/src/context_picker/file_context_picker.rs b/crates/agent_ui/src/context_picker/file_context_picker.rs index eaf9ed16d6fc7a09854d9f0160d87e23f3c5ffd8..4f74e2cea4f9960fdc30279fb7e9297f10675b81 100644 --- a/crates/agent_ui/src/context_picker/file_context_picker.rs +++ b/crates/agent_ui/src/context_picker/file_context_picker.rs @@ -315,7 +315,7 @@ pub fn render_file_context_entry( context_store: WeakEntity, cx: &App, ) -> Stateful
{ - let (file_name, directory) = extract_file_name_and_directory(&path, path_prefix); + let (file_name, directory) = extract_file_name_and_directory(path, path_prefix); let added = context_store.upgrade().and_then(|context_store| { let project_path = ProjectPath { @@ -334,7 +334,7 @@ pub fn render_file_context_entry( let file_icon = if is_directory { FileIcons::get_folder_icon(false, cx) } else { - FileIcons::get_icon(&path, cx) + FileIcons::get_icon(path, cx) } .map(Icon::from_path) .unwrap_or_else(|| Icon::new(IconName::File)); diff --git a/crates/agent_ui/src/context_picker/symbol_context_picker.rs b/crates/agent_ui/src/context_picker/symbol_context_picker.rs index 05e77deece6117d250d6efedbd9d24c6716b757e..805c10c96553afe6482793974478392c0441e350 100644 --- a/crates/agent_ui/src/context_picker/symbol_context_picker.rs +++ b/crates/agent_ui/src/context_picker/symbol_context_picker.rs @@ -289,7 +289,7 @@ pub(crate) fn search_symbols( .iter() .enumerate() .map(|(id, symbol)| { - StringMatchCandidate::new(id, &symbol.label.filter_text()) + StringMatchCandidate::new(id, symbol.label.filter_text()) }) .partition(|candidate| { project 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 15cc731f8f2b7c82885c566273bc1cda9f3c156a..e660e64ae349e2edc810fdaac68528f80a6005a7 100644 --- a/crates/agent_ui/src/context_picker/thread_context_picker.rs +++ b/crates/agent_ui/src/context_picker/thread_context_picker.rs @@ -167,7 +167,7 @@ impl PickerDelegate for ThreadContextPickerDelegate { return; }; let open_thread_task = - thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx)); + thread_store.update(cx, |this, cx| this.open_thread(id, window, cx)); cx.spawn(async move |this, cx| { let thread = open_thread_task.await?; @@ -236,7 +236,7 @@ pub fn render_thread_context_entry( let is_added = match entry { ThreadContextEntry::Thread { id, .. } => context_store .upgrade() - .map_or(false, |ctx_store| ctx_store.read(cx).includes_thread(&id)), + .map_or(false, |ctx_store| ctx_store.read(cx).includes_thread(id)), ThreadContextEntry::Context { path, .. } => { context_store.upgrade().map_or(false, |ctx_store| { ctx_store.read(cx).includes_text_thread(path) @@ -338,7 +338,7 @@ pub(crate) fn search_threads( let candidates = threads .iter() .enumerate() - .map(|(id, (_, thread))| StringMatchCandidate::new(id, &thread.title())) + .map(|(id, (_, thread))| StringMatchCandidate::new(id, thread.title())) .collect::>(); let matches = fuzzy::match_strings( &candidates, diff --git a/crates/agent_ui/src/context_strip.rs b/crates/agent_ui/src/context_strip.rs index 369964f165dc4d4460fd446c949538ec820fb82e..51ed3a5e1169907b85e1944305d2acfc22fdf551 100644 --- a/crates/agent_ui/src/context_strip.rs +++ b/crates/agent_ui/src/context_strip.rs @@ -145,7 +145,7 @@ impl ContextStrip { } let file_name = active_buffer.file()?.file_name(cx); - let icon_path = FileIcons::get_icon(&Path::new(&file_name), cx); + let icon_path = FileIcons::get_icon(Path::new(&file_name), cx); Some(SuggestedContext::File { name: file_name.to_string_lossy().into_owned().into(), buffer: active_buffer_entity.downgrade(), @@ -377,7 +377,7 @@ impl ContextStrip { fn add_suggested_context(&mut self, suggested: &SuggestedContext, cx: &mut Context) { self.context_store.update(cx, |context_store, cx| { - context_store.add_suggested_context(&suggested, cx) + context_store.add_suggested_context(suggested, cx) }); cx.notify(); } diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index bbd35958059346750cb899746823d5e12b2de1b8..781e242fba5d8c5cb938c6607d6416896291de90 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -526,9 +526,9 @@ impl InlineAssistant { if assist_to_focus.is_none() { let focus_assist = if newest_selection.reversed { - range.start.to_point(&snapshot) == newest_selection.start + range.start.to_point(snapshot) == newest_selection.start } else { - range.end.to_point(&snapshot) == newest_selection.end + range.end.to_point(snapshot) == newest_selection.end }; if focus_assist { assist_to_focus = Some(assist_id); @@ -550,7 +550,7 @@ impl InlineAssistant { let editor_assists = self .assists_by_editor .entry(editor.downgrade()) - .or_insert_with(|| EditorInlineAssists::new(&editor, window, cx)); + .or_insert_with(|| EditorInlineAssists::new(editor, window, cx)); let mut assist_group = InlineAssistGroup::new(); for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists { let codegen = prompt_editor.read(cx).codegen().clone(); @@ -649,7 +649,7 @@ impl InlineAssistant { let editor_assists = self .assists_by_editor .entry(editor.downgrade()) - .or_insert_with(|| EditorInlineAssists::new(&editor, window, cx)); + .or_insert_with(|| EditorInlineAssists::new(editor, window, cx)); let mut assist_group = InlineAssistGroup::new(); self.assists.insert( diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index e6fca1698496b064917a6b1b8257388e81a00df7..6f12050f883636c0a3027a1342ea5fe2d0e150a9 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -75,7 +75,7 @@ impl Render for PromptEditor { let codegen = codegen.read(cx); if codegen.alternative_count(cx) > 1 { - buttons.push(self.render_cycle_controls(&codegen, cx)); + buttons.push(self.render_cycle_controls(codegen, cx)); } let editor_margins = editor_margins.lock(); diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index fa8ca490d882eab7e5c363bb133a86e831622124..845540979a9559e5eb0f573d0a039de3c2095917 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -296,7 +296,7 @@ impl ModelMatcher { pub fn fuzzy_search(&self, query: &str) -> Vec { let mut matches = self.bg_executor.block(match_strings( &self.candidates, - &query, + query, false, true, 100, diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index d6c9a778a60a73a57db6aaf4a38fd907f5366615..181a0dd5d2d4f484fb336b388f9845c6b69c8d70 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1166,7 +1166,7 @@ impl MessageEditor { .buffer_font(cx) }); - let file_icon = FileIcons::get_icon(&path, cx) + let file_icon = FileIcons::get_icon(path, cx) .map(Icon::from_path) .map(|icon| icon.color(Color::Muted).size(IconSize::Small)) .unwrap_or_else(|| { @@ -1559,9 +1559,8 @@ impl ContextCreasesAddon { cx: &mut Context, ) { self.creases.entry(key).or_default().extend(creases); - self._subscription = Some(cx.subscribe( - &context_store, - |editor, _, event, cx| match event { + self._subscription = Some( + cx.subscribe(context_store, |editor, _, event, cx| match event { ContextStoreEvent::ContextRemoved(key) => { let Some(this) = editor.addon_mut::() else { return; @@ -1581,8 +1580,8 @@ impl ContextCreasesAddon { editor.edit(ranges.into_iter().zip(replacement_texts), cx); cx.notify(); } - }, - )) + }), + ) } pub fn into_inner(self) -> HashMap> { diff --git a/crates/agent_ui/src/slash_command_picker.rs b/crates/agent_ui/src/slash_command_picker.rs index 678562e0594b69f43524155c70bb176727f57b46..bab2364679b472488340e9b849681a50837011ad 100644 --- a/crates/agent_ui/src/slash_command_picker.rs +++ b/crates/agent_ui/src/slash_command_picker.rs @@ -214,7 +214,7 @@ impl PickerDelegate for SlashCommandDelegate { let mut label = format!("{}", info.name); if let Some(args) = info.args.as_ref().filter(|_| selected) { - label.push_str(&args); + label.push_str(args); } Label::new(label) .single_line() diff --git a/crates/agent_ui/src/terminal_codegen.rs b/crates/agent_ui/src/terminal_codegen.rs index 54f5b52f584cb87fd2953a148aa8ae48ea38b862..5a4a9d560a16e858dcaedf706f2067a24bc12c5f 100644 --- a/crates/agent_ui/src/terminal_codegen.rs +++ b/crates/agent_ui/src/terminal_codegen.rs @@ -48,7 +48,7 @@ impl TerminalCodegen { let prompt = prompt_task.await; let model_telemetry_id = model.telemetry_id(); let model_provider_id = model.provider_id(); - let response = model.stream_completion_text(prompt, &cx).await; + let response = model.stream_completion_text(prompt, cx).await; let generate = async { let message_id = response .as_ref() diff --git a/crates/agent_ui/src/ui/context_pill.rs b/crates/agent_ui/src/ui/context_pill.rs index 5dd57de24490df03ce0f2c41a844be33fb675793..4e33e151cdc04686a5525ea9ed164237d86c90e9 100644 --- a/crates/agent_ui/src/ui/context_pill.rs +++ b/crates/agent_ui/src/ui/context_pill.rs @@ -353,7 +353,7 @@ impl AddedContext { name, parent, tooltip: Some(full_path_string), - icon_path: FileIcons::get_icon(&full_path, cx), + icon_path: FileIcons::get_icon(full_path, cx), status: ContextStatus::Ready, render_hover: None, handle: AgentContextHandle::File(handle), @@ -615,7 +615,7 @@ impl AddedContext { let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into(); let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, &full_path_string); - let icon_path = FileIcons::get_icon(&full_path, cx); + let icon_path = FileIcons::get_icon(full_path, cx); (name, parent, icon_path) } else { ("Image".into(), None, None) @@ -706,7 +706,7 @@ impl ContextFileExcerpt { .and_then(|p| p.file_name()) .map(|n| n.to_string_lossy().into_owned().into()); - let icon_path = FileIcons::get_icon(&full_path, cx); + let icon_path = FileIcons::get_icon(full_path, cx); ContextFileExcerpt { file_name_and_range: file_name_and_range.into(), diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 557f9592e4d12e86c4e73d1bc742dfa74535d66c..06abbad39f5d9bb80addcc089ccf655409826425 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -592,7 +592,7 @@ impl MessageMetadata { pub fn is_cache_valid(&self, buffer: &BufferSnapshot, range: &Range) -> bool { let result = match &self.cache { Some(MessageCacheMetadata { cached_at, .. }) => !buffer.has_edits_since_in_range( - &cached_at, + cached_at, Range { start: buffer.anchor_at(range.start, Bias::Right), end: buffer.anchor_at(range.end, Bias::Left), @@ -1413,7 +1413,7 @@ impl AssistantContext { } let request = { - let mut req = self.to_completion_request(Some(&model), cx); + let mut req = self.to_completion_request(Some(model), cx); // Skip the last message because it's likely to change and // therefore would be a waste to cache. req.messages.pop(); @@ -1428,7 +1428,7 @@ impl AssistantContext { let model = Arc::clone(model); self.pending_cache_warming_task = cx.spawn(async move |this, cx| { async move { - match model.stream_completion(request, &cx).await { + match model.stream_completion(request, cx).await { Ok(mut stream) => { stream.next().await; log::info!("Cache warming completed successfully"); @@ -1661,12 +1661,12 @@ impl AssistantContext { ) -> Range { let buffer = self.buffer.read(cx); let start_ix = match all_annotations - .binary_search_by(|probe| probe.range().end.cmp(&range.start, &buffer)) + .binary_search_by(|probe| probe.range().end.cmp(&range.start, buffer)) { Ok(ix) | Err(ix) => ix, }; let end_ix = match all_annotations - .binary_search_by(|probe| probe.range().start.cmp(&range.end, &buffer)) + .binary_search_by(|probe| probe.range().start.cmp(&range.end, buffer)) { Ok(ix) => ix + 1, Err(ix) => ix, @@ -2045,7 +2045,7 @@ impl AssistantContext { let task = cx.spawn({ async move |this, cx| { - let stream = model.stream_completion(request, &cx); + let stream = model.stream_completion(request, cx); let assistant_message_id = assistant_message.id; let mut response_latency = None; let stream_completion = async { @@ -2708,7 +2708,7 @@ impl AssistantContext { self.summary_task = cx.spawn(async move |this, cx| { let result = async { - let stream = model.model.stream_completion_text(request, &cx); + let stream = model.model.stream_completion_text(request, cx); let mut messages = stream.await?; let mut replaced = !replace_old; @@ -2927,7 +2927,7 @@ impl AssistantContext { if let Some(old_path) = old_path.as_ref() { if new_path.as_path() != old_path.as_ref() { fs.rename( - &old_path, + old_path, &new_path, RenameOptions { overwrite: true, diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_context/src/assistant_context_tests.rs index efcad8ed9654449c747ee4853c7e7aa689c0568b..eae7741358be6bfae680a4be3f5fb454387093f6 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_context/src/assistant_context_tests.rs @@ -1300,7 +1300,7 @@ fn test_summarize_error( context.assist(cx); }); - simulate_successful_response(&model, cx); + simulate_successful_response(model, cx); context.read_with(cx, |context, _| { assert!(!context.summary().content().unwrap().done); diff --git a/crates/assistant_slash_commands/src/context_server_command.rs b/crates/assistant_slash_commands/src/context_server_command.rs index f223d3b184ccf6d795b80caca9a6a616aafc7f33..15f3901bfbd60c14d576108272ebf27caf965061 100644 --- a/crates/assistant_slash_commands/src/context_server_command.rs +++ b/crates/assistant_slash_commands/src/context_server_command.rs @@ -44,7 +44,7 @@ impl SlashCommand for ContextServerSlashCommand { parts.push(arg.name.as_str()); } } - create_label_for_command(&parts[0], &parts[1..], cx) + create_label_for_command(parts[0], &parts[1..], cx) } fn description(&self) -> String { diff --git a/crates/assistant_slash_commands/src/diagnostics_command.rs b/crates/assistant_slash_commands/src/diagnostics_command.rs index 2feabd8b1e018cc6495a88fe5a89276e3e19dfb1..31014f8fb80435e09619669e6cc2e800eed473e2 100644 --- a/crates/assistant_slash_commands/src/diagnostics_command.rs +++ b/crates/assistant_slash_commands/src/diagnostics_command.rs @@ -249,7 +249,7 @@ fn collect_diagnostics( let worktree = worktree.read(cx); let worktree_root_path = Path::new(worktree.root_name()); let relative_path = path.strip_prefix(worktree_root_path).ok()?; - worktree.absolutize(&relative_path).ok() + worktree.absolutize(relative_path).ok() }) }) .is_some() @@ -365,7 +365,7 @@ pub fn collect_buffer_diagnostics( ) { for (_, group) in snapshot.diagnostic_groups(None) { let entry = &group.entries[group.primary_ix]; - collect_diagnostic(output, entry, &snapshot, include_warnings) + collect_diagnostic(output, entry, snapshot, include_warnings) } } @@ -396,7 +396,7 @@ fn collect_diagnostic( let start_row = range.start.row.saturating_sub(EXCERPT_EXPANSION_SIZE); let end_row = (range.end.row + EXCERPT_EXPANSION_SIZE).min(snapshot.max_point().row) + 1; let excerpt_range = - Point::new(start_row, 0).to_offset(&snapshot)..Point::new(end_row, 0).to_offset(&snapshot); + Point::new(start_row, 0).to_offset(snapshot)..Point::new(end_row, 0).to_offset(snapshot); output.text.push_str("```"); if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) { diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index e819c51e1edb841954508dbfad0fd1d2e85b51c4..039f9d9316f7849fc36a814d3d3d56df8fa3b0e5 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -536,7 +536,7 @@ fn resolve_path( let parent_entry = parent_project_path .as_ref() - .and_then(|path| project.entry_for_path(&path, cx)) + .and_then(|path| project.entry_for_path(path, cx)) .context("Can't create file: parent directory doesn't exist")?; anyhow::ensure!( @@ -723,13 +723,13 @@ impl EditFileToolCard { let buffer = buffer.read(cx); let diff = diff.read(cx); let mut ranges = diff - .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer)) + .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) + .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) .collect::>(); ranges.extend( self.revealed_ranges .iter() - .map(|range| range.to_point(&buffer)), + .map(|range| range.to_point(buffer)), ); ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end))); diff --git a/crates/assistant_tools/src/grep_tool.rs b/crates/assistant_tools/src/grep_tool.rs index a5ce07823fd68ff9531c7d834973166384645601..1f00332c5ae7b8df8adf9767533859288afa663f 100644 --- a/crates/assistant_tools/src/grep_tool.rs +++ b/crates/assistant_tools/src/grep_tool.rs @@ -894,7 +894,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.is_empty(), "grep_tool should not find files outside the project worktree" @@ -920,7 +920,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.iter().any(|p| p.contains("allowed_file.rs")), "grep_tool should be able to search files inside worktrees" @@ -946,7 +946,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.is_empty(), "grep_tool should not search files in .secretdir (file_scan_exclusions)" @@ -971,7 +971,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.is_empty(), "grep_tool should not search .mymetadata files (file_scan_exclusions)" @@ -997,7 +997,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.is_empty(), "grep_tool should not search .mysecrets (private_files)" @@ -1022,7 +1022,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.is_empty(), "grep_tool should not search .privatekey files (private_files)" @@ -1047,7 +1047,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.is_empty(), "grep_tool should not search .mysensitive files (private_files)" @@ -1073,7 +1073,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.iter().any(|p| p.contains("normal_file.rs")), "Should be able to search normal files" @@ -1100,7 +1100,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.is_empty(), "grep_tool should not allow escaping project boundaries with relative paths" @@ -1206,7 +1206,7 @@ mod tests { .unwrap(); let content = result.content.as_str().unwrap(); - let paths = extract_paths_from_results(&content); + let paths = extract_paths_from_results(content); // Should find matches in non-private files assert!( @@ -1271,7 +1271,7 @@ mod tests { .unwrap(); let content = result.content.as_str().unwrap(); - let paths = extract_paths_from_results(&content); + let paths = extract_paths_from_results(content); // Should only find matches in worktree1 *.rs files (excluding private ones) assert!( diff --git a/crates/assistant_tools/src/project_notifications_tool.rs b/crates/assistant_tools/src/project_notifications_tool.rs index c65cfd0ca76d91f454982ded5f2893159ab7a32a..e30d80207dae4de1e69efe99724a2a5343b57664 100644 --- a/crates/assistant_tools/src/project_notifications_tool.rs +++ b/crates/assistant_tools/src/project_notifications_tool.rs @@ -81,7 +81,7 @@ fn fit_patch_to_size(patch: &str, max_size: usize) -> String { // Compression level 1: remove context lines in diff bodies, but // leave the counts and positions of inserted/deleted lines let mut current_size = patch.len(); - let mut file_patches = split_patch(&patch); + let mut file_patches = split_patch(patch); file_patches.sort_by_key(|patch| patch.len()); let compressed_patches = file_patches .iter() diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 46227f130d6c706c598466045057075786b21cd3..3de22ad28da617a31c3dd7c9e361f9cbb074c82d 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -105,7 +105,7 @@ impl Tool for TerminalTool { let first_line = lines.next().unwrap_or_default(); let remaining_line_count = lines.count(); match remaining_line_count { - 0 => MarkdownInlineCode(&first_line).to_string(), + 0 => MarkdownInlineCode(first_line).to_string(), 1 => MarkdownInlineCode(&format!( "{} - {} more line", first_line, remaining_line_count diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 8eed7497da0fea8cb0227b22885599b446e5aac0..990fc27fbd40288a8850a48ac5a91116e4a2f23c 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -231,7 +231,7 @@ fn apply_dirty_filename_style( let highlight = vec![(filename_position..text.len(), highlight_style)]; Some( StyledText::new(text) - .with_default_highlights(&text_style, highlight) + .with_default_highlights(text_style, highlight) .into_any(), ) } diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 97f529fe377c0eaa3d74dca1600e1b3f0c3499db..e20ea9713fbfb0e58df9a86b166e5629a88e2dd1 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -928,7 +928,7 @@ impl BufferDiff { let new_index_text = self.inner.stage_or_unstage_hunks_impl( &self.secondary_diff.as_ref()?.read(cx).inner, stage, - &hunks, + hunks, buffer, file_exists, ); @@ -952,12 +952,12 @@ impl BufferDiff { cx: &App, ) -> Option> { let start = self - .hunks_intersecting_range(range.clone(), &buffer, cx) + .hunks_intersecting_range(range.clone(), buffer, cx) .next()? .buffer_range .start; let end = self - .hunks_intersecting_range_rev(range.clone(), &buffer) + .hunks_intersecting_range_rev(range.clone(), buffer) .next()? .buffer_range .end; @@ -1031,18 +1031,18 @@ impl BufferDiff { && state.base_text.syntax_update_count() == new_state.base_text.syntax_update_count() => { - (false, new_state.compare(&state, buffer)) + (false, new_state.compare(state, buffer)) } _ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)), }; if let Some(secondary_changed_range) = secondary_diff_change { if let Some(secondary_hunk_range) = - self.range_to_hunk_range(secondary_changed_range, &buffer, cx) + self.range_to_hunk_range(secondary_changed_range, buffer, cx) { if let Some(range) = &mut changed_range { - range.start = secondary_hunk_range.start.min(&range.start, &buffer); - range.end = secondary_hunk_range.end.max(&range.end, &buffer); + range.start = secondary_hunk_range.start.min(&range.start, buffer); + range.end = secondary_hunk_range.end.max(&range.end, buffer); } else { changed_range = Some(secondary_hunk_range); } @@ -1057,8 +1057,8 @@ impl BufferDiff { if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last()) { if let Some(range) = &mut changed_range { - range.start = range.start.min(&first.buffer_range.start, &buffer); - range.end = range.end.max(&last.buffer_range.end, &buffer); + range.start = range.start.min(&first.buffer_range.start, buffer); + range.end = range.end.max(&last.buffer_range.end, buffer); } else { changed_range = Some(first.buffer_range.start..last.buffer_range.end); } @@ -1797,7 +1797,7 @@ mod tests { uncommitted_diff.update(cx, |diff, cx| { let hunks = diff - .hunks_intersecting_range(hunk_range.clone(), &buffer, &cx) + .hunks_intersecting_range(hunk_range.clone(), &buffer, cx) .collect::>(); for hunk in &hunks { assert_ne!( @@ -1812,7 +1812,7 @@ mod tests { .to_string(); let hunks = diff - .hunks_intersecting_range(hunk_range.clone(), &buffer, &cx) + .hunks_intersecting_range(hunk_range.clone(), &buffer, cx) .collect::>(); for hunk in &hunks { assert_eq!( @@ -1870,7 +1870,7 @@ mod tests { .to_string(); assert_eq!(new_index_text, buffer_text); - let hunk = diff.hunks(&buffer, &cx).next().unwrap(); + let hunk = diff.hunks(&buffer, cx).next().unwrap(); assert_eq!( hunk.secondary_status, DiffHunkSecondaryStatus::SecondaryHunkRemovalPending @@ -1882,7 +1882,7 @@ mod tests { .to_string(); assert_eq!(index_text, head_text); - let hunk = diff.hunks(&buffer, &cx).next().unwrap(); + let hunk = diff.hunks(&buffer, cx).next().unwrap(); // optimistically unstaged (fine, could also be HasSecondaryHunk) assert_eq!( hunk.secondary_status, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 67591167dfdbfd758f480b5538471aa65175e859..a61d8e09112418b466889b6b4426f51a48d3f651 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -518,11 +518,11 @@ mod linux { ) -> Result<(), std::io::Error> { for _ in 0..100 { thread::sleep(Duration::from_millis(10)); - if sock.connect_addr(&sock_addr).is_ok() { + if sock.connect_addr(sock_addr).is_ok() { return Ok(()); } } - sock.connect_addr(&sock_addr) + sock.connect_addr(sock_addr) } } } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 0f004713566bf0974e880fd41ffcea3b4a331378..91bdf001d8a112590d79a2115dba6adef6dd7053 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -162,7 +162,7 @@ pub fn init(client: &Arc, cx: &mut App) { let client = client.clone(); move |_: &SignIn, cx| { if let Some(client) = client.upgrade() { - cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, &cx).await) + cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, cx).await) .detach_and_log_err(cx); } } @@ -173,7 +173,7 @@ pub fn init(client: &Arc, cx: &mut App) { move |_: &SignOut, cx| { if let Some(client) = client.upgrade() { cx.spawn(async move |cx| { - client.sign_out(&cx).await; + client.sign_out(cx).await; }) .detach(); } @@ -185,7 +185,7 @@ pub fn init(client: &Arc, cx: &mut App) { move |_: &Reconnect, cx| { if let Some(client) = client.upgrade() { cx.spawn(async move |cx| { - client.reconnect(&cx); + client.reconnect(cx); }) .detach(); } @@ -677,7 +677,7 @@ impl Client { let mut delay = INITIAL_RECONNECTION_DELAY; loop { - match client.connect(true, &cx).await { + match client.connect(true, cx).await { ConnectionResult::Timeout => { log::error!("client connect attempt timed out") } @@ -701,7 +701,7 @@ impl Client { Status::ReconnectionError { next_reconnection: Instant::now() + delay, }, - &cx, + cx, ); let jitter = Duration::from_millis(rng.gen_range(0..delay.as_millis() as u64)); @@ -1151,7 +1151,7 @@ impl Client { let this = self.clone(); async move |cx| { while let Some(message) = incoming.next().await { - this.handle_message(message, &cx); + this.handle_message(message, cx); // Don't starve the main thread when receiving lots of messages at once. smol::future::yield_now().await; } @@ -1169,12 +1169,12 @@ impl Client { peer_id, }) { - this.set_status(Status::SignedOut, &cx); + this.set_status(Status::SignedOut, cx); } } Err(err) => { log::error!("connection error: {:?}", err); - this.set_status(Status::ConnectionLost, &cx); + this.set_status(Status::ConnectionLost, cx); } } }) diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 82f74d910ba0d12c1473719189e066eb9d0307eb..6783d8ed2aa7ce0f21e6f40ed1f088b8299d97b9 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -943,21 +943,21 @@ impl Database { let current_merge_conflicts = db_repository_entry .current_merge_conflicts .as_ref() - .map(|conflicts| serde_json::from_str(&conflicts)) + .map(|conflicts| serde_json::from_str(conflicts)) .transpose()? .unwrap_or_default(); let branch_summary = db_repository_entry .branch_summary .as_ref() - .map(|branch_summary| serde_json::from_str(&branch_summary)) + .map(|branch_summary| serde_json::from_str(branch_summary)) .transpose()? .unwrap_or_default(); let head_commit_details = db_repository_entry .head_commit_details .as_ref() - .map(|head_commit_details| serde_json::from_str(&head_commit_details)) + .map(|head_commit_details| serde_json::from_str(head_commit_details)) .transpose()? .unwrap_or_default(); diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index c63d7133be2ec616a95fa73359a5050c289501bf..1b128e3a237d06737bdb5ee99d3d78209db06c6e 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -746,21 +746,21 @@ impl Database { let current_merge_conflicts = db_repository .current_merge_conflicts .as_ref() - .map(|conflicts| serde_json::from_str(&conflicts)) + .map(|conflicts| serde_json::from_str(conflicts)) .transpose()? .unwrap_or_default(); let branch_summary = db_repository .branch_summary .as_ref() - .map(|branch_summary| serde_json::from_str(&branch_summary)) + .map(|branch_summary| serde_json::from_str(branch_summary)) .transpose()? .unwrap_or_default(); let head_commit_details = db_repository .head_commit_details .as_ref() - .map(|head_commit_details| serde_json::from_str(&head_commit_details)) + .map(|head_commit_details| serde_json::from_str(head_commit_details)) .transpose()? .unwrap_or_default(); diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 03d39cb8ced169f59167b1a1f6e91102a268a37d..28d60d9221ab2fb49e4aa77048cef01e621bea89 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -245,7 +245,7 @@ impl MessageEditor { if !candidates.is_empty() { return cx.spawn(async move |_, cx| { let completion_response = Self::completions_for_candidates( - &cx, + cx, query.as_str(), &candidates, start_anchor..end_anchor, @@ -263,7 +263,7 @@ impl MessageEditor { if !candidates.is_empty() { return cx.spawn(async move |_, cx| { let completion_response = Self::completions_for_candidates( - &cx, + cx, query.as_str(), candidates, start_anchor..end_anchor, diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index c2cc6a7ad5cb9813ec618df5ca45f47aa1075305..8016481f6fcd92677cc01aea5d941390bc8eaff1 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2317,7 +2317,7 @@ impl CollabPanel { let client = this.client.clone(); cx.spawn_in(window, async move |_, cx| { client - .connect(true, &cx) + .connect(true, cx) .await .into_response() .notify_async_err(cx); diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index a3420d603b02b9e9d54d2b5bb441a9ba119840aa..01ca533c10ddb0fe72c516a49abe8ca788000be4 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -643,7 +643,7 @@ impl Render for NotificationPanel { let client = client.clone(); window .spawn(cx, async move |cx| { - match client.connect(true, &cx).await { + match client.connect(true, cx).await { util::ConnectionResult::Timeout => { log::error!("Connection timeout"); } diff --git a/crates/context_server/src/listener.rs b/crates/context_server/src/listener.rs index 0e85fb21292739ab0a92d0898fc449a31efe6f29..f3c199a14e3ce42511c24c8dadfd93b2b271b87e 100644 --- a/crates/context_server/src/listener.rs +++ b/crates/context_server/src/listener.rs @@ -315,12 +315,12 @@ impl McpServer { Self::send_err( request_id, format!("Tool not found: {}", params.name), - &outgoing_tx, + outgoing_tx, ); } } Err(err) => { - Self::send_err(request_id, err.to_string(), &outgoing_tx); + Self::send_err(request_id, err.to_string(), outgoing_tx); } } } diff --git a/crates/context_server/src/types.rs b/crates/context_server/src/types.rs index 5fa2420a3d40ce04ee97b4f88c1105711dea8793..e92a18c763fd6fb674014c505a9c1b52ff80a43b 100644 --- a/crates/context_server/src/types.rs +++ b/crates/context_server/src/types.rs @@ -691,7 +691,7 @@ impl CallToolResponse { let mut text = String::new(); for chunk in &self.content { if let ToolResponseContent::Text { text: chunk } = chunk { - text.push_str(&chunk) + text.push_str(chunk) }; } text diff --git a/crates/copilot/src/copilot_chat.rs b/crates/copilot/src/copilot_chat.rs index 4c91b4fedb790ab3500273ff21aba767cacd28e0..e8e2251648e4b941fe616b4524337fe565513950 100644 --- a/crates/copilot/src/copilot_chat.rs +++ b/crates/copilot/src/copilot_chat.rs @@ -484,7 +484,7 @@ impl CopilotChat { }; if this.oauth_token.is_some() { - cx.spawn(async move |this, mut cx| Self::update_models(&this, &mut cx).await) + cx.spawn(async move |this, cx| Self::update_models(&this, cx).await) .detach_and_log_err(cx); } @@ -863,7 +863,7 @@ mod tests { "object": "list" }"#; - let schema: ModelSchema = serde_json::from_str(&json).unwrap(); + let schema: ModelSchema = serde_json::from_str(json).unwrap(); assert_eq!(schema.data.len(), 2); assert_eq!(schema.data[0].id, "gpt-4"); diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 687305ae94da3bc1ddd72e9e9f4594f4f4a19ee4..2cef266677c1314f6fd253b9caf77914050ceb96 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -285,7 +285,7 @@ pub async fn download_adapter_from_github( } if !adapter_path.exists() { - fs.create_dir(&adapter_path.as_path()) + fs.create_dir(adapter_path.as_path()) .await .context("Failed creating adapter path")?; } diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index 22d8262b93e36b17e548ae4dcc9bb725da8ca7cb..db8a45ceb49963eab053f3e7728d4cf715e9a40b 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -36,7 +36,7 @@ impl GoDebugAdapter { delegate: &Arc, ) -> Result { let release = latest_github_release( - &"zed-industries/delve-shim-dap", + "zed-industries/delve-shim-dap", true, false, delegate.http_client(), diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 2d19921a0f0c979fe53ede5860ac0c4d26b510c3..70b06381204a395e3f68a554497eb39aa6c4c1dd 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -514,7 +514,7 @@ impl DebugAdapter for JsDebugAdapter { } } - self.get_installed_binary(delegate, &config, user_installed_path, user_args, cx) + self.get_installed_binary(delegate, config, user_installed_path, user_args, cx) .await } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 7b90f80fe24145ddfd0371838eaa9e2f7787af84..6e80ec484c20bde6618bfde0199aa170b44f6514 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -717,7 +717,7 @@ impl DebugAdapter for PythonDebugAdapter { local_path.display() ); return self - .get_installed_binary(delegate, &config, Some(local_path.clone()), user_args, None) + .get_installed_binary(delegate, config, Some(local_path.clone()), user_args, None) .await; } @@ -754,7 +754,7 @@ impl DebugAdapter for PythonDebugAdapter { return self .get_installed_binary( delegate, - &config, + config, None, user_args, Some(toolchain.path.to_string()), @@ -762,7 +762,7 @@ impl DebugAdapter for PythonDebugAdapter { .await; } - self.get_installed_binary(delegate, &config, None, user_args, None) + self.get_installed_binary(delegate, config, None, user_args, None) .await } diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index de55212cbadfdd3c66ede66b706a3d120dd765c5..7fed761f5a05ea6638f1718cbbe115b16f6a54c5 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -238,7 +238,7 @@ mod tests { .unwrap(); let _bad_db = open_db::( tempdir.path(), - &release_channel::ReleaseChannel::Dev.dev_name(), + release_channel::ReleaseChannel::Dev.dev_name(), ) .await; } @@ -279,7 +279,7 @@ mod tests { { let corrupt_db = open_db::( tempdir.path(), - &release_channel::ReleaseChannel::Dev.dev_name(), + release_channel::ReleaseChannel::Dev.dev_name(), ) .await; assert!(corrupt_db.persistent()); @@ -287,7 +287,7 @@ mod tests { let good_db = open_db::( tempdir.path(), - &release_channel::ReleaseChannel::Dev.dev_name(), + release_channel::ReleaseChannel::Dev.dev_name(), ) .await; assert!( @@ -334,7 +334,7 @@ mod tests { // Setup the bad database let corrupt_db = open_db::( tempdir.path(), - &release_channel::ReleaseChannel::Dev.dev_name(), + release_channel::ReleaseChannel::Dev.dev_name(), ) .await; assert!(corrupt_db.persistent()); @@ -347,7 +347,7 @@ mod tests { let guard = thread::spawn(move || { let good_db = smol::block_on(open_db::( tmp_path.as_path(), - &release_channel::ReleaseChannel::Dev.dev_name(), + release_channel::ReleaseChannel::Dev.dev_name(), )); assert!( good_db.select_row::("SELECT * FROM test2").unwrap()() diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index b806381d251c6595a5dd12022dc3d1df8b71739f..14154e5b39d723f5835c1e0f92d9245866eea549 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -485,7 +485,7 @@ impl LogStore { &mut self, id: &LogStoreEntryIdentifier<'_>, ) -> Option<&Vec> { - self.get_debug_adapter_state(&id) + self.get_debug_adapter_state(id) .map(|state| &state.rpc_messages.initialization_sequence) } } @@ -536,11 +536,11 @@ impl Render for DapLogToolbarItemView { }) .unwrap_or_else(|| "No adapter selected".into()), )) - .menu(move |mut window, cx| { + .menu(move |window, cx| { let log_view = log_view.clone(); let menu_rows = menu_rows.clone(); let project = project.clone(); - ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| { + ContextMenu::build(window, cx, move |mut menu, window, _cx| { for row in menu_rows.into_iter() { menu = menu.custom_row(move |_window, _cx| { div() @@ -1131,7 +1131,7 @@ impl LogStore { project: &WeakEntity, session_id: SessionId, ) -> Vec { - self.projects.get(&project).map_or(vec![], |state| { + self.projects.get(project).map_or(vec![], |state| { state .debug_sessions .get(&session_id) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 1d44c5c2448afba50f682ea8ae96da8d3104945f..cf038871bc917cfbff8678ebb0091da43c693000 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -693,7 +693,7 @@ impl DebugPanel { ) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, _window, cx| { this.pause_thread(cx); }, @@ -719,7 +719,7 @@ impl DebugPanel { ) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, _window, cx| this.continue_thread(cx), )) .disabled(thread_status != ThreadStatus::Stopped) @@ -742,7 +742,7 @@ impl DebugPanel { IconButton::new("debug-step-over", IconName::ArrowRight) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, _window, cx| { this.step_over(cx); }, @@ -768,7 +768,7 @@ impl DebugPanel { ) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, _window, cx| { this.step_in(cx); }, @@ -791,7 +791,7 @@ impl DebugPanel { IconButton::new("debug-step-out", IconName::ArrowUpRight) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, _window, cx| { this.step_out(cx); }, @@ -815,7 +815,7 @@ impl DebugPanel { IconButton::new("debug-restart", IconName::RotateCcw) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, window, cx| { this.rerun_session(window, cx); }, @@ -837,7 +837,7 @@ impl DebugPanel { IconButton::new("debug-stop", IconName::Power) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, _window, cx| { if this.session().read(cx).is_building() { this.session().update(cx, |session, cx| { @@ -892,7 +892,7 @@ impl DebugPanel { ) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, _, cx| { this.detach_client(cx); }, @@ -1160,7 +1160,7 @@ impl DebugPanel { workspace .project() .read(cx) - .project_path_for_absolute_path(&path, cx) + .project_path_for_absolute_path(path, cx) .context( "Couldn't get project path for .zed/debug.json in active worktree", ) diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index 4ac8e371a15052a00ed962480a9f694a8802007c..51ea25a5cb925f6e3c57964159280f6336db1c69 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -413,7 +413,7 @@ impl NewProcessModal { let Some(adapter) = self.debugger.as_ref() else { return; }; - let scenario = self.debug_scenario(&adapter, cx); + let scenario = self.debug_scenario(adapter, cx); cx.spawn_in(window, async move |this, cx| { let scenario = scenario.await.context("no scenario to save")?; let worktree_id = task_contexts @@ -659,12 +659,7 @@ impl Render for NewProcessModal { this.mode = NewProcessMode::Attach; if let Some(debugger) = this.debugger.as_ref() { - Self::update_attach_picker( - &this.attach_mode, - &debugger, - window, - cx, - ); + Self::update_attach_picker(&this.attach_mode, debugger, window, cx); } this.mode_focus_handle(cx).focus(window); cx.notify(); @@ -1083,7 +1078,7 @@ impl DebugDelegate { .into_iter() .map(|(scenario, context)| { let (kind, scenario) = - Self::get_scenario_kind(&languages, &dap_registry, scenario); + Self::get_scenario_kind(&languages, dap_registry, scenario); (kind, scenario, Some(context)) }) .chain( @@ -1100,7 +1095,7 @@ impl DebugDelegate { .filter(|(_, scenario)| valid_adapters.contains(&scenario.adapter)) .map(|(kind, scenario)| { let (language, scenario) = - Self::get_scenario_kind(&languages, &dap_registry, scenario); + Self::get_scenario_kind(&languages, dap_registry, scenario); (language.or(Some(kind)), scenario, None) }), ) diff --git a/crates/debugger_ui/src/persistence.rs b/crates/debugger_ui/src/persistence.rs index 3a0ad7a40e60d4dc28f2086b94a0a43186978542..f0d7fd6fddff66ed7846db6fa9e712e11436ea42 100644 --- a/crates/debugger_ui/src/persistence.rs +++ b/crates/debugger_ui/src/persistence.rs @@ -341,7 +341,7 @@ impl SerializedPaneLayout { pub(crate) fn in_order(&self) -> Vec { let mut panes = vec![]; - Self::inner_in_order(&self, &mut panes); + Self::inner_in_order(self, &mut panes); panes } diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index f3117aee0797e2dd183a25a31bbe50ea560f21bc..3c1d35cdd33c2051a7e44f0eecf8fc8a2bc2a797 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -102,7 +102,7 @@ impl Render for RunningState { .find(|pane| pane.read(cx).is_zoomed()); let active = self.panes.panes().into_iter().next(); - let pane = if let Some(ref zoomed_pane) = zoomed_pane { + let pane = if let Some(zoomed_pane) = zoomed_pane { zoomed_pane.update(cx, |pane, cx| pane.render(window, cx).into_any_element()) } else if let Some(active) = active { self.panes @@ -627,7 +627,7 @@ impl RunningState { if s.starts_with("\"$ZED_") && s.ends_with('"') { *s = s[1..s.len() - 1].to_string(); } - if let Some(substituted) = substitute_variables_in_str(&s, context) { + if let Some(substituted) = substitute_variables_in_str(s, context) { *s = substituted; } } @@ -657,7 +657,7 @@ impl RunningState { } resolve_path(s); - if let Some(substituted) = substitute_variables_in_str(&s, context) { + if let Some(substituted) = substitute_variables_in_str(s, context) { *s = substituted; } } @@ -954,7 +954,7 @@ impl RunningState { inventory.read(cx).task_template_by_label( buffer, worktree_id, - &label, + label, cx, ) }) @@ -1310,7 +1310,7 @@ impl RunningState { let mut pane_item_status = IndexMap::from_iter( DebuggerPaneItem::all() .iter() - .filter(|kind| kind.is_supported(&caps)) + .filter(|kind| kind.is_supported(caps)) .map(|kind| (*kind, false)), ); self.panes.panes().iter().for_each(|pane| { @@ -1371,7 +1371,7 @@ impl RunningState { this.serialize_layout(window, cx); match event { Event::Remove { .. } => { - let _did_find_pane = this.panes.remove(&source_pane).is_ok(); + let _did_find_pane = this.panes.remove(source_pane).is_ok(); debug_assert!(_did_find_pane); cx.notify(); } diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 38108dbfbcc62e777ea9ee9aa9f1ab1f7d2c2f3d..9768f02e8e9b60350e9938c297ea7e7db6629ea6 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -494,7 +494,7 @@ impl BreakpointList { fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context) { if let Some(session) = &self.session { session.update(cx, |this, cx| { - this.toggle_data_breakpoint(&id, cx); + this.toggle_data_breakpoint(id, cx); }); } } @@ -502,7 +502,7 @@ impl BreakpointList { fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context) { if let Some(session) = &self.session { session.update(cx, |this, cx| { - this.toggle_exception_breakpoint(&id, cx); + this.toggle_exception_breakpoint(id, cx); }); cx.notify(); const EXCEPTION_SERIALIZATION_INTERVAL: Duration = Duration::from_secs(1); diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index e6308518e4dea66e6ef155e3dbf6ccfa74c18f55..42989ddc209efcbeb5044d8e5c3b85d6afb9f739 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -697,7 +697,7 @@ impl ConsoleQueryBarCompletionProvider { new_bytes: &[u8], snapshot: &TextBufferSnapshot, ) -> Range { - let buffer_offset = buffer_position.to_offset(&snapshot); + let buffer_offset = buffer_position.to_offset(snapshot); let buffer_bytes = &buffer_text.as_bytes()[0..buffer_offset]; let mut prefix_len = 0; @@ -977,7 +977,7 @@ mod tests { &cx.buffer_text(), snapshot.anchor_before(buffer_position), replacement.as_bytes(), - &snapshot, + snapshot, ); cx.update_editor(|editor, _, cx| { diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index f936d908b157ae2631a20b78bfe9fcea26b47b96..a09df6e728d061307b445e4b4048a04151d87cdf 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -262,7 +262,7 @@ impl MemoryView { cx: &mut Context, ) { use parse_int::parse; - let Ok(as_address) = parse::(&memory_reference) else { + let Ok(as_address) = parse::(memory_reference) else { return; }; let access_size = evaluate_name @@ -931,7 +931,7 @@ impl Render for MemoryView { v_flex() .size_full() .on_drag_move(cx.listener(|this, evt, _, _| { - this.handle_memory_drag(&evt); + this.handle_memory_drag(evt); })) .child(self.render_memory(cx).size_full()) .children(self.open_context_menu.as_ref().map(|(menu, position, _)| { diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index efbc72e8cfc9099a5d699493898440d17fbf615b..b54ee29e158536e12b5669cbebd6ba1a853fbbb6 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -1289,7 +1289,7 @@ impl VariableList { }), ) .child(self.render_variable_value( - &entry, + entry, &variable_color, watcher.value.to_string(), cx, @@ -1494,7 +1494,7 @@ impl VariableList { }), ) .child(self.render_variable_value( - &variable, + variable, &variable_color, dap.value.clone(), cx, diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index 906a7a0d4bd76f0451d6b5d5cfa5beff0136c613..80e2b73d5a100bbd21462f0ad80def1997e184de 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -139,7 +139,7 @@ async fn test_show_attach_modal_and_select_process( workspace .update(cx, |_, window, cx| { let names = - attach_modal.update(cx, |modal, cx| attach_modal::_process_names(&modal, cx)); + attach_modal.update(cx, |modal, cx| attach_modal::_process_names(modal, cx)); // Initially all processes are visible. assert_eq!(3, names.len()); attach_modal.update(cx, |this, cx| { @@ -154,7 +154,7 @@ async fn test_show_attach_modal_and_select_process( workspace .update(cx, |_, _, cx| { let names = - attach_modal.update(cx, |modal, cx| attach_modal::_process_names(&modal, cx)); + attach_modal.update(cx, |modal, cx| attach_modal::_process_names(modal, cx)); // Initially all processes are visible. assert_eq!(2, names.len()); }) diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index d6b0dfa00429f9487eafbe38dca5f072ed547779..5ac6af389d23eecba8180f9f7ddf7de279f2cc83 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -107,7 +107,7 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths( let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") { input_path - .replace("$ZED_WORKTREE_ROOT", &path!("/test/worktree/path")) + .replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path")) .to_owned() } else { input_path.to_string() diff --git a/crates/diagnostics/src/diagnostic_renderer.rs b/crates/diagnostics/src/diagnostic_renderer.rs index ce7b253702a01e24e7f4a457ac418572e0fa2729..cb1c052925cb7a3117b817406e0caec0adfc568f 100644 --- a/crates/diagnostics/src/diagnostic_renderer.rs +++ b/crates/diagnostics/src/diagnostic_renderer.rs @@ -46,7 +46,7 @@ impl DiagnosticRenderer { markdown.push_str(" ("); } if let Some(source) = diagnostic.source.as_ref() { - markdown.push_str(&Markdown::escape(&source)); + markdown.push_str(&Markdown::escape(source)); } if diagnostic.source.is_some() && diagnostic.code.is_some() { markdown.push(' '); @@ -306,7 +306,7 @@ impl DiagnosticBlock { cx: &mut Context, ) { let snapshot = &editor.buffer().read(cx).snapshot(cx); - let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot); + let range = range.start.to_offset(snapshot)..range.end.to_offset(snapshot); editor.unfold_ranges(&[range.start..range.end], true, false, cx); editor.change_selections(Default::default(), window, cx, |s| { diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index e7660920da30ddcc088c2bbee6bfb1cf05d51d58..23dbf333227e03da6d329d98d760754670354108 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -528,7 +528,7 @@ impl ProjectDiagnosticsEditor { lsp::DiagnosticSeverity::ERROR }; - cx.spawn_in(window, async move |this, mut cx| { + cx.spawn_in(window, async move |this, cx| { let diagnostics = buffer_snapshot .diagnostics_in_range::<_, text::Anchor>( Point::zero()..buffer_snapshot.max_point(), @@ -595,7 +595,7 @@ impl ProjectDiagnosticsEditor { b.initial_range.clone(), DEFAULT_MULTIBUFFER_CONTEXT, buffer_snapshot.clone(), - &mut cx, + cx, ) .await; let i = excerpt_ranges diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 17804b428145ed49d6bb274ab5f13d5b46e5f7f4..29011352fbb565a8ead2f1e3d6e23258682fc246 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -129,7 +129,7 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet) let Some((name, value)) = line.split_once(':') else { errors.insert(PreprocessorError::InvalidFrontmatterLine(format!( "{}: {}", - chapter_breadcrumbs(&chapter), + chapter_breadcrumbs(chapter), line ))); continue; @@ -402,11 +402,11 @@ fn handle_postprocessing() -> Result<()> { path: &'a std::path::PathBuf, root: &'a std::path::PathBuf, ) -> &'a std::path::Path { - &path.strip_prefix(&root).unwrap_or(&path) + path.strip_prefix(&root).unwrap_or(path) } fn extract_title_from_page(contents: &str, pretty_path: &std::path::Path) -> String { let title_tag_contents = &title_regex() - .captures(&contents) + .captures(contents) .with_context(|| format!("Failed to find title in {:?}", pretty_path)) .expect("Page has element")[1]; let title = title_tag_contents diff --git a/crates/editor/src/clangd_ext.rs b/crates/editor/src/clangd_ext.rs index 3239fdc653e0e2acdbdaa3396e30c0546ef259cf..07be9ea9e92d1f5db0d4cae343f83ab3a9480526 100644 --- a/crates/editor/src/clangd_ext.rs +++ b/crates/editor/src/clangd_ext.rs @@ -104,6 +104,6 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: & .filter_map(|buffer| buffer.read(cx).language()) .any(|language| is_c_language(language)) { - register_action(&editor, window, switch_source_header); + register_action(editor, window, switch_source_header); } } diff --git a/crates/editor/src/code_completion_tests.rs b/crates/editor/src/code_completion_tests.rs index fd8db29584d8eb6944ff674dd8bf5d860ce32428..a1d9f04a9c590ef1f20779bf19c2fe0be8905709 100644 --- a/crates/editor/src/code_completion_tests.rs +++ b/crates/editor/src/code_completion_tests.rs @@ -317,7 +317,7 @@ async fn filter_and_sort_matches( let candidates: Arc<[StringMatchCandidate]> = completions .iter() .enumerate() - .map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text())) + .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text())) .collect(); let cancel_flag = Arc::new(AtomicBool::new(false)); let background_executor = cx.executor(); @@ -331,5 +331,5 @@ async fn filter_and_sort_matches( background_executor, ) .await; - CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, &completions) + CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, completions) } diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 4ae2a14ca730dafa7cfecd9e9b3bacbe3f7bc47b..24d2cfddcb09ba1ecc24b3d2702c15b693a1a3e0 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -321,7 +321,7 @@ impl CompletionsMenu { let match_candidates = choices .iter() .enumerate() - .map(|(id, completion)| StringMatchCandidate::new(id, &completion)) + .map(|(id, completion)| StringMatchCandidate::new(id, completion)) .collect(); let entries = choices .iter() diff --git a/crates/editor/src/display_map/custom_highlights.rs b/crates/editor/src/display_map/custom_highlights.rs index ae69e9cf8c710acecc840ef14082c8f9d91d7c03..f3737ea4b7cc3f504b6288a2e4241977c8ffd20e 100644 --- a/crates/editor/src/display_map/custom_highlights.rs +++ b/crates/editor/src/display_map/custom_highlights.rs @@ -77,7 +77,7 @@ fn create_highlight_endpoints( let ranges = &text_highlights.1; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&start, &buffer); + let cmp = probe.end.cmp(&start, buffer); if cmp.is_gt() { cmp::Ordering::Greater } else { @@ -88,18 +88,18 @@ fn create_highlight_endpoints( }; for range in &ranges[start_ix..] { - if range.start.cmp(&end, &buffer).is_ge() { + if range.start.cmp(&end, buffer).is_ge() { break; } highlight_endpoints.push(HighlightEndpoint { - offset: range.start.to_offset(&buffer), + offset: range.start.to_offset(buffer), is_start: true, tag, style, }); highlight_endpoints.push(HighlightEndpoint { - offset: range.end.to_offset(&buffer), + offset: range.end.to_offset(buffer), is_start: false, tag, style, diff --git a/crates/editor/src/display_map/invisibles.rs b/crates/editor/src/display_map/invisibles.rs index 199986f2a41c82894acc0259be578a28e785a235..19e4c2b42aa32448234fa2219c8cc656431e781e 100644 --- a/crates/editor/src/display_map/invisibles.rs +++ b/crates/editor/src/display_map/invisibles.rs @@ -36,8 +36,8 @@ pub fn is_invisible(c: char) -> bool { } else if c >= '\u{7f}' { c <= '\u{9f}' || (c.is_whitespace() && c != IDEOGRAPHIC_SPACE) - || contains(c, &FORMAT) - || contains(c, &OTHER) + || contains(c, FORMAT) + || contains(c, OTHER) } else { false } @@ -50,7 +50,7 @@ pub fn replacement(c: char) -> Option<&'static str> { Some(C0_SYMBOLS[c as usize]) } else if c == '\x7f' { Some(DEL) - } else if contains(c, &PRESERVE) { + } else if contains(c, PRESERVE) { None } else { Some("\u{2007}") // fixed width space diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index caa4882a6ebbb00aaa1e498e49dfb530153a0e8e..0d2d1c4a4cc7f72b396631330e210b691a2615cb 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1461,7 +1461,7 @@ mod tests { } let mut prev_ix = 0; - for boundary in line_wrapper.wrap_line(&[LineFragment::text(&line)], wrap_width) { + for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) { wrapped_text.push_str(&line[prev_ix..boundary.ix]); wrapped_text.push('\n'); wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e645bfee6738540d7a099c1a69718933afeda331..6edd4e9d8c7579427f5bc14bb68b5a8a0e9fab8a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2379,7 +2379,7 @@ impl Editor { pending_selection .selection .range() - .includes(&range, &snapshot) + .includes(range, &snapshot) }) { return true; @@ -3342,9 +3342,9 @@ impl Editor { let old_cursor_position = &state.old_cursor_position; - self.selections_did_change(true, &old_cursor_position, state.effects, window, cx); + self.selections_did_change(true, old_cursor_position, state.effects, window, cx); - if self.should_open_signature_help_automatically(&old_cursor_position, cx) { + if self.should_open_signature_help_automatically(old_cursor_position, cx) { self.show_signature_help(&ShowSignatureHelp, window, cx); } } @@ -3764,9 +3764,9 @@ impl Editor { ColumnarSelectionState::FromMouse { selection_tail, display_point, - } => display_point.unwrap_or_else(|| selection_tail.to_display_point(&display_map)), + } => display_point.unwrap_or_else(|| selection_tail.to_display_point(display_map)), ColumnarSelectionState::FromSelection { selection_tail } => { - selection_tail.to_display_point(&display_map) + selection_tail.to_display_point(display_map) } }; @@ -6082,7 +6082,7 @@ impl Editor { if let Some(tasks) = &tasks { if let Some(project) = project { task_context_task = - Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx); + Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx); } } @@ -6864,7 +6864,7 @@ impl Editor { for (buffer_snapshot, search_range, excerpt_id) in buffer_ranges { match_ranges.extend( regex - .search(&buffer_snapshot, Some(search_range.clone())) + .search(buffer_snapshot, Some(search_range.clone())) .await .into_iter() .filter_map(|match_range| { @@ -7206,7 +7206,7 @@ impl Editor { return Some(false); } let provider = self.edit_prediction_provider()?; - if !provider.is_enabled(&buffer, buffer_position, cx) { + if !provider.is_enabled(buffer, buffer_position, cx) { return Some(false); } let buffer = buffer.read(cx); @@ -7966,7 +7966,7 @@ impl Editor { let multi_buffer_anchor = Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), breakpoint.position); let position = multi_buffer_anchor - .to_point(&multi_buffer_snapshot) + .to_point(multi_buffer_snapshot) .to_display_point(&snapshot); breakpoint_display_points.insert( @@ -8859,7 +8859,7 @@ impl Editor { } let highlighted_edits = if let Some(edit_preview) = edit_preview.as_ref() { - crate::edit_prediction_edit_text(&snapshot, edits, edit_preview, false, cx) + crate::edit_prediction_edit_text(snapshot, edits, edit_preview, false, cx) } else { // Fallback for providers without edit_preview crate::edit_prediction_fallback_text(edits, cx) @@ -9222,7 +9222,7 @@ impl Editor { .child(div().px_1p5().child(match &prediction.completion { EditPrediction::Move { target, snapshot } => { use text::ToPoint as _; - if target.text_anchor.to_point(&snapshot).row > cursor_point.row + if target.text_anchor.to_point(snapshot).row > cursor_point.row { Icon::new(IconName::ZedPredictDown) } else { @@ -9424,7 +9424,7 @@ impl Editor { .gap_2() .flex_1() .child( - if target.text_anchor.to_point(&snapshot).row > cursor_point.row { + if target.text_anchor.to_point(snapshot).row > cursor_point.row { Icon::new(IconName::ZedPredictDown) } else { Icon::new(IconName::ZedPredictUp) @@ -9440,14 +9440,14 @@ impl Editor { snapshot, display_mode: _, } => { - let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row; + let first_edit_row = edits.first()?.0.start.text_anchor.to_point(snapshot).row; let (highlighted_edits, has_more_lines) = if let Some(edit_preview) = edit_preview.as_ref() { - crate::edit_prediction_edit_text(&snapshot, &edits, edit_preview, true, cx) + crate::edit_prediction_edit_text(snapshot, edits, edit_preview, true, cx) .first_line_preview() } else { - crate::edit_prediction_fallback_text(&edits, cx).first_line_preview() + crate::edit_prediction_fallback_text(edits, cx).first_line_preview() }; let styled_text = gpui::StyledText::new(highlighted_edits.text) @@ -9770,7 +9770,7 @@ impl Editor { if let Some(choices) = &snippet.choices[snippet.active_index] { if let Some(selection) = current_ranges.first() { - self.show_snippet_choices(&choices, selection.clone(), cx); + self.show_snippet_choices(choices, selection.clone(), cx); } } @@ -12284,7 +12284,7 @@ impl Editor { let trigger_in_words = this.show_edit_predictions_in_menu() || !had_active_edit_prediction; - this.trigger_completion_on_input(&text, trigger_in_words, window, cx); + this.trigger_completion_on_input(text, trigger_in_words, window, cx); }); } @@ -17896,7 +17896,7 @@ impl Editor { ranges: &[Range<Anchor>], snapshot: &MultiBufferSnapshot, ) -> bool { - let mut hunks = self.diff_hunks_in_ranges(ranges, &snapshot); + let mut hunks = self.diff_hunks_in_ranges(ranges, snapshot); hunks.any(|hunk| hunk.status().has_secondary_hunk()) } @@ -19042,8 +19042,8 @@ impl Editor { buffer_ranges.last() }?; - let selection = text::ToPoint::to_point(&range.start, &buffer).row - ..text::ToPoint::to_point(&range.end, &buffer).row; + let selection = text::ToPoint::to_point(&range.start, buffer).row + ..text::ToPoint::to_point(&range.end, buffer).row; Some(( multi_buffer.buffer(buffer.remote_id()).unwrap().clone(), selection, @@ -20055,8 +20055,7 @@ impl Editor { self.registered_buffers .entry(edited_buffer.read(cx).remote_id()) .or_insert_with(|| { - project - .register_buffer_with_language_servers(&edited_buffer, cx) + project.register_buffer_with_language_servers(edited_buffer, cx) }); }); } @@ -21079,7 +21078,7 @@ impl Editor { }; if let Some((workspace, path)) = workspace.as_ref().zip(path) { let Some(task) = cx - .update_window_entity(&workspace, |workspace, window, cx| { + .update_window_entity(workspace, |workspace, window, cx| { workspace .open_path_preview(path, None, false, false, false, window, cx) }) @@ -21303,14 +21302,14 @@ fn process_completion_for_edit( debug_assert!( insert_range .start - .cmp(&cursor_position, &buffer_snapshot) + .cmp(cursor_position, &buffer_snapshot) .is_le(), "insert_range should start before or at cursor position" ); debug_assert!( replace_range .start - .cmp(&cursor_position, &buffer_snapshot) + .cmp(cursor_position, &buffer_snapshot) .is_le(), "replace_range should start before or at cursor position" ); @@ -21344,7 +21343,7 @@ fn process_completion_for_edit( LspInsertMode::ReplaceSuffix => { if replace_range .end - .cmp(&cursor_position, &buffer_snapshot) + .cmp(cursor_position, &buffer_snapshot) .is_gt() { let range_after_cursor = *cursor_position..replace_range.end; @@ -21380,7 +21379,7 @@ fn process_completion_for_edit( if range_to_replace .end - .cmp(&cursor_position, &buffer_snapshot) + .cmp(cursor_position, &buffer_snapshot) .is_lt() { range_to_replace.end = *cursor_position; @@ -21388,7 +21387,7 @@ fn process_completion_for_edit( CompletionEdit { new_text, - replace_range: range_to_replace.to_offset(&buffer), + replace_range: range_to_replace.to_offset(buffer), snippet, } } @@ -22137,7 +22136,7 @@ fn snippet_completions( snippet .prefix .iter() - .map(move |prefix| StringMatchCandidate::new(ix, &prefix)) + .map(move |prefix| StringMatchCandidate::new(ix, prefix)) }) .collect::<Vec<StringMatchCandidate>>(); @@ -22366,10 +22365,10 @@ impl SemanticsProvider for Entity<Project> { cx: &mut App, ) -> Option<Task<Result<Vec<LocationLink>>>> { Some(self.update(cx, |project, cx| match kind { - GotoDefinitionKind::Symbol => project.definitions(&buffer, position, cx), - GotoDefinitionKind::Declaration => project.declarations(&buffer, position, cx), - GotoDefinitionKind::Type => project.type_definitions(&buffer, position, cx), - GotoDefinitionKind::Implementation => project.implementations(&buffer, position, cx), + GotoDefinitionKind::Symbol => project.definitions(buffer, position, cx), + GotoDefinitionKind::Declaration => project.declarations(buffer, position, cx), + GotoDefinitionKind::Type => project.type_definitions(buffer, position, cx), + GotoDefinitionKind::Implementation => project.implementations(buffer, position, cx), })) } @@ -23778,7 +23777,7 @@ fn all_edits_insertions_or_deletions( let mut all_deletions = true; for (range, new_text) in edits.iter() { - let range_is_empty = range.to_offset(&snapshot).is_empty(); + let range_is_empty = range.to_offset(snapshot).is_empty(); let text_is_empty = new_text.is_empty(); if range_is_empty != text_is_empty { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index f97dcd712c99959ae4aee22ff0b43fbf59669fd8..189bdd1bf7d73250b2b9eaa7e1b7cecf39740eb0 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -8393,7 +8393,7 @@ async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) buffer.set_language(Some(language), cx); }); - cx.set_state(&r#"struct A {ˇ}"#); + cx.set_state(r#"struct A {ˇ}"#); cx.update_editor(|editor, window, cx| { editor.newline(&Default::default(), window, cx); @@ -8405,7 +8405,7 @@ async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) }" )); - cx.set_state(&r#"select_biased!(ˇ)"#); + cx.set_state(r#"select_biased!(ˇ)"#); cx.update_editor(|editor, window, cx| { editor.newline(&Default::default(), window, cx); @@ -12319,7 +12319,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) let counter = Arc::new(AtomicUsize::new(0)); handle_completion_request_with_insert_and_replace( &mut cx, - &buffer_marked_text, + buffer_marked_text, vec![(completion_text, completion_text)], counter.clone(), ) @@ -12333,7 +12333,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) .confirm_completion_replace(&ConfirmCompletionReplace, window, cx) .unwrap() }); - cx.assert_editor_state(&expected_with_replace_mode); + cx.assert_editor_state(expected_with_replace_mode); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); @@ -12353,7 +12353,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) }); handle_completion_request_with_insert_and_replace( &mut cx, - &buffer_marked_text, + buffer_marked_text, vec![(completion_text, completion_text)], counter.clone(), ) @@ -12367,7 +12367,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) .confirm_completion_insert(&ConfirmCompletionInsert, window, cx) .unwrap() }); - cx.assert_editor_state(&expected_with_insert_mode); + cx.assert_editor_state(expected_with_insert_mode); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); } @@ -13141,7 +13141,7 @@ async fn test_word_completion(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["first", "last"], "When LSP server is fast to reply, no fallback word completions are used" ); @@ -13164,7 +13164,7 @@ async fn test_word_completion(cx: &mut TestAppContext) { cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"], + assert_eq!(completion_menu_entries(menu), &["one", "three", "two"], "When LSP server is slow, document words can be shown instead, if configured accordingly"); } else { panic!("expected completion menu to be open"); @@ -13225,7 +13225,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["first", "last", "second"], "Word completions that has the same edit as the any of the LSP ones, should not be proposed" ); @@ -13281,7 +13281,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["first", "last", "second"], "`ShowWordCompletions` action should show word completions" ); @@ -13298,7 +13298,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["last"], "After showing word completions, further editing should filter them and not query the LSP" ); @@ -13337,7 +13337,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["let"], "With no digits in the completion query, no digits should be in the word completions" ); @@ -13362,7 +13362,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \ + assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \ return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)"); } else { panic!("expected completion menu to be open"); @@ -13599,7 +13599,7 @@ async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) { cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["first", "last"]); + assert_eq!(completion_menu_entries(menu), &["first", "last"]); } else { panic!("expected completion menu to be open"); } @@ -16702,7 +16702,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["bg-blue", "bg-red", "bg-yellow"] ); } else { @@ -16715,7 +16715,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]); + assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]); } else { panic!("expected completion menu to be open"); } @@ -16729,7 +16729,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]); + assert_eq!(completion_menu_entries(menu), &["bg-yellow"]); } else { panic!("expected completion menu to be open"); } @@ -17298,7 +17298,7 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) { (buffer_2.clone(), base_text_2), (buffer_3.clone(), base_text_3), ] { - let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx)); + let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx)); editor .buffer .update(cx, |buffer, cx| buffer.add_diff(diff, cx)); @@ -17919,7 +17919,7 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) { (buffer_2.clone(), file_2_old), (buffer_3.clone(), file_3_old), ] { - let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx)); + let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx)); editor .buffer .update(cx, |buffer, cx| buffer.add_diff(diff, cx)); @@ -21024,7 +21024,7 @@ async fn assert_highlighted_edits( cx.update(|_window, cx| { let highlighted_edits = edit_prediction_edit_text( - &snapshot.as_singleton().unwrap().2, + snapshot.as_singleton().unwrap().2, &edits, &edit_preview, include_deletions, @@ -21091,7 +21091,7 @@ fn add_log_breakpoint_at_cursor( .buffer_snapshot .anchor_before(Point::new(cursor_position.row, 0)); - (breakpoint_position, Breakpoint::new_log(&log_message)) + (breakpoint_position, Breakpoint::new_log(log_message)) }); editor.edit_breakpoint_at_anchor( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e56ac45fab86efe15660f73c07e39fd4b0b94de6..927a207358e2ee1544ca2b7938c39d23caaee9a0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1162,7 +1162,7 @@ impl EditorElement { .map_or(false, |state| state.keyboard_grace); if mouse_over_inline_blame || mouse_over_popover { - editor.show_blame_popover(&blame_entry, event.position, false, cx); + editor.show_blame_popover(blame_entry, event.position, false, cx); } else if !keyboard_grace { editor.hide_blame_popover(cx); } @@ -2818,7 +2818,7 @@ impl EditorElement { } let row = - MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(&snapshot).row); + MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(snapshot).row); if snapshot.is_line_folded(row) { return None; } @@ -3312,7 +3312,7 @@ impl EditorElement { let chunks = snapshot.highlighted_chunks(rows.clone(), true, style); LineWithInvisibles::from_chunks( chunks, - &style, + style, MAX_LINE_LEN, rows.len(), &snapshot.mode, @@ -3393,7 +3393,7 @@ impl EditorElement { let line_ix = align_to.row().0.checked_sub(rows.start.0); x_position = if let Some(layout) = line_ix.and_then(|ix| line_layouts.get(ix as usize)) { - x_and_width(&layout) + x_and_width(layout) } else { x_and_width(&layout_line( align_to.row(), @@ -5549,9 +5549,9 @@ impl EditorElement { // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor. // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor. if is_singleton { - window.set_cursor_style(CursorStyle::IBeam, &hitbox); + window.set_cursor_style(CursorStyle::IBeam, hitbox); } else { - window.set_cursor_style(CursorStyle::PointingHand, &hitbox); + window.set_cursor_style(CursorStyle::PointingHand, hitbox); } } } @@ -5570,7 +5570,7 @@ impl EditorElement { &layout.position_map.snapshot, line_height, layout.gutter_hitbox.bounds, - &hunk, + hunk, ); Some(( hunk_bounds, @@ -6092,10 +6092,10 @@ impl EditorElement { if axis == ScrollbarAxis::Vertical { let fast_markers = - self.collect_fast_scrollbar_markers(layout, &scrollbar_layout, cx); + self.collect_fast_scrollbar_markers(layout, scrollbar_layout, cx); // Refresh slow scrollbar markers in the background. Below, we // paint whatever markers have already been computed. - self.refresh_slow_scrollbar_markers(layout, &scrollbar_layout, window, cx); + self.refresh_slow_scrollbar_markers(layout, scrollbar_layout, window, cx); let markers = self.editor.read(cx).scrollbar_marker_state.markers.clone(); for marker in markers.iter().chain(&fast_markers) { @@ -6129,7 +6129,7 @@ impl EditorElement { if any_scrollbar_dragged { window.set_window_cursor_style(CursorStyle::Arrow); } else { - window.set_cursor_style(CursorStyle::Arrow, &hitbox); + window.set_cursor_style(CursorStyle::Arrow, hitbox); } } }) @@ -9782,7 +9782,7 @@ pub fn layout_line( let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style); LineWithInvisibles::from_chunks( chunks, - &style, + style, MAX_LINE_LEN, 1, &snapshot.mode, diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 02f93e6829a3f7ac08ec7dfa390cd846560bb7d5..8b6e2cea842ecc331ca94ffc43611056b302e38c 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -794,7 +794,7 @@ pub(crate) async fn find_file( ) -> Option<ResolvedPath> { project .update(cx, |project, cx| { - project.resolve_path_in_buffer(&candidate_file_path, buffer, cx) + project.resolve_path_in_buffer(candidate_file_path, buffer, cx) }) .ok()? .await diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 34533002ff2cea587c4179d6f0f0770ff53b4b98..22430ab5e174a95f87db36e7d2002c5dfe2ce479 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -524,8 +524,8 @@ fn serialize_selection( ) -> proto::Selection { proto::Selection { id: selection.id as u64, - start: Some(serialize_anchor(&selection.start, &buffer)), - end: Some(serialize_anchor(&selection.end, &buffer)), + start: Some(serialize_anchor(&selection.start, buffer)), + end: Some(serialize_anchor(&selection.end, buffer)), reversed: selection.reversed, } } @@ -1010,7 +1010,7 @@ impl Item for Editor { self.workspace = Some((workspace.weak_handle(), workspace.database_id())); if let Some(workspace) = &workspace.weak_handle().upgrade() { cx.subscribe( - &workspace, + workspace, |editor, _, event: &workspace::Event, _cx| match event { workspace::Event::ModalOpened => { editor.mouse_context_menu.take(); @@ -1296,7 +1296,7 @@ impl SerializableItem for Editor { project .read(cx) .worktree_for_id(worktree_id, cx) - .and_then(|worktree| worktree.read(cx).absolutize(&file.path()).ok()) + .and_then(|worktree| worktree.read(cx).absolutize(file.path()).ok()) .or_else(|| { let full_path = file.full_path(cx); let project_path = project.read(cx).find_project_path(&full_path, cx)?; @@ -1385,14 +1385,14 @@ impl ProjectItem for Editor { }) { editor.fold_ranges( - clip_ranges(&restoration_data.folds, &snapshot), + clip_ranges(&restoration_data.folds, snapshot), false, window, cx, ); if !restoration_data.selections.is_empty() { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(clip_ranges(&restoration_data.selections, &snapshot)); + s.select_ranges(clip_ranges(&restoration_data.selections, snapshot)); }); } let (top_row, offset) = restoration_data.scroll_position; diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index 95a792583953e02a77e592ea957b752f0f8042bb..f358ab7b936c912f5a11fbe2ae6fc62a974ad56c 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -37,7 +37,7 @@ pub(crate) fn should_auto_close( let text = buffer .text_for_range(edited_range.clone()) .collect::<String>(); - let edited_range = edited_range.to_offset(&buffer); + let edited_range = edited_range.to_offset(buffer); if !text.ends_with(">") { continue; } diff --git a/crates/editor/src/lsp_colors.rs b/crates/editor/src/lsp_colors.rs index 08cf9078f2301e84ec96b49cbc1abb16eb611d68..29eb9f249abca71ff3cfbb9f5ad3f56464efe942 100644 --- a/crates/editor/src/lsp_colors.rs +++ b/crates/editor/src/lsp_colors.rs @@ -207,7 +207,7 @@ impl Editor { .entry(buffer_snapshot.remote_id()) .or_insert_with(Vec::new); let excerpt_point_range = - excerpt_range.context.to_point_utf16(&buffer_snapshot); + excerpt_range.context.to_point_utf16(buffer_snapshot); excerpt_data.push(( excerpt_id, buffer_snapshot.clone(), diff --git a/crates/editor/src/lsp_ext.rs b/crates/editor/src/lsp_ext.rs index 6161afbbc0377d377e352f357b5a0ea6b0606770..d02fc0f901e928619e42d7ff9ef7fb34351fdc28 100644 --- a/crates/editor/src/lsp_ext.rs +++ b/crates/editor/src/lsp_ext.rs @@ -76,7 +76,7 @@ async fn lsp_task_context( let project_env = project .update(cx, |project, cx| { - project.buffer_environment(&buffer, &worktree_store, cx) + project.buffer_environment(buffer, &worktree_store, cx) }) .ok()? .await; diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 9d5145dec1f380013fbf76776efd077d7b466a37..7f9eb374e8f3480f36b892246ac8f5c9d176c737 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -102,11 +102,11 @@ impl MouseContextMenu { let display_snapshot = &editor .display_map .update(cx, |display_map, cx| display_map.snapshot(cx)); - let selection_init_range = selection_init.display_range(&display_snapshot); + let selection_init_range = selection_init.display_range(display_snapshot); let selection_now_range = editor .selections .newest_anchor() - .display_range(&display_snapshot); + .display_range(display_snapshot); if selection_now_range == selection_init_range { return; } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index fdda0e82bca6a85b25042ad7e8a662ff2fdae49d..0bf875095b2f9e553f146096c377ce0b271a2ccf 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -439,17 +439,17 @@ pub fn start_of_excerpt( }; match direction { Direction::Prev => { - let mut start = excerpt.start_anchor().to_display_point(&map); + let mut start = excerpt.start_anchor().to_display_point(map); if start >= display_point && start.row() > DisplayRow(0) { let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else { return display_point; }; - start = excerpt.start_anchor().to_display_point(&map); + start = excerpt.start_anchor().to_display_point(map); } start } Direction::Next => { - let mut end = excerpt.end_anchor().to_display_point(&map); + let mut end = excerpt.end_anchor().to_display_point(map); *end.row_mut() += 1; map.clip_point(end, Bias::Right) } @@ -467,7 +467,7 @@ pub fn end_of_excerpt( }; match direction { Direction::Prev => { - let mut start = excerpt.start_anchor().to_display_point(&map); + let mut start = excerpt.start_anchor().to_display_point(map); if start.row() > DisplayRow(0) { *start.row_mut() -= 1; } @@ -476,7 +476,7 @@ pub fn end_of_excerpt( start } Direction::Next => { - let mut end = excerpt.end_anchor().to_display_point(&map); + let mut end = excerpt.end_anchor().to_display_point(map); *end.column_mut() = 0; if end <= display_point { *end.row_mut() += 1; @@ -485,7 +485,7 @@ pub fn end_of_excerpt( else { return display_point; }; - end = excerpt.end_anchor().to_display_point(&map); + end = excerpt.end_anchor().to_display_point(map); *end.column_mut() = 0; } end diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 1ead45b3de89c0705510f8afc55ecf6176a4d7a2..e549f64758b99199c67123d433447d45ec07bf00 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -478,7 +478,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { } fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &mut App) -> bool { - if let Some(buffer) = self.to_base(&buffer, &[], cx) { + if let Some(buffer) = self.to_base(buffer, &[], cx) { self.0.supports_inlay_hints(&buffer, cx) } else { false @@ -491,7 +491,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { position: text::Anchor, cx: &mut App, ) -> Option<Task<anyhow::Result<Vec<project::DocumentHighlight>>>> { - let buffer = self.to_base(&buffer, &[position], cx)?; + let buffer = self.to_base(buffer, &[position], cx)?; self.0.document_highlights(&buffer, position, cx) } @@ -502,7 +502,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { kind: crate::GotoDefinitionKind, cx: &mut App, ) -> Option<Task<anyhow::Result<Vec<project::LocationLink>>>> { - let buffer = self.to_base(&buffer, &[position], cx)?; + let buffer = self.to_base(buffer, &[position], cx)?; self.0.definitions(&buffer, position, kind, cx) } diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index 2b8150de67050ccced22100bfedd02be44f63907..bee9464124496cb027355aaa3ac464a792479fc9 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -35,12 +35,12 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: & .filter_map(|buffer| buffer.read(cx).language()) .any(|language| is_rust_language(language)) { - register_action(&editor, window, go_to_parent_module); - register_action(&editor, window, expand_macro_recursively); - register_action(&editor, window, open_docs); - register_action(&editor, window, cancel_flycheck_action); - register_action(&editor, window, run_flycheck_action); - register_action(&editor, window, clear_flycheck_action); + register_action(editor, window, go_to_parent_module); + register_action(editor, window, expand_macro_recursively); + register_action(editor, window, open_docs); + register_action(editor, window, cancel_flycheck_action); + register_action(editor, window, run_flycheck_action); + register_action(editor, window, clear_flycheck_action); } } diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index e0736a6e9f1973fba8f34e88fd4b06bfce59e6c2..5c9800ab55e5f1b53b941c205a2e5601f8f22524 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -196,7 +196,7 @@ impl Editor { .highlight_text(&text, 0..signature.label.len()) .into_iter() .flat_map(|(range, highlight_id)| { - Some((range, highlight_id.style(&cx.theme().syntax())?)) + Some((range, highlight_id.style(cx.theme().syntax())?)) }); signature.highlights = combine_highlights(signature.highlights.clone(), highlights) diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index f328945dbe6ae961d3fcb1ef5c80055b6adb0afb..819d6d9fed41cd02fe93922723e4096c2541c8b5 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -189,7 +189,7 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo continue; } }; - let content = block_content_for_tests(&editor, custom_block.id, cx) + let content = block_content_for_tests(editor, custom_block.id, cx) .expect("block content not found"); // 2: "related info 1 for diagnostic 0" if let Some(height) = custom_block.height { diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 6558222d89769f329ce50c238ad145e5d6aebc0f..53c911393428c86b893d488d344d5299399d8998 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -520,7 +520,7 @@ async fn judge_example( enable_telemetry: bool, cx: &AsyncApp, ) -> JudgeOutput { - let judge_output = example.judge(model.clone(), &run_output, cx).await; + let judge_output = example.judge(model.clone(), run_output, cx).await; if enable_telemetry { telemetry::event!( diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index 23c8814916da2df4016c4196d7767b748da54280..82e95728a1cdb6e23e3defe692f0e1833277c80f 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -64,7 +64,7 @@ impl ExampleMetadata { self.url .split('/') .next_back() - .unwrap_or(&"") + .unwrap_or("") .trim_end_matches(".git") .into() } @@ -255,7 +255,7 @@ impl ExampleContext { thread.update(cx, |thread, _cx| { if let Some(tool_use) = pending_tool_use { let mut tool_metrics = tool_metrics.lock().unwrap(); - if let Some(tool_result) = thread.tool_result(&tool_use_id) { + if let Some(tool_result) = thread.tool_result(tool_use_id) { let message = if tool_result.is_error { format!("✖︎ {}", tool_use.name) } else { diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index 0f2b4c18eade06060f9002615b6b995d9bfdde0d..e3b67ed3557c335b8c4d26d9aca7b02011528460 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -459,8 +459,8 @@ impl ExampleInstance { let mut output_file = File::create(self.run_directory.join("judge.md")).expect("failed to create judge.md"); - let diff_task = self.judge_diff(model.clone(), &run_output, cx); - let thread_task = self.judge_thread(model.clone(), &run_output, cx); + let diff_task = self.judge_diff(model.clone(), run_output, cx); + let thread_task = self.judge_thread(model.clone(), run_output, cx); let (diff_result, thread_result) = futures::join!(diff_task, thread_task); @@ -661,7 +661,7 @@ pub fn wait_for_lang_server( .update(cx, |buffer, cx| { lsp_store.update(cx, |lsp_store, cx| { lsp_store - .language_servers_for_local_buffer(&buffer, cx) + .language_servers_for_local_buffer(buffer, cx) .next() .is_some() }) @@ -693,7 +693,7 @@ pub fn wait_for_lang_server( _ => {} } }), - cx.subscribe(&project, { + cx.subscribe(project, { let buffer = buffer.clone(); move |project, event, cx| match event { project::Event::LanguageServerAdded(_, _, _) => { @@ -838,7 +838,7 @@ fn messages_to_markdown<'a>(message_iter: impl IntoIterator<Item = &'a Message>) for segment in &message.segments { match segment { MessageSegment::Text(text) => { - messages.push_str(&text); + messages.push_str(text); messages.push_str("\n\n"); } MessageSegment::Thinking { text, signature } => { @@ -846,7 +846,7 @@ fn messages_to_markdown<'a>(message_iter: impl IntoIterator<Item = &'a Message>) if let Some(sig) = signature { messages.push_str(&format!("Signature: {}\n\n", sig)); } - messages.push_str(&text); + messages.push_str(text); messages.push_str("\n"); } MessageSegment::RedactedThinking(items) => { @@ -878,7 +878,7 @@ pub async fn send_language_model_request( request: LanguageModelRequest, cx: &AsyncApp, ) -> anyhow::Result<String> { - match model.stream_completion_text(request, &cx).await { + match model.stream_completion_text(request, cx).await { Ok(mut stream) => { let mut full_response = String::new(); while let Some(chunk_result) = stream.stream.next().await { diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index 621ba9250c12f8edd4ab49bbdef13bc976a239dd..b80525798bff3e34bedfb5d62d4b5563691f93b1 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -452,7 +452,7 @@ impl ExtensionBuilder { let mut output = Vec::new(); let mut stack = Vec::new(); - for payload in Parser::new(0).parse_all(&input) { + for payload in Parser::new(0).parse_all(input) { let payload = payload?; // Track nesting depth, so that we don't mess with inner producer sections: diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index e795fa5ac598416ca804d0a01a73fcaf8ed28dc0..4ee948dda870b8d29757c77294ec7e670a386918 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1341,7 +1341,7 @@ impl ExtensionStore { &extension_path, &extension.manifest, wasm_host.clone(), - &cx, + cx, ) .await .with_context(|| format!("Loading extension from {extension_path:?}")); @@ -1776,7 +1776,7 @@ impl ExtensionStore { })?; for client in clients { - Self::sync_extensions_over_ssh(&this, client, cx) + Self::sync_extensions_over_ssh(this, client, cx) .await .log_err(); } diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs index 8ce3847376a4f02c04178cf62554704348c7e0f3..a6305118cd3355f69a42914ec86bb5edcfc74810 100644 --- a/crates/extension_host/src/headless_host.rs +++ b/crates/extension_host/src/headless_host.rs @@ -175,7 +175,7 @@ impl HeadlessExtensionStore { } let wasm_extension: Arc<dyn Extension> = - Arc::new(WasmExtension::load(&extension_dir, &manifest, wasm_host.clone(), &cx).await?); + Arc::new(WasmExtension::load(&extension_dir, &manifest, wasm_host.clone(), cx).await?); for (language_server_id, language_server_config) in &manifest.language_servers { for language in language_server_config.languages() { diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index c6997ccdc0c89be67442e9ac2b16f61512feb141..e8f80e5ef2092bb0f9b213a53f82638fe59b928d 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -210,7 +210,7 @@ impl FileFinder { return; }; if self.picker.read(cx).delegate.has_changed_selected_index { - if !event.modified() || !init_modifiers.is_subset_of(&event) { + if !event.modified() || !init_modifiers.is_subset_of(event) { self.init_modifiers = None; window.dispatch_action(menu::Confirm.boxed_clone(), cx); } @@ -497,7 +497,7 @@ impl Match { fn panel_match(&self) -> Option<&ProjectPanelOrdMatch> { match self { Match::History { panel_match, .. } => panel_match.as_ref(), - Match::Search(panel_match) => Some(&panel_match), + Match::Search(panel_match) => Some(panel_match), Match::CreateNew(_) => None, } } @@ -537,7 +537,7 @@ impl Matches { self.matches.binary_search_by(|m| { // `reverse()` since if cmp_matches(a, b) == Ordering::Greater, then a is better than b. // And we want the better entries go first. - Self::cmp_matches(self.separate_history, currently_opened, &m, &entry).reverse() + Self::cmp_matches(self.separate_history, currently_opened, m, entry).reverse() }) } } @@ -1082,7 +1082,7 @@ impl FileFinderDelegate { if let Some(user_home_path) = std::env::var("HOME").ok() { let user_home_path = user_home_path.trim(); if !user_home_path.is_empty() { - if (&full_path).starts_with(user_home_path) { + if full_path.starts_with(user_home_path) { full_path.replace_range(0..user_home_path.len(), "~"); full_path_positions.retain_mut(|pos| { if *pos >= user_home_path.len() { @@ -1402,7 +1402,7 @@ impl PickerDelegate for FileFinderDelegate { cx.notify(); Task::ready(()) } else { - let path_position = PathWithPosition::parse_str(&raw_query); + let path_position = PathWithPosition::parse_str(raw_query); #[cfg(windows)] let raw_query = raw_query.trim().to_owned().replace("/", "\\"); diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index db259ccef854b1d3c5c4fae3bc9ebad08e398891..8203d1b1fdf684c3dcbd1fb6c058a7b7e6bab9cb 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -1614,7 +1614,7 @@ async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppCon let picker = open_file_picker(&workspace, cx); picker.update(cx, |finder, _| { - assert_match_selection(&finder, 0, "1_qw"); + assert_match_selection(finder, 0, "1_qw"); }); } @@ -2623,7 +2623,7 @@ async fn open_queried_buffer( workspace: &Entity<Workspace>, cx: &mut gpui::VisualTestContext, ) -> Vec<FoundPath> { - let picker = open_file_picker(&workspace, cx); + let picker = open_file_picker(workspace, cx); cx.simulate_input(input); let history_items = picker.update(cx, |finder, _| { diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 68ba7a78b52fee42588b732d7a6a3c582a80061f..7235568e4f8dcb1f462e2d151705bcc2998e8d6b 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -637,7 +637,7 @@ impl PickerDelegate for OpenPathDelegate { FileIcons::get_folder_icon(false, cx)? } else { let path = path::Path::new(&candidate.path.string); - FileIcons::get_icon(&path, cx)? + FileIcons::get_icon(path, cx)? }; Some(Icon::from_path(icon).color(Color::Muted)) }); diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 22bfdbcd66ee0b3193ef51e3ec461dfe225fa8f0..64eeae99d1097eecb0a60730d657b69499005302 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -776,7 +776,7 @@ impl Fs for RealFs { } // Check if path is a symlink and follow the target parent - if let Some(mut target) = self.read_link(&path).await.ok() { + if let Some(mut target) = self.read_link(path).await.ok() { // Check if symlink target is relative path, if so make it absolute if target.is_relative() { if let Some(parent) = path.parent() { @@ -1677,7 +1677,7 @@ impl FakeFs { /// by mutating the head, index, and unmerged state. pub fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, FileStatus)]) { let workdir_path = dot_git.parent().unwrap(); - let workdir_contents = self.files_with_contents(&workdir_path); + let workdir_contents = self.files_with_contents(workdir_path); self.with_git_state(dot_git, true, |state| { state.index_contents.clear(); state.head_contents.clear(); @@ -2244,7 +2244,7 @@ impl Fs for FakeFs { async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>> { self.simulate_random_delay().await; let mut state = self.state.lock(); - let inode = match state.entry(&path)? { + let inode = match state.entry(path)? { FakeFsEntry::File { inode, .. } => *inode, FakeFsEntry::Dir { inode, .. } => *inode, _ => unreachable!(), diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 49eee848404a0bc866c55fed404365da26538d8b..ae8c5f849cba1c4c8ffe6a9bf56b3b6328e13171 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -858,7 +858,7 @@ impl GitRepository for RealGitRepository { let output = new_smol_command(&git_binary_path) .current_dir(&working_directory) .envs(env.iter()) - .args(["update-index", "--add", "--cacheinfo", "100644", &sha]) + .args(["update-index", "--add", "--cacheinfo", "100644", sha]) .arg(path.to_unix_style()) .output() .await?; @@ -959,7 +959,7 @@ impl GitRepository for RealGitRepository { Ok(working_directory) => working_directory, Err(e) => return Task::ready(Err(e)), }; - let args = git_status_args(&path_prefixes); + let args = git_status_args(path_prefixes); log::debug!("Checking for git status in {path_prefixes:?}"); self.executor.spawn(async move { let output = new_std_command(&git_binary_path) @@ -1056,7 +1056,7 @@ impl GitRepository for RealGitRepository { let (_, branch_name) = name.split_once("/").context("Unexpected branch format")?; let revision = revision.get(); let branch_commit = revision.peel_to_commit()?; - let mut branch = repo.branch(&branch_name, &branch_commit, false)?; + let mut branch = repo.branch(branch_name, &branch_commit, false)?; branch.set_upstream(Some(&name))?; branch } else { @@ -2349,7 +2349,7 @@ mod tests { #[allow(clippy::octal_escapes)] let input = "*\0060964da10574cd9bf06463a53bf6e0769c5c45e\0\0refs/heads/zed-patches\0refs/remotes/origin/zed-patches\0\01733187470\0generated protobuf\n"; assert_eq!( - parse_branch_input(&input).unwrap(), + parse_branch_input(input).unwrap(), vec![Branch { is_head: true, ref_name: "refs/heads/zed-patches".into(), diff --git a/crates/git/src/status.rs b/crates/git/src/status.rs index 6158b5179838c2b3bd36fb91f2aa9e2286c52ca1..92836042f2b9382728bf153b964a3f4585d9a502 100644 --- a/crates/git/src/status.rs +++ b/crates/git/src/status.rs @@ -468,7 +468,7 @@ impl FromStr for GitStatus { Some((path, status)) }) .collect::<Vec<_>>(); - entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b)); + entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(b)); // When a file exists in HEAD, is deleted in the index, and exists again in the working copy, // git produces two lines for it, one reading `D ` (deleted in index, unmodified in working copy) // and the other reading `??` (untracked). Merge these two into the equivalent of `DA`. diff --git a/crates/git_hosting_providers/src/git_hosting_providers.rs b/crates/git_hosting_providers/src/git_hosting_providers.rs index b31412ed4a46b0dc2695ae0229638fad409de13c..d4b3a59375f42db4a21c811ca6c4f94c912a4b3b 100644 --- a/crates/git_hosting_providers/src/git_hosting_providers.rs +++ b/crates/git_hosting_providers/src/git_hosting_providers.rs @@ -55,7 +55,7 @@ pub fn get_host_from_git_remote_url(remote_url: &str) -> Result<String> { } } - Url::parse(&remote_url) + Url::parse(remote_url) .ok() .and_then(|remote_url| remote_url.host_str().map(|host| host.to_string())) }) diff --git a/crates/git_hosting_providers/src/providers/chromium.rs b/crates/git_hosting_providers/src/providers/chromium.rs index b68c629ec7faaf9e37316cd0f7fb4f297b55f502..5d940fb496be6fde2778272abe640987e3b2a4af 100644 --- a/crates/git_hosting_providers/src/providers/chromium.rs +++ b/crates/git_hosting_providers/src/providers/chromium.rs @@ -292,7 +292,7 @@ mod tests { assert_eq!( Chromium - .extract_pull_request(&remote, &message) + .extract_pull_request(&remote, message) .unwrap() .url .as_str(), diff --git a/crates/git_hosting_providers/src/providers/github.rs b/crates/git_hosting_providers/src/providers/github.rs index 30f8d058a7c46798209685930518f4b040dbe714..4475afeb495f41e89273ce0336d830db4cc869cf 100644 --- a/crates/git_hosting_providers/src/providers/github.rs +++ b/crates/git_hosting_providers/src/providers/github.rs @@ -474,7 +474,7 @@ mod tests { assert_eq!( github - .extract_pull_request(&remote, &message) + .extract_pull_request(&remote, message) .unwrap() .url .as_str(), @@ -488,6 +488,6 @@ mod tests { See the original PR, this is a fix. "# }; - assert_eq!(github.extract_pull_request(&remote, &message), None); + assert_eq!(github.extract_pull_request(&remote, message), None); } } diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index c8c237fe90f12f2ac4ead04e0f2f0b4955f8bc1c..07896b0c011bc58747381de53db0c2996da23c0e 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -160,7 +160,7 @@ impl CommitView { }); } - cx.spawn(async move |this, mut cx| { + cx.spawn(async move |this, cx| { for file in commit_diff.files { let is_deleted = file.new_text.is_none(); let new_text = file.new_text.unwrap_or_default(); @@ -179,9 +179,9 @@ impl CommitView { worktree_id, }) as Arc<dyn language::File>; - let buffer = build_buffer(new_text, file, &language_registry, &mut cx).await?; + let buffer = build_buffer(new_text, file, &language_registry, cx).await?; let buffer_diff = - build_buffer_diff(old_text, &buffer, &language_registry, &mut cx).await?; + build_buffer_diff(old_text, &buffer, &language_registry, cx).await?; this.update(cx, |this, cx| { this.multibuffer.update(cx, |multibuffer, cx| { diff --git a/crates/git_ui/src/conflict_view.rs b/crates/git_ui/src/conflict_view.rs index 6482ebb9f8fa6b7fa5688a7263968319427ac2a4..5c1b1325a5a0c8ac3418d34a640ef780b2912fbe 100644 --- a/crates/git_ui/src/conflict_view.rs +++ b/crates/git_ui/src/conflict_view.rs @@ -156,7 +156,7 @@ fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mu .unwrap() .buffers .retain(|buffer_id, buffer| { - if removed_buffer_ids.contains(&buffer_id) { + if removed_buffer_ids.contains(buffer_id) { removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id)); false } else { @@ -222,12 +222,12 @@ fn conflicts_updated( let precedes_start = range .context .start - .cmp(&conflict_range.start, &buffer_snapshot) + .cmp(&conflict_range.start, buffer_snapshot) .is_le(); let follows_end = range .context .end - .cmp(&conflict_range.start, &buffer_snapshot) + .cmp(&conflict_range.start, buffer_snapshot) .is_ge(); precedes_start && follows_end }) else { @@ -268,12 +268,12 @@ fn conflicts_updated( let precedes_start = range .context .start - .cmp(&conflict.range.start, &buffer_snapshot) + .cmp(&conflict.range.start, buffer_snapshot) .is_le(); let follows_end = range .context .end - .cmp(&conflict.range.start, &buffer_snapshot) + .cmp(&conflict.range.start, buffer_snapshot) .is_ge(); precedes_start && follows_end }) else { diff --git a/crates/git_ui/src/file_diff_view.rs b/crates/git_ui/src/file_diff_view.rs index 2f8a744ed893761f6491f23a31e19bfb55a4db62..f7d29cdfa70d5b8691a319312b52869e84c815e8 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -398,7 +398,7 @@ mod tests { let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - let (workspace, mut cx) = + let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let diff_view = workspace @@ -417,7 +417,7 @@ mod tests { // Verify initial diff assert_state_with_diff( &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()), - &mut cx, + cx, &unindent( " - old line 1 @@ -452,7 +452,7 @@ mod tests { cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE); assert_state_with_diff( &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()), - &mut cx, + cx, &unindent( " - old line 1 @@ -487,7 +487,7 @@ mod tests { cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE); assert_state_with_diff( &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()), - &mut cx, + cx, &unindent( " ˇnew line 1 diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 754812cbdfe9dc95b6e9fdf58813043af5c17e24..c21ac286cb381ba1218f231a83d6dd363242bac2 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -103,7 +103,7 @@ fn prompt<T>( where T: IntoEnumIterator + VariantNames + 'static, { - let rx = window.prompt(PromptLevel::Info, msg, detail, &T::VARIANTS, cx); + let rx = window.prompt(PromptLevel::Info, msg, detail, T::VARIANTS, cx); cx.spawn(async move |_| Ok(T::iter().nth(rx.await?).unwrap())) } @@ -652,14 +652,14 @@ impl GitPanel { if GitPanelSettings::get_global(cx).sort_by_path { return self .entries - .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path)) + .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path)) .ok(); } if self.conflicted_count > 0 { let conflicted_start = 1; if let Ok(ix) = self.entries[conflicted_start..conflicted_start + self.conflicted_count] - .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path)) + .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path)) { return Some(conflicted_start + ix); } @@ -671,7 +671,7 @@ impl GitPanel { 0 } + 1; if let Ok(ix) = self.entries[tracked_start..tracked_start + self.tracked_count] - .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path)) + .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path)) { return Some(tracked_start + ix); } @@ -687,7 +687,7 @@ impl GitPanel { 0 } + 1; if let Ok(ix) = self.entries[untracked_start..untracked_start + self.new_count] - .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path)) + .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path)) { return Some(untracked_start + ix); } @@ -1341,7 +1341,7 @@ impl GitPanel { .iter() .filter_map(|entry| entry.status_entry()) .filter(|status_entry| { - section.contains(&status_entry, repository) + section.contains(status_entry, repository) && status_entry.staging.as_bool() != Some(goal_staged_state) }) .map(|status_entry| status_entry.clone()) @@ -1952,7 +1952,7 @@ impl GitPanel { thinking_allowed: false, }; - let stream = model.stream_completion_text(request, &cx); + let stream = model.stream_completion_text(request, cx); match stream.await { Ok(mut messages) => { if !text_empty { @@ -4620,7 +4620,7 @@ impl editor::Addon for GitPanelAddon { git_panel .read(cx) - .render_buffer_header_controls(&git_panel, &file, window, cx) + .render_buffer_header_controls(&git_panel, file, window, cx) } } diff --git a/crates/git_ui/src/picker_prompt.rs b/crates/git_ui/src/picker_prompt.rs index 4077e0f3623e0925a87824e252a77755c78721ea..3f1d507c42f71eddaef4b2b8a114a84f2aabf875 100644 --- a/crates/git_ui/src/picker_prompt.rs +++ b/crates/git_ui/src/picker_prompt.rs @@ -152,7 +152,7 @@ impl PickerDelegate for PickerPromptDelegate { .all_options .iter() .enumerate() - .map(|(ix, option)| StringMatchCandidate::new(ix, &option)) + .map(|(ix, option)| StringMatchCandidate::new(ix, option)) .collect::<Vec<StringMatchCandidate>>() }); let Some(candidates) = candidates.log_err() else { diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index d6a4e27286af1bb38dcd1acc488bce9da1813a42..e312d6a2aae270ef90324f6cfd1c0dca50c5eab2 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -1173,7 +1173,7 @@ impl RenderOnce for ProjectDiffEmptyState { .child(Label::new("No Changes").color(Color::Muted)) } else { this.when_some(self.current_branch.as_ref(), |this, branch| { - this.child(has_branch_container(&branch)) + this.child(has_branch_container(branch)) }) } }), @@ -1332,14 +1332,14 @@ fn merge_anchor_ranges<'a>( loop { if let Some(left_range) = left .peek() - .filter(|range| range.start.cmp(&next_range.end, &snapshot).is_le()) + .filter(|range| range.start.cmp(&next_range.end, snapshot).is_le()) .cloned() { left.next(); next_range.end = left_range.end; } else if let Some(right_range) = right .peek() - .filter(|range| range.start.cmp(&next_range.end, &snapshot).is_le()) + .filter(|range| range.start.cmp(&next_range.end, snapshot).is_le()) .cloned() { right.next(); diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index 005c1e18b40727f42df81437c7038f4e5a7ef905..d07868c3e1eee4d5ccfb0b0e3c15cdc869795411 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -686,7 +686,7 @@ mod tests { let project = Project::test(fs, [project_root.as_ref()], cx).await; - let (workspace, mut cx) = + let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let buffer = project @@ -725,7 +725,7 @@ mod tests { assert_state_with_diff( &diff_view.read_with(cx, |diff_view, _| diff_view.diff_editor.clone()), - &mut cx, + cx, expected_diff, ); diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index 93a1c15c41dd173a35ffc0adf06af6c449809890..3a80ee12a00ec24db64b2371637324b4c2393277 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -374,7 +374,7 @@ mod windows { shader_path, "vs_4_1", ); - generate_rust_binding(&const_name, &output_file, &rust_binding_path); + generate_rust_binding(&const_name, &output_file, rust_binding_path); // Compile fragment shader let output_file = format!("{}/{}_ps.h", out_dir, module); @@ -387,7 +387,7 @@ mod windows { shader_path, "ps_4_1", ); - generate_rust_binding(&const_name, &output_file, &rust_binding_path); + generate_rust_binding(&const_name, &output_file, rust_binding_path); } fn compile_shader_impl( diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index b0f560e38d4896f889a30b5315a265c83065d068..170df3cad726605d10dbc42b34060d75e72ff20e 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -137,14 +137,14 @@ impl TextInput { fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) { if !self.selected_range.is_empty() { cx.write_to_clipboard(ClipboardItem::new_string( - (&self.content[self.selected_range.clone()]).to_string(), + self.content[self.selected_range.clone()].to_string(), )); } } fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) { if !self.selected_range.is_empty() { cx.write_to_clipboard(ClipboardItem::new_string( - (&self.content[self.selected_range.clone()]).to_string(), + self.content[self.selected_range.clone()].to_string(), )); self.replace_text_in_range(None, "", window, cx) } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index e1df6d0be4d7122b6ac8108e4a59774bcfc3d016..ed1b935c58b93a591867a357be1f75499567889f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1310,7 +1310,7 @@ impl App { T: 'static, { let window_handle = window.handle; - self.observe_release(&handle, move |entity, cx| { + self.observe_release(handle, move |entity, cx| { let _ = window_handle.update(cx, |_, window, cx| on_release(entity, window, cx)); }) } @@ -1917,7 +1917,7 @@ impl AppContext for App { G: Global, { let mut g = self.global::<G>(); - callback(&g, self) + callback(g, self) } } diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index fccb417caa70c7526a0f15a307d74caeabcdab77..48b2bcaf989516c8a2c82fee3cb417a352c5c931 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -661,7 +661,7 @@ pub struct WeakEntity<T> { impl<T> std::fmt::Debug for WeakEntity<T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct(&type_name::<Self>()) + f.debug_struct(type_name::<Self>()) .field("entity_id", &self.any_entity.entity_id) .field("entity_type", &type_name::<T>()) .finish() diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 09afbff929b99bb927d365621ea0550c28dcedf8..78114b7ecf78e5ff0f53883523fdacf227f8fba4 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -2785,7 +2785,7 @@ fn handle_tooltip_check_visible_and_update( match action { Action::None => {} - Action::Hide => clear_active_tooltip(&active_tooltip, window), + Action::Hide => clear_active_tooltip(active_tooltip, window), Action::ScheduleHide(tooltip) => { let delayed_hide_task = window.spawn(cx, { let active_tooltip = active_tooltip.clone(); diff --git a/crates/gpui/src/inspector.rs b/crates/gpui/src/inspector.rs index 23c46edcc11ed36cfbe3ad110dc296af3e129784..9f86576a599845bb9e09760e8001333b9dea745d 100644 --- a/crates/gpui/src/inspector.rs +++ b/crates/gpui/src/inspector.rs @@ -164,7 +164,7 @@ mod conditional { if let Some(render_inspector) = cx .inspector_element_registry .renderers_by_type_id - .remove(&type_id) + .remove(type_id) { let mut element = (render_inspector)( active_element.id.clone(), diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index c3f5d186030bf2a38c2345724666ca38003cd484..f682b78c4115ab9a05e2e23fca22a5b1c817b5f4 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -408,7 +408,7 @@ impl DispatchTree { keymap .bindings_for_action(action) .filter(|binding| { - Self::binding_matches_predicate_and_not_shadowed(&keymap, &binding, context_stack) + Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack) }) .cloned() .collect() @@ -426,7 +426,7 @@ impl DispatchTree { .bindings_for_action(action) .rev() .find(|binding| { - Self::binding_matches_predicate_and_not_shadowed(&keymap, &binding, context_stack) + Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack) }) .cloned() } diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 83d7479a04423d249a2be69c69756211eb9d485d..66f191ca5db6d82e8e38f2628f73ef7380790244 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -148,7 +148,7 @@ impl Keymap { let mut pending_bindings = SmallVec::<[(BindingIndex, &KeyBinding); 1]>::new(); for (ix, binding) in self.bindings().enumerate().rev() { - let Some(depth) = self.binding_enabled(binding, &context_stack) else { + let Some(depth) = self.binding_enabled(binding, context_stack) else { continue; }; let Some(pending) = binding.match_keystrokes(input) else { diff --git a/crates/gpui/src/path_builder.rs b/crates/gpui/src/path_builder.rs index 6c8cfddd523c4d56c81ebcbbf1437b5cc418d73c..38903ea5885a4fbd0ed1454046a9021aa572d6e3 100644 --- a/crates/gpui/src/path_builder.rs +++ b/crates/gpui/src/path_builder.rs @@ -278,7 +278,7 @@ impl PathBuilder { options: &StrokeOptions, ) -> Result<Path<Pixels>, Error> { let path = if let Some(dash_array) = dash_array { - let measurements = lyon::algorithms::measure::PathMeasurements::from_path(&path, 0.01); + let measurements = lyon::algorithms::measure::PathMeasurements::from_path(path, 0.01); let mut sampler = measurements .create_sampler(path, lyon::algorithms::measure::SampleType::Normalized); let mut builder = lyon::path::Path::builder(); diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index ffd68d60e6f5a87443d099ffeeb1d856ab5e910f..3e002309e46395432d5cec21042c6747fcde8397 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1508,7 +1508,7 @@ impl ClipboardItem { for entry in self.entries.iter() { if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry { - answer.push_str(&text); + answer.push_str(text); any_entries = true; } } diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 86e5a79e8ae50a39841acd1a05df42d84ba369d9..a1da088b757b6ad6aaabb89ab4a33587c6dbbc98 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -642,7 +642,7 @@ pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::S let mut state: Option<xkb::compose::State> = None; for locale in locales { if let Ok(table) = - xkb::compose::Table::new_from_locale(&cx, &locale, xkb::compose::COMPILE_NO_FLAGS) + xkb::compose::Table::new_from_locale(cx, &locale, xkb::compose::COMPILE_NO_FLAGS) { state = Some(xkb::compose::State::new( &table, diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 72e4477ecf697a9f6443dffb80e0637202d3b848..0ab61fbf0c19bfc6390962742fd1ec8fc25d5f62 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -1145,7 +1145,7 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr { .globals .text_input_manager .as_ref() - .map(|text_input_manager| text_input_manager.get_text_input(&seat, qh, ())); + .map(|text_input_manager| text_input_manager.get_text_input(seat, qh, ())); if let Some(wl_keyboard) = &state.wl_keyboard { wl_keyboard.release(); @@ -1294,7 +1294,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { match key_state { wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => { let mut keystroke = - Keystroke::from_xkb(&keymap_state, state.modifiers, keycode); + Keystroke::from_xkb(keymap_state, state.modifiers, keycode); if let Some(mut compose) = state.compose_state.take() { compose.feed(keysym); match compose.status() { @@ -1538,12 +1538,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr { cursor_shape_device.set_shape(serial, style.to_shape()); } else { let scale = window.primary_output_scale(); - state.cursor.set_icon( - &wl_pointer, - serial, - style.to_icon_names(), - scale, - ); + state + .cursor + .set_icon(wl_pointer, serial, style.to_icon_names(), scale); } } drop(state); @@ -1580,7 +1577,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr { if state .keyboard_focused_window .as_ref() - .map_or(false, |keyboard_window| window.ptr_eq(&keyboard_window)) + .map_or(false, |keyboard_window| window.ptr_eq(keyboard_window)) { state.enter_token = None; } diff --git a/crates/gpui/src/platform/linux/wayland/cursor.rs b/crates/gpui/src/platform/linux/wayland/cursor.rs index 2a24d0e1ba347fb718da126120bc809c65d93b33..bfbedf234dc66c3f82040ce08a6eb0e99f04add9 100644 --- a/crates/gpui/src/platform/linux/wayland/cursor.rs +++ b/crates/gpui/src/platform/linux/wayland/cursor.rs @@ -144,7 +144,7 @@ impl Cursor { hot_y as i32 / scale, ); - self.surface.attach(Some(&buffer), 0, 0); + self.surface.attach(Some(buffer), 0, 0); self.surface.damage(0, 0, width as i32, height as i32); self.surface.commit(); } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 053cd0387b25f418696f12838187088229aaf044..dd0cea32902046d204cb70c2eb1f4bdf6d48cfe2 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1212,7 +1212,7 @@ impl X11Client { state = self.0.borrow_mut(); if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) { - let scroll_delta = get_scroll_delta_and_update_state(&mut pointer, &event); + let scroll_delta = get_scroll_delta_and_update_state(pointer, &event); drop(state); if let Some(scroll_delta) = scroll_delta { window.handle_input(PlatformInput::ScrollWheel(make_scroll_wheel_event( @@ -1271,7 +1271,7 @@ impl X11Client { Event::XinputDeviceChanged(event) => { let mut state = self.0.borrow_mut(); if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) { - reset_pointer_device_scroll_positions(&mut pointer); + reset_pointer_device_scroll_positions(pointer); } } _ => {} @@ -2038,7 +2038,7 @@ fn xdnd_get_supported_atom( { if let Some(atoms) = reply.value32() { for atom in atoms { - if xdnd_is_atom_supported(atom, &supported_atoms) { + if xdnd_is_atom_supported(atom, supported_atoms) { return atom; } } diff --git a/crates/gpui/src/platform/linux/x11/event.rs b/crates/gpui/src/platform/linux/x11/event.rs index cd4cef24a33f33aaa2f2e685089eb1a2368719e2..a566762c540a1a39ae2e8fcfea78f4fdd0b1d436 100644 --- a/crates/gpui/src/platform/linux/x11/event.rs +++ b/crates/gpui/src/platform/linux/x11/event.rs @@ -73,8 +73,8 @@ pub(crate) fn get_valuator_axis_index( // valuator present in this event's axisvalues. Axisvalues is ordered from // lowest valuator number to highest, so counting bits before the 1 bit for // this valuator yields the index in axisvalues. - if bit_is_set_in_vec(&valuator_mask, valuator_number) { - Some(popcount_upto_bit_index(&valuator_mask, valuator_number) as usize) + if bit_is_set_in_vec(valuator_mask, valuator_number) { + Some(popcount_upto_bit_index(valuator_mask, valuator_number) as usize) } else { None } diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 1a3c323c35129b9ea56595b7f81775de4b036454..2bf58d6184e1b542adabd72eaddd11dc091ec28c 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -397,7 +397,7 @@ impl X11WindowState { .display_id .map_or(x_main_screen_index, |did| did.0 as usize); - let visual_set = find_visuals(&xcb, x_screen_index); + let visual_set = find_visuals(xcb, x_screen_index); let visual = match visual_set.transparent { Some(visual) => visual, @@ -604,7 +604,7 @@ impl X11WindowState { ), )?; - xcb_flush(&xcb); + xcb_flush(xcb); let renderer = { let raw_window = RawWindow { @@ -664,7 +664,7 @@ impl X11WindowState { || "X11 DestroyWindow failed while cleaning it up after setup failure.", xcb.destroy_window(x_window), )?; - xcb_flush(&xcb); + xcb_flush(xcb); } setup_result diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index a686d8c45bf846fe7f36123cb559e5c412bf1783..49a5edceb25c097615e4db3a7a0dbdd221c2d0ed 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -445,14 +445,14 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - &command_encoder, + command_encoder, ), PrimitiveBatch::Quads(quads) => self.draw_quads( quads, instance_buffer, &mut instance_offset, viewport_size, - &command_encoder, + command_encoder, ), PrimitiveBatch::Paths(paths) => { command_encoder.end_encoding(); @@ -480,7 +480,7 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - &command_encoder, + command_encoder, ) } else { false @@ -491,7 +491,7 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - &command_encoder, + command_encoder, ), PrimitiveBatch::MonochromeSprites { texture_id, @@ -502,7 +502,7 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - &command_encoder, + command_encoder, ), PrimitiveBatch::PolychromeSprites { texture_id, @@ -513,14 +513,14 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - &command_encoder, + command_encoder, ), PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces( surfaces, instance_buffer, &mut instance_offset, viewport_size, - &command_encoder, + command_encoder, ), }; if !ok { @@ -763,7 +763,7 @@ impl MetalRenderer { viewport_size: Size<DevicePixels>, command_encoder: &metal::RenderCommandEncoderRef, ) -> bool { - let Some(ref first_path) = paths.first() else { + let Some(first_path) = paths.first() else { return true; }; diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 79177fb2c9cf616b7208af6060b31afec03926bd..f094ed9f30bed2f54aa0698c13ad3454e4a7e677 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -371,7 +371,7 @@ impl MacPlatform { item = NSMenuItem::alloc(nil) .initWithTitle_action_keyEquivalent_( - ns_string(&name), + ns_string(name), selector, ns_string(key_to_native(&keystroke.key).as_ref()), ) @@ -383,7 +383,7 @@ impl MacPlatform { } else { item = NSMenuItem::alloc(nil) .initWithTitle_action_keyEquivalent_( - ns_string(&name), + ns_string(name), selector, ns_string(""), ) @@ -392,7 +392,7 @@ impl MacPlatform { } else { item = NSMenuItem::alloc(nil) .initWithTitle_action_keyEquivalent_( - ns_string(&name), + ns_string(name), selector, ns_string(""), ) @@ -412,7 +412,7 @@ impl MacPlatform { submenu.addItem_(Self::create_menu_item(item, delegate, actions, keymap)); } item.setSubmenu_(submenu); - item.setTitle_(ns_string(&name)); + item.setTitle_(ns_string(name)); item } MenuItem::SystemMenu(OsMenu { name, menu_type }) => { @@ -420,7 +420,7 @@ impl MacPlatform { let submenu = NSMenu::new(nil).autorelease(); submenu.setDelegate_(delegate); item.setSubmenu_(submenu); - item.setTitle_(ns_string(&name)); + item.setTitle_(ns_string(name)); match menu_type { SystemMenuType::Services => { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index aedf131909a6956e9a4501b107c81ce242b80a49..40a03b6c4a7eb41620f0909ba3f58a26d8cfdffb 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1480,9 +1480,9 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: if key_down_event.is_held { if let Some(key_char) = key_down_event.keystroke.key_char.as_ref() { - let handled = with_input_handler(&this, |input_handler| { + let handled = with_input_handler(this, |input_handler| { if !input_handler.apple_press_and_hold_enabled() { - input_handler.replace_text_in_range(None, &key_char); + input_handler.replace_text_in_range(None, key_char); return YES; } NO @@ -1949,7 +1949,7 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS let text = text.to_str(); let replacement_range = replacement_range.to_range(); with_input_handler(this, |input_handler| { - input_handler.replace_text_in_range(replacement_range, &text) + input_handler.replace_text_in_range(replacement_range, text) }); } } @@ -1973,7 +1973,7 @@ extern "C" fn set_marked_text( let replacement_range = replacement_range.to_range(); let text = text.to_str(); with_input_handler(this, |input_handler| { - input_handler.replace_and_mark_text_in_range(replacement_range, &text, selected_range) + input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range) }); } } diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index 75cb50243b9c8ec845e256f4095cdedc40d2eea2..a86a1fab62a404c4f49e785491bb2925a6f3cf61 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -850,7 +850,7 @@ impl DirectWriteState { } let bitmap_data = if params.is_emoji { - if let Ok(color) = self.rasterize_color(¶ms, glyph_bounds) { + if let Ok(color) = self.rasterize_color(params, glyph_bounds) { color } else { let monochrome = self.rasterize_monochrome(params, glyph_bounds)?; @@ -1784,7 +1784,7 @@ fn apply_font_features( } unsafe { - direct_write_features.AddFontFeature(make_direct_write_feature(&tag, *value))?; + direct_write_features.AddFontFeature(make_direct_write_feature(tag, *value))?; } } unsafe { diff --git a/crates/gpui/src/platform/windows/directx_renderer.rs b/crates/gpui/src/platform/windows/directx_renderer.rs index 4e72ded5341479c2d861c441fc3c43d5fee7056c..f84a1c1b6d0d158684e4c6cad6edbf72105425e0 100644 --- a/crates/gpui/src/platform/windows/directx_renderer.rs +++ b/crates/gpui/src/platform/windows/directx_renderer.rs @@ -758,7 +758,7 @@ impl DirectXRenderPipelines { impl DirectComposition { pub fn new(dxgi_device: &IDXGIDevice, hwnd: HWND) -> Result<Self> { - let comp_device = get_comp_device(&dxgi_device)?; + let comp_device = get_comp_device(dxgi_device)?; let comp_target = unsafe { comp_device.CreateTargetForHwnd(hwnd, true) }?; let comp_visual = unsafe { comp_device.CreateVisual() }?; @@ -1144,7 +1144,7 @@ fn create_resources( [D3D11_VIEWPORT; 1], )> { let (render_target, render_target_view) = - create_render_target_and_its_view(&swap_chain, &devices.device)?; + create_render_target_and_its_view(swap_chain, &devices.device)?; let (path_intermediate_texture, path_intermediate_srv) = create_path_intermediate_texture(&devices.device, width, height)?; let (path_intermediate_msaa_texture, path_intermediate_msaa_view) = diff --git a/crates/gpui/src/tab_stop.rs b/crates/gpui/src/tab_stop.rs index 7dde42efed8a138de3a29657683d95c60e27dda0..30d24e85e79744721e4fa8d07ad7cbca9ccb57ad 100644 --- a/crates/gpui/src/tab_stop.rs +++ b/crates/gpui/src/tab_stop.rs @@ -90,7 +90,7 @@ mod tests { ]; for handle in focus_handles.iter() { - tab.insert(&handle); + tab.insert(handle); } assert_eq!( tab.handles diff --git a/crates/gpui_macros/src/test.rs b/crates/gpui_macros/src/test.rs index adb27f42ea2c7689d19290d17b78299ff149fdd2..5a8b1cf7fca771813d2ac5c18a49a643c55c205c 100644 --- a/crates/gpui_macros/src/test.rs +++ b/crates/gpui_macros/src/test.rs @@ -73,7 +73,7 @@ impl Parse for Args { (Meta::NameValue(meta), "seed") => { seeds = vec![parse_usize_from_expr(&meta.value)? as u64] } - (Meta::List(list), "seeds") => seeds = parse_u64_array(&list)?, + (Meta::List(list), "seeds") => seeds = parse_u64_array(list)?, (Meta::Path(_), _) => { return Err(syn::Error::new(meta.span(), "invalid path argument")); } diff --git a/crates/install_cli/src/install_cli.rs b/crates/install_cli/src/install_cli.rs index 12c094448b8362c8d638ac62da5838544b4fcc6d..dc9e0e31ab093ec2414aafb4f4885395ee3b2efc 100644 --- a/crates/install_cli/src/install_cli.rs +++ b/crates/install_cli/src/install_cli.rs @@ -105,7 +105,7 @@ pub fn install_cli(window: &mut Window, cx: &mut Context<Workspace>) { cx, ) })?; - register_zed_scheme(&cx).await.log_err(); + register_zed_scheme(cx).await.log_err(); Ok(()) }) .detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None); diff --git a/crates/jj/src/jj_store.rs b/crates/jj/src/jj_store.rs index a10f06fad48a3867ce6e19ffb5fc721c931ae6e4..2d2d958d7f964cdfc7723827fb2241e50d172697 100644 --- a/crates/jj/src/jj_store.rs +++ b/crates/jj/src/jj_store.rs @@ -16,7 +16,7 @@ pub struct JujutsuStore { impl JujutsuStore { pub fn init_global(cx: &mut App) { - let Some(repository) = RealJujutsuRepository::new(&Path::new(".")).ok() else { + let Some(repository) = RealJujutsuRepository::new(Path::new(".")).ok() else { return; }; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e2bcc938faf6be215d4b6298edf83482e4d5d838..abb8d3b151c471a658a00895408c37e9b2ab111d 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -716,7 +716,7 @@ impl EditPreview { &self.applied_edits_snapshot, &self.syntax_snapshot, None, - &syntax_theme, + syntax_theme, ); } @@ -727,7 +727,7 @@ impl EditPreview { ¤t_snapshot.text, ¤t_snapshot.syntax, Some(deletion_highlight_style), - &syntax_theme, + syntax_theme, ); } @@ -737,7 +737,7 @@ impl EditPreview { &self.applied_edits_snapshot, &self.syntax_snapshot, Some(insertion_highlight_style), - &syntax_theme, + syntax_theme, ); } @@ -749,7 +749,7 @@ impl EditPreview { &self.applied_edits_snapshot, &self.syntax_snapshot, None, - &syntax_theme, + syntax_theme, ); highlighted_text.build() diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index c377d7440a49ea7a8535e58b85cae0f0f70f7781..3a417331912707939572a91e00b982aa7064c75d 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1830,7 +1830,7 @@ impl Language { impl LanguageScope { pub fn path_suffixes(&self) -> &[String] { - &self.language.path_suffixes() + self.language.path_suffixes() } pub fn language_name(&self) -> LanguageName { diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 6a89b90462dcb832b4f6bf6895c362f057e103b7..83c16f4558c66b2ab3c72315e52369237e50397b 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -1102,7 +1102,7 @@ impl LanguageRegistry { use gpui::AppContext as _; let mut state = self.state.write(); - let fake_entry = state.fake_server_entries.get_mut(&name)?; + let fake_entry = state.fake_server_entries.get_mut(name)?; let (server, mut fake_server) = lsp::FakeLanguageServer::new( server_id, binary, diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 29669ba2a04c26a78cd69d36679df3e0e109dffa..62fe75b6a8aff79f3c7069d6a06c5c1214c688a5 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -187,8 +187,8 @@ impl LanguageSettings { let rest = available_language_servers .iter() .filter(|&available_language_server| { - !disabled_language_servers.contains(&available_language_server) - && !enabled_language_servers.contains(&available_language_server) + !disabled_language_servers.contains(available_language_server) + && !enabled_language_servers.contains(available_language_server) }) .cloned() .collect::<Vec<_>>(); diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index c56ffed0663a9419419201f902f3db8311acb9bd..30bbc88f7e7240551ee9784da6a389b08c11b5f5 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -1297,7 +1297,7 @@ fn parse_text( ) -> anyhow::Result<Tree> { with_parser(|parser| { let mut chunks = text.chunks_in_range(start_byte..text.len()); - parser.set_included_ranges(&ranges)?; + parser.set_included_ranges(ranges)?; parser.set_language(&grammar.ts_language)?; parser .parse_with_options( diff --git a/crates/language/src/text_diff.rs b/crates/language/src/text_diff.rs index f9221f571afb1baa0ba0b824922e799fcec01c88..af8ce608818cf66dec6d6db7a7556636276ad9bc 100644 --- a/crates/language/src/text_diff.rs +++ b/crates/language/src/text_diff.rs @@ -154,19 +154,19 @@ fn diff_internal( input, |old_tokens: Range<u32>, new_tokens: Range<u32>| { old_offset += token_len( - &input, + input, &input.before[old_token_ix as usize..old_tokens.start as usize], ); new_offset += token_len( - &input, + input, &input.after[new_token_ix as usize..new_tokens.start as usize], ); let old_len = token_len( - &input, + input, &input.before[old_tokens.start as usize..old_tokens.end as usize], ); let new_len = token_len( - &input, + input, &input.after[new_tokens.start as usize..new_tokens.end as usize], ); let old_byte_range = old_offset..old_offset + old_len; diff --git a/crates/language_extension/src/language_extension.rs b/crates/language_extension/src/language_extension.rs index 7bca0eb48566b739e68c3efb4f2502cabb80994f..510f870ce8afbda090817e0ce515d4c5c2e3c63b 100644 --- a/crates/language_extension/src/language_extension.rs +++ b/crates/language_extension/src/language_extension.rs @@ -61,6 +61,6 @@ impl ExtensionLanguageProxy for LanguageServerRegistryProxy { grammars_to_remove: &[Arc<str>], ) { self.language_registry - .remove_languages(&languages_to_remove, &grammars_to_remove); + .remove_languages(languages_to_remove, grammars_to_remove); } } diff --git a/crates/language_model/src/request.rs b/crates/language_model/src/request.rs index edce3d03b7063b383e51d88d4de7dc52ace0d04c..8c2d169973729c7e3891f45548a71e5dc72a377d 100644 --- a/crates/language_model/src/request.rs +++ b/crates/language_model/src/request.rs @@ -220,7 +220,7 @@ impl<'de> Deserialize<'de> for LanguageModelToolResultContent { // Accept wrapped text format: { "type": "text", "text": "..." } if let (Some(type_value), Some(text_value)) = - (get_field(&obj, "type"), get_field(&obj, "text")) + (get_field(obj, "type"), get_field(obj, "text")) { if let Some(type_str) = type_value.as_str() { if type_str.to_lowercase() == "text" { @@ -255,7 +255,7 @@ impl<'de> Deserialize<'de> for LanguageModelToolResultContent { } // Try as direct Image (object with "source" and "size" fields) - if let Some(image) = LanguageModelImage::from_json(&obj) { + if let Some(image) = LanguageModelImage::from_json(obj) { return Ok(Self::Image(image)); } } @@ -272,7 +272,7 @@ impl<'de> Deserialize<'de> for LanguageModelToolResultContent { impl LanguageModelToolResultContent { pub fn to_str(&self) -> Option<&str> { match self { - Self::Text(text) => Some(&text), + Self::Text(text) => Some(text), Self::Image(_) => None, } } diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 810d4a5f44ec6ed7c2747b0b0580e3e90844828a..7ba56ec7755afd8ab5e2c8528435c14132fbd293 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -114,7 +114,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .ok(); this.update(cx, |this, cx| { @@ -133,7 +133,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await .ok(); @@ -212,7 +212,7 @@ impl AnthropicLanguageModelProvider { } else { cx.spawn(async move |cx| { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 4e6744d745df9a282b952e9fb46cac9f54ac149c..f33a00972ddf6593252eb6c71cc3d2c417298bb2 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -150,7 +150,7 @@ impl State { let credentials_provider = <dyn CredentialsProvider>::global(cx); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(AMAZON_AWS_URL, &cx) + .delete_credentials(AMAZON_AWS_URL, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -174,7 +174,7 @@ impl State { AMAZON_AWS_URL, "Bearer", &serde_json::to_vec(&credentials)?, - &cx, + cx, ) .await?; this.update(cx, |this, cx| { @@ -206,7 +206,7 @@ impl State { (credentials, true) } else { let (_, credentials) = credentials_provider - .read_credentials(AMAZON_AWS_URL, &cx) + .read_credentials(AMAZON_AWS_URL, cx) .await? .ok_or_else(|| AuthenticateError::CredentialsNotFound)?; ( @@ -465,7 +465,7 @@ impl BedrockModel { Result<BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>>, > { let Ok(runtime_client) = self - .get_or_init_client(&cx) + .get_or_init_client(cx) .cloned() .context("Bedrock client not initialized") else { diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index c3f4399832dc9d71cf54bb59f8e825a62a20a56f..f226d0c6a8eac5382e7ac38cccfaf83975dcd908 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -193,7 +193,7 @@ impl State { fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<()>> { let client = self.client.clone(); cx.spawn(async move |state, cx| { - client.sign_in_with_optional_connect(true, &cx).await?; + client.sign_in_with_optional_connect(true, cx).await?; state.update(cx, |_, cx| cx.notify()) }) } diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index 2b30d456ee3beb3f2a07afbdef5233c0905b70b0..8c7f8bcc351851ebb9cc90aaa9c9fa2eed59119e 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -77,7 +77,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -96,7 +96,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await?; this.update(cx, |this, cx| { this.api_key = Some(api_key); @@ -120,7 +120,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index 32f8838df75de7fed44dd96a5bb5356cf4a1dbcb..1bb9f3fa00b25f663797df46e2a1ede2fcc73710 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -110,7 +110,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -129,7 +129,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await?; this.update(cx, |this, cx| { this.api_key = Some(api_key); @@ -156,7 +156,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index e1d55801eb8bdf2703c51b0ca0fb837a6b03462a..3f8c2e2a678d019aad1ff90688b145ffe6c740dd 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -76,7 +76,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -95,7 +95,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await?; this.update(cx, |this, cx| { this.api_key = Some(api_key); @@ -119,7 +119,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 04d89f2db19f0dee56a6741600f0cb510b7ecadf..1a5c09cdc449bbe66f2412abd7c90de5d19feeb3 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -75,7 +75,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -94,7 +94,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await .log_err(); this.update(cx, |this, cx| { @@ -119,7 +119,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index c6b980c3ec4bef89bf924df3616075450d54c36d..55df534cc9416149ca5574e16f0230f1c8160220 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -87,7 +87,7 @@ impl State { let api_url = self.settings.api_url.clone(); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -103,7 +103,7 @@ impl State { let api_url = self.settings.api_url.clone(); cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await .log_err(); this.update(cx, |this, cx| { @@ -126,7 +126,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 5d8bace6d397d7c36eee29138ea69237014c5604..8f2abfce35852c617ddee22de18432908660fe95 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -112,7 +112,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -131,7 +131,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await .log_err(); this.update(cx, |this, cx| { @@ -157,7 +157,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 98e4f60b6bb8c146741058611ec6e55d01a505eb..84f3175d1e5493fd55cafd2ea9c4a0604d2a97b4 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -71,7 +71,7 @@ impl State { }; cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -92,7 +92,7 @@ impl State { }; cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await .log_err(); this.update(cx, |this, cx| { @@ -119,7 +119,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index 2b8238cc5c7db3aadb879912c14f1a6dd814a82c..b37a55e19f389bcf7e5ccd09b52e2b3f6b7ff094 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -71,7 +71,7 @@ impl State { }; cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -92,7 +92,7 @@ impl State { }; cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await .log_err(); this.update(cx, |this, cx| { @@ -119,7 +119,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 823d59ce12ea45bfd5bc45d5889bff5ee7800d2a..c303a8c305af4f09c535b40fc30c0a185efab3da 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -661,7 +661,7 @@ impl LogStore { IoKind::StdOut => true, IoKind::StdIn => false, IoKind::StdErr => { - self.add_language_server_log(language_server_id, MessageType::LOG, &message, cx); + self.add_language_server_log(language_server_id, MessageType::LOG, message, cx); return Some(()); } }; diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index a1a5418220442f44072b8a8b88b96184778c972b..2480d4026883ab5b6e3af7f8a4d8bbbb59757879 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -106,7 +106,7 @@ impl LspAdapter for CssLspAdapter { .should_install_npm_package( Self::PACKAGE_NAME, &server_path, - &container_dir, + container_dir, VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/github_download.rs b/crates/languages/src/github_download.rs index 5b0f1d0729c6ca620c6983ce3c3d64c5d7274314..766c894fbb2b660778f09933b4facd2114ebb5bf 100644 --- a/crates/languages/src/github_download.rs +++ b/crates/languages/src/github_download.rs @@ -96,7 +96,7 @@ async fn stream_response_archive( AssetKind::TarGz => extract_tar_gz(destination_path, url, response).await?, AssetKind::Gz => extract_gz(destination_path, url, response).await?, AssetKind::Zip => { - util::archive::extract_zip(&destination_path, response).await?; + util::archive::extract_zip(destination_path, response).await?; } }; Ok(()) @@ -113,11 +113,11 @@ async fn stream_file_archive( AssetKind::Gz => extract_gz(destination_path, url, file_archive).await?, #[cfg(not(windows))] AssetKind::Zip => { - util::archive::extract_seekable_zip(&destination_path, file_archive).await?; + util::archive::extract_seekable_zip(destination_path, file_archive).await?; } #[cfg(windows)] AssetKind::Zip => { - util::archive::extract_zip(&destination_path, file_archive).await?; + util::archive::extract_zip(destination_path, file_archive).await?; } }; Ok(()) diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 4db48c67f05870d10d2ab2313900f86650fe9e6e..6f57ace4889e4f190f4993e4d7cbf0d4f51f6188 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -343,7 +343,7 @@ impl LspAdapter for JsonLspAdapter { .should_install_npm_package( Self::PACKAGE_NAME, &server_path, - &container_dir, + container_dir, VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 222e3f1946968faf654c0ea9b33c4b8da43d5c10..17d0d98fad07de0b21d5b950fdb8f56cf0a59cc3 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -204,7 +204,7 @@ impl LspAdapter for PythonLspAdapter { .should_install_npm_package( Self::SERVER_NAME.as_ref(), &server_path, - &container_dir, + container_dir, VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 3ef7c1ba3442ee9fd953fbeed5ac528feb500a9a..bbdfcdb4990d0ab02aadfa976669639c69615d64 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -581,7 +581,7 @@ impl ContextProvider for RustContextProvider { if let (Some(path), Some(stem)) = (&local_abs_path, task_variables.get(&VariableName::Stem)) { - let fragment = test_fragment(&variables, &path, stem); + let fragment = test_fragment(&variables, path, stem); variables.insert(RUST_TEST_FRAGMENT_TASK_VARIABLE, fragment); }; if let Some(test_name) = @@ -607,7 +607,7 @@ impl ContextProvider for RustContextProvider { } if let Some(path) = local_abs_path.as_ref() && let Some((target, manifest_path)) = - target_info_from_abs_path(&path, project_env.as_ref()).await + target_info_from_abs_path(path, project_env.as_ref()).await { if let Some(target) = target { variables.extend(TaskVariables::from_iter([ @@ -1570,7 +1570,7 @@ mod tests { let found = test_fragment( &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))), path, - &path.file_stem().unwrap().to_str().unwrap(), + path.file_stem().unwrap().to_str().unwrap(), ); assert_eq!(expected, found); } diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 27939c645cdb5a03f842974f849fb057d68e40b1..29a96d95151afb74a63cd051fa9bf58f923d8c9f 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -111,7 +111,7 @@ impl LspAdapter for TailwindLspAdapter { .should_install_npm_package( Self::PACKAGE_NAME, &server_path, - &container_dir, + container_dir, VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index dec7df4060463886236b7a296756b214b15a7359..d477acc7f641484d5cd99c4d7874877a286d071d 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -587,7 +587,7 @@ impl LspAdapter for TypeScriptLspAdapter { .should_install_npm_package( Self::PACKAGE_NAME, &server_path, - &container_dir, + container_dir, VersionStrategy::Latest(version.typescript_version.as_str()), ) .await; diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 137a9c2282eaf1ed62c042e1ad9581b3f6e3bbf8..6ac92e0b2bad1351e74a4a514fc8303971424b91 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -105,7 +105,7 @@ impl LspAdapter for YamlLspAdapter { .should_install_npm_package( Self::PACKAGE_NAME, &server_path, - &container_dir, + container_dir, VersionStrategy::Latest(version), ) .await; diff --git a/crates/livekit_client/src/test.rs b/crates/livekit_client/src/test.rs index e02c4d876fbe3411cf1730f3d97aaf8db3e208b6..e0058d1163d9d0a66d598ed3e09c9dcda221a90b 100644 --- a/crates/livekit_client/src/test.rs +++ b/crates/livekit_client/src/test.rs @@ -421,7 +421,7 @@ impl TestServer { track_sid: &TrackSid, muted: bool, ) -> Result<()> { - let claims = livekit_api::token::validate(&token, &self.secret_key)?; + let claims = livekit_api::token::validate(token, &self.secret_key)?; let room_name = claims.video.room.unwrap(); let identity = ParticipantIdentity(claims.sub.unwrap().to_string()); let mut server_rooms = self.rooms.lock(); @@ -475,7 +475,7 @@ impl TestServer { } pub(crate) fn is_track_muted(&self, token: &str, track_sid: &TrackSid) -> Option<bool> { - let claims = livekit_api::token::validate(&token, &self.secret_key).ok()?; + let claims = livekit_api::token::validate(token, &self.secret_key).ok()?; let room_name = claims.video.room.unwrap(); let mut server_rooms = self.rooms.lock(); diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index a3235a977359270a9c1db0850ad7bb096a90d02d..e5709bc07c5aaf76800226fcc6594b33031e17bf 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -875,7 +875,7 @@ impl Element for MarkdownElement { (CodeBlockRenderer::Custom { render, .. }, _) => { let parent_container = render( kind, - &parsed_markdown, + parsed_markdown, range.clone(), metadata.clone(), window, diff --git a/crates/markdown/src/parser.rs b/crates/markdown/src/parser.rs index 1035335ccb40f63133c727b5a5be8930d42b818f..3720e5b1ef5f61f0a209ac5617119de61ed05517 100644 --- a/crates/markdown/src/parser.rs +++ b/crates/markdown/src/parser.rs @@ -247,7 +247,7 @@ pub fn parse_markdown( events.push(event_for( text, range.source_range.start..range.source_range.start + prefix_len, - &head, + head, )); range.parsed = CowStr::Boxed(tail.into()); range.merged_range.start += prefix_len; diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 37d2ca21105566f1e2e3271f49c75a3ce1d7846b..3acc4b560025cc3c8f5c98ddd2a53f646955a57b 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -459,13 +459,13 @@ fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) - let mut max_lengths: Vec<usize> = vec![0; parsed.header.children.len()]; for (index, cell) in parsed.header.children.iter().enumerate() { - let length = paragraph_len(&cell); + let length = paragraph_len(cell); max_lengths[index] = length; } for row in &parsed.body { for (index, cell) in row.children.iter().enumerate() { - let length = paragraph_len(&cell); + let length = paragraph_len(cell); if length > max_lengths[index] { max_lengths[index] = length; diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index b425f7f1d5dc691ed1501d712ab72556412f7eb6..88e3e12f024805f4e2834e169acefdeac324818e 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -37,7 +37,7 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Opt let mut edits = vec![]; while let Some(mat) = matches.next() { if let Some((_, callback)) = patterns.get(mat.pattern_index) { - edits.extend(callback(&text, &mat, query)); + edits.extend(callback(text, mat, query)); } } @@ -170,7 +170,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> { pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> { migrate( - &text, + text, &[( SETTINGS_NESTED_KEY_VALUE_PATTERN, migrations::m_2025_01_29::replace_edit_prediction_provider_setting, @@ -293,12 +293,12 @@ mod tests { use super::*; fn assert_migrate_keymap(input: &str, output: Option<&str>) { - let migrated = migrate_keymap(&input).unwrap(); + let migrated = migrate_keymap(input).unwrap(); pretty_assertions::assert_eq!(migrated.as_deref(), output); } fn assert_migrate_settings(input: &str, output: Option<&str>) { - let migrated = migrate_settings(&input).unwrap(); + let migrated = migrate_settings(input).unwrap(); pretty_assertions::assert_eq!(migrated.as_deref(), output); } diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs index 1305328d384023517dbb80d25e210b44e632eed8..8584519d56dfd49f2a8e43eaea32c11d38d2c25c 100644 --- a/crates/multi_buffer/src/anchor.rs +++ b/crates/multi_buffer/src/anchor.rs @@ -145,7 +145,7 @@ impl Anchor { .map(|diff| diff.base_text()) { if a.buffer_id == Some(base_text.remote_id()) { - return a.bias_right(&base_text); + return a.bias_right(base_text); } } a @@ -212,7 +212,7 @@ impl AnchorRangeExt for Range<Anchor> { } fn includes(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool { - self.start.cmp(&other.start, &buffer).is_le() && other.end.cmp(&self.end, &buffer).is_le() + self.start.cmp(&other.start, buffer).is_le() && other.end.cmp(&self.end, buffer).is_le() } fn overlaps(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool { diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index eb12e6929cbc4bf74f44a2cb6eb9970c825d0fe3..59eaa9934dc5432418c5758dbeb700d4658fdc93 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1686,7 +1686,7 @@ impl MultiBuffer { cx: &mut Context<Self>, ) -> (Vec<Range<Anchor>>, bool) { let (excerpt_ids, added_a_new_excerpt) = - self.update_path_excerpts(path, buffer, &buffer_snapshot, new, cx); + self.update_path_excerpts(path, buffer, buffer_snapshot, new, cx); let mut result = Vec::new(); let mut ranges = ranges.into_iter(); @@ -1784,7 +1784,7 @@ impl MultiBuffer { } Some(( *existing_id, - excerpt.range.context.to_point(&buffer_snapshot), + excerpt.range.context.to_point(buffer_snapshot), )) } else { None @@ -3056,7 +3056,7 @@ impl MultiBuffer { snapshot.has_conflict = has_conflict; for (id, diff) in self.diffs.iter() { - if snapshot.diffs.get(&id).is_none() { + if snapshot.diffs.get(id).is_none() { snapshot.diffs.insert(*id, diff.diff.read(cx).snapshot(cx)); } } @@ -3177,7 +3177,7 @@ impl MultiBuffer { &mut new_diff_transforms, &mut end_of_current_insert, &mut old_expanded_hunks, - &snapshot, + snapshot, change_kind, ); @@ -3223,7 +3223,7 @@ impl MultiBuffer { old_expanded_hunks.clear(); self.push_buffer_content_transform( - &snapshot, + snapshot, &mut new_diff_transforms, excerpt_offset, end_of_current_insert, @@ -3916,8 +3916,8 @@ impl MultiBufferSnapshot { &self, range: Range<T>, ) -> Vec<(&BufferSnapshot, Range<usize>, ExcerptId)> { - let start = range.start.to_offset(&self); - let end = range.end.to_offset(&self); + let start = range.start.to_offset(self); + let end = range.end.to_offset(self); let mut cursor = self.cursor::<usize>(); cursor.seek(&start); @@ -3955,8 +3955,8 @@ impl MultiBufferSnapshot { &self, range: Range<T>, ) -> impl Iterator<Item = (&BufferSnapshot, Range<usize>, ExcerptId, Option<Anchor>)> + '_ { - let start = range.start.to_offset(&self); - let end = range.end.to_offset(&self); + let start = range.start.to_offset(self); + let end = range.end.to_offset(self); let mut cursor = self.cursor::<usize>(); cursor.seek(&start); @@ -4186,7 +4186,7 @@ impl MultiBufferSnapshot { } let start = Anchor::in_buffer(excerpt.id, excerpt.buffer_id, hunk.buffer_range.start) - .to_point(&self); + .to_point(self); return Some(MultiBufferRow(start.row)); } } @@ -4204,7 +4204,7 @@ impl MultiBufferSnapshot { continue; }; let start = Anchor::in_buffer(excerpt.id, excerpt.buffer_id, hunk.buffer_range.start) - .to_point(&self); + .to_point(self); return Some(MultiBufferRow(start.row)); } } @@ -4455,7 +4455,7 @@ impl MultiBufferSnapshot { let mut buffer_position = region.buffer_range.start; buffer_position.add_assign(&overshoot); let clipped_buffer_position = - clip_buffer_position(®ion.buffer, buffer_position, bias); + clip_buffer_position(region.buffer, buffer_position, bias); let mut position = region.range.start; position.add_assign(&(clipped_buffer_position - region.buffer_range.start)); position @@ -4485,7 +4485,7 @@ impl MultiBufferSnapshot { let buffer_start_value = region.buffer_range.start.value.unwrap(); let mut buffer_key = buffer_start_key; buffer_key.add_assign(&(key - start_key)); - let buffer_value = convert_buffer_dimension(®ion.buffer, buffer_key); + let buffer_value = convert_buffer_dimension(region.buffer, buffer_key); let mut result = start_value; result.add_assign(&(buffer_value - buffer_start_value)); result @@ -4633,7 +4633,7 @@ impl MultiBufferSnapshot { .as_str() == **delimiter { - indent.push_str(&delimiter); + indent.push_str(delimiter); break; } } @@ -4897,8 +4897,8 @@ impl MultiBufferSnapshot { if let Some(base_text) = self.diffs.get(buffer_id).map(|diff| diff.base_text()) { - if base_text.can_resolve(&diff_base_anchor) { - let base_text_offset = diff_base_anchor.to_offset(&base_text); + if base_text.can_resolve(diff_base_anchor) { + let base_text_offset = diff_base_anchor.to_offset(base_text); if base_text_offset >= base_text_byte_range.start && base_text_offset <= base_text_byte_range.end { @@ -6418,7 +6418,7 @@ impl MultiBufferSnapshot { for (ix, entry) in excerpt_ids.iter().enumerate() { if ix == 0 { - if entry.id.cmp(&ExcerptId::min(), &self).is_le() { + if entry.id.cmp(&ExcerptId::min(), self).is_le() { panic!("invalid first excerpt id {:?}", entry.id); } } else if entry.id <= excerpt_ids[ix - 1].id { @@ -6648,7 +6648,7 @@ where hunk_info, .. } => { - let diff = self.diffs.get(&buffer_id)?; + let diff = self.diffs.get(buffer_id)?; let buffer = diff.base_text(); let mut rope_cursor = buffer.as_rope().cursor(0); let buffer_start = rope_cursor.summary::<D>(base_text_byte_range.start); @@ -7767,7 +7767,7 @@ impl<'a> Iterator for MultiBufferChunks<'a> { } chunks } else { - let base_buffer = &self.diffs.get(&buffer_id)?.base_text(); + let base_buffer = &self.diffs.get(buffer_id)?.base_text(); base_buffer.chunks(base_text_start..base_text_end, self.language_aware) }; diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 824efa559f6d52bf654d8f6c6ff9655eaf4a0e52..fefeddb4da049ccf26e76fdb7075dede99fb10e4 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -473,7 +473,7 @@ fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) { let base_text = "one\ntwo\nfour\nfive\nsix\nseven\n"; let text = "one\ntwo\nTHREE\nfour\nfive\nseven\n"; let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer, cx)); + let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { @@ -2265,14 +2265,14 @@ impl ReferenceMultibuffer { } if !excerpt.expanded_diff_hunks.iter().any(|expanded_anchor| { - expanded_anchor.to_offset(&buffer).max(buffer_range.start) + expanded_anchor.to_offset(buffer).max(buffer_range.start) == hunk_range.start.max(buffer_range.start) }) { log::trace!("skipping a hunk that's not marked as expanded"); continue; } - if !hunk.buffer_range.start.is_valid(&buffer) { + if !hunk.buffer_range.start.is_valid(buffer) { log::trace!("skipping hunk with deleted start: {:?}", hunk.range); continue; } @@ -2449,7 +2449,7 @@ impl ReferenceMultibuffer { return false; } while let Some(hunk) = hunks.peek() { - match hunk.buffer_range.start.cmp(&hunk_anchor, &buffer) { + match hunk.buffer_range.start.cmp(hunk_anchor, &buffer) { cmp::Ordering::Less => { hunks.next(); } @@ -2519,8 +2519,8 @@ async fn test_random_set_ranges(cx: &mut TestAppContext, mut rng: StdRng) { let mut seen_ranges = Vec::default(); for (_, buf, range) in snapshot.excerpts() { - let start = range.context.start.to_point(&buf); - let end = range.context.end.to_point(&buf); + let start = range.context.start.to_point(buf); + let end = range.context.end.to_point(buf); seen_ranges.push(start..end); if let Some(last_end) = last_end.take() { @@ -2739,9 +2739,8 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { let id = buffer_handle.read(cx).remote_id(); if multibuffer.diff_for(id).is_none() { let base_text = base_texts.get(&id).unwrap(); - let diff = cx.new(|cx| { - BufferDiff::new_with_base_text(base_text, &buffer_handle, cx) - }); + let diff = cx + .new(|cx| BufferDiff::new_with_base_text(base_text, buffer_handle, cx)); reference.add_diff(diff.clone(), cx); multibuffer.add_diff(diff, cx) } @@ -3604,7 +3603,7 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) { offsets[ix - 1], ); assert!( - prev_anchor.cmp(&anchor, snapshot).is_lt(), + prev_anchor.cmp(anchor, snapshot).is_lt(), "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()", offsets[ix - 1], offsets[ix], diff --git a/crates/multi_buffer/src/position.rs b/crates/multi_buffer/src/position.rs index 06508750597b97d7275b964114bcdad0d0e34c79..8a3ce78d0d9f7a6880dbc3202c002507c800b7b0 100644 --- a/crates/multi_buffer/src/position.rs +++ b/crates/multi_buffer/src/position.rs @@ -126,17 +126,17 @@ impl<T> Default for TypedRow<T> { impl<T> PartialOrd for TypedOffset<T> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { - Some(self.cmp(&other)) + Some(self.cmp(other)) } } impl<T> PartialOrd for TypedPoint<T> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { - Some(self.cmp(&other)) + Some(self.cmp(other)) } } impl<T> PartialOrd for TypedRow<T> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { - Some(self.cmp(&other)) + Some(self.cmp(other)) } } diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index e07a8dc9fb6c6c20b311863da1414dfd6e83eecd..884374a72fe8b71bc55803b800c9429c19a96d5e 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -494,7 +494,7 @@ impl Onboarding { window .spawn(cx, async move |cx| { client - .sign_in_with_optional_connect(true, &cx) + .sign_in_with_optional_connect(true, cx) .await .notify_async_err(cx); }) diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 610f6a98e322b24207777aa7f307b848e3a49f3c..3fe9c32a48c4f6ee9cb3756e08b1eb9a836657dc 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -104,7 +104,7 @@ impl<const COLS: usize> Section<COLS> { self.entries .iter() .enumerate() - .map(|(index, entry)| entry.render(index_offset + index, &focus, window, cx)), + .map(|(index, entry)| entry.render(index_offset + index, focus, window, cx)), ) } } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 004a27b0cf06a2be90969767ddd95b8eb4de47e6..9514fd7e364d7969bad33e5e4a7e4bc0d70124aa 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -5498,7 +5498,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5514,7 +5514,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5532,7 +5532,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5569,7 +5569,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5583,7 +5583,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5602,7 +5602,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5630,7 +5630,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5718,7 +5718,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, None, cx, @@ -5741,7 +5741,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, None, cx, @@ -5767,7 +5767,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, None, cx, @@ -5873,7 +5873,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5896,7 +5896,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5933,7 +5933,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5970,7 +5970,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6073,7 +6073,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6099,7 +6099,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6123,7 +6123,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6144,7 +6144,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6232,7 +6232,7 @@ struct OutlineEntryExcerpt { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6259,7 +6259,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6286,7 +6286,7 @@ outline: struct OutlineEntryExcerpt <==== selected assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6313,7 +6313,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6340,7 +6340,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6367,7 +6367,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6394,7 +6394,7 @@ outline: struct OutlineEntryExcerpt <==== selected assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6421,7 +6421,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6448,7 +6448,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6475,7 +6475,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6502,7 +6502,7 @@ outline: struct OutlineEntryExcerpt <==== selected assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6608,7 +6608,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6645,7 +6645,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6673,7 +6673,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6705,7 +6705,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6736,7 +6736,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6864,7 +6864,7 @@ outline: struct OutlineEntryExcerpt .render_data .get_or_init(|| SearchData::new( &search_entry.match_range, - &multi_buffer_snapshot + multi_buffer_snapshot )) .context_text ) @@ -7255,7 +7255,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7314,7 +7314,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7338,7 +7338,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7403,7 +7403,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7544,7 +7544,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7582,7 +7582,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7616,7 +7616,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7648,7 +7648,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index c96ab4e8f3ba87133d9b64e9701130f5d32adfb9..f80f24bb717cda4e9d061f6dbc0cc98186036f08 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -368,7 +368,7 @@ impl ContextServerStore { } pub fn restart_server(&mut self, id: &ContextServerId, cx: &mut Context<Self>) -> Result<()> { - if let Some(state) = self.servers.get(&id) { + if let Some(state) = self.servers.get(id) { let configuration = state.configuration(); self.stop_server(&state.server().id(), cx)?; @@ -397,7 +397,7 @@ impl ContextServerStore { let server = server.clone(); let configuration = configuration.clone(); async move |this, cx| { - match server.clone().start(&cx).await { + match server.clone().start(cx).await { Ok(_) => { log::info!("Started {} context server", id); debug_assert!(server.client().is_some()); @@ -588,7 +588,7 @@ impl ContextServerStore { for server_id in this.servers.keys() { // All servers that are not in desired_servers should be removed from the store. // This can happen if the user removed a server from the context server settings. - if !configured_servers.contains_key(&server_id) { + if !configured_servers.contains_key(server_id) { if disabled_servers.contains_key(&server_id.0) { servers_to_stop.insert(server_id.clone()); } else { diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 025dca410069db0350d8d32509244a4889c62415..091189db7c2345e33d5a830669b0169d1d2b0ff2 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -317,8 +317,8 @@ impl BreakpointStore { .iter() .filter_map(|breakpoint| { breakpoint.bp.bp.to_proto( - &path, - &breakpoint.position(), + path, + breakpoint.position(), &breakpoint.session_state, ) }) @@ -753,7 +753,7 @@ impl BreakpointStore { .iter() .map(|breakpoint| { let position = snapshot - .summary_for_anchor::<PointUtf16>(&breakpoint.position()) + .summary_for_anchor::<PointUtf16>(breakpoint.position()) .row; let breakpoint = &breakpoint.bp; SourceBreakpoint { diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 6f834b5dc0cfd3fc6357d92403bdb7cbfefdd4b0..ccda64fba8ed3ada5e97d12048337d5d90ce65ac 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -215,7 +215,7 @@ impl DapStore { dap_settings.and_then(|s| s.binary.as_ref().map(PathBuf::from)); let user_args = dap_settings.map(|s| s.args.clone()); - let delegate = self.delegate(&worktree, console, cx); + let delegate = self.delegate(worktree, console, cx); let cwd: Arc<Path> = worktree.read(cx).abs_path().as_ref().into(); cx.spawn(async move |this, cx| { @@ -902,7 +902,7 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate { } fn worktree_root_path(&self) -> &Path { - &self.worktree.abs_path() + self.worktree.abs_path() } fn http_client(&self) -> Arc<dyn HttpClient> { self.http_client.clone() diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index fa265dae586148f9c8efe14187ee26c805c65e42..9a36584e717d9d9d7fcb8b013d5a15a9826d35a8 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -187,12 +187,12 @@ impl DapLocator for CargoLocator { .cloned(); } let executable = { - if let Some(ref name) = test_name.as_ref().and_then(|name| { + if let Some(name) = test_name.as_ref().and_then(|name| { name.strip_prefix('$') .map(|name| build_config.env.get(name)) .unwrap_or(Some(name)) }) { - find_best_executable(&executables, &name).await + find_best_executable(&executables, name).await } else { None } diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index d9c28df497b3baa4543e6271106ddb1cd11b4419..b5ae7148410884c75fb03bbf9ba68bef90eadad7 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1630,7 +1630,7 @@ impl Session { + 'static, cx: &mut Context<Self>, ) -> Task<Option<T::Response>> { - if !T::is_supported(&capabilities) { + if !T::is_supported(capabilities) { log::warn!( "Attempted to send a DAP request that isn't supported: {:?}", request @@ -1688,7 +1688,7 @@ impl Session { self.requests .entry((&*key.0 as &dyn Any).type_id()) .and_modify(|request_map| { - request_map.remove(&key); + request_map.remove(key); }); } diff --git a/crates/project/src/environment.rs b/crates/project/src/environment.rs index 7379a7ef726c6004fc2b29a5b61a47cb9603fbb3..d109e307a89181f0a416d6d01a3fa74684a138a7 100644 --- a/crates/project/src/environment.rs +++ b/crates/project/src/environment.rs @@ -198,7 +198,7 @@ async fn load_directory_shell_environment( ); }; - load_shell_environment(&dir, load_direnv).await + load_shell_environment(dir, load_direnv).await } Err(err) => ( None, diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 3163a10239f6ccdba7452697b9d9cac18a721ec3..e8ba2425d140f9bddf079cf835b25f621a5fe7f8 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -561,7 +561,7 @@ impl GitStore { pub fn active_repository(&self) -> Option<Entity<Repository>> { self.active_repo_id .as_ref() - .map(|id| self.repositories[&id].clone()) + .map(|id| self.repositories[id].clone()) } pub fn open_unstaged_diff( @@ -1277,7 +1277,7 @@ impl GitStore { ) { match event { BufferStoreEvent::BufferAdded(buffer) => { - cx.subscribe(&buffer, |this, buffer, event, cx| { + cx.subscribe(buffer, |this, buffer, event, cx| { if let BufferEvent::LanguageChanged = event { let buffer_id = buffer.read(cx).remote_id(); if let Some(diff_state) = this.diffs.get(&buffer_id) { @@ -1295,7 +1295,7 @@ impl GitStore { } } BufferStoreEvent::BufferDropped(buffer_id) => { - self.diffs.remove(&buffer_id); + self.diffs.remove(buffer_id); for diffs in self.shared_diffs.values_mut() { diffs.remove(buffer_id); } @@ -1384,8 +1384,8 @@ impl GitStore { repository.update(cx, |repository, cx| { let repo_abs_path = &repository.work_directory_abs_path; if changed_repos.iter().any(|update| { - update.old_work_directory_abs_path.as_ref() == Some(&repo_abs_path) - || update.new_work_directory_abs_path.as_ref() == Some(&repo_abs_path) + update.old_work_directory_abs_path.as_ref() == Some(repo_abs_path) + || update.new_work_directory_abs_path.as_ref() == Some(repo_abs_path) }) { repository.reload_buffer_diff_bases(cx); } @@ -1536,7 +1536,7 @@ impl GitStore { }); if is_new { this._subscriptions - .push(cx.subscribe(&repo, Self::on_repository_event)) + .push(cx.subscribe(repo, Self::on_repository_event)) } repo.update(cx, { @@ -2353,7 +2353,7 @@ impl GitStore { // All paths prefixed by a given repo will constitute a continuous range. while let Some(path) = entries.get(ix) && let Some(repo_path) = - RepositorySnapshot::abs_path_to_repo_path_inner(&repo_path, &path) + RepositorySnapshot::abs_path_to_repo_path_inner(&repo_path, path) { paths.push((repo_path, ix)); ix += 1; @@ -2875,14 +2875,14 @@ impl RepositorySnapshot { } pub fn had_conflict_on_last_merge_head_change(&self, repo_path: &RepoPath) -> bool { - self.merge.conflicted_paths.contains(&repo_path) + self.merge.conflicted_paths.contains(repo_path) } pub fn has_conflict(&self, repo_path: &RepoPath) -> bool { let had_conflict_on_last_merge_head_change = - self.merge.conflicted_paths.contains(&repo_path); + self.merge.conflicted_paths.contains(repo_path); let has_conflict_currently = self - .status_for_path(&repo_path) + .status_for_path(repo_path) .map_or(false, |entry| entry.status.is_conflicted()); had_conflict_on_last_merge_head_change || has_conflict_currently } diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index bbcffe046debd8ab4529cf2b661abbebefd13f47..de5ff9b93509a26023eb814076c86e0867f05257 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -211,7 +211,7 @@ impl Deref for GitEntryRef<'_> { type Target = Entry; fn deref(&self) -> &Self::Target { - &self.entry + self.entry } } diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index 79f134b91a36a2f7d1f3f256506931b47ae8cf9c..54d87d230cb856fb62d84fb34dc77907b2e6df19 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -224,7 +224,7 @@ impl ProjectItem for ImageItem { path: &ProjectPath, cx: &mut App, ) -> Option<Task<anyhow::Result<Entity<Self>>>> { - if is_image_file(&project, &path, cx) { + if is_image_file(project, path, cx) { Some(cx.spawn({ let path = path.clone(); let project = project.clone(); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index fcfeb9c66081624332d587a19b4ba9837aaf37bd..d5c3cc424f7521c8b31e740c991501e8404f4b93 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1165,7 +1165,7 @@ pub async fn location_link_from_lsp( server_id: LanguageServerId, cx: &mut AsyncApp, ) -> Result<LocationLink> { - let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, cx)?; + let (_, language_server) = language_server_for_buffer(lsp_store, buffer, server_id, cx)?; let (origin_range, target_uri, target_range) = ( link.origin_selection_range, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 11c78aad8d1080abee0138cacc16889232845f82..1bc6770d4e824c4dec6f1f28766749570ec1794f 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -442,14 +442,14 @@ impl LocalLspStore { match result { Ok(server) => { lsp_store - .update(cx, |lsp_store, mut cx| { + .update(cx, |lsp_store, cx| { lsp_store.insert_newly_running_language_server( adapter, server.clone(), server_id, key, pending_workspace_folders, - &mut cx, + cx, ); }) .ok(); @@ -1927,7 +1927,7 @@ impl LocalLspStore { if let Some(lsp_edits) = lsp_edits { this.update(cx, |this, cx| { this.as_local_mut().unwrap().edits_from_lsp( - &buffer_handle, + buffer_handle, lsp_edits, language_server.server_id(), None, @@ -3115,7 +3115,7 @@ impl LocalLspStore { let mut servers_to_remove = BTreeSet::default(); let mut servers_to_preserve = HashSet::default(); - for (seed, ref state) in &self.language_server_ids { + for (seed, state) in &self.language_server_ids { if seed.worktree_id == id_to_remove { servers_to_remove.insert(state.id); } else { @@ -3169,7 +3169,7 @@ impl LocalLspStore { for watcher in watchers { if let Some((worktree, literal_prefix, pattern)) = - self.worktree_and_path_for_file_watcher(&worktrees, &watcher, cx) + self.worktree_and_path_for_file_watcher(&worktrees, watcher, cx) { worktree.update(cx, |worktree, _| { if let Some((tree, glob)) = @@ -4131,7 +4131,7 @@ impl LspStore { local.registered_buffers.remove(&buffer_id); local.buffers_opened_in_servers.remove(&buffer_id); if let Some(file) = File::from_dyn(buffer.read(cx).file()).cloned() { - local.unregister_old_buffer_from_language_servers(&buffer, &file, cx); + local.unregister_old_buffer_from_language_servers(buffer, &file, cx); } } }) @@ -4453,7 +4453,7 @@ impl LspStore { .contains(&server_status.name) .then_some(server_id) }) - .filter_map(|server_id| self.lsp_server_capabilities.get(&server_id)) + .filter_map(|server_id| self.lsp_server_capabilities.get(server_id)) .any(check) } @@ -5419,7 +5419,7 @@ impl LspStore { ) -> Task<Result<Vec<LocationLink>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetTypeDefinitions { position }; - if !self.is_capable_for_proto_request(&buffer, &request, cx) { + if !self.is_capable_for_proto_request(buffer, &request, cx) { return Task::ready(Ok(Vec::new())); } let request_task = upstream_client.request(proto::MultiLspQuery { @@ -5573,7 +5573,7 @@ impl LspStore { ) -> Task<Result<Vec<Location>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetReferences { position }; - if !self.is_capable_for_proto_request(&buffer, &request, cx) { + if !self.is_capable_for_proto_request(buffer, &request, cx) { return Task::ready(Ok(Vec::new())); } let request_task = upstream_client.request(proto::MultiLspQuery { @@ -5755,7 +5755,7 @@ impl LspStore { let lsp_data = self.lsp_code_lens.entry(buffer_id).or_default(); if let Some((updating_for, running_update)) = &lsp_data.update { - if !version_queried_for.changed_since(&updating_for) { + if !version_queried_for.changed_since(updating_for) { return running_update.clone(); } } @@ -6786,7 +6786,7 @@ impl LspStore { let lsp_data = self.lsp_document_colors.entry(buffer_id).or_default(); if let Some((updating_for, running_update)) = &lsp_data.colors_update { - if !version_queried_for.changed_since(&updating_for) { + if !version_queried_for.changed_since(updating_for) { return Some(running_update.clone()); } } @@ -10057,7 +10057,7 @@ impl LspStore { ) -> Shared<Task<Option<HashMap<String, String>>>> { if let Some(environment) = &self.as_local().map(|local| local.environment.clone()) { environment.update(cx, |env, cx| { - env.get_buffer_environment(&buffer, &self.worktree_store, cx) + env.get_buffer_environment(buffer, &self.worktree_store, cx) }) } else { Task::ready(None).shared() @@ -11175,7 +11175,7 @@ impl LspStore { let Some(local) = self.as_local() else { return }; local.prettier_store.update(cx, |prettier_store, cx| { - prettier_store.update_prettier_settings(&worktree_handle, changes, cx) + prettier_store.update_prettier_settings(worktree_handle, changes, cx) }); let worktree_id = worktree_handle.read(cx).id(); diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index 8621d24d0631229d9f424e7ef7cc9040a58a6780..f68905d14c21b831ba579a0f7d8a80646156c46e 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -199,7 +199,7 @@ impl ManifestTree { ) { match evt { WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => { - self.root_points.remove(&worktree_id); + self.root_points.remove(worktree_id); } _ => {} } diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index 49c0cff7305da38872c18195287cc2612b516608..7da43feeeff91a84a23e1c44daba5328ae4ca435 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -192,7 +192,7 @@ impl LanguageServerTree { ) }); languages.insert(language_name.clone()); - Arc::downgrade(&node).into() + Arc::downgrade(node).into() }) } @@ -245,7 +245,7 @@ impl LanguageServerTree { if !settings.enable_language_server { return Default::default(); } - let available_lsp_adapters = self.languages.lsp_adapters(&language_name); + let available_lsp_adapters = self.languages.lsp_adapters(language_name); let available_language_servers = available_lsp_adapters .iter() .map(|lsp_adapter| lsp_adapter.name.clone()) @@ -287,7 +287,7 @@ impl LanguageServerTree { // (e.g., native vs extension) still end up in the right order at the end, rather than // it being based on which language server happened to be loaded in first. self.languages.reorder_language_servers( - &language_name, + language_name, adapters_with_settings .values() .map(|(_, adapter)| adapter.clone()) @@ -314,7 +314,7 @@ impl LanguageServerTree { pub(crate) fn remove_nodes(&mut self, ids: &BTreeSet<LanguageServerId>) { for (_, servers) in &mut self.instances { for (_, nodes) in &mut servers.roots { - nodes.retain(|_, (node, _)| node.id.get().map_or(true, |id| !ids.contains(&id))); + nodes.retain(|_, (node, _)| node.id.get().map_or(true, |id| !ids.contains(id))); } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 57afaceecabac903f20db1d38e09b3984335cdf1..17997850b630dbc8dd05c97d24c904198ceb7b75 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1848,7 +1848,7 @@ impl Project { cx: &'a mut App, ) -> Shared<Task<Option<HashMap<String, String>>>> { self.environment.update(cx, |environment, cx| { - environment.get_buffer_environment(&buffer, &worktree_store, cx) + environment.get_buffer_environment(buffer, worktree_store, cx) }) } @@ -2592,7 +2592,7 @@ impl Project { cx: &mut App, ) -> OpenLspBufferHandle { self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.register_buffer_with_language_servers(&buffer, HashSet::default(), false, cx) + lsp_store.register_buffer_with_language_servers(buffer, HashSet::default(), false, cx) }) } @@ -4167,15 +4167,14 @@ impl Project { }) .collect(); - cx.spawn(async move |_, mut cx| { + cx.spawn(async move |_, cx| { if let Some(buffer_worktree_id) = buffer_worktree_id { if let Some((worktree, _)) = worktrees_with_ids .iter() .find(|(_, id)| *id == buffer_worktree_id) { for candidate in candidates.iter() { - if let Some(path) = - Self::resolve_path_in_worktree(&worktree, candidate, &mut cx) + if let Some(path) = Self::resolve_path_in_worktree(worktree, candidate, cx) { return Some(path); } @@ -4187,9 +4186,7 @@ impl Project { continue; } for candidate in candidates.iter() { - if let Some(path) = - Self::resolve_path_in_worktree(&worktree, candidate, &mut cx) - { + if let Some(path) = Self::resolve_path_in_worktree(&worktree, candidate, cx) { return Some(path); } } @@ -5329,7 +5326,7 @@ impl ResolvedPath { pub fn project_path(&self) -> Option<&ProjectPath> { match self { - Self::ProjectPath { project_path, .. } => Some(&project_path), + Self::ProjectPath { project_path, .. } => Some(project_path), _ => None, } } @@ -5399,7 +5396,7 @@ impl Completion { _ => None, }) .unwrap_or(DEFAULT_KIND_KEY); - (kind_key, &self.label.filter_text()) + (kind_key, self.label.filter_text()) } /// Whether this completion is a snippet. diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index d78526ddd0432919ae17f9e753c207fc17a09b8b..050ca60e7a43d68ed926c1c94f7e7f270476186c 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -1105,7 +1105,7 @@ impl SettingsObserver { cx: &mut Context<Self>, ) -> Task<()> { let mut user_tasks_file_rx = - watch_config_file(&cx.background_executor(), fs, file_path.clone()); + watch_config_file(cx.background_executor(), fs, file_path.clone()); let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next()); let weak_entry = cx.weak_entity(); cx.spawn(async move |settings_observer, cx| { @@ -1160,7 +1160,7 @@ impl SettingsObserver { cx: &mut Context<Self>, ) -> Task<()> { let mut user_tasks_file_rx = - watch_config_file(&cx.background_executor(), fs, file_path.clone()); + watch_config_file(cx.background_executor(), fs, file_path.clone()); let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next()); let weak_entry = cx.weak_entity(); cx.spawn(async move |settings_observer, cx| { diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index d0f1c71daf797681d08a741ea15ab28f4a9289a0..8d8a1bd008d01f0e7b4c9ec8605dad6cbd715eff 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -333,7 +333,7 @@ impl Inventory { for locator in locators.values() { if let Some(scenario) = locator - .create_scenario(&task.original_task(), &task.display_label(), &adapter) + .create_scenario(task.original_task(), task.display_label(), &adapter) .await { scenarios.push((kind, scenario)); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index d5ddd89419b4955a031d904655c40eaa105d0b3c..892847a380bf1522750720ec59f74ed8db6e99dc 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -503,7 +503,7 @@ impl ProjectPanel { if let Some((worktree, expanded_dir_ids)) = project .read(cx) .worktree_for_id(*worktree_id, cx) - .zip(this.expanded_dir_ids.get_mut(&worktree_id)) + .zip(this.expanded_dir_ids.get_mut(worktree_id)) { let worktree = worktree.read(cx); @@ -3043,7 +3043,7 @@ impl ProjectPanel { if hide_root && Some(entry.entry) == worktree.read(cx).root_entry() { if new_entry_parent_id == Some(entry.id) { visible_worktree_entries.push(Self::create_new_git_entry( - &entry.entry, + entry.entry, entry.git_summary, new_entry_kind, )); @@ -3106,7 +3106,7 @@ impl ProjectPanel { }; if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) { visible_worktree_entries.push(Self::create_new_git_entry( - &entry.entry, + entry.entry, entry.git_summary, new_entry_kind, )); @@ -3503,7 +3503,7 @@ impl ProjectPanel { let base_index = ix + entry_range.start; for (i, entry) in visible.entries[entry_range].iter().enumerate() { let global_index = base_index + i; - callback(&entry, global_index, entries, window, cx); + callback(entry, global_index, entries, window, cx); } ix = end_ix; } @@ -4669,7 +4669,7 @@ impl ProjectPanel { }; let (depth, difference) = - ProjectPanel::calculate_depth_and_difference(&entry, entries_paths); + ProjectPanel::calculate_depth_and_difference(entry, entries_paths); let filename = match difference { diff if diff > 1 => entry diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 47aed8f470f3538f34bff0a0accdd55d9f1ac70e..9fffbde5f770d83fcbd569a8b27fa38e9f3c45ad 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -191,7 +191,7 @@ impl PickerDelegate for ProjectSymbolsDelegate { .iter() .enumerate() .map(|(id, symbol)| { - StringMatchCandidate::new(id, &symbol.label.filter_text()) + StringMatchCandidate::new(id, symbol.label.filter_text()) }) .partition(|candidate| { project diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 81259c1aac8bfdb61fe6d7e8d537bfbc6c06a56a..bc837b1a1ea0cc5724f9e9d852f1dac978ee1d2c 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1490,7 +1490,7 @@ impl RemoteServerProjects { .track_focus(&self.focus_handle(cx)) .id("ssh-server-list") .overflow_y_scroll() - .track_scroll(&scroll_handle) + .track_scroll(scroll_handle) .size_full() .child(connect_button) .child( diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index ea383ac26403a2dd5ca1bffb579044e7ffa1a530..71e8f6e8e758f47b82031cf2a817fd8d90569d60 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -730,7 +730,7 @@ impl SshRemoteClient { cx, ); - let multiplex_task = Self::monitor(this.downgrade(), io_task, &cx); + let multiplex_task = Self::monitor(this.downgrade(), io_task, cx); if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await { log::error!("failed to establish connection: {}", error); @@ -918,8 +918,8 @@ impl SshRemoteClient { } }; - let multiplex_task = Self::monitor(this.clone(), io_task, &cx); - client.reconnect(incoming_rx, outgoing_tx, &cx); + let multiplex_task = Self::monitor(this.clone(), io_task, cx); + client.reconnect(incoming_rx, outgoing_tx, cx); if let Err(error) = client.resync(HEARTBEAT_TIMEOUT).await { failed!(error, attempts, ssh_connection, delegate); @@ -1005,8 +1005,8 @@ impl SshRemoteClient { if missed_heartbeats != 0 { missed_heartbeats = 0; - let _ =this.update(cx, |this, mut cx| { - this.handle_heartbeat_result(missed_heartbeats, &mut cx) + let _ =this.update(cx, |this, cx| { + this.handle_heartbeat_result(missed_heartbeats, cx) })?; } } @@ -1036,8 +1036,8 @@ impl SshRemoteClient { continue; } - let result = this.update(cx, |this, mut cx| { - this.handle_heartbeat_result(missed_heartbeats, &mut cx) + let result = this.update(cx, |this, cx| { + this.handle_heartbeat_result(missed_heartbeats, cx) })?; if result.is_break() { return Ok(()); @@ -1214,7 +1214,7 @@ impl SshRemoteClient { .await .unwrap(); - connection.simulate_disconnect(&cx); + connection.simulate_disconnect(cx); }) } @@ -1523,7 +1523,7 @@ impl RemoteConnection for SshRemoteConnection { incoming_tx, outgoing_rx, connection_activity_tx, - &cx, + cx, ) } @@ -1908,8 +1908,8 @@ impl SshRemoteConnection { "-H", "Content-Type: application/json", "-d", - &body, - &url, + body, + url, "-o", &tmp_path_gz.to_string(), ], @@ -1930,8 +1930,8 @@ impl SshRemoteConnection { "--method=GET", "--header=Content-Type: application/json", "--body-data", - &body, - &url, + body, + url, "-O", &tmp_path_gz.to_string(), ], @@ -1982,7 +1982,7 @@ impl SshRemoteConnection { tmp_path_gz, size / 1024 ); - self.upload_file(&src_path, &tmp_path_gz) + self.upload_file(src_path, tmp_path_gz) .await .context("failed to upload server binary")?; log::info!("uploaded remote development server in {:?}", t0.elapsed()); @@ -2654,7 +2654,7 @@ mod fake { let (outgoing_tx, _) = mpsc::unbounded::<Envelope>(); let (_, incoming_rx) = mpsc::unbounded::<Envelope>(); self.server_channel - .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(&cx)); + .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx)); } fn start_proxy( diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index ac1737ba4bc4e9fe1c8ddcc38d0c28c73a424c10..6b0cc2219f205667a3b36c5ea41cd22a4892d421 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -348,7 +348,7 @@ impl HeadlessProject { .iter() .map(|action| action.title.to_string()) .collect(), - level: Some(prompt_to_proto(&prompt)), + level: Some(prompt_to_proto(prompt)), lsp_name: prompt.lsp_name.clone(), message: prompt.message.clone(), }); @@ -388,7 +388,7 @@ impl HeadlessProject { let parent = fs.canonicalize(parent).await.map_err(|_| { anyhow!( proto::ErrorCode::DevServerProjectPathDoesNotExist - .with_tag("path", &path.to_string_lossy().as_ref()) + .with_tag("path", path.to_string_lossy().as_ref()) ) })?; parent.join(path.file_name().unwrap()) diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index dc7fab8c3cda7416de2788cb421984a11203f769..4daacb3eec5d63987a3a9576153ba29a82e4fa32 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -155,7 +155,7 @@ fn init_panic_hook(session_id: String) { log::error!( "panic occurred: {}\nBacktrace:\n{}", &payload, - (&backtrace).join("\n") + backtrace.join("\n") ); let panic_data = telemetry_events::Panic { @@ -796,11 +796,8 @@ fn initialize_settings( fs: Arc<dyn Fs>, cx: &mut App, ) -> watch::Receiver<Option<NodeBinaryOptions>> { - let user_settings_file_rx = watch_config_file( - &cx.background_executor(), - fs, - paths::settings_file().clone(), - ); + let user_settings_file_rx = + watch_config_file(cx.background_executor(), fs, paths::settings_file().clone()); handle_settings_file_changes(user_settings_file_rx, cx, { let session = session.clone(); diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 36a0af30d0d86e4548acd1ff4244dc096a5fb788..a84f147dd27a178f25bd01ee8695de26ecde811f 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -575,7 +575,7 @@ impl project::ProjectItem for NotebookItem { .with_context(|| format!("finding the absolute path of {path:?}"))?; // todo: watch for changes to the file - let file_content = fs.load(&abs_path.as_path()).await?; + let file_content = fs.load(abs_path.as_path()).await?; let notebook = nbformat::parse_notebook(&file_content); let notebook = match notebook { diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index dc00674380ff712d7f99dab0171db1290f3eb128..96f7d1db1104cccb962c9d2ca740ccc679951bd4 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -49,7 +49,7 @@ impl Chunk { self.chars_utf16 |= slice.chars_utf16 << base_ix; self.newlines |= slice.newlines << base_ix; self.tabs |= slice.tabs << base_ix; - self.text.push_str(&slice.text); + self.text.push_str(slice.text); } #[inline(always)] @@ -623,7 +623,7 @@ mod tests { let text = &text[..ix]; log::info!("Chunk: {:?}", text); - let chunk = Chunk::new(&text); + let chunk = Chunk::new(text); verify_chunk(chunk.as_slice(), text); for _ in 0..10 { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 904c74d03c9606c2864513026cf88e017aaba74a..1afbc2c23b6ff74379a4fea6a6a375890584c6e4 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -142,7 +142,7 @@ impl SearchOption { SearchSource::Buffer => { let focus_handle = focus_handle.clone(); button.on_click(move |_: &ClickEvent, window, cx| { - if !focus_handle.is_focused(&window) { + if !focus_handle.is_focused(window) { window.focus(&focus_handle); } window.dispatch_action(action.boxed_clone(), cx); diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 8cc838a8a69e0278af49b7fc28062ebf70f3fc49..44f6b3fdd21388f37cfbe2011e5a3e530b0be654 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -26,7 +26,7 @@ pub(super) fn render_action_button( .on_click({ let focus_handle = focus_handle.clone(); move |_, window, cx| { - if !focus_handle.is_focused(&window) { + if !focus_handle.is_focused(window) { window.focus(&focus_handle); } window.dispatch_action(action.boxed_clone(), cx) diff --git a/crates/semantic_index/src/summary_index.rs b/crates/semantic_index/src/summary_index.rs index 6e3aae1344d8873ef2ac602e6afd648ceff57384..20858c8d3f0d2b908c19f852c90da3d259dd7b2d 100644 --- a/crates/semantic_index/src/summary_index.rs +++ b/crates/semantic_index/src/summary_index.rs @@ -324,7 +324,7 @@ impl SummaryIndex { ) -> Vec<(Arc<Path>, Option<MTime>)> { let entry_db_key = db_key_for_path(&entry.path); - match digest_db.get(&txn, &entry_db_key) { + match digest_db.get(txn, &entry_db_key) { Ok(opt_saved_digest) => { // The file path is the same, but the mtime is different. (Or there was no mtime.) // It needs updating, so add it to the backlog! Then, if the backlog is full, drain it and summarize its contents. @@ -575,7 +575,7 @@ impl SummaryIndex { let code_len = code.len(); cx.spawn(async move |cx| { - let stream = model.stream_completion(request, &cx); + let stream = model.stream_completion(request, cx); cx.background_spawn(async move { let answer: String = stream .await? diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index fb036622907487fd0e42b3e58d28d93acf77b340..b0f7d2449e51bdfd8877d6e4d57e6c524d493279 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -358,11 +358,11 @@ impl KeymapFile { let action_input = items[1].clone(); let action_input_string = action_input.to_string(); ( - cx.build_action(&name, Some(action_input)), + cx.build_action(name, Some(action_input)), Some(action_input_string), ) } - Value::String(name) => (cx.build_action(&name, None), None), + Value::String(name) => (cx.build_action(name, None), None), Value::Null => (Ok(NoAction.boxed_clone()), None), _ => { return Err(format!( @@ -839,7 +839,7 @@ impl KeymapFile { if &action.0 != target_action_value { continue; } - return Some((index, &keystrokes_str)); + return Some((index, keystrokes_str)); } } None diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 5181d86a789680483b642087e7e0df1ff8a5f562..58090d2060fd9d1af0429a0bf7c511f5ee0e4ed3 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -270,7 +270,7 @@ impl ConflictState { for origin in indices.iter() { conflicts[origin.index] = - origin.get_conflict_with(if origin == fst { &snd } else { &fst }) + origin.get_conflict_with(if origin == fst { snd } else { fst }) } has_user_conflicts |= fst.override_source == KeybindSource::User @@ -673,8 +673,8 @@ impl KeymapEditor { action_name, action_arguments, &actions_with_schemas, - &action_documentation, - &humanized_action_names, + action_documentation, + humanized_action_names, ); let index = processed_bindings.len(); @@ -696,8 +696,8 @@ impl KeymapEditor { action_name, None, &actions_with_schemas, - &action_documentation, - &humanized_action_names, + action_documentation, + humanized_action_names, ); let string_match_candidate = StringMatchCandidate::new(index, &action_information.humanized_name); @@ -2187,7 +2187,7 @@ impl KeybindingEditorModal { }) .transpose()?; - cx.build_action(&self.editing_keybind.action().name, value) + cx.build_action(self.editing_keybind.action().name, value) .context("Failed to validate action arguments")?; Ok(action_arguments) } @@ -2862,11 +2862,8 @@ impl CompletionProvider for KeyContextCompletionProvider { break; } } - let start_anchor = buffer.anchor_before( - buffer_position - .to_offset(&buffer) - .saturating_sub(count_back), - ); + let start_anchor = + buffer.anchor_before(buffer_position.to_offset(buffer).saturating_sub(count_back)); let replace_range = start_anchor..buffer_position; gpui::Task::ready(Ok(vec![project::CompletionResponse { completions: self @@ -2983,14 +2980,14 @@ async fn save_keybinding_update( let target = settings::KeybindUpdateTarget { context: existing_context, keystrokes: existing_keystrokes, - action_name: &existing.action().name, + action_name: existing.action().name, action_arguments: existing_args, }; let source = settings::KeybindUpdateTarget { context: action_mapping.context.as_ref().map(|a| &***a), keystrokes: &action_mapping.keystrokes, - action_name: &existing.action().name, + action_name: existing.action().name, action_arguments: new_args, }; @@ -3044,7 +3041,7 @@ async fn remove_keybinding( target: settings::KeybindUpdateTarget { context: existing.context().and_then(KeybindContextString::local_str), keystrokes, - action_name: &existing.action().name, + action_name: existing.action().name, action_arguments: existing .action() .arguments diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 2b3e815f369a96235c8628935df433737d58b0ce..66dd636d21eb2b3f1372fe869e8bb5d15ce31627 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -343,7 +343,7 @@ impl TableInteractionState { .on_any_mouse_down(|_, _, cx| { cx.stop_propagation(); }) - .on_scroll_wheel(Self::listener(&this, |_, _, _, cx| { + .on_scroll_wheel(Self::listener(this, |_, _, _, cx| { cx.notify(); })) .children(Scrollbar::vertical( diff --git a/crates/streaming_diff/src/streaming_diff.rs b/crates/streaming_diff/src/streaming_diff.rs index f7649b1bf1a3099a689feef33e2fe63faa84ab97..704164e01eedc64cac9a1e8e4e82f584a0b4fdb9 100644 --- a/crates/streaming_diff/src/streaming_diff.rs +++ b/crates/streaming_diff/src/streaming_diff.rs @@ -303,10 +303,10 @@ impl LineDiff { self.flush_insert(old_text); self.buffered_insert.push_str(suffix); } else { - self.buffered_insert.push_str(&text); + self.buffered_insert.push_str(text); } } else { - self.buffered_insert.push_str(&text); + self.buffered_insert.push_str(text); if !text.ends_with('\n') { self.flush_insert(old_text); } @@ -523,7 +523,7 @@ mod tests { apply_line_operations(old_text, &new_text, &expected_line_ops) ); - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!(line_ops, expected_line_ops); } @@ -534,7 +534,7 @@ mod tests { CharOperation::Keep { bytes: 5 }, CharOperation::Delete { bytes: 4 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -559,7 +559,7 @@ mod tests { text: "\ncccc".into(), }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -582,7 +582,7 @@ mod tests { CharOperation::Delete { bytes: 5 }, CharOperation::Keep { bytes: 4 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -609,7 +609,7 @@ mod tests { }, CharOperation::Keep { bytes: 5 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -638,7 +638,7 @@ mod tests { text: "\nEEEE".into(), }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -664,7 +664,7 @@ mod tests { CharOperation::Insert { text: "A".into() }, CharOperation::Keep { bytes: 10 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -689,7 +689,7 @@ mod tests { CharOperation::Keep { bytes: 4 }, ]; let new_text = apply_char_operations(old_text, &char_ops); - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -710,7 +710,7 @@ mod tests { CharOperation::Insert { text: "\n".into() }, CharOperation::Keep { bytes: 9 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -733,7 +733,7 @@ mod tests { CharOperation::Delete { bytes: 1 }, CharOperation::Keep { bytes: 4 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -759,7 +759,7 @@ mod tests { }, CharOperation::Keep { bytes: 4 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -783,7 +783,7 @@ mod tests { CharOperation::Delete { bytes: 2 }, CharOperation::Keep { bytes: 4 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -814,7 +814,7 @@ mod tests { }, CharOperation::Keep { bytes: 6 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ diff --git a/crates/task/src/vscode_debug_format.rs b/crates/task/src/vscode_debug_format.rs index 29907166860d7404b33a7bb1a676d037534f09da..e688760a5e67d00d111c798d90499e78c5a3fe17 100644 --- a/crates/task/src/vscode_debug_format.rs +++ b/crates/task/src/vscode_debug_format.rs @@ -131,7 +131,7 @@ mod tests { } "#; let parsed: VsCodeDebugTaskFile = - serde_json_lenient::from_str(&raw).expect("deserializing launch.json"); + serde_json_lenient::from_str(raw).expect("deserializing launch.json"); let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates"); pretty_assertions::assert_eq!( zed, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 86728cc11cfb6b6da3c20c93b3fc5f167a4f5d5b..2f3b7aa28d3635431391cdc358d9e1911ab3ff4f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -890,15 +890,15 @@ impl Terminal { if self.vi_mode_enabled { match *scroll { AlacScroll::Delta(delta) => { - term.vi_mode_cursor = term.vi_mode_cursor.scroll(&term, delta); + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, delta); } AlacScroll::PageUp => { let lines = term.screen_lines() as i32; - term.vi_mode_cursor = term.vi_mode_cursor.scroll(&term, lines); + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines); } AlacScroll::PageDown => { let lines = -(term.screen_lines() as i32); - term.vi_mode_cursor = term.vi_mode_cursor.scroll(&term, lines); + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines); } AlacScroll::Top => { let point = AlacPoint::new(term.topmost_line(), Column(0)); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 568dc1db2e840071fa5b18724aaab94da21ddc08..cdf405b642d3a85d46d40138a83c6df681724b59 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -346,7 +346,7 @@ impl TerminalPanel { pane::Event::RemovedItem { .. } => self.serialize(cx), pane::Event::Remove { focus_on_pane } => { let pane_count_before_removal = self.center.panes().len(); - let _removal_result = self.center.remove(&pane); + let _removal_result = self.center.remove(pane); if pane_count_before_removal == 1 { self.center.first_pane().update(cx, |pane, cx| { pane.set_zoomed(false, cx); @@ -1181,10 +1181,10 @@ impl Render for TerminalPanel { registrar.size_full().child(self.center.render( workspace.zoomed_item(), &workspace::PaneRenderContext { - follower_states: &&HashMap::default(), + follower_states: &HashMap::default(), active_call: workspace.active_call(), active_pane: &self.active_pane, - app_state: &workspace.app_state(), + app_state: workspace.app_state(), project: workspace.project(), workspace: &workspace.weak_handle(), }, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 534c0a805161c946177589ca0fc30f944880fa6f..559faea42a4503fa81f60f7cffba3de9dbe6972e 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1604,15 +1604,15 @@ impl Item for TerminalView { TaskStatus::Running => ( IconName::PlayFilled, Color::Disabled, - TerminalView::rerun_button(&terminal_task), + TerminalView::rerun_button(terminal_task), ), TaskStatus::Unknown => ( IconName::Warning, Color::Warning, - TerminalView::rerun_button(&terminal_task), + TerminalView::rerun_button(terminal_task), ), TaskStatus::Completed { success } => { - let rerun_button = TerminalView::rerun_button(&terminal_task); + let rerun_button = TerminalView::rerun_button(terminal_task); if *success { (IconName::Check, Color::Success, rerun_button) diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index eb317a5616f81592a146cc406356108690e58ba4..84622888f1ba7b9d7ddd2760f394c6f0a747a19e 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -478,7 +478,7 @@ impl TitleBar { repo.branch .as_ref() .map(|branch| branch.name()) - .map(|name| util::truncate_and_trailoff(&name, MAX_BRANCH_NAME_LENGTH)) + .map(|name| util::truncate_and_trailoff(name, MAX_BRANCH_NAME_LENGTH)) .or_else(|| { repo.head_commit.as_ref().map(|commit| { commit @@ -617,7 +617,7 @@ impl TitleBar { window .spawn(cx, async move |cx| { client - .sign_in_with_optional_connect(true, &cx) + .sign_in_with_optional_connect(true, cx) .await .notify_async_err(cx); }) diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs index e3dc1f35fa8a8509d8a03eab3b6fea37f7df42e7..5e6f4ee8ba18569a43365830eeb7ed12cbb501cc 100644 --- a/crates/ui/src/components/indent_guides.rs +++ b/crates/ui/src/components/indent_guides.rs @@ -216,7 +216,7 @@ mod uniform_list { }; let visible_entries = &compute_indents_fn(visible_range.clone(), window, cx); let indent_guides = compute_indent_guides( - &visible_entries, + visible_entries, visible_range.start, includes_trailing_indent, ); @@ -241,7 +241,7 @@ mod sticky_items { window: &mut Window, cx: &mut App, ) -> AnyElement { - let indent_guides = compute_indent_guides(&indents, 0, false); + let indent_guides = compute_indent_guides(indents, 0, false); self.render_from_layout(indent_guides, bounds, item_height, window, cx) } } diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 56be867796aaa7e8628e478414947cedb9943baa..bbce6101f469940259ab7046ad1c50a2a23217ac 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -163,7 +163,7 @@ pub fn render_keystroke( let size = size.into(); if use_text { - let element = Key::new(keystroke_text(&keystroke, platform_style, vim_mode), color) + let element = Key::new(keystroke_text(keystroke, platform_style, vim_mode), color) .size(size) .into_any_element(); vec![element] @@ -176,7 +176,7 @@ pub fn render_keystroke( size, true, )); - elements.push(render_key(&keystroke, color, platform_style, size)); + elements.push(render_key(keystroke, color, platform_style, size)); elements } } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index ce5e5a0300387743bc9dc84ea6e895941f6885e2..fe1537684c897ef4a03a073edaf700fa23f0c19d 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -95,7 +95,7 @@ impl VimOption { } } - Self::possibilities(&prefix) + Self::possibilities(prefix) .map(|possible| { let mut options = prefix_of_options.clone(); options.push(possible); diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index a6a07e7b2f80ddde1bd9573d102c7f8480d21fff..367b5130b64b1c37f856079b4b57349f674587ea 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -2280,8 +2280,8 @@ fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) - } let mut last_position = None; for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() { - let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer) - ..language::ToOffset::to_offset(&range.context.end, &buffer); + let excerpt_range = language::ToOffset::to_offset(&range.context.start, buffer) + ..language::ToOffset::to_offset(&range.context.end, buffer); if offset >= excerpt_range.start && offset <= excerpt_range.end { let text_anchor = buffer.anchor_after(offset); let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor); @@ -2882,7 +2882,7 @@ fn method_motion( } else { possibilities.min().unwrap_or(offset) }; - let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left); + let new_point = map.clip_point(dest.to_display_point(map), Bias::Left); if new_point == display_point { break; } @@ -2936,7 +2936,7 @@ fn comment_motion( } else { possibilities.min().unwrap_or(offset) }; - let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left); + let new_point = map.clip_point(dest.to_display_point(map), Bias::Left); if new_point == display_point { break; } @@ -3003,7 +3003,7 @@ fn section_motion( possibilities.min().unwrap_or(map.buffer_snapshot.len()) }; - let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left); + let new_point = map.clip_point(offset.to_display_point(map), Bias::Left); if new_point == display_point { break; } diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 007514e47297b1a51ababd057e4a0f1e02748c72..115aef1dabd16826e18cf5a39182f0eec3e670e0 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -155,7 +155,7 @@ fn increment_decimal_string(num: &str, delta: i64) -> String { } fn increment_hex_string(num: &str, delta: i64) -> String { - let result = if let Ok(val) = u64::from_str_radix(&num, 16) { + let result = if let Ok(val) = u64::from_str_radix(num, 16) { val.wrapping_add_signed(delta) } else { u64::MAX @@ -181,7 +181,7 @@ fn should_use_lowercase(num: &str) -> bool { } fn increment_binary_string(num: &str, delta: i64) -> String { - let result = if let Ok(val) = u64::from_str_radix(&num, 2) { + let result = if let Ok(val) = u64::from_str_radix(num, 2) { val.wrapping_add_signed(delta) } else { u64::MAX diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index af13bc0fd0fd354d4fb47505441d3de58d9b4b7f..9eb8367f57ade6a7ccf090f0e16d87e73f4a9f25 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -549,7 +549,7 @@ mod test { cx.set_neovim_option("nowrap").await; let content = "ˇ01234567890123456789"; - cx.set_shared_state(&content).await; + cx.set_shared_state(content).await; cx.simulate_shared_keystrokes("z shift-l").await; cx.shared_state().await.assert_eq("012345ˇ67890123456789"); @@ -560,7 +560,7 @@ mod test { cx.shared_state().await.assert_eq("012345ˇ67890123456789"); let content = "ˇ01234567890123456789"; - cx.set_shared_state(&content).await; + cx.set_shared_state(content).await; cx.simulate_shared_keystrokes("z l").await; cx.shared_state().await.assert_eq("0ˇ1234567890123456789"); diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 423859dadca94f86b673ba95327b22d6879ec710..2e8e2f76bd3de363a4554004786bd6fb535b4631 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -540,7 +540,7 @@ impl MarksState { cx: &mut Context<Self>, ) { let buffer = multibuffer.read(cx).as_singleton(); - let abs_path = buffer.as_ref().and_then(|b| self.path_for_buffer(&b, cx)); + let abs_path = buffer.as_ref().and_then(|b| self.path_for_buffer(b, cx)); let Some(abs_path) = abs_path else { self.multibuffer_marks @@ -606,7 +606,7 @@ impl MarksState { match target? { MarkLocation::Buffer(entity_id) => { - let anchors = self.multibuffer_marks.get(&entity_id)?; + let anchors = self.multibuffer_marks.get(entity_id)?; return Some(Mark::Buffer(*entity_id, anchors.get(name)?.clone())); } MarkLocation::Path(path) => { @@ -636,7 +636,7 @@ impl MarksState { match target { MarkLocation::Buffer(entity_id) => { self.multibuffer_marks - .get_mut(&entity_id) + .get_mut(entity_id) .map(|m| m.remove(&mark_name.clone())); return; } @@ -1042,7 +1042,7 @@ impl Operator { } => format!("^K{}", make_visible(&first_char.to_string())), Operator::Literal { prefix: Some(prefix), - } => format!("^V{}", make_visible(&prefix)), + } => format!("^V{}", make_visible(prefix)), Operator::AutoIndent => "=".to_string(), Operator::ShellCommand => "=".to_string(), _ => self.id().to_string(), diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 46ea261cd65b6a43005a44b58c870ab6cedd5f5c..45cef3a2b9f134f6fe4fe82299da4bbe55ead92d 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -67,7 +67,7 @@ impl NeovimConnection { // Ensure we don't create neovim connections in parallel let _lock = NEOVIM_LOCK.lock(); let (nvim, join_handle, child) = new_child_cmd( - &mut Command::new("nvim") + Command::new("nvim") .arg("--embed") .arg("--clean") // disable swap (otherwise after about 1000 test runs you run out of swap file names) @@ -161,7 +161,7 @@ impl NeovimConnection { #[cfg(feature = "neovim")] pub async fn set_state(&mut self, marked_text: &str) { - let (text, selections) = parse_state(&marked_text); + let (text, selections) = parse_state(marked_text); let nvim_buffer = self .nvim diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 44d9b8f4565aff7a83ee998f05b1f09e37b5c8ac..15b0b443b5556c9dcf9d24c096fb4f2654352166 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -265,7 +265,7 @@ pub fn init(cx: &mut App) { workspace.register_action(|workspace, _: &MaximizePane, window, cx| { let pane = workspace.active_pane(); - let Some(size) = workspace.bounding_box_for_pane(&pane) else { + let Some(size) = workspace.bounding_box_for_pane(pane) else { return; }; @@ -1599,7 +1599,7 @@ impl Vim { second_char, smartcase: VimSettings::get_global(cx).use_smartcase_find, }; - Vim::globals(cx).last_find = Some((&sneak).clone()); + Vim::globals(cx).last_find = Some(sneak.clone()); self.motion(sneak, window, cx) } } else { @@ -1616,7 +1616,7 @@ impl Vim { second_char, smartcase: VimSettings::get_global(cx).use_smartcase_find, }; - Vim::globals(cx).last_find = Some((&sneak).clone()); + Vim::globals(cx).last_find = Some(sneak.clone()); self.motion(sneak, window, cx) } } else { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 3b789b1f3e0962aded2500cf21466ec47ea6e0c9..ffbae3ff76dbf31d504f5a6df9063ced0d3a8e6e 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -414,7 +414,7 @@ impl Vim { ); } - let original_point = selection.tail().to_point(&map); + let original_point = selection.tail().to_point(map); if let Some(range) = object.range(map, mut_selection, around, count) { if !range.is_empty() { diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 1356322a5c4eea864a5fb9c3eca4f9823d56e802..8af39be3e74b3a01d198e0929cf9a5034af095b2 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -1038,7 +1038,7 @@ where { fn detach_and_notify_err(self, window: &mut Window, cx: &mut App) { window - .spawn(cx, async move |mut cx| self.await.notify_async_err(&mut cx)) + .spawn(cx, async move |cx| self.await.notify_async_err(cx)) .detach(); } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 860a57c21ff3d952d65abd30e1fc7fcc4fef9361..0a40dbc12c2e94856c74e66b18b28759c5f2da8b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1627,8 +1627,7 @@ impl Pane { items_to_close .iter() .filter(|item| { - item.is_dirty(cx) - && !Self::skip_save_on_close(item.as_ref(), &workspace, cx) + item.is_dirty(cx) && !Self::skip_save_on_close(item.as_ref(), workspace, cx) }) .map(|item| item.boxed_clone()) .collect::<Vec<_>>() @@ -1657,7 +1656,7 @@ impl Pane { let mut should_save = true; if save_intent == SaveIntent::Close { workspace.update(cx, |workspace, cx| { - if Self::skip_save_on_close(item_to_close.as_ref(), &workspace, cx) { + if Self::skip_save_on_close(item_to_close.as_ref(), workspace, cx) { should_save = false; } })?; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 02eac1665bdac207f2f0d1de00603c276a441d7c..8ec61b6f10ac48a14c7cfe106306584bf2aedab0 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -647,7 +647,7 @@ impl ProjectItemRegistry { .build_project_item_for_path_fns .iter() .rev() - .find_map(|open_project_item| open_project_item(&project, &path, window, cx)) + .find_map(|open_project_item| open_project_item(project, path, window, cx)) else { return Task::ready(Err(anyhow!("cannot open file {:?}", path.path))); }; @@ -2431,7 +2431,7 @@ impl Workspace { ); window.prompt( PromptLevel::Warning, - &"Do you want to save all changes in the following files?", + "Do you want to save all changes in the following files?", Some(&detail), &["Save all", "Discard all", "Cancel"], cx, @@ -2767,9 +2767,9 @@ impl Workspace { let item = pane.read(cx).active_item(); let pane = pane.downgrade(); - window.spawn(cx, async move |mut cx| { + window.spawn(cx, async move |cx| { if let Some(item) = item { - Pane::save_item(project, &pane, item.as_ref(), save_intent, &mut cx) + Pane::save_item(project, &pane, item.as_ref(), save_intent, cx) .await .map(|_| ()) } else { @@ -3889,14 +3889,14 @@ impl Workspace { pane.track_alternate_file_items(); }); if *local { - self.unfollow_in_pane(&pane, window, cx); + self.unfollow_in_pane(pane, window, cx); } serialize_workspace = *focus_changed || pane != self.active_pane(); if pane == self.active_pane() { self.active_item_path_changed(window, cx); self.update_active_view_for_followers(window, cx); } else if *local { - self.set_active_pane(&pane, window, cx); + self.set_active_pane(pane, window, cx); } } pane::Event::UserSavedItem { item, save_intent } => { @@ -7182,9 +7182,9 @@ pub fn open_paths( .collect::<Vec<_>>(); cx.update(|cx| { - for window in local_workspace_windows(&cx) { - if let Ok(workspace) = window.read(&cx) { - let m = workspace.project.read(&cx).visibility_for_paths( + for window in local_workspace_windows(cx) { + if let Ok(workspace) = window.read(cx) { + let m = workspace.project.read(cx).visibility_for_paths( &abs_paths, &all_metadatas, open_options.open_new_workspace == None, @@ -7341,7 +7341,7 @@ pub fn open_ssh_project_with_new_connection( ) -> Task<Result<()>> { cx.spawn(async move |cx| { let (serialized_ssh_project, workspace_id, serialized_workspace) = - serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?; + serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; let session = match cx .update(|cx| { @@ -7395,7 +7395,7 @@ pub fn open_ssh_project_with_existing_connection( ) -> Task<Result<()>> { cx.spawn(async move |cx| { let (serialized_ssh_project, workspace_id, serialized_workspace) = - serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?; + serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; open_ssh_project_inner( project, diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index b5a0f71e81171be061c834e15d2f50e04688bbb5..f110726afddc6a3445ef5e5c6973fd336b23119c 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3199,7 +3199,7 @@ impl BackgroundScannerState { } async fn is_git_dir(path: &Path, fs: &dyn Fs) -> bool { - if path.file_name() == Some(&*DOT_GIT) { + if path.file_name() == Some(*DOT_GIT) { return true; } @@ -3575,7 +3575,7 @@ impl<'a> cursor_location: &Dimensions<TraversalProgress<'a>, GitSummary>, _: &(), ) -> Ordering { - self.cmp_path(&cursor_location.0.max_path) + self.cmp_path(cursor_location.0.max_path) } } @@ -5364,13 +5364,13 @@ impl PathTarget<'_> { impl<'a, S: Summary> SeekTarget<'a, PathSummary<S>, PathProgress<'a>> for PathTarget<'_> { fn cmp(&self, cursor_location: &PathProgress<'a>, _: &S::Context) -> Ordering { - self.cmp_path(&cursor_location.max_path) + self.cmp_path(cursor_location.max_path) } } impl<'a, S: Summary> SeekTarget<'a, PathSummary<S>, TraversalProgress<'a>> for PathTarget<'_> { fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &S::Context) -> Ordering { - self.cmp_path(&cursor_location.max_path) + self.cmp_path(cursor_location.max_path) } } @@ -5396,7 +5396,7 @@ impl<'a> TraversalTarget<'a> { fn cmp_progress(&self, progress: &TraversalProgress) -> Ordering { match self { - TraversalTarget::Path(path) => path.cmp_path(&progress.max_path), + TraversalTarget::Path(path) => path.cmp_path(progress.max_path), TraversalTarget::Count { count, include_files, @@ -5551,7 +5551,7 @@ fn discover_git_paths(dot_git_abs_path: &Arc<Path>, fs: &dyn Fs) -> (Arc<Path>, let mut repository_dir_abs_path = dot_git_abs_path.clone(); let mut common_dir_abs_path = dot_git_abs_path.clone(); - if let Some(path) = smol::block_on(fs.load(&dot_git_abs_path)) + if let Some(path) = smol::block_on(fs.load(dot_git_abs_path)) .ok() .as_ref() .and_then(|contents| parse_gitfile(contents).log_err()) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 2a82f81b5b314b83bbe552747d385049dc61585d..a66b30c44a6a87a4294396dd6ba9342bd90d0622 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -371,9 +371,9 @@ pub fn main() { { cx.spawn({ let app_state = app_state.clone(); - async move |mut cx| { - if let Err(e) = restore_or_create_workspace(app_state, &mut cx).await { - fail_to_open_window_async(e, &mut cx) + async move |cx| { + if let Err(e) = restore_or_create_workspace(app_state, cx).await { + fail_to_open_window_async(e, cx) } } }) @@ -690,7 +690,7 @@ pub fn main() { cx.spawn({ let client = app_state.client.clone(); - async move |cx| authenticate(client, &cx).await + async move |cx| authenticate(client, cx).await }) .detach_and_log_err(cx); @@ -722,9 +722,9 @@ pub fn main() { None => { cx.spawn({ let app_state = app_state.clone(); - async move |mut cx| { - if let Err(e) = restore_or_create_workspace(app_state, &mut cx).await { - fail_to_open_window_async(e, &mut cx) + async move |cx| { + if let Err(e) = restore_or_create_workspace(app_state, cx).await { + fail_to_open_window_async(e, cx) } } }) @@ -795,14 +795,14 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut } if let Some(connection_options) = request.ssh_connection { - cx.spawn(async move |mut cx| { + cx.spawn(async move |cx| { let paths: Vec<PathBuf> = request.open_paths.into_iter().map(PathBuf::from).collect(); open_ssh_project( connection_options, paths, app_state, workspace::OpenOptions::default(), - &mut cx, + cx, ) .await }) @@ -813,7 +813,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut let mut task = None; if !request.open_paths.is_empty() || !request.diff_paths.is_empty() { let app_state = app_state.clone(); - task = Some(cx.spawn(async move |mut cx| { + task = Some(cx.spawn(async move |cx| { let paths_with_position = derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await; let (_window, results) = open_paths_with_positions( @@ -821,7 +821,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut &request.diff_paths, app_state, workspace::OpenOptions::default(), - &mut cx, + cx, ) .await?; for result in results.into_iter().flatten() { @@ -834,7 +834,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut } if !request.open_channel_notes.is_empty() || request.join_channel.is_some() { - cx.spawn(async move |mut cx| { + cx.spawn(async move |cx| { let result = maybe!(async { if let Some(task) = task { task.await?; @@ -842,7 +842,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut let client = app_state.client.clone(); // we continue even if authentication fails as join_channel/ open channel notes will // show a visible error message. - authenticate(client, &cx).await.log_err(); + authenticate(client, cx).await.log_err(); if let Some(channel_id) = request.join_channel { cx.update(|cx| { @@ -878,14 +878,14 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut }) .await; if let Err(err) = result { - fail_to_open_window_async(err, &mut cx); + fail_to_open_window_async(err, cx); } }) .detach() } else if let Some(task) = task { - cx.spawn(async move |mut cx| { + cx.spawn(async move |cx| { if let Err(err) = task.await { - fail_to_open_window_async(err, &mut cx); + fail_to_open_window_async(err, cx); } }) .detach(); diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index 0a54572f6bf96b5bb0e979b0178bb5b7846bb3d7..f2e65b4f53907858392920a8d54ceb0a08b29844 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -536,7 +536,7 @@ async fn upload_previous_panics( }); if let Some(panic) = panic - && upload_panic(&http, &panic_report_url, panic, &mut most_recent_panic).await? + && upload_panic(&http, panic_report_url, panic, &mut most_recent_panic).await? { // We've done what we can, delete the file fs::remove_file(child_path) @@ -566,7 +566,7 @@ pub async fn upload_previous_minidumps(http: Arc<HttpClientWithUrl>) -> anyhow:: if let Ok(metadata) = serde_json::from_slice(&smol::fs::read(&json_path).await?) { if upload_minidump( http.clone(), - &minidump_endpoint, + minidump_endpoint, smol::fs::read(&child_path) .await .context("Failed to read minidump")?, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 6d5aecba7035881b6e5e604759de865a01131580..535cb12e1ad4c78913dbce6565605e0b340999fa 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -327,7 +327,7 @@ pub fn initialize_workspace( cx.subscribe_in(&workspace_handle, window, { move |workspace, _, event, window, cx| match event { workspace::Event::PaneAdded(pane) => { - initialize_pane(workspace, &pane, window, cx); + initialize_pane(workspace, pane, window, cx); } workspace::Event::OpenBundledFile { text, @@ -796,7 +796,7 @@ fn register_actions( .register_action(install_cli) .register_action(|_, _: &install_cli::RegisterZedScheme, window, cx| { cx.spawn_in(window, async move |workspace, cx| { - install_cli::register_zed_scheme(&cx).await?; + install_cli::register_zed_scheme(cx).await?; workspace.update_in(cx, |workspace, _, cx| { struct RegisterZedScheme; diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index 4609ecce9bb01d08d753e70789e8dcc81ee8e245..915c40030a26240cc24d3dd79e7c716010abb469 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -650,7 +650,7 @@ impl ComponentPreview { _window: &mut Window, _cx: &mut Context<Self>, ) -> impl IntoElement { - let component = self.component_map.get(&component_id); + let component = self.component_map.get(component_id); if let Some(component) = component { v_flex() diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index 5b0826413b1a9771f7daaa719bba96f3820245dc..587786fe8f248536796909911022bdf3b97de3eb 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -147,7 +147,7 @@ fn assign_edit_prediction_providers( assign_edit_prediction_provider( editor, provider, - &client, + client, user_store.clone(), window, cx, @@ -248,7 +248,7 @@ fn assign_edit_prediction_provider( if let Some(buffer) = &singleton_buffer { if buffer.read(cx).file().is_some() { zeta.update(cx, |zeta, cx| { - zeta.register_buffer(&buffer, cx); + zeta.register_buffer(buffer, cx); }); } } diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 82d3795e94d0d0e12c4c65ff3ab92a70ba4a0039..f282860e2cdc6ffc343724718f386054418338af 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -432,13 +432,13 @@ async fn open_workspaces( .connection_options_for(ssh.host, ssh.port, ssh.user) }); if let Ok(connection_options) = connection_options { - cx.spawn(async move |mut cx| { + cx.spawn(async move |cx| { open_ssh_project( connection_options, ssh.paths.into_iter().map(PathBuf::from).collect(), app_state, OpenOptions::default(), - &mut cx, + cx, ) .await .log_err(); diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 2b7c38f997db4eb5d5210f5d0887dc4443be549d..d65053c05f66c7520d92a7dccb05e44e1d019941 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -182,7 +182,7 @@ impl Render for QuickActionBar { let code_action_element = if is_deployed { editor.update(cx, |editor, cx| { if let Some(style) = editor.style() { - editor.render_context_menu(&style, MAX_CODE_ACTION_MENU_LINES, window, cx) + editor.render_context_menu(style, MAX_CODE_ACTION_MENU_LINES, window, cx) } else { None } diff --git a/crates/zeta/src/license_detection.rs b/crates/zeta/src/license_detection.rs index fa1eabf524ef5342731a1bc2272805a5dc474eb8..3dd025c1e156c95cb3a8bdf5f5a2e49060033304 100644 --- a/crates/zeta/src/license_detection.rs +++ b/crates/zeta/src/license_detection.rs @@ -198,7 +198,7 @@ mod tests { #[test] fn test_mit_positive_detection() { - assert!(is_license_eligible_for_data_collection(&MIT_LICENSE)); + assert!(is_license_eligible_for_data_collection(MIT_LICENSE)); } #[test] diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 1a6a8c29348d7181a2b0179649e5d214d0c97650..956e416fe98087168bb61932e5d0211443a0ea4e 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -505,7 +505,7 @@ impl Zeta { input_events, input_excerpt, buffer_snapshotted_at, - &cx, + cx, ) .await; @@ -981,7 +981,7 @@ and then another old_text, new_text, editable_range.start, - &snapshot, + snapshot, )) } @@ -991,7 +991,7 @@ and then another offset: usize, snapshot: &BufferSnapshot, ) -> Vec<(Range<Anchor>, String)> { - text_diff(&old_text, &new_text) + text_diff(&old_text, new_text) .into_iter() .map(|(mut old_range, new_text)| { old_range.start += offset; @@ -1182,7 +1182,7 @@ pub fn gather_context( .filter_map(|(language_server_id, diagnostic_group)| { let language_server = local_lsp_store.running_language_server_for_id(language_server_id)?; - let diagnostic_group = diagnostic_group.resolve::<usize>(&snapshot); + let diagnostic_group = diagnostic_group.resolve::<usize>(snapshot); let language_server_name = language_server.name().to_string(); let serialized = serde_json::to_value(diagnostic_group).unwrap(); Some((language_server_name, serialized)) @@ -1313,10 +1313,10 @@ impl CurrentEditPrediction { return true; } - let Some(old_edits) = old_completion.completion.interpolate(&snapshot) else { + let Some(old_edits) = old_completion.completion.interpolate(snapshot) else { return true; }; - let Some(new_edits) = self.completion.interpolate(&snapshot) else { + let Some(new_edits) = self.completion.interpolate(snapshot) else { return false; }; @@ -1664,7 +1664,7 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { if let Some(old_completion) = this.current_completion.as_ref() { let snapshot = buffer.read(cx).snapshot(); - if new_completion.should_replace_completion(&old_completion, &snapshot) { + if new_completion.should_replace_completion(old_completion, &snapshot) { this.zeta.update(cx, |zeta, cx| { zeta.completion_shown(&new_completion.completion, cx); }); diff --git a/crates/zeta_cli/src/main.rs b/crates/zeta_cli/src/main.rs index d78035bc9d8d58328d34d576779cab65dade7143..ba854b87323bbda3629e1d2ef4823db4e3c4b6f3 100644 --- a/crates/zeta_cli/src/main.rs +++ b/crates/zeta_cli/src/main.rs @@ -131,7 +131,7 @@ async fn get_context( let (project, _lsp_open_handle, buffer) = if use_language_server { let (project, lsp_open_handle, buffer) = - open_buffer_with_language_server(&worktree_path, &cursor.path, &app_state, cx).await?; + open_buffer_with_language_server(&worktree_path, &cursor.path, app_state, cx).await?; (Some(project), Some(lsp_open_handle), buffer) } else { let abs_path = worktree_path.join(&cursor.path); @@ -260,7 +260,7 @@ pub fn wait_for_lang_server( .update(cx, |buffer, cx| { lsp_store.update(cx, |lsp_store, cx| { lsp_store - .language_servers_for_local_buffer(&buffer, cx) + .language_servers_for_local_buffer(buffer, cx) .next() .is_some() }) @@ -291,7 +291,7 @@ pub fn wait_for_lang_server( _ => {} } }), - cx.subscribe(&project, { + cx.subscribe(project, { let buffer = buffer.clone(); move |project, event, cx| match event { project::Event::LanguageServerAdded(_, _, _) => { diff --git a/crates/zlog/src/filter.rs b/crates/zlog/src/filter.rs index 56350d34c3c89385180733b73f19224c6c749b53..cf1604bd9f5d39f3602276deaf3aee86fc714154 100644 --- a/crates/zlog/src/filter.rs +++ b/crates/zlog/src/filter.rs @@ -82,7 +82,7 @@ pub fn is_scope_enabled(scope: &Scope, module_path: Option<&str>, level: log::Le // if no scopes are enabled, return false because it's not <= LEVEL_ENABLED_MAX_STATIC return is_enabled_by_default; } - let enabled_status = map.is_enabled(&scope, module_path, level); + let enabled_status = map.is_enabled(scope, module_path, level); return match enabled_status { EnabledStatus::NotConfigured => is_enabled_by_default, EnabledStatus::Enabled => true, From bb640c6a1c8b77031b8aabf1d5100945bf1d00ff Mon Sep 17 00:00:00 2001 From: Gregor <mail@watwa.re> Date: Tue, 19 Aug 2025 00:01:46 +0200 Subject: [PATCH 112/744] Add multi selection support to UnwrapSyntaxNode (#35991) Closes #35932 Closes #35933 I only intended to fix multi select in this, I accidentally drive-by fixed the VIM issue as well. `replace_text_in_range` which I was using before has two, to me unexpected, side-effects: - it no-ops when input is disabled, which is the case in VIM's Insert/Visual modes - it takes the current selection into account, and does not just operate on the given range (which I erroneously assumed before) Now the code is using `buffer.edit` instead, which seems more lower level, and does not have those side-effects. I was enthused to see that it accepts a vec of edits, so I didn't have to calculate offsets for following edits... until I also wanted to set selections, where I do need to do it by hand. I'm still wondering if there is a simpler way to do it, but for now it at least passes my muster Release Notes: - Added multiple selection support to UnwrapSyntaxNode action - Fixed UnwrapSyntaxNode not working in VIM Insert/Visual modes --- crates/editor/src/editor.rs | 85 +++++++++++++++++-------------- crates/editor/src/editor_tests.rs | 17 ++----- 2 files changed, 50 insertions(+), 52 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6edd4e9d8c7579427f5bc14bb68b5a8a0e9fab8a..365cd1ea5a8cd93c886daaf2033eba89f5e45281 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -14834,15 +14834,18 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let buffer = self.buffer.read(cx).snapshot(cx); - let old_selections: Box<[_]> = self.selections.all::<usize>(cx).into(); + let selections = self + .selections + .all::<usize>(cx) + .into_iter() + // subtracting the offset requires sorting + .sorted_by_key(|i| i.start); - let edits = old_selections - .iter() - // only consider the first selection for now - .take(1) - .map(|selection| { + let full_edits = selections + .into_iter() + .filter_map(|selection| { // Only requires two branches once if-let-chains stabilize (#53667) - let selection_range = if !selection.is_empty() { + let child = if !selection.is_empty() { selection.range() } else if let Some((_, ancestor_range)) = buffer.syntax_ancestor(selection.start..selection.end) @@ -14855,48 +14858,52 @@ impl Editor { selection.range() }; - let mut new_range = selection_range.clone(); - while let Some((_, ancestor_range)) = buffer.syntax_ancestor(new_range.clone()) { - new_range = match ancestor_range { + let mut parent = child.clone(); + while let Some((_, ancestor_range)) = buffer.syntax_ancestor(parent.clone()) { + parent = match ancestor_range { MultiOrSingleBufferOffsetRange::Single(range) => range, MultiOrSingleBufferOffsetRange::Multi(range) => range, }; - if new_range.start < selection_range.start - || new_range.end > selection_range.end - { + if parent.start < child.start || parent.end > child.end { break; } } - (selection, selection_range, new_range) + if parent == child { + return None; + } + let text = buffer.text_for_range(child.clone()).collect::<String>(); + Some((selection.id, parent, text)) }) .collect::<Vec<_>>(); - self.transact(window, cx, |editor, window, cx| { - for (_, child, parent) in &edits { - let text = buffer.text_for_range(child.clone()).collect::<String>(); - editor.replace_text_in_range(Some(parent.clone()), &text, window, cx); - } - - editor.change_selections( - SelectionEffects::scroll(Autoscroll::fit()), - window, - cx, - |s| { - s.select( - edits - .iter() - .map(|(s, old, new)| Selection { - id: s.id, - start: new.start, - end: new.start + old.len(), - goal: SelectionGoal::None, - reversed: s.reversed, - }) - .collect(), - ); - }, - ); + self.transact(window, cx, |this, window, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit( + full_edits + .iter() + .map(|(_, p, t)| (p.clone(), t.clone())) + .collect::<Vec<_>>(), + None, + cx, + ); + }); + this.change_selections(Default::default(), window, cx, |s| { + let mut offset = 0; + let mut selections = vec![]; + for (id, parent, text) in full_edits { + let start = parent.start - offset; + offset += parent.len() - text.len(); + selections.push(Selection { + id: id, + start, + end: start + text.len(), + reversed: false, + goal: Default::default(), + }); + } + s.select(selections); + }); }); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 189bdd1bf7d73250b2b9eaa7e1b7cecf39740eb0..685cc47cdbc6f75ae6dbd6ca621b5cebf616912e 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -8015,7 +8015,7 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte } #[gpui::test] -async fn test_unwrap_syntax_node(cx: &mut gpui::TestAppContext) { +async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; @@ -8029,21 +8029,12 @@ async fn test_unwrap_syntax_node(cx: &mut gpui::TestAppContext) { buffer.set_language(Some(language), cx); }); - cx.set_state( - &r#" - use mod1::mod2::{«mod3ˇ», mod4}; - "# - .unindent(), - ); + cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# }); cx.update_editor(|editor, window, cx| { editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx); }); - cx.assert_editor_state( - &r#" - use mod1::mod2::«mod3ˇ»; - "# - .unindent(), - ); + + cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# }); } #[gpui::test] From e7b7c206a0233c19c982cf0ef95c87d98a2fd8a9 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Tue, 19 Aug 2025 01:03:16 +0300 Subject: [PATCH 113/744] terminal: Fix python venv path when spawning tasks on windows (#35909) I haven't found any issues related to this, but it seems like currently the wrong directory is added to the path when spawning tasks on windows with a python virtual environment. I also deduplicated the logic at a few places. The same constant exists in the languages crate, but we don't want to pull an additional dependency just for this. -1 papercut Release Notes: - Fix python venv path when spawning tasks on windows --- crates/project/src/terminals.rs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 5ea7b87fbe0e83dc35c42ef27441ef24ce8d6eae..f5d08990b5723b43749ec01de4100432f803191f 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -23,6 +23,13 @@ use util::{ paths::{PathStyle, RemotePathBuf}, }; +/// The directory inside a Python virtual environment that contains executables +const PYTHON_VENV_BIN_DIR: &str = if cfg!(target_os = "windows") { + "Scripts" +} else { + "bin" +}; + pub struct Terminals { pub(crate) local_handles: Vec<WeakEntity<terminal::Terminal>>, } @@ -368,7 +375,8 @@ impl Project { } None => { if let Some(venv_path) = &python_venv_directory { - add_environment_path(&mut env, &venv_path.join("bin")).log_err(); + add_environment_path(&mut env, &venv_path.join(PYTHON_VENV_BIN_DIR)) + .log_err(); } let shell = if let Some(program) = spawn_task.command { @@ -478,16 +486,12 @@ impl Project { venv_settings: &terminal_settings::VenvSettingsContent, cx: &App, ) -> Option<PathBuf> { - let bin_dir_name = match std::env::consts::OS { - "windows" => "Scripts", - _ => "bin", - }; venv_settings .directories .iter() .map(|name| abs_path.join(name)) .find(|venv_path| { - let bin_path = venv_path.join(bin_dir_name); + let bin_path = venv_path.join(PYTHON_VENV_BIN_DIR); self.find_worktree(&bin_path, cx) .and_then(|(worktree, relative_path)| { worktree.read(cx).entry_for_path(&relative_path) @@ -504,16 +508,12 @@ impl Project { ) -> Option<PathBuf> { let (worktree, _) = self.find_worktree(abs_path, cx)?; let fs = worktree.read(cx).as_local()?.fs(); - let bin_dir_name = match std::env::consts::OS { - "windows" => "Scripts", - _ => "bin", - }; venv_settings .directories .iter() .map(|name| abs_path.join(name)) .find(|venv_path| { - let bin_path = venv_path.join(bin_dir_name); + let bin_path = venv_path.join(PYTHON_VENV_BIN_DIR); // One-time synchronous check is acceptable for terminal/task initialization smol::block_on(fs.metadata(&bin_path)) .ok() @@ -589,10 +589,7 @@ impl Project { if venv_settings.venv_name.is_empty() { let path = venv_base_directory - .join(match std::env::consts::OS { - "windows" => "Scripts", - _ => "bin", - }) + .join(PYTHON_VENV_BIN_DIR) .join(activate_script_name) .to_string_lossy() .to_string(); From 3648dbe939172bba070be8b29e64cb5b7d749a0c Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Mon, 18 Aug 2025 18:09:30 -0400 Subject: [PATCH 114/744] terminal: Temporarily disable `test_basic_terminal` test (#36447) This PR temporarily disables the `test_basic_terminal` test, as it flakes on macOS. Release Notes: - N/A --- crates/terminal/src/terminal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 2f3b7aa28d3635431391cdc358d9e1911ab3ff4f..3dfde8a9afaaff788c4e7b9d0a59fcd9edaa04cc 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -2160,7 +2160,7 @@ mod tests { use gpui::{Pixels, Point, TestAppContext, bounds, point, size}; use rand::{Rng, distributions::Alphanumeric, rngs::ThreadRng, thread_rng}; - #[cfg_attr(windows, ignore = "TODO: fix on windows")] + #[ignore = "Test is flaky on macOS, and doesn't run on Windows"] #[gpui::test] async fn test_basic_terminal(cx: &mut TestAppContext) { cx.executor().allow_parking(); From 567ceffd429d8711a0ef6674bd01f22dfc0e98ff Mon Sep 17 00:00:00 2001 From: Kirill Bulatov <kirill@zed.dev> Date: Tue, 19 Aug 2025 01:54:37 +0300 Subject: [PATCH 115/744] Remove an unused struct (#36448) Release Notes: - N/A --- crates/workspace/src/workspace.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8ec61b6f10ac48a14c7cfe106306584bf2aedab0..babf2ac1d56b1507133c1eb1cc81f16e6cd4d394 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -256,11 +256,6 @@ actions!( ] ); -#[derive(Clone, PartialEq)] -pub struct OpenPaths { - pub paths: Vec<PathBuf>, -} - /// Activates a specific pane by its index. #[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] #[action(namespace = workspace)] @@ -6823,14 +6818,6 @@ impl WorkspaceHandle for Entity<Workspace> { } } -impl std::fmt::Debug for OpenPaths { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("OpenPaths") - .field("paths", &self.paths) - .finish() - } -} - pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> { DB.last_workspace().await.log_err().flatten() } From 33fbe53d48291c6c622e8d3b4bbf4d0210d41025 Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Mon, 18 Aug 2025 19:16:28 -0400 Subject: [PATCH 116/744] client: Make `Client::sign_in_with_optional_connect` a no-op when already connected to Collab (#36449) This PR makes it so `Client::sign_in_with_optional_connect` does nothing when the user is already connected to Collab. This fixes the issue where clicking on a channel link would temporarily disconnect you from Collab. Release Notes: - N/A --- crates/client/src/client.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 91bdf001d8a112590d79a2115dba6adef6dd7053..66d5fd89b151431272f5d2ce138b896acfc414ea 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -973,6 +973,11 @@ impl Client { try_provider: bool, cx: &AsyncApp, ) -> Result<()> { + // Don't try to sign in again if we're already connected to Collab, as it will temporarily disconnect us. + if self.status().borrow().is_connected() { + return Ok(()); + } + let (is_staff_tx, is_staff_rx) = oneshot::channel::<bool>(); let mut is_staff_tx = Some(is_staff_tx); cx.update(|cx| { From b578031120b7ab294e86877656d54bd95157683c Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Mon, 18 Aug 2025 20:27:08 -0300 Subject: [PATCH 117/744] claude: Respect always allow setting (#36450) Claude will now respect the `agent.always_allow_tool_actions` setting and will set it when "Always Allow" is clicked. Release Notes: - N/A --- Cargo.lock | 1 + crates/agent_servers/Cargo.toml | 1 + crates/agent_servers/src/claude.rs | 3 +- crates/agent_servers/src/claude/mcp_server.rs | 70 ++++++++++++++++--- 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3bc2b638434a0b8476478809f4f269b4e39b2cbd..6c05839ef3ed9f52f0eb88b472b41c2b3fbab44a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,6 +258,7 @@ version = "0.1.0" dependencies = [ "acp_thread", "agent-client-protocol", + "agent_settings", "agentic-coding-protocol", "anyhow", "collections", diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index f894bb15bf7cb66af8aad356ead6443c32763b26..886f650470185ff6e5d4b70e5873037329f08b21 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -19,6 +19,7 @@ doctest = false [dependencies] acp_thread.workspace = true agent-client-protocol.workspace = true +agent_settings.workspace = true agentic-coding-protocol.workspace = true anyhow.workspace = true collections.workspace = true diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 354bda494d70b4994b8f0bdd98309abb2e665a9b..9b273cb091d28334515430e9b035b873d881875c 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -111,7 +111,8 @@ impl AgentConnection for ClaudeAgentConnection { })?; let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid()); - let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), cx).await?; + let fs = project.read_with(cx, |project, _cx| project.fs().clone())?; + let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), fs, cx).await?; let mut mcp_servers = HashMap::default(); mcp_servers.insert( diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index 22cb2f8f8d7c15608527eb7492220bf9c7fe920c..38587574db63a91ea468da9f1154725290f79f1e 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -1,8 +1,10 @@ use std::path::PathBuf; +use std::sync::Arc; use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams}; use acp_thread::AcpThread; use agent_client_protocol as acp; +use agent_settings::AgentSettings; use anyhow::{Context, Result}; use collections::HashMap; use context_server::listener::{McpServerTool, ToolResponse}; @@ -11,8 +13,11 @@ use context_server::types::{ ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests, }; use gpui::{App, AsyncApp, Task, WeakEntity}; +use project::Fs; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use settings::{Settings as _, update_settings_file}; +use util::debug_panic; pub struct ClaudeZedMcpServer { server: context_server::listener::McpServer, @@ -23,6 +28,7 @@ pub const SERVER_NAME: &str = "zed"; impl ClaudeZedMcpServer { pub async fn new( thread_rx: watch::Receiver<WeakEntity<AcpThread>>, + fs: Arc<dyn Fs>, cx: &AsyncApp, ) -> Result<Self> { let mut mcp_server = context_server::listener::McpServer::new(cx).await?; @@ -30,6 +36,7 @@ impl ClaudeZedMcpServer { mcp_server.add_tool(PermissionTool { thread_rx: thread_rx.clone(), + fs: fs.clone(), }); mcp_server.add_tool(ReadTool { thread_rx: thread_rx.clone(), @@ -102,6 +109,7 @@ pub struct McpServerConfig { #[derive(Clone)] pub struct PermissionTool { + fs: Arc<dyn Fs>, thread_rx: watch::Receiver<WeakEntity<AcpThread>>, } @@ -141,6 +149,24 @@ impl McpServerTool for PermissionTool { input: Self::Input, cx: &mut AsyncApp, ) -> Result<ToolResponse<Self::Output>> { + if agent_settings::AgentSettings::try_read_global(cx, |settings| { + settings.always_allow_tool_actions + }) + .unwrap_or(false) + { + let response = PermissionToolResponse { + behavior: PermissionToolBehavior::Allow, + updated_input: input.input, + }; + + return Ok(ToolResponse { + content: vec![ToolResponseContent::Text { + text: serde_json::to_string(&response)?, + }], + structured_content: (), + }); + } + let mut thread_rx = self.thread_rx.clone(); let Some(thread) = thread_rx.recv().await?.upgrade() else { anyhow::bail!("Thread closed"); @@ -148,8 +174,10 @@ impl McpServerTool for PermissionTool { let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone()); let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into()); - let allow_option_id = acp::PermissionOptionId("allow".into()); - let reject_option_id = acp::PermissionOptionId("reject".into()); + + const ALWAYS_ALLOW: &'static str = "always_allow"; + const ALLOW: &'static str = "allow"; + const REJECT: &'static str = "reject"; let chosen_option = thread .update(cx, |thread, cx| { @@ -157,12 +185,17 @@ impl McpServerTool for PermissionTool { claude_tool.as_acp(tool_call_id).into(), vec![ acp::PermissionOption { - id: allow_option_id.clone(), + id: acp::PermissionOptionId(ALWAYS_ALLOW.into()), + name: "Always Allow".into(), + kind: acp::PermissionOptionKind::AllowAlways, + }, + acp::PermissionOption { + id: acp::PermissionOptionId(ALLOW.into()), name: "Allow".into(), kind: acp::PermissionOptionKind::AllowOnce, }, acp::PermissionOption { - id: reject_option_id.clone(), + id: acp::PermissionOptionId(REJECT.into()), name: "Reject".into(), kind: acp::PermissionOptionKind::RejectOnce, }, @@ -172,16 +205,33 @@ impl McpServerTool for PermissionTool { })?? .await?; - let response = if chosen_option == allow_option_id { - PermissionToolResponse { + let response = match chosen_option.0.as_ref() { + ALWAYS_ALLOW => { + cx.update(|cx| { + update_settings_file::<AgentSettings>(self.fs.clone(), cx, |settings, _| { + settings.set_always_allow_tool_actions(true); + }); + })?; + + PermissionToolResponse { + behavior: PermissionToolBehavior::Allow, + updated_input: input.input, + } + } + ALLOW => PermissionToolResponse { behavior: PermissionToolBehavior::Allow, updated_input: input.input, - } - } else { - debug_assert_eq!(chosen_option, reject_option_id); - PermissionToolResponse { + }, + REJECT => PermissionToolResponse { behavior: PermissionToolBehavior::Deny, updated_input: input.input, + }, + opt => { + debug_panic!("Unexpected option: {}", opt); + PermissionToolResponse { + behavior: PermissionToolBehavior::Deny, + updated_input: input.input, + } } }; From b7edc89a87e2589fbe69c13a53aba57260371a5f Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:44:07 -0300 Subject: [PATCH 118/744] agent: Improve error and warnings display (#36425) This PR refactors the callout component and improves how we display errors and warnings in the agent panel, along with improvements for specific cases (e.g., you have `zed.dev` as your LLM provider and is signed out). Still a work in progress, though, wrapping up some details. Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 145 ++++---- crates/agent_ui/src/active_thread.rs | 2 +- .../add_llm_provider_modal.rs | 2 +- crates/agent_ui/src/agent_panel.rs | 345 +++++++++--------- crates/agent_ui/src/message_editor.rs | 50 +-- .../agent_ui/src/ui/preview/usage_callouts.rs | 14 +- .../ai_onboarding/src/young_account_banner.rs | 2 +- crates/language_model/src/registry.rs | 2 +- crates/settings_ui/src/keybindings.rs | 14 +- crates/ui/src/components/banner.rs | 9 - crates/ui/src/components/callout.rs | 217 ++++++----- crates/ui/src/prelude.rs | 4 +- crates/ui/src/styles.rs | 2 + crates/ui/src/styles/severity.rs | 10 + 14 files changed, 430 insertions(+), 388 deletions(-) create mode 100644 crates/ui/src/styles/severity.rs diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 4a8f9bf209be2c16f83cb745d2265d950becb96a..0d15e27e0c4808cbdabd1b7a5f13e881ee8ed350 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -3259,44 +3259,33 @@ impl AcpThreadView { } }; - Some( - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .child(content), - ) + Some(div().child(content)) } fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout { - let icon = Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error); - Callout::new() - .icon(icon) + .severity(Severity::Error) .title("Error") .description(error.clone()) - .secondary_action(self.create_copy_button(error.to_string())) - .primary_action(self.dismiss_error_button(cx)) - .bg_color(self.error_callout_bg(cx)) + .actions_slot(self.create_copy_button(error.to_string())) + .dismiss_action(self.dismiss_error_button(cx)) } fn render_payment_required_error(&self, cx: &mut Context<Self>) -> Callout { const ERROR_MESSAGE: &str = "You reached your free usage limit. Upgrade to Zed Pro for more prompts."; - let icon = Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error); - Callout::new() - .icon(icon) + .severity(Severity::Error) .title("Free Usage Exceeded") .description(ERROR_MESSAGE) - .tertiary_action(self.upgrade_button(cx)) - .secondary_action(self.create_copy_button(ERROR_MESSAGE)) - .primary_action(self.dismiss_error_button(cx)) - .bg_color(self.error_callout_bg(cx)) + .actions_slot( + h_flex() + .gap_0p5() + .child(self.upgrade_button(cx)) + .child(self.create_copy_button(ERROR_MESSAGE)), + ) + .dismiss_action(self.dismiss_error_button(cx)) } fn render_model_request_limit_reached_error( @@ -3311,18 +3300,17 @@ impl AcpThreadView { } }; - let icon = Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error); - Callout::new() - .icon(icon) + .severity(Severity::Error) .title("Model Prompt Limit Reached") .description(error_message) - .tertiary_action(self.upgrade_button(cx)) - .secondary_action(self.create_copy_button(error_message)) - .primary_action(self.dismiss_error_button(cx)) - .bg_color(self.error_callout_bg(cx)) + .actions_slot( + h_flex() + .gap_0p5() + .child(self.upgrade_button(cx)) + .child(self.create_copy_button(error_message)), + ) + .dismiss_action(self.dismiss_error_button(cx)) } fn render_tool_use_limit_reached_error( @@ -3338,52 +3326,59 @@ impl AcpThreadView { let focus_handle = self.focus_handle(cx); - let icon = Icon::new(IconName::Info) - .size(IconSize::Small) - .color(Color::Info); - Some( Callout::new() - .icon(icon) + .icon(IconName::Info) .title("Consecutive tool use limit reached.") - .when(supports_burn_mode, |this| { - this.secondary_action( - Button::new("continue-burn-mode", "Continue with Burn Mode") - .style(ButtonStyle::Filled) - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .layer(ElevationIndex::ModalSurface) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &ContinueWithBurnMode, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(10.))), + .actions_slot( + h_flex() + .gap_0p5() + .when(supports_burn_mode, |this| { + this.child( + Button::new("continue-burn-mode", "Continue with Burn Mode") + .style(ButtonStyle::Filled) + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .layer(ElevationIndex::ModalSurface) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &ContinueWithBurnMode, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .tooltip(Tooltip::text( + "Enable Burn Mode for unlimited tool use.", + )) + .on_click({ + cx.listener(move |this, _, _window, cx| { + thread.update(cx, |thread, _cx| { + thread.set_completion_mode(CompletionMode::Burn); + }); + this.resume_chat(cx); + }) + }), ) - .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use.")) - .on_click({ - cx.listener(move |this, _, _window, cx| { - thread.update(cx, |thread, _cx| { - thread.set_completion_mode(CompletionMode::Burn); - }); + }) + .child( + Button::new("continue-conversation", "Continue") + .layer(ElevationIndex::ModalSurface) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &ContinueThread, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .on_click(cx.listener(|this, _, _window, cx| { this.resume_chat(cx); - }) - }), - ) - }) - .primary_action( - Button::new("continue-conversation", "Continue") - .layer(ElevationIndex::ModalSurface) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in(&ContinueThread, &focus_handle, window, cx) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .on_click(cx.listener(|this, _, _window, cx| { - this.resume_chat(cx); - })), + })), + ), ), ) } @@ -3424,10 +3419,6 @@ impl AcpThreadView { } })) } - - fn error_callout_bg(&self, cx: &Context<Self>) -> Hsla { - cx.theme().status().error.opacity(0.08) - } } impl Focusable for AcpThreadView { diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 38be2b193cab0e86f01b7ec88819d431e0bc7d0d..d2f448635e48cff3f407263b4bb21d1f21a28071 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -2597,7 +2597,7 @@ impl ActiveThread { .id(("message-container", ix)) .py_1() .px_2p5() - .child(Banner::new().severity(ui::Severity::Warning).child(message)) + .child(Banner::new().severity(Severity::Warning).child(message)) } fn render_message_thinking_segment( 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 c68c9c2730ad9ed894e5e3a2b2a842f281b7f281..998641bf01c950d3717fa651a5400e95cbedb618 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 @@ -454,7 +454,7 @@ impl Render for AddLlmProviderModal { this.section( Section::new().child( Banner::new() - .severity(ui::Severity::Warning) + .severity(Severity::Warning) .child(div().text_xs().child(error)), ), ) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index e1174a41917c4cce035a16bc05309dc5f07c9895..cb354222b6b0de897a1053b160ce96a73793ee5a 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -48,9 +48,8 @@ use feature_flags::{self, FeatureFlagAppExt}; use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem, - Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla, - KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, - pulsating_between, + Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, + Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, }; use language::LanguageRegistry; use language_model::{ @@ -2712,20 +2711,22 @@ impl AgentPanel { action_slot: Option<AnyElement>, cx: &mut Context<Self>, ) -> impl IntoElement { - h_flex() - .mt_2() - .pl_1p5() - .pb_1() - .w_full() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child( - Label::new(label.into()) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .children(action_slot) + div().pl_1().pr_1p5().child( + h_flex() + .mt_2() + .pl_1p5() + .pb_1() + .w_full() + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child( + Label::new(label.into()) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .children(action_slot), + ) } fn render_thread_empty_state( @@ -2831,22 +2832,12 @@ impl AgentPanel { }), ), ) - }) - .when_some(configuration_error.as_ref(), |this, err| { - this.child(self.render_configuration_error( - err, - &focus_handle, - window, - cx, - )) }), ) }) .when(!recent_history.is_empty(), |parent| { - let focus_handle = focus_handle.clone(); parent .overflow_hidden() - .p_1p5() .justify_end() .gap_1() .child( @@ -2874,10 +2865,11 @@ impl AgentPanel { ), ) .child( - v_flex() - .gap_1() - .children(recent_history.into_iter().enumerate().map( - |(index, entry)| { + v_flex().p_1().pr_1p5().gap_1().children( + recent_history + .into_iter() + .enumerate() + .map(|(index, entry)| { // TODO: Add keyboard navigation. let is_hovered = self.hovered_recent_history_item == Some(index); @@ -2896,30 +2888,68 @@ impl AgentPanel { }, )) .into_any_element() - }, - )), + }), + ), ) - .when_some(configuration_error.as_ref(), |this, err| { - this.child(self.render_configuration_error(err, &focus_handle, window, cx)) - }) + }) + .when_some(configuration_error.as_ref(), |this, err| { + this.child(self.render_configuration_error(false, err, &focus_handle, window, cx)) }) } fn render_configuration_error( &self, + border_bottom: bool, configuration_error: &ConfigurationError, focus_handle: &FocusHandle, window: &mut Window, cx: &mut App, ) -> impl IntoElement { - match configuration_error { - ConfigurationError::ModelNotFound - | ConfigurationError::ProviderNotAuthenticated(_) - | ConfigurationError::NoProvider => Banner::new() - .severity(ui::Severity::Warning) - .child(Label::new(configuration_error.to_string())) - .action_slot( - Button::new("settings", "Configure Provider") + let zed_provider_configured = AgentSettings::get_global(cx) + .default_model + .as_ref() + .map_or(false, |selection| { + selection.provider.0.as_str() == "zed.dev" + }); + + let callout = if zed_provider_configured { + Callout::new() + .icon(IconName::Warning) + .severity(Severity::Warning) + .when(border_bottom, |this| { + this.border_position(ui::BorderPosition::Bottom) + }) + .title("Sign in to continue using Zed as your LLM provider.") + .actions_slot( + Button::new("sign_in", "Sign In") + .style(ButtonStyle::Tinted(ui::TintColor::Warning)) + .label_size(LabelSize::Small) + .on_click({ + let workspace = self.workspace.clone(); + move |_, _, cx| { + let Ok(client) = + workspace.update(cx, |workspace, _| workspace.client().clone()) + else { + return; + }; + + cx.spawn(async move |cx| { + client.sign_in_with_optional_connect(true, cx).await + }) + .detach_and_log_err(cx); + } + }), + ) + } else { + Callout::new() + .icon(IconName::Warning) + .severity(Severity::Warning) + .when(border_bottom, |this| { + this.border_position(ui::BorderPosition::Bottom) + }) + .title(configuration_error.to_string()) + .actions_slot( + Button::new("settings", "Configure") .style(ButtonStyle::Tinted(ui::TintColor::Warning)) .label_size(LabelSize::Small) .key_binding( @@ -2929,16 +2959,23 @@ impl AgentPanel { .on_click(|_event, window, cx| { window.dispatch_action(OpenSettings.boxed_clone(), cx) }), - ), + ) + }; + + match configuration_error { + ConfigurationError::ModelNotFound + | ConfigurationError::ProviderNotAuthenticated(_) + | ConfigurationError::NoProvider => callout.into_any_element(), ConfigurationError::ProviderPendingTermsAcceptance(provider) => { - Banner::new().severity(ui::Severity::Warning).child( - h_flex().w_full().children( + Banner::new() + .severity(Severity::Warning) + .child(h_flex().w_full().children( provider.render_accept_terms( LanguageModelProviderTosView::ThreadEmptyState, cx, ), - ), - ) + )) + .into_any_element() } } } @@ -2970,7 +3007,7 @@ impl AgentPanel { let focus_handle = self.focus_handle(cx); let banner = Banner::new() - .severity(ui::Severity::Info) + .severity(Severity::Info) .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small)) .action_slot( h_flex() @@ -3081,10 +3118,6 @@ impl AgentPanel { })) } - fn error_callout_bg(&self, cx: &Context<Self>) -> Hsla { - cx.theme().status().error.opacity(0.08) - } - fn render_payment_required_error( &self, thread: &Entity<ActiveThread>, @@ -3093,23 +3126,18 @@ impl AgentPanel { const ERROR_MESSAGE: &str = "You reached your free usage limit. Upgrade to Zed Pro for more prompts."; - let icon = Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error); - - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .child( - Callout::new() - .icon(icon) - .title("Free Usage Exceeded") - .description(ERROR_MESSAGE) - .tertiary_action(self.upgrade_button(thread, cx)) - .secondary_action(self.create_copy_button(ERROR_MESSAGE)) - .primary_action(self.dismiss_error_button(thread, cx)) - .bg_color(self.error_callout_bg(cx)), + Callout::new() + .severity(Severity::Error) + .icon(IconName::XCircle) + .title("Free Usage Exceeded") + .description(ERROR_MESSAGE) + .actions_slot( + h_flex() + .gap_0p5() + .child(self.upgrade_button(thread, cx)) + .child(self.create_copy_button(ERROR_MESSAGE)), ) + .dismiss_action(self.dismiss_error_button(thread, cx)) .into_any_element() } @@ -3124,40 +3152,22 @@ impl AgentPanel { Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.", }; - let icon = Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error); - - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .child( - Callout::new() - .icon(icon) - .title("Model Prompt Limit Reached") - .description(error_message) - .tertiary_action(self.upgrade_button(thread, cx)) - .secondary_action(self.create_copy_button(error_message)) - .primary_action(self.dismiss_error_button(thread, cx)) - .bg_color(self.error_callout_bg(cx)), + Callout::new() + .severity(Severity::Error) + .title("Model Prompt Limit Reached") + .description(error_message) + .actions_slot( + h_flex() + .gap_0p5() + .child(self.upgrade_button(thread, cx)) + .child(self.create_copy_button(error_message)), ) + .dismiss_action(self.dismiss_error_button(thread, cx)) .into_any_element() } - fn render_error_message( - &self, - header: SharedString, - message: SharedString, - thread: &Entity<ActiveThread>, - cx: &mut Context<Self>, - ) -> AnyElement { - let message_with_header = format!("{}\n{}", header, message); - - let icon = Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error); - - let retry_button = Button::new("retry", "Retry") + fn render_retry_button(&self, thread: &Entity<ActiveThread>) -> AnyElement { + Button::new("retry", "Retry") .icon(IconName::RotateCw) .icon_position(IconPosition::Start) .icon_size(IconSize::Small) @@ -3172,21 +3182,31 @@ impl AgentPanel { }); }); } - }); + }) + .into_any_element() + } - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .child( - Callout::new() - .icon(icon) - .title(header) - .description(message.clone()) - .primary_action(retry_button) - .secondary_action(self.dismiss_error_button(thread, cx)) - .tertiary_action(self.create_copy_button(message_with_header)) - .bg_color(self.error_callout_bg(cx)), + fn render_error_message( + &self, + header: SharedString, + message: SharedString, + thread: &Entity<ActiveThread>, + cx: &mut Context<Self>, + ) -> AnyElement { + let message_with_header = format!("{}\n{}", header, message); + + Callout::new() + .severity(Severity::Error) + .icon(IconName::XCircle) + .title(header) + .description(message.clone()) + .actions_slot( + h_flex() + .gap_0p5() + .child(self.render_retry_button(thread)) + .child(self.create_copy_button(message_with_header)), ) + .dismiss_action(self.dismiss_error_button(thread, cx)) .into_any_element() } @@ -3195,60 +3215,39 @@ impl AgentPanel { message: SharedString, can_enable_burn_mode: bool, thread: &Entity<ActiveThread>, - cx: &mut Context<Self>, ) -> AnyElement { - let icon = Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error); - - let retry_button = Button::new("retry", "Retry") - .icon(IconName::RotateCw) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .label_size(LabelSize::Small) - .on_click({ - let thread = thread.clone(); - move |_, window, cx| { - thread.update(cx, |thread, cx| { - thread.clear_last_error(); - thread.thread().update(cx, |thread, cx| { - thread.retry_last_completion(Some(window.window_handle()), cx); - }); - }); - } - }); - - let mut callout = Callout::new() - .icon(icon) + Callout::new() + .severity(Severity::Error) .title("Error") .description(message.clone()) - .bg_color(self.error_callout_bg(cx)) - .primary_action(retry_button); - - if can_enable_burn_mode { - let burn_mode_button = Button::new("enable_burn_retry", "Enable Burn Mode and Retry") - .icon(IconName::ZedBurnMode) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .label_size(LabelSize::Small) - .on_click({ - let thread = thread.clone(); - move |_, window, cx| { - thread.update(cx, |thread, cx| { - thread.clear_last_error(); - thread.thread().update(cx, |thread, cx| { - thread.enable_burn_mode_and_retry(Some(window.window_handle()), cx); - }); - }); - } - }); - callout = callout.secondary_action(burn_mode_button); - } - - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .child(callout) + .actions_slot( + h_flex() + .gap_0p5() + .when(can_enable_burn_mode, |this| { + this.child( + Button::new("enable_burn_retry", "Enable Burn Mode and Retry") + .icon(IconName::ZedBurnMode) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .label_size(LabelSize::Small) + .on_click({ + let thread = thread.clone(); + move |_, window, cx| { + thread.update(cx, |thread, cx| { + thread.clear_last_error(); + thread.thread().update(cx, |thread, cx| { + thread.enable_burn_mode_and_retry( + Some(window.window_handle()), + cx, + ); + }); + }); + } + }), + ) + }) + .child(self.render_retry_button(thread)), + ) .into_any_element() } @@ -3503,7 +3502,6 @@ impl Render for AgentPanel { message, can_enable_burn_mode, thread, - cx, ), }) .into_any(), @@ -3531,16 +3529,13 @@ impl Render for AgentPanel { if !self.should_render_onboarding(cx) && let Some(err) = configuration_error.as_ref() { - this.child( - div().bg(cx.theme().colors().editor_background).p_2().child( - self.render_configuration_error( - err, - &self.focus_handle(cx), - window, - cx, - ), - ), - ) + this.child(self.render_configuration_error( + true, + err, + &self.focus_handle(cx), + window, + cx, + )) } else { this } diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 181a0dd5d2d4f484fb336b388f9845c6b69c8d70..ddb51154f524ecb3bc34373da7eca4ab579f4d37 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1323,14 +1323,10 @@ impl MessageEditor { token_usage_ratio: TokenUsageRatio, cx: &mut Context<Self>, ) -> Option<Div> { - let icon = if token_usage_ratio == TokenUsageRatio::Exceeded { - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::XSmall) + let (icon, severity) = if token_usage_ratio == TokenUsageRatio::Exceeded { + (IconName::Close, Severity::Error) } else { - Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::XSmall) + (IconName::Warning, Severity::Warning) }; let title = if token_usage_ratio == TokenUsageRatio::Exceeded { @@ -1345,29 +1341,33 @@ impl MessageEditor { "To continue, start a new thread from a summary." }; - let mut callout = Callout::new() + let callout = Callout::new() .line_height(line_height) + .severity(severity) .icon(icon) .title(title) .description(description) - .primary_action( - Button::new("start-new-thread", "Start New Thread") - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - let from_thread_id = Some(this.thread.read(cx).id().clone()); - window.dispatch_action(Box::new(NewThread { from_thread_id }), cx); - })), - ); - - if self.is_using_zed_provider(cx) { - callout = callout.secondary_action( - IconButton::new("burn-mode-callout", IconName::ZedBurnMode) - .icon_size(IconSize::XSmall) - .on_click(cx.listener(|this, _event, window, cx| { - this.toggle_burn_mode(&ToggleBurnMode, window, cx); - })), + .actions_slot( + h_flex() + .gap_0p5() + .when(self.is_using_zed_provider(cx), |this| { + this.child( + IconButton::new("burn-mode-callout", IconName::ZedBurnMode) + .icon_size(IconSize::XSmall) + .on_click(cx.listener(|this, _event, window, cx| { + this.toggle_burn_mode(&ToggleBurnMode, window, cx); + })), + ) + }) + .child( + Button::new("start-new-thread", "Start New Thread") + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + let from_thread_id = Some(this.thread.read(cx).id().clone()); + window.dispatch_action(Box::new(NewThread { from_thread_id }), cx); + })), + ), ); - } Some( div() diff --git a/crates/agent_ui/src/ui/preview/usage_callouts.rs b/crates/agent_ui/src/ui/preview/usage_callouts.rs index eef878a9d1b9cac72cb13cde0c8fbd92c1519afc..29b12ea62772fc21b49526ff0fcd06d053dc9c48 100644 --- a/crates/agent_ui/src/ui/preview/usage_callouts.rs +++ b/crates/agent_ui/src/ui/preview/usage_callouts.rs @@ -80,14 +80,10 @@ impl RenderOnce for UsageCallout { } }; - let icon = if is_limit_reached { - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::XSmall) + let (icon, severity) = if is_limit_reached { + (IconName::Close, Severity::Error) } else { - Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::XSmall) + (IconName::Warning, Severity::Warning) }; div() @@ -95,10 +91,12 @@ impl RenderOnce for UsageCallout { .border_color(cx.theme().colors().border) .child( Callout::new() + .icon(icon) + .severity(severity) .icon(icon) .title(title) .description(message) - .primary_action( + .actions_slot( Button::new("upgrade", button_text) .label_size(LabelSize::Small) .on_click(move |_, _, cx| { diff --git a/crates/ai_onboarding/src/young_account_banner.rs b/crates/ai_onboarding/src/young_account_banner.rs index 54f563e4aac8ca71fff16199cd6c2e8f81ad5376..ed9a6b3b35fb2e8e3afaa9d9b658539dd3fa6541 100644 --- a/crates/ai_onboarding/src/young_account_banner.rs +++ b/crates/ai_onboarding/src/young_account_banner.rs @@ -17,6 +17,6 @@ impl RenderOnce for YoungAccountBanner { div() .max_w_full() .my_1() - .child(Banner::new().severity(ui::Severity::Warning).child(label)) + .child(Banner::new().severity(Severity::Warning).child(label)) } } diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 078b90a291bb993c443b6948ebce8ac90e5c83ad..8f52f8c1c369b4614851a05c40c3c9146dcd01d8 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -21,7 +21,7 @@ impl Global for GlobalLanguageModelRegistry {} pub enum ConfigurationError { #[error("Configure at least one LLM provider to start using the panel.")] NoProvider, - #[error("LLM Provider is not configured or does not support the configured model.")] + #[error("LLM provider is not configured or does not support the configured model.")] ModelNotFound, #[error("{} LLM provider is not configured.", .0.name().0)] ProviderNotAuthenticated(Arc<dyn LanguageModelProvider>), diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 58090d2060fd9d1af0429a0bf7c511f5ee0e4ed3..757a0ca22655782fe735ee8447abe3523b4aa6ff 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -2021,21 +2021,21 @@ impl RenderOnce for SyntaxHighlightedText { #[derive(PartialEq)] struct InputError { - severity: ui::Severity, + severity: Severity, content: SharedString, } impl InputError { fn warning(message: impl Into<SharedString>) -> Self { Self { - severity: ui::Severity::Warning, + severity: Severity::Warning, content: message.into(), } } fn error(message: anyhow::Error) -> Self { Self { - severity: ui::Severity::Error, + severity: Severity::Error, content: message.to_string().into(), } } @@ -2162,9 +2162,11 @@ impl KeybindingEditorModal { } fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool { - if self.error.as_ref().is_some_and(|old_error| { - old_error.severity == ui::Severity::Warning && *old_error == error - }) { + if self + .error + .as_ref() + .is_some_and(|old_error| old_error.severity == Severity::Warning && *old_error == error) + { false } else { self.error = Some(error); diff --git a/crates/ui/src/components/banner.rs b/crates/ui/src/components/banner.rs index d493e8a0d3842ab39f1c5da426585cc5577cedde..7458ad8eb07a7f9c473b18c98c902a9582db27d1 100644 --- a/crates/ui/src/components/banner.rs +++ b/crates/ui/src/components/banner.rs @@ -1,15 +1,6 @@ use crate::prelude::*; use gpui::{AnyElement, IntoElement, ParentElement, Styled}; -/// Severity levels that determine the style of the banner. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Severity { - Info, - Success, - Warning, - Error, -} - /// Banners provide informative and brief messages without interrupting the user. /// This component offers four severity levels that can be used depending on the message. /// diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs index abb03198ab1a4ad59e30dfbe2f34e627744f5249..22ba0468cde7fa22a197395379621c8a7c876517 100644 --- a/crates/ui/src/components/callout.rs +++ b/crates/ui/src/components/callout.rs @@ -1,7 +1,13 @@ -use gpui::{AnyElement, Hsla}; +use gpui::AnyElement; use crate::prelude::*; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BorderPosition { + Top, + Bottom, +} + /// A callout component for displaying important information that requires user attention. /// /// # Usage Example @@ -10,42 +16,48 @@ use crate::prelude::*; /// use ui::{Callout}; /// /// Callout::new() -/// .icon(Icon::new(IconName::Warning).color(Color::Warning)) +/// .severity(Severity::Warning) +/// .icon(IconName::Warning) /// .title(Label::new("Be aware of your subscription!")) /// .description(Label::new("Your subscription is about to expire. Renew now!")) -/// .primary_action(Button::new("renew", "Renew Now")) -/// .secondary_action(Button::new("remind", "Remind Me Later")) +/// .actions_slot(Button::new("renew", "Renew Now")) /// ``` /// #[derive(IntoElement, RegisterComponent)] pub struct Callout { - icon: Option<Icon>, + severity: Severity, + icon: Option<IconName>, title: Option<SharedString>, description: Option<SharedString>, - primary_action: Option<AnyElement>, - secondary_action: Option<AnyElement>, - tertiary_action: Option<AnyElement>, + actions_slot: Option<AnyElement>, + dismiss_action: Option<AnyElement>, line_height: Option<Pixels>, - bg_color: Option<Hsla>, + border_position: BorderPosition, } impl Callout { /// Creates a new `Callout` component with default styling. pub fn new() -> Self { Self { + severity: Severity::Info, icon: None, title: None, description: None, - primary_action: None, - secondary_action: None, - tertiary_action: None, + actions_slot: None, + dismiss_action: None, line_height: None, - bg_color: None, + border_position: BorderPosition::Top, } } + /// Sets the severity of the callout. + pub fn severity(mut self, severity: Severity) -> Self { + self.severity = severity; + self + } + /// Sets the icon to display in the callout. - pub fn icon(mut self, icon: Icon) -> Self { + pub fn icon(mut self, icon: IconName) -> Self { self.icon = Some(icon); self } @@ -64,20 +76,14 @@ impl Callout { } /// Sets the primary call-to-action button. - pub fn primary_action(mut self, action: impl IntoElement) -> Self { - self.primary_action = Some(action.into_any_element()); - self - } - - /// Sets an optional secondary call-to-action button. - pub fn secondary_action(mut self, action: impl IntoElement) -> Self { - self.secondary_action = Some(action.into_any_element()); + pub fn actions_slot(mut self, action: impl IntoElement) -> Self { + self.actions_slot = Some(action.into_any_element()); self } /// Sets an optional tertiary call-to-action button. - pub fn tertiary_action(mut self, action: impl IntoElement) -> Self { - self.tertiary_action = Some(action.into_any_element()); + pub fn dismiss_action(mut self, action: impl IntoElement) -> Self { + self.dismiss_action = Some(action.into_any_element()); self } @@ -87,9 +93,9 @@ impl Callout { self } - /// Sets a custom background color for the callout content. - pub fn bg_color(mut self, color: Hsla) -> Self { - self.bg_color = Some(color); + /// Sets the border position in the callout. + pub fn border_position(mut self, border_position: BorderPosition) -> Self { + self.border_position = border_position; self } } @@ -97,21 +103,51 @@ impl Callout { impl RenderOnce for Callout { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let line_height = self.line_height.unwrap_or(window.line_height()); - let bg_color = self - .bg_color - .unwrap_or(cx.theme().colors().panel_background); - let has_actions = self.primary_action.is_some() - || self.secondary_action.is_some() - || self.tertiary_action.is_some(); + + let has_actions = self.actions_slot.is_some() || self.dismiss_action.is_some(); + + let (icon, icon_color, bg_color) = match self.severity { + Severity::Info => ( + IconName::Info, + Color::Muted, + cx.theme().colors().panel_background.opacity(0.), + ), + Severity::Success => ( + IconName::Check, + Color::Success, + cx.theme().status().success.opacity(0.1), + ), + Severity::Warning => ( + IconName::Warning, + Color::Warning, + cx.theme().status().warning_background.opacity(0.2), + ), + Severity::Error => ( + IconName::XCircle, + Color::Error, + cx.theme().status().error.opacity(0.08), + ), + }; h_flex() + .min_w_0() .p_2() .gap_2() .items_start() + .map(|this| match self.border_position { + BorderPosition::Top => this.border_t_1(), + BorderPosition::Bottom => this.border_b_1(), + }) + .border_color(cx.theme().colors().border) .bg(bg_color) .overflow_x_hidden() - .when_some(self.icon, |this, icon| { - this.child(h_flex().h(line_height).justify_center().child(icon)) + .when(self.icon.is_some(), |this| { + this.child( + h_flex() + .h(line_height) + .justify_center() + .child(Icon::new(icon).size(IconSize::Small).color(icon_color)), + ) }) .child( v_flex() @@ -119,10 +155,11 @@ impl RenderOnce for Callout { .w_full() .child( h_flex() - .h(line_height) + .min_h(line_height) .w_full() .gap_1() .justify_between() + .flex_wrap() .when_some(self.title, |this, title| { this.child(h_flex().child(Label::new(title).size(LabelSize::Small))) }) @@ -130,13 +167,10 @@ impl RenderOnce for Callout { this.child( h_flex() .gap_0p5() - .when_some(self.tertiary_action, |this, action| { + .when_some(self.actions_slot, |this, action| { this.child(action) }) - .when_some(self.secondary_action, |this, action| { - this.child(action) - }) - .when_some(self.primary_action, |this, action| { + .when_some(self.dismiss_action, |this, action| { this.child(action) }), ) @@ -168,84 +202,101 @@ impl Component for Callout { } fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> { - let callout_examples = vec![ + let single_action = || Button::new("got-it", "Got it").label_size(LabelSize::Small); + let multiple_actions = || { + h_flex() + .gap_0p5() + .child(Button::new("update", "Backup & Update").label_size(LabelSize::Small)) + .child(Button::new("dismiss", "Dismiss").label_size(LabelSize::Small)) + }; + + let basic_examples = vec![ single_example( "Simple with Title Only", Callout::new() - .icon( - Icon::new(IconName::Info) - .color(Color::Accent) - .size(IconSize::Small), - ) + .icon(IconName::Info) .title("System maintenance scheduled for tonight") - .primary_action(Button::new("got-it", "Got it").label_size(LabelSize::Small)) + .actions_slot(single_action()) .into_any_element(), ) .width(px(580.)), single_example( "With Title and Description", Callout::new() - .icon( - Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::Small), - ) + .icon(IconName::Warning) .title("Your settings contain deprecated values") .description( "We'll backup your current settings and update them to the new format.", ) - .primary_action( - Button::new("update", "Backup & Update").label_size(LabelSize::Small), - ) - .secondary_action( - Button::new("dismiss", "Dismiss").label_size(LabelSize::Small), - ) + .actions_slot(single_action()) .into_any_element(), ) .width(px(580.)), single_example( "Error with Multiple Actions", Callout::new() - .icon( - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small), - ) + .icon(IconName::Close) .title("Thread reached the token limit") .description("Start a new thread from a summary to continue the conversation.") - .primary_action( - Button::new("new-thread", "Start New Thread").label_size(LabelSize::Small), - ) - .secondary_action( - Button::new("view-summary", "View Summary").label_size(LabelSize::Small), - ) + .actions_slot(multiple_actions()) .into_any_element(), ) .width(px(580.)), single_example( "Multi-line Description", Callout::new() - .icon( - Icon::new(IconName::Sparkle) - .color(Color::Accent) - .size(IconSize::Small), - ) + .icon(IconName::Sparkle) .title("Upgrade to Pro") .description("• Unlimited threads\n• Priority support\n• Advanced analytics") - .primary_action( - Button::new("upgrade", "Upgrade Now").label_size(LabelSize::Small), - ) - .secondary_action( - Button::new("learn-more", "Learn More").label_size(LabelSize::Small), - ) + .actions_slot(multiple_actions()) .into_any_element(), ) .width(px(580.)), ]; + let severity_examples = vec![ + single_example( + "Info", + Callout::new() + .icon(IconName::Info) + .title("System maintenance scheduled for tonight") + .actions_slot(single_action()) + .into_any_element(), + ), + single_example( + "Warning", + Callout::new() + .severity(Severity::Warning) + .icon(IconName::Triangle) + .title("System maintenance scheduled for tonight") + .actions_slot(single_action()) + .into_any_element(), + ), + single_example( + "Error", + Callout::new() + .severity(Severity::Error) + .icon(IconName::XCircle) + .title("System maintenance scheduled for tonight") + .actions_slot(single_action()) + .into_any_element(), + ), + single_example( + "Success", + Callout::new() + .severity(Severity::Success) + .icon(IconName::Check) + .title("System maintenance scheduled for tonight") + .actions_slot(single_action()) + .into_any_element(), + ), + ]; + Some( - example_group(callout_examples) - .vertical() + v_flex() + .gap_4() + .child(example_group(basic_examples).vertical()) + .child(example_group_with_title("Severity", severity_examples).vertical()) .into_any_element(), ) } diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 80f8f863f8e60d7f2ab65b1680bc0491729c28be..0357e498bb240113d7ff792bfba213d2c784444b 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -14,7 +14,9 @@ pub use ui_macros::RegisterComponent; pub use crate::DynamicSpacing; pub use crate::animation::{AnimationDirection, AnimationDuration, DefaultAnimations}; -pub use crate::styles::{PlatformStyle, StyledTypography, TextSize, rems_from_px, vh, vw}; +pub use crate::styles::{ + PlatformStyle, Severity, StyledTypography, TextSize, rems_from_px, vh, vw, +}; pub use crate::traits::clickable::*; pub use crate::traits::disableable::*; pub use crate::traits::fixed::*; diff --git a/crates/ui/src/styles.rs b/crates/ui/src/styles.rs index af6ab570291ecea442140326926d1a6d16b3781b..bc2399f54b6d7877c966dd4d378fab54189356a9 100644 --- a/crates/ui/src/styles.rs +++ b/crates/ui/src/styles.rs @@ -3,6 +3,7 @@ mod appearance; mod color; mod elevation; mod platform; +mod severity; mod spacing; mod typography; mod units; @@ -11,6 +12,7 @@ pub use appearance::*; pub use color::*; pub use elevation::*; pub use platform::*; +pub use severity::*; pub use spacing::*; pub use typography::*; pub use units::*; diff --git a/crates/ui/src/styles/severity.rs b/crates/ui/src/styles/severity.rs new file mode 100644 index 0000000000000000000000000000000000000000..464f835186c16e611bb7e37f3b14f43bbc047a64 --- /dev/null +++ b/crates/ui/src/styles/severity.rs @@ -0,0 +1,10 @@ +/// Severity levels that determine the style of the component. +/// Usually, it affects the background. Most of the time, +/// it also follows with an icon corresponding the severity level. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Severity { + Info, + Success, + Warning, + Error, +} From 6ee06bf2a0f0db312e4ec916e2802bd5bef034e8 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:53:05 -0300 Subject: [PATCH 119/744] ai onboarding: Adjust the Zed Pro banner (#36452) Release Notes: - N/A --- crates/ai_onboarding/src/ai_onboarding.rs | 30 ++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 75177d4bd2bf22b203cf9f50134bb821438a433f..717abebfd1dd59ee277f7ae2343024aaafeb825e 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -332,17 +332,25 @@ impl ZedAiOnboarding { .mb_2(), ) .child(plan_definitions.pro_plan(false)) - .child( - Button::new("pro", "Continue with Zed Pro") - .full_width() - .style(ButtonStyle::Outlined) - .on_click({ - let callback = self.continue_with_zed_ai.clone(); - move |_, window, cx| { - telemetry::event!("Banner Dismissed", source = "AI Onboarding"); - callback(window, cx) - } - }), + .when_some( + self.dismiss_onboarding.as_ref(), + |this, dismiss_callback| { + let callback = dismiss_callback.clone(); + this.child( + h_flex().absolute().top_0().right_0().child( + IconButton::new("dismiss_onboarding", IconName::Close) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Dismiss")) + .on_click(move |_, window, cx| { + telemetry::event!( + "Banner Dismissed", + source = "AI Onboarding", + ); + callback(window, cx) + }), + ), + ) + }, ) .into_any_element() } From 4abfcbaff987c0b42081e501aa431935e5dad27d Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Mon, 18 Aug 2025 21:08:20 -0500 Subject: [PATCH 120/744] git: Suggest merge commit message in remote (#36430) Closes #ISSUE Adds `merge_message` field to the `UpdateRepository` proto message so that suggested merge messages are displayed in remote projects. Release Notes: - git: Fixed an issue where suggested merge commit messages would not appear for remote projects --- .../migrations.sqlite/20221109000000_test_schema.sql | 1 + .../migrations/20250818192156_add_git_merge_message.sql | 1 + crates/collab/src/db/queries/projects.rs | 7 +++++-- crates/collab/src/db/queries/rooms.rs | 1 + crates/collab/src/db/tables/project_repository.rs | 2 ++ crates/project/src/git_store.rs | 3 +++ crates/proto/proto/git.proto | 1 + 7 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 crates/collab/migrations/20250818192156_add_git_merge_message.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 170ac7b0a2201996f069d526cd041a5509f3efc5..43581fd9421e5a8d10460a9ed15c565bd66a6e5e 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -116,6 +116,7 @@ CREATE TABLE "project_repositories" ( "scan_id" INTEGER NOT NULL, "is_deleted" BOOL NOT NULL, "current_merge_conflicts" VARCHAR, + "merge_message" VARCHAR, "branch_summary" VARCHAR, "head_commit_details" VARCHAR, PRIMARY KEY (project_id, id) diff --git a/crates/collab/migrations/20250818192156_add_git_merge_message.sql b/crates/collab/migrations/20250818192156_add_git_merge_message.sql new file mode 100644 index 0000000000000000000000000000000000000000..335ea2f82493082e0e20d7762b5282696dc50224 --- /dev/null +++ b/crates/collab/migrations/20250818192156_add_git_merge_message.sql @@ -0,0 +1 @@ +ALTER TABLE "project_repositories" ADD COLUMN "merge_message" VARCHAR; diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 6783d8ed2aa7ce0f21e6f40ed1f088b8299d97b9..9abab25edebd3a537d9a4a678e1ccdef5f45be2a 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -349,11 +349,11 @@ impl Database { serde_json::to_string(&repository.current_merge_conflicts) .unwrap(), )), - - // Old clients do not use abs path, entry ids or head_commit_details. + // Old clients do not use abs path, entry ids, head_commit_details, or merge_message. abs_path: ActiveValue::set(String::new()), entry_ids: ActiveValue::set("[]".into()), head_commit_details: ActiveValue::set(None), + merge_message: ActiveValue::set(None), } }), ) @@ -502,6 +502,7 @@ impl Database { current_merge_conflicts: ActiveValue::Set(Some( serde_json::to_string(&update.current_merge_conflicts).unwrap(), )), + merge_message: ActiveValue::set(update.merge_message.clone()), }) .on_conflict( OnConflict::columns([ @@ -515,6 +516,7 @@ impl Database { project_repository::Column::AbsPath, project_repository::Column::CurrentMergeConflicts, project_repository::Column::HeadCommitDetails, + project_repository::Column::MergeMessage, ]) .to_owned(), ) @@ -990,6 +992,7 @@ impl Database { head_commit_details, scan_id: db_repository_entry.scan_id as u64, is_last_update: true, + merge_message: db_repository_entry.merge_message, }); } } diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 1b128e3a237d06737bdb5ee99d3d78209db06c6e..9e7cabf9b29c91d7e486f42d5e6b12020b0f514e 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -793,6 +793,7 @@ impl Database { abs_path: db_repository.abs_path, scan_id: db_repository.scan_id as u64, is_last_update: true, + merge_message: db_repository.merge_message, }); } } diff --git a/crates/collab/src/db/tables/project_repository.rs b/crates/collab/src/db/tables/project_repository.rs index 665e87cd1fe8c492170a8459dbf7ac6c086f9e00..eb653ecee37d48ce79e26450eb85d87dec411c1e 100644 --- a/crates/collab/src/db/tables/project_repository.rs +++ b/crates/collab/src/db/tables/project_repository.rs @@ -16,6 +16,8 @@ pub struct Model { pub is_deleted: bool, // JSON array typed string pub current_merge_conflicts: Option<String>, + // The suggested merge commit message + pub merge_message: Option<String>, // A JSON object representing the current Branch values pub branch_summary: Option<String>, // A JSON object representing the current Head commit values diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index e8ba2425d140f9bddf079cf835b25f621a5fe7f8..9539008530cd16d479f32f6b9cf60aa8241dae40 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -2774,6 +2774,7 @@ impl RepositorySnapshot { .iter() .map(|repo_path| repo_path.to_proto()) .collect(), + merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()), project_id, id: self.id.to_proto(), abs_path: self.work_directory_abs_path.to_proto(), @@ -2836,6 +2837,7 @@ impl RepositorySnapshot { .iter() .map(|path| path.as_ref().to_proto()) .collect(), + merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()), project_id, id: self.id.to_proto(), abs_path: self.work_directory_abs_path.to_proto(), @@ -4266,6 +4268,7 @@ impl Repository { .map(proto_to_commit_details); self.snapshot.merge.conflicted_paths = conflicted_paths; + self.snapshot.merge.message = update.merge_message.map(SharedString::from); let edits = update .removed_statuses diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index f2c388a3a3db223e4a7dfabcd2b868c47dbbadb1..cfb036987519b50f591d3fb0ca3c11f157a72f08 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -121,6 +121,7 @@ message UpdateRepository { uint64 scan_id = 9; bool is_last_update = 10; optional GitCommitDetails head_commit_details = 11; + optional string merge_message = 12; } message RemoveRepository { From 5004cb647bd843e46c47c830085f3564771f476e Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Mon, 18 Aug 2025 22:43:27 -0400 Subject: [PATCH 121/744] collab: Add `orb_subscription_id` to `billing_subscriptions` (#36455) This PR adds an `orb_subscription_id` column to the `billing_subscriptions` table. Release Notes: - N/A --- ...9022421_add_orb_subscription_id_to_billing_subscriptions.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 crates/collab/migrations/20250819022421_add_orb_subscription_id_to_billing_subscriptions.sql diff --git a/crates/collab/migrations/20250819022421_add_orb_subscription_id_to_billing_subscriptions.sql b/crates/collab/migrations/20250819022421_add_orb_subscription_id_to_billing_subscriptions.sql new file mode 100644 index 0000000000000000000000000000000000000000..317f6a7653e3d1762f74e795a17d2f99b3831201 --- /dev/null +++ b/crates/collab/migrations/20250819022421_add_orb_subscription_id_to_billing_subscriptions.sql @@ -0,0 +1,2 @@ +alter table billing_subscriptions + add column orb_subscription_id text; From 1b6fd996f8bd0ed1934c99495f36e7f9b16c41fd Mon Sep 17 00:00:00 2001 From: Michael Sloan <michael@zed.dev> Date: Mon, 18 Aug 2025 21:23:07 -0600 Subject: [PATCH 122/744] Fix `InlineCompletion` -> `EditPrediction` keymap migration (#36457) Accidentally regressed this in #35512, causing this migration to not work and an error log to appear when one of these actions is in the user keymap Release Notes: - N/A --- .../migrator/src/migrations/m_2025_01_29/keymap.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 646af8f63dc90b6ebe3faef9432eecc54140b438..c32da88229b429ad206168eeee30f401863b39bd 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<HashMap<&str, &str>> = LazyLock::new(|| { "inline_completion::ToggleMenu", "edit_prediction::ToggleMenu", ), - ("editor::NextEditPrediction", "editor::NextEditPrediction"), + ("editor::NextInlineCompletion", "editor::NextEditPrediction"), ( - "editor::PreviousEditPrediction", + "editor::PreviousInlineCompletion", "editor::PreviousEditPrediction", ), ( - "editor::AcceptPartialEditPrediction", + "editor::AcceptPartialInlineCompletion", "editor::AcceptPartialEditPrediction", ), - ("editor::ShowEditPrediction", "editor::ShowEditPrediction"), + ("editor::ShowInlineCompletion", "editor::ShowEditPrediction"), ( - "editor::AcceptEditPrediction", + "editor::AcceptInlineCompletion", "editor::AcceptEditPrediction", ), ( - "editor::ToggleEditPredictions", + "editor::ToggleInlineCompletions", "editor::ToggleEditPrediction", ), ]) From 821e97a392d9ec8c9cf736f26fae86d188dcb409 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Mon, 18 Aug 2025 23:26:15 -0400 Subject: [PATCH 123/744] agent2: Add hover preview for image creases (#36427) Note that (at least for now) this only works for creases in the "new message" editor, not when editing past messages. That's because we don't have the original image available when putting together the creases for past messages, only the base64-encoded language model content. Release Notes: - N/A --- crates/agent_ui/src/acp/message_editor.rs | 162 +++++++++++------- .../ui/src/components/button/button_like.rs | 13 ++ 2 files changed, 111 insertions(+), 64 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index d5922317263fe7e1ae55be08f3bf3f2c1e4b4754..441ca9cf180624be311856839b665b68603df028 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -178,6 +178,56 @@ impl MessageEditor { return; }; + if let MentionUri::File { abs_path, .. } = &mention_uri { + let extension = abs_path + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + + if Img::extensions().contains(&extension) && !extension.contains("svg") { + let project = self.project.clone(); + let Some(project_path) = project + .read(cx) + .project_path_for_absolute_path(abs_path, cx) + else { + return; + }; + let image = cx + .spawn(async move |_, cx| { + let image = project + .update(cx, |project, cx| project.open_image(project_path, cx)) + .map_err(|e| e.to_string())? + .await + .map_err(|e| e.to_string())?; + image + .read_with(cx, |image, _cx| image.image.clone()) + .map_err(|e| e.to_string()) + }) + .shared(); + let Some(crease_id) = insert_crease_for_image( + *excerpt_id, + start, + content_len, + Some(abs_path.as_path().into()), + image.clone(), + self.editor.clone(), + window, + cx, + ) else { + return; + }; + self.confirm_mention_for_image( + crease_id, + anchor, + Some(abs_path.clone()), + image, + window, + cx, + ); + return; + } + } + let Some(crease_id) = crate::context_picker::insert_crease_for_mention( *excerpt_id, start, @@ -195,71 +245,21 @@ impl MessageEditor { MentionUri::Fetch { url } => { self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx); } - MentionUri::File { - abs_path, - is_directory, - } => { - self.confirm_mention_for_file( - crease_id, - anchor, - abs_path, - is_directory, - window, - cx, - ); - } MentionUri::Thread { id, name } => { self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx); } MentionUri::TextThread { path, name } => { self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx); } - MentionUri::Symbol { .. } | MentionUri::Rule { .. } | MentionUri::Selection { .. } => { + MentionUri::File { .. } + | MentionUri::Symbol { .. } + | MentionUri::Rule { .. } + | MentionUri::Selection { .. } => { self.mention_set.insert_uri(crease_id, mention_uri.clone()); } } } - fn confirm_mention_for_file( - &mut self, - crease_id: CreaseId, - anchor: Anchor, - abs_path: PathBuf, - is_directory: bool, - window: &mut Window, - cx: &mut Context<Self>, - ) { - let extension = abs_path - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default(); - - if Img::extensions().contains(&extension) && !extension.contains("svg") { - let project = self.project.clone(); - let Some(project_path) = project - .read(cx) - .project_path_for_absolute_path(&abs_path, cx) - else { - return; - }; - let image = cx.spawn(async move |_, cx| { - let image = project - .update(cx, |project, cx| project.open_image(project_path, cx))? - .await?; - image.read_with(cx, |image, _cx| image.image.clone()) - }); - self.confirm_mention_for_image(crease_id, anchor, Some(abs_path), image, window, cx); - } else { - self.mention_set.insert_uri( - crease_id, - MentionUri::File { - abs_path, - is_directory, - }, - ); - } - } - fn confirm_mention_for_fetch( &mut self, crease_id: CreaseId, @@ -498,25 +498,20 @@ impl MessageEditor { let Some(anchor) = multibuffer_anchor else { return; }; + let task = Task::ready(Ok(Arc::new(image))).shared(); let Some(crease_id) = insert_crease_for_image( excerpt_id, text_anchor, content_len, None.clone(), + task.clone(), self.editor.clone(), window, cx, ) else { return; }; - self.confirm_mention_for_image( - crease_id, - anchor, - None, - Task::ready(Ok(Arc::new(image))), - window, - cx, - ); + self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx); } } @@ -584,7 +579,7 @@ impl MessageEditor { crease_id: CreaseId, anchor: Anchor, abs_path: Option<PathBuf>, - image: Task<Result<Arc<Image>>>, + image: Shared<Task<Result<Arc<Image>, String>>>, window: &mut Window, cx: &mut Context<Self>, ) { @@ -937,6 +932,7 @@ pub(crate) fn insert_crease_for_image( anchor: text::Anchor, content_len: usize, abs_path: Option<Arc<Path>>, + image: Shared<Task<Result<Arc<Image>, String>>>, editor: Entity<Editor>, window: &mut Window, cx: &mut App, @@ -956,7 +952,7 @@ pub(crate) fn insert_crease_for_image( let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len); let placeholder = FoldPlaceholder { - render: render_image_fold_icon_button(crease_label, cx.weak_entity()), + render: render_image_fold_icon_button(crease_label, image, cx.weak_entity()), merge_adjacent: false, ..Default::default() }; @@ -978,9 +974,11 @@ pub(crate) fn insert_crease_for_image( fn render_image_fold_icon_button( label: SharedString, + image_task: Shared<Task<Result<Arc<Image>, String>>>, editor: WeakEntity<Editor>, ) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> { Arc::new({ + let image_task = image_task.clone(); move |fold_id, fold_range, cx| { let is_in_text_selection = editor .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) @@ -1005,11 +1003,47 @@ fn render_image_fold_icon_button( .single_line(), ), ) + .hoverable_tooltip({ + let image_task = image_task.clone(); + move |_, cx| { + let image = image_task.peek().cloned().transpose().ok().flatten(); + let image_task = image_task.clone(); + cx.new::<ImageHover>(|cx| ImageHover { + image, + _task: cx.spawn(async move |this, cx| { + if let Ok(image) = image_task.clone().await { + this.update(cx, |this, cx| { + if this.image.replace(image).is_none() { + cx.notify(); + } + }) + .ok(); + } + }), + }) + .into() + } + }) .into_any_element() } }) } +struct ImageHover { + image: Option<Arc<Image>>, + _task: Task<()>, +} + +impl Render for ImageHover { + fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { + if let Some(image) = self.image.clone() { + gpui::img(image).max_w_96().max_h_96().into_any_element() + } else { + gpui::Empty.into_any_element() + } + } +} + #[derive(Debug, Eq, PartialEq)] pub enum Mention { Text { uri: MentionUri, content: String }, diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 0b30007e44d02f7a02949d1e788f9d3a06c1a2dc..31bf76e84341dd38a6c4c0d7e804034b46774abf 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -400,6 +400,7 @@ pub struct ButtonLike { size: ButtonSize, rounding: Option<ButtonLikeRounding>, tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>, + hoverable_tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>, cursor_style: CursorStyle, on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>, on_right_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>, @@ -420,6 +421,7 @@ impl ButtonLike { size: ButtonSize::Default, rounding: Some(ButtonLikeRounding::All), tooltip: None, + hoverable_tooltip: None, children: SmallVec::new(), cursor_style: CursorStyle::PointingHand, on_click: None, @@ -463,6 +465,14 @@ impl ButtonLike { self.on_right_click = Some(Box::new(handler)); self } + + pub fn hoverable_tooltip( + mut self, + tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, + ) -> Self { + self.hoverable_tooltip = Some(Box::new(tooltip)); + self + } } impl Disableable for ButtonLike { @@ -654,6 +664,9 @@ impl RenderOnce for ButtonLike { .when_some(self.tooltip, |this, tooltip| { this.tooltip(move |window, cx| tooltip(window, cx)) }) + .when_some(self.hoverable_tooltip, |this, tooltip| { + this.hoverable_tooltip(move |window, cx| tooltip(window, cx)) + }) .children(self.children) } } From 7bcea7dc2c0fbeb6d9f42cddc55fa1e4bdf97744 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Tue, 19 Aug 2025 00:09:43 -0400 Subject: [PATCH 124/744] agent2: Support directories in @file mentions (#36416) Release Notes: - N/A --- crates/acp_thread/src/mention.rs | 66 ++-- crates/agent2/src/thread.rs | 14 +- .../agent_ui/src/acp/completion_provider.rs | 13 +- crates/agent_ui/src/acp/message_editor.rs | 369 ++++++++++++------ crates/agent_ui/src/acp/thread_view.rs | 31 +- 5 files changed, 325 insertions(+), 168 deletions(-) diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 17bc265facbecd8fa8141f58f2fa7397be3f7a55..25e64acbee697b23f9c25a006bf709c48ef8d60b 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -15,7 +15,9 @@ use url::Url; pub enum MentionUri { File { abs_path: PathBuf, - is_directory: bool, + }, + Directory { + abs_path: PathBuf, }, Symbol { path: PathBuf, @@ -79,14 +81,14 @@ impl MentionUri { }) } } else { - let file_path = + let abs_path = PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path)); - let is_directory = input.ends_with("/"); - Ok(Self::File { - abs_path: file_path, - is_directory, - }) + if input.ends_with("/") { + Ok(Self::Directory { abs_path }) + } else { + Ok(Self::File { abs_path }) + } } } "zed" => { @@ -120,7 +122,7 @@ impl MentionUri { pub fn name(&self) -> String { match self { - MentionUri::File { abs_path, .. } => abs_path + MentionUri::File { abs_path, .. } | MentionUri::Directory { abs_path, .. } => abs_path .file_name() .unwrap_or_default() .to_string_lossy() @@ -138,18 +140,11 @@ impl MentionUri { pub fn icon_path(&self, cx: &mut App) -> SharedString { match self { - MentionUri::File { - abs_path, - is_directory, - } => { - if *is_directory { - FileIcons::get_folder_icon(false, cx) - .unwrap_or_else(|| IconName::Folder.path().into()) - } else { - FileIcons::get_icon(abs_path, cx) - .unwrap_or_else(|| IconName::File.path().into()) - } + MentionUri::File { abs_path } => { + FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into()) } + MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx) + .unwrap_or_else(|| IconName::Folder.path().into()), MentionUri::Symbol { .. } => IconName::Code.path().into(), MentionUri::Thread { .. } => IconName::Thread.path().into(), MentionUri::TextThread { .. } => IconName::Thread.path().into(), @@ -165,13 +160,16 @@ impl MentionUri { pub fn to_uri(&self) -> Url { match self { - MentionUri::File { - abs_path, - is_directory, - } => { + MentionUri::File { abs_path } => { + let mut url = Url::parse("file:///").unwrap(); + let path = abs_path.to_string_lossy(); + url.set_path(&path); + url + } + MentionUri::Directory { abs_path } => { let mut url = Url::parse("file:///").unwrap(); let mut path = abs_path.to_string_lossy().to_string(); - if *is_directory && !path.ends_with("/") { + if !path.ends_with("/") { path.push_str("/"); } url.set_path(&path); @@ -274,12 +272,8 @@ mod tests { let file_uri = "file:///path/to/file.rs"; let parsed = MentionUri::parse(file_uri).unwrap(); match &parsed { - MentionUri::File { - abs_path, - is_directory, - } => { + MentionUri::File { abs_path } => { assert_eq!(abs_path.to_str().unwrap(), "/path/to/file.rs"); - assert!(!is_directory); } _ => panic!("Expected File variant"), } @@ -291,32 +285,26 @@ mod tests { let file_uri = "file:///path/to/dir/"; let parsed = MentionUri::parse(file_uri).unwrap(); match &parsed { - MentionUri::File { - abs_path, - is_directory, - } => { + MentionUri::Directory { abs_path } => { assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir/"); - assert!(is_directory); } - _ => panic!("Expected File variant"), + _ => panic!("Expected Directory variant"), } assert_eq!(parsed.to_uri().to_string(), file_uri); } #[test] fn test_to_directory_uri_with_slash() { - let uri = MentionUri::File { + let uri = MentionUri::Directory { abs_path: PathBuf::from("/path/to/dir/"), - is_directory: true, }; assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/"); } #[test] fn test_to_directory_uri_without_slash() { - let uri = MentionUri::File { + let uri = MentionUri::Directory { abs_path: PathBuf::from("/path/to/dir"), - is_directory: true, }; assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/"); } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index eed374e3969076aa135029ab91107954179d8224..e0819abcc5eaa8442755e99f143a86b26c604898 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -146,6 +146,7 @@ impl UserMessage { They are up-to-date and don't need to be re-read.\n\n"; const OPEN_FILES_TAG: &str = "<files>"; + const OPEN_DIRECTORIES_TAG: &str = "<directories>"; const OPEN_SYMBOLS_TAG: &str = "<symbols>"; const OPEN_THREADS_TAG: &str = "<threads>"; const OPEN_FETCH_TAG: &str = "<fetched_urls>"; @@ -153,6 +154,7 @@ impl UserMessage { "<rules>\nThe user has specified the following rules that should be applied:\n"; let mut file_context = OPEN_FILES_TAG.to_string(); + let mut directory_context = OPEN_DIRECTORIES_TAG.to_string(); let mut symbol_context = OPEN_SYMBOLS_TAG.to_string(); let mut thread_context = OPEN_THREADS_TAG.to_string(); let mut fetch_context = OPEN_FETCH_TAG.to_string(); @@ -168,7 +170,7 @@ impl UserMessage { } UserMessageContent::Mention { uri, content } => { match uri { - MentionUri::File { abs_path, .. } => { + MentionUri::File { abs_path } => { write!( &mut symbol_context, "\n{}", @@ -179,6 +181,9 @@ impl UserMessage { ) .ok(); } + MentionUri::Directory { .. } => { + write!(&mut directory_context, "\n{}\n", content).ok(); + } MentionUri::Symbol { path, line_range, .. } @@ -233,6 +238,13 @@ impl UserMessage { .push(language_model::MessageContent::Text(file_context)); } + if directory_context.len() > OPEN_DIRECTORIES_TAG.len() { + directory_context.push_str("</directories>\n"); + message + .content + .push(language_model::MessageContent::Text(directory_context)); + } + if symbol_context.len() > OPEN_SYMBOLS_TAG.len() { symbol_context.push_str("</symbols>\n"); message diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index e2ddd03f2700283442bc1f803490465ab05f59cb..d2af2a880ddca00842248929644963edf799efee 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -445,19 +445,20 @@ impl ContextPickerCompletionProvider { let abs_path = project.read(cx).absolute_path(&project_path, cx)?; - let file_uri = MentionUri::File { - abs_path, - is_directory, + let uri = if is_directory { + MentionUri::Directory { abs_path } + } else { + MentionUri::File { abs_path } }; - let crease_icon_path = file_uri.icon_path(cx); + let crease_icon_path = uri.icon_path(cx); let completion_icon_path = if is_recent { IconName::HistoryRerun.path().into() } else { crease_icon_path.clone() }; - let new_text = format!("{} ", file_uri.as_link()); + let new_text = format!("{} ", uri.as_link()); let new_text_len = new_text.len(); Some(Completion { replace_range: source_range.clone(), @@ -472,7 +473,7 @@ impl ContextPickerCompletionProvider { source_range.start, new_text_len - 1, message_editor, - file_uri, + uri, )), }) } diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 441ca9cf180624be311856839b665b68603df028..e5ecf43ef5be0eb6688df0d3b03b1dc5d892fe96 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -6,6 +6,7 @@ use acp_thread::{MentionUri, selection_name}; use agent::{TextThreadStore, ThreadId, ThreadStore}; use agent_client_protocol as acp; use anyhow::{Context as _, Result, anyhow}; +use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, @@ -15,7 +16,7 @@ use editor::{ }; use futures::{ FutureExt as _, TryFutureExt as _, - future::{Shared, try_join_all}, + future::{Shared, join_all, try_join_all}, }; use gpui::{ AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image, @@ -23,12 +24,12 @@ use gpui::{ }; use language::{Buffer, Language}; use language_model::LanguageModelImage; -use project::{CompletionIntent, Project}; +use project::{CompletionIntent, Project, ProjectPath, Worktree}; use rope::Point; use settings::Settings; use std::{ ffi::OsStr, - fmt::Write, + fmt::{Display, Write}, ops::Range, path::{Path, PathBuf}, rc::Rc, @@ -245,6 +246,9 @@ impl MessageEditor { MentionUri::Fetch { url } => { self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx); } + MentionUri::Directory { abs_path } => { + self.confirm_mention_for_directory(crease_id, anchor, abs_path, window, cx); + } MentionUri::Thread { id, name } => { self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx); } @@ -260,6 +264,124 @@ impl MessageEditor { } } + fn confirm_mention_for_directory( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + abs_path: PathBuf, + window: &mut Window, + cx: &mut Context<Self>, + ) { + fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> { + let mut files = Vec::new(); + + for entry in worktree.child_entries(path) { + if entry.is_dir() { + files.extend(collect_files_in_path(worktree, &entry.path)); + } else if entry.is_file() { + files.push((entry.path.clone(), worktree.full_path(&entry.path))); + } + } + + files + } + + let uri = MentionUri::Directory { + abs_path: abs_path.clone(), + }; + let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return; + }; + let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else { + return; + }; + let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else { + return; + }; + let project = self.project.clone(); + let task = cx.spawn(async move |_, cx| { + let directory_path = entry.path.clone(); + + let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?; + let file_paths = worktree.read_with(cx, |worktree, _cx| { + collect_files_in_path(worktree, &directory_path) + })?; + let descendants_future = cx.update(|cx| { + join_all(file_paths.into_iter().map(|(worktree_path, full_path)| { + let rel_path = worktree_path + .strip_prefix(&directory_path) + .log_err() + .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into()); + + let open_task = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + let project_path = ProjectPath { + worktree_id, + path: worktree_path, + }; + buffer_store.open_buffer(project_path, cx) + }) + }); + + // TODO: report load errors instead of just logging + let rope_task = cx.spawn(async move |cx| { + let buffer = open_task.await.log_err()?; + let rope = buffer + .read_with(cx, |buffer, _cx| buffer.as_rope().clone()) + .log_err()?; + Some(rope) + }); + + cx.background_spawn(async move { + let rope = rope_task.await?; + Some((rel_path, full_path, rope.to_string())) + }) + })) + })?; + + let contents = cx + .background_spawn(async move { + let contents = descendants_future.await.into_iter().flatten(); + contents.collect() + }) + .await; + anyhow::Ok(contents) + }); + let task = cx + .spawn(async move |_, _| { + task.await + .map(|contents| DirectoryContents(contents).to_string()) + .map_err(|e| e.to_string()) + }) + .shared(); + + self.mention_set.directories.insert(abs_path, task.clone()); + + let editor = self.editor.clone(); + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + this.update(cx, |this, _| { + this.mention_set.insert_uri(crease_id, uri); + }) + .ok(); + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + } + }) + .detach(); + } + fn confirm_mention_for_fetch( &mut self, crease_id: CreaseId, @@ -361,6 +483,104 @@ impl MessageEditor { } } + fn confirm_mention_for_thread( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + id: ThreadId, + name: String, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let uri = MentionUri::Thread { + id: id.clone(), + name, + }; + let open_task = self.thread_store.update(cx, |thread_store, cx| { + thread_store.open_thread(&id, window, cx) + }); + let task = cx + .spawn(async move |_, cx| { + let thread = open_task.await.map_err(|e| e.to_string())?; + let content = thread + .read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text()) + .map_err(|e| e.to_string())?; + Ok(content) + }) + .shared(); + + self.mention_set.insert_thread(id, task.clone()); + + let editor = self.editor.clone(); + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + this.update(cx, |this, _| { + this.mention_set.insert_uri(crease_id, uri); + }) + .ok(); + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + } + }) + .detach(); + } + + fn confirm_mention_for_text_thread( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + path: PathBuf, + name: String, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let uri = MentionUri::TextThread { + path: path.clone(), + name, + }; + let context = self.text_thread_store.update(cx, |text_thread_store, cx| { + text_thread_store.open_local_context(path.as_path().into(), cx) + }); + let task = cx + .spawn(async move |_, cx| { + let context = context.await.map_err(|e| e.to_string())?; + let xml = context + .update(cx, |context, cx| context.to_xml(cx)) + .map_err(|e| e.to_string())?; + Ok(xml) + }) + .shared(); + + self.mention_set.insert_text_thread(path, task.clone()); + + let editor = self.editor.clone(); + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + this.update(cx, |this, _| { + this.mention_set.insert_uri(crease_id, uri); + }) + .ok(); + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + } + }) + .detach(); + } + pub fn contents( &self, window: &mut Window, @@ -613,13 +833,8 @@ impl MessageEditor { if task.await.notify_async_err(cx).is_some() { if let Some(abs_path) = abs_path.clone() { this.update(cx, |this, _cx| { - this.mention_set.insert_uri( - crease_id, - MentionUri::File { - abs_path, - is_directory: false, - }, - ); + this.mention_set + .insert_uri(crease_id, MentionUri::File { abs_path }); }) .ok(); } @@ -637,104 +852,6 @@ impl MessageEditor { .detach(); } - fn confirm_mention_for_thread( - &mut self, - crease_id: CreaseId, - anchor: Anchor, - id: ThreadId, - name: String, - window: &mut Window, - cx: &mut Context<Self>, - ) { - let uri = MentionUri::Thread { - id: id.clone(), - name, - }; - let open_task = self.thread_store.update(cx, |thread_store, cx| { - thread_store.open_thread(&id, window, cx) - }); - let task = cx - .spawn(async move |_, cx| { - let thread = open_task.await.map_err(|e| e.to_string())?; - let content = thread - .read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text()) - .map_err(|e| e.to_string())?; - Ok(content) - }) - .shared(); - - self.mention_set.insert_thread(id, task.clone()); - - let editor = self.editor.clone(); - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - this.update(cx, |this, _| { - this.mention_set.insert_uri(crease_id, uri); - }) - .ok(); - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - } - }) - .detach(); - } - - fn confirm_mention_for_text_thread( - &mut self, - crease_id: CreaseId, - anchor: Anchor, - path: PathBuf, - name: String, - window: &mut Window, - cx: &mut Context<Self>, - ) { - let uri = MentionUri::TextThread { - path: path.clone(), - name, - }; - let context = self.text_thread_store.update(cx, |text_thread_store, cx| { - text_thread_store.open_local_context(path.as_path().into(), cx) - }); - let task = cx - .spawn(async move |_, cx| { - let context = context.await.map_err(|e| e.to_string())?; - let xml = context - .update(cx, |context, cx| context.to_xml(cx)) - .map_err(|e| e.to_string())?; - Ok(xml) - }) - .shared(); - - self.mention_set.insert_text_thread(path, task.clone()); - - let editor = self.editor.clone(); - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - this.update(cx, |this, _| { - this.mention_set.insert_uri(crease_id, uri); - }) - .ok(); - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - } - }) - .detach(); - } - pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.set_mode(mode); @@ -817,6 +934,10 @@ impl MessageEditor { self.mention_set .add_fetch_result(url, Task::ready(Ok(text)).shared()); } + MentionUri::Directory { abs_path } => { + let task = Task::ready(Ok(text)).shared(); + self.mention_set.directories.insert(abs_path, task); + } MentionUri::File { .. } | MentionUri::Symbol { .. } | MentionUri::Rule { .. } @@ -882,6 +1003,18 @@ impl MessageEditor { } } +struct DirectoryContents(Arc<[(Arc<Path>, PathBuf, String)]>); + +impl Display for DirectoryContents { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (_relative_path, full_path, content) in self.0.iter() { + let fence = codeblock_fence_for_path(Some(full_path), None); + write!(f, "\n{fence}\n{content}\n```")?; + } + Ok(()) + } +} + impl Focusable for MessageEditor { fn focus_handle(&self, cx: &App) -> FocusHandle { self.editor.focus_handle(cx) @@ -1064,6 +1197,7 @@ pub struct MentionSet { images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>, thread_summaries: HashMap<ThreadId, Shared<Task<Result<SharedString, String>>>>, text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>, + directories: HashMap<PathBuf, Shared<Task<Result<String, String>>>>, } impl MentionSet { @@ -1116,7 +1250,6 @@ impl MentionSet { .map(|(&crease_id, uri)| { match uri { MentionUri::File { abs_path, .. } => { - // TODO directories let uri = uri.clone(); let abs_path = abs_path.to_path_buf(); @@ -1141,6 +1274,24 @@ impl MentionSet { anyhow::Ok((crease_id, Mention::Text { uri, content })) }) } + MentionUri::Directory { abs_path } => { + let Some(content) = self.directories.get(abs_path).cloned() else { + return Task::ready(Err(anyhow!("missing directory load task"))); + }; + let uri = uri.clone(); + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Text { + uri, + content: content + .await + .map_err(|e| anyhow::anyhow!("{e}"))? + .to_string(), + }, + )) + }) + } MentionUri::Symbol { path, line_range, .. } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 0d15e27e0c4808cbdabd1b7a5f13e881ee8ed350..b3ebe8667439be11c802a2345bfc11ebb476d6e4 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2790,25 +2790,30 @@ impl AcpThreadView { if let Some(mention) = MentionUri::parse(&url).log_err() { workspace.update(cx, |workspace, cx| match mention { - MentionUri::File { abs_path, .. } => { + MentionUri::File { abs_path } => { let project = workspace.project(); - let Some((path, entry)) = project.update(cx, |project, cx| { + let Some(path) = + project.update(cx, |project, cx| project.find_project_path(abs_path, cx)) + else { + return; + }; + + workspace + .open_path(path, None, true, window, cx) + .detach_and_log_err(cx); + } + MentionUri::Directory { abs_path } => { + let project = workspace.project(); + let Some(entry) = project.update(cx, |project, cx| { let path = project.find_project_path(abs_path, cx)?; - let entry = project.entry_for_path(&path, cx)?; - Some((path, entry)) + project.entry_for_path(&path, cx) }) else { return; }; - if entry.is_dir() { - project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(entry.id)); - }); - } else { - workspace - .open_path(path, None, true, window, cx) - .detach_and_log_err(cx); - } + project.update(cx, |_, cx| { + cx.emit(project::Event::RevealInProjectPanel(entry.id)); + }); } MentionUri::Symbol { path, line_range, .. From d30b017d1f7dda921ebd1ab6a3ef726e1f796571 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Tue, 19 Aug 2025 02:00:41 -0400 Subject: [PATCH 125/744] Prevent sending slash commands in CC threads (#36453) Highlight them as errors in the editor, and add a leading space when sending them so users don't hit the odd behavior when sending these commands to the SDK. Release Notes: - N/A --- crates/agent2/src/native_agent_server.rs | 6 +- crates/agent_servers/src/agent_servers.rs | 9 + crates/agent_servers/src/claude.rs | 4 + crates/agent_servers/src/gemini.rs | 6 +- crates/agent_ui/src/acp/entry_view_state.rs | 5 + crates/agent_ui/src/acp/message_editor.rs | 223 +++++++++++++++++++- crates/agent_ui/src/acp/thread_view.rs | 9 +- crates/editor/src/hover_popover.rs | 17 +- 8 files changed, 263 insertions(+), 16 deletions(-) diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index cadd88a8462ca0c297ef0b7b8cd516f87104c4eb..6f09ee117574c23c22761e3b7ef3d011d0a11f6a 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -1,4 +1,4 @@ -use std::{path::Path, rc::Rc, sync::Arc}; +use std::{any::Any, path::Path, rc::Rc, sync::Arc}; use agent_servers::AgentServer; use anyhow::Result; @@ -66,4 +66,8 @@ impl AgentServer for NativeAgentServer { Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>) }) } + + fn into_any(self: Rc<Self>) -> Rc<dyn Any> { + self + } } diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index b3b8a3317049927986a6a578bc50c4e5506b7650..8f8aa1d7887b93c4ce7f7487d916d154f7d16de1 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -18,6 +18,7 @@ use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{ + any::Any, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -40,6 +41,14 @@ pub trait AgentServer: Send { project: &Entity<Project>, cx: &mut App, ) -> Task<Result<Rc<dyn AgentConnection>>>; + + fn into_any(self: Rc<Self>) -> Rc<dyn Any>; +} + +impl dyn AgentServer { + pub fn downcast<T: 'static + AgentServer + Sized>(self: Rc<Self>) -> Option<Rc<T>> { + self.into_any().downcast().ok() + } } impl std::fmt::Debug for AgentServerCommand { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 9b273cb091d28334515430e9b035b873d881875c..7034d6fbcea3370e1e528173591973e14ddeee1c 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -65,6 +65,10 @@ impl AgentServer for ClaudeCode { Task::ready(Ok(Rc::new(connection) as _)) } + + fn into_any(self: Rc<Self>) -> Rc<dyn Any> { + self + } } struct ClaudeAgentConnection { diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index ad883f6da8bd344044e1db0051ca6f24120d5057..167e632d79847027e1e6822964cf6a2beedb5155 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -1,5 +1,5 @@ -use std::path::Path; use std::rc::Rc; +use std::{any::Any, path::Path}; use crate::{AgentServer, AgentServerCommand}; use acp_thread::{AgentConnection, LoadError}; @@ -86,6 +86,10 @@ impl AgentServer for Gemini { result }) } + + fn into_any(self: Rc<Self>) -> Rc<dyn Any> { + self + } } #[cfg(test)] diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 18ef1ce2abe33783a0fb1b5f44c559e48c667617..0b0b8471a7debb9388e26659b347137b4e062c05 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -24,6 +24,7 @@ pub struct EntryViewState { thread_store: Entity<ThreadStore>, text_thread_store: Entity<TextThreadStore>, entries: Vec<Entry>, + prevent_slash_commands: bool, } impl EntryViewState { @@ -32,6 +33,7 @@ impl EntryViewState { project: Entity<Project>, thread_store: Entity<ThreadStore>, text_thread_store: Entity<TextThreadStore>, + prevent_slash_commands: bool, ) -> Self { Self { workspace, @@ -39,6 +41,7 @@ impl EntryViewState { thread_store, text_thread_store, entries: Vec::new(), + prevent_slash_commands, } } @@ -77,6 +80,7 @@ impl EntryViewState { self.thread_store.clone(), self.text_thread_store.clone(), "Edit message - @ to include context", + self.prevent_slash_commands, editor::EditorMode::AutoHeight { min_lines: 1, max_lines: None, @@ -382,6 +386,7 @@ mod tests { project.clone(), thread_store, text_thread_store, + false, ) }); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index e5ecf43ef5be0eb6688df0d3b03b1dc5d892fe96..a32d0ce6ce8c3e0b292d56fb9fccbdd598c75888 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -10,7 +10,8 @@ use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, - EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, ToOffset, + EditorEvent, EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, + SemanticsProvider, ToOffset, actions::Paste, display_map::{Crease, CreaseId, FoldId}, }; @@ -19,8 +20,9 @@ use futures::{ future::{Shared, join_all, try_join_all}, }; use gpui::{ - AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image, - ImageFormat, Img, Task, TextStyle, WeakEntity, + AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, + HighlightStyle, Image, ImageFormat, Img, Subscription, Task, TextStyle, UnderlineStyle, + WeakEntity, }; use language::{Buffer, Language}; use language_model::LanguageModelImage; @@ -28,26 +30,30 @@ use project::{CompletionIntent, Project, ProjectPath, Worktree}; use rope::Point; use settings::Settings; use std::{ + cell::Cell, ffi::OsStr, fmt::{Display, Write}, ops::Range, path::{Path, PathBuf}, rc::Rc, sync::Arc, + time::Duration, }; -use text::OffsetRangeExt; +use text::{OffsetRangeExt, ToOffset as _}; use theme::ThemeSettings; use ui::{ ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement, Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div, - h_flex, + h_flex, px, }; use url::Url; use util::ResultExt; use workspace::{Workspace, notifications::NotifyResultExt as _}; use zed_actions::agent::Chat; +const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50); + pub struct MessageEditor { mention_set: MentionSet, editor: Entity<Editor>, @@ -55,6 +61,9 @@ pub struct MessageEditor { workspace: WeakEntity<Workspace>, thread_store: Entity<ThreadStore>, text_thread_store: Entity<TextThreadStore>, + prevent_slash_commands: bool, + _subscriptions: Vec<Subscription>, + _parse_slash_command_task: Task<()>, } #[derive(Clone, Copy)] @@ -73,6 +82,7 @@ impl MessageEditor { thread_store: Entity<ThreadStore>, text_thread_store: Entity<TextThreadStore>, placeholder: impl Into<Arc<str>>, + prevent_slash_commands: bool, mode: EditorMode, window: &mut Window, cx: &mut Context<Self>, @@ -90,6 +100,9 @@ impl MessageEditor { text_thread_store.downgrade(), cx.weak_entity(), ); + let semantics_provider = Rc::new(SlashCommandSemanticsProvider { + range: Cell::new(None), + }); let mention_set = MentionSet::default(); let editor = cx.new(|cx| { let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); @@ -106,6 +119,9 @@ impl MessageEditor { max_entries_visible: 12, placement: Some(ContextMenuPlacement::Above), }); + if prevent_slash_commands { + editor.set_semantics_provider(Some(semantics_provider.clone())); + } editor }); @@ -114,6 +130,24 @@ impl MessageEditor { }) .detach(); + let mut subscriptions = Vec::new(); + if prevent_slash_commands { + subscriptions.push(cx.subscribe_in(&editor, window, { + let semantics_provider = semantics_provider.clone(); + move |this, editor, event, window, cx| match event { + EditorEvent::Edited { .. } => { + this.highlight_slash_command( + semantics_provider.clone(), + editor.clone(), + window, + cx, + ); + } + _ => {} + } + })); + } + Self { editor, project, @@ -121,6 +155,9 @@ impl MessageEditor { thread_store, text_thread_store, workspace, + prevent_slash_commands, + _subscriptions: subscriptions, + _parse_slash_command_task: Task::ready(()), } } @@ -590,6 +627,7 @@ impl MessageEditor { self.mention_set .contents(self.project.clone(), self.thread_store.clone(), window, cx); let editor = self.editor.clone(); + let prevent_slash_commands = self.prevent_slash_commands; cx.spawn(async move |_, cx| { let contents = contents.await?; @@ -612,7 +650,15 @@ impl MessageEditor { let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot); if crease_range.start > ix { - chunks.push(text[ix..crease_range.start].into()); + let chunk = if prevent_slash_commands + && ix == 0 + && parse_slash_command(&text[ix..]).is_some() + { + format!(" {}", &text[ix..crease_range.start]).into() + } else { + text[ix..crease_range.start].into() + }; + chunks.push(chunk); } let chunk = match mention { Mention::Text { uri, content } => { @@ -644,7 +690,14 @@ impl MessageEditor { } if ix < text.len() { - let last_chunk = text[ix..].trim_end(); + let last_chunk = if prevent_slash_commands + && ix == 0 + && parse_slash_command(&text[ix..]).is_some() + { + format!(" {}", text[ix..].trim_end()) + } else { + text[ix..].trim_end().to_owned() + }; if !last_chunk.is_empty() { chunks.push(last_chunk.into()); } @@ -990,6 +1043,48 @@ impl MessageEditor { cx.notify(); } + fn highlight_slash_command( + &mut self, + semantics_provider: Rc<SlashCommandSemanticsProvider>, + editor: Entity<Editor>, + window: &mut Window, + cx: &mut Context<Self>, + ) { + struct InvalidSlashCommand; + + self._parse_slash_command_task = cx.spawn_in(window, async move |_, cx| { + cx.background_executor() + .timer(PARSE_SLASH_COMMAND_DEBOUNCE) + .await; + editor + .update_in(cx, |editor, window, cx| { + let snapshot = editor.snapshot(window, cx); + let range = parse_slash_command(&editor.text(cx)); + semantics_provider.range.set(range); + if let Some((start, end)) = range { + editor.highlight_text::<InvalidSlashCommand>( + vec![ + snapshot.buffer_snapshot.anchor_after(start) + ..snapshot.buffer_snapshot.anchor_before(end), + ], + HighlightStyle { + underline: Some(UnderlineStyle { + thickness: px(1.), + color: Some(gpui::red()), + wavy: true, + }), + ..Default::default() + }, + cx, + ); + } else { + editor.clear_highlights::<InvalidSlashCommand>(cx); + } + }) + .ok(); + }) + } + #[cfg(test)] pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { @@ -1416,6 +1511,118 @@ impl MentionSet { } } +struct SlashCommandSemanticsProvider { + range: Cell<Option<(usize, usize)>>, +} + +impl SemanticsProvider for SlashCommandSemanticsProvider { + fn hover( + &self, + buffer: &Entity<Buffer>, + position: text::Anchor, + cx: &mut App, + ) -> Option<Task<Vec<project::Hover>>> { + let snapshot = buffer.read(cx).snapshot(); + let offset = position.to_offset(&snapshot); + let (start, end) = self.range.get()?; + if !(start..end).contains(&offset) { + return None; + } + let range = snapshot.anchor_after(start)..snapshot.anchor_after(end); + return Some(Task::ready(vec![project::Hover { + contents: vec![project::HoverBlock { + text: "Slash commands are not supported".into(), + kind: project::HoverBlockKind::PlainText, + }], + range: Some(range), + language: None, + }])); + } + + fn inline_values( + &self, + _buffer_handle: Entity<Buffer>, + _range: Range<text::Anchor>, + _cx: &mut App, + ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> { + None + } + + fn inlay_hints( + &self, + _buffer_handle: Entity<Buffer>, + _range: Range<text::Anchor>, + _cx: &mut App, + ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> { + None + } + + fn resolve_inlay_hint( + &self, + _hint: project::InlayHint, + _buffer_handle: Entity<Buffer>, + _server_id: lsp::LanguageServerId, + _cx: &mut App, + ) -> Option<Task<anyhow::Result<project::InlayHint>>> { + None + } + + fn supports_inlay_hints(&self, _buffer: &Entity<Buffer>, _cx: &mut App) -> bool { + false + } + + fn document_highlights( + &self, + _buffer: &Entity<Buffer>, + _position: text::Anchor, + _cx: &mut App, + ) -> Option<Task<Result<Vec<project::DocumentHighlight>>>> { + None + } + + fn definitions( + &self, + _buffer: &Entity<Buffer>, + _position: text::Anchor, + _kind: editor::GotoDefinitionKind, + _cx: &mut App, + ) -> Option<Task<Result<Vec<project::LocationLink>>>> { + None + } + + fn range_for_rename( + &self, + _buffer: &Entity<Buffer>, + _position: text::Anchor, + _cx: &mut App, + ) -> Option<Task<Result<Option<Range<text::Anchor>>>>> { + None + } + + fn perform_rename( + &self, + _buffer: &Entity<Buffer>, + _position: text::Anchor, + _new_name: String, + _cx: &mut App, + ) -> Option<Task<Result<project::ProjectTransaction>>> { + None + } +} + +fn parse_slash_command(text: &str) -> Option<(usize, usize)> { + if let Some(remainder) = text.strip_prefix('/') { + let pos = remainder + .find(char::is_whitespace) + .unwrap_or(remainder.len()); + let command = &remainder[..pos]; + if !command.is_empty() && command.chars().all(char::is_alphanumeric) { + return Some((0, 1 + command.len())); + } + } + None +} + #[cfg(test)] mod tests { use std::{ops::Range, path::Path, sync::Arc}; @@ -1463,6 +1670,7 @@ mod tests { thread_store.clone(), text_thread_store.clone(), "Test", + false, EditorMode::AutoHeight { min_lines: 1, max_lines: None, @@ -1661,6 +1869,7 @@ mod tests { thread_store.clone(), text_thread_store.clone(), "Test", + false, EditorMode::AutoHeight { max_lines: None, min_lines: 1, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index b3ebe8667439be11c802a2345bfc11ebb476d6e4..2cfedfe840e06f4b08a88c4c63e185a2aab37c6d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -7,7 +7,7 @@ use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::{self as acp}; -use agent_servers::AgentServer; +use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; use anyhow::bail; use audio::{Audio, Sound}; @@ -160,6 +160,7 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context<Self>, ) -> Self { + let prevent_slash_commands = agent.clone().downcast::<ClaudeCode>().is_some(); let message_editor = cx.new(|cx| { MessageEditor::new( workspace.clone(), @@ -167,6 +168,7 @@ impl AcpThreadView { thread_store.clone(), text_thread_store.clone(), "Message the agent - @ to include context", + prevent_slash_commands, editor::EditorMode::AutoHeight { min_lines: MIN_EDITOR_LINES, max_lines: Some(MAX_EDITOR_LINES), @@ -184,6 +186,7 @@ impl AcpThreadView { project.clone(), thread_store.clone(), text_thread_store.clone(), + prevent_slash_commands, ) }); @@ -3925,6 +3928,10 @@ pub(crate) mod tests { ) -> Task<gpui::Result<Rc<dyn AgentConnection>>> { Task::ready(Ok(Rc::new(self.connection.clone()))) } + + fn into_any(self: Rc<Self>) -> Rc<dyn Any> { + self + } } #[derive(Clone)] diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 3fc673bad9197a9142b4027ffe77a1e123a0522a..6fe981fd6e9aac29402d13b2edb6a2d05cca67bc 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -167,7 +167,8 @@ pub fn hover_at_inlay( let language_registry = project.read_with(cx, |p, _| p.languages().clone())?; let blocks = vec![inlay_hover.tooltip]; - let parsed_content = parse_blocks(&blocks, &language_registry, None, cx).await; + let parsed_content = + parse_blocks(&blocks, Some(&language_registry), None, cx).await; let scroll_handle = ScrollHandle::new(); @@ -251,7 +252,9 @@ fn show_hover( let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?; - let language_registry = editor.project()?.read(cx).languages().clone(); + let language_registry = editor + .project() + .map(|project| project.read(cx).languages().clone()); let provider = editor.semantics_provider.clone()?; if !ignore_timeout { @@ -443,7 +446,8 @@ fn show_hover( text: format!("Unicode character U+{:02X}", invisible as u32), kind: HoverBlockKind::PlainText, }]; - let parsed_content = parse_blocks(&blocks, &language_registry, None, cx).await; + let parsed_content = + parse_blocks(&blocks, language_registry.as_ref(), None, cx).await; let scroll_handle = ScrollHandle::new(); let subscription = this .update(cx, |_, cx| { @@ -493,7 +497,8 @@ fn show_hover( let blocks = hover_result.contents; let language = hover_result.language; - let parsed_content = parse_blocks(&blocks, &language_registry, language, cx).await; + let parsed_content = + parse_blocks(&blocks, language_registry.as_ref(), language, cx).await; let scroll_handle = ScrollHandle::new(); hover_highlights.push(range.clone()); let subscription = this @@ -583,7 +588,7 @@ fn same_diagnostic_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anc async fn parse_blocks( blocks: &[HoverBlock], - language_registry: &Arc<LanguageRegistry>, + language_registry: Option<&Arc<LanguageRegistry>>, language: Option<Arc<Language>>, cx: &mut AsyncWindowContext, ) -> Option<Entity<Markdown>> { @@ -603,7 +608,7 @@ async fn parse_blocks( .new_window_entity(|_window, cx| { Markdown::new( combined_text.into(), - Some(language_registry.clone()), + language_registry.cloned(), language.map(|language| language.name()), cx, ) From 176c445817c431ec2557d2df074d97e600983b96 Mon Sep 17 00:00:00 2001 From: 0x5457 <0x5457@protonmail.com> Date: Tue, 19 Aug 2025 15:28:24 +0800 Subject: [PATCH 126/744] Avoid symlink conflicts when re-extracting `eslint-xx.tar.gz` (#36068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #34325 **Background** When upgrading/reinstalling the ESLint language server, extracting the archive over an existing version directory that contains symlinks can fail and interrupt the installation. ``` failed to unpack .../vscode-eslint-2.4.4/.../client/src/shared File exists (os error 17) when symlinking ../../$shared/ to .../client/src/shared ``` **Root cause** Extracting into a non-empty directory conflicts with leftover files/symlinks (e.g., `client/src/shared -> ../../$shared`), causing “File exists (os error 17)”. When `fs::metadata(&server_path).await.is_err()` is true, the code falls back to cached_server_binary, but that still targets the same (potentially corrupted/half-installed) directory and does not run `npm install` or `npm run compile`, so the system cannot recover and remains broken. **Change** Before downloading and extracting, delete the target version directory (vscode-eslint-<version>) to ensure an empty extraction destination and avoid conflicts. **Alternative approaches** temp directory + rename: extract into a clean temp directory and rename into place to avoid half-installed states [async-tar](https://github.com/dignifiedquire/async-tar) enhancement: tolerate already-existing symlinks (or add a “replace-existing” option). Release Notes: - Fixed eslint installation not clearing files after previous attempts' --- crates/languages/src/typescript.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index d477acc7f641484d5cd99c4d7874877a286d071d..7937adbc09925176256044dbe766e159400b4b2c 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -910,7 +910,7 @@ impl LspAdapter for EsLintLspAdapter { let server_path = destination_path.join(Self::SERVER_PATH); if fs::metadata(&server_path).await.is_err() { - remove_matching(&container_dir, |entry| entry != destination_path).await; + remove_matching(&container_dir, |_| true).await; download_server_binary( delegate, From 1fbb318714624e5fa1e7fdd5e97cfa325ae0b5ca Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:06:35 +0300 Subject: [PATCH 127/744] Fix iterator related clippy style lint violations (#36437) Release Notes: - N/A --- Cargo.toml | 5 +++++ crates/agent_ui/src/agent_diff.rs | 3 +-- crates/agent_ui/src/message_editor.rs | 6 +----- crates/agent_ui/src/text_thread_editor.rs | 6 +----- .../debugger_ui/src/session/running/variable_list.rs | 2 +- crates/editor/src/editor.rs | 2 +- crates/git_ui/src/git_panel.rs | 10 +++++----- crates/git_ui/src/project_diff.rs | 2 +- crates/language/src/proto.rs | 2 +- crates/language_tools/src/key_context_view.rs | 4 +--- crates/settings_ui/src/keybindings.rs | 8 +------- crates/title_bar/src/collab.rs | 2 +- crates/vim/src/normal.rs | 2 +- 13 files changed, 21 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3854ebe0109a7162c023992f7089bd2f9bf70947..b61eb3c2600dca6c5a856f24b6caf049f9c32a6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -820,6 +820,11 @@ single_range_in_vec_init = "allow" style = { level = "allow", priority = -1 } # Temporary list of style lints that we've fixed so far. +iter_cloned_collect = "warn" +iter_next_slice = "warn" +iter_nth = "warn" +iter_nth_zero = "warn" +iter_skip_next = "warn" module_inception = { level = "deny" } question_mark = { level = "deny" } redundant_closure = { level = "deny" } diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 85e729781027238e5c106f30fc26ee14d811763c..3522a0c9ab7d5e140860b87b926d94ee42f65b5d 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -503,8 +503,7 @@ fn update_editor_selection( &[last_kept_hunk_end..editor::Anchor::max()], buffer_snapshot, ) - .skip(1) - .next() + .nth(1) }) .or_else(|| { let first_kept_hunk = diff_hunks.first()?; diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index ddb51154f524ecb3bc34373da7eca4ab579f4d37..64c9a873f56fcc6ff4f4a353543cf25a369a1fd4 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -690,11 +690,7 @@ impl MessageEditor { .as_ref() .map(|model| { self.incompatible_tools_state.update(cx, |state, cx| { - state - .incompatible_tools(&model.model, cx) - .iter() - .cloned() - .collect::<Vec<_>>() + state.incompatible_tools(&model.model, cx).to_vec() }) }) .unwrap_or_default(); diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 8c1e163ecaf54d305f5f3d440e29d23ea7a0f0f8..376d3c54fd59fac56e83e9003deb0b2d7aa2115b 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -747,11 +747,7 @@ impl TextThreadEditor { self.context.read(cx).invoked_slash_command(&command_id) { if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { - let run_commands_in_ranges = invoked_slash_command - .run_commands_in_ranges - .iter() - .cloned() - .collect::<Vec<_>>(); + let run_commands_in_ranges = invoked_slash_command.run_commands_in_ranges.clone(); for range in run_commands_in_ranges { let commands = self.context.update(cx, |context, cx| { context.reparse(cx); diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index b54ee29e158536e12b5669cbebd6ba1a853fbbb6..3cc5fbc272bd3c59ba1bb6d35d37bfc3964e4d9e 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -272,7 +272,7 @@ impl VariableList { let mut entries = vec![]; let scopes: Vec<_> = self.session.update(cx, |session, cx| { - session.scopes(stack_frame_id, cx).iter().cloned().collect() + session.scopes(stack_frame_id, cx).to_vec() }); let mut contains_local_scope = false; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 365cd1ea5a8cd93c886daaf2033eba89f5e45281..a49f1dba860763a8009b7b868df8a2c9ff5097c5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -20932,7 +20932,7 @@ impl Editor { let existing_pending = self .text_highlights::<PendingInput>(cx) - .map(|(_, ranges)| ranges.iter().cloned().collect::<Vec<_>>()); + .map(|(_, ranges)| ranges.to_vec()); if existing_pending.is_none() && pending.is_empty() { return; } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index c21ac286cb381ba1218f231a83d6dd363242bac2..b1bdcdc3e0b8895426354ea9b50e04faef462dcb 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2756,7 +2756,7 @@ impl GitPanel { for pending in self.pending.iter() { if pending.target_status == TargetStatus::Staged { pending_staged_count += pending.entries.len(); - last_pending_staged = pending.entries.iter().next().cloned(); + last_pending_staged = pending.entries.first().cloned(); } if let Some(single_staged) = &single_staged_entry { if pending @@ -5261,7 +5261,7 @@ mod tests { project .read(cx) .worktrees(cx) - .nth(0) + .next() .unwrap() .read(cx) .as_local() @@ -5386,7 +5386,7 @@ mod tests { project .read(cx) .worktrees(cx) - .nth(0) + .next() .unwrap() .read(cx) .as_local() @@ -5437,7 +5437,7 @@ mod tests { project .read(cx) .worktrees(cx) - .nth(0) + .next() .unwrap() .read(cx) .as_local() @@ -5486,7 +5486,7 @@ mod tests { project .read(cx) .worktrees(cx) - .nth(0) + .next() .unwrap() .read(cx) .as_local() diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index e312d6a2aae270ef90324f6cfd1c0dca50c5eab2..09c5ce11523aee66bd9d09e8f23ca5494ca1979c 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -280,7 +280,7 @@ impl ProjectDiff { fn button_states(&self, cx: &App) -> ButtonStates { let editor = self.editor.read(cx); let snapshot = self.multibuffer.read(cx).snapshot(cx); - let prev_next = snapshot.diff_hunks().skip(1).next().is_some(); + let prev_next = snapshot.diff_hunks().nth(1).is_some(); let mut selection = true; let mut ranges = editor diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index acae97019f0cfc73cef6c8fa91da68efa3d51e18..3be189cea08f247f97d05e6b9714f07d17289a8a 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -86,7 +86,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { proto::operation::UpdateCompletionTriggers { replica_id: lamport_timestamp.replica_id as u32, lamport_timestamp: lamport_timestamp.value, - triggers: triggers.iter().cloned().collect(), + triggers: triggers.clone(), language_server_id: server_id.to_proto(), }, ), diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 88131781ec3af336d3ae793cf1820e5bcf731605..320668cfc28ab659fa7f7b688c5639de898ccc08 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -98,9 +98,7 @@ impl KeyContextView { cx.notify(); }); let sub2 = cx.observe_pending_input(window, |this, window, cx| { - this.pending_keystrokes = window - .pending_input_keystrokes() - .map(|k| k.iter().cloned().collect()); + this.pending_keystrokes = window.pending_input_keystrokes().map(|k| k.to_vec()); if this.pending_keystrokes.is_some() { this.last_keystrokes.take(); } diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 757a0ca22655782fe735ee8447abe3523b4aa6ff..b8c52602a627b63bec4964ae0b0cf4fc1da1b86e 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -472,13 +472,7 @@ impl KeymapEditor { fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> { match self.search_mode { - SearchMode::KeyStroke { .. } => self - .keystroke_editor - .read(cx) - .keystrokes() - .iter() - .cloned() - .collect(), + SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(), SearchMode::Normal => Default::default(), } } diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index b458c64b5f32c11ffb9a6840e374c7b6132212ab..c2171d3899285aebc5c90db7e53dbaec42db0637 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -601,7 +601,7 @@ fn pick_default_screen(cx: &App) -> Task<anyhow::Result<Option<Rc<dyn ScreenCapt .metadata() .is_ok_and(|meta| meta.is_main.unwrap_or_default()) }) - .or_else(|| available_sources.iter().next()) + .or_else(|| available_sources.first()) .cloned()) }) } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index b74d85b7c58bbb659724e6c8add18f772edb37a6..0c7b6e55a10f60f673cc44dddd0710f03a7d0435 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -221,7 +221,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) { return; }; - let anchors = last_change.iter().cloned().collect::<Vec<_>>(); + let anchors = last_change.to_vec(); let mut last_row = None; let ranges: Vec<_> = anchors .iter() From ed14ab8c02e6c96e67053764da1f012df3ad7f74 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:26:37 +0200 Subject: [PATCH 128/744] gpui: Introduce stacker to address stack overflows with deep layout trees (#35813) Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Lukas Wirth <lukas@zed.dev> Co-authored-by: Ben Kunkle <ben@zed.dev> Release Notes: - N/A Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Lukas Wirth <lukas@zed.dev> Co-authored-by: Ben Kunkle <ben@zed.dev> --- Cargo.lock | 35 +++++++++++++++++++++++++++++++++ Cargo.toml | 1 + crates/gpui/Cargo.toml | 1 + crates/gpui/src/elements/div.rs | 9 +++++++-- crates/gpui/src/taffy.rs | 15 +++++++++++--- 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c05839ef3ed9f52f0eb88b472b41c2b3fbab44a..2ef91c79c936578a4cc06abf107e385d125f236c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7482,6 +7482,7 @@ dependencies = [ "slotmap", "smallvec", "smol", + "stacksafe", "strum 0.27.1", "sum_tree", "taffy", @@ -15541,6 +15542,40 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stacker" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + +[[package]] +name = "stacksafe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9c1172965d317e87ddb6d364a040d958b40a1db82b6ef97da26253a8b3d090" +dependencies = [ + "stacker", + "stacksafe-macro", +] + +[[package]] +name = "stacksafe-macro" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69" +dependencies = [ + "proc-macro-error2", + "quote", + "syn 2.0.101", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index b61eb3c2600dca6c5a856f24b6caf049f9c32a6e..f326090b51a066dfbc303b4a44228759ec4b96b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -590,6 +590,7 @@ simplelog = "0.12.2" smallvec = { version = "1.6", features = ["union"] } smol = "2.0" sqlformat = "0.2" +stacksafe = "0.1" streaming-iterator = "0.1" strsim = "0.11" strum = { version = "0.27.0", features = ["derive"] } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 6be8c5fd1f29e535ddcbd855dbafaa49cdbda591..9f5b66087da1110f50ac08d9106ec960e2f965aa 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -119,6 +119,7 @@ serde_json.workspace = true slotmap = "1.0.6" smallvec.workspace = true smol.workspace = true +stacksafe.workspace = true strum.workspace = true sum_tree.workspace = true taffy = "=0.9.0" diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 78114b7ecf78e5ff0f53883523fdacf227f8fba4..f553bf55f652daa0907983c18481a1d897923108 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -27,6 +27,7 @@ use crate::{ use collections::HashMap; use refineable::Refineable; use smallvec::SmallVec; +use stacksafe::{StackSafe, stacksafe}; use std::{ any::{Any, TypeId}, cell::RefCell, @@ -1195,7 +1196,7 @@ pub fn div() -> Div { /// A [`Div`] element, the all-in-one element for building complex UIs in GPUI pub struct Div { interactivity: Interactivity, - children: SmallVec<[AnyElement; 2]>, + children: SmallVec<[StackSafe<AnyElement>; 2]>, prepaint_listener: Option<Box<dyn Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static>>, image_cache: Option<Box<dyn ImageCacheProvider>>, } @@ -1256,7 +1257,8 @@ impl InteractiveElement for Div { impl ParentElement for Div { fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) { - self.children.extend(elements) + self.children + .extend(elements.into_iter().map(StackSafe::new)) } } @@ -1272,6 +1274,7 @@ impl Element for Div { self.interactivity.source_location() } + #[stacksafe] fn request_layout( &mut self, global_id: Option<&GlobalElementId>, @@ -1307,6 +1310,7 @@ impl Element for Div { (layout_id, DivFrameState { child_layout_ids }) } + #[stacksafe] fn prepaint( &mut self, global_id: Option<&GlobalElementId>, @@ -1376,6 +1380,7 @@ impl Element for Div { ) } + #[stacksafe] fn paint( &mut self, global_id: Option<&GlobalElementId>, diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index ee21ecd8c4a4b5b4c4a3853b56af9fe210bc5481..f78d6b30c7648b278d9076daf0cca2e4c65acb01 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -3,6 +3,7 @@ use crate::{ }; use collections::{FxHashMap, FxHashSet}; use smallvec::SmallVec; +use stacksafe::{StackSafe, stacksafe}; use std::{fmt::Debug, ops::Range}; use taffy::{ TaffyTree, TraversePartialTree as _, @@ -11,8 +12,15 @@ use taffy::{ tree::NodeId, }; -type NodeMeasureFn = Box< - dyn FnMut(Size<Option<Pixels>>, Size<AvailableSpace>, &mut Window, &mut App) -> Size<Pixels>, +type NodeMeasureFn = StackSafe< + Box< + dyn FnMut( + Size<Option<Pixels>>, + Size<AvailableSpace>, + &mut Window, + &mut App, + ) -> Size<Pixels>, + >, >; struct NodeContext { @@ -88,7 +96,7 @@ impl TaffyLayoutEngine { .new_leaf_with_context( taffy_style, NodeContext { - measure: Box::new(measure), + measure: StackSafe::new(Box::new(measure)), }, ) .expect(EXPECT_MESSAGE) @@ -143,6 +151,7 @@ impl TaffyLayoutEngine { Ok(edges) } + #[stacksafe] pub fn compute_layout( &mut self, id: LayoutId, From b8ddb0141c0625a47fdc7b68aa8a8a782c439f62 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Tue, 19 Aug 2025 11:12:57 +0200 Subject: [PATCH 129/744] agent2: Port rules UI (#36429) Release Notes: - N/A --- crates/agent2/src/agent.rs | 19 +-- crates/agent2/src/tests/mod.rs | 10 +- crates/agent2/src/thread.rs | 20 +-- crates/agent2/src/tools/edit_file_tool.rs | 20 +-- crates/agent_ui/src/acp/thread_view.rs | 160 +++++++++++++++++++++- 5 files changed, 197 insertions(+), 32 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 985de4d123b0dffc3e9b1fac8fd35b767738b71d..6347f5f9a4a606c5703cc970f6101ad0ee635462 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -22,7 +22,6 @@ use prompt_store::{ }; use settings::update_settings_file; use std::any::Any; -use std::cell::RefCell; use std::collections::HashMap; use std::path::Path; use std::rc::Rc; @@ -156,7 +155,7 @@ pub struct NativeAgent { /// Session ID -> Session mapping sessions: HashMap<acp::SessionId, Session>, /// Shared project context for all threads - project_context: Rc<RefCell<ProjectContext>>, + project_context: Entity<ProjectContext>, project_context_needs_refresh: watch::Sender<()>, _maintain_project_context: Task<Result<()>>, context_server_registry: Entity<ContextServerRegistry>, @@ -200,7 +199,7 @@ impl NativeAgent { watch::channel(()); Self { sessions: HashMap::new(), - project_context: Rc::new(RefCell::new(project_context)), + project_context: cx.new(|_| project_context), project_context_needs_refresh: project_context_needs_refresh_tx, _maintain_project_context: cx.spawn(async move |this, cx| { Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await @@ -233,7 +232,9 @@ impl NativeAgent { Self::build_project_context(&this.project, this.prompt_store.as_ref(), cx) })? .await; - this.update(cx, |this, _| this.project_context.replace(project_context))?; + this.update(cx, |this, cx| { + this.project_context = cx.new(|_| project_context); + })?; } Ok(()) @@ -872,8 +873,8 @@ mod tests { ) .await .unwrap(); - agent.read_with(cx, |agent, _| { - assert_eq!(agent.project_context.borrow().worktrees, vec![]) + agent.read_with(cx, |agent, cx| { + assert_eq!(agent.project_context.read(cx).worktrees, vec![]) }); let worktree = project @@ -881,9 +882,9 @@ mod tests { .await .unwrap(); cx.run_until_parked(); - agent.read_with(cx, |agent, _| { + agent.read_with(cx, |agent, cx| { assert_eq!( - agent.project_context.borrow().worktrees, + agent.project_context.read(cx).worktrees, vec![WorktreeContext { root_name: "a".into(), abs_path: Path::new("/a").into(), @@ -898,7 +899,7 @@ mod tests { agent.read_with(cx, |agent, cx| { let rules_entry = worktree.read(cx).entry_for_path(".rules").unwrap(); assert_eq!( - agent.project_context.borrow().worktrees, + agent.project_context.read(cx).worktrees, vec![WorktreeContext { root_name: "a".into(), abs_path: Path::new("/a").into(), diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index e3e3050d49d688804eeec5fa7c3dc2b883246a06..13b37fbaa2853bae6ec77e0d4c0b424c088dc6b8 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use settings::SettingsStore; use smol::stream::StreamExt; -use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc, time::Duration}; +use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; use util::path; mod test_tools; @@ -101,7 +101,9 @@ async fn test_system_prompt(cx: &mut TestAppContext) { } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - project_context.borrow_mut().shell = "test-shell".into(); + project_context.update(cx, |project_context, _cx| { + project_context.shell = "test-shell".into() + }); thread.update(cx, |thread, _| thread.add_tool(EchoTool)); thread .update(cx, |thread, cx| { @@ -1447,7 +1449,7 @@ fn stop_events(result_events: Vec<Result<AgentResponseEvent>>) -> Vec<acp::StopR struct ThreadTest { model: Arc<dyn LanguageModel>, thread: Entity<Thread>, - project_context: Rc<RefCell<ProjectContext>>, + project_context: Entity<ProjectContext>, fs: Arc<FakeFs>, } @@ -1543,7 +1545,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { }) .await; - let project_context = Rc::new(RefCell::new(ProjectContext::default())); + let project_context = cx.new(|_cx| ProjectContext::default()); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let action_log = cx.new(|_| ActionLog::new(project.clone())); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index e0819abcc5eaa8442755e99f143a86b26c604898..7f0465f5ce5838d4b0bccbed3962bf7ac86848cd 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -25,7 +25,7 @@ use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; use settings::{Settings, update_settings_file}; use smol::stream::StreamExt; -use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc}; +use std::{collections::BTreeMap, path::Path, sync::Arc}; use std::{fmt::Write, ops::Range}; use util::{ResultExt, markdown::MarkdownCodeBlock}; use uuid::Uuid; @@ -479,7 +479,7 @@ pub struct Thread { tool_use_limit_reached: bool, context_server_registry: Entity<ContextServerRegistry>, profile_id: AgentProfileId, - project_context: Rc<RefCell<ProjectContext>>, + project_context: Entity<ProjectContext>, templates: Arc<Templates>, model: Option<Arc<dyn LanguageModel>>, project: Entity<Project>, @@ -489,7 +489,7 @@ pub struct Thread { impl Thread { pub fn new( project: Entity<Project>, - project_context: Rc<RefCell<ProjectContext>>, + project_context: Entity<ProjectContext>, context_server_registry: Entity<ContextServerRegistry>, action_log: Entity<ActionLog>, templates: Arc<Templates>, @@ -520,6 +520,10 @@ impl Thread { &self.project } + pub fn project_context(&self) -> &Entity<ProjectContext> { + &self.project_context + } + pub fn action_log(&self) -> &Entity<ActionLog> { &self.action_log } @@ -750,10 +754,10 @@ impl Thread { Ok(events_rx) } - pub fn build_system_message(&self) -> LanguageModelRequestMessage { + pub fn build_system_message(&self, cx: &App) -> LanguageModelRequestMessage { log::debug!("Building system message"); let prompt = SystemPromptTemplate { - project: &self.project_context.borrow(), + project: &self.project_context.read(cx), available_tools: self.tools.keys().cloned().collect(), } .render(&self.templates) @@ -1030,7 +1034,7 @@ impl Thread { log::debug!("Completion intent: {:?}", completion_intent); log::debug!("Completion mode: {:?}", self.completion_mode); - let messages = self.build_request_messages(); + let messages = self.build_request_messages(cx); log::info!("Request will include {} messages", messages.len()); let tools = if let Some(tools) = self.tools(cx).log_err() { @@ -1101,12 +1105,12 @@ impl Thread { ))) } - fn build_request_messages(&self) -> Vec<LanguageModelRequestMessage> { + fn build_request_messages(&self, cx: &App) -> Vec<LanguageModelRequestMessage> { log::trace!( "Building request messages from {} thread messages", self.messages.len() ); - let mut messages = vec![self.build_system_message()]; + let mut messages = vec![self.build_system_message(cx)]; for message in &self.messages { match message { Message::User(message) => messages.push(message.to_request()), diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index e70e5e8a141726826e1b5e6aa95fb2654c1ee96d..8ebd2936a5ea29d1e461696f2088ebeb99248552 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -503,9 +503,9 @@ mod tests { use fs::Fs; use gpui::{TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; + use prompt_store::ProjectContext; use serde_json::json; use settings::SettingsStore; - use std::rc::Rc; use util::path; #[gpui::test] @@ -522,7 +522,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project, - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry, action_log, Templates::new(), @@ -719,7 +719,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project, - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry, action_log.clone(), Templates::new(), @@ -855,7 +855,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project, - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry, action_log.clone(), Templates::new(), @@ -981,7 +981,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project, - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry, action_log.clone(), Templates::new(), @@ -1118,7 +1118,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project, - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry, action_log.clone(), Templates::new(), @@ -1228,7 +1228,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project.clone(), - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), action_log.clone(), Templates::new(), @@ -1309,7 +1309,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project.clone(), - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), action_log.clone(), Templates::new(), @@ -1393,7 +1393,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project.clone(), - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), action_log.clone(), Templates::new(), @@ -1474,7 +1474,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project.clone(), - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry, action_log.clone(), Templates::new(), diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2cfedfe840e06f4b08a88c4c63e185a2aab37c6d..2fffe1b1796cb9e4bb5bdb6d4e1b7f8952e5fce5 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -30,7 +30,7 @@ use language::Buffer; use language_model::LanguageModelRegistry; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; -use project::Project; +use project::{Project, ProjectEntryId}; use prompt_store::PromptId; use rope::Point; use settings::{Settings as _, SettingsStore}; @@ -703,6 +703,38 @@ impl AcpThreadView { }) } + fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) { + let Some(thread) = self.as_native_thread(cx) else { + return; + }; + let project_context = thread.read(cx).project_context().read(cx); + + let project_entry_ids = project_context + .worktrees + .iter() + .flat_map(|worktree| worktree.rules_file.as_ref()) + .map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id)) + .collect::<Vec<_>>(); + + self.workspace + .update(cx, move |workspace, cx| { + // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules + // files clear. For example, if rules file 1 is already open but rules file 2 is not, + // this would open and focus rules file 2 in a tab that is not next to rules file 1. + let project = workspace.project().read(cx); + let project_paths = project_entry_ids + .into_iter() + .flat_map(|entry_id| project.path_for_entry(entry_id, cx)) + .collect::<Vec<_>>(); + for project_path in project_paths { + workspace + .open_path(project_path, None, true, window, cx) + .detach_and_log_err(cx); + } + }) + .ok(); + } + fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context<Self>) { self.thread_error = Some(ThreadError::from_err(error)); cx.notify(); @@ -858,6 +890,12 @@ impl AcpThreadView { let editor_focus = editor.focus_handle(cx).is_focused(window); let focus_border = cx.theme().colors().border_focused; + let rules_item = if entry_ix == 0 { + self.render_rules_item(cx) + } else { + None + }; + div() .id(("user_message", entry_ix)) .py_4() @@ -874,6 +912,7 @@ impl AcpThreadView { })) }) })) + .children(rules_item) .child( div() .relative() @@ -1862,6 +1901,125 @@ impl AcpThreadView { .into_any_element() } + fn render_rules_item(&self, cx: &Context<Self>) -> Option<AnyElement> { + let project_context = self + .as_native_thread(cx)? + .read(cx) + .project_context() + .read(cx); + + let user_rules_text = if project_context.user_rules.is_empty() { + None + } else if project_context.user_rules.len() == 1 { + let user_rules = &project_context.user_rules[0]; + + match user_rules.title.as_ref() { + Some(title) => Some(format!("Using \"{title}\" user rule")), + None => Some("Using user rule".into()), + } + } else { + Some(format!( + "Using {} user rules", + project_context.user_rules.len() + )) + }; + + let first_user_rules_id = project_context + .user_rules + .first() + .map(|user_rules| user_rules.uuid.0); + + let rules_files = project_context + .worktrees + .iter() + .filter_map(|worktree| worktree.rules_file.as_ref()) + .collect::<Vec<_>>(); + + let rules_file_text = match rules_files.as_slice() { + &[] => None, + &[rules_file] => Some(format!( + "Using project {:?} file", + rules_file.path_in_worktree + )), + rules_files => Some(format!("Using {} project rules files", rules_files.len())), + }; + + if user_rules_text.is_none() && rules_file_text.is_none() { + return None; + } + + Some( + v_flex() + .pt_2() + .px_2p5() + .gap_1() + .when_some(user_rules_text, |parent, user_rules_text| { + parent.child( + h_flex() + .w_full() + .child( + Icon::new(IconName::Reader) + .size(IconSize::XSmall) + .color(Color::Disabled), + ) + .child( + Label::new(user_rules_text) + .size(LabelSize::XSmall) + .color(Color::Muted) + .truncate() + .buffer_font(cx) + .ml_1p5() + .mr_0p5(), + ) + .child( + IconButton::new("open-prompt-library", IconName::ArrowUpRight) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(Color::Ignored) + // TODO: Figure out a way to pass focus handle here so we can display the `OpenRulesLibrary` keybinding + .tooltip(Tooltip::text("View User Rules")) + .on_click(move |_event, window, cx| { + window.dispatch_action( + Box::new(OpenRulesLibrary { + prompt_to_select: first_user_rules_id, + }), + cx, + ) + }), + ), + ) + }) + .when_some(rules_file_text, |parent, rules_file_text| { + parent.child( + h_flex() + .w_full() + .child( + Icon::new(IconName::File) + .size(IconSize::XSmall) + .color(Color::Disabled), + ) + .child( + Label::new(rules_file_text) + .size(LabelSize::XSmall) + .color(Color::Muted) + .buffer_font(cx) + .ml_1p5() + .mr_0p5(), + ) + .child( + IconButton::new("open-rule", IconName::ArrowUpRight) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(Color::Ignored) + .on_click(cx.listener(Self::handle_open_rules)) + .tooltip(Tooltip::text("View Rules")), + ), + ) + }) + .into_any(), + ) + } + fn render_empty_state(&self, cx: &App) -> AnyElement { let loading = matches!(&self.thread_state, ThreadState::Loading { .. }); From 47e1d4511cda45a2044435523209282ffd2f8627 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Tue, 19 Aug 2025 14:43:41 +0530 Subject: [PATCH 130/744] editor: Fix `edit_predictions_disabled_in` not disabling predictions (#36469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #25744 Only setting changes and editor init determined whether to show predictions, so glob patterns and toggles correctly disabled them. On cursor changes we call `update_visible_edit_prediction`, but we weren’t discarding predictions when the scope changed. This PR fixes that. Release Notes: - Fixed an issue where the `edit_predictions_disabled_in` setting was ignored in some cases. --- crates/editor/src/edit_prediction_tests.rs | 42 +++++++++++++++++++++- crates/editor/src/editor.rs | 8 +++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/edit_prediction_tests.rs b/crates/editor/src/edit_prediction_tests.rs index 7bf51e45d72f383b4af34cf6ad493792f8e9d351..bba632e81f77ba91927abd1c0e3448a732e1c6f5 100644 --- a/crates/editor/src/edit_prediction_tests.rs +++ b/crates/editor/src/edit_prediction_tests.rs @@ -7,7 +7,9 @@ use std::ops::Range; use text::{Point, ToOffset}; use crate::{ - EditPrediction, editor_tests::init_test, test::editor_test_context::EditorTestContext, + EditPrediction, + editor_tests::{init_test, update_test_language_settings}, + test::editor_test_context::EditorTestContext, }; #[gpui::test] @@ -271,6 +273,44 @@ async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui: }); } +#[gpui::test] +async fn test_edit_predictions_disabled_in_scope(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + update_test_language_settings(cx, |settings| { + settings.defaults.edit_predictions_disabled_in = Some(vec!["string".to_string()]); + }); + + let mut cx = EditorTestContext::new(cx).await; + let provider = cx.new(|_| FakeEditPredictionProvider::default()); + assign_editor_completion_provider(provider.clone(), &mut cx); + + let language = languages::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into()); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // Test disabled inside of string + cx.set_state("const x = \"hello ˇworld\";"); + propose_edits(&provider, vec![(17..17, "beautiful ")], &mut cx); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + cx.editor(|editor, _, _| { + assert!( + editor.active_edit_prediction.is_none(), + "Edit predictions should be disabled in string scopes when configured in edit_predictions_disabled_in" + ); + }); + + // Test enabled outside of string + cx.set_state("const x = \"hello world\"; ˇ"); + propose_edits(&provider, vec![(24..24, "// comment")], &mut cx); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + cx.editor(|editor, _, _| { + assert!( + editor.active_edit_prediction.is_some(), + "Edit predictions should work outside of disabled scopes" + ); + }); +} + fn assert_editor_active_edit_completion( cx: &mut EditorTestContext, assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a49f1dba860763a8009b7b868df8a2c9ff5097c5..c52a59a9093da90981a830ac76ce2a10fb4af187 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7764,6 +7764,14 @@ impl Editor { self.edit_prediction_settings = self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx); + match self.edit_prediction_settings { + EditPredictionSettings::Disabled => { + self.discard_edit_prediction(false, cx); + return None; + } + _ => {} + }; + self.edit_prediction_indent_conflict = multibuffer.is_line_whitespace_upto(cursor); if self.edit_prediction_indent_conflict { From 0ea0d466d289ff2c57bdac3ab4b20f61c9ab7494 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Tue, 19 Aug 2025 11:41:55 +0200 Subject: [PATCH 131/744] agent2: Port retry logic (#36421) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 15 ++ crates/agent2/src/agent.rs | 5 + crates/agent2/src/tests/mod.rs | 166 +++++++++++- crates/agent2/src/thread.rs | 286 ++++++++++++++++++--- crates/agent_ui/src/acp/thread_view.rs | 61 ++++- crates/agent_ui/src/agent_diff.rs | 1 + crates/language_model/src/fake_provider.rs | 32 ++- 7 files changed, 514 insertions(+), 52 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 8bc06354755b0feb8e0e41061df82e3ac9415a53..916f48cbe049faf00626ece2480e9b17f4b98f54 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -24,6 +24,7 @@ use std::fmt::{Formatter, Write}; use std::ops::Range; use std::process::ExitStatus; use std::rc::Rc; +use std::time::{Duration, Instant}; use std::{fmt::Display, mem, path::PathBuf, sync::Arc}; use ui::App; use util::ResultExt; @@ -658,6 +659,15 @@ impl PlanEntry { } } +#[derive(Debug, Clone)] +pub struct RetryStatus { + pub last_error: SharedString, + pub attempt: usize, + pub max_attempts: usize, + pub started_at: Instant, + pub duration: Duration, +} + pub struct AcpThread { title: SharedString, entries: Vec<AgentThreadEntry>, @@ -676,6 +686,7 @@ pub enum AcpThreadEvent { EntryUpdated(usize), EntriesRemoved(Range<usize>), ToolAuthorizationRequired, + Retry(RetryStatus), Stopped, Error, ServerExited(ExitStatus), @@ -916,6 +927,10 @@ impl AcpThread { cx.emit(AcpThreadEvent::NewEntry); } + pub fn update_retry_status(&mut self, status: RetryStatus, cx: &mut Context<Self>) { + cx.emit(AcpThreadEvent::Retry(status)); + } + pub fn update_tool_call( &mut self, update: impl Into<ToolCallUpdate>, diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 6347f5f9a4a606c5703cc970f6101ad0ee635462..480b2baa95d2d4e42cb6970045f85b8763ff7fe1 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -546,6 +546,11 @@ impl NativeAgentConnection { thread.update_tool_call(update, cx) })??; } + AgentResponseEvent::Retry(status) => { + acp_thread.update(cx, |thread, cx| { + thread.update_retry_status(status, cx) + })?; + } AgentResponseEvent::Stop(stop_reason) => { log::debug!("Assistant message complete: {:?}", stop_reason); return Ok(acp::PromptResponse { stop_reason }); diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 13b37fbaa2853bae6ec77e0d4c0b424c088dc6b8..c83479f2cf8e0378ef10a36662206b8ac61b2d87 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -6,15 +6,16 @@ use agent_settings::AgentProfileId; use anyhow::Result; use client::{Client, UserStore}; use fs::{FakeFs, Fs}; -use futures::channel::mpsc::UnboundedReceiver; +use futures::{StreamExt, channel::mpsc::UnboundedReceiver}; use gpui::{ App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient, }; use indoc::indoc; use language_model::{ - LanguageModel, LanguageModelCompletionEvent, LanguageModelId, LanguageModelRegistry, - LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse, MessageContent, - Role, StopReason, fake_provider::FakeLanguageModel, + LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, + LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequestMessage, + LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason, + fake_provider::FakeLanguageModel, }; use pretty_assertions::assert_eq; use project::Project; @@ -24,7 +25,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::json; use settings::SettingsStore; -use smol::stream::StreamExt; use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; use util::path; @@ -1435,6 +1435,162 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_send_no_retry_on_success(cx: &mut TestAppContext) { + let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let mut events = thread + .update(cx, |thread, cx| { + thread.set_completion_mode(agent_settings::CompletionMode::Burn); + thread.send(UserMessageId::new(), ["Hello!"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + fake_model.send_last_completion_stream_text_chunk("Hey!"); + fake_model.end_last_completion_stream(); + + let mut retry_events = Vec::new(); + while let Some(Ok(event)) = events.next().await { + match event { + AgentResponseEvent::Retry(retry_status) => { + retry_events.push(retry_status); + } + AgentResponseEvent::Stop(..) => break, + _ => {} + } + } + + assert_eq!(retry_events.len(), 0); + thread.read_with(cx, |thread, _cx| { + assert_eq!( + thread.to_markdown(), + indoc! {" + ## User + + Hello! + + ## Assistant + + Hey! + "} + ) + }); +} + +#[gpui::test] +async fn test_send_retry_on_error(cx: &mut TestAppContext) { + let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let mut events = thread + .update(cx, |thread, cx| { + thread.set_completion_mode(agent_settings::CompletionMode::Burn); + thread.send(UserMessageId::new(), ["Hello!"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded { + provider: LanguageModelProviderName::new("Anthropic"), + retry_after: Some(Duration::from_secs(3)), + }); + fake_model.end_last_completion_stream(); + + cx.executor().advance_clock(Duration::from_secs(3)); + cx.run_until_parked(); + + fake_model.send_last_completion_stream_text_chunk("Hey!"); + fake_model.end_last_completion_stream(); + + let mut retry_events = Vec::new(); + while let Some(Ok(event)) = events.next().await { + match event { + AgentResponseEvent::Retry(retry_status) => { + retry_events.push(retry_status); + } + AgentResponseEvent::Stop(..) => break, + _ => {} + } + } + + assert_eq!(retry_events.len(), 1); + assert!(matches!( + retry_events[0], + acp_thread::RetryStatus { attempt: 1, .. } + )); + thread.read_with(cx, |thread, _cx| { + assert_eq!( + thread.to_markdown(), + indoc! {" + ## User + + Hello! + + ## Assistant + + Hey! + "} + ) + }); +} + +#[gpui::test] +async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { + let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let mut events = thread + .update(cx, |thread, cx| { + thread.set_completion_mode(agent_settings::CompletionMode::Burn); + thread.send(UserMessageId::new(), ["Hello!"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + for _ in 0..crate::thread::MAX_RETRY_ATTEMPTS + 1 { + fake_model.send_last_completion_stream_error( + LanguageModelCompletionError::ServerOverloaded { + provider: LanguageModelProviderName::new("Anthropic"), + retry_after: Some(Duration::from_secs(3)), + }, + ); + fake_model.end_last_completion_stream(); + cx.executor().advance_clock(Duration::from_secs(3)); + cx.run_until_parked(); + } + + let mut errors = Vec::new(); + let mut retry_events = Vec::new(); + while let Some(event) = events.next().await { + match event { + Ok(AgentResponseEvent::Retry(retry_status)) => { + retry_events.push(retry_status); + } + Ok(AgentResponseEvent::Stop(..)) => break, + Err(error) => errors.push(error), + _ => {} + } + } + + assert_eq!( + retry_events.len(), + crate::thread::MAX_RETRY_ATTEMPTS as usize + ); + for i in 0..crate::thread::MAX_RETRY_ATTEMPTS as usize { + assert_eq!(retry_events[i].attempt, i + 1); + } + assert_eq!(errors.len(), 1); + let error = errors[0] + .downcast_ref::<LanguageModelCompletionError>() + .unwrap(); + assert!(matches!( + error, + LanguageModelCompletionError::ServerOverloaded { .. } + )); +} + /// Filters out the stop events for asserting against in tests fn stop_events(result_events: Vec<Result<AgentResponseEvent>>) -> Vec<acp::StopReason> { result_events diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 7f0465f5ce5838d4b0bccbed3962bf7ac86848cd..beb780850cadbad1726d7a9c4f742ad56e1f2015 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -12,12 +12,12 @@ use futures::{ channel::{mpsc, oneshot}, stream::FuturesUnordered, }; -use gpui::{App, Context, Entity, SharedString, Task}; +use gpui::{App, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; use language_model::{ - LanguageModel, LanguageModelCompletionEvent, LanguageModelImage, LanguageModelProviderId, - LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, - LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, - LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, + LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, + LanguageModelProviderId, LanguageModelRequest, LanguageModelRequestMessage, + LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, + LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, }; use project::Project; use prompt_store::ProjectContext; @@ -25,7 +25,12 @@ use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; use settings::{Settings, update_settings_file}; use smol::stream::StreamExt; -use std::{collections::BTreeMap, path::Path, sync::Arc}; +use std::{ + collections::BTreeMap, + path::Path, + sync::Arc, + time::{Duration, Instant}, +}; use std::{fmt::Write, ops::Range}; use util::{ResultExt, markdown::MarkdownCodeBlock}; use uuid::Uuid; @@ -71,6 +76,21 @@ impl std::fmt::Display for PromptId { } } +pub(crate) const MAX_RETRY_ATTEMPTS: u8 = 4; +pub(crate) const BASE_RETRY_DELAY: Duration = Duration::from_secs(5); + +#[derive(Debug, Clone)] +enum RetryStrategy { + ExponentialBackoff { + initial_delay: Duration, + max_attempts: u8, + }, + Fixed { + delay: Duration, + max_attempts: u8, + }, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Message { User(UserMessage), @@ -455,6 +475,7 @@ pub enum AgentResponseEvent { ToolCall(acp::ToolCall), ToolCallUpdate(acp_thread::ToolCallUpdate), ToolCallAuthorization(ToolCallAuthorization), + Retry(acp_thread::RetryStatus), Stop(acp::StopReason), } @@ -662,41 +683,18 @@ impl Thread { })??; log::info!("Calling model.stream_completion"); - let mut events = model.stream_completion(request, cx).await?; - log::debug!("Stream completion started successfully"); let mut tool_use_limit_reached = false; - let mut tool_uses = FuturesUnordered::new(); - while let Some(event) = events.next().await { - match event? { - LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::ToolUseLimitReached, - ) => { - tool_use_limit_reached = true; - } - LanguageModelCompletionEvent::Stop(reason) => { - event_stream.send_stop(reason); - if reason == StopReason::Refusal { - this.update(cx, |this, _cx| { - this.flush_pending_message(); - this.messages.truncate(message_ix); - })?; - return Ok(()); - } - } - event => { - log::trace!("Received completion event: {:?}", event); - this.update(cx, |this, cx| { - tool_uses.extend(this.handle_streamed_completion_event( - event, - &event_stream, - cx, - )); - }) - .ok(); - } - } - } + let mut tool_uses = Self::stream_completion_with_retries( + this.clone(), + model.clone(), + request, + message_ix, + &event_stream, + &mut tool_use_limit_reached, + cx, + ) + .await?; let used_tools = tool_uses.is_empty(); while let Some(tool_result) = tool_uses.next().await { @@ -754,10 +752,105 @@ impl Thread { Ok(events_rx) } + async fn stream_completion_with_retries( + this: WeakEntity<Self>, + model: Arc<dyn LanguageModel>, + request: LanguageModelRequest, + message_ix: usize, + event_stream: &AgentResponseEventStream, + tool_use_limit_reached: &mut bool, + cx: &mut AsyncApp, + ) -> Result<FuturesUnordered<Task<LanguageModelToolResult>>> { + log::debug!("Stream completion started successfully"); + + let mut attempt = None; + 'retry: loop { + let mut events = model.stream_completion(request.clone(), cx).await?; + let mut tool_uses = FuturesUnordered::new(); + while let Some(event) = events.next().await { + match event { + Ok(LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::ToolUseLimitReached, + )) => { + *tool_use_limit_reached = true; + } + Ok(LanguageModelCompletionEvent::Stop(reason)) => { + event_stream.send_stop(reason); + if reason == StopReason::Refusal { + this.update(cx, |this, _cx| { + this.flush_pending_message(); + this.messages.truncate(message_ix); + })?; + return Ok(tool_uses); + } + } + Ok(event) => { + log::trace!("Received completion event: {:?}", event); + this.update(cx, |this, cx| { + tool_uses.extend(this.handle_streamed_completion_event( + event, + event_stream, + cx, + )); + }) + .ok(); + } + Err(error) => { + let completion_mode = + this.read_with(cx, |thread, _cx| thread.completion_mode())?; + if completion_mode == CompletionMode::Normal { + return Err(error.into()); + } + + let Some(strategy) = Self::retry_strategy_for(&error) else { + return Err(error.into()); + }; + + let max_attempts = match &strategy { + RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, + RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, + }; + + let attempt = attempt.get_or_insert(0u8); + + *attempt += 1; + + let attempt = *attempt; + if attempt > max_attempts { + return Err(error.into()); + } + + let delay = match &strategy { + RetryStrategy::ExponentialBackoff { initial_delay, .. } => { + let delay_secs = + initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); + Duration::from_secs(delay_secs) + } + RetryStrategy::Fixed { delay, .. } => *delay, + }; + log::debug!("Retry attempt {attempt} with delay {delay:?}"); + + event_stream.send_retry(acp_thread::RetryStatus { + last_error: error.to_string().into(), + attempt: attempt as usize, + max_attempts: max_attempts as usize, + started_at: Instant::now(), + duration: delay, + }); + + cx.background_executor().timer(delay).await; + continue 'retry; + } + } + } + return Ok(tool_uses); + } + } + pub fn build_system_message(&self, cx: &App) -> LanguageModelRequestMessage { log::debug!("Building system message"); let prompt = SystemPromptTemplate { - project: &self.project_context.read(cx), + project: self.project_context.read(cx), available_tools: self.tools.keys().cloned().collect(), } .render(&self.templates) @@ -1158,6 +1251,113 @@ impl Thread { fn advance_prompt_id(&mut self) { self.prompt_id = PromptId::new(); } + + fn retry_strategy_for(error: &LanguageModelCompletionError) -> Option<RetryStrategy> { + use LanguageModelCompletionError::*; + use http_client::StatusCode; + + // General strategy here: + // - If retrying won't help (e.g. invalid API key or payload too large), return None so we don't retry at all. + // - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), retry up to 4 times with exponential backoff. + // - If it's an issue that *might* be fixed by retrying (e.g. internal server error), retry up to 3 times. + match error { + HttpResponseError { + status_code: StatusCode::TOO_MANY_REQUESTS, + .. + } => Some(RetryStrategy::ExponentialBackoff { + initial_delay: BASE_RETRY_DELAY, + max_attempts: MAX_RETRY_ATTEMPTS, + }), + ServerOverloaded { retry_after, .. } | RateLimitExceeded { retry_after, .. } => { + Some(RetryStrategy::Fixed { + delay: retry_after.unwrap_or(BASE_RETRY_DELAY), + max_attempts: MAX_RETRY_ATTEMPTS, + }) + } + UpstreamProviderError { + status, + retry_after, + .. + } => match *status { + StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE => { + Some(RetryStrategy::Fixed { + delay: retry_after.unwrap_or(BASE_RETRY_DELAY), + max_attempts: MAX_RETRY_ATTEMPTS, + }) + } + StatusCode::INTERNAL_SERVER_ERROR => Some(RetryStrategy::Fixed { + delay: retry_after.unwrap_or(BASE_RETRY_DELAY), + // Internal Server Error could be anything, retry up to 3 times. + max_attempts: 3, + }), + status => { + // There is no StatusCode variant for the unofficial HTTP 529 ("The service is overloaded"), + // but we frequently get them in practice. See https://http.dev/529 + if status.as_u16() == 529 { + Some(RetryStrategy::Fixed { + delay: retry_after.unwrap_or(BASE_RETRY_DELAY), + max_attempts: MAX_RETRY_ATTEMPTS, + }) + } else { + Some(RetryStrategy::Fixed { + delay: retry_after.unwrap_or(BASE_RETRY_DELAY), + max_attempts: 2, + }) + } + } + }, + ApiInternalServerError { .. } => Some(RetryStrategy::Fixed { + delay: BASE_RETRY_DELAY, + max_attempts: 3, + }), + ApiReadResponseError { .. } + | HttpSend { .. } + | DeserializeResponse { .. } + | BadRequestFormat { .. } => Some(RetryStrategy::Fixed { + delay: BASE_RETRY_DELAY, + max_attempts: 3, + }), + // Retrying these errors definitely shouldn't help. + HttpResponseError { + status_code: + StatusCode::PAYLOAD_TOO_LARGE | StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED, + .. + } + | AuthenticationError { .. } + | PermissionError { .. } + | NoApiKey { .. } + | ApiEndpointNotFound { .. } + | PromptTooLarge { .. } => None, + // These errors might be transient, so retry them + SerializeRequest { .. } | BuildRequestBody { .. } => Some(RetryStrategy::Fixed { + delay: BASE_RETRY_DELAY, + max_attempts: 1, + }), + // Retry all other 4xx and 5xx errors once. + HttpResponseError { status_code, .. } + if status_code.is_client_error() || status_code.is_server_error() => + { + Some(RetryStrategy::Fixed { + delay: BASE_RETRY_DELAY, + max_attempts: 3, + }) + } + Other(err) + if err.is::<language_model::PaymentRequiredError>() + || err.is::<language_model::ModelRequestLimitReachedError>() => + { + // Retrying won't help for Payment Required or Model Request Limit errors (where + // the user must upgrade to usage-based billing to get more requests, or else wait + // for a significant amount of time for the request limit to reset). + None + } + // Conservatively assume that any other errors are non-retryable + HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed { + delay: BASE_RETRY_DELAY, + max_attempts: 2, + }), + } + } } struct RunningTurn { @@ -1367,6 +1567,12 @@ impl AgentResponseEventStream { .ok(); } + fn send_retry(&self, status: acp_thread::RetryStatus) { + self.0 + .unbounded_send(Ok(AgentResponseEvent::Retry(status))) + .ok(); + } + fn send_stop(&self, reason: StopReason) { match reason { StopReason::EndTurn => { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2fffe1b1796cb9e4bb5bdb6d4e1b7f8952e5fce5..370dae53e4f3037e9162407dfd4c3ec3bf727183 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,7 +1,7 @@ use acp_thread::{ AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, - AuthRequired, LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, - UserMessageId, + AuthRequired, LoadError, MentionUri, RetryStatus, ThreadStatus, ToolCall, ToolCallContent, + ToolCallStatus, UserMessageId, }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; @@ -35,6 +35,7 @@ use prompt_store::PromptId; use rope::Point; use settings::{Settings as _, SettingsStore}; use std::sync::Arc; +use std::time::Instant; use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration}; use text::Anchor; use theme::ThemeSettings; @@ -115,6 +116,7 @@ pub struct AcpThreadView { profile_selector: Option<Entity<ProfileSelector>>, notifications: Vec<WindowHandle<AgentNotification>>, notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>, + thread_retry_status: Option<RetryStatus>, thread_error: Option<ThreadError>, list_state: ListState, scrollbar_state: ScrollbarState, @@ -209,6 +211,7 @@ impl AcpThreadView { notification_subscriptions: HashMap::default(), list_state: list_state.clone(), scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), + thread_retry_status: None, thread_error: None, auth_task: None, expanded_tool_calls: HashSet::default(), @@ -445,6 +448,7 @@ impl AcpThreadView { pub fn cancel_generation(&mut self, cx: &mut Context<Self>) { self.thread_error.take(); + self.thread_retry_status.take(); if let Some(thread) = self.thread() { self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx))); @@ -775,7 +779,11 @@ impl AcpThreadView { AcpThreadEvent::ToolAuthorizationRequired => { self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx); } + AcpThreadEvent::Retry(retry) => { + self.thread_retry_status = Some(retry.clone()); + } AcpThreadEvent::Stopped => { + self.thread_retry_status.take(); let used_tools = thread.read(cx).used_tools_since_last_user_message(); self.notify_with_sound( if used_tools { @@ -789,6 +797,7 @@ impl AcpThreadView { ); } AcpThreadEvent::Error => { + self.thread_retry_status.take(); self.notify_with_sound( "Agent stopped due to an error", IconName::Warning, @@ -797,6 +806,7 @@ impl AcpThreadView { ); } AcpThreadEvent::ServerExited(status) => { + self.thread_retry_status.take(); self.thread_state = ThreadState::ServerExited { status: *status }; } } @@ -3413,7 +3423,51 @@ impl AcpThreadView { }) } - fn render_thread_error(&self, window: &mut Window, cx: &mut Context<'_, Self>) -> Option<Div> { + fn render_thread_retry_status_callout( + &self, + _window: &mut Window, + _cx: &mut Context<Self>, + ) -> Option<Callout> { + let state = self.thread_retry_status.as_ref()?; + + let next_attempt_in = state + .duration + .saturating_sub(Instant::now().saturating_duration_since(state.started_at)); + if next_attempt_in.is_zero() { + return None; + } + + let next_attempt_in_secs = next_attempt_in.as_secs() + 1; + + let retry_message = if state.max_attempts == 1 { + if next_attempt_in_secs == 1 { + "Retrying. Next attempt in 1 second.".to_string() + } else { + format!("Retrying. Next attempt in {next_attempt_in_secs} seconds.") + } + } else { + if next_attempt_in_secs == 1 { + format!( + "Retrying. Next attempt in 1 second (Attempt {} of {}).", + state.attempt, state.max_attempts, + ) + } else { + format!( + "Retrying. Next attempt in {next_attempt_in_secs} seconds (Attempt {} of {}).", + state.attempt, state.max_attempts, + ) + } + }; + + Some( + Callout::new() + .severity(Severity::Warning) + .title(state.last_error.clone()) + .description(retry_message), + ) + } + + fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> { let content = match self.thread_error.as_ref()? { ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx), ThreadError::PaymentRequired => self.render_payment_required_error(cx), @@ -3678,6 +3732,7 @@ impl Render for AcpThreadView { } _ => this, }) + .children(self.render_thread_retry_status_callout(window, cx)) .children(self.render_thread_error(window, cx)) .child(self.render_message_editor(window, cx)) } diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 3522a0c9ab7d5e140860b87b926d94ee42f65b5d..b0b06583a479a48c8ad2afda51b45b3789bd3595 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1523,6 +1523,7 @@ impl AgentDiff { AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::Stopped | AcpThreadEvent::ToolAuthorizationRequired + | AcpThreadEvent::Retry(_) | AcpThreadEvent::Error | AcpThreadEvent::ServerExited(_) => {} } diff --git a/crates/language_model/src/fake_provider.rs b/crates/language_model/src/fake_provider.rs index 67fba4488700a637b643294fb99aa905b41f7480..ebfd37d16cf622a622047b7f5babedebd541ad57 100644 --- a/crates/language_model/src/fake_provider.rs +++ b/crates/language_model/src/fake_provider.rs @@ -4,10 +4,11 @@ use crate::{ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, }; -use futures::{FutureExt, StreamExt, channel::mpsc, future::BoxFuture, stream::BoxStream}; +use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream}; use gpui::{AnyView, App, AsyncApp, Entity, Task, Window}; use http_client::Result; use parking_lot::Mutex; +use smol::stream::StreamExt; use std::sync::Arc; #[derive(Clone)] @@ -100,7 +101,9 @@ pub struct FakeLanguageModel { current_completion_txs: Mutex< Vec<( LanguageModelRequest, - mpsc::UnboundedSender<LanguageModelCompletionEvent>, + mpsc::UnboundedSender< + Result<LanguageModelCompletionEvent, LanguageModelCompletionError>, + >, )>, >, } @@ -150,7 +153,21 @@ impl FakeLanguageModel { .find(|(req, _)| req == request) .map(|(_, tx)| tx) .unwrap(); - tx.unbounded_send(event.into()).unwrap(); + tx.unbounded_send(Ok(event.into())).unwrap(); + } + + pub fn send_completion_stream_error( + &self, + request: &LanguageModelRequest, + error: impl Into<LanguageModelCompletionError>, + ) { + let current_completion_txs = self.current_completion_txs.lock(); + let tx = current_completion_txs + .iter() + .find(|(req, _)| req == request) + .map(|(_, tx)| tx) + .unwrap(); + tx.unbounded_send(Err(error.into())).unwrap(); } pub fn end_completion_stream(&self, request: &LanguageModelRequest) { @@ -170,6 +187,13 @@ impl FakeLanguageModel { self.send_completion_stream_event(self.pending_completions().last().unwrap(), event); } + pub fn send_last_completion_stream_error( + &self, + error: impl Into<LanguageModelCompletionError>, + ) { + self.send_completion_stream_error(self.pending_completions().last().unwrap(), error); + } + pub fn end_last_completion_stream(&self) { self.end_completion_stream(self.pending_completions().last().unwrap()); } @@ -229,7 +253,7 @@ impl LanguageModel for FakeLanguageModel { > { let (tx, rx) = mpsc::unbounded(); self.current_completion_txs.lock().push((request, tx)); - async move { Ok(rx.map(Ok).boxed()) }.boxed() + async move { Ok(rx.boxed()) }.boxed() } fn as_fake(&self) -> &Self { From 5df9c7c1c22f60cf48abbc9bb7b8519481923ed7 Mon Sep 17 00:00:00 2001 From: Lukas Wirth <lukas@zed.dev> Date: Tue, 19 Aug 2025 12:16:49 +0200 Subject: [PATCH 132/744] search: Fix project search query flickering (#36470) Release Notes: - N/A Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com> --- crates/search/src/project_search.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 056c3556ba0442b9a12b2f06192b4f5c2a4e3213..443bbb0427cab3a9b47c457fce25e5dfde7f3b53 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1925,13 +1925,15 @@ impl Render for ProjectSearchBar { let limit_reached = project_search.limit_reached; let color_override = match ( + &project_search.pending_search, project_search.no_results, &project_search.active_query, &project_search.last_search_query_text, ) { - (Some(true), Some(q), Some(p)) if q.as_str() == p => Some(Color::Error), + (None, Some(true), Some(q), Some(p)) if q.as_str() == p => Some(Color::Error), _ => None, }; + let match_text = search .active_match_index .and_then(|index| { From 97a31c59c99781e33143321849e7613c62acd482 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Tue, 19 Aug 2025 12:22:17 +0200 Subject: [PATCH 133/744] agent2: Fix agent location still being present after thread stopped (#36471) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 2 ++ crates/agent_ui/src/agent_diff.rs | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 916f48cbe049faf00626ece2480e9b17f4b98f54..b86696d437a2b9b10647d5cf83b8dd03de826a6c 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1282,6 +1282,8 @@ impl AcpThread { .await?; this.update(cx, |this, cx| { + this.project + .update(cx, |project, cx| project.set_agent_location(None, cx)); match response { Ok(Err(e)) => { this.send_task.take(); diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index b0b06583a479a48c8ad2afda51b45b3789bd3595..b010f8a424fb75790d7cd913699a21ef0ae332ee 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1520,12 +1520,12 @@ impl AgentDiff { self.update_reviewing_editors(workspace, window, cx); } } + AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::ServerExited(_) => { + self.update_reviewing_editors(workspace, window, cx); + } AcpThreadEvent::EntriesRemoved(_) - | AcpThreadEvent::Stopped | AcpThreadEvent::ToolAuthorizationRequired - | AcpThreadEvent::Retry(_) - | AcpThreadEvent::Error - | AcpThreadEvent::ServerExited(_) => {} + | AcpThreadEvent::Retry(_) => {} } } From 790a2a0cfa603b0fcf1ddff29eab9434fcdc1e65 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Tue, 19 Aug 2025 12:40:02 +0200 Subject: [PATCH 134/744] agent2: Support `preferred_completion_mode` setting (#36473) Release Notes: - N/A --- crates/agent2/src/thread.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index beb780850cadbad1726d7a9c4f742ad56e1f2015..f0b5d2f08acfaac9a84a9bbd9c34c9a13954b185 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -522,7 +522,7 @@ impl Thread { id: ThreadId::new(), prompt_id: PromptId::new(), messages: Vec::new(), - completion_mode: CompletionMode::Normal, + completion_mode: AgentSettings::get_global(cx).preferred_completion_mode, running_turn: None, pending_message: None, tools: BTreeMap::default(), From e6d5a6a4fdb0ebcdfdc6c1f903cf98469934dcce Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Tue, 19 Aug 2025 12:59:34 +0200 Subject: [PATCH 135/744] agent: Remove `thread-auto-capture` feature (#36474) We never ended up using this in practice (the feature flag is not enabled for anyone, not even staff) Release Notes: - N/A --- Cargo.lock | 1 - crates/agent/Cargo.toml | 1 - crates/agent/src/thread.rs | 55 ----------------------- crates/feature_flags/src/feature_flags.rs | 8 ---- 4 files changed, 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ef91c79c936578a4cc06abf107e385d125f236c..d7edc54257b2248feda0c642d2afede7dd3961e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,7 +131,6 @@ dependencies = [ "component", "context_server", "convert_case 0.8.0", - "feature_flags", "fs", "futures 0.3.31", "git", diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 53ad2f496758bfc288a5c9dc25f8e2e99851d5b2..391abb38fe826923b06646511c0dd6c5ce5c6ca4 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -31,7 +31,6 @@ collections.workspace = true component.workspace = true context_server.workspace = true convert_case.workspace = true -feature_flags.workspace = true fs.workspace = true futures.workspace = true git.workspace = true diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 469135a967ab12199486c5014dc1ce3676fe7b78..a3f903a60d23cb1e17543419a3da72b082b595ea 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -16,7 +16,6 @@ use chrono::{DateTime, Utc}; use client::{ModelRequestUsage, RequestUsage}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit}; use collections::HashMap; -use feature_flags::{self, FeatureFlagAppExt}; use futures::{FutureExt, StreamExt as _, future::Shared}; use git::repository::DiffType; use gpui::{ @@ -388,7 +387,6 @@ pub struct Thread { feedback: Option<ThreadFeedback>, retry_state: Option<RetryState>, message_feedback: HashMap<MessageId, ThreadFeedback>, - last_auto_capture_at: Option<Instant>, last_received_chunk_at: Option<Instant>, request_callback: Option< Box<dyn FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>])>, @@ -489,7 +487,6 @@ impl Thread { feedback: None, retry_state: None, message_feedback: HashMap::default(), - last_auto_capture_at: None, last_error_context: None, last_received_chunk_at: None, request_callback: None, @@ -614,7 +611,6 @@ impl Thread { tool_use_limit_reached: serialized.tool_use_limit_reached, feedback: None, message_feedback: HashMap::default(), - last_auto_capture_at: None, last_error_context: None, last_received_chunk_at: None, request_callback: None, @@ -1033,8 +1029,6 @@ impl Thread { }); } - self.auto_capture_telemetry(cx); - message_id } @@ -1906,7 +1900,6 @@ impl Thread { cx.emit(ThreadEvent::StreamedCompletion); cx.notify(); - thread.auto_capture_telemetry(cx); Ok(()) })??; @@ -2081,8 +2074,6 @@ impl Thread { request_callback(request, response_events); } - thread.auto_capture_telemetry(cx); - if let Ok(initial_usage) = initial_token_usage { let usage = thread.cumulative_token_usage - initial_usage; @@ -2536,7 +2527,6 @@ impl Thread { model: Arc<dyn LanguageModel>, cx: &mut Context<Self>, ) -> Vec<PendingToolUse> { - self.auto_capture_telemetry(cx); let request = Arc::new(self.to_completion_request(model.clone(), CompletionIntent::ToolResults, cx)); let pending_tool_uses = self @@ -2745,7 +2735,6 @@ impl Thread { if !canceled { self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx); } - self.auto_capture_telemetry(cx); } } @@ -3147,50 +3136,6 @@ impl Thread { &self.project } - pub fn auto_capture_telemetry(&mut self, cx: &mut Context<Self>) { - if !cx.has_flag::<feature_flags::ThreadAutoCaptureFeatureFlag>() { - return; - } - - let now = Instant::now(); - if let Some(last) = self.last_auto_capture_at { - if now.duration_since(last).as_secs() < 10 { - return; - } - } - - self.last_auto_capture_at = Some(now); - - let thread_id = self.id().clone(); - let github_login = self - .project - .read(cx) - .user_store() - .read(cx) - .current_user() - .map(|user| user.github_login.clone()); - let client = self.project.read(cx).client(); - let serialize_task = self.serialize(cx); - - cx.background_executor() - .spawn(async move { - if let Ok(serialized_thread) = serialize_task.await { - if let Ok(thread_data) = serde_json::to_value(serialized_thread) { - telemetry::event!( - "Agent Thread Auto-Captured", - thread_id = thread_id.to_string(), - thread_data = thread_data, - auto_capture_reason = "tracked_user", - github_login = github_login - ); - - client.telemetry().flush_events().await; - } - } - }) - .detach(); - } - pub fn cumulative_token_usage(&self) -> TokenUsage { self.cumulative_token_usage } diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index ef357adf35997bfb7560f1e1849ef69e780cd1f9..f87932bfaf99411120de61edf95535ea46e1117c 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -77,14 +77,6 @@ impl FeatureFlag for NotebookFeatureFlag { const NAME: &'static str = "notebooks"; } -pub struct ThreadAutoCaptureFeatureFlag {} -impl FeatureFlag for ThreadAutoCaptureFeatureFlag { - const NAME: &'static str = "thread-auto-capture"; - - fn enabled_for_staff() -> bool { - false - } -} pub struct PanicFeatureFlag; impl FeatureFlag for PanicFeatureFlag { From 2fb89c9b3eb0f76f57f179a3cc4f0b37f2007b42 Mon Sep 17 00:00:00 2001 From: Vincent Durewski <vincent.durewski@gmail.com> Date: Tue, 19 Aug 2025 13:08:10 +0200 Subject: [PATCH 136/744] chore: Default settings: Comments: dock option (#36476) Minor tweak in the wording of the comments for the default settings regarding the `dock` option of the panels, in order to make them congruent across all panels. Release Notes: - N/A --- assets/settings/default.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 72e4dcbf4f79230d3d906d4b41944ce95a40656d..c290baf0038d1b731f041e9c828746758bf9ffe3 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -717,7 +717,7 @@ // Can be 'never', 'always', or 'when_in_call', // or a boolean (interpreted as 'never'/'always'). "button": "when_in_call", - // Where to the chat panel. Can be 'left' or 'right'. + // Where to dock the chat panel. Can be 'left' or 'right'. "dock": "right", // Default width of the chat panel. "default_width": 240 @@ -725,7 +725,7 @@ "git_panel": { // Whether to show the git panel button in the status bar. "button": true, - // Where to show the git panel. Can be 'left' or 'right'. + // Where to dock the git panel. Can be 'left' or 'right'. "dock": "left", // Default width of the git panel. "default_width": 360, From 9e8ec72bd5c697edc6b61f4e18542afc4e343a1b Mon Sep 17 00:00:00 2001 From: Lukas Wirth <lukas@zed.dev> Date: Tue, 19 Aug 2025 14:32:26 +0200 Subject: [PATCH 137/744] Revert "project: Handle `textDocument/didSave` and `textDocument/didChange` (un)registration and usage correctly (#36441)" (#36480) This reverts commit c5991e74bb6f305c299684dc7ac3f6ee9055efcd. This PR broke rust-analyzer's check on save function, so reverting for now Release Notes: - N/A --- crates/lsp/src/lsp.rs | 2 +- crates/project/src/lsp_store.rs | 72 ++++++++------------------------- 2 files changed, 17 insertions(+), 57 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index ce9e2fe229c0aded6fac31c260e334445f987f03..366005a4abf3f984f6e68d527da3fcf10da4f6cf 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -827,7 +827,7 @@ impl LanguageServer { }), synchronization: Some(TextDocumentSyncClientCapabilities { did_save: Some(true), - dynamic_registration: Some(true), + dynamic_registration: Some(false), ..TextDocumentSyncClientCapabilities::default() }), code_lens: Some(CodeLensClientCapabilities { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 1bc6770d4e824c4dec6f1f28766749570ec1794f..9410eea74258b16ccfc9c8b19bb92580a32a1670 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11820,28 +11820,8 @@ impl LspStore { .transpose()? { server.update_capabilities(|capabilities| { - let mut sync_options = - Self::take_text_document_sync_options(capabilities); - sync_options.change = Some(sync_kind); capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/didSave" => { - if let Some(save_options) = reg - .register_options - .and_then(|opts| opts.get("includeText").cloned()) - .map(serde_json::from_value::<lsp::TextDocumentSyncSaveOptions>) - .transpose()? - { - server.update_capabilities(|capabilities| { - let mut sync_options = - Self::take_text_document_sync_options(capabilities); - sync_options.save = Some(save_options); - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)); }); notify_server_capabilities_updated(&server, cx); } @@ -11993,19 +11973,7 @@ impl LspStore { } "textDocument/didChange" => { server.update_capabilities(|capabilities| { - let mut sync_options = Self::take_text_document_sync_options(capabilities); - sync_options.change = None; - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/didSave" => { - server.update_capabilities(|capabilities| { - let mut sync_options = Self::take_text_document_sync_options(capabilities); - sync_options.save = None; - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + capabilities.text_document_sync = None; }); notify_server_capabilities_updated(&server, cx); } @@ -12033,20 +12001,6 @@ impl LspStore { Ok(()) } - - fn take_text_document_sync_options( - capabilities: &mut lsp::ServerCapabilities, - ) -> lsp::TextDocumentSyncOptions { - match capabilities.text_document_sync.take() { - Some(lsp::TextDocumentSyncCapability::Options(sync_options)) => sync_options, - Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)) => { - let mut sync_options = lsp::TextDocumentSyncOptions::default(); - sync_options.change = Some(sync_kind); - sync_options - } - None => lsp::TextDocumentSyncOptions::default(), - } - } } // Registration with empty capabilities should be ignored. @@ -13149,18 +13103,24 @@ async fn populate_labels_for_symbols( fn include_text(server: &lsp::LanguageServer) -> Option<bool> { match server.capabilities().text_document_sync.as_ref()? { - lsp::TextDocumentSyncCapability::Options(opts) => match opts.save.as_ref()? { - // Server wants didSave but didn't specify includeText. - lsp::TextDocumentSyncSaveOptions::Supported(true) => Some(false), - // Server doesn't want didSave at all. - lsp::TextDocumentSyncSaveOptions::Supported(false) => None, - // Server provided SaveOptions. + lsp::TextDocumentSyncCapability::Kind(kind) => match *kind { + lsp::TextDocumentSyncKind::NONE => None, + lsp::TextDocumentSyncKind::FULL => Some(true), + lsp::TextDocumentSyncKind::INCREMENTAL => Some(false), + _ => None, + }, + lsp::TextDocumentSyncCapability::Options(options) => match options.save.as_ref()? { + lsp::TextDocumentSyncSaveOptions::Supported(supported) => { + if *supported { + Some(true) + } else { + None + } + } lsp::TextDocumentSyncSaveOptions::SaveOptions(save_options) => { Some(save_options.include_text.unwrap_or(false)) } }, - // We do not have any save info. Kind affects didChange only. - lsp::TextDocumentSyncCapability::Kind(_) => None, } } From 8f567383e4bef1914c2e349fc8e984cfa5aae397 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:27:24 +0200 Subject: [PATCH 138/744] Auto-fix clippy::collapsible_if violations (#36428) Release Notes: - N/A --- Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 25 +- crates/action_log/src/action_log.rs | 8 +- .../src/activity_indicator.rs | 65 +- crates/agent/src/context.rs | 36 +- crates/agent/src/context_store.rs | 8 +- crates/agent/src/thread.rs | 49 +- crates/agent/src/thread_store.rs | 62 +- crates/agent/src/tool_use.rs | 20 +- crates/agent2/src/thread.rs | 12 +- crates/agent2/src/tools/edit_file_tool.rs | 14 +- crates/agent2/src/tools/grep_tool.rs | 10 +- crates/agent_servers/src/claude.rs | 16 +- crates/agent_settings/src/agent_settings.rs | 16 +- crates/agent_ui/src/acp/thread_view.rs | 170 ++-- crates/agent_ui/src/active_thread.rs | 136 ++- .../configure_context_server_modal.rs | 32 +- .../src/agent_configuration/tool_picker.rs | 8 +- crates/agent_ui/src/agent_diff.rs | 68 +- crates/agent_ui/src/agent_panel.rs | 13 +- crates/agent_ui/src/buffer_codegen.rs | 68 +- crates/agent_ui/src/context_strip.rs | 8 +- crates/agent_ui/src/inline_assistant.rs | 113 ++- .../agent_ui/src/terminal_inline_assistant.rs | 59 +- crates/agent_ui/src/text_thread_editor.rs | 39 +- crates/agent_ui/src/thread_history.rs | 9 +- .../src/assistant_context.rs | 122 ++- crates/assistant_context/src/context_store.rs | 47 +- .../src/context_server_command.rs | 8 +- .../src/delta_command.rs | 69 +- .../src/diagnostics_command.rs | 8 +- .../src/tab_command.rs | 16 +- crates/assistant_tool/src/tool_schema.rs | 38 +- crates/assistant_tools/src/edit_agent.rs | 45 +- .../assistant_tools/src/edit_agent/evals.rs | 16 +- crates/assistant_tools/src/edit_file_tool.rs | 16 +- crates/assistant_tools/src/grep_tool.rs | 10 +- crates/assistant_tools/src/schema.rs | 11 +- crates/auto_update_helper/src/dialog.rs | 10 +- crates/breadcrumbs/src/breadcrumbs.rs | 11 +- crates/buffer_diff/src/buffer_diff.rs | 29 +- crates/call/src/call_impl/room.rs | 62 +- crates/channel/src/channel_buffer.rs | 11 +- crates/channel/src/channel_chat.rs | 121 ++- crates/channel/src/channel_store.rs | 78 +- crates/cli/src/main.rs | 9 +- crates/client/src/client.rs | 29 +- crates/client/src/user.rs | 8 +- crates/collab/src/api/events.rs | 160 ++-- crates/collab/src/api/extensions.rs | 14 +- crates/collab/src/auth.rs | 38 +- crates/collab/src/db/queries/extensions.rs | 16 +- crates/collab/src/db/queries/projects.rs | 8 +- crates/collab/src/rpc.rs | 93 +- .../random_project_collaboration_tests.rs | 5 +- .../src/tests/randomized_test_helpers.rs | 30 +- crates/collab/src/user_backfiller.rs | 22 +- crates/collab_ui/src/channel_view.rs | 80 +- crates/collab_ui/src/chat_panel.rs | 83 +- .../src/chat_panel/message_editor.rs | 65 +- crates/collab_ui/src/collab_panel.rs | 181 ++-- crates/collab_ui/src/notification_panel.rs | 76 +- crates/context_server/src/client.rs | 8 +- crates/copilot/src/copilot.rs | 121 ++- crates/dap_adapters/src/javascript.rs | 8 +- crates/debugger_tools/src/dap_log.rs | 10 +- crates/debugger_ui/src/debugger_panel.rs | 13 +- crates/debugger_ui/src/new_process_modal.rs | 8 +- .../src/session/running/breakpoint_list.rs | 40 +- .../src/session/running/console.rs | 17 +- crates/diagnostics/src/diagnostics.rs | 38 +- crates/editor/src/display_map.rs | 36 +- crates/editor/src/display_map/block_map.rs | 146 ++-- crates/editor/src/display_map/fold_map.rs | 60 +- crates/editor/src/display_map/inlay_map.rs | 10 +- crates/editor/src/display_map/wrap_map.rs | 94 +- crates/editor/src/editor.rs | 822 +++++++++--------- crates/editor/src/element.rs | 261 +++--- crates/editor/src/git/blame.rs | 31 +- crates/editor/src/hover_links.rs | 41 +- crates/editor/src/hover_popover.rs | 135 ++- crates/editor/src/indent_guides.rs | 10 +- crates/editor/src/inlay_hint_cache.rs | 121 ++- crates/editor/src/items.rs | 61 +- crates/editor/src/jsx_tag_auto_close.rs | 17 +- crates/editor/src/lsp_ext.rs | 19 +- crates/editor/src/movement.rs | 50 +- crates/editor/src/rust_analyzer_ext.rs | 10 +- crates/editor/src/scroll.rs | 24 +- crates/editor/src/scroll/autoscroll.rs | 12 +- crates/editor/src/test.rs | 8 +- crates/eval/src/eval.rs | 5 +- crates/eval/src/explorer.rs | 28 +- crates/eval/src/instance.rs | 5 +- crates/extension/src/extension.rs | 17 +- crates/extension_host/src/extension_host.rs | 71 +- crates/extension_host/src/wasm_host.rs | 17 +- crates/extensions_ui/src/extensions_ui.rs | 15 +- crates/file_finder/src/file_finder.rs | 401 +++++---- crates/file_finder/src/open_path_prompt.rs | 20 +- crates/fs/src/fs.rs | 82 +- crates/fs/src/mac_watcher.rs | 5 +- crates/fsevent/src/fsevent.rs | 65 +- crates/git/src/blame.rs | 12 +- crates/git/src/repository.rs | 16 +- .../src/git_hosting_providers.rs | 8 +- crates/git_ui/src/commit_modal.rs | 19 +- crates/git_ui/src/git_panel.rs | 42 +- crates/git_ui/src/project_diff.rs | 8 +- crates/go_to_line/src/cursor_position.rs | 16 +- crates/go_to_line/src/go_to_line.rs | 10 +- crates/google_ai/src/google_ai.rs | 5 +- crates/gpui/build.rs | 15 +- crates/gpui/examples/input.rs | 8 +- crates/gpui/src/app.rs | 40 +- crates/gpui/src/app/context.rs | 20 +- crates/gpui/src/element.rs | 6 +- crates/gpui/src/elements/div.rs | 233 +++-- crates/gpui/src/elements/image_cache.rs | 8 +- crates/gpui/src/elements/img.rs | 13 +- crates/gpui/src/elements/list.rs | 74 +- crates/gpui/src/elements/text.rs | 32 +- crates/gpui/src/keymap/binding.rs | 8 +- .../gpui/src/platform/blade/blade_renderer.rs | 30 +- .../gpui/src/platform/linux/wayland/client.rs | 52 +- .../gpui/src/platform/linux/wayland/cursor.rs | 9 +- .../gpui/src/platform/linux/wayland/window.rs | 27 +- crates/gpui/src/platform/linux/x11/client.rs | 29 +- .../gpui/src/platform/linux/x11/clipboard.rs | 38 +- crates/gpui/src/platform/linux/x11/window.rs | 49 +- crates/gpui/src/platform/mac/open_type.rs | 16 +- crates/gpui/src/platform/mac/platform.rs | 27 +- crates/gpui/src/platform/mac/window.rs | 41 +- crates/gpui/src/platform/test/dispatcher.rs | 10 +- crates/gpui/src/platform/test/platform.rs | 8 +- crates/gpui/src/platform/windows/events.rs | 75 +- crates/gpui/src/platform/windows/platform.rs | 16 +- crates/gpui/src/text_system.rs | 43 +- crates/gpui/src/text_system/line.rs | 24 +- crates/gpui/src/text_system/line_layout.rs | 8 +- crates/gpui/src/view.rs | 31 +- crates/gpui/src/window.rs | 68 +- .../src/derive_inspector_reflection.rs | 18 +- crates/gpui_macros/src/test.rs | 116 +-- crates/html_to_markdown/src/markdown.rs | 17 +- crates/http_client/src/github.rs | 8 +- crates/journal/src/journal.rs | 32 +- crates/language/src/buffer.rs | 215 +++-- crates/language/src/language.rs | 33 +- crates/language/src/syntax_map.rs | 104 +-- crates/language/src/text_diff.rs | 10 +- crates/language_model/src/request.rs | 41 +- .../language_models/src/provider/anthropic.rs | 10 +- .../language_models/src/provider/bedrock.rs | 18 +- crates/language_models/src/provider/cloud.rs | 52 +- .../src/active_buffer_language.rs | 8 +- crates/language_tools/src/lsp_log.rs | 116 ++- crates/language_tools/src/syntax_tree_view.rs | 23 +- crates/languages/src/go.rs | 24 +- crates/languages/src/lib.rs | 7 +- crates/languages/src/python.rs | 96 +- crates/languages/src/rust.rs | 8 +- crates/languages/src/typescript.rs | 8 +- crates/livekit_client/src/test.rs | 16 +- crates/markdown/src/markdown.rs | 71 +- .../markdown_preview/src/markdown_parser.rs | 21 +- .../src/markdown_preview_view.rs | 52 +- .../src/migrations/m_2025_06_16/settings.rs | 28 +- .../src/migrations/m_2025_06_25/settings.rs | 16 +- .../src/migrations/m_2025_06_27/settings.rs | 25 +- crates/multi_buffer/src/anchor.rs | 113 ++- crates/multi_buffer/src/multi_buffer.rs | 475 +++++----- crates/multi_buffer/src/multi_buffer_tests.rs | 32 +- .../notifications/src/notification_store.rs | 53 +- crates/open_router/src/open_router.rs | 8 +- crates/outline_panel/src/outline_panel.rs | 238 +++-- crates/prettier/src/prettier.rs | 38 +- crates/project/src/buffer_store.rs | 32 +- .../project/src/debugger/breakpoint_store.rs | 11 +- crates/project/src/debugger/memory.rs | 13 +- crates/project/src/git_store.rs | 152 ++-- crates/project/src/git_store/git_traversal.rs | 10 +- crates/project/src/lsp_command.rs | 26 +- crates/project/src/lsp_store.rs | 392 ++++----- crates/project/src/manifest_tree/path_trie.rs | 10 +- crates/project/src/prettier_store.rs | 21 +- crates/project/src/project.rs | 147 ++-- crates/project/src/search.rs | 16 +- crates/project/src/search_history.rs | 23 +- crates/project/src/terminals.rs | 52 +- crates/project_panel/src/project_panel.rs | 491 +++++------ crates/prompt_store/src/prompts.rs | 19 +- crates/proto/src/error.rs | 8 +- crates/recent_projects/src/remote_servers.rs | 8 +- .../src/derive_refineable.rs | 12 +- crates/remote/src/ssh_session.rs | 41 +- crates/remote_server/src/unix.rs | 14 +- crates/repl/src/kernels/native_kernel.rs | 24 +- crates/repl/src/outputs.rs | 39 +- crates/repl/src/repl_editor.rs | 8 +- crates/repl/src/repl_sessions_ui.rs | 25 +- crates/repl/src/repl_store.rs | 8 +- crates/reqwest_client/src/reqwest_client.rs | 11 +- crates/rich_text/src/rich_text.rs | 21 +- crates/rope/src/rope.rs | 43 +- crates/rpc/src/notification.rs | 8 +- crates/rpc/src/peer.rs | 8 +- crates/rules_library/src/rules_library.rs | 34 +- crates/search/src/buffer_search.rs | 180 ++-- crates/search/src/project_search.rs | 45 +- crates/semantic_index/src/embedding_index.rs | 10 +- crates/semantic_index/src/project_index.rs | 8 +- crates/semantic_index/src/semantic_index.rs | 15 +- crates/semantic_index/src/summary_index.rs | 20 +- crates/session/src/session.rs | 10 +- crates/settings/src/keymap_file.rs | 48 +- crates/settings/src/settings_file.rs | 27 +- crates/settings/src/settings_json.rs | 17 +- crates/settings/src/settings_store.rs | 109 ++- crates/snippets_ui/src/snippets_ui.rs | 13 +- crates/sqlez/src/connection.rs | 63 +- crates/sum_tree/src/cursor.rs | 8 +- crates/sum_tree/src/sum_tree.rs | 30 +- crates/svg_preview/src/svg_preview_view.rs | 160 ++-- crates/task/src/debug_format.rs | 9 +- crates/task/src/vscode_debug_format.rs | 12 +- crates/tasks_ui/src/tasks_ui.rs | 17 +- crates/terminal/src/terminal.rs | 36 +- crates/terminal/src/terminal_settings.rs | 8 +- crates/terminal_view/src/terminal_element.rs | 29 +- crates/terminal_view/src/terminal_panel.rs | 23 +- crates/terminal_view/src/terminal_view.rs | 80 +- crates/text/src/text.rs | 26 +- crates/theme/src/fallback_themes.rs | 8 +- crates/title_bar/src/application_menu.rs | 43 +- crates/title_bar/src/title_bar.rs | 10 +- .../src/active_toolchain.rs | 47 +- .../src/toolchain_selector.rs | 9 +- .../src/components/label/highlighted_label.rs | 12 +- crates/ui/src/components/popover_menu.rs | 32 +- crates/ui/src/components/right_click_menu.rs | 9 +- crates/util/src/fs.rs | 27 +- crates/util/src/schemars.rs | 13 +- crates/util/src/util.rs | 8 +- crates/vim/src/command.rs | 55 +- crates/vim/src/digraph.rs | 16 +- crates/vim/src/motion.rs | 24 +- crates/vim/src/normal/delete.rs | 8 +- crates/vim/src/normal/mark.rs | 6 +- crates/vim/src/normal/repeat.rs | 24 +- crates/vim/src/object.rs | 56 +- crates/vim/src/state.rs | 71 +- crates/vim/src/surrounds.rs | 22 +- crates/vim/src/test/neovim_connection.rs | 9 +- crates/vim/src/vim.rs | 54 +- crates/workspace/src/dock.rs | 41 +- crates/workspace/src/history_manager.rs | 10 +- crates/workspace/src/item.rs | 26 +- crates/workspace/src/modal_layer.rs | 8 +- crates/workspace/src/pane.rs | 133 ++- crates/workspace/src/pane_group.rs | 55 +- crates/workspace/src/workspace.rs | 434 +++++---- crates/workspace/src/workspace_settings.rs | 28 +- crates/worktree/src/worktree.rs | 126 ++- crates/zed/build.rs | 26 +- crates/zed/src/main.rs | 113 ++- crates/zed/src/reliability.rs | 43 +- crates/zed/src/zed.rs | 73 +- crates/zed/src/zed/component_preview.rs | 99 +-- .../zed/src/zed/edit_prediction_registry.rs | 42 +- crates/zed/src/zed/mac_only_instance.rs | 23 +- crates/zed/src/zed/open_listener.rs | 65 +- crates/zeta/src/rate_completion_modal.rs | 12 +- crates/zeta/src/zeta.rs | 9 +- crates/zlog/src/sink.rs | 8 +- crates/zlog/src/zlog.rs | 30 +- extensions/glsl/src/glsl.rs | 8 +- extensions/ruff/src/ruff.rs | 14 +- extensions/snippets/src/snippets.rs | 8 +- .../test-extension/src/test_extension.rs | 8 +- extensions/toml/src/toml.rs | 14 +- 281 files changed, 6652 insertions(+), 7113 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f326090b51a066dfbc303b4a44228759ec4b96b7..89aadbcba0a74bc0ed43ebedc419ad9533216ccc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -830,6 +830,7 @@ module_inception = { level = "deny" } question_mark = { level = "deny" } redundant_closure = { level = "deny" } declare_interior_mutable_const = { level = "deny" } +collapsible_if = { level = "warn"} needless_borrow = { level = "warn"} # Individual rules that have violations in the codebase: type_complexity = "allow" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index b86696d437a2b9b10647d5cf83b8dd03de826a6c..227ca984d466d92a263add2011599f3af1677808 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -249,14 +249,13 @@ impl ToolCall { } if let Some(raw_output) = raw_output { - if self.content.is_empty() { - if let Some(markdown) = markdown_for_raw_output(&raw_output, &language_registry, cx) - { - self.content - .push(ToolCallContent::ContentBlock(ContentBlock::Markdown { - markdown, - })); - } + if self.content.is_empty() + && let Some(markdown) = markdown_for_raw_output(&raw_output, &language_registry, cx) + { + self.content + .push(ToolCallContent::ContentBlock(ContentBlock::Markdown { + markdown, + })); } self.raw_output = Some(raw_output); } @@ -430,11 +429,11 @@ impl ContentBlock { language_registry: &Arc<LanguageRegistry>, cx: &mut App, ) { - if matches!(self, ContentBlock::Empty) { - if let acp::ContentBlock::ResourceLink(resource_link) = block { - *self = ContentBlock::ResourceLink { resource_link }; - return; - } + if matches!(self, ContentBlock::Empty) + && let acp::ContentBlock::ResourceLink(resource_link) = block + { + *self = ContentBlock::ResourceLink { resource_link }; + return; } let new_content = self.block_string_contents(block); diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index 20ba9586ea459b3f1e863fe1936258c843186b7b..ceced1bcdd2e8edb0e4cd950bef68b646b3a252c 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -614,10 +614,10 @@ impl ActionLog { false } }); - if tracked_buffer.unreviewed_edits.is_empty() { - if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status { - tracked_buffer.status = TrackedBufferStatus::Modified; - } + if tracked_buffer.unreviewed_edits.is_empty() + && let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status + { + tracked_buffer.status = TrackedBufferStatus::Modified; } tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); } diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 090252d3389cb636298636933a75a140ff235681..8faf74736af6229a408cba96a13e27a0b4fab241 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -458,26 +458,24 @@ impl ActivityIndicator { .map(|r| r.read(cx)) .and_then(Repository::current_job); // Show any long-running git command - if let Some(job_info) = current_job { - if Instant::now() - job_info.start >= GIT_OPERATION_DELAY { - return Some(Content { - icon: Some( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ) - .into_any_element(), - ), - message: job_info.message.into(), - on_click: None, - tooltip_message: None, - }); - } + if let Some(job_info) = current_job + && Instant::now() - job_info.start >= GIT_OPERATION_DELAY + { + return Some(Content { + icon: Some( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Small) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + ) + .into_any_element(), + ), + message: job_info.message.into(), + on_click: None, + tooltip_message: None, + }); } // Show any language server installation info. @@ -740,21 +738,20 @@ impl ActivityIndicator { if let Some(extension_store) = ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx)) + && let Some(extension_id) = extension_store.outstanding_operations().keys().next() { - if let Some(extension_id) = extension_store.outstanding_operations().keys().next() { - return Some(Content { - icon: Some( - Icon::new(IconName::Download) - .size(IconSize::Small) - .into_any_element(), - ), - message: format!("Updating {extension_id} extension…"), - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_error_message(&DismissErrorMessage, window, cx) - })), - tooltip_message: None, - }); - } + return Some(Content { + icon: Some( + Icon::new(IconName::Download) + .size(IconSize::Small) + .into_any_element(), + ), + message: format!("Updating {extension_id} extension…"), + on_click: Some(Arc::new(|this, window, cx| { + this.dismiss_error_message(&DismissErrorMessage, window, cx) + })), + tooltip_message: None, + }); } None diff --git a/crates/agent/src/context.rs b/crates/agent/src/context.rs index 8cdb87ef8d9f3363e68c14053c01f34ece64b3b9..9bb8fc0eaef2126687f5e3277016469801af682c 100644 --- a/crates/agent/src/context.rs +++ b/crates/agent/src/context.rs @@ -201,24 +201,24 @@ impl FileContextHandle { parse_status.changed().await.log_err(); } - if let Ok(snapshot) = buffer.read_with(cx, |buffer, _| buffer.snapshot()) { - if let Some(outline) = snapshot.outline(None) { - let items = outline - .items - .into_iter() - .map(|item| item.to_point(&snapshot)); - - if let Ok(outline_text) = - outline::render_outline(items, None, 0, usize::MAX).await - { - let context = AgentContext::File(FileContext { - handle: self, - full_path, - text: outline_text.into(), - is_outline: true, - }); - return Some((context, vec![buffer])); - } + if let Ok(snapshot) = buffer.read_with(cx, |buffer, _| buffer.snapshot()) + && let Some(outline) = snapshot.outline(None) + { + let items = outline + .items + .into_iter() + .map(|item| item.to_point(&snapshot)); + + if let Ok(outline_text) = + outline::render_outline(items, None, 0, usize::MAX).await + { + let context = AgentContext::File(FileContext { + handle: self, + full_path, + text: outline_text.into(), + is_outline: true, + }); + return Some((context, vec![buffer])); } } } diff --git a/crates/agent/src/context_store.rs b/crates/agent/src/context_store.rs index 60ba5527dcca22d81b7da62657c6abc00aa51607..b531852a184ffeaf86862990f03210ceb6033395 100644 --- a/crates/agent/src/context_store.rs +++ b/crates/agent/src/context_store.rs @@ -338,11 +338,9 @@ impl ContextStore { image_task, context_id: self.next_context_id.post_inc(), }); - if self.has_context(&context) { - if remove_if_exists { - self.remove_context(&context, cx); - return None; - } + if self.has_context(&context) && remove_if_exists { + self.remove_context(&context, cx); + return None; } self.insert_context(context.clone(), cx); diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index a3f903a60d23cb1e17543419a3da72b082b595ea..5c4b2b8ebfae97aa0919fae68a5132fefc7b342f 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1967,11 +1967,9 @@ impl Thread { if let Some(prev_message) = thread.messages.get(ix - 1) - { - if prev_message.role == Role::Assistant { + && prev_message.role == Role::Assistant { break; } - } } } @@ -2476,13 +2474,13 @@ impl Thread { .ok()?; // Save thread so its summary can be reused later - if let Some(thread) = thread.upgrade() { - if let Ok(Ok(save_task)) = cx.update(|cx| { + if let Some(thread) = thread.upgrade() + && let Ok(Ok(save_task)) = cx.update(|cx| { thread_store .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx)) - }) { - save_task.await.log_err(); - } + }) + { + save_task.await.log_err(); } Some(()) @@ -2730,12 +2728,11 @@ impl Thread { window: Option<AnyWindowHandle>, cx: &mut Context<Self>, ) { - if self.all_tools_finished() { - if let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() { - if !canceled { - self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx); - } - } + if self.all_tools_finished() + && let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() + && !canceled + { + self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx); } cx.emit(ThreadEvent::ToolFinished { @@ -2922,11 +2919,11 @@ impl Thread { let buffer_store = project.read(app_cx).buffer_store(); for buffer_handle in buffer_store.read(app_cx).buffers() { let buffer = buffer_handle.read(app_cx); - if buffer.is_dirty() { - if let Some(file) = buffer.file() { - let path = file.path().to_string_lossy().to_string(); - unsaved_buffers.push(path); - } + if buffer.is_dirty() + && let Some(file) = buffer.file() + { + let path = file.path().to_string_lossy().to_string(); + unsaved_buffers.push(path); } } }) @@ -3178,13 +3175,13 @@ impl Thread { .model .max_token_count_for_mode(self.completion_mode().into()); - if let Some(exceeded_error) = &self.exceeded_window_error { - if model.model.id() == exceeded_error.model_id { - return Some(TotalTokenUsage { - total: exceeded_error.token_count, - max, - }); - } + if let Some(exceeded_error) = &self.exceeded_window_error + && model.model.id() == exceeded_error.model_id + { + return Some(TotalTokenUsage { + total: exceeded_error.token_count, + max, + }); } let total = self diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 12c94a522d52de78e52dab4764a7f187054eca47..96bf6393068d9949123b3da92fdeee86b4c41dc6 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -581,33 +581,32 @@ impl ThreadStore { return; }; - if protocol.capable(context_server::protocol::ServerCapability::Tools) { - if let Some(response) = protocol + if protocol.capable(context_server::protocol::ServerCapability::Tools) + && let Some(response) = protocol .request::<context_server::types::requests::ListTools>(()) .await .log_err() - { - let tool_ids = tool_working_set - .update(cx, |tool_working_set, cx| { - tool_working_set.extend( - response.tools.into_iter().map(|tool| { - Arc::new(ContextServerTool::new( - context_server_store.clone(), - server.id(), - tool, - )) as Arc<dyn Tool> - }), - cx, - ) - }) - .log_err(); - - if let Some(tool_ids) = tool_ids { - this.update(cx, |this, _| { - this.context_server_tool_ids.insert(server_id, tool_ids); - }) - .log_err(); - } + { + let tool_ids = tool_working_set + .update(cx, |tool_working_set, cx| { + tool_working_set.extend( + response.tools.into_iter().map(|tool| { + Arc::new(ContextServerTool::new( + context_server_store.clone(), + server.id(), + tool, + )) as Arc<dyn Tool> + }), + cx, + ) + }) + .log_err(); + + if let Some(tool_ids) = tool_ids { + this.update(cx, |this, _| { + this.context_server_tool_ids.insert(server_id, tool_ids); + }) + .log_err(); } } }) @@ -697,13 +696,14 @@ impl SerializedThreadV0_1_0 { let mut messages: Vec<SerializedMessage> = Vec::with_capacity(self.0.messages.len()); for message in self.0.messages { - if message.role == Role::User && !message.tool_results.is_empty() { - if let Some(last_message) = messages.last_mut() { - debug_assert!(last_message.role == Role::Assistant); - - last_message.tool_results = message.tool_results; - continue; - } + if message.role == Role::User + && !message.tool_results.is_empty() + && let Some(last_message) = messages.last_mut() + { + debug_assert!(last_message.role == Role::Assistant); + + last_message.tool_results = message.tool_results; + continue; } messages.push(message); diff --git a/crates/agent/src/tool_use.rs b/crates/agent/src/tool_use.rs index 74dfaf9a85852d151554df9439a53fee90ec5686..d109891bf2a84e3833875719f0d709123b041695 100644 --- a/crates/agent/src/tool_use.rs +++ b/crates/agent/src/tool_use.rs @@ -112,19 +112,13 @@ impl ToolUseState { }, ); - if let Some(window) = &mut window { - if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) { - if let Some(output) = tool_result.output.clone() { - if let Some(card) = tool.deserialize_card( - output, - project.clone(), - window, - cx, - ) { - this.tool_result_cards.insert(tool_use_id, card); - } - } - } + if let Some(window) = &mut window + && let Some(tool) = this.tools.read(cx).tool(tool_use, cx) + && let Some(output) = tool_result.output.clone() + && let Some(card) = + tool.deserialize_card(output, project.clone(), window, cx) + { + this.tool_result_cards.insert(tool_use_id, card); } } } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index f0b5d2f08acfaac9a84a9bbd9c34c9a13954b185..856e70ce593e91bb48c652c61701977e969b88b7 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1037,12 +1037,12 @@ impl Thread { log::info!("Running tool {}", tool_use.name); Some(cx.foreground_executor().spawn(async move { let tool_result = tool_result.await.and_then(|output| { - if let LanguageModelToolResultContent::Image(_) = &output.llm_output { - if !supports_images { - return Err(anyhow!( - "Attempted to read an image, but this model doesn't support it.", - )); - } + if let LanguageModelToolResultContent::Image(_) = &output.llm_output + && !supports_images + { + return Err(anyhow!( + "Attempted to read an image, but this model doesn't support it.", + )); } Ok(output) }); diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 8ebd2936a5ea29d1e461696f2088ebeb99248552..7687d687026705d467d0f471275ace2717d12576 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -156,13 +156,13 @@ impl EditFileTool { // It's also possible that the global config dir is configured to be inside the project, // so check for that edge case too. - if let Ok(canonical_path) = std::fs::canonicalize(&input.path) { - if canonical_path.starts_with(paths::config_dir()) { - return event_stream.authorize( - format!("{} (global settings)", input.display_description), - cx, - ); - } + if let Ok(canonical_path) = std::fs::canonicalize(&input.path) + && canonical_path.starts_with(paths::config_dir()) + { + return event_stream.authorize( + format!("{} (global settings)", input.display_description), + cx, + ); } // Check if path is inside the global config directory diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent2/src/tools/grep_tool.rs index e5d92b3c1d8c49501721242980865e9622855256..6d7c05d2115498b18d26a6963ea4330f1dadd4c2 100644 --- a/crates/agent2/src/tools/grep_tool.rs +++ b/crates/agent2/src/tools/grep_tool.rs @@ -179,15 +179,14 @@ impl AgentTool for GrepTool { // Check if this file should be excluded based on its worktree settings if let Ok(Some(project_path)) = project.read_with(cx, |project, cx| { project.find_project_path(&path, cx) - }) { - if cx.update(|cx| { + }) + && cx.update(|cx| { let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); worktree_settings.is_path_excluded(&project_path.path) || worktree_settings.is_path_private(&project_path.path) }).unwrap_or(false) { continue; } - } while *parse_status.borrow() != ParseStatus::Idle { parse_status.changed().await?; @@ -275,12 +274,11 @@ impl AgentTool for GrepTool { output.extend(snapshot.text_for_range(range)); output.push_str("\n```\n"); - if let Some(ancestor_range) = ancestor_range { - if end_row < ancestor_range.end.row { + if let Some(ancestor_range) = ancestor_range + && end_row < ancestor_range.end.row { let remaining_lines = ancestor_range.end.row - end_row; writeln!(output, "\n{} lines remaining in ancestor node. Read the file to see all.", remaining_lines)?; } - } matches_found += 1; } diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 7034d6fbcea3370e1e528173591973e14ddeee1c..34d55f39dc99f5d01d47556a8c9315646f8b1a68 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -203,14 +203,14 @@ impl AgentConnection for ClaudeAgentConnection { .await } - if let Some(status) = child.status().await.log_err() { - if let Some(thread) = thread_rx.recv().await.ok() { - thread - .update(cx, |thread, cx| { - thread.emit_server_exited(status, cx); - }) - .ok(); - } + if let Some(status) = child.status().await.log_err() + && let Some(thread) = thread_rx.recv().await.ok() + { + thread + .update(cx, |thread, cx| { + thread.emit_server_exited(status, cx); + }) + .ok(); } } }); diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index fd38ba1f7f0df640fbc9dd50976112092acc2db2..afc834cdd8f9c2f5d053002049e9ae439fe166c0 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -116,15 +116,15 @@ pub struct LanguageModelParameters { impl LanguageModelParameters { pub fn matches(&self, model: &Arc<dyn LanguageModel>) -> bool { - if let Some(provider) = &self.provider { - if provider.0 != model.provider_id().0 { - return false; - } + if let Some(provider) = &self.provider + && provider.0 != model.provider_id().0 + { + return false; } - if let Some(setting_model) = &self.model { - if *setting_model != model.id().0 { - return false; - } + if let Some(setting_model) = &self.model + && *setting_model != model.id().0 + { + return false; } true } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 370dae53e4f3037e9162407dfd4c3ec3bf727183..ad0920bc4a9cd5bc3bf046c04cd8cab16726834f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -371,20 +371,20 @@ impl AcpThreadView { let provider_id = provider_id.clone(); let this = this.clone(); move |_, ev, window, cx| { - if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev { - if &provider_id == updated_provider_id { - this.update(cx, |this, cx| { - this.thread_state = Self::initial_state( - agent.clone(), - this.workspace.clone(), - this.project.clone(), - window, - cx, - ); - cx.notify(); - }) - .ok(); - } + if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev + && &provider_id == updated_provider_id + { + this.update(cx, |this, cx| { + this.thread_state = Self::initial_state( + agent.clone(), + this.workspace.clone(), + this.project.clone(), + window, + cx, + ); + cx.notify(); + }) + .ok(); } } }); @@ -547,11 +547,11 @@ impl AcpThreadView { } fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) { - if let Some(thread) = self.thread() { - if thread.read(cx).status() != ThreadStatus::Idle { - self.stop_current_and_send_new_message(window, cx); - return; - } + if let Some(thread) = self.thread() + && thread.read(cx).status() != ThreadStatus::Idle + { + self.stop_current_and_send_new_message(window, cx); + return; } let contents = self @@ -628,25 +628,24 @@ impl AcpThreadView { return; }; - if let Some(index) = self.editing_message.take() { - if let Some(editor) = self + if let Some(index) = self.editing_message.take() + && let Some(editor) = self .entry_view_state .read(cx) .entry(index) .and_then(|e| e.message_editor()) .cloned() - { - editor.update(cx, |editor, cx| { - if let Some(user_message) = thread - .read(cx) - .entries() - .get(index) - .and_then(|e| e.user_message()) - { - editor.set_message(user_message.chunks.clone(), window, cx); - } - }) - } + { + editor.update(cx, |editor, cx| { + if let Some(user_message) = thread + .read(cx) + .entries() + .get(index) + .and_then(|e| e.user_message()) + { + editor.set_message(user_message.chunks.clone(), window, cx); + } + }) }; self.focus_handle(cx).focus(window); cx.notify(); @@ -3265,62 +3264,61 @@ impl AcpThreadView { }) }) .log_err() + && let Some(pop_up) = screen_window.entity(cx).log_err() { - if let Some(pop_up) = screen_window.entity(cx).log_err() { - self.notification_subscriptions - .entry(screen_window) - .or_insert_with(Vec::new) - .push(cx.subscribe_in(&pop_up, window, { - |this, _, event, window, cx| match event { - AgentNotificationEvent::Accepted => { - let handle = window.window_handle(); - cx.activate(true); - - let workspace_handle = this.workspace.clone(); - - // If there are multiple Zed windows, activate the correct one. - cx.defer(move |cx| { - handle - .update(cx, |_view, window, _cx| { - window.activate_window(); - - if let Some(workspace) = workspace_handle.upgrade() { - workspace.update(_cx, |workspace, cx| { - workspace.focus_panel::<AgentPanel>(window, cx); - }); - } - }) - .log_err(); - }); + self.notification_subscriptions + .entry(screen_window) + .or_insert_with(Vec::new) + .push(cx.subscribe_in(&pop_up, window, { + |this, _, event, window, cx| match event { + AgentNotificationEvent::Accepted => { + let handle = window.window_handle(); + cx.activate(true); + + let workspace_handle = this.workspace.clone(); + + // If there are multiple Zed windows, activate the correct one. + cx.defer(move |cx| { + handle + .update(cx, |_view, window, _cx| { + window.activate_window(); + + if let Some(workspace) = workspace_handle.upgrade() { + workspace.update(_cx, |workspace, cx| { + workspace.focus_panel::<AgentPanel>(window, cx); + }); + } + }) + .log_err(); + }); - this.dismiss_notifications(cx); - } - AgentNotificationEvent::Dismissed => { - this.dismiss_notifications(cx); - } + this.dismiss_notifications(cx); } - })); - - self.notifications.push(screen_window); - - // If the user manually refocuses the original window, dismiss the popup. - self.notification_subscriptions - .entry(screen_window) - .or_insert_with(Vec::new) - .push({ - let pop_up_weak = pop_up.downgrade(); - - cx.observe_window_activation(window, move |_, window, cx| { - if window.is_window_active() { - if let Some(pop_up) = pop_up_weak.upgrade() { - pop_up.update(cx, |_, cx| { - cx.emit(AgentNotificationEvent::Dismissed); - }); - } - } - }) - }); - } + AgentNotificationEvent::Dismissed => { + this.dismiss_notifications(cx); + } + } + })); + + self.notifications.push(screen_window); + + // If the user manually refocuses the original window, dismiss the popup. + self.notification_subscriptions + .entry(screen_window) + .or_insert_with(Vec::new) + .push({ + let pop_up_weak = pop_up.downgrade(); + + cx.observe_window_activation(window, move |_, window, cx| { + if window.is_window_active() + && let Some(pop_up) = pop_up_weak.upgrade() + { + pop_up.update(cx, |_, cx| { + cx.emit(AgentNotificationEvent::Dismissed); + }); + } + }) + }); } } diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index d2f448635e48cff3f407263b4bb21d1f21a28071..3defa42d1729f898f362c18d41a8e8ceac34554f 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1072,8 +1072,8 @@ impl ActiveThread { } ThreadEvent::MessageEdited(message_id) => { self.clear_last_error(); - if let Some(index) = self.messages.iter().position(|id| id == message_id) { - if let Some(rendered_message) = self.thread.update(cx, |thread, cx| { + if let Some(index) = self.messages.iter().position(|id| id == message_id) + && let Some(rendered_message) = self.thread.update(cx, |thread, cx| { thread.message(*message_id).map(|message| { let mut rendered_message = RenderedMessage { language_registry: self.language_registry.clone(), @@ -1084,14 +1084,14 @@ impl ActiveThread { } rendered_message }) - }) { - self.list_state.splice(index..index + 1, 1); - self.rendered_messages_by_id - .insert(*message_id, rendered_message); - self.scroll_to_bottom(cx); - self.save_thread(cx); - cx.notify(); - } + }) + { + self.list_state.splice(index..index + 1, 1); + self.rendered_messages_by_id + .insert(*message_id, rendered_message); + self.scroll_to_bottom(cx); + self.save_thread(cx); + cx.notify(); } } ThreadEvent::MessageDeleted(message_id) => { @@ -1272,62 +1272,61 @@ impl ActiveThread { }) }) .log_err() + && let Some(pop_up) = screen_window.entity(cx).log_err() { - if let Some(pop_up) = screen_window.entity(cx).log_err() { - self.notification_subscriptions - .entry(screen_window) - .or_insert_with(Vec::new) - .push(cx.subscribe_in(&pop_up, window, { - |this, _, event, window, cx| match event { - AgentNotificationEvent::Accepted => { - let handle = window.window_handle(); - cx.activate(true); - - let workspace_handle = this.workspace.clone(); - - // If there are multiple Zed windows, activate the correct one. - cx.defer(move |cx| { - handle - .update(cx, |_view, window, _cx| { - window.activate_window(); - - if let Some(workspace) = workspace_handle.upgrade() { - workspace.update(_cx, |workspace, cx| { - workspace.focus_panel::<AgentPanel>(window, cx); - }); - } - }) - .log_err(); - }); + self.notification_subscriptions + .entry(screen_window) + .or_insert_with(Vec::new) + .push(cx.subscribe_in(&pop_up, window, { + |this, _, event, window, cx| match event { + AgentNotificationEvent::Accepted => { + let handle = window.window_handle(); + cx.activate(true); + + let workspace_handle = this.workspace.clone(); + + // If there are multiple Zed windows, activate the correct one. + cx.defer(move |cx| { + handle + .update(cx, |_view, window, _cx| { + window.activate_window(); + + if let Some(workspace) = workspace_handle.upgrade() { + workspace.update(_cx, |workspace, cx| { + workspace.focus_panel::<AgentPanel>(window, cx); + }); + } + }) + .log_err(); + }); - this.dismiss_notifications(cx); - } - AgentNotificationEvent::Dismissed => { - this.dismiss_notifications(cx); - } + this.dismiss_notifications(cx); } - })); - - self.notifications.push(screen_window); - - // If the user manually refocuses the original window, dismiss the popup. - self.notification_subscriptions - .entry(screen_window) - .or_insert_with(Vec::new) - .push({ - let pop_up_weak = pop_up.downgrade(); - - cx.observe_window_activation(window, move |_, window, cx| { - if window.is_window_active() { - if let Some(pop_up) = pop_up_weak.upgrade() { - pop_up.update(cx, |_, cx| { - cx.emit(AgentNotificationEvent::Dismissed); - }); - } - } - }) - }); - } + AgentNotificationEvent::Dismissed => { + this.dismiss_notifications(cx); + } + } + })); + + self.notifications.push(screen_window); + + // If the user manually refocuses the original window, dismiss the popup. + self.notification_subscriptions + .entry(screen_window) + .or_insert_with(Vec::new) + .push({ + let pop_up_weak = pop_up.downgrade(); + + cx.observe_window_activation(window, move |_, window, cx| { + if window.is_window_active() + && let Some(pop_up) = pop_up_weak.upgrade() + { + pop_up.update(cx, |_, cx| { + cx.emit(AgentNotificationEvent::Dismissed); + }); + } + }) + }); } } @@ -2269,13 +2268,12 @@ impl ActiveThread { let mut error = None; if let Some(last_restore_checkpoint) = self.thread.read(cx).last_restore_checkpoint() + && last_restore_checkpoint.message_id() == message_id { - if last_restore_checkpoint.message_id() == message_id { - match last_restore_checkpoint { - LastRestoreCheckpoint::Pending { .. } => is_pending = true, - LastRestoreCheckpoint::Error { error: err, .. } => { - error = Some(err.clone()); - } + match last_restore_checkpoint { + LastRestoreCheckpoint::Pending { .. } => is_pending = true, + LastRestoreCheckpoint::Error { error: err, .. } => { + error = Some(err.clone()); } } } diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index 32360dd56ef925d56310ff7e2e5668de1973f472..311f75af3ba3f85e2db2193d8a739c08b3e37c89 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -163,10 +163,10 @@ impl ConfigurationSource { .read(cx) .text(cx); let settings = serde_json_lenient::from_str::<serde_json::Value>(&text)?; - if let Some(settings_validator) = settings_validator { - if let Err(error) = settings_validator.validate(&settings) { - return Err(anyhow::anyhow!(error.to_string())); - } + if let Some(settings_validator) = settings_validator + && let Err(error) = settings_validator.validate(&settings) + { + return Err(anyhow::anyhow!(error.to_string())); } Ok(( id.clone(), @@ -716,24 +716,24 @@ fn wait_for_context_server( project::context_server_store::Event::ServerStatusChanged { server_id, status } => { match status { ContextServerStatus::Running => { - if server_id == &context_server_id { - if let Some(tx) = tx.lock().unwrap().take() { - let _ = tx.send(Ok(())); - } + if server_id == &context_server_id + && let Some(tx) = tx.lock().unwrap().take() + { + let _ = tx.send(Ok(())); } } ContextServerStatus::Stopped => { - if server_id == &context_server_id { - if let Some(tx) = tx.lock().unwrap().take() { - let _ = tx.send(Err("Context server stopped running".into())); - } + if server_id == &context_server_id + && let Some(tx) = tx.lock().unwrap().take() + { + let _ = tx.send(Err("Context server stopped running".into())); } } ContextServerStatus::Error(error) => { - if server_id == &context_server_id { - if let Some(tx) = tx.lock().unwrap().take() { - let _ = tx.send(Err(error.clone())); - } + if server_id == &context_server_id + && let Some(tx) = tx.lock().unwrap().take() + { + let _ = tx.send(Err(error.clone())); } } _ => {} diff --git a/crates/agent_ui/src/agent_configuration/tool_picker.rs b/crates/agent_ui/src/agent_configuration/tool_picker.rs index 8f1e0d71c0bd8ef56a71c1a88db1bf67929b060c..25947a1e589d96f1f4bfc64a04f43f48153420b9 100644 --- a/crates/agent_ui/src/agent_configuration/tool_picker.rs +++ b/crates/agent_ui/src/agent_configuration/tool_picker.rs @@ -191,10 +191,10 @@ impl PickerDelegate for ToolPickerDelegate { BTreeMap::default(); for item in all_items.iter() { - if let PickerItem::Tool { server_id, name } = item.clone() { - if name.contains(&query) { - tools_by_provider.entry(server_id).or_default().push(name); - } + if let PickerItem::Tool { server_id, name } = item.clone() + && name.contains(&query) + { + tools_by_provider.entry(server_id).or_default().push(name); } } diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index b010f8a424fb75790d7cd913699a21ef0ae332ee..f474fdf3ae85d30a2d36dc9483e01fda89ad09b6 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1043,18 +1043,18 @@ impl ToolbarItemView for AgentDiffToolbar { return self.location(cx); } - if let Some(editor) = item.act_as::<Editor>(cx) { - if editor.read(cx).mode().is_full() { - let agent_diff = AgentDiff::global(cx); - - self.active_item = Some(AgentDiffToolbarItem::Editor { - editor: editor.downgrade(), - state: agent_diff.read(cx).editor_state(&editor.downgrade()), - _diff_subscription: cx.observe(&agent_diff, Self::handle_diff_notify), - }); + if let Some(editor) = item.act_as::<Editor>(cx) + && editor.read(cx).mode().is_full() + { + let agent_diff = AgentDiff::global(cx); - return self.location(cx); - } + self.active_item = Some(AgentDiffToolbarItem::Editor { + editor: editor.downgrade(), + state: agent_diff.read(cx).editor_state(&editor.downgrade()), + _diff_subscription: cx.observe(&agent_diff, Self::handle_diff_notify), + }); + + return self.location(cx); } } @@ -1538,16 +1538,10 @@ impl AgentDiff { ) { match event { workspace::Event::ItemAdded { item } => { - if let Some(editor) = item.downcast::<Editor>() { - if let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) { - self.register_editor( - workspace.downgrade(), - buffer.clone(), - editor, - window, - cx, - ); - } + if let Some(editor) = item.downcast::<Editor>() + && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) + { + self.register_editor(workspace.downgrade(), buffer.clone(), editor, window, cx); } } _ => {} @@ -1850,22 +1844,22 @@ impl AgentDiff { let thread = thread.upgrade()?; - if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) { - if let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton() { - let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx); - - let mut keys = changed_buffers.keys().cycle(); - keys.find(|k| *k == &curr_buffer); - let next_project_path = keys - .next() - .filter(|k| *k != &curr_buffer) - .and_then(|after| after.read(cx).project_path(cx)); - - if let Some(path) = next_project_path { - let task = workspace.open_path(path, None, true, window, cx); - let task = cx.spawn(async move |_, _cx| task.await.map(|_| ())); - return Some(task); - } + if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) + && let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton() + { + let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx); + + let mut keys = changed_buffers.keys().cycle(); + keys.find(|k| *k == &curr_buffer); + let next_project_path = keys + .next() + .filter(|k| *k != &curr_buffer) + .and_then(|after| after.read(cx).project_path(cx)); + + if let Some(path) = next_project_path { + let task = workspace.open_path(path, None, true, window, cx); + let task = cx.spawn(async move |_, _cx| task.await.map(|_| ())); + return Some(task); } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index cb354222b6b0de897a1053b160ce96a73793ee5a..55d07ed495800b098f1f32b050072d5a61e1a6ab 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1398,14 +1398,13 @@ impl AgentPanel { if LanguageModelRegistry::read_global(cx) .default_model() .map_or(true, |model| model.provider.id() != provider.id()) + && let Some(model) = provider.default_model(cx) { - if let Some(model) = provider.default_model(cx) { - update_settings_file::<AgentSettings>( - self.fs.clone(), - cx, - move |settings, _| settings.set_model(model), - ); - } + update_settings_file::<AgentSettings>( + self.fs.clone(), + cx, + move |settings, _| settings.set_model(model), + ); } self.new_thread(&NewThread::default(), window, cx); diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index 23e04266db73c654ff0ecc97fe69852e6f531b19..ff5e9362dd3e6f734590e8d3a3efd4779c613316 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -352,12 +352,12 @@ impl CodegenAlternative { event: &multi_buffer::Event, cx: &mut Context<Self>, ) { - if let multi_buffer::Event::TransactionUndone { transaction_id } = event { - if self.transformation_transaction_id == Some(*transaction_id) { - self.transformation_transaction_id = None; - self.generation = Task::ready(()); - cx.emit(CodegenEvent::Undone); - } + if let multi_buffer::Event::TransactionUndone { transaction_id } = event + && self.transformation_transaction_id == Some(*transaction_id) + { + self.transformation_transaction_id = None; + self.generation = Task::ready(()); + cx.emit(CodegenEvent::Undone); } } @@ -576,38 +576,34 @@ impl CodegenAlternative { let mut lines = chunk.split('\n').peekable(); while let Some(line) = lines.next() { new_text.push_str(line); - if line_indent.is_none() { - if let Some(non_whitespace_ch_ix) = + if line_indent.is_none() + && let Some(non_whitespace_ch_ix) = new_text.find(|ch: char| !ch.is_whitespace()) - { - line_indent = Some(non_whitespace_ch_ix); - base_indent = base_indent.or(line_indent); - - let line_indent = line_indent.unwrap(); - let base_indent = base_indent.unwrap(); - let indent_delta = - line_indent as i32 - base_indent as i32; - let mut corrected_indent_len = cmp::max( - 0, - suggested_line_indent.len as i32 + indent_delta, - ) - as usize; - if first_line { - corrected_indent_len = corrected_indent_len - .saturating_sub( - selection_start.column as usize, - ); - } - - let indent_char = suggested_line_indent.char(); - let mut indent_buffer = [0; 4]; - let indent_str = - indent_char.encode_utf8(&mut indent_buffer); - new_text.replace_range( - ..line_indent, - &indent_str.repeat(corrected_indent_len), - ); + { + line_indent = Some(non_whitespace_ch_ix); + base_indent = base_indent.or(line_indent); + + let line_indent = line_indent.unwrap(); + let base_indent = base_indent.unwrap(); + let indent_delta = line_indent as i32 - base_indent as i32; + let mut corrected_indent_len = cmp::max( + 0, + suggested_line_indent.len as i32 + indent_delta, + ) + as usize; + if first_line { + corrected_indent_len = corrected_indent_len + .saturating_sub(selection_start.column as usize); } + + let indent_char = suggested_line_indent.char(); + let mut indent_buffer = [0; 4]; + let indent_str = + indent_char.encode_utf8(&mut indent_buffer); + new_text.replace_range( + ..line_indent, + &indent_str.repeat(corrected_indent_len), + ); } if line_indent.is_some() { diff --git a/crates/agent_ui/src/context_strip.rs b/crates/agent_ui/src/context_strip.rs index 51ed3a5e1169907b85e1944305d2acfc22fdf551..d25d7d35443e6ca7c28bb0894f72c0063f500721 100644 --- a/crates/agent_ui/src/context_strip.rs +++ b/crates/agent_ui/src/context_strip.rs @@ -368,10 +368,10 @@ impl ContextStrip { _window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(suggested) = self.suggested_context(cx) { - if self.is_suggested_focused(&self.added_contexts(cx)) { - self.add_suggested_context(&suggested, cx); - } + if let Some(suggested) = self.suggested_context(cx) + && self.is_suggested_focused(&self.added_contexts(cx)) + { + self.add_suggested_context(&suggested, cx); } } diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 781e242fba5d8c5cb938c6607d6416896291de90..101eb899b232f3361170561c78e0f2e2c01ba629 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -182,13 +182,13 @@ impl InlineAssistant { match event { workspace::Event::UserSavedItem { item, .. } => { // When the user manually saves an editor, automatically accepts all finished transformations. - if let Some(editor) = item.upgrade().and_then(|item| item.act_as::<Editor>(cx)) { - if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) { - for assist_id in editor_assists.assist_ids.clone() { - let assist = &self.assists[&assist_id]; - if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) { - self.finish_assist(assist_id, false, window, cx) - } + if let Some(editor) = item.upgrade().and_then(|item| item.act_as::<Editor>(cx)) + && let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) + { + for assist_id in editor_assists.assist_ids.clone() { + let assist = &self.assists[&assist_id]; + if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) { + self.finish_assist(assist_id, false, window, cx) } } } @@ -342,13 +342,11 @@ impl InlineAssistant { ) .await .ok(); - if let Some(answer) = answer { - if answer == 0 { - cx.update(|window, cx| { - window.dispatch_action(Box::new(OpenSettings), cx) - }) + if let Some(answer) = answer + && answer == 0 + { + cx.update(|window, cx| window.dispatch_action(Box::new(OpenSettings), cx)) .ok(); - } } anyhow::Ok(()) }) @@ -435,11 +433,11 @@ impl InlineAssistant { } } - if let Some(prev_selection) = selections.last_mut() { - if selection.start <= prev_selection.end { - prev_selection.end = selection.end; - continue; - } + if let Some(prev_selection) = selections.last_mut() + && selection.start <= prev_selection.end + { + prev_selection.end = selection.end; + continue; } let latest_selection = newest_selection.get_or_insert_with(|| selection.clone()); @@ -985,14 +983,13 @@ impl InlineAssistant { EditorEvent::SelectionsChanged { .. } => { for assist_id in editor_assists.assist_ids.clone() { let assist = &self.assists[&assist_id]; - if let Some(decorations) = assist.decorations.as_ref() { - if decorations + if let Some(decorations) = assist.decorations.as_ref() + && decorations .prompt_editor .focus_handle(cx) .is_focused(window) - { - return; - } + { + return; } } @@ -1503,20 +1500,18 @@ impl InlineAssistant { window: &mut Window, cx: &mut App, ) -> Option<InlineAssistTarget> { - if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) { - if terminal_panel + if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) + && terminal_panel .read(cx) .focus_handle(cx) .contains_focused(window, cx) - { - if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| { - pane.read(cx) - .active_item() - .and_then(|t| t.downcast::<TerminalView>()) - }) { - return Some(InlineAssistTarget::Terminal(terminal_view)); - } - } + && let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| { + pane.read(cx) + .active_item() + .and_then(|t| t.downcast::<TerminalView>()) + }) + { + return Some(InlineAssistTarget::Terminal(terminal_view)); } let context_editor = agent_panel @@ -1741,22 +1736,20 @@ impl InlineAssist { return; }; - if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) { - if assist.decorations.is_none() { - if let Some(workspace) = assist.workspace.upgrade() { - let error = format!("Inline assistant error: {}", error); - workspace.update(cx, |workspace, cx| { - struct InlineAssistantError; - - let id = - NotificationId::composite::<InlineAssistantError>( - assist_id.0, - ); - - workspace.show_toast(Toast::new(id, error), cx); - }) - } - } + if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) + && assist.decorations.is_none() + && let Some(workspace) = assist.workspace.upgrade() + { + let error = format!("Inline assistant error: {}", error); + workspace.update(cx, |workspace, cx| { + struct InlineAssistantError; + + let id = NotificationId::composite::<InlineAssistantError>( + assist_id.0, + ); + + workspace.show_toast(Toast::new(id, error), cx); + }) } if assist.decorations.is_none() { @@ -1821,18 +1814,18 @@ impl CodeActionProvider for AssistantCodeActionProvider { has_diagnostics = true; } if has_diagnostics { - if let Some(symbols_containing_start) = snapshot.symbols_containing(range.start, None) { - if let Some(symbol) = symbols_containing_start.last() { - range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot)); - range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot)); - } + if let Some(symbols_containing_start) = snapshot.symbols_containing(range.start, None) + && let Some(symbol) = symbols_containing_start.last() + { + range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot)); + range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot)); } - if let Some(symbols_containing_end) = snapshot.symbols_containing(range.end, None) { - if let Some(symbol) = symbols_containing_end.last() { - range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot)); - range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot)); - } + if let Some(symbols_containing_end) = snapshot.symbols_containing(range.end, None) + && let Some(symbol) = symbols_containing_end.last() + { + range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot)); + range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot)); } Task::ready(Ok(vec![CodeAction { diff --git a/crates/agent_ui/src/terminal_inline_assistant.rs b/crates/agent_ui/src/terminal_inline_assistant.rs index bcbc308c99da7b80e716fce9e60461352dcb814c..3859863ebed516de75962899234021137ea24996 100644 --- a/crates/agent_ui/src/terminal_inline_assistant.rs +++ b/crates/agent_ui/src/terminal_inline_assistant.rs @@ -388,20 +388,20 @@ impl TerminalInlineAssistant { window: &mut Window, cx: &mut App, ) { - if let Some(assist) = self.assists.get_mut(&assist_id) { - if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() { - assist - .terminal - .update(cx, |terminal, cx| { - terminal.clear_block_below_cursor(cx); - let block = terminal_view::BlockProperties { - height, - render: Box::new(move |_| prompt_editor.clone().into_any_element()), - }; - terminal.set_block_below_cursor(block, window, cx); - }) - .log_err(); - } + if let Some(assist) = self.assists.get_mut(&assist_id) + && let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() + { + assist + .terminal + .update(cx, |terminal, cx| { + terminal.clear_block_below_cursor(cx); + let block = terminal_view::BlockProperties { + height, + render: Box::new(move |_| prompt_editor.clone().into_any_element()), + }; + terminal.set_block_below_cursor(block, window, cx); + }) + .log_err(); } } } @@ -450,23 +450,20 @@ impl TerminalInlineAssist { return; }; - if let CodegenStatus::Error(error) = &codegen.read(cx).status { - if assist.prompt_editor.is_none() { - if let Some(workspace) = assist.workspace.upgrade() { - let error = - format!("Terminal inline assistant error: {}", error); - workspace.update(cx, |workspace, cx| { - struct InlineAssistantError; - - let id = - NotificationId::composite::<InlineAssistantError>( - assist_id.0, - ); - - workspace.show_toast(Toast::new(id, error), cx); - }) - } - } + if let CodegenStatus::Error(error) = &codegen.read(cx).status + && assist.prompt_editor.is_none() + && let Some(workspace) = assist.workspace.upgrade() + { + let error = format!("Terminal inline assistant error: {}", error); + workspace.update(cx, |workspace, cx| { + struct InlineAssistantError; + + let id = NotificationId::composite::<InlineAssistantError>( + assist_id.0, + ); + + workspace.show_toast(Toast::new(id, error), cx); + }) } if assist.prompt_editor.is_none() { diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 376d3c54fd59fac56e83e9003deb0b2d7aa2115b..3b5f2e5069d9980dcbb9aa6c5ec568d9a2b85ac8 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -745,28 +745,27 @@ impl TextThreadEditor { ) { if let Some(invoked_slash_command) = self.context.read(cx).invoked_slash_command(&command_id) + && let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { - if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { - let run_commands_in_ranges = invoked_slash_command.run_commands_in_ranges.clone(); - for range in run_commands_in_ranges { - let commands = self.context.update(cx, |context, cx| { - context.reparse(cx); - context - .pending_commands_for_range(range.clone(), cx) - .to_vec() - }); + let run_commands_in_ranges = invoked_slash_command.run_commands_in_ranges.clone(); + for range in run_commands_in_ranges { + let commands = self.context.update(cx, |context, cx| { + context.reparse(cx); + context + .pending_commands_for_range(range.clone(), cx) + .to_vec() + }); - for command in commands { - self.run_command( - command.source_range, - &command.name, - &command.arguments, - false, - self.workspace.clone(), - window, - cx, - ); - } + for command in commands { + self.run_command( + command.source_range, + &command.name, + &command.arguments, + false, + self.workspace.clone(), + window, + cx, + ); } } } diff --git a/crates/agent_ui/src/thread_history.rs b/crates/agent_ui/src/thread_history.rs index 66afe2c2c5835387f10d095c7ee9649bda177f0b..4ec2078e5db376fbe45c528d9cce2c13e3175ba5 100644 --- a/crates/agent_ui/src/thread_history.rs +++ b/crates/agent_ui/src/thread_history.rs @@ -166,14 +166,13 @@ impl ThreadHistory { this.all_entries.len().saturating_sub(1), cx, ); - } else if let Some(prev_id) = previously_selected_entry { - if let Some(new_ix) = this + } else if let Some(prev_id) = previously_selected_entry + && let Some(new_ix) = this .all_entries .iter() .position(|probe| probe.id() == prev_id) - { - this.set_selected_entry_index(new_ix, cx); - } + { + this.set_selected_entry_index(new_ix, cx); } } SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => { diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 06abbad39f5d9bb80addcc089ccf655409826425..151586564f159f826f8c87b6dea82d7b271fcd9a 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -1076,20 +1076,20 @@ impl AssistantContext { timestamp, .. } => { - if let Some(slash_command) = self.invoked_slash_commands.get_mut(&id) { - if timestamp > slash_command.timestamp { - slash_command.timestamp = timestamp; - match error_message { - Some(message) => { - slash_command.status = - InvokedSlashCommandStatus::Error(message.into()); - } - None => { - slash_command.status = InvokedSlashCommandStatus::Finished; - } + if let Some(slash_command) = self.invoked_slash_commands.get_mut(&id) + && timestamp > slash_command.timestamp + { + slash_command.timestamp = timestamp; + match error_message { + Some(message) => { + slash_command.status = + InvokedSlashCommandStatus::Error(message.into()); + } + None => { + slash_command.status = InvokedSlashCommandStatus::Finished; } - cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id }); } + cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id }); } } ContextOperation::BufferOperation(_) => unreachable!(), @@ -1368,10 +1368,10 @@ impl AssistantContext { continue; } - if let Some(last_anchor) = last_anchor { - if message.id == last_anchor { - hit_last_anchor = true; - } + if let Some(last_anchor) = last_anchor + && message.id == last_anchor + { + hit_last_anchor = true; } new_anchor_needs_caching = new_anchor_needs_caching @@ -1406,10 +1406,10 @@ impl AssistantContext { if !self.pending_completions.is_empty() { return; } - if let Some(cache_configuration) = cache_configuration { - if !cache_configuration.should_speculate { - return; - } + if let Some(cache_configuration) = cache_configuration + && !cache_configuration.should_speculate + { + return; } let request = { @@ -1552,25 +1552,24 @@ impl AssistantContext { }) .map(ToOwned::to_owned) .collect::<SmallVec<_>>(); - if let Some(command) = self.slash_commands.command(name, cx) { - if !command.requires_argument() || !arguments.is_empty() { - let start_ix = offset + command_line.name.start - 1; - let end_ix = offset - + command_line - .arguments - .last() - .map_or(command_line.name.end, |argument| argument.end); - let source_range = - buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix); - let pending_command = ParsedSlashCommand { - name: name.to_string(), - arguments, - source_range, - status: PendingSlashCommandStatus::Idle, - }; - updated.push(pending_command.clone()); - new_commands.push(pending_command); - } + if let Some(command) = self.slash_commands.command(name, cx) + && (!command.requires_argument() || !arguments.is_empty()) + { + let start_ix = offset + command_line.name.start - 1; + let end_ix = offset + + command_line + .arguments + .last() + .map_or(command_line.name.end, |argument| argument.end); + let source_range = buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix); + let pending_command = ParsedSlashCommand { + name: name.to_string(), + arguments, + source_range, + status: PendingSlashCommandStatus::Idle, + }; + updated.push(pending_command.clone()); + new_commands.push(pending_command); } } @@ -1799,14 +1798,13 @@ impl AssistantContext { }); let end = this.buffer.read(cx).anchor_before(insert_position); - if run_commands_in_text { - if let Some(invoked_slash_command) = + if run_commands_in_text + && let Some(invoked_slash_command) = this.invoked_slash_commands.get_mut(&command_id) - { - invoked_slash_command - .run_commands_in_ranges - .push(start..end); - } + { + invoked_slash_command + .run_commands_in_ranges + .push(start..end); } } SlashCommandEvent::EndSection => { @@ -2741,10 +2739,10 @@ impl AssistantContext { } this.read_with(cx, |this, _cx| { - if let Some(summary) = this.summary.content() { - if summary.text.is_empty() { - bail!("Model generated an empty summary"); - } + if let Some(summary) = this.summary.content() + && summary.text.is_empty() + { + bail!("Model generated an empty summary"); } Ok(()) })??; @@ -2924,18 +2922,18 @@ impl AssistantContext { fs.create_dir(contexts_dir().as_ref()).await?; // rename before write ensures that only one file exists - if let Some(old_path) = old_path.as_ref() { - if new_path.as_path() != old_path.as_ref() { - fs.rename( - old_path, - &new_path, - RenameOptions { - overwrite: true, - ignore_if_exists: true, - }, - ) - .await?; - } + if let Some(old_path) = old_path.as_ref() + && new_path.as_path() != old_path.as_ref() + { + fs.rename( + old_path, + &new_path, + RenameOptions { + overwrite: true, + ignore_if_exists: true, + }, + ) + .await?; } // update path before write in case it fails diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index 622d8867a7194924f0a7eacb520fe4e26f29539b..af43b912e94ec8d64ebdef2f538a224751b8d648 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -894,34 +894,33 @@ impl ContextStore { return; }; - if protocol.capable(context_server::protocol::ServerCapability::Prompts) { - if let Some(response) = protocol + if protocol.capable(context_server::protocol::ServerCapability::Prompts) + && let Some(response) = protocol .request::<context_server::types::requests::PromptsList>(()) .await .log_err() - { - let slash_command_ids = response - .prompts - .into_iter() - .filter(assistant_slash_commands::acceptable_prompt) - .map(|prompt| { - log::info!("registering context server command: {:?}", prompt.name); - slash_command_working_set.insert(Arc::new( - assistant_slash_commands::ContextServerSlashCommand::new( - context_server_store.clone(), - server.id(), - prompt, - ), - )) - }) - .collect::<Vec<_>>(); - - this.update(cx, |this, _cx| { - this.context_server_slash_command_ids - .insert(server_id.clone(), slash_command_ids); + { + let slash_command_ids = response + .prompts + .into_iter() + .filter(assistant_slash_commands::acceptable_prompt) + .map(|prompt| { + log::info!("registering context server command: {:?}", prompt.name); + slash_command_working_set.insert(Arc::new( + assistant_slash_commands::ContextServerSlashCommand::new( + context_server_store.clone(), + server.id(), + prompt, + ), + )) }) - .log_err(); - } + .collect::<Vec<_>>(); + + this.update(cx, |this, _cx| { + this.context_server_slash_command_ids + .insert(server_id.clone(), slash_command_ids); + }) + .log_err(); } }) .detach(); diff --git a/crates/assistant_slash_commands/src/context_server_command.rs b/crates/assistant_slash_commands/src/context_server_command.rs index 15f3901bfbd60c14d576108272ebf27caf965061..219c3b30bc8328fd900299adfdc406167f5f341d 100644 --- a/crates/assistant_slash_commands/src/context_server_command.rs +++ b/crates/assistant_slash_commands/src/context_server_command.rs @@ -39,10 +39,10 @@ impl SlashCommand for ContextServerSlashCommand { fn label(&self, cx: &App) -> language::CodeLabel { let mut parts = vec![self.prompt.name.as_str()]; - if let Some(args) = &self.prompt.arguments { - if let Some(arg) = args.first() { - parts.push(arg.name.as_str()); - } + if let Some(args) = &self.prompt.arguments + && let Some(arg) = args.first() + { + parts.push(arg.name.as_str()); } create_label_for_command(parts[0], &parts[1..], cx) } diff --git a/crates/assistant_slash_commands/src/delta_command.rs b/crates/assistant_slash_commands/src/delta_command.rs index 8c840c17b2c7fe9d8c8995b21c35cb35980dd71b..2cc4591386633ef85ae180c5fa0a802887485e7e 100644 --- a/crates/assistant_slash_commands/src/delta_command.rs +++ b/crates/assistant_slash_commands/src/delta_command.rs @@ -66,23 +66,22 @@ impl SlashCommand for DeltaSlashCommand { .metadata .as_ref() .and_then(|value| serde_json::from_value::<FileCommandMetadata>(value.clone()).ok()) + && paths.insert(metadata.path.clone()) { - if paths.insert(metadata.path.clone()) { - file_command_old_outputs.push( - context_buffer - .as_rope() - .slice(section.range.to_offset(&context_buffer)), - ); - file_command_new_outputs.push(Arc::new(FileSlashCommand).run( - std::slice::from_ref(&metadata.path), - context_slash_command_output_sections, - context_buffer.clone(), - workspace.clone(), - delegate.clone(), - window, - cx, - )); - } + file_command_old_outputs.push( + context_buffer + .as_rope() + .slice(section.range.to_offset(&context_buffer)), + ); + file_command_new_outputs.push(Arc::new(FileSlashCommand).run( + std::slice::from_ref(&metadata.path), + context_slash_command_output_sections, + context_buffer.clone(), + workspace.clone(), + delegate.clone(), + window, + cx, + )); } } @@ -95,25 +94,25 @@ impl SlashCommand for DeltaSlashCommand { .into_iter() .zip(file_command_new_outputs) { - if let Ok(new_output) = new_output { - if let Ok(new_output) = SlashCommandOutput::from_event_stream(new_output).await - { - if let Some(file_command_range) = new_output.sections.first() { - let new_text = &new_output.text[file_command_range.range.clone()]; - if old_text.chars().ne(new_text.chars()) { - changes_detected = true; - output.sections.extend(new_output.sections.into_iter().map( - |section| SlashCommandOutputSection { - range: output.text.len() + section.range.start - ..output.text.len() + section.range.end, - icon: section.icon, - label: section.label, - metadata: section.metadata, - }, - )); - output.text.push_str(&new_output.text); - } - } + if let Ok(new_output) = new_output + && let Ok(new_output) = SlashCommandOutput::from_event_stream(new_output).await + && let Some(file_command_range) = new_output.sections.first() + { + let new_text = &new_output.text[file_command_range.range.clone()]; + if old_text.chars().ne(new_text.chars()) { + changes_detected = true; + output + .sections + .extend(new_output.sections.into_iter().map(|section| { + SlashCommandOutputSection { + range: output.text.len() + section.range.start + ..output.text.len() + section.range.end, + icon: section.icon, + label: section.label, + metadata: section.metadata, + } + })); + output.text.push_str(&new_output.text); } } } diff --git a/crates/assistant_slash_commands/src/diagnostics_command.rs b/crates/assistant_slash_commands/src/diagnostics_command.rs index 31014f8fb80435e09619669e6cc2e800eed473e2..45c976c82611ae36b4d3b65306c88d6d7f93e80e 100644 --- a/crates/assistant_slash_commands/src/diagnostics_command.rs +++ b/crates/assistant_slash_commands/src/diagnostics_command.rs @@ -280,10 +280,10 @@ fn collect_diagnostics( let mut project_summary = DiagnosticSummary::default(); for (project_path, path, summary) in diagnostic_summaries { - if let Some(path_matcher) = &options.path_matcher { - if !path_matcher.is_match(&path) { - continue; - } + if let Some(path_matcher) = &options.path_matcher + && !path_matcher.is_match(&path) + { + continue; } project_summary.error_count += summary.error_count; diff --git a/crates/assistant_slash_commands/src/tab_command.rs b/crates/assistant_slash_commands/src/tab_command.rs index ca7601bc4c3a48d9d9c352ad545d72c032e7c47e..e4ae391a9c9482b3961b6cc8ffd98e2ae0627fd2 100644 --- a/crates/assistant_slash_commands/src/tab_command.rs +++ b/crates/assistant_slash_commands/src/tab_command.rs @@ -195,16 +195,14 @@ fn tab_items_for_queries( } for editor in workspace.items_of_type::<Editor>(cx) { - if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() { - if let Some(timestamp) = + if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() + && let Some(timestamp) = timestamps_by_entity_id.get(&editor.entity_id()) - { - if visited_buffers.insert(buffer.read(cx).remote_id()) { - let snapshot = buffer.read(cx).snapshot(); - let full_path = snapshot.resolve_file_path(cx, true); - open_buffers.push((full_path, snapshot, *timestamp)); - } - } + && visited_buffers.insert(buffer.read(cx).remote_id()) + { + let snapshot = buffer.read(cx).snapshot(); + let full_path = snapshot.resolve_file_path(cx, true); + open_buffers.push((full_path, snapshot, *timestamp)); } } diff --git a/crates/assistant_tool/src/tool_schema.rs b/crates/assistant_tool/src/tool_schema.rs index 7b48f93ba6d23bcc1a6e2cf051737efaf69fa595..192f7c8a2bb565ece01a3472a9e46dad316377f4 100644 --- a/crates/assistant_tool/src/tool_schema.rs +++ b/crates/assistant_tool/src/tool_schema.rs @@ -24,16 +24,16 @@ pub fn adapt_schema_to_format( fn preprocess_json_schema(json: &mut Value) -> Result<()> { // `additionalProperties` defaults to `false` unless explicitly specified. // This prevents models from hallucinating tool parameters. - if let Value::Object(obj) = json { - if matches!(obj.get("type"), Some(Value::String(s)) if s == "object") { - if !obj.contains_key("additionalProperties") { - obj.insert("additionalProperties".to_string(), Value::Bool(false)); - } + if let Value::Object(obj) = json + && matches!(obj.get("type"), Some(Value::String(s)) if s == "object") + { + if !obj.contains_key("additionalProperties") { + obj.insert("additionalProperties".to_string(), Value::Bool(false)); + } - // OpenAI API requires non-missing `properties` - if !obj.contains_key("properties") { - obj.insert("properties".to_string(), Value::Object(Default::default())); - } + // OpenAI API requires non-missing `properties` + if !obj.contains_key("properties") { + obj.insert("properties".to_string(), Value::Object(Default::default())); } } Ok(()) @@ -59,10 +59,10 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> { ("optional", |value| value.is_boolean()), ]; for (key, predicate) in KEYS_TO_REMOVE { - if let Some(value) = obj.get(key) { - if predicate(value) { - obj.remove(key); - } + if let Some(value) = obj.get(key) + && predicate(value) + { + obj.remove(key); } } @@ -77,12 +77,12 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> { } // Handle oneOf -> anyOf conversion - if let Some(subschemas) = obj.get_mut("oneOf") { - if subschemas.is_array() { - let subschemas_clone = subschemas.clone(); - obj.remove("oneOf"); - obj.insert("anyOf".to_string(), subschemas_clone); - } + if let Some(subschemas) = obj.get_mut("oneOf") + && subschemas.is_array() + { + let subschemas_clone = subschemas.clone(); + obj.remove("oneOf"); + obj.insert("anyOf".to_string(), subschemas_clone); } // Recursively process all nested objects and arrays diff --git a/crates/assistant_tools/src/edit_agent.rs b/crates/assistant_tools/src/edit_agent.rs index aa321aa8f30117e21a04e4acb52b5c5cdbfedfaa..665ece2baaeed0dac32e5c0153ec1d79fef47f12 100644 --- a/crates/assistant_tools/src/edit_agent.rs +++ b/crates/assistant_tools/src/edit_agent.rs @@ -672,29 +672,30 @@ impl EditAgent { cx: &mut AsyncApp, ) -> Result<BoxStream<'static, Result<String, LanguageModelCompletionError>>> { let mut messages_iter = conversation.messages.iter_mut(); - if let Some(last_message) = messages_iter.next_back() { - if last_message.role == Role::Assistant { - let old_content_len = last_message.content.len(); - last_message - .content - .retain(|content| !matches!(content, MessageContent::ToolUse(_))); - let new_content_len = last_message.content.len(); - - // We just removed pending tool uses from the content of the - // last message, so it doesn't make sense to cache it anymore - // (e.g., the message will look very different on the next - // request). Thus, we move the flag to the message prior to it, - // as it will still be a valid prefix of the conversation. - if old_content_len != new_content_len && last_message.cache { - if let Some(prev_message) = messages_iter.next_back() { - last_message.cache = false; - prev_message.cache = true; - } - } + if let Some(last_message) = messages_iter.next_back() + && last_message.role == Role::Assistant + { + let old_content_len = last_message.content.len(); + last_message + .content + .retain(|content| !matches!(content, MessageContent::ToolUse(_))); + let new_content_len = last_message.content.len(); + + // We just removed pending tool uses from the content of the + // last message, so it doesn't make sense to cache it anymore + // (e.g., the message will look very different on the next + // request). Thus, we move the flag to the message prior to it, + // as it will still be a valid prefix of the conversation. + if old_content_len != new_content_len + && last_message.cache + && let Some(prev_message) = messages_iter.next_back() + { + last_message.cache = false; + prev_message.cache = true; + } - if last_message.content.is_empty() { - conversation.messages.pop(); - } + if last_message.content.is_empty() { + conversation.messages.pop(); } } diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index 9a8e7624559e9a1284ace7c932f428c7389b6254..0d529a55735d07f15e05fbc50ec8b1f1230b50fa 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -1283,14 +1283,14 @@ impl EvalAssertion { // Parse the score from the response let re = regex::Regex::new(r"<score>(\d+)</score>").unwrap(); - if let Some(captures) = re.captures(&output) { - if let Some(score_match) = captures.get(1) { - let score = score_match.as_str().parse().unwrap_or(0); - return Ok(EvalAssertionOutcome { - score, - message: Some(output), - }); - } + if let Some(captures) = re.captures(&output) + && let Some(score_match) = captures.get(1) + { + let score = score_match.as_str().parse().unwrap_or(0); + return Ok(EvalAssertionOutcome { + score, + message: Some(output), + }); } anyhow::bail!("No score found in response. Raw output: {output}"); diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 039f9d9316f7849fc36a814d3d3d56df8fa3b0e5..2d6b5ce92450ccd2e59c69538bcc65d3f3644bc9 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -155,10 +155,10 @@ impl Tool for EditFileTool { // It's also possible that the global config dir is configured to be inside the project, // so check for that edge case too. - if let Ok(canonical_path) = std::fs::canonicalize(&input.path) { - if canonical_path.starts_with(paths::config_dir()) { - return true; - } + if let Ok(canonical_path) = std::fs::canonicalize(&input.path) + && canonical_path.starts_with(paths::config_dir()) + { + return true; } // Check if path is inside the global config directory @@ -199,10 +199,10 @@ impl Tool for EditFileTool { .any(|c| c.as_os_str() == local_settings_folder.as_os_str()) { description.push_str(" (local settings)"); - } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) { - if canonical_path.starts_with(paths::config_dir()) { - description.push_str(" (global settings)"); - } + } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) + && canonical_path.starts_with(paths::config_dir()) + { + description.push_str(" (global settings)"); } description diff --git a/crates/assistant_tools/src/grep_tool.rs b/crates/assistant_tools/src/grep_tool.rs index 1f00332c5ae7b8df8adf9767533859288afa663f..1dd74b99e752b610a090f79ee1d894881f1fa5f8 100644 --- a/crates/assistant_tools/src/grep_tool.rs +++ b/crates/assistant_tools/src/grep_tool.rs @@ -188,15 +188,14 @@ impl Tool for GrepTool { // Check if this file should be excluded based on its worktree settings if let Ok(Some(project_path)) = project.read_with(cx, |project, cx| { project.find_project_path(&path, cx) - }) { - if cx.update(|cx| { + }) + && cx.update(|cx| { let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); worktree_settings.is_path_excluded(&project_path.path) || worktree_settings.is_path_private(&project_path.path) }).unwrap_or(false) { continue; } - } while *parse_status.borrow() != ParseStatus::Idle { parse_status.changed().await?; @@ -284,12 +283,11 @@ impl Tool for GrepTool { output.extend(snapshot.text_for_range(range)); output.push_str("\n```\n"); - if let Some(ancestor_range) = ancestor_range { - if end_row < ancestor_range.end.row { + if let Some(ancestor_range) = ancestor_range + && end_row < ancestor_range.end.row { let remaining_lines = ancestor_range.end.row - end_row; writeln!(output, "\n{} lines remaining in ancestor node. Read the file to see all.", remaining_lines)?; } - } matches_found += 1; } diff --git a/crates/assistant_tools/src/schema.rs b/crates/assistant_tools/src/schema.rs index 10a8bf0acd99131d2c0a80411072f312c9a42f50..dab7384efd8ba23669db645c87dcf79e95538d3a 100644 --- a/crates/assistant_tools/src/schema.rs +++ b/crates/assistant_tools/src/schema.rs @@ -43,12 +43,11 @@ impl Transform for ToJsonSchemaSubsetTransform { fn transform(&mut self, schema: &mut Schema) { // Ensure that the type field is not an array, this happens when we use // Option<T>, the type will be [T, "null"]. - if let Some(type_field) = schema.get_mut("type") { - if let Some(types) = type_field.as_array() { - if let Some(first_type) = types.first() { - *type_field = first_type.clone(); - } - } + if let Some(type_field) = schema.get_mut("type") + && let Some(types) = type_field.as_array() + && let Some(first_type) = types.first() + { + *type_field = first_type.clone(); } // oneOf is not supported, use anyOf instead diff --git a/crates/auto_update_helper/src/dialog.rs b/crates/auto_update_helper/src/dialog.rs index 757819df519a533fb79aa21bec5bed8c5a077590..903ac34da227b2929705ff2af72db3770cff6532 100644 --- a/crates/auto_update_helper/src/dialog.rs +++ b/crates/auto_update_helper/src/dialog.rs @@ -186,11 +186,11 @@ unsafe extern "system" fn wnd_proc( }), WM_TERMINATE => { with_dialog_data(hwnd, |data| { - if let Ok(result) = data.borrow_mut().rx.recv() { - if let Err(e) = result { - log::error!("Failed to update Zed: {:?}", e); - show_error(format!("Error: {:?}", e)); - } + if let Ok(result) = data.borrow_mut().rx.recv() + && let Err(e) = result + { + log::error!("Failed to update Zed: {:?}", e); + show_error(format!("Error: {:?}", e)); } }); unsafe { PostQuitMessage(0) }; diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 990fc27fbd40288a8850a48ac5a91116e4a2f23c..a6b27476fe36b1143103e1acd035bda6cda15132 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -82,11 +82,12 @@ impl Render for Breadcrumbs { } text_style.color = Color::Muted.color(cx); - if index == 0 && !TabBarSettings::get_global(cx).show && active_item.is_dirty(cx) { - if let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx) - { - return styled_element; - } + if index == 0 + && !TabBarSettings::get_global(cx).show + && active_item.is_dirty(cx) + && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx) + { + return styled_element; } StyledText::new(segment.text.replace('\n', "⏎")) diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index e20ea9713fbfb0e58df9a86b166e5629a88e2dd1..6b38fe557672726a690cca14b8e93085438726c0 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -572,14 +572,14 @@ impl BufferDiffInner { pending_range.end.column = 0; } - if pending_range == (start_point..end_point) { - if !buffer.has_edits_since_in_range( + if pending_range == (start_point..end_point) + && !buffer.has_edits_since_in_range( &pending_hunk.buffer_version, start_anchor..end_anchor, - ) { - has_pending = true; - secondary_status = pending_hunk.new_status; - } + ) + { + has_pending = true; + secondary_status = pending_hunk.new_status; } } @@ -1036,16 +1036,15 @@ impl BufferDiff { _ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)), }; - if let Some(secondary_changed_range) = secondary_diff_change { - if let Some(secondary_hunk_range) = + if let Some(secondary_changed_range) = secondary_diff_change + && let Some(secondary_hunk_range) = self.range_to_hunk_range(secondary_changed_range, buffer, cx) - { - if let Some(range) = &mut changed_range { - range.start = secondary_hunk_range.start.min(&range.start, buffer); - range.end = secondary_hunk_range.end.max(&range.end, buffer); - } else { - changed_range = Some(secondary_hunk_range); - } + { + if let Some(range) = &mut changed_range { + range.start = secondary_hunk_range.start.min(&range.start, buffer); + range.end = secondary_hunk_range.end.max(&range.end, buffer); + } else { + changed_range = Some(secondary_hunk_range); } } diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index 73cb8518a63781e8c2b3a92aa7b3111996f05f83..bab99cd3f32c409b9b549df6b5276868a2e3951d 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -827,24 +827,23 @@ impl Room { ); Audio::play_sound(Sound::Joined, cx); - if let Some(livekit_participants) = &livekit_participants { - if let Some(livekit_participant) = livekit_participants + if let Some(livekit_participants) = &livekit_participants + && let Some(livekit_participant) = livekit_participants .get(&ParticipantIdentity(user.id.to_string())) + { + for publication in + livekit_participant.track_publications().into_values() { - for publication in - livekit_participant.track_publications().into_values() - { - if let Some(track) = publication.track() { - this.livekit_room_updated( - RoomEvent::TrackSubscribed { - track, - publication, - participant: livekit_participant.clone(), - }, - cx, - ) - .warn_on_err(); - } + if let Some(track) = publication.track() { + this.livekit_room_updated( + RoomEvent::TrackSubscribed { + track, + publication, + participant: livekit_participant.clone(), + }, + cx, + ) + .warn_on_err(); } } } @@ -940,10 +939,9 @@ impl Room { self.client.user_id() ) })?; - if self.live_kit.as_ref().map_or(true, |kit| kit.deafened) { - if publication.is_audio() { - publication.set_enabled(false, cx); - } + if self.live_kit.as_ref().map_or(true, |kit| kit.deafened) && publication.is_audio() + { + publication.set_enabled(false, cx); } match track { livekit_client::RemoteTrack::Audio(track) => { @@ -1005,10 +1003,10 @@ impl Room { for (sid, participant) in &mut self.remote_participants { participant.speaking = speaker_ids.binary_search(sid).is_ok(); } - if let Some(id) = self.client.user_id() { - if let Some(room) = &mut self.live_kit { - room.speaking = speaker_ids.binary_search(&id).is_ok(); - } + if let Some(id) = self.client.user_id() + && let Some(room) = &mut self.live_kit + { + room.speaking = speaker_ids.binary_search(&id).is_ok(); } } @@ -1042,18 +1040,16 @@ impl Room { if let LocalTrack::Published { track_publication, .. } = &room.microphone_track + && track_publication.sid() == publication.sid() { - if track_publication.sid() == publication.sid() { - room.microphone_track = LocalTrack::None; - } + room.microphone_track = LocalTrack::None; } if let LocalTrack::Published { track_publication, .. } = &room.screen_track + && track_publication.sid() == publication.sid() { - if track_publication.sid() == publication.sid() { - room.screen_track = LocalTrack::None; - } + room.screen_track = LocalTrack::None; } } } @@ -1484,10 +1480,8 @@ impl Room { self.set_deafened(deafened, cx); - if should_change_mute { - if let Some(task) = self.set_mute(deafened, cx) { - task.detach_and_log_err(cx); - } + if should_change_mute && let Some(task) = self.set_mute(deafened, cx) { + task.detach_and_log_err(cx); } } } diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 183f7eb3c6a47dad4cb35b95dd2d3e096e0a612f..a367ffbf099f07ba83c8bc902c8463c4372cf464 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -191,12 +191,11 @@ impl ChannelBuffer { operation, is_local: true, } => { - if *ZED_ALWAYS_ACTIVE { - if let language::Operation::UpdateSelections { selections, .. } = operation { - if selections.is_empty() { - return; - } - } + if *ZED_ALWAYS_ACTIVE + && let language::Operation::UpdateSelections { selections, .. } = operation + && selections.is_empty() + { + return; } let operation = language::proto::serialize_operation(operation); self.client diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 4ac37ffd14ca2602756afecc788aae9f6065cad9..02b5ccec6809d068912dba410b7c2b21d59ae261 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -329,24 +329,24 @@ impl ChannelChat { loop { let step = chat .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::<Dimensions<ChannelMessageId, Count>>(&()); - let message_id = ChannelMessageId::Saved(message_id); - cursor.seek(&message_id, Bias::Left); - return ControlFlow::Break( - if cursor - .item() - .map_or(false, |message| message.id == message_id) - { - Some(cursor.start().1.0) - } else { - None - }, - ); - } + if let Some(first_id) = chat.first_loaded_message_id() + && first_id <= message_id + { + let mut cursor = chat + .messages + .cursor::<Dimensions<ChannelMessageId, Count>>(&()); + let message_id = ChannelMessageId::Saved(message_id); + cursor.seek(&message_id, Bias::Left); + return ControlFlow::Break( + if cursor + .item() + .map_or(false, |message| message.id == message_id) + { + Some(cursor.start().1.0) + } else { + None + }, + ); } ControlFlow::Continue(chat.load_more_messages(cx)) }) @@ -359,22 +359,21 @@ impl ChannelChat { } pub fn acknowledge_last_message(&mut self, cx: &mut Context<Self>) { - if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id { - if self + if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id + && self .last_acknowledged_id .map_or(true, |acknowledged_id| acknowledged_id < latest_message_id) - { - self.rpc - .send(proto::AckChannelMessage { - channel_id: self.channel_id.0, - message_id: latest_message_id, - }) - .ok(); - self.last_acknowledged_id = Some(latest_message_id); - self.channel_store.update(cx, |store, cx| { - store.acknowledge_message_id(self.channel_id, latest_message_id, cx); - }); - } + { + self.rpc + .send(proto::AckChannelMessage { + channel_id: self.channel_id.0, + message_id: latest_message_id, + }) + .ok(); + self.last_acknowledged_id = Some(latest_message_id); + self.channel_store.update(cx, |store, cx| { + store.acknowledge_message_id(self.channel_id, latest_message_id, cx); + }); } } @@ -407,10 +406,10 @@ impl ChannelChat { let missing_ancestors = loaded_messages .iter() .filter_map(|message| { - if let Some(ancestor_id) = message.reply_to_message_id { - if !loaded_message_ids.contains(&ancestor_id) { - return Some(ancestor_id); - } + if let Some(ancestor_id) = message.reply_to_message_id + && !loaded_message_ids.contains(&ancestor_id) + { + return Some(ancestor_id); } None }) @@ -646,32 +645,32 @@ impl ChannelChat { fn message_removed(&mut self, id: u64, cx: &mut Context<Self>) { let mut cursor = self.messages.cursor::<ChannelMessageId>(&()); let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left); - if let Some(item) = cursor.item() { - if item.id == ChannelMessageId::Saved(id) { - let deleted_message_ix = messages.summary().count; - cursor.next(); - messages.append(cursor.suffix(), &()); - drop(cursor); - self.messages = messages; - - // If the message that was deleted was the last acknowledged message, - // replace the acknowledged message with an earlier one. - self.channel_store.update(cx, |store, _| { - let summary = self.messages.summary(); - if summary.count == 0 { - store.set_acknowledged_message_id(self.channel_id, None); - } else if deleted_message_ix == summary.count { - if let ChannelMessageId::Saved(id) = summary.max_id { - store.set_acknowledged_message_id(self.channel_id, Some(id)); - } - } - }); + if let Some(item) = cursor.item() + && item.id == ChannelMessageId::Saved(id) + { + let deleted_message_ix = messages.summary().count; + cursor.next(); + messages.append(cursor.suffix(), &()); + drop(cursor); + self.messages = messages; + + // If the message that was deleted was the last acknowledged message, + // replace the acknowledged message with an earlier one. + self.channel_store.update(cx, |store, _| { + let summary = self.messages.summary(); + if summary.count == 0 { + store.set_acknowledged_message_id(self.channel_id, None); + } else if deleted_message_ix == summary.count + && let ChannelMessageId::Saved(id) = summary.max_id + { + store.set_acknowledged_message_id(self.channel_id, Some(id)); + } + }); - cx.emit(ChannelChatEvent::MessagesUpdated { - old_range: deleted_message_ix..deleted_message_ix + 1, - new_count: 0, - }); - } + cx.emit(ChannelChatEvent::MessagesUpdated { + old_range: deleted_message_ix..deleted_message_ix + 1, + new_count: 0, + }); } } diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 4ad156b9fb08e8af95e5ea49132c4c4786e348a1..6d1716a7eaf5e0a4498fc7e80f2ec026232b034d 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -262,13 +262,12 @@ impl ChannelStore { } } status = status_receiver.next().fuse() => { - if let Some(status) = status { - if status.is_connected() { + if let Some(status) = status + && status.is_connected() { this.update(cx, |this, _cx| { this.initialize(); }).ok(); } - } continue; } _ = timer => { @@ -336,10 +335,10 @@ impl ChannelStore { } pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &App) -> bool { - if let Some(buffer) = self.opened_buffers.get(&channel_id) { - if let OpenEntityHandle::Open(buffer) = buffer { - return buffer.upgrade().is_some(); - } + if let Some(buffer) = self.opened_buffers.get(&channel_id) + && let OpenEntityHandle::Open(buffer) = buffer + { + return buffer.upgrade().is_some(); } false } @@ -408,13 +407,12 @@ impl ChannelStore { pub fn last_acknowledge_message_id(&self, channel_id: ChannelId) -> Option<u64> { self.channel_states.get(&channel_id).and_then(|state| { - if let Some(last_message_id) = state.latest_chat_message { - if state + if let Some(last_message_id) = state.latest_chat_message + && state .last_acknowledged_message_id() .is_some_and(|id| id < last_message_id) - { - return state.last_acknowledged_message_id(); - } + { + return state.last_acknowledged_message_id(); } None @@ -962,27 +960,27 @@ impl ChannelStore { self.disconnect_channel_buffers_task.take(); for chat in self.opened_chats.values() { - if let OpenEntityHandle::Open(chat) = chat { - if let Some(chat) = chat.upgrade() { - chat.update(cx, |chat, cx| { - chat.rejoin(cx); - }); - } + if let OpenEntityHandle::Open(chat) = chat + && let Some(chat) = chat.upgrade() + { + chat.update(cx, |chat, cx| { + chat.rejoin(cx); + }); } } let mut buffer_versions = Vec::new(); for buffer in self.opened_buffers.values() { - if let OpenEntityHandle::Open(buffer) = buffer { - if let Some(buffer) = buffer.upgrade() { - let channel_buffer = buffer.read(cx); - let buffer = channel_buffer.buffer().read(cx); - buffer_versions.push(proto::ChannelBufferVersion { - channel_id: channel_buffer.channel_id.0, - epoch: channel_buffer.epoch(), - version: language::proto::serialize_version(&buffer.version()), - }); - } + if let OpenEntityHandle::Open(buffer) = buffer + && let Some(buffer) = buffer.upgrade() + { + let channel_buffer = buffer.read(cx); + let buffer = channel_buffer.buffer().read(cx); + buffer_versions.push(proto::ChannelBufferVersion { + channel_id: channel_buffer.channel_id.0, + epoch: channel_buffer.epoch(), + version: language::proto::serialize_version(&buffer.version()), + }); } } @@ -1078,10 +1076,10 @@ impl ChannelStore { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { for (_, buffer) in &this.opened_buffers { - if let OpenEntityHandle::Open(buffer) = &buffer { - if let Some(buffer) = buffer.upgrade() { - buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); - } + if let OpenEntityHandle::Open(buffer) = &buffer + && let Some(buffer) = buffer.upgrade() + { + buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); } } }) @@ -1157,10 +1155,9 @@ impl ChannelStore { } if let Some(OpenEntityHandle::Open(buffer)) = self.opened_buffers.remove(&channel_id) + && let Some(buffer) = buffer.upgrade() { - if let Some(buffer) = buffer.upgrade() { - buffer.update(cx, ChannelBuffer::disconnect); - } + buffer.update(cx, ChannelBuffer::disconnect); } } } @@ -1170,12 +1167,11 @@ impl ChannelStore { let id = ChannelId(channel.id); let channel_changed = index.insert(channel); - if channel_changed { - if let Some(OpenEntityHandle::Open(buffer)) = self.opened_buffers.get(&id) { - if let Some(buffer) = buffer.upgrade() { - buffer.update(cx, ChannelBuffer::channel_changed); - } - } + if channel_changed + && let Some(OpenEntityHandle::Open(buffer)) = self.opened_buffers.get(&id) + && let Some(buffer) = buffer.upgrade() + { + buffer.update(cx, ChannelBuffer::channel_changed); } } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a61d8e09112418b466889b6b4426f51a48d3f651..d8b46dabb69e803aa4be2cf77b8fb11ad08d6d5f 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -587,13 +587,10 @@ mod flatpak { pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args { if env::var(NO_ESCAPE_ENV_NAME).is_ok() && env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed")) + && args.zed.is_none() { - if args.zed.is_none() { - args.zed = Some("/app/libexec/zed-editor".into()); - unsafe { - env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed") - }; - } + args.zed = Some("/app/libexec/zed-editor".into()); + unsafe { env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed") }; } args } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 66d5fd89b151431272f5d2ce138b896acfc414ea..d7d8b602119f8f11627f47832b8dc5511b1f3220 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -864,22 +864,23 @@ impl Client { let mut credentials = None; let old_credentials = self.state.read().credentials.clone(); - if let Some(old_credentials) = old_credentials { - if self.validate_credentials(&old_credentials, cx).await? { - credentials = Some(old_credentials); - } + if let Some(old_credentials) = old_credentials + && 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.validate_credentials(&stored_credentials, cx).await? { - credentials = Some(stored_credentials); - } else { - self.credentials_provider - .delete_credentials(cx) - .await - .log_err(); - } + if credentials.is_none() + && try_provider + && let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await + { + if self.validate_credentials(&stored_credentials, cx).await? { + credentials = Some(stored_credentials); + } else { + self.credentials_provider + .delete_credentials(cx) + .await + .log_err(); } } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index da7f50076b38a7ecf5dbce5a8f229f2912629409..3509a8c57fe5c114fe66d9ded7ddf7c204c06086 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -894,10 +894,10 @@ impl UserStore { let mut ret = Vec::with_capacity(users.len()); for user in users { let user = User::new(user); - if let Some(old) = self.users.insert(user.id, user.clone()) { - if old.github_login != user.github_login { - self.by_github_login.remove(&old.github_login); - } + if let Some(old) = self.users.insert(user.id, user.clone()) + && old.github_login != user.github_login + { + self.by_github_login.remove(&old.github_login); } self.by_github_login .insert(user.github_login.clone(), user.id); diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index cd1dc42e64460655c59f3cffe022dcc7a2ed431a..c500872fd787476d7b7197cd17c31de07575d5b6 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -149,35 +149,35 @@ pub async fn post_crash( "crash report" ); - if let Some(kinesis_client) = app.kinesis_client.clone() { - if let Some(stream) = app.config.kinesis_stream.clone() { - let properties = json!({ - "app_version": report.header.app_version, - "os_version": report.header.os_version, - "os_name": "macOS", - "bundle_id": report.header.bundle_id, - "incident_id": report.header.incident_id, - "installation_id": installation_id, - "description": description, - "backtrace": summary, - }); - let row = SnowflakeRow::new( - "Crash Reported", - None, - false, - Some(installation_id), - properties, - ); - let data = serde_json::to_vec(&row)?; - kinesis_client - .put_record() - .stream_name(stream) - .partition_key(row.insert_id.unwrap_or_default()) - .data(data.into()) - .send() - .await - .log_err(); - } + if let Some(kinesis_client) = app.kinesis_client.clone() + && let Some(stream) = app.config.kinesis_stream.clone() + { + let properties = json!({ + "app_version": report.header.app_version, + "os_version": report.header.os_version, + "os_name": "macOS", + "bundle_id": report.header.bundle_id, + "incident_id": report.header.incident_id, + "installation_id": installation_id, + "description": description, + "backtrace": summary, + }); + let row = SnowflakeRow::new( + "Crash Reported", + None, + false, + Some(installation_id), + properties, + ); + let data = serde_json::to_vec(&row)?; + kinesis_client + .put_record() + .stream_name(stream) + .partition_key(row.insert_id.unwrap_or_default()) + .data(data.into()) + .send() + .await + .log_err(); } if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() { @@ -359,34 +359,34 @@ pub async fn post_panic( "panic report" ); - if let Some(kinesis_client) = app.kinesis_client.clone() { - if let Some(stream) = app.config.kinesis_stream.clone() { - let properties = json!({ - "app_version": panic.app_version, - "os_name": panic.os_name, - "os_version": panic.os_version, - "incident_id": incident_id, - "installation_id": panic.installation_id, - "description": panic.payload, - "backtrace": backtrace, - }); - let row = SnowflakeRow::new( - "Panic Reported", - None, - false, - panic.installation_id.clone(), - properties, - ); - let data = serde_json::to_vec(&row)?; - kinesis_client - .put_record() - .stream_name(stream) - .partition_key(row.insert_id.unwrap_or_default()) - .data(data.into()) - .send() - .await - .log_err(); - } + if let Some(kinesis_client) = app.kinesis_client.clone() + && let Some(stream) = app.config.kinesis_stream.clone() + { + let properties = json!({ + "app_version": panic.app_version, + "os_name": panic.os_name, + "os_version": panic.os_version, + "incident_id": incident_id, + "installation_id": panic.installation_id, + "description": panic.payload, + "backtrace": backtrace, + }); + let row = SnowflakeRow::new( + "Panic Reported", + None, + false, + panic.installation_id.clone(), + properties, + ); + let data = serde_json::to_vec(&row)?; + kinesis_client + .put_record() + .stream_name(stream) + .partition_key(row.insert_id.unwrap_or_default()) + .data(data.into()) + .send() + .await + .log_err(); } if !report_to_slack(&panic) { @@ -518,31 +518,31 @@ pub async fn post_events( let first_event_at = chrono::Utc::now() - chrono::Duration::milliseconds(last_event.milliseconds_since_first_event); - if let Some(kinesis_client) = app.kinesis_client.clone() { - if let Some(stream) = app.config.kinesis_stream.clone() { - let mut request = kinesis_client.put_records().stream_name(stream); - let mut has_records = false; - for row in for_snowflake( - request_body.clone(), - first_event_at, - country_code.clone(), - checksum_matched, - ) { - if let Some(data) = serde_json::to_vec(&row).log_err() { - request = request.records( - aws_sdk_kinesis::types::PutRecordsRequestEntry::builder() - .partition_key(request_body.system_id.clone().unwrap_or_default()) - .data(data.into()) - .build() - .unwrap(), - ); - has_records = true; - } - } - if has_records { - request.send().await.log_err(); + if let Some(kinesis_client) = app.kinesis_client.clone() + && let Some(stream) = app.config.kinesis_stream.clone() + { + let mut request = kinesis_client.put_records().stream_name(stream); + let mut has_records = false; + for row in for_snowflake( + request_body.clone(), + first_event_at, + country_code.clone(), + checksum_matched, + ) { + if let Some(data) = serde_json::to_vec(&row).log_err() { + request = request.records( + aws_sdk_kinesis::types::PutRecordsRequestEntry::builder() + .partition_key(request_body.system_id.clone().unwrap_or_default()) + .data(data.into()) + .build() + .unwrap(), + ); + has_records = true; } } + if has_records { + request.send().await.log_err(); + } }; Ok(()) diff --git a/crates/collab/src/api/extensions.rs b/crates/collab/src/api/extensions.rs index 9170c39e472d33420fc889972e4c96e44914db15..1ace433db298be7ffd159128b54b194395ba4fe5 100644 --- a/crates/collab/src/api/extensions.rs +++ b/crates/collab/src/api/extensions.rs @@ -337,8 +337,7 @@ async fn fetch_extensions_from_blob_store( if known_versions .binary_search_by_key(&published_version, |known_version| known_version) .is_err() - { - if let Some(extension) = fetch_extension_manifest( + && let Some(extension) = fetch_extension_manifest( blob_store_client, blob_store_bucket, extension_id, @@ -346,12 +345,11 @@ async fn fetch_extensions_from_blob_store( ) .await .log_err() - { - new_versions - .entry(extension_id) - .or_default() - .push(extension); - } + { + new_versions + .entry(extension_id) + .or_default() + .push(extension); } } } diff --git a/crates/collab/src/auth.rs b/crates/collab/src/auth.rs index 00f37c675874ce200cf79f2e8763450f4494fc79..5a2a1329bbf0cf50cbfbd47c80148ea93b70d37e 100644 --- a/crates/collab/src/auth.rs +++ b/crates/collab/src/auth.rs @@ -79,27 +79,27 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into verify_access_token(access_token, user_id, &state.db).await }; - if let Ok(validate_result) = validate_result { - if validate_result.is_valid { - let user = state + if let Ok(validate_result) = validate_result + && validate_result.is_valid + { + let user = state + .db + .get_user_by_id(user_id) + .await? + .with_context(|| format!("user {user_id} not found"))?; + + if let Some(impersonator_id) = validate_result.impersonator_id { + let admin = state .db - .get_user_by_id(user_id) + .get_user_by_id(impersonator_id) .await? - .with_context(|| format!("user {user_id} not found"))?; - - if let Some(impersonator_id) = validate_result.impersonator_id { - let admin = state - .db - .get_user_by_id(impersonator_id) - .await? - .with_context(|| format!("user {impersonator_id} not found"))?; - req.extensions_mut() - .insert(Principal::Impersonated { user, admin }); - } else { - req.extensions_mut().insert(Principal::User(user)); - }; - return Ok::<_, Error>(next.run(req).await); - } + .with_context(|| format!("user {impersonator_id} not found"))?; + req.extensions_mut() + .insert(Principal::Impersonated { user, admin }); + } else { + req.extensions_mut().insert(Principal::User(user)); + }; + return Ok::<_, Error>(next.run(req).await); } Err(Error::http( diff --git a/crates/collab/src/db/queries/extensions.rs b/crates/collab/src/db/queries/extensions.rs index 7d8aad2be4bd3581cbdbe3dc3a1dfbc935f81966..f218ff28507cf51a72cd0aa00a044ad75f64f839 100644 --- a/crates/collab/src/db/queries/extensions.rs +++ b/crates/collab/src/db/queries/extensions.rs @@ -87,10 +87,10 @@ impl Database { continue; }; - if let Some((_, max_extension_version)) = &max_versions.get(&version.extension_id) { - if max_extension_version > &extension_version { - continue; - } + if let Some((_, max_extension_version)) = &max_versions.get(&version.extension_id) + && max_extension_version > &extension_version + { + continue; } if let Some(constraints) = constraints { @@ -331,10 +331,10 @@ impl Database { .exec_without_returning(&*tx) .await?; - if let Ok(db_version) = semver::Version::parse(&extension.latest_version) { - if db_version >= latest_version.version { - continue; - } + if let Ok(db_version) = semver::Version::parse(&extension.latest_version) + && db_version >= latest_version.version + { + continue; } let mut extension = extension.into_active_model(); diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 9abab25edebd3a537d9a4a678e1ccdef5f45be2a..393f2c80f8e733aa2d2b3b5f4b811c9868e0a620 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -1321,10 +1321,10 @@ impl Database { .await?; let mut connection_ids = HashSet::default(); - if let Some(host_connection) = project.host_connection().log_err() { - if !exclude_dev_server { - connection_ids.insert(host_connection); - } + if let Some(host_connection) = project.host_connection().log_err() + && !exclude_dev_server + { + connection_ids.insert(host_connection); } while let Some(collaborator) = collaborators.next().await { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index ef749ac9b7de96422a91f20c2b02ec91fab87d3c..01f553edf28dcaab516cb04bb8b29ffbf0232aac 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -616,10 +616,10 @@ impl Server { } } - if let Some(live_kit) = livekit_client.as_ref() { - if delete_livekit_room { - live_kit.delete_room(livekit_room).await.trace_err(); - } + if let Some(live_kit) = livekit_client.as_ref() + && delete_livekit_room + { + live_kit.delete_room(livekit_room).await.trace_err(); } } } @@ -1015,47 +1015,47 @@ impl Server { inviter_id: UserId, invitee_id: UserId, ) -> Result<()> { - if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await? { - if let Some(code) = &user.invite_code { - let pool = self.connection_pool.lock(); - let invitee_contact = contact_for_user(invitee_id, false, &pool); - for connection_id in pool.user_connection_ids(inviter_id) { - self.peer.send( - connection_id, - proto::UpdateContacts { - contacts: vec![invitee_contact.clone()], - ..Default::default() - }, - )?; - self.peer.send( - connection_id, - proto::UpdateInviteInfo { - url: format!("{}{}", self.app_state.config.invite_link_prefix, &code), - count: user.invite_count as u32, - }, - )?; - } + if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await? + && let Some(code) = &user.invite_code + { + let pool = self.connection_pool.lock(); + let invitee_contact = contact_for_user(invitee_id, false, &pool); + for connection_id in pool.user_connection_ids(inviter_id) { + self.peer.send( + connection_id, + proto::UpdateContacts { + contacts: vec![invitee_contact.clone()], + ..Default::default() + }, + )?; + self.peer.send( + connection_id, + proto::UpdateInviteInfo { + url: format!("{}{}", self.app_state.config.invite_link_prefix, &code), + count: user.invite_count as u32, + }, + )?; } } Ok(()) } pub async fn invite_count_updated(self: &Arc<Self>, user_id: UserId) -> Result<()> { - if let Some(user) = self.app_state.db.get_user_by_id(user_id).await? { - if let Some(invite_code) = &user.invite_code { - let pool = self.connection_pool.lock(); - for connection_id in pool.user_connection_ids(user_id) { - self.peer.send( - connection_id, - proto::UpdateInviteInfo { - url: format!( - "{}{}", - self.app_state.config.invite_link_prefix, invite_code - ), - count: user.invite_count as u32, - }, - )?; - } + if let Some(user) = self.app_state.db.get_user_by_id(user_id).await? + && let Some(invite_code) = &user.invite_code + { + let pool = self.connection_pool.lock(); + for connection_id in pool.user_connection_ids(user_id) { + self.peer.send( + connection_id, + proto::UpdateInviteInfo { + url: format!( + "{}{}", + self.app_state.config.invite_link_prefix, invite_code + ), + count: user.invite_count as u32, + }, + )?; } } Ok(()) @@ -1101,10 +1101,10 @@ fn broadcast<F>( F: FnMut(ConnectionId) -> anyhow::Result<()>, { for receiver_id in receiver_ids { - if Some(receiver_id) != sender_id { - if let Err(error) = f(receiver_id) { - tracing::error!("failed to send to {:?} {}", receiver_id, error); - } + if Some(receiver_id) != sender_id + && let Err(error) = f(receiver_id) + { + tracing::error!("failed to send to {:?} {}", receiver_id, error); } } } @@ -2294,11 +2294,10 @@ async fn update_language_server( let db = session.db().await; if let Some(proto::update_language_server::Variant::MetadataUpdated(update)) = &request.variant + && let Some(capabilities) = update.capabilities.clone() { - if let Some(capabilities) = update.capabilities.clone() { - db.update_server_capabilities(project_id, request.language_server_id, capabilities) - .await?; - } + db.update_server_capabilities(project_id, request.language_server_id, capabilities) + .await?; } let project_connection_ids = db diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 4d94d041b9b5ca5e6d0ed3bd1f54b5f86f224c56..ca8a42d54de73c71a324f259b1c92bf23e94358c 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -1162,8 +1162,8 @@ impl RandomizedTest for ProjectCollaborationTest { Some((project, cx)) }); - if !guest_project.is_disconnected(cx) { - if let Some((host_project, host_cx)) = host_project { + if !guest_project.is_disconnected(cx) + && let Some((host_project, host_cx)) = host_project { let host_worktree_snapshots = host_project.read_with(host_cx, |host_project, cx| { host_project @@ -1235,7 +1235,6 @@ impl RandomizedTest for ProjectCollaborationTest { ); } } - } for buffer in guest_project.opened_buffers(cx) { let buffer = buffer.read(cx); diff --git a/crates/collab/src/tests/randomized_test_helpers.rs b/crates/collab/src/tests/randomized_test_helpers.rs index cabf10cfbcec2e13a322ed742745b410ba760fd9..d6c299a6a9ed4e0439573e9b33fabe8ff122963d 100644 --- a/crates/collab/src/tests/randomized_test_helpers.rs +++ b/crates/collab/src/tests/randomized_test_helpers.rs @@ -198,11 +198,11 @@ pub async fn run_randomized_test<T: RandomizedTest>( } pub fn save_randomized_test_plan() { - if let Some(serialize_plan) = LAST_PLAN.lock().take() { - if let Some(path) = plan_save_path() { - eprintln!("saved test plan to path {:?}", path); - std::fs::write(path, serialize_plan()).unwrap(); - } + if let Some(serialize_plan) = LAST_PLAN.lock().take() + && let Some(path) = plan_save_path() + { + eprintln!("saved test plan to path {:?}", path); + std::fs::write(path, serialize_plan()).unwrap(); } } @@ -290,10 +290,9 @@ impl<T: RandomizedTest> TestPlan<T> { if let StoredOperation::Client { user_id, batch_id, .. } = operation + && batch_id == current_batch_id { - if batch_id == current_batch_id { - return Some(user_id); - } + return Some(user_id); } None })); @@ -366,10 +365,9 @@ impl<T: RandomizedTest> TestPlan<T> { }, applied, ) = stored_operation + && user_id == ¤t_user_id { - if user_id == ¤t_user_id { - return Some((operation.clone(), applied.clone())); - } + return Some((operation.clone(), applied.clone())); } } None @@ -550,11 +548,11 @@ impl<T: RandomizedTest> TestPlan<T> { .unwrap(); let pool = server.connection_pool.lock(); for contact in contacts { - if let db::Contact::Accepted { user_id, busy, .. } = contact { - if user_id == removed_user_id { - assert!(!pool.is_user_online(user_id)); - assert!(!busy); - } + if let db::Contact::Accepted { user_id, busy, .. } = contact + && user_id == removed_user_id + { + assert!(!pool.is_user_online(user_id)); + assert!(!busy); } } } diff --git a/crates/collab/src/user_backfiller.rs b/crates/collab/src/user_backfiller.rs index 71b99a3d4c62560597ce47de926816e741507e44..569a298c9cd5bca6c65e5b7a39b45a784635ad35 100644 --- a/crates/collab/src/user_backfiller.rs +++ b/crates/collab/src/user_backfiller.rs @@ -130,17 +130,17 @@ impl UserBackfiller { .and_then(|value| value.parse::<i64>().ok()) .and_then(|value| DateTime::from_timestamp(value, 0)); - if rate_limit_remaining == Some(0) { - if let Some(reset_at) = rate_limit_reset { - let now = Utc::now(); - if reset_at > now { - let sleep_duration = reset_at - now; - log::info!( - "rate limit reached. Sleeping for {} seconds", - sleep_duration.num_seconds() - ); - self.executor.sleep(sleep_duration.to_std().unwrap()).await; - } + if rate_limit_remaining == Some(0) + && let Some(reset_at) = rate_limit_reset + { + let now = Utc::now(); + if reset_at > now { + let sleep_duration = reset_at - now; + log::info!( + "rate limit reached. Sleeping for {} seconds", + sleep_duration.num_seconds() + ); + self.executor.sleep(sleep_duration.to_std().unwrap()).await; } } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index b86d72d92faede8c52e40a8e209fde5bf1ea9f0b..9993c0841c0c88fb854764ce9044751f6955dfb9 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -107,43 +107,32 @@ impl ChannelView { .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id); // If this channel buffer is already open in this pane, just return it. - if let Some(existing_view) = existing_view.clone() { - if existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer - { - if let Some(link_position) = link_position { - existing_view.update(cx, |channel_view, cx| { - channel_view.focus_position_from_link( - link_position, - true, - window, - cx, - ) - }); - } - return existing_view; + if let Some(existing_view) = existing_view.clone() + && existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer + { + if let Some(link_position) = link_position { + existing_view.update(cx, |channel_view, cx| { + channel_view.focus_position_from_link(link_position, true, window, cx) + }); } + return existing_view; } // If the pane contained a disconnected view for this channel buffer, // replace that. - if let Some(existing_item) = existing_view { - if let Some(ix) = pane.index_for_item(&existing_item) { - pane.close_item_by_id( - existing_item.entity_id(), - SaveIntent::Skip, - window, - cx, - ) + if let Some(existing_item) = existing_view + && let Some(ix) = pane.index_for_item(&existing_item) + { + pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, window, cx) .detach(); - pane.add_item( - Box::new(channel_view.clone()), - true, - true, - Some(ix), - window, - cx, - ); - } + pane.add_item( + Box::new(channel_view.clone()), + true, + true, + Some(ix), + window, + cx, + ); } if let Some(link_position) = link_position { @@ -259,26 +248,21 @@ impl ChannelView { .editor .update(cx, |editor, cx| editor.snapshot(window, cx)); - if let Some(outline) = snapshot.buffer_snapshot.outline(None) { - if let Some(item) = outline + if let Some(outline) = snapshot.buffer_snapshot.outline(None) + && let Some(item) = outline .items .iter() .find(|item| &Channel::slug(&item.text).to_lowercase() == &position) - { - self.editor.update(cx, |editor, cx| { - editor.change_selections( - SelectionEffects::scroll(Autoscroll::focused()), - window, - cx, - |s| { - s.replace_cursors_with(|map| { - vec![item.range.start.to_display_point(map)] - }) - }, - ) - }); - return; - } + { + self.editor.update(cx, |editor, cx| { + editor.change_selections( + SelectionEffects::scroll(Autoscroll::focused()), + window, + cx, + |s| s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)]), + ) + }); + return; } if !first_attempt { diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 2bbaa8446c20e4455504499d468e7c46dff8ced8..77ce74d58149d0df00206d49189d118db177e8e2 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -287,19 +287,20 @@ impl ChatPanel { } fn acknowledge_last_message(&mut self, cx: &mut Context<Self>) { - if self.active && self.is_scrolled_to_bottom { - if let Some((chat, _)) = &self.active_chat { - if let Some(channel_id) = self.channel_id(cx) { - self.last_acknowledged_message_id = self - .channel_store - .read(cx) - .last_acknowledge_message_id(channel_id); - } - - chat.update(cx, |chat, cx| { - chat.acknowledge_last_message(cx); - }); + if self.active + && self.is_scrolled_to_bottom + && let Some((chat, _)) = &self.active_chat + { + if let Some(channel_id) = self.channel_id(cx) { + self.last_acknowledged_message_id = self + .channel_store + .read(cx) + .last_acknowledge_message_id(channel_id); } + + chat.update(cx, |chat, cx| { + chat.acknowledge_last_message(cx); + }); } } @@ -405,14 +406,13 @@ impl ChatPanel { && last_message.id != this_message.id && duration_since_last_message < Duration::from_secs(5 * 60); - if let ChannelMessageId::Saved(id) = this_message.id { - if this_message + if let ChannelMessageId::Saved(id) = this_message.id + && this_message .mentions .iter() .any(|(_, user_id)| Some(*user_id) == self.client.user_id()) - { - active_chat.acknowledge_message(id); - } + { + active_chat.acknowledge_message(id); } (this_message, is_continuation_from_previous, is_admin) @@ -871,34 +871,33 @@ impl ChatPanel { scroll_to_message_id.or(this.last_acknowledged_message_id) })?; - if let Some(message_id) = scroll_to_message_id { - if let Some(item_ix) = + if let Some(message_id) = scroll_to_message_id + && let Some(item_ix) = ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone()) .await - { - this.update(cx, |this, cx| { - if let Some(highlight_message_id) = highlight_message_id { - let task = cx.spawn(async move |this, cx| { - cx.background_executor().timer(Duration::from_secs(2)).await; - this.update(cx, |this, cx| { - this.highlighted_message.take(); - cx.notify(); - }) - .ok(); - }); - - this.highlighted_message = Some((highlight_message_id, task)); - } + { + this.update(cx, |this, cx| { + if let Some(highlight_message_id) = highlight_message_id { + let task = cx.spawn(async move |this, cx| { + cx.background_executor().timer(Duration::from_secs(2)).await; + this.update(cx, |this, cx| { + this.highlighted_message.take(); + cx.notify(); + }) + .ok(); + }); - if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) { - this.message_list.scroll_to(ListOffset { - item_ix, - offset_in_item: px(0.0), - }); - cx.notify(); - } - })?; - } + this.highlighted_message = Some((highlight_message_id, task)); + } + + if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) { + this.message_list.scroll_to(ListOffset { + item_ix, + offset_in_item: px(0.0), + }); + cx.notify(); + } + })?; } Ok(()) diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 28d60d9221ab2fb49e4aa77048cef01e621bea89..57f63412971e844d5e98f2c58a5f5ad03d6eda30 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -241,38 +241,36 @@ impl MessageEditor { ) -> Task<Result<Vec<CompletionResponse>>> { if let Some((start_anchor, query, candidates)) = self.collect_mention_candidates(buffer, end_anchor, cx) + && !candidates.is_empty() { - if !candidates.is_empty() { - return cx.spawn(async move |_, cx| { - let completion_response = Self::completions_for_candidates( - cx, - query.as_str(), - &candidates, - start_anchor..end_anchor, - Self::completion_for_mention, - ) - .await; - Ok(vec![completion_response]) - }); - } + return cx.spawn(async move |_, cx| { + let completion_response = Self::completions_for_candidates( + cx, + query.as_str(), + &candidates, + start_anchor..end_anchor, + Self::completion_for_mention, + ) + .await; + Ok(vec![completion_response]) + }); } if let Some((start_anchor, query, candidates)) = self.collect_emoji_candidates(buffer, end_anchor, cx) + && !candidates.is_empty() { - if !candidates.is_empty() { - return cx.spawn(async move |_, cx| { - let completion_response = Self::completions_for_candidates( - cx, - query.as_str(), - candidates, - start_anchor..end_anchor, - Self::completion_for_emoji, - ) - .await; - Ok(vec![completion_response]) - }); - } + return cx.spawn(async move |_, cx| { + let completion_response = Self::completions_for_candidates( + cx, + query.as_str(), + candidates, + start_anchor..end_anchor, + Self::completion_for_emoji, + ) + .await; + Ok(vec![completion_response]) + }); } Task::ready(Ok(vec![CompletionResponse { @@ -474,18 +472,17 @@ impl MessageEditor { for range in ranges { text.clear(); text.extend(buffer.text_for_range(range.clone())); - if let Some(username) = text.strip_prefix('@') { - if let Some(user) = this + if let Some(username) = text.strip_prefix('@') + && let Some(user) = this .user_store .read(cx) .cached_user_by_github_login(username) - { - let start = multi_buffer.anchor_after(range.start); - let end = multi_buffer.anchor_after(range.end); + { + let start = multi_buffer.anchor_after(range.start); + let end = multi_buffer.anchor_after(range.end); - mentioned_user_ids.push(user.id); - anchor_ranges.push(start..end); - } + mentioned_user_ids.push(user.id); + anchor_ranges.push(start..end); } } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 8016481f6fcd92677cc01aea5d941390bc8eaff1..526aacf066bc2619d198e23762b1137dd2b8cad9 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -311,10 +311,10 @@ impl CollabPanel { window, |this: &mut Self, _, event, window, cx| { if let editor::EditorEvent::Blurred = event { - if let Some(state) = &this.channel_editing_state { - if state.pending_name().is_some() { - return; - } + if let Some(state) = &this.channel_editing_state + && state.pending_name().is_some() + { + return; } this.take_editing_state(window, cx); this.update_entries(false, cx); @@ -491,11 +491,11 @@ impl CollabPanel { if !self.collapsed_sections.contains(&Section::ActiveCall) { let room = room.read(cx); - if query.is_empty() { - if let Some(channel_id) = room.channel_id() { - self.entries.push(ListEntry::ChannelNotes { channel_id }); - self.entries.push(ListEntry::ChannelChat { channel_id }); - } + if query.is_empty() + && let Some(channel_id) = room.channel_id() + { + self.entries.push(ListEntry::ChannelNotes { channel_id }); + self.entries.push(ListEntry::ChannelChat { channel_id }); } // Populate the active user. @@ -639,10 +639,10 @@ impl CollabPanel { &Default::default(), executor.clone(), )); - if let Some(state) = &self.channel_editing_state { - if matches!(state, ChannelEditingState::Create { location: None, .. }) { - self.entries.push(ListEntry::ChannelEditor { depth: 0 }); - } + if let Some(state) = &self.channel_editing_state + && matches!(state, ChannelEditingState::Create { location: None, .. }) + { + self.entries.push(ListEntry::ChannelEditor { depth: 0 }); } let mut collapse_depth = None; for mat in matches { @@ -1552,98 +1552,93 @@ impl CollabPanel { return; } - if let Some(selection) = self.selection { - if let Some(entry) = self.entries.get(selection) { - match entry { - ListEntry::Header(section) => match section { - Section::ActiveCall => Self::leave_call(window, cx), - Section::Channels => self.new_root_channel(window, cx), - Section::Contacts => self.toggle_contact_finder(window, cx), - Section::ContactRequests - | Section::Online - | Section::Offline - | Section::ChannelInvites => { - self.toggle_section_expanded(*section, cx); - } - }, - ListEntry::Contact { contact, calling } => { - if contact.online && !contact.busy && !calling { - self.call(contact.user.id, window, cx); - } + if let Some(selection) = self.selection + && let Some(entry) = self.entries.get(selection) + { + match entry { + ListEntry::Header(section) => match section { + Section::ActiveCall => Self::leave_call(window, cx), + Section::Channels => self.new_root_channel(window, cx), + Section::Contacts => self.toggle_contact_finder(window, cx), + Section::ContactRequests + | Section::Online + | Section::Offline + | Section::ChannelInvites => { + self.toggle_section_expanded(*section, cx); } - ListEntry::ParticipantProject { - project_id, - host_user_id, - .. - } => { - if let Some(workspace) = self.workspace.upgrade() { - let app_state = workspace.read(cx).app_state().clone(); - workspace::join_in_room_project( - *project_id, - *host_user_id, - app_state, - cx, - ) + }, + ListEntry::Contact { contact, calling } => { + if contact.online && !contact.busy && !calling { + self.call(contact.user.id, window, cx); + } + } + ListEntry::ParticipantProject { + project_id, + host_user_id, + .. + } => { + if let Some(workspace) = self.workspace.upgrade() { + let app_state = workspace.read(cx).app_state().clone(); + workspace::join_in_room_project(*project_id, *host_user_id, app_state, cx) .detach_and_prompt_err( "Failed to join project", window, cx, |_, _, _| None, ); - } - } - ListEntry::ParticipantScreen { peer_id, .. } => { - let Some(peer_id) = peer_id else { - return; - }; - if let Some(workspace) = self.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - workspace.open_shared_screen(*peer_id, window, cx) - }); - } - } - ListEntry::Channel { channel, .. } => { - let is_active = maybe!({ - let call_channel = ActiveCall::global(cx) - .read(cx) - .room()? - .read(cx) - .channel_id()?; - - Some(call_channel == channel.id) - }) - .unwrap_or(false); - if is_active { - self.open_channel_notes(channel.id, window, cx) - } else { - self.join_channel(channel.id, window, cx) - } } - ListEntry::ContactPlaceholder => self.toggle_contact_finder(window, cx), - ListEntry::CallParticipant { user, peer_id, .. } => { - if Some(user) == self.user_store.read(cx).current_user().as_ref() { - Self::leave_call(window, cx); - } else if let Some(peer_id) = peer_id { - self.workspace - .update(cx, |workspace, cx| workspace.follow(*peer_id, window, cx)) - .ok(); - } - } - ListEntry::IncomingRequest(user) => { - self.respond_to_contact_request(user.id, true, window, cx) - } - ListEntry::ChannelInvite(channel) => { - self.respond_to_channel_invite(channel.id, true, cx) + } + ListEntry::ParticipantScreen { peer_id, .. } => { + let Some(peer_id) = peer_id else { + return; + }; + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.open_shared_screen(*peer_id, window, cx) + }); } - ListEntry::ChannelNotes { channel_id } => { - self.open_channel_notes(*channel_id, window, cx) + } + ListEntry::Channel { channel, .. } => { + let is_active = maybe!({ + let call_channel = ActiveCall::global(cx) + .read(cx) + .room()? + .read(cx) + .channel_id()?; + + Some(call_channel == channel.id) + }) + .unwrap_or(false); + if is_active { + self.open_channel_notes(channel.id, window, cx) + } else { + self.join_channel(channel.id, window, cx) } - ListEntry::ChannelChat { channel_id } => { - self.join_channel_chat(*channel_id, window, cx) + } + ListEntry::ContactPlaceholder => self.toggle_contact_finder(window, cx), + ListEntry::CallParticipant { user, peer_id, .. } => { + if Some(user) == self.user_store.read(cx).current_user().as_ref() { + Self::leave_call(window, cx); + } else if let Some(peer_id) = peer_id { + self.workspace + .update(cx, |workspace, cx| workspace.follow(*peer_id, window, cx)) + .ok(); } - ListEntry::OutgoingRequest(_) => {} - ListEntry::ChannelEditor { .. } => {} } + ListEntry::IncomingRequest(user) => { + self.respond_to_contact_request(user.id, true, window, cx) + } + ListEntry::ChannelInvite(channel) => { + self.respond_to_channel_invite(channel.id, true, cx) + } + ListEntry::ChannelNotes { channel_id } => { + self.open_channel_notes(*channel_id, window, cx) + } + ListEntry::ChannelChat { channel_id } => { + self.join_channel_chat(*channel_id, window, cx) + } + ListEntry::OutgoingRequest(_) => {} + ListEntry::ChannelEditor { .. } => {} } } } diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 01ca533c10ddb0fe72c516a49abe8ca788000be4..00c3bbf623321e9648dde867934786274683a777 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -121,13 +121,12 @@ impl NotificationPanel { let notification_list = ListState::new(0, ListAlignment::Top, px(1000.)); notification_list.set_scroll_handler(cx.listener( |this, event: &ListScrollEvent, _, cx| { - if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD { - if let Some(task) = this + if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD + && let Some(task) = this .notification_store .update(cx, |store, cx| store.load_more_notifications(false, cx)) - { - task.detach(); - } + { + task.detach(); } }, )); @@ -469,20 +468,19 @@ impl NotificationPanel { channel_id, .. } = notification.clone() + && let Some(workspace) = self.workspace.upgrade() { - if let Some(workspace) = self.workspace.upgrade() { - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = workspace.focus_panel::<ChatPanel>(window, cx) { - panel.update(cx, |panel, cx| { - panel - .select_channel(ChannelId(channel_id), Some(message_id), cx) - .detach_and_log_err(cx); - }); - } - }); + window.defer(cx, move |window, cx| { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = workspace.focus_panel::<ChatPanel>(window, cx) { + panel.update(cx, |panel, cx| { + panel + .select_channel(ChannelId(channel_id), Some(message_id), cx) + .detach_and_log_err(cx); + }); + } }); - } + }); } } @@ -491,18 +489,18 @@ impl NotificationPanel { return false; } - if let Notification::ChannelMessageMention { channel_id, .. } = ¬ification { - if let Some(workspace) = self.workspace.upgrade() { - return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) { - let panel = panel.read(cx); - panel.is_scrolled_to_bottom() - && panel - .active_chat() - .map_or(false, |chat| chat.read(cx).channel_id.0 == *channel_id) - } else { - false - }; - } + if let Notification::ChannelMessageMention { channel_id, .. } = ¬ification + && let Some(workspace) = self.workspace.upgrade() + { + return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) { + let panel = panel.read(cx); + panel.is_scrolled_to_bottom() + && panel + .active_chat() + .map_or(false, |chat| chat.read(cx).channel_id.0 == *channel_id) + } else { + false + }; } false @@ -582,16 +580,16 @@ impl NotificationPanel { } fn remove_toast(&mut self, notification_id: u64, cx: &mut Context<Self>) { - if let Some((current_id, _)) = &self.current_notification_toast { - if *current_id == notification_id { - self.current_notification_toast.take(); - self.workspace - .update(cx, |workspace, cx| { - let id = NotificationId::unique::<NotificationToast>(); - workspace.dismiss_notification(&id, cx) - }) - .ok(); - } + if let Some((current_id, _)) = &self.current_notification_toast + && *current_id == notification_id + { + self.current_notification_toast.take(); + self.workspace + .update(cx, |workspace, cx| { + let id = NotificationId::unique::<NotificationToast>(); + workspace.dismiss_notification(&id, cx) + }) + .ok(); } } diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index 65283afa87d94fae3ec51f8a89574713080bded2..609d2c43e36f3cb51a18abc9fe4a1cb61e4c6508 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -271,10 +271,10 @@ impl Client { ); } } else if let Ok(response) = serde_json::from_str::<AnyResponse>(&message) { - if let Some(handlers) = response_handlers.lock().as_mut() { - if let Some(handler) = handlers.remove(&response.id) { - handler(Ok(message.to_string())); - } + if let Some(handlers) = response_handlers.lock().as_mut() + && let Some(handler) = handlers.remove(&response.id) + { + handler(Ok(message.to_string())); } } else if let Ok(notification) = serde_json::from_str::<AnyNotification>(&message) { let mut notification_handlers = notification_handlers.lock(); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index dcebeae7212119867bc582ce930d2f51fae49d34..1916853a692f5123fd0e09fbb14491368448622a 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -608,15 +608,13 @@ impl Copilot { sign_in_status: status, .. }) = &mut this.server - { - if let SignInStatus::SigningIn { + && let SignInStatus::SigningIn { prompt: prompt_flow, .. } = status - { - *prompt_flow = Some(flow.clone()); - cx.notify(); - } + { + *prompt_flow = Some(flow.clone()); + cx.notify(); } })?; let response = lsp @@ -782,59 +780,58 @@ impl Copilot { event: &language::BufferEvent, cx: &mut Context<Self>, ) -> Result<()> { - if let Ok(server) = self.server.as_running() { - if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id()) - { - match event { - language::BufferEvent::Edited => { - drop(registered_buffer.report_changes(&buffer, cx)); - } - language::BufferEvent::Saved => { + if let Ok(server) = self.server.as_running() + && let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id()) + { + match event { + language::BufferEvent::Edited => { + drop(registered_buffer.report_changes(&buffer, cx)); + } + language::BufferEvent::Saved => { + server + .lsp + .notify::<lsp::notification::DidSaveTextDocument>( + &lsp::DidSaveTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new( + registered_buffer.uri.clone(), + ), + text: None, + }, + )?; + } + language::BufferEvent::FileHandleChanged + | language::BufferEvent::LanguageChanged => { + let new_language_id = id_for_language(buffer.read(cx).language()); + let Ok(new_uri) = uri_for_buffer(&buffer, cx) else { + return Ok(()); + }; + if new_uri != registered_buffer.uri + || new_language_id != registered_buffer.language_id + { + let old_uri = mem::replace(&mut registered_buffer.uri, new_uri); + registered_buffer.language_id = new_language_id; + server + .lsp + .notify::<lsp::notification::DidCloseTextDocument>( + &lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(old_uri), + }, + )?; server .lsp - .notify::<lsp::notification::DidSaveTextDocument>( - &lsp::DidSaveTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new( + .notify::<lsp::notification::DidOpenTextDocument>( + &lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( registered_buffer.uri.clone(), + registered_buffer.language_id.clone(), + registered_buffer.snapshot_version, + registered_buffer.snapshot.text(), ), - text: None, }, )?; } - language::BufferEvent::FileHandleChanged - | language::BufferEvent::LanguageChanged => { - let new_language_id = id_for_language(buffer.read(cx).language()); - let Ok(new_uri) = uri_for_buffer(&buffer, cx) else { - return Ok(()); - }; - if new_uri != registered_buffer.uri - || new_language_id != registered_buffer.language_id - { - let old_uri = mem::replace(&mut registered_buffer.uri, new_uri); - registered_buffer.language_id = new_language_id; - server - .lsp - .notify::<lsp::notification::DidCloseTextDocument>( - &lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(old_uri), - }, - )?; - server - .lsp - .notify::<lsp::notification::DidOpenTextDocument>( - &lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - registered_buffer.uri.clone(), - registered_buffer.language_id.clone(), - registered_buffer.snapshot_version, - registered_buffer.snapshot.text(), - ), - }, - )?; - } - } - _ => {} } + _ => {} } } @@ -842,17 +839,17 @@ impl Copilot { } fn unregister_buffer(&mut self, buffer: &WeakEntity<Buffer>) { - if let Ok(server) = self.server.as_running() { - if let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id()) { - server - .lsp - .notify::<lsp::notification::DidCloseTextDocument>( - &lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(buffer.uri), - }, - ) - .ok(); - } + if let Ok(server) = self.server.as_running() + && let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id()) + { + server + .lsp + .notify::<lsp::notification::DidCloseTextDocument>( + &lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer.uri), + }, + ) + .ok(); } } diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 70b06381204a395e3f68a554497eb39aa6c4c1dd..a8826d563b09925068dd6da1be865f1e17bce0ec 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -99,10 +99,10 @@ impl JsDebugAdapter { } } - if let Some(env) = configuration.get("env").cloned() { - if let Ok(env) = serde_json::from_value(env) { - envs = env; - } + if let Some(env) = configuration.get("env").cloned() + && let Ok(env) = serde_json::from_value(env) + { + envs = env; } configuration diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 14154e5b39d723f5835c1e0f92d9245866eea549..e60c08cd0faffb0725fdc0a534580e9c612ce2f5 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -661,11 +661,11 @@ impl ToolbarItemView for DapLogToolbarItemView { _window: &mut Window, cx: &mut Context<Self>, ) -> workspace::ToolbarItemLocation { - if let Some(item) = active_pane_item { - if let Some(log_view) = item.downcast::<DapLogView>() { - self.log_view = Some(log_view.clone()); - return workspace::ToolbarItemLocation::PrimaryLeft; - } + if let Some(item) = active_pane_item + && let Some(log_view) = item.downcast::<DapLogView>() + { + self.log_view = Some(log_view.clone()); + return workspace::ToolbarItemLocation::PrimaryLeft; } self.log_view = None; diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index cf038871bc917cfbff8678ebb0091da43c693000..4e1b0d19e250638a84589ab8bfc210677c386406 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -530,10 +530,9 @@ impl DebugPanel { .active_session .as_ref() .map(|session| session.entity_id()) + && active_session_id == entity_id { - if active_session_id == entity_id { - this.active_session = this.sessions_with_children.keys().next().cloned(); - } + this.active_session = this.sessions_with_children.keys().next().cloned(); } cx.notify() }) @@ -1302,10 +1301,10 @@ impl DebugPanel { cx: &mut Context<'_, Self>, ) -> Option<SharedString> { let adapter = parent_session.read(cx).adapter(); - if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) { - if let Some(label) = adapter.label_for_child_session(request) { - return Some(label.into()); - } + if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) + && let Some(label) = adapter.label_for_child_session(request) + { + return Some(label.into()); } None } diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index 51ea25a5cb925f6e3c57964159280f6336db1c69..eb0ad92dcc64b6ac6925001006df7df07bcc8d10 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -343,10 +343,10 @@ impl NewProcessModal { return; } - if let NewProcessMode::Launch = &self.mode { - if self.configure_mode.read(cx).save_to_debug_json.selected() { - self.save_debug_scenario(window, cx); - } + if let NewProcessMode::Launch = &self.mode + && self.configure_mode.read(cx).save_to_debug_json.selected() + { + self.save_debug_scenario(window, cx); } let Some(debugger) = self.debugger.clone() else { diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 9768f02e8e9b60350e9938c297ea7e7db6629ea6..095b069fa3fe25c700ce3fb5bf9a5b8778aece24 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -239,11 +239,9 @@ impl BreakpointList { } fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) { - if self.strip_mode.is_some() { - if self.input.focus_handle(cx).contains_focused(window, cx) { - cx.propagate(); - return; - } + if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; } let ix = match self.selected_ix { _ if self.breakpoints.len() == 0 => None, @@ -265,11 +263,9 @@ impl BreakpointList { window: &mut Window, cx: &mut Context<Self>, ) { - if self.strip_mode.is_some() { - if self.input.focus_handle(cx).contains_focused(window, cx) { - cx.propagate(); - return; - } + if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; } let ix = match self.selected_ix { _ if self.breakpoints.len() == 0 => None, @@ -286,11 +282,9 @@ impl BreakpointList { } fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) { - if self.strip_mode.is_some() { - if self.input.focus_handle(cx).contains_focused(window, cx) { - cx.propagate(); - return; - } + if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; } let ix = if self.breakpoints.len() > 0 { Some(0) @@ -301,11 +295,9 @@ impl BreakpointList { } fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) { - if self.strip_mode.is_some() { - if self.input.focus_handle(cx).contains_focused(window, cx) { - cx.propagate(); - return; - } + if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; } let ix = if self.breakpoints.len() > 0 { Some(self.breakpoints.len() - 1) @@ -401,11 +393,9 @@ impl BreakpointList { let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else { return; }; - if self.strip_mode.is_some() { - if self.input.focus_handle(cx).contains_focused(window, cx) { - cx.propagate(); - return; - } + if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; } match &mut entry.kind { diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 42989ddc209efcbeb5044d8e5c3b85d6afb9f739..05d2231da45ec81c0db05a6175dd69563eb8a024 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -611,17 +611,16 @@ impl ConsoleQueryBarCompletionProvider { for variable in console.variable_list.update(cx, |variable_list, cx| { variable_list.completion_variables(cx) }) { - if let Some(evaluate_name) = &variable.evaluate_name { - if variables + if let Some(evaluate_name) = &variable.evaluate_name + && variables .insert(evaluate_name.clone(), variable.value.clone()) .is_none() - { - string_matches.push(StringMatchCandidate { - id: 0, - string: evaluate_name.clone(), - char_bag: evaluate_name.chars().collect(), - }); - } + { + string_matches.push(StringMatchCandidate { + id: 0, + string: evaluate_name.clone(), + char_bag: evaluate_name.chars().collect(), + }); } if variables diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 23dbf333227e03da6d329d98d760754670354108..c15c0f2493fb4ea0cabdfee963a2238416899e33 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -639,17 +639,15 @@ impl ProjectDiagnosticsEditor { #[cfg(test)] let cloned_blocks = blocks.clone(); - if was_empty { - if let Some(anchor_range) = anchor_ranges.first() { - let range_to_select = anchor_range.start..anchor_range.start; - this.editor.update(cx, |editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.select_anchor_ranges([range_to_select]); - }) - }); - if this.focus_handle.is_focused(window) { - this.editor.read(cx).focus_handle(cx).focus(window); - } + if was_empty && let Some(anchor_range) = anchor_ranges.first() { + let range_to_select = anchor_range.start..anchor_range.start; + this.editor.update(cx, |editor, cx| { + editor.change_selections(Default::default(), window, cx, |s| { + s.select_anchor_ranges([range_to_select]); + }) + }); + if this.focus_handle.is_focused(window) { + this.editor.read(cx).focus_handle(cx).focus(window); } } @@ -980,18 +978,16 @@ async fn heuristic_syntactic_expand( // Remove blank lines from start and end if let Some(start_row) = (outline_range.start.row..outline_range.end.row) .find(|row| !snapshot.line_indent_for_row(*row).is_line_blank()) - { - if let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1) + && let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1) .rev() .find(|row| !snapshot.line_indent_for_row(*row).is_line_blank()) - { - let row_count = end_row.saturating_sub(start_row); - if row_count <= max_row_count { - return Some(RangeInclusive::new( - outline_range.start.row, - outline_range.end.row, - )); - } + { + let row_count = end_row.saturating_sub(start_row); + if row_count <= max_row_count { + return Some(RangeInclusive::new( + outline_range.start.row, + outline_range.end.row, + )); } } } diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index a16e516a70c9638965585cc5d6a23d8a9f67b639..cc1cc2c44078a0f95ecff09ee7abbcc8f4143567 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -969,13 +969,13 @@ impl DisplaySnapshot { if let Some(chunk_highlight) = chunk.highlight_style { // For color inlays, blend the color with the editor background let mut processed_highlight = chunk_highlight; - if chunk.is_inlay { - if let Some(inlay_color) = chunk_highlight.color { - // Only blend if the color has transparency (alpha < 1.0) - if inlay_color.a < 1.0 { - let blended_color = editor_style.background.blend(inlay_color); - processed_highlight.color = Some(blended_color); - } + if chunk.is_inlay + && let Some(inlay_color) = chunk_highlight.color + { + // Only blend if the color has transparency (alpha < 1.0) + if inlay_color.a < 1.0 { + let blended_color = editor_style.background.blend(inlay_color); + processed_highlight.color = Some(blended_color); } } @@ -2351,11 +2351,12 @@ pub mod tests { .highlight_style .and_then(|style| style.color) .map_or(black, |color| color.to_rgb()); - if let Some((last_chunk, last_severity, last_color)) = chunks.last_mut() { - if *last_severity == chunk.diagnostic_severity && *last_color == color { - last_chunk.push_str(chunk.text); - continue; - } + if let Some((last_chunk, last_severity, last_color)) = chunks.last_mut() + && *last_severity == chunk.diagnostic_severity + && *last_color == color + { + last_chunk.push_str(chunk.text); + continue; } chunks.push((chunk.text.to_string(), chunk.diagnostic_severity, color)); @@ -2901,11 +2902,12 @@ pub mod tests { .syntax_highlight_id .and_then(|id| id.style(theme)?.color); let highlight_color = chunk.highlight_style.and_then(|style| style.color); - if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() { - if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color { - last_chunk.push_str(chunk.text); - continue; - } + if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() + && syntax_color == *last_syntax_color + && highlight_color == *last_highlight_color + { + last_chunk.push_str(chunk.text); + continue; } chunks.push((chunk.text.to_string(), syntax_color, highlight_color)); } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index c4c9f2004adedcda0a2215aa3f073ef10f5aa78e..5ae37d20fa11e47de69b4d19595681f300d62fde 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -525,26 +525,25 @@ impl BlockMap { // * Below blocks that end at the start of the edit // However, if we hit a replace block that ends at the start of the edit we want to reconstruct it. new_transforms.append(cursor.slice(&old_start, Bias::Left), &()); - if let Some(transform) = cursor.item() { - if transform.summary.input_rows > 0 - && cursor.end() == old_start - && transform - .block - .as_ref() - .map_or(true, |b| !b.is_replacement()) - { - // Preserve the transform (push and next) - new_transforms.push(transform.clone(), &()); - cursor.next(); + if let Some(transform) = cursor.item() + && transform.summary.input_rows > 0 + && cursor.end() == old_start + && transform + .block + .as_ref() + .map_or(true, |b| !b.is_replacement()) + { + // Preserve the transform (push and next) + new_transforms.push(transform.clone(), &()); + cursor.next(); - // Preserve below blocks at end of edit - while let Some(transform) = cursor.item() { - if transform.block.as_ref().map_or(false, |b| b.place_below()) { - new_transforms.push(transform.clone(), &()); - cursor.next(); - } else { - break; - } + // Preserve below blocks at end of edit + while let Some(transform) = cursor.item() { + if transform.block.as_ref().map_or(false, |b| b.place_below()) { + new_transforms.push(transform.clone(), &()); + cursor.next(); + } else { + break; } } } @@ -657,10 +656,10 @@ impl BlockMap { .iter() .filter_map(|block| { let placement = block.placement.to_wrap_row(wrap_snapshot)?; - if let BlockPlacement::Above(row) = placement { - if row < new_start { - return None; - } + if let BlockPlacement::Above(row) = placement + && row < new_start + { + return None; } Some((placement, Block::Custom(block.clone()))) }), @@ -977,10 +976,10 @@ impl BlockMapReader<'_> { break; } - if let Some(BlockId::Custom(id)) = transform.block.as_ref().map(|block| block.id()) { - if id == block_id { - return Some(cursor.start().1); - } + if let Some(BlockId::Custom(id)) = transform.block.as_ref().map(|block| block.id()) + && id == block_id + { + return Some(cursor.start().1); } cursor.next(); } @@ -1299,14 +1298,14 @@ impl BlockSnapshot { let mut input_start = transform_input_start; let mut input_end = transform_input_start; - if let Some(transform) = cursor.item() { - if transform.block.is_none() { - input_start += rows.start - transform_output_start; - input_end += cmp::min( - rows.end - transform_output_start, - transform.summary.input_rows, - ); - } + if let Some(transform) = cursor.item() + && transform.block.is_none() + { + input_start += rows.start - transform_output_start; + input_end += cmp::min( + rows.end - transform_output_start, + transform.summary.input_rows, + ); } BlockChunks { @@ -1472,18 +1471,18 @@ impl BlockSnapshot { longest_row_chars = summary.longest_row_chars; } - if let Some(transform) = cursor.item() { - if transform.block.is_none() { - 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; - let summary = self - .wrap_snapshot - .text_summary_for_range(wrap_start_row..wrap_end_row); - if summary.longest_row_chars > longest_row_chars { - longest_row = BlockRow(output_start.0 + summary.longest_row); - } + if let Some(transform) = cursor.item() + && transform.block.is_none() + { + 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; + let summary = self + .wrap_snapshot + .text_summary_for_range(wrap_start_row..wrap_end_row); + if summary.longest_row_chars > longest_row_chars { + longest_row = BlockRow(output_start.0 + summary.longest_row); } } } @@ -1557,12 +1556,11 @@ impl BlockSnapshot { match transform.block.as_ref() { Some(block) => { - if block.is_replacement() { - if ((bias == Bias::Left || search_left) && output_start <= point.0) - || (!search_left && output_start >= point.0) - { - return BlockPoint(output_start); - } + if block.is_replacement() + && (((bias == Bias::Left || search_left) && output_start <= point.0) + || (!search_left && output_start >= point.0)) + { + return BlockPoint(output_start); } } None => { @@ -3228,34 +3226,32 @@ mod tests { let mut is_in_replace_block = false; if let Some((BlockPlacement::Replace(replace_range), block)) = sorted_blocks_iter.peek() + && wrap_row >= replace_range.start().0 { - if wrap_row >= replace_range.start().0 { - is_in_replace_block = true; + is_in_replace_block = true; - if wrap_row == replace_range.start().0 { - if matches!(block, Block::FoldedBuffer { .. }) { - expected_buffer_rows.push(None); - } else { - expected_buffer_rows - .push(input_buffer_rows[multibuffer_row as usize]); - } + if wrap_row == replace_range.start().0 { + if matches!(block, Block::FoldedBuffer { .. }) { + expected_buffer_rows.push(None); + } else { + expected_buffer_rows.push(input_buffer_rows[multibuffer_row as usize]); } + } - if wrap_row == replace_range.end().0 { - expected_block_positions.push((block_row, block.id())); - let text = "\n".repeat((block.height() - 1) as usize); - if block_row > 0 { - expected_text.push('\n'); - } - expected_text.push_str(&text); - - for _ in 1..block.height() { - expected_buffer_rows.push(None); - } - block_row += block.height(); + if wrap_row == replace_range.end().0 { + expected_block_positions.push((block_row, block.id())); + let text = "\n".repeat((block.height() - 1) as usize); + if block_row > 0 { + expected_text.push('\n'); + } + expected_text.push_str(&text); - sorted_blocks_iter.next(); + for _ in 1..block.height() { + expected_buffer_rows.push(None); } + block_row += block.height(); + + sorted_blocks_iter.next(); } } diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index c4e53a0f4361d83429158f106bd81326c8ddb573..3509bcbba8d24e98d06ba8ca77a0f341909e9382 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -289,25 +289,25 @@ impl FoldMapWriter<'_> { let ChunkRendererId::Fold(id) = id else { continue; }; - if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() { - if Some(new_width) != metadata.width { - let buffer_start = metadata.range.start.to_offset(buffer); - let buffer_end = metadata.range.end.to_offset(buffer); - let inlay_range = inlay_snapshot.to_inlay_offset(buffer_start) - ..inlay_snapshot.to_inlay_offset(buffer_end); - edits.push(InlayEdit { - old: inlay_range.clone(), - new: inlay_range.clone(), - }); + if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() + && Some(new_width) != metadata.width + { + let buffer_start = metadata.range.start.to_offset(buffer); + let buffer_end = metadata.range.end.to_offset(buffer); + let inlay_range = inlay_snapshot.to_inlay_offset(buffer_start) + ..inlay_snapshot.to_inlay_offset(buffer_end); + edits.push(InlayEdit { + old: inlay_range.clone(), + new: inlay_range.clone(), + }); - self.0.snapshot.fold_metadata_by_id.insert( - id, - FoldMetadata { - range: metadata.range, - width: Some(new_width), - }, - ); - } + self.0.snapshot.fold_metadata_by_id.insert( + id, + FoldMetadata { + range: metadata.range, + width: Some(new_width), + }, + ); } } @@ -417,18 +417,18 @@ impl FoldMap { cursor.seek(&InlayOffset(0), Bias::Right); while let Some(mut edit) = inlay_edits_iter.next() { - if let Some(item) = cursor.item() { - if !item.is_fold() { - new_transforms.update_last( - |transform| { - if !transform.is_fold() { - transform.summary.add_summary(&item.summary, &()); - cursor.next(); - } - }, - &(), - ); - } + if let Some(item) = cursor.item() + && !item.is_fold() + { + new_transforms.update_last( + |transform| { + if !transform.is_fold() { + transform.summary.add_summary(&item.summary, &()); + cursor.next(); + } + }, + &(), + ); } new_transforms.append(cursor.slice(&edit.old.start, Bias::Left), &()); edit.new.start -= edit.old.start - *cursor.start(); diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 76148af587d8f6ac6e5488a5fe9c6fde7a7043a8..626dbf5cba73c5ed3865b7f51c775b62086aeedf 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -557,11 +557,11 @@ impl InlayMap { 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), &()); - if let Some(Transform::Isomorphic(transform)) = cursor.item() { - if cursor.end().0 == buffer_edit.old.start { - push_isomorphic(&mut new_transforms, *transform); - cursor.next(); - } + if let Some(Transform::Isomorphic(transform)) = cursor.item() + && cursor.end().0 == buffer_edit.old.start + { + push_isomorphic(&mut new_transforms, *transform); + cursor.next(); } // Remove all the inlays and transforms contained by the edit. diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 0d2d1c4a4cc7f72b396631330e210b691a2615cb..7aa252a7f3b3103cfa1bc440098dfc03089c0452 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -249,48 +249,48 @@ impl WrapMap { return; } - if let Some(wrap_width) = self.wrap_width { - if self.background_task.is_none() { - let pending_edits = self.pending_edits.clone(); - let mut snapshot = self.snapshot.clone(); - let text_system = cx.text_system().clone(); - let (font, font_size) = self.font_with_size.clone(); - let update_task = cx.background_spawn(async move { - let mut edits = Patch::default(); - let mut line_wrapper = text_system.line_wrapper(font, font_size); - for (tab_snapshot, tab_edits) in pending_edits { - let wrap_edits = snapshot - .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper) - .await; - edits = edits.compose(&wrap_edits); - } - (snapshot, edits) - }); + if let Some(wrap_width) = self.wrap_width + && self.background_task.is_none() + { + let pending_edits = self.pending_edits.clone(); + let mut snapshot = self.snapshot.clone(); + let text_system = cx.text_system().clone(); + let (font, font_size) = self.font_with_size.clone(); + let update_task = cx.background_spawn(async move { + let mut edits = Patch::default(); + let mut line_wrapper = text_system.line_wrapper(font, font_size); + for (tab_snapshot, tab_edits) in pending_edits { + let wrap_edits = snapshot + .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper) + .await; + edits = edits.compose(&wrap_edits); + } + (snapshot, edits) + }); - match cx - .background_executor() - .block_with_timeout(Duration::from_millis(1), update_task) - { - Ok((snapshot, output_edits)) => { - self.snapshot = snapshot; - self.edits_since_sync = self.edits_since_sync.compose(&output_edits); - } - Err(update_task) => { - self.background_task = Some(cx.spawn(async move |this, cx| { - let (snapshot, edits) = update_task.await; - this.update(cx, |this, cx| { - this.snapshot = snapshot; - this.edits_since_sync = this - .edits_since_sync - .compose(mem::take(&mut this.interpolated_edits).invert()) - .compose(&edits); - this.background_task = None; - this.flush_edits(cx); - cx.notify(); - }) - .ok(); - })); - } + match cx + .background_executor() + .block_with_timeout(Duration::from_millis(1), update_task) + { + Ok((snapshot, output_edits)) => { + self.snapshot = snapshot; + self.edits_since_sync = self.edits_since_sync.compose(&output_edits); + } + Err(update_task) => { + self.background_task = Some(cx.spawn(async move |this, cx| { + let (snapshot, edits) = update_task.await; + this.update(cx, |this, cx| { + this.snapshot = snapshot; + this.edits_since_sync = this + .edits_since_sync + .compose(mem::take(&mut this.interpolated_edits).invert()) + .compose(&edits); + this.background_task = None; + this.flush_edits(cx); + cx.notify(); + }) + .ok(); + })); } } } @@ -1065,12 +1065,12 @@ impl sum_tree::Item for Transform { } fn push_isomorphic(transforms: &mut Vec<Transform>, summary: TextSummary) { - if let Some(last_transform) = transforms.last_mut() { - if last_transform.is_isomorphic() { - last_transform.summary.input += &summary; - last_transform.summary.output += &summary; - return; - } + if let Some(last_transform) = transforms.last_mut() + && last_transform.is_isomorphic() + { + last_transform.summary.input += &summary; + last_transform.summary.output += &summary; + return; } transforms.push(Transform::isomorphic(summary)); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c52a59a9093da90981a830ac76ce2a10fb4af187..ca1f1f8828a8846173454d75a20855286ecbeb76 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -942,10 +942,10 @@ impl ChangeList { } pub fn invert_last_group(&mut self) { - if let Some(last) = self.changes.last_mut() { - if let Some(current) = last.current.as_mut() { - mem::swap(&mut last.original, current); - } + if let Some(last) = self.changes.last_mut() + && let Some(current) = last.current.as_mut() + { + mem::swap(&mut last.original, current); } } } @@ -1861,114 +1861,110 @@ impl Editor { .then(|| language_settings::SoftWrap::None); let mut project_subscriptions = Vec::new(); - if full_mode { - if let Some(project) = project.as_ref() { - project_subscriptions.push(cx.subscribe_in( - project, - window, - |editor, _, event, window, cx| match event { - project::Event::RefreshCodeLens => { - // we always query lens with actions, without storing them, always refreshing them - } - project::Event::RefreshInlayHints => { - editor - .refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); - } - project::Event::LanguageServerAdded(..) - | project::Event::LanguageServerRemoved(..) => { - if editor.tasks_update_task.is_none() { - editor.tasks_update_task = - Some(editor.refresh_runnables(window, cx)); - } + if full_mode && let Some(project) = project.as_ref() { + project_subscriptions.push(cx.subscribe_in( + project, + window, + |editor, _, event, window, cx| match event { + project::Event::RefreshCodeLens => { + // we always query lens with actions, without storing them, always refreshing them + } + project::Event::RefreshInlayHints => { + editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); + } + project::Event::LanguageServerAdded(..) + | project::Event::LanguageServerRemoved(..) => { + if editor.tasks_update_task.is_none() { + editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); } - project::Event::SnippetEdit(id, snippet_edits) => { - if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { - let focus_handle = editor.focus_handle(cx); - if focus_handle.is_focused(window) { - let snapshot = buffer.read(cx).snapshot(); - for (range, snippet) in snippet_edits { - let editor_range = - language::range_from_lsp(*range).to_offset(&snapshot); - editor - .insert_snippet( - &[editor_range], - snippet.clone(), - window, - cx, - ) - .ok(); - } + } + project::Event::SnippetEdit(id, snippet_edits) => { + if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { + let focus_handle = editor.focus_handle(cx); + if focus_handle.is_focused(window) { + let snapshot = buffer.read(cx).snapshot(); + for (range, snippet) in snippet_edits { + let editor_range = + language::range_from_lsp(*range).to_offset(&snapshot); + editor + .insert_snippet( + &[editor_range], + snippet.clone(), + window, + cx, + ) + .ok(); } } } - 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); - } + } + 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); } - _ => {} + } + _ => {} + }, + )); + if let Some(task_inventory) = project + .read(cx) + .task_store() + .read(cx) + .task_inventory() + .cloned() + { + project_subscriptions.push(cx.observe_in( + &task_inventory, + window, + |editor, _, window, cx| { + editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); }, )); - if let Some(task_inventory) = project - .read(cx) - .task_store() - .read(cx) - .task_inventory() - .cloned() - { - project_subscriptions.push(cx.observe_in( - &task_inventory, - window, - |editor, _, window, cx| { - editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); - }, - )); - }; + }; - project_subscriptions.push(cx.subscribe_in( - &project.read(cx).breakpoint_store(), - window, - |editor, _, event, window, cx| match event { - BreakpointStoreEvent::ClearDebugLines => { - editor.clear_row_highlights::<ActiveDebugLine>(); - editor.refresh_inline_values(cx); + project_subscriptions.push(cx.subscribe_in( + &project.read(cx).breakpoint_store(), + window, + |editor, _, event, window, cx| match event { + BreakpointStoreEvent::ClearDebugLines => { + editor.clear_row_highlights::<ActiveDebugLine>(); + editor.refresh_inline_values(cx); + } + BreakpointStoreEvent::SetDebugLine => { + if editor.go_to_active_debug_line(window, cx) { + cx.stop_propagation(); } - BreakpointStoreEvent::SetDebugLine => { - if editor.go_to_active_debug_line(window, cx) { - cx.stop_propagation(); - } - editor.refresh_inline_values(cx); - } - _ => {} - }, - )); - let git_store = project.read(cx).git_store().clone(); - let project = project.clone(); - project_subscriptions.push(cx.subscribe(&git_store, move |this, _, event, cx| { - match event { - GitStoreEvent::RepositoryUpdated( - _, - RepositoryEvent::Updated { - new_instance: true, .. - }, - _, - ) => { - this.load_diff_task = Some( - update_uncommitted_diff_for_buffer( - cx.entity(), - &project, - this.buffer.read(cx).all_buffers(), - this.buffer.clone(), - cx, - ) - .shared(), - ); - } - _ => {} + editor.refresh_inline_values(cx); } - })); - } + _ => {} + }, + )); + let git_store = project.read(cx).git_store().clone(); + let project = project.clone(); + project_subscriptions.push(cx.subscribe(&git_store, move |this, _, event, cx| { + match event { + GitStoreEvent::RepositoryUpdated( + _, + RepositoryEvent::Updated { + new_instance: true, .. + }, + _, + ) => { + this.load_diff_task = Some( + update_uncommitted_diff_for_buffer( + cx.entity(), + &project, + this.buffer.read(cx).all_buffers(), + this.buffer.clone(), + cx, + ) + .shared(), + ); + } + _ => {} + } + })); } let buffer_snapshot = buffer.read(cx).snapshot(cx); @@ -2323,15 +2319,15 @@ impl Editor { editor.go_to_active_debug_line(window, cx); - if let Some(buffer) = buffer.read(cx).as_singleton() { - if let Some(project) = editor.project() { - let handle = project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - editor - .registered_buffers - .insert(buffer.read(cx).remote_id(), handle); - } + if let Some(buffer) = buffer.read(cx).as_singleton() + && let Some(project) = editor.project() + { + let handle = project.update(cx, |project, cx| { + project.register_buffer_with_language_servers(&buffer, cx) + }); + editor + .registered_buffers + .insert(buffer.read(cx).remote_id(), handle); } editor.minimap = @@ -3035,20 +3031,19 @@ impl Editor { } if local { - if let Some(buffer_id) = new_cursor_position.buffer_id { - if !self.registered_buffers.contains_key(&buffer_id) { - if let Some(project) = self.project.as_ref() { - project.update(cx, |project, cx| { - let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else { - return; - }; - self.registered_buffers.insert( - buffer_id, - project.register_buffer_with_language_servers(&buffer, cx), - ); - }) - } - } + if let Some(buffer_id) = new_cursor_position.buffer_id + && !self.registered_buffers.contains_key(&buffer_id) + && let Some(project) = self.project.as_ref() + { + project.update(cx, |project, cx| { + let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else { + return; + }; + self.registered_buffers.insert( + buffer_id, + project.register_buffer_with_language_servers(&buffer, cx), + ); + }) } let mut context_menu = self.context_menu.borrow_mut(); @@ -3063,28 +3058,28 @@ impl Editor { let completion_position = completion_menu.map(|menu| menu.initial_position); drop(context_menu); - if effects.completions { - if let Some(completion_position) = completion_position { - let start_offset = selection_start.to_offset(buffer); - let position_matches = start_offset == completion_position.to_offset(buffer); - let continue_showing = if position_matches { - if self.snippet_stack.is_empty() { - buffer.char_kind_before(start_offset, true) == Some(CharKind::Word) - } else { - // Snippet choices can be shown even when the cursor is in whitespace. - // Dismissing the menu with actions like backspace is handled by - // invalidation regions. - true - } - } else { - false - }; - - if continue_showing { - self.show_completions(&ShowCompletions { trigger: None }, window, cx); + if effects.completions + && let Some(completion_position) = completion_position + { + let start_offset = selection_start.to_offset(buffer); + let position_matches = start_offset == completion_position.to_offset(buffer); + let continue_showing = if position_matches { + if self.snippet_stack.is_empty() { + buffer.char_kind_before(start_offset, true) == Some(CharKind::Word) } else { - self.hide_context_menu(window, cx); + // Snippet choices can be shown even when the cursor is in whitespace. + // Dismissing the menu with actions like backspace is handled by + // invalidation regions. + true } + } else { + false + }; + + if continue_showing { + self.show_completions(&ShowCompletions { trigger: None }, window, cx); + } else { + self.hide_context_menu(window, cx); } } @@ -3115,30 +3110,27 @@ impl Editor { if selections.len() == 1 { cx.emit(SearchEvent::ActiveMatchChanged) } - if local { - if let Some((_, _, buffer_snapshot)) = buffer.as_singleton() { - let inmemory_selections = selections - .iter() - .map(|s| { - text::ToPoint::to_point(&s.range().start.text_anchor, buffer_snapshot) - ..text::ToPoint::to_point(&s.range().end.text_anchor, buffer_snapshot) - }) - .collect(); - self.update_restoration_data(cx, |data| { - data.selections = inmemory_selections; - }); + if local && let Some((_, _, buffer_snapshot)) = buffer.as_singleton() { + let inmemory_selections = selections + .iter() + .map(|s| { + text::ToPoint::to_point(&s.range().start.text_anchor, buffer_snapshot) + ..text::ToPoint::to_point(&s.range().end.text_anchor, buffer_snapshot) + }) + .collect(); + self.update_restoration_data(cx, |data| { + data.selections = inmemory_selections; + }); - if WorkspaceSettings::get(None, cx).restore_on_startup - != RestoreOnStartupBehavior::None - { - if let Some(workspace_id) = - self.workspace.as_ref().and_then(|workspace| workspace.1) - { - let snapshot = self.buffer().read(cx).snapshot(cx); - let selections = selections.clone(); - let background_executor = cx.background_executor().clone(); - let editor_id = cx.entity().entity_id().as_u64() as ItemId; - self.serialize_selections = cx.background_spawn(async move { + if WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None + && let Some(workspace_id) = + self.workspace.as_ref().and_then(|workspace| workspace.1) + { + let snapshot = self.buffer().read(cx).snapshot(cx); + let selections = selections.clone(); + let background_executor = cx.background_executor().clone(); + let editor_id = cx.entity().entity_id().as_u64() as ItemId; + self.serialize_selections = cx.background_spawn(async move { background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; let db_selections = selections .iter() @@ -3155,8 +3147,6 @@ impl Editor { .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}")) .log_err(); }); - } - } } } @@ -4154,42 +4144,38 @@ impl Editor { if self.auto_replace_emoji_shortcode && selection.is_empty() && text.as_ref().ends_with(':') - { - if let Some(possible_emoji_short_code) = + && let Some(possible_emoji_short_code) = Self::find_possible_emoji_shortcode_at_position(&snapshot, selection.start) - { - if !possible_emoji_short_code.is_empty() { - if let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code) { - let emoji_shortcode_start = Point::new( - selection.start.row, - selection.start.column - possible_emoji_short_code.len() as u32 - 1, - ); + && !possible_emoji_short_code.is_empty() + && let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code) + { + let emoji_shortcode_start = Point::new( + selection.start.row, + selection.start.column - possible_emoji_short_code.len() as u32 - 1, + ); - // Remove shortcode from buffer - edits.push(( - emoji_shortcode_start..selection.start, - "".to_string().into(), - )); - new_selections.push(( - Selection { - id: selection.id, - start: snapshot.anchor_after(emoji_shortcode_start), - end: snapshot.anchor_before(selection.start), - reversed: selection.reversed, - goal: selection.goal, - }, - 0, - )); + // Remove shortcode from buffer + edits.push(( + emoji_shortcode_start..selection.start, + "".to_string().into(), + )); + new_selections.push(( + Selection { + id: selection.id, + start: snapshot.anchor_after(emoji_shortcode_start), + end: snapshot.anchor_before(selection.start), + reversed: selection.reversed, + goal: selection.goal, + }, + 0, + )); - // Insert emoji - let selection_start_anchor = snapshot.anchor_after(selection.start); - new_selections.push((selection.map(|_| selection_start_anchor), 0)); - edits.push((selection.start..selection.end, emoji.to_string().into())); + // Insert emoji + let selection_start_anchor = snapshot.anchor_after(selection.start); + new_selections.push((selection.map(|_| selection_start_anchor), 0)); + edits.push((selection.start..selection.end, emoji.to_string().into())); - continue; - } - } - } + continue; } // If not handling any auto-close operation, then just replace the selected @@ -4303,12 +4289,11 @@ impl Editor { |s| s.select(new_selections), ); - if !bracket_inserted { - if let Some(on_type_format_task) = + if !bracket_inserted + && let Some(on_type_format_task) = this.trigger_on_type_formatting(text.to_string(), window, cx) - { - on_type_format_task.detach_and_log_err(cx); - } + { + on_type_format_task.detach_and_log_err(cx); } let editor_settings = EditorSettings::get_global(cx); @@ -5274,10 +5259,10 @@ impl Editor { } let language = buffer.language()?; - if let Some(restrict_to_languages) = restrict_to_languages { - if !restrict_to_languages.contains(language) { - return None; - } + if let Some(restrict_to_languages) = restrict_to_languages + && !restrict_to_languages.contains(language) + { + return None; } Some(( excerpt_id, @@ -5605,15 +5590,15 @@ impl Editor { // that having one source with `is_incomplete: true` doesn't cause all to be re-queried. let mut completions = Vec::new(); let mut is_incomplete = false; - if let Some(provider_responses) = provider_responses.await.log_err() { - if !provider_responses.is_empty() { - for response in provider_responses { - completions.extend(response.completions); - is_incomplete = is_incomplete || response.is_incomplete; - } - if completion_settings.words == WordsCompletionMode::Fallback { - words = Task::ready(BTreeMap::default()); - } + if let Some(provider_responses) = provider_responses.await.log_err() + && !provider_responses.is_empty() + { + for response in provider_responses { + completions.extend(response.completions); + is_incomplete = is_incomplete || response.is_incomplete; + } + if completion_settings.words == WordsCompletionMode::Fallback { + words = Task::ready(BTreeMap::default()); } } @@ -5718,21 +5703,21 @@ impl Editor { editor .update_in(cx, |editor, window, cx| { - if editor.focus_handle.is_focused(window) { - if let Some(menu) = menu { - *editor.context_menu.borrow_mut() = - Some(CodeContextMenu::Completions(menu)); - - crate::hover_popover::hide_hover(editor, cx); - if editor.show_edit_predictions_in_menu() { - editor.update_visible_edit_prediction(window, cx); - } else { - editor.discard_edit_prediction(false, cx); - } + if editor.focus_handle.is_focused(window) + && let Some(menu) = menu + { + *editor.context_menu.borrow_mut() = + Some(CodeContextMenu::Completions(menu)); - cx.notify(); - return; + crate::hover_popover::hide_hover(editor, cx); + if editor.show_edit_predictions_in_menu() { + editor.update_visible_edit_prediction(window, cx); + } else { + editor.discard_edit_prediction(false, cx); } + + cx.notify(); + return; } if editor.completion_tasks.len() <= 1 { @@ -6079,11 +6064,11 @@ impl Editor { Some(CodeActionSource::Indicator(_)) => Task::ready(Ok(Default::default())), _ => { let mut task_context_task = Task::ready(None); - if let Some(tasks) = &tasks { - if let Some(project) = project { - task_context_task = - Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx); - } + if let Some(tasks) = &tasks + && let Some(project) = project + { + task_context_task = + Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx); } cx.spawn_in(window, { @@ -6148,14 +6133,14 @@ impl Editor { deployed_from, })); cx.notify(); - if spawn_straight_away { - if let Some(task) = editor.confirm_code_action( + if spawn_straight_away + && let Some(task) = editor.confirm_code_action( &ConfirmCodeAction { item_ix: Some(0) }, window, cx, - ) { - return task; - } + ) + { + return task; } Task::ready(Ok(())) @@ -6342,21 +6327,20 @@ impl Editor { .read(cx) .excerpt_containing(editor.selections.newest_anchor().head(), cx) })?; - if let Some((_, excerpted_buffer, excerpt_range)) = excerpt { - if excerpted_buffer == *buffer { - let all_edits_within_excerpt = buffer.read_with(cx, |buffer, _| { - let excerpt_range = excerpt_range.to_offset(buffer); - buffer - .edited_ranges_for_transaction::<usize>(transaction) - .all(|range| { - excerpt_range.start <= range.start - && excerpt_range.end >= range.end - }) - })?; + if let Some((_, excerpted_buffer, excerpt_range)) = excerpt + && excerpted_buffer == *buffer + { + let all_edits_within_excerpt = buffer.read_with(cx, |buffer, _| { + let excerpt_range = excerpt_range.to_offset(buffer); + buffer + .edited_ranges_for_transaction::<usize>(transaction) + .all(|range| { + excerpt_range.start <= range.start && excerpt_range.end >= range.end + }) + })?; - if all_edits_within_excerpt { - return Ok(()); - } + if all_edits_within_excerpt { + return Ok(()); } } } @@ -7779,10 +7763,10 @@ impl Editor { let indents = multibuffer.suggested_indents(cursor_point.row..cursor_point.row + 1, cx); - if let Some((_, indent)) = indents.iter().next() { - if indent.len == cursor_point.column { - self.edit_prediction_indent_conflict = false; - } + if let Some((_, indent)) = indents.iter().next() + && indent.len == cursor_point.column + { + self.edit_prediction_indent_conflict = false; } } @@ -9531,10 +9515,10 @@ impl Editor { let context_menu = self.context_menu.borrow_mut().take(); 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); - } + if let Some(CodeContextMenu::Completions(_)) = &context_menu + && let Some(completion_provider) = &self.completion_provider + { + completion_provider.selection_changed(None, window, cx); } context_menu } @@ -9639,10 +9623,10 @@ impl Editor { s.select_ranges(tabstop.ranges.iter().rev().cloned()); }); - if let Some(choices) = &tabstop.choices { - if let Some(selection) = tabstop.ranges.first() { - self.show_snippet_choices(choices, selection.clone(), cx) - } + if let Some(choices) = &tabstop.choices + && let Some(selection) = tabstop.ranges.first() + { + self.show_snippet_choices(choices, selection.clone(), cx) } // If we're already at the last tabstop and it's at the end of the snippet, @@ -9776,10 +9760,10 @@ impl Editor { s.select_ranges(current_ranges.iter().rev().cloned()) }); - if let Some(choices) = &snippet.choices[snippet.active_index] { - if let Some(selection) = current_ranges.first() { - self.show_snippet_choices(choices, selection.clone(), cx); - } + if let Some(choices) = &snippet.choices[snippet.active_index] + && let Some(selection) = current_ranges.first() + { + self.show_snippet_choices(choices, selection.clone(), cx); } // If snippet state is not at the last tabstop, push it back on the stack @@ -10176,10 +10160,10 @@ impl Editor { // Avoid re-outdenting a row that has already been outdented by a // previous selection. - if let Some(last_row) = last_outdent { - if last_row == rows.start { - rows.start = rows.start.next_row(); - } + if let Some(last_row) = last_outdent + && last_row == rows.start + { + rows.start = rows.start.next_row(); } let has_multiple_rows = rows.len() > 1; for row in rows.iter_rows() { @@ -10357,11 +10341,11 @@ impl Editor { MultiBufferRow(selection.end.row) }; - if let Some(last_row_range) = row_ranges.last_mut() { - if start <= last_row_range.end { - last_row_range.end = end; - continue; - } + if let Some(last_row_range) = row_ranges.last_mut() + && start <= last_row_range.end + { + last_row_range.end = end; + continue; } row_ranges.push(start..end); } @@ -15331,17 +15315,15 @@ impl Editor { if direction == ExpandExcerptDirection::Down { let multi_buffer = self.buffer.read(cx); let snapshot = multi_buffer.snapshot(cx); - if let Some(buffer_id) = snapshot.buffer_id_for_excerpt(excerpt) { - if let Some(buffer) = multi_buffer.buffer(buffer_id) { - if let Some(excerpt_range) = snapshot.buffer_range_for_excerpt(excerpt) { - let buffer_snapshot = buffer.read(cx).snapshot(); - let excerpt_end_row = - Point::from_anchor(&excerpt_range.end, &buffer_snapshot).row; - let last_row = buffer_snapshot.max_point().row; - let lines_below = last_row.saturating_sub(excerpt_end_row); - should_scroll_up = lines_below >= lines_to_expand; - } - } + if let Some(buffer_id) = snapshot.buffer_id_for_excerpt(excerpt) + && let Some(buffer) = multi_buffer.buffer(buffer_id) + && let Some(excerpt_range) = snapshot.buffer_range_for_excerpt(excerpt) + { + let buffer_snapshot = buffer.read(cx).snapshot(); + let excerpt_end_row = Point::from_anchor(&excerpt_range.end, &buffer_snapshot).row; + let last_row = buffer_snapshot.max_point().row; + let lines_below = last_row.saturating_sub(excerpt_end_row); + should_scroll_up = lines_below >= lines_to_expand; } } @@ -15426,10 +15408,10 @@ impl Editor { let selection = self.selections.newest::<usize>(cx); let mut active_group_id = None; - if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics { - if active_group.active_range.start.to_offset(&buffer) == selection.start { - active_group_id = Some(active_group.group_id); - } + if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics + && active_group.active_range.start.to_offset(&buffer) == selection.start + { + active_group_id = Some(active_group.group_id); } fn filtered( @@ -16674,10 +16656,10 @@ impl Editor { buffer .update(cx, |buffer, cx| { - if let Some(transaction) = transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0, cx); - } + if let Some(transaction) = transaction + && !buffer.is_singleton() + { + buffer.push_transaction(&transaction.0, cx); } cx.notify(); }) @@ -16743,10 +16725,10 @@ impl Editor { buffer .update(cx, |buffer, cx| { // check if we need this - if let Some(transaction) = transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0, cx); - } + if let Some(transaction) = transaction + && !buffer.is_singleton() + { + buffer.push_transaction(&transaction.0, cx); } cx.notify(); }) @@ -17378,12 +17360,12 @@ impl Editor { } for row in (0..=range.start.row).rev() { - if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { - if crease.range().end.row >= buffer_start_row { - to_fold.push(crease); - if row <= range.start.row { - break; - } + if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) + && crease.range().end.row >= buffer_start_row + { + to_fold.push(crease); + if row <= range.start.row { + break; } } } @@ -18693,10 +18675,10 @@ impl Editor { pub fn working_directory(&self, cx: &App) -> Option<PathBuf> { if let Some(buffer) = self.buffer().read(cx).as_singleton() { - if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { - if let Some(dir) = file.abs_path(cx).parent() { - return Some(dir.to_owned()); - } + if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) + && let Some(dir) = file.abs_path(cx).parent() + { + return Some(dir.to_owned()); } if let Some(project_path) = buffer.read(cx).project_path(cx) { @@ -18756,10 +18738,10 @@ impl Editor { _window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(path) = self.target_file_abs_path(cx) { - if let Some(path) = path.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); - } + if let Some(path) = self.target_file_abs_path(cx) + && let Some(path) = path.to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); } } @@ -18769,10 +18751,10 @@ impl Editor { _window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(path) = self.target_file_path(cx) { - if let Some(path) = path.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); - } + if let Some(path) = self.target_file_path(cx) + && let Some(path) = path.to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); } } @@ -18841,22 +18823,20 @@ impl Editor { _: &mut Window, cx: &mut Context<Self>, ) { - if let Some(file) = self.target_file(cx) { - if let Some(file_stem) = file.path().file_stem() { - if let Some(name) = file_stem.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); - } - } + if let Some(file) = self.target_file(cx) + && let Some(file_stem) = file.path().file_stem() + && let Some(name) = file_stem.to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); } } pub fn copy_file_name(&mut self, _: &CopyFileName, _: &mut Window, cx: &mut Context<Self>) { - if let Some(file) = self.target_file(cx) { - if let Some(file_name) = file.path().file_name() { - if let Some(name) = file_name.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); - } - } + if let Some(file) = self.target_file(cx) + && let Some(file_name) = file.path().file_name() + && let Some(name) = file_name.to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); } } @@ -19126,10 +19106,10 @@ impl Editor { cx: &mut Context<Self>, ) { let selection = self.selections.newest::<Point>(cx).start.row + 1; - if let Some(file) = self.target_file(cx) { - if let Some(path) = file.path().to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}"))); - } + if let Some(file) = self.target_file(cx) + && let Some(path) = file.path().to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}"))); } } @@ -19769,10 +19749,10 @@ impl Editor { break; } let end = range.end.to_point(&display_snapshot.buffer_snapshot); - if let Some(current_row) = &end_row { - if end.row == current_row.row { - continue; - } + if let Some(current_row) = &end_row + && end.row == current_row.row + { + continue; } let start = range.start.to_point(&display_snapshot.buffer_snapshot); if start_row.is_none() { @@ -20064,16 +20044,16 @@ impl Editor { 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 { - project.update(cx, |project, cx| { - self.registered_buffers - .entry(edited_buffer.read(cx).remote_id()) - .or_insert_with(|| { - project.register_buffer_with_language_servers(edited_buffer, cx) - }); - }); - } + if let Some(project) = self.project.as_ref() + && let Some(edited_buffer) = edited_buffer + { + project.update(cx, |project, cx| { + self.registered_buffers + .entry(edited_buffer.read(cx).remote_id()) + .or_insert_with(|| { + project.register_buffer_with_language_servers(edited_buffer, cx) + }); + }); } cx.emit(EditorEvent::BufferEdited); cx.emit(SearchEvent::MatchesInvalidated); @@ -20083,10 +20063,10 @@ impl Editor { } if *singleton_buffer_edited { - if let Some(buffer) = edited_buffer { - if buffer.read(cx).file().is_none() { - cx.emit(EditorEvent::TitleChanged); - } + if let Some(buffer) = edited_buffer + && buffer.read(cx).file().is_none() + { + cx.emit(EditorEvent::TitleChanged); } if let Some(project) = &self.project { #[allow(clippy::mutable_key_type)] @@ -20132,17 +20112,17 @@ impl Editor { } => { self.tasks_update_task = Some(self.refresh_runnables(window, cx)); let buffer_id = buffer.read(cx).remote_id(); - if self.buffer.read(cx).diff_for(buffer_id).is_none() { - if let Some(project) = &self.project { - update_uncommitted_diff_for_buffer( - cx.entity(), - project, - [buffer.clone()], - self.buffer.clone(), - cx, - ) - .detach(); - } + if self.buffer.read(cx).diff_for(buffer_id).is_none() + && let Some(project) = &self.project + { + update_uncommitted_diff_for_buffer( + cx.entity(), + project, + [buffer.clone()], + self.buffer.clone(), + cx, + ) + .detach(); } self.update_lsp_data(false, Some(buffer_id), window, cx); cx.emit(EditorEvent::ExcerptsAdded { @@ -20746,11 +20726,11 @@ impl Editor { let mut chunk_lines = chunk.text.split('\n').peekable(); while let Some(text) = chunk_lines.next() { let mut merged_with_last_token = false; - if let Some(last_token) = line.back_mut() { - if last_token.highlight == highlight { - last_token.text.push_str(text); - merged_with_last_token = true; - } + if let Some(last_token) = line.back_mut() + && last_token.highlight == highlight + { + last_token.text.push_str(text); + merged_with_last_token = true; } if !merged_with_last_token { @@ -21209,39 +21189,37 @@ impl Editor { { let buffer_snapshot = OnceCell::new(); - if let Some(folds) = DB.get_editor_folds(item_id, workspace_id).log_err() { - if !folds.is_empty() { - let snapshot = - buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); - self.fold_ranges( - folds - .into_iter() - .map(|(start, end)| { - snapshot.clip_offset(start, Bias::Left) - ..snapshot.clip_offset(end, Bias::Right) - }) - .collect(), - false, - window, - cx, - ); - } - } - - if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() { - if !selections.is_empty() { - let snapshot = - buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); - // skip adding the initial selection to selection history - self.selection_history.mode = SelectionHistoryMode::Skipping; - self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(selections.into_iter().map(|(start, end)| { + if let Some(folds) = DB.get_editor_folds(item_id, workspace_id).log_err() + && !folds.is_empty() + { + let snapshot = buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); + self.fold_ranges( + folds + .into_iter() + .map(|(start, end)| { snapshot.clip_offset(start, Bias::Left) ..snapshot.clip_offset(end, Bias::Right) - })); - }); - self.selection_history.mode = SelectionHistoryMode::Normal; - } + }) + .collect(), + false, + window, + cx, + ); + } + + if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() + && !selections.is_empty() + { + let snapshot = buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); + // skip adding the initial selection to selection history + self.selection_history.mode = SelectionHistoryMode::Skipping; + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(selections.into_iter().map(|(start, end)| { + snapshot.clip_offset(start, Bias::Left) + ..snapshot.clip_offset(end, Bias::Right) + })); + }); + self.selection_history.mode = SelectionHistoryMode::Normal; }; } @@ -21283,17 +21261,15 @@ fn process_completion_for_edit( let mut snippet_source = completion.new_text.clone(); let mut previous_point = text::ToPoint::to_point(cursor_position, buffer); previous_point.column = previous_point.column.saturating_sub(1); - if let Some(scope) = buffer_snapshot.language_scope_at(previous_point) { - if scope.prefers_label_for_snippet_in_completion() { - if let Some(label) = completion.label() { - if matches!( - completion.kind(), - Some(CompletionItemKind::FUNCTION) | Some(CompletionItemKind::METHOD) - ) { - snippet_source = label; - } - } - } + if let Some(scope) = buffer_snapshot.language_scope_at(previous_point) + && scope.prefers_label_for_snippet_in_completion() + && let Some(label) = completion.label() + && matches!( + completion.kind(), + Some(CompletionItemKind::FUNCTION) | Some(CompletionItemKind::METHOD) + ) + { + snippet_source = label; } match Snippet::parse(&snippet_source).log_err() { Some(parsed_snippet) => (Some(parsed_snippet.clone()), parsed_snippet.text), @@ -21347,10 +21323,10 @@ fn process_completion_for_edit( ); let mut current_needle = text_to_replace.next(); for haystack_ch in completion.label.text.chars() { - if let Some(needle_ch) = current_needle { - if haystack_ch.eq_ignore_ascii_case(&needle_ch) { - current_needle = text_to_replace.next(); - } + if let Some(needle_ch) = current_needle + && haystack_ch.eq_ignore_ascii_case(&needle_ch) + { + current_needle = text_to_replace.next(); } } current_needle.is_none() @@ -21604,11 +21580,11 @@ impl<'a> Iterator for WordBreakingTokenizer<'a> { offset += first_grapheme.len(); grapheme_len += 1; if is_grapheme_ideographic(first_grapheme) && !is_whitespace { - if let Some(grapheme) = iter.peek().copied() { - if should_stay_with_preceding_ideograph(grapheme) { - offset += grapheme.len(); - grapheme_len += 1; - } + if let Some(grapheme) = iter.peek().copied() + && should_stay_with_preceding_ideograph(grapheme) + { + offset += grapheme.len(); + grapheme_len += 1; } } else { let mut words = self.input[offset..].split_word_bound_indices().peekable(); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 927a207358e2ee1544ca2b7938c39d23caaee9a0..915a3cdc381c4cc949561f85c24e13477dd0f906 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -917,6 +917,10 @@ impl EditorElement { } else if cfg!(any(target_os = "linux", target_os = "freebsd")) && event.button == MouseButton::Middle { + #[allow( + clippy::collapsible_if, + reason = "The cfg-block below makes this a false positive" + )] if !text_hitbox.is_hovered(window) || editor.read_only(cx) { return; } @@ -1387,29 +1391,27 @@ impl EditorElement { ref drop_cursor, ref hide_drop_cursor, } = editor.selection_drag_state + && !hide_drop_cursor + && (drop_cursor + .start + .cmp(&selection.start, &snapshot.buffer_snapshot) + .eq(&Ordering::Less) + || drop_cursor + .end + .cmp(&selection.end, &snapshot.buffer_snapshot) + .eq(&Ordering::Greater)) { - if !hide_drop_cursor - && (drop_cursor - .start - .cmp(&selection.start, &snapshot.buffer_snapshot) - .eq(&Ordering::Less) - || drop_cursor - .end - .cmp(&selection.end, &snapshot.buffer_snapshot) - .eq(&Ordering::Greater)) - { - let drag_cursor_layout = SelectionLayout::new( - drop_cursor.clone(), - false, - CursorShape::Bar, - &snapshot.display_snapshot, - false, - false, - None, - ); - let absent_color = cx.theme().players().absent(); - selections.push((absent_color, vec![drag_cursor_layout])); - } + let drag_cursor_layout = SelectionLayout::new( + drop_cursor.clone(), + false, + CursorShape::Bar, + &snapshot.display_snapshot, + false, + false, + None, + ); + let absent_color = cx.theme().players().absent(); + selections.push((absent_color, vec![drag_cursor_layout])); } } @@ -1420,19 +1422,15 @@ impl EditorElement { CollaboratorId::PeerId(peer_id) => { if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&peer_id) - { - if let Some(participant_index) = collaboration_hub + && let Some(participant_index) = collaboration_hub .user_participant_indices(cx) .get(&collaborator.user_id) - { - if let Some((local_selection_style, _)) = selections.first_mut() - { - *local_selection_style = cx - .theme() - .players() - .color_for_participant(participant_index.0); - } - } + && let Some((local_selection_style, _)) = selections.first_mut() + { + *local_selection_style = cx + .theme() + .players() + .color_for_participant(participant_index.0); } } CollaboratorId::Agent => { @@ -3518,33 +3516,33 @@ impl EditorElement { let mut x_offset = px(0.); let mut is_block = true; - if let BlockId::Custom(custom_block_id) = block_id { - if block.has_height() { - if block.place_near() { - if let Some((x_target, line_width)) = x_position { - let margin = em_width * 2; - if line_width + final_size.width + margin - < editor_width + editor_margins.gutter.full_width() - && !row_block_types.contains_key(&(row - 1)) - && element_height_in_lines == 1 - { - x_offset = line_width + margin; - row = row - 1; - is_block = false; - element_height_in_lines = 0; - row_block_types.insert(row, is_block); - } else { - let max_offset = editor_width + editor_margins.gutter.full_width() - - final_size.width; - let min_offset = (x_target + em_width - final_size.width) - .max(editor_margins.gutter.full_width()); - x_offset = x_target.min(max_offset).max(min_offset); - } - } - }; - if element_height_in_lines != block.height() { - resized_blocks.insert(custom_block_id, element_height_in_lines); + if let BlockId::Custom(custom_block_id) = block_id + && block.has_height() + { + if block.place_near() + && let Some((x_target, line_width)) = x_position + { + let margin = em_width * 2; + if line_width + final_size.width + margin + < editor_width + editor_margins.gutter.full_width() + && !row_block_types.contains_key(&(row - 1)) + && element_height_in_lines == 1 + { + x_offset = line_width + margin; + row = row - 1; + is_block = false; + element_height_in_lines = 0; + row_block_types.insert(row, is_block); + } else { + let max_offset = + editor_width + editor_margins.gutter.full_width() - final_size.width; + let min_offset = (x_target + em_width - final_size.width) + .max(editor_margins.gutter.full_width()); + x_offset = x_target.min(max_offset).max(min_offset); } + }; + if element_height_in_lines != block.height() { + resized_blocks.insert(custom_block_id, element_height_in_lines); } } for i in 0..element_height_in_lines { @@ -3987,60 +3985,58 @@ impl EditorElement { } } - if let Some(focused_block) = focused_block { - if let Some(focus_handle) = focused_block.focus_handle.upgrade() { - if focus_handle.is_focused(window) { - if let Some(block) = snapshot.block_for_id(focused_block.id) { - let style = block.style(); - let width = match style { - BlockStyle::Fixed => AvailableSpace::MinContent, - BlockStyle::Flex => AvailableSpace::Definite( - hitbox - .size - .width - .max(fixed_block_max_width) - .max(editor_margins.gutter.width + *scroll_width), - ), - BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width), - }; + if let Some(focused_block) = focused_block + && let Some(focus_handle) = focused_block.focus_handle.upgrade() + && focus_handle.is_focused(window) + && let Some(block) = snapshot.block_for_id(focused_block.id) + { + let style = block.style(); + let width = match style { + BlockStyle::Fixed => AvailableSpace::MinContent, + BlockStyle::Flex => AvailableSpace::Definite( + hitbox + .size + .width + .max(fixed_block_max_width) + .max(editor_margins.gutter.width + *scroll_width), + ), + BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width), + }; - if let Some((element, element_size, _, x_offset)) = self.render_block( - &block, - width, - focused_block.id, - rows.end, - snapshot, - text_x, - &rows, - line_layouts, - editor_margins, - line_height, - em_width, - text_hitbox, - editor_width, - scroll_width, - &mut resized_blocks, - &mut row_block_types, - selections, - selected_buffer_ids, - is_row_soft_wrapped, - sticky_header_excerpt_id, - window, - cx, - ) { - blocks.push(BlockLayout { - id: block.id(), - x_offset, - row: None, - element, - available_space: size(width, element_size.height.into()), - style, - overlaps_gutter: true, - is_buffer_header: block.is_buffer_header(), - }); - } - } - } + if let Some((element, element_size, _, x_offset)) = self.render_block( + &block, + width, + focused_block.id, + rows.end, + snapshot, + text_x, + &rows, + line_layouts, + editor_margins, + line_height, + em_width, + text_hitbox, + editor_width, + scroll_width, + &mut resized_blocks, + &mut row_block_types, + selections, + selected_buffer_ids, + is_row_soft_wrapped, + sticky_header_excerpt_id, + window, + cx, + ) { + blocks.push(BlockLayout { + id: block.id(), + x_offset, + row: None, + element, + available_space: size(width, element_size.height.into()), + style, + overlaps_gutter: true, + is_buffer_header: block.is_buffer_header(), + }); } } @@ -4203,19 +4199,19 @@ impl EditorElement { edit_prediction_popover_visible = true; } - if editor.context_menu_visible() { - if let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin() { - let (min_height_in_lines, max_height_in_lines) = editor - .context_menu_options - .as_ref() - .map_or((3, 12), |options| { - (options.min_entries_visible, options.max_entries_visible) - }); + if editor.context_menu_visible() + && let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin() + { + let (min_height_in_lines, max_height_in_lines) = editor + .context_menu_options + .as_ref() + .map_or((3, 12), |options| { + (options.min_entries_visible, options.max_entries_visible) + }); - min_menu_height += line_height * min_height_in_lines as f32 + POPOVER_Y_PADDING; - max_menu_height += line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING; - context_menu_visible = true; - } + min_menu_height += line_height * min_height_in_lines as f32 + POPOVER_Y_PADDING; + max_menu_height += line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING; + context_menu_visible = true; } context_menu_placement = editor .context_menu_options @@ -5761,16 +5757,15 @@ impl EditorElement { cx: &mut App, ) { for (_, hunk_hitbox) in &layout.display_hunks { - if let Some(hunk_hitbox) = hunk_hitbox { - if !self + if let Some(hunk_hitbox) = hunk_hitbox + && !self .editor .read(cx) .buffer() .read(cx) .all_diff_hunks_expanded() - { - window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox); - } + { + window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox); } } @@ -10152,10 +10147,10 @@ fn compute_auto_height_layout( let overscroll = size(em_width, px(0.)); let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width; - if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None) { - if editor.set_wrap_width(Some(editor_width), cx) { - snapshot = editor.snapshot(window, cx); - } + if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None) + && editor.set_wrap_width(Some(editor_width), cx) + { + snapshot = editor.snapshot(window, cx); } let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height; diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index fc350a5a15b4f7b105872e61e5a2401d183c1a6d..712325f339867a8629b7f6ffca60369d7603e9cd 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -312,10 +312,10 @@ impl GitBlame { .as_ref() .and_then(|entry| entry.author.as_ref()) .map(|author| author.len()); - if let Some(author_len) = author_len { - if author_len > max_author_length { - max_author_length = author_len; - } + if let Some(author_len) = author_len + && author_len > max_author_length + { + max_author_length = author_len; } } @@ -416,20 +416,19 @@ impl GitBlame { if row_edits .peek() .map_or(true, |next_edit| next_edit.old.start >= old_end) + && let Some(entry) = cursor.item() { - if let Some(entry) = cursor.item() { - if old_end > edit.old.end { - new_entries.push( - GitBlameEntry { - rows: cursor.end() - edit.old.end, - blame: entry.blame.clone(), - }, - &(), - ); - } - - cursor.next(); + if old_end > edit.old.end { + new_entries.push( + GitBlameEntry { + rows: cursor.end() - edit.old.end, + blame: entry.blame.clone(), + }, + &(), + ); } + + cursor.next(); } } new_entries.append(cursor.suffix(), &()); diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 8b6e2cea842ecc331ca94ffc43611056b302e38c..b431834d350a17bd097fa7a4f04f17ea12922451 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -418,24 +418,22 @@ pub fn update_inlay_link_and_hover_points( } if let Some((language_server_id, location)) = hovered_hint_part.location + && secondary_held + && !editor.has_pending_nonempty_selection() { - if secondary_held - && !editor.has_pending_nonempty_selection() - { - go_to_definition_updated = true; - show_link_definition( - shift_held, - editor, - TriggerPoint::InlayHint( - highlight, - location, - language_server_id, - ), - snapshot, - window, - cx, - ); - } + go_to_definition_updated = true; + show_link_definition( + shift_held, + editor, + TriggerPoint::InlayHint( + highlight, + location, + language_server_id, + ), + snapshot, + window, + cx, + ); } } } @@ -766,10 +764,11 @@ pub(crate) fn find_url_from_range( let mut finder = LinkFinder::new(); finder.kinds(&[LinkKind::Url]); - if let Some(link) = finder.links(&text).next() { - if link.start() == 0 && link.end() == text.len() { - return Some(link.as_str().to_string()); - } + if let Some(link) = finder.links(&text).next() + && link.start() == 0 + && link.end() == text.len() + { + return Some(link.as_str().to_string()); } None diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 6fe981fd6e9aac29402d13b2edb6a2d05cca67bc..a8cdfa99df4136f1c79821abf31c6ba7932cc891 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -142,11 +142,11 @@ pub fn hover_at_inlay( .info_popovers .iter() .any(|InfoPopover { symbol_range, .. }| { - if let RangeInEditor::Inlay(range) = symbol_range { - if range == &inlay_hover.range { - // Hover triggered from same location as last time. Don't show again. - return true; - } + if let RangeInEditor::Inlay(range) = symbol_range + && range == &inlay_hover.range + { + // Hover triggered from same location as last time. Don't show again. + return true; } false }) @@ -270,13 +270,12 @@ fn show_hover( } // Don't request again if the location is the same as the previous request - if let Some(triggered_from) = &editor.hover_state.triggered_from { - if triggered_from + if let Some(triggered_from) = &editor.hover_state.triggered_from + && triggered_from .cmp(&anchor, &snapshot.buffer_snapshot) .is_eq() - { - return None; - } + { + return None; } let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay; @@ -717,59 +716,54 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { } pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) { - if let Ok(uri) = Url::parse(&link) { - if uri.scheme() == "file" { - if let Some(workspace) = window.root::<Workspace>().flatten() { - workspace.update(cx, |workspace, cx| { - let task = workspace.open_abs_path( - PathBuf::from(uri.path()), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ); + if let Ok(uri) = Url::parse(&link) + && uri.scheme() == "file" + && let Some(workspace) = window.root::<Workspace>().flatten() + { + workspace.update(cx, |workspace, cx| { + let task = workspace.open_abs_path( + PathBuf::from(uri.path()), + OpenOptions { + visible: Some(OpenVisible::None), + ..Default::default() + }, + window, + cx, + ); - cx.spawn_in(window, async move |_, cx| { - let item = task.await?; - // Ruby LSP uses URLs with #L1,1-4,4 - // we'll just take the first number and assume it's a line number - let Some(fragment) = uri.fragment() else { - return anyhow::Ok(()); - }; - let mut accum = 0u32; - for c in fragment.chars() { - if c >= '0' && c <= '9' && accum < u32::MAX / 2 { - accum *= 10; - accum += c as u32 - '0' as u32; - } else if accum > 0 { - break; - } - } - if accum == 0 { - return Ok(()); - } - let Some(editor) = cx.update(|_, cx| item.act_as::<Editor>(cx))? else { - return Ok(()); - }; - editor.update_in(cx, |editor, window, cx| { - editor.change_selections( - Default::default(), - window, - cx, - |selections| { - selections.select_ranges([text::Point::new(accum - 1, 0) - ..text::Point::new(accum - 1, 0)]); - }, - ); - }) - }) - .detach_and_log_err(cx); - }); - return; - } - } + cx.spawn_in(window, async move |_, cx| { + let item = task.await?; + // Ruby LSP uses URLs with #L1,1-4,4 + // we'll just take the first number and assume it's a line number + let Some(fragment) = uri.fragment() else { + return anyhow::Ok(()); + }; + let mut accum = 0u32; + for c in fragment.chars() { + if c >= '0' && c <= '9' && accum < u32::MAX / 2 { + accum *= 10; + accum += c as u32 - '0' as u32; + } else if accum > 0 { + break; + } + } + if accum == 0 { + return Ok(()); + } + let Some(editor) = cx.update(|_, cx| item.act_as::<Editor>(cx))? else { + return Ok(()); + }; + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_ranges([ + text::Point::new(accum - 1, 0)..text::Point::new(accum - 1, 0) + ]); + }); + }) + }) + .detach_and_log_err(cx); + }); + return; } cx.open_url(&link); } @@ -839,20 +833,19 @@ impl HoverState { pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool { let mut hover_popover_is_focused = false; for info_popover in &self.info_popovers { - if let Some(markdown_view) = &info_popover.parsed_content { - if markdown_view.focus_handle(cx).is_focused(window) { - hover_popover_is_focused = true; - } + if let Some(markdown_view) = &info_popover.parsed_content + && markdown_view.focus_handle(cx).is_focused(window) + { + hover_popover_is_focused = true; } } - if let Some(diagnostic_popover) = &self.diagnostic_popover { - if diagnostic_popover + if let Some(diagnostic_popover) = &self.diagnostic_popover + && diagnostic_popover .markdown .focus_handle(cx) .is_focused(window) - { - hover_popover_is_focused = true; - } + { + hover_popover_is_focused = true; } hover_popover_is_focused } diff --git a/crates/editor/src/indent_guides.rs b/crates/editor/src/indent_guides.rs index f6d51c929a95ac3d256095b627c303a6c49a64a5..a1de2b604bd51cdae7efaaf19492ade911d6156c 100644 --- a/crates/editor/src/indent_guides.rs +++ b/crates/editor/src/indent_guides.rs @@ -168,11 +168,11 @@ pub fn indent_guides_in_range( while let Some(fold) = folds.next() { let start = fold.range.start.to_point(&snapshot.buffer_snapshot); let end = fold.range.end.to_point(&snapshot.buffer_snapshot); - if let Some(last_range) = fold_ranges.last_mut() { - if last_range.end >= start { - last_range.end = last_range.end.max(end); - continue; - } + if let Some(last_range) = fold_ranges.last_mut() + && last_range.end >= start + { + last_range.end = last_range.end.max(end); + continue; } fold_ranges.push(start..end); } diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 60ad0e5bf6c5672a3ce651793b8f76a82ab4c0ff..cea0e32d7fe633afd3835e80a38426c3603c35bb 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -498,16 +498,14 @@ impl InlayHintCache { cmp::Ordering::Less | cmp::Ordering::Equal => { if !old_kinds.contains(&cached_hint.kind) && new_kinds.contains(&cached_hint.kind) - { - if let Some(anchor) = multi_buffer_snapshot + && let Some(anchor) = multi_buffer_snapshot .anchor_in_excerpt(*excerpt_id, cached_hint.position) - { - to_insert.push(Inlay::hint( - cached_hint_id.id(), - anchor, - cached_hint, - )); - } + { + to_insert.push(Inlay::hint( + cached_hint_id.id(), + anchor, + cached_hint, + )); } excerpt_cache.next(); } @@ -522,16 +520,16 @@ impl InlayHintCache { for cached_hint_id in excerpt_cache { let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id]; let cached_hint_kind = maybe_missed_cached_hint.kind; - if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) { - if let Some(anchor) = multi_buffer_snapshot + if !old_kinds.contains(&cached_hint_kind) + && new_kinds.contains(&cached_hint_kind) + && let Some(anchor) = multi_buffer_snapshot .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position) - { - to_insert.push(Inlay::hint( - cached_hint_id.id(), - anchor, - maybe_missed_cached_hint, - )); - } + { + to_insert.push(Inlay::hint( + cached_hint_id.id(), + anchor, + maybe_missed_cached_hint, + )); } } } @@ -620,44 +618,44 @@ impl InlayHintCache { ) { if let Some(excerpt_hints) = self.hints.get(&excerpt_id) { let mut guard = excerpt_hints.write(); - if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) { - if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state { - let hint_to_resolve = cached_hint.clone(); - let server_id = *server_id; - cached_hint.resolve_state = ResolveState::Resolving; - drop(guard); - cx.spawn_in(window, async move |editor, cx| { - let resolved_hint_task = editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).buffer(buffer_id)?; - editor.semantics_provider.as_ref()?.resolve_inlay_hint( - hint_to_resolve, - buffer, - server_id, - cx, - ) - })?; - if let Some(resolved_hint_task) = resolved_hint_task { - let mut resolved_hint = - resolved_hint_task.await.context("hint resolve task")?; - editor.read_with(cx, |editor, _| { - if let Some(excerpt_hints) = - editor.inlay_hint_cache.hints.get(&excerpt_id) + if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) + && let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state + { + let hint_to_resolve = cached_hint.clone(); + let server_id = *server_id; + cached_hint.resolve_state = ResolveState::Resolving; + drop(guard); + cx.spawn_in(window, async move |editor, cx| { + let resolved_hint_task = editor.update(cx, |editor, cx| { + let buffer = editor.buffer().read(cx).buffer(buffer_id)?; + editor.semantics_provider.as_ref()?.resolve_inlay_hint( + hint_to_resolve, + buffer, + server_id, + cx, + ) + })?; + if let Some(resolved_hint_task) = resolved_hint_task { + let mut resolved_hint = + resolved_hint_task.await.context("hint resolve task")?; + editor.read_with(cx, |editor, _| { + if let Some(excerpt_hints) = + editor.inlay_hint_cache.hints.get(&excerpt_id) + { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) + && cached_hint.resolve_state == ResolveState::Resolving { - let mut guard = excerpt_hints.write(); - if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) { - if cached_hint.resolve_state == ResolveState::Resolving { - resolved_hint.resolve_state = ResolveState::Resolved; - *cached_hint = resolved_hint; - } - } + resolved_hint.resolve_state = ResolveState::Resolved; + *cached_hint = resolved_hint; } - })?; - } + } + })?; + } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } } @@ -990,8 +988,8 @@ fn fetch_and_update_hints( let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?; - if !editor.registered_buffers.contains_key(&query.buffer_id) { - if let Some(project) = editor.project.as_ref() { + if !editor.registered_buffers.contains_key(&query.buffer_id) + && let Some(project) = editor.project.as_ref() { project.update(cx, |project, cx| { editor.registered_buffers.insert( query.buffer_id, @@ -999,7 +997,6 @@ fn fetch_and_update_hints( ); }) } - } editor .semantics_provider @@ -1240,14 +1237,12 @@ fn apply_hint_update( .inlay_hint_cache .allowed_hint_kinds .contains(&new_hint.kind) - { - if let Some(new_hint_position) = + && let Some(new_hint_position) = multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position) - { - splice - .to_insert - .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); - } + { + splice + .to_insert + .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); } let new_id = InlayId::Hint(new_inlay_id); cached_excerpt_hints.hints_by_id.insert(new_id, new_hint); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 22430ab5e174a95f87db36e7d2002c5dfe2ce479..136b0b314d9d74ca04e4de778c6855963160330c 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -930,10 +930,10 @@ impl Item for Editor { })?; buffer .update(cx, |buffer, cx| { - if let Some(transaction) = transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0, cx); - } + if let Some(transaction) = transaction + && !buffer.is_singleton() + { + buffer.push_transaction(&transaction.0, cx); } }) .ok(); @@ -1374,36 +1374,33 @@ impl ProjectItem for Editor { let mut editor = Self::for_buffer(buffer.clone(), Some(project), window, cx); if let Some((excerpt_id, buffer_id, snapshot)) = editor.buffer().read(cx).snapshot(cx).as_singleton() + && WorkspaceSettings::get(None, cx).restore_on_file_reopen + && let Some(restoration_data) = Self::project_item_kind() + .and_then(|kind| pane.as_ref()?.project_item_restoration_data.get(&kind)) + .and_then(|data| data.downcast_ref::<EditorRestorationData>()) + .and_then(|data| { + let file = project::File::from_dyn(buffer.read(cx).file())?; + data.entries.get(&file.abs_path(cx)) + }) { - if WorkspaceSettings::get(None, cx).restore_on_file_reopen { - if let Some(restoration_data) = Self::project_item_kind() - .and_then(|kind| pane.as_ref()?.project_item_restoration_data.get(&kind)) - .and_then(|data| data.downcast_ref::<EditorRestorationData>()) - .and_then(|data| { - let file = project::File::from_dyn(buffer.read(cx).file())?; - data.entries.get(&file.abs_path(cx)) - }) - { - editor.fold_ranges( - clip_ranges(&restoration_data.folds, snapshot), - false, - window, - cx, - ); - if !restoration_data.selections.is_empty() { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(clip_ranges(&restoration_data.selections, snapshot)); - }); - } - let (top_row, offset) = restoration_data.scroll_position; - let anchor = Anchor::in_buffer( - *excerpt_id, - buffer_id, - snapshot.anchor_before(Point::new(top_row, 0)), - ); - editor.set_scroll_anchor(ScrollAnchor { anchor, offset }, window, cx); - } + editor.fold_ranges( + clip_ranges(&restoration_data.folds, snapshot), + false, + window, + cx, + ); + if !restoration_data.selections.is_empty() { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(clip_ranges(&restoration_data.selections, snapshot)); + }); } + let (top_row, offset) = restoration_data.scroll_position; + let anchor = Anchor::in_buffer( + *excerpt_id, + buffer_id, + snapshot.anchor_before(Point::new(top_row, 0)), + ); + editor.set_scroll_anchor(ScrollAnchor { anchor, offset }, window, cx); } editor diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index f358ab7b936c912f5a11fbe2ae6fc62a974ad56c..cae4b565b4b2b7dc1462e362d072bcff9c9cfd36 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -51,12 +51,11 @@ pub(crate) fn should_auto_close( continue; }; let mut jsx_open_tag_node = node; - if node.grammar_name() != config.open_tag_node_name { - if let Some(parent) = node.parent() { - if parent.grammar_name() == config.open_tag_node_name { - jsx_open_tag_node = parent; - } - } + if node.grammar_name() != config.open_tag_node_name + && let Some(parent) = node.parent() + && parent.grammar_name() == config.open_tag_node_name + { + jsx_open_tag_node = parent; } if jsx_open_tag_node.grammar_name() != config.open_tag_node_name { continue; @@ -284,10 +283,8 @@ pub(crate) fn generate_auto_close_edits( unclosed_open_tag_count -= 1; } } else if has_erroneous_close_tag && kind == erroneous_close_tag_node_name { - if tag_node_name_equals(&node, &tag_name) { - if !is_after_open_tag(&node) { - unclosed_open_tag_count -= 1; - } + if tag_node_name_equals(&node, &tag_name) && !is_after_open_tag(&node) { + unclosed_open_tag_count -= 1; } } else if kind == config.jsx_element_node_name { // perf: filter only open,close,element,erroneous nodes diff --git a/crates/editor/src/lsp_ext.rs b/crates/editor/src/lsp_ext.rs index d02fc0f901e928619e42d7ff9ef7fb34351fdc28..18ad2d71c835e5ec7e3bbd540de21f7e38425c39 100644 --- a/crates/editor/src/lsp_ext.rs +++ b/crates/editor/src/lsp_ext.rs @@ -147,16 +147,15 @@ pub fn lsp_tasks( }, cx, ) - }) { - if let Some(new_runnables) = runnables_task.await.log_err() { - new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map( - |(location, runnable)| { - let resolved_task = - runnable.resolve_task(&id_base, &lsp_buffer_context)?; - Some((location, resolved_task)) - }, - )); - } + }) && let Some(new_runnables) = runnables_task.await.log_err() + { + new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map( + |(location, runnable)| { + let resolved_task = + runnable.resolve_task(&id_base, &lsp_buffer_context)?; + Some((location, resolved_task)) + }, + )); } lsp_tasks .entry(source_kind) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 0bf875095b2f9e553f146096c377ce0b271a2ccf..7a008e3ba257173429448a02be7abe5ab00cedec 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -510,10 +510,10 @@ pub fn find_preceding_boundary_point( if find_range == FindRange::SingleLine && ch == '\n' { break; } - if let Some(prev_ch) = prev_ch { - if is_boundary(ch, prev_ch) { - break; - } + if let Some(prev_ch) = prev_ch + && is_boundary(ch, prev_ch) + { + break; } offset -= ch.len_utf8(); @@ -562,13 +562,13 @@ pub fn find_boundary_point( if find_range == FindRange::SingleLine && ch == '\n' { break; } - if let Some(prev_ch) = prev_ch { - if is_boundary(prev_ch, ch) { - if return_point_before_boundary { - return map.clip_point(prev_offset.to_display_point(map), Bias::Right); - } else { - break; - } + if let Some(prev_ch) = prev_ch + && is_boundary(prev_ch, ch) + { + if return_point_before_boundary { + return map.clip_point(prev_offset.to_display_point(map), Bias::Right); + } else { + break; } } prev_offset = offset; @@ -603,13 +603,13 @@ pub fn find_preceding_boundary_trail( // Find the boundary let start_offset = offset; for ch in forward { - if let Some(prev_ch) = prev_ch { - if is_boundary(prev_ch, ch) { - if start_offset == offset { - trail_offset = Some(offset); - } else { - break; - } + if let Some(prev_ch) = prev_ch + && is_boundary(prev_ch, ch) + { + if start_offset == offset { + trail_offset = Some(offset); + } else { + break; } } offset -= ch.len_utf8(); @@ -651,13 +651,13 @@ pub fn find_boundary_trail( // Find the boundary let start_offset = offset; for ch in forward { - if let Some(prev_ch) = prev_ch { - if is_boundary(prev_ch, ch) { - if start_offset == offset { - trail_offset = Some(offset); - } else { - break; - } + if let Some(prev_ch) = prev_ch + && is_boundary(prev_ch, ch) + { + if start_offset == offset { + trail_offset = Some(offset); + } else { + break; } } offset += ch.len_utf8(); diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index bee9464124496cb027355aaa3ac464a792479fc9..e3d83ab1609907834083f2c6a0be6640ce110f3e 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -285,11 +285,11 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu workspace.update(cx, |_workspace, cx| { // Check if the local document exists, otherwise fallback to the online document. // Open with the default browser. - if let Some(local_url) = docs_urls.local { - if fs::metadata(Path::new(&local_url[8..])).is_ok() { - cx.open_url(&local_url); - return; - } + if let Some(local_url) = docs_urls.local + && fs::metadata(Path::new(&local_url[8..])).is_ok() + { + cx.open_url(&local_url); + return; } if let Some(web_url) = docs_urls.web { diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 08ff23f8f70be4e512826c2793a2d95e2aee1690..b47f1cd711571d55f61012989c01234aa26609fb 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -703,20 +703,20 @@ impl Editor { if matches!( settings.defaults.soft_wrap, SoftWrap::PreferredLineLength | SoftWrap::Bounded - ) { - if (settings.defaults.preferred_line_length as f32) < visible_column_count { - visible_column_count = settings.defaults.preferred_line_length as f32; - } + ) && (settings.defaults.preferred_line_length as f32) < visible_column_count + { + visible_column_count = settings.defaults.preferred_line_length as f32; } // If the scroll position is currently at the left edge of the document // (x == 0.0) and the intent is to scroll right, the gutter's margin // should first be added to the current position, otherwise the cursor // will end at the column position minus the margin, which looks off. - if current_position.x == 0.0 && amount.columns(visible_column_count) > 0. { - if let Some(last_position_map) = &self.last_position_map { - current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance; - } + if current_position.x == 0.0 + && amount.columns(visible_column_count) > 0. + && let Some(last_position_map) = &self.last_position_map + { + current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance; } let new_position = current_position + point( @@ -749,12 +749,10 @@ impl Editor { if let (Some(visible_lines), Some(visible_columns)) = (self.visible_line_count(), self.visible_column_count()) + && newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) + && newest_head.column() <= screen_top.column() + visible_columns as u32 { - if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) - && newest_head.column() <= screen_top.column() + visible_columns as u32 - { - return Ordering::Equal; - } + return Ordering::Equal; } Ordering::Greater diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 88d3b52d764d15280c8ed03dd87f42b8c32d0911..057d622903ed12b4d996759cd93dc76f2ba9ee8d 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -116,12 +116,12 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut scroll_position = self.scroll_manager.scroll_position(&display_map); let original_y = scroll_position.y; - if let Some(last_bounds) = self.expect_bounds_change.take() { - if scroll_position.y != 0. { - scroll_position.y += (bounds.top() - last_bounds.top()) / line_height; - if scroll_position.y < 0. { - scroll_position.y = 0.; - } + if let Some(last_bounds) = self.expect_bounds_change.take() + && scroll_position.y != 0. + { + scroll_position.y += (bounds.top() - last_bounds.top()) / line_height; + if scroll_position.y < 0. { + scroll_position.y = 0.; } } if scroll_position.y > max_scroll_top { diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 819d6d9fed41cd02fe93922723e4096c2541c8b5..d388e8f3b79d704f5e023901ce59afd281131da2 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -184,10 +184,10 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo for (row, block) in blocks { match block { Block::Custom(custom_block) => { - if let BlockPlacement::Near(x) = &custom_block.placement { - if snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot)) { - continue; - } + if let BlockPlacement::Near(x) = &custom_block.placement + && snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot)) + { + continue; }; let content = block_content_for_tests(editor, custom_block.id, cx) .expect("block content not found"); diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 53c911393428c86b893d488d344d5299399d8998..809b530ed77257930cd4d6cb1c17720655529f2a 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -167,15 +167,14 @@ fn main() { continue; } - if let Some(language) = meta.language_server { - if !languages.contains(&language.file_extension) { + if let Some(language) = meta.language_server + && !languages.contains(&language.file_extension) { panic!( "Eval for {:?} could not be run because no language server was found for extension {:?}", meta.name, language.file_extension ); } - } // TODO: This creates a worktree per repetition. Ideally these examples should // either be run sequentially on the same worktree, or reuse worktrees when there diff --git a/crates/eval/src/explorer.rs b/crates/eval/src/explorer.rs index ee1dfa95c3840af42bdd134be1110bd2483c97aa..3326070cea4e860210f8ba7e0038fec2f3404c30 100644 --- a/crates/eval/src/explorer.rs +++ b/crates/eval/src/explorer.rs @@ -46,27 +46,25 @@ fn find_target_files_recursive( max_depth, found_files, )?; - } else if path.is_file() { - if let Some(filename_osstr) = path.file_name() { - if let Some(filename_str) = filename_osstr.to_str() { - if filename_str == target_filename { - found_files.push(path); - } - } - } + } else if path.is_file() + && let Some(filename_osstr) = path.file_name() + && let Some(filename_str) = filename_osstr.to_str() + && filename_str == target_filename + { + found_files.push(path); } } Ok(()) } pub fn generate_explorer_html(input_paths: &[PathBuf], output_path: &PathBuf) -> Result<String> { - if let Some(parent) = output_path.parent() { - if !parent.exists() { - fs::create_dir_all(parent).context(format!( - "Failed to create output directory: {}", - parent.display() - ))?; - } + if let Some(parent) = output_path.parent() + && !parent.exists() + { + fs::create_dir_all(parent).context(format!( + "Failed to create output directory: {}", + parent.display() + ))?; } let template_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/explorer.html"); diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index e3b67ed3557c335b8c4d26d9aca7b02011528460..dd9b4f8bba6c466b9f750e97dfc7cb261d2c8226 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -376,11 +376,10 @@ impl ExampleInstance { ); let result = this.thread.conversation(&mut example_cx).await; - if let Err(err) = result { - if !err.is::<FailedAssertion>() { + if let Err(err) = result + && !err.is::<FailedAssertion>() { return Err(err); } - } println!("{}Stopped", this.log_prefix); diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs index 35f7f419383cb9f3c6cc518663ad818735eab80e..6af793253bce2d122a5361f6b83f33cb39d45253 100644 --- a/crates/extension/src/extension.rs +++ b/crates/extension/src/extension.rs @@ -178,16 +178,15 @@ pub fn parse_wasm_extension_version( for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) { if let wasmparser::Payload::CustomSection(s) = part.context("error parsing wasm extension")? + && s.name() == "zed:api-version" { - if s.name() == "zed:api-version" { - version = parse_wasm_extension_version_custom_section(s.data()); - if version.is_none() { - bail!( - "extension {} has invalid zed:api-version section: {:?}", - extension_id, - s.data() - ); - } + version = parse_wasm_extension_version_custom_section(s.data()); + if version.is_none() { + bail!( + "extension {} has invalid zed:api-version section: {:?}", + extension_id, + s.data() + ); } } } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 4ee948dda870b8d29757c77294ec7e670a386918..01edb5c033b9f9f35a93aefb9cca5c79453bf5b1 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -93,10 +93,9 @@ pub fn is_version_compatible( .wasm_api_version .as_ref() .and_then(|wasm_api_version| SemanticVersion::from_str(wasm_api_version).ok()) + && !is_supported_wasm_api_version(release_channel, wasm_api_version) { - if !is_supported_wasm_api_version(release_channel, wasm_api_version) { - return false; - } + return false; } true @@ -292,19 +291,17 @@ impl ExtensionStore { // it must be asynchronously rebuilt. let mut extension_index = ExtensionIndex::default(); let mut extension_index_needs_rebuild = true; - if let Ok(index_content) = index_content { - if let Some(index) = serde_json::from_str(&index_content).log_err() { - extension_index = index; - if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) = - (index_metadata, extensions_metadata) - { - if index_metadata - .mtime - .bad_is_greater_than(extensions_metadata.mtime) - { - extension_index_needs_rebuild = false; - } - } + if let Ok(index_content) = index_content + && let Some(index) = serde_json::from_str(&index_content).log_err() + { + extension_index = index; + if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) = + (index_metadata, extensions_metadata) + && index_metadata + .mtime + .bad_is_greater_than(extensions_metadata.mtime) + { + extension_index_needs_rebuild = false; } } @@ -392,10 +389,9 @@ impl ExtensionStore { if let Some(path::Component::Normal(extension_dir_name)) = event_path.components().next() + && let Some(extension_id) = extension_dir_name.to_str() { - if let Some(extension_id) = extension_dir_name.to_str() { - reload_tx.unbounded_send(Some(extension_id.into())).ok(); - } + reload_tx.unbounded_send(Some(extension_id.into())).ok(); } } } @@ -763,8 +759,8 @@ impl ExtensionStore { if let ExtensionOperation::Install = operation { this.update( cx, |this, cx| { cx.emit(Event::ExtensionInstalled(extension_id.clone())); - if let Some(events) = ExtensionEvents::try_global(cx) { - if let Some(manifest) = this.extension_manifest_for_id(&extension_id) { + if let Some(events) = ExtensionEvents::try_global(cx) + && let Some(manifest) = this.extension_manifest_for_id(&extension_id) { events.update(cx, |this, cx| { this.emit( extension::Event::ExtensionInstalled(manifest.clone()), @@ -772,7 +768,6 @@ impl ExtensionStore { ) }); } - } }) .ok(); } @@ -912,12 +907,12 @@ impl ExtensionStore { extension_store.update(cx, |_, cx| { cx.emit(Event::ExtensionUninstalled(extension_id.clone())); - if let Some(events) = ExtensionEvents::try_global(cx) { - if let Some(manifest) = extension_manifest { - events.update(cx, |this, cx| { - this.emit(extension::Event::ExtensionUninstalled(manifest.clone()), cx) - }); - } + if let Some(events) = ExtensionEvents::try_global(cx) + && let Some(manifest) = extension_manifest + { + events.update(cx, |this, cx| { + this.emit(extension::Event::ExtensionUninstalled(manifest.clone()), cx) + }); } })?; @@ -997,12 +992,12 @@ impl ExtensionStore { this.update(cx, |this, cx| this.reload(None, cx))?.await; this.update(cx, |this, cx| { cx.emit(Event::ExtensionInstalled(extension_id.clone())); - if let Some(events) = ExtensionEvents::try_global(cx) { - if let Some(manifest) = this.extension_manifest_for_id(&extension_id) { - events.update(cx, |this, cx| { - this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx) - }); - } + if let Some(events) = ExtensionEvents::try_global(cx) + && let Some(manifest) = this.extension_manifest_for_id(&extension_id) + { + events.update(cx, |this, cx| { + this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx) + }); } })?; @@ -1788,10 +1783,10 @@ impl ExtensionStore { let connection_options = client.read(cx).connection_options(); let ssh_url = connection_options.ssh_url(); - if let Some(existing_client) = self.ssh_clients.get(&ssh_url) { - if existing_client.upgrade().is_some() { - return; - } + if let Some(existing_client) = self.ssh_clients.get(&ssh_url) + && existing_client.upgrade().is_some() + { + return; } self.ssh_clients.insert(ssh_url, client.downgrade()); diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index d990b670f49221aca2f0af901293c70d341cf029..4fe27aedc9f197719c5be2add091214c056066d8 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -701,16 +701,15 @@ pub fn parse_wasm_extension_version( for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) { if let wasmparser::Payload::CustomSection(s) = part.context("error parsing wasm extension")? + && s.name() == "zed:api-version" { - if s.name() == "zed:api-version" { - version = parse_wasm_extension_version_custom_section(s.data()); - if version.is_none() { - bail!( - "extension {} has invalid zed:api-version section: {:?}", - extension_id, - s.data() - ); - } + version = parse_wasm_extension_version_custom_section(s.data()); + if version.is_none() { + bail!( + "extension {} has invalid zed:api-version section: {:?}", + extension_id, + s.data() + ); } } } diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 7c7f9e68365dc45445f085ded87e693a83c1f033..7f0e8171f6e5f5d32e87f08e9202491b7fc15b8d 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -1031,15 +1031,14 @@ impl ExtensionsPage { .read(cx) .extension_manifest_for_id(&extension_id) .cloned() + && let Some(events) = extension::ExtensionEvents::try_global(cx) { - if let Some(events) = extension::ExtensionEvents::try_global(cx) { - events.update(cx, |this, cx| { - this.emit( - extension::Event::ConfigureExtensionRequested(manifest), - cx, - ) - }); - } + events.update(cx, |this, cx| { + this.emit( + extension::Event::ConfigureExtensionRequested(manifest), + cx, + ) + }); } } }) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index e8f80e5ef2092bb0f9b213a53f82638fe59b928d..aebc262af05675b4a8687d4af6a1b82712001c4d 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -209,11 +209,11 @@ impl FileFinder { let Some(init_modifiers) = self.init_modifiers.take() else { return; }; - if self.picker.read(cx).delegate.has_changed_selected_index { - if !event.modified() || !init_modifiers.is_subset_of(event) { - self.init_modifiers = None; - window.dispatch_action(menu::Confirm.boxed_clone(), cx); - } + if self.picker.read(cx).delegate.has_changed_selected_index + && (!event.modified() || !init_modifiers.is_subset_of(event)) + { + self.init_modifiers = None; + window.dispatch_action(menu::Confirm.boxed_clone(), cx); } } @@ -323,27 +323,27 @@ impl FileFinder { ) { self.picker.update(cx, |picker, cx| { let delegate = &mut picker.delegate; - if let Some(workspace) = delegate.workspace.upgrade() { - if let Some(m) = delegate.matches.get(delegate.selected_index()) { - let path = match &m { - Match::History { path, .. } => { - let worktree_id = path.project.worktree_id; - ProjectPath { - worktree_id, - path: Arc::clone(&path.project.path), - } + if let Some(workspace) = delegate.workspace.upgrade() + && let Some(m) = delegate.matches.get(delegate.selected_index()) + { + let path = match &m { + Match::History { path, .. } => { + let worktree_id = path.project.worktree_id; + ProjectPath { + worktree_id, + path: Arc::clone(&path.project.path), } - Match::Search(m) => ProjectPath { - worktree_id: WorktreeId::from_usize(m.0.worktree_id), - path: m.0.path.clone(), - }, - Match::CreateNew(p) => p.clone(), - }; - let open_task = workspace.update(cx, move |workspace, cx| { - workspace.split_path_preview(path, false, Some(split_direction), window, cx) - }); - open_task.detach_and_log_err(cx); - } + } + Match::Search(m) => ProjectPath { + worktree_id: WorktreeId::from_usize(m.0.worktree_id), + path: m.0.path.clone(), + }, + Match::CreateNew(p) => p.clone(), + }; + let open_task = workspace.update(cx, move |workspace, cx| { + workspace.split_path_preview(path, false, Some(split_direction), window, cx) + }); + open_task.detach_and_log_err(cx); } }) } @@ -675,17 +675,17 @@ impl Matches { let path_str = panel_match.0.path.to_string_lossy(); let filename_str = filename.to_string_lossy(); - if let Some(filename_pos) = path_str.rfind(&*filename_str) { - if panel_match.0.positions[0] >= filename_pos { - let mut prev_position = panel_match.0.positions[0]; - for p in &panel_match.0.positions[1..] { - if *p != prev_position + 1 { - return false; - } - prev_position = *p; + if let Some(filename_pos) = path_str.rfind(&*filename_str) + && panel_match.0.positions[0] >= filename_pos + { + let mut prev_position = panel_match.0.positions[0]; + for p in &panel_match.0.positions[1..] { + if *p != prev_position + 1 { + return false; } - return true; + prev_position = *p; } + return true; } } @@ -1045,10 +1045,10 @@ impl FileFinderDelegate { ) } else { let mut path = Arc::clone(project_relative_path); - if project_relative_path.as_ref() == Path::new("") { - if let Some(absolute_path) = &entry_path.absolute { - path = Arc::from(absolute_path.as_path()); - } + if project_relative_path.as_ref() == Path::new("") + && let Some(absolute_path) = &entry_path.absolute + { + path = Arc::from(absolute_path.as_path()); } let mut path_match = PathMatch { @@ -1078,23 +1078,21 @@ impl FileFinderDelegate { ), }; - if file_name_positions.is_empty() { - if let Some(user_home_path) = std::env::var("HOME").ok() { - let user_home_path = user_home_path.trim(); - if !user_home_path.is_empty() { - if full_path.starts_with(user_home_path) { - full_path.replace_range(0..user_home_path.len(), "~"); - full_path_positions.retain_mut(|pos| { - if *pos >= user_home_path.len() { - *pos -= user_home_path.len(); - *pos += 1; - true - } else { - false - } - }) + if file_name_positions.is_empty() + && let Some(user_home_path) = std::env::var("HOME").ok() + { + let user_home_path = user_home_path.trim(); + if !user_home_path.is_empty() && full_path.starts_with(user_home_path) { + full_path.replace_range(0..user_home_path.len(), "~"); + full_path_positions.retain_mut(|pos| { + if *pos >= user_home_path.len() { + *pos -= user_home_path.len(); + *pos += 1; + true + } else { + false } - } + }) } } @@ -1242,14 +1240,13 @@ impl FileFinderDelegate { /// Skips first history match (that is displayed topmost) if it's currently opened. fn calculate_selected_index(&self, cx: &mut Context<Picker<Self>>) -> usize { - if FileFinderSettings::get_global(cx).skip_focus_for_active_in_search { - if let Some(Match::History { path, .. }) = self.matches.get(0) { - if Some(path) == self.currently_opened_path.as_ref() { - let elements_after_first = self.matches.len() - 1; - if elements_after_first > 0 { - return 1; - } - } + if FileFinderSettings::get_global(cx).skip_focus_for_active_in_search + && let Some(Match::History { path, .. }) = self.matches.get(0) + && Some(path) == self.currently_opened_path.as_ref() + { + let elements_after_first = self.matches.len() - 1; + if elements_after_first > 0 { + return 1; } } @@ -1310,10 +1307,10 @@ impl PickerDelegate for FileFinderDelegate { .enumerate() .find(|(_, m)| !matches!(m, Match::History { .. })) .map(|(i, _)| i); - if let Some(first_non_history_index) = first_non_history_index { - if first_non_history_index > 0 { - return vec![first_non_history_index - 1]; - } + if let Some(first_non_history_index) = first_non_history_index + && first_non_history_index > 0 + { + return vec![first_non_history_index - 1]; } } Vec::new() @@ -1436,69 +1433,101 @@ impl PickerDelegate for FileFinderDelegate { window: &mut Window, cx: &mut Context<Picker<FileFinderDelegate>>, ) { - if let Some(m) = self.matches.get(self.selected_index()) { - if let Some(workspace) = self.workspace.upgrade() { - let open_task = workspace.update(cx, |workspace, cx| { - let split_or_open = - |workspace: &mut Workspace, - project_path, - window: &mut Window, - cx: &mut Context<Workspace>| { - let allow_preview = - PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder; - if secondary { - workspace.split_path_preview( - project_path, - allow_preview, - None, - window, - cx, - ) - } else { - workspace.open_path_preview( - project_path, - None, - true, - allow_preview, - true, - window, - cx, - ) - } - }; - match &m { - Match::CreateNew(project_path) => { - // Create a new file with the given filename - if secondary { - workspace.split_path_preview( - project_path.clone(), - false, - None, - window, - cx, - ) - } else { - workspace.open_path_preview( - project_path.clone(), - None, - true, - false, - true, - window, - cx, - ) - } + if let Some(m) = self.matches.get(self.selected_index()) + && let Some(workspace) = self.workspace.upgrade() + { + let open_task = workspace.update(cx, |workspace, cx| { + let split_or_open = + |workspace: &mut Workspace, + project_path, + window: &mut Window, + cx: &mut Context<Workspace>| { + let allow_preview = + PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder; + if secondary { + workspace.split_path_preview( + project_path, + allow_preview, + None, + window, + cx, + ) + } else { + workspace.open_path_preview( + project_path, + None, + true, + allow_preview, + true, + window, + cx, + ) } + }; + match &m { + Match::CreateNew(project_path) => { + // Create a new file with the given filename + if secondary { + workspace.split_path_preview( + project_path.clone(), + false, + None, + window, + cx, + ) + } else { + workspace.open_path_preview( + project_path.clone(), + None, + true, + false, + true, + window, + cx, + ) + } + } - Match::History { path, .. } => { - let worktree_id = path.project.worktree_id; - if workspace - .project() - .read(cx) - .worktree_for_id(worktree_id, cx) - .is_some() - { - split_or_open( + Match::History { path, .. } => { + let worktree_id = path.project.worktree_id; + if workspace + .project() + .read(cx) + .worktree_for_id(worktree_id, cx) + .is_some() + { + split_or_open( + workspace, + ProjectPath { + worktree_id, + path: Arc::clone(&path.project.path), + }, + window, + cx, + ) + } else { + match path.absolute.as_ref() { + Some(abs_path) => { + if secondary { + workspace.split_abs_path( + abs_path.to_path_buf(), + false, + window, + cx, + ) + } else { + workspace.open_abs_path( + abs_path.to_path_buf(), + OpenOptions { + visible: Some(OpenVisible::None), + ..Default::default() + }, + window, + cx, + ) + } + } + None => split_or_open( workspace, ProjectPath { worktree_id, @@ -1506,88 +1535,52 @@ impl PickerDelegate for FileFinderDelegate { }, window, cx, - ) - } else { - match path.absolute.as_ref() { - Some(abs_path) => { - if secondary { - workspace.split_abs_path( - abs_path.to_path_buf(), - false, - window, - cx, - ) - } else { - workspace.open_abs_path( - abs_path.to_path_buf(), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ) - } - } - None => split_or_open( - workspace, - ProjectPath { - worktree_id, - path: Arc::clone(&path.project.path), - }, - window, - cx, - ), - } + ), } } - Match::Search(m) => split_or_open( - workspace, - ProjectPath { - worktree_id: WorktreeId::from_usize(m.0.worktree_id), - path: m.0.path.clone(), - }, - window, - cx, - ), } - }); + Match::Search(m) => split_or_open( + workspace, + ProjectPath { + worktree_id: WorktreeId::from_usize(m.0.worktree_id), + path: m.0.path.clone(), + }, + window, + cx, + ), + } + }); - let row = self - .latest_search_query - .as_ref() - .and_then(|query| query.path_position.row) - .map(|row| row.saturating_sub(1)); - let col = self - .latest_search_query - .as_ref() - .and_then(|query| query.path_position.column) - .unwrap_or(0) - .saturating_sub(1); - let finder = self.file_finder.clone(); - - cx.spawn_in(window, async move |_, cx| { - let item = open_task.await.notify_async_err(cx)?; - if let Some(row) = row { - if let Some(active_editor) = item.downcast::<Editor>() { - active_editor - .downgrade() - .update_in(cx, |editor, window, cx| { - editor.go_to_singleton_buffer_point( - Point::new(row, col), - window, - cx, - ); - }) - .log_err(); - } - } - finder.update(cx, |_, cx| cx.emit(DismissEvent)).ok()?; + let row = self + .latest_search_query + .as_ref() + .and_then(|query| query.path_position.row) + .map(|row| row.saturating_sub(1)); + let col = self + .latest_search_query + .as_ref() + .and_then(|query| query.path_position.column) + .unwrap_or(0) + .saturating_sub(1); + let finder = self.file_finder.clone(); + + cx.spawn_in(window, async move |_, cx| { + let item = open_task.await.notify_async_err(cx)?; + if let Some(row) = row + && let Some(active_editor) = item.downcast::<Editor>() + { + active_editor + .downgrade() + .update_in(cx, |editor, window, cx| { + editor.go_to_singleton_buffer_point(Point::new(row, col), window, cx); + }) + .log_err(); + } + finder.update(cx, |_, cx| cx.emit(DismissEvent)).ok()?; - Some(()) - }) - .detach(); - } + Some(()) + }) + .detach(); } } diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 7235568e4f8dcb1f462e2d151705bcc2998e8d6b..3a99afc8cbc0b1ecae384c9184fabb548318187a 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -75,16 +75,16 @@ impl OpenPathDelegate { .. } => { let mut i = selected_match_index; - if let Some(user_input) = user_input { - if !user_input.exists || !user_input.is_dir { - if i == 0 { - return Some(CandidateInfo { - path: user_input.file.clone(), - is_dir: false, - }); - } else { - i -= 1; - } + if let Some(user_input) = user_input + && (!user_input.exists || !user_input.is_dir) + { + if i == 0 { + return Some(CandidateInfo { + path: user_input.file.clone(), + is_dir: false, + }); + } else { + i -= 1; } } let id = self.string_matches.get(i)?.candidate_id; diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 64eeae99d1097eecb0a60730d657b69499005302..847e98d6c4ef090b5905a4fd06ddaf16359eb12f 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -420,18 +420,19 @@ impl Fs for RealFs { async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> { #[cfg(windows)] - if let Ok(Some(metadata)) = self.metadata(path).await { - if metadata.is_symlink && metadata.is_dir { - self.remove_dir( - path, - RemoveOptions { - recursive: false, - ignore_if_not_exists: true, - }, - ) - .await?; - return Ok(()); - } + if let Ok(Some(metadata)) = self.metadata(path).await + && metadata.is_symlink + && metadata.is_dir + { + self.remove_dir( + path, + RemoveOptions { + recursive: false, + ignore_if_not_exists: true, + }, + ) + .await?; + return Ok(()); } match smol::fs::remove_file(path).await { @@ -467,11 +468,11 @@ impl Fs for RealFs { #[cfg(any(target_os = "linux", target_os = "freebsd"))] async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> { - if let Ok(Some(metadata)) = self.metadata(path).await { - if metadata.is_symlink { - // TODO: trash_file does not support trashing symlinks yet - https://github.com/bilelmoussaoui/ashpd/issues/255 - return self.remove_file(path, RemoveOptions::default()).await; - } + if let Ok(Some(metadata)) = self.metadata(path).await + && metadata.is_symlink + { + // TODO: trash_file does not support trashing symlinks yet - https://github.com/bilelmoussaoui/ashpd/issues/255 + return self.remove_file(path, RemoveOptions::default()).await; } let file = smol::fs::File::open(path).await?; match trash::trash_file(&file.as_fd()).await { @@ -766,24 +767,23 @@ impl Fs for RealFs { let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default(); let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone())); - if watcher.add(path).is_err() { - // If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created. - if let Some(parent) = path.parent() { - if let Err(e) = watcher.add(parent) { - log::warn!("Failed to watch: {e}"); - } - } + // If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created. + if watcher.add(path).is_err() + && let Some(parent) = path.parent() + && let Err(e) = watcher.add(parent) + { + log::warn!("Failed to watch: {e}"); } // Check if path is a symlink and follow the target parent if let Some(mut target) = self.read_link(path).await.ok() { // Check if symlink target is relative path, if so make it absolute - if target.is_relative() { - if let Some(parent) = path.parent() { - target = parent.join(target); - if let Ok(canonical) = self.canonicalize(&target).await { - target = SanitizedPath::from(canonical).as_path().to_path_buf(); - } + if target.is_relative() + && let Some(parent) = path.parent() + { + target = parent.join(target); + if let Ok(canonical) = self.canonicalize(&target).await { + target = SanitizedPath::from(canonical).as_path().to_path_buf(); } } watcher.add(&target).ok(); @@ -1068,13 +1068,13 @@ impl FakeFsState { let current_entry = *entry_stack.last()?; if let FakeFsEntry::Dir { entries, .. } = current_entry { let entry = entries.get(name.to_str().unwrap())?; - if path_components.peek().is_some() || follow_symlink { - if let FakeFsEntry::Symlink { target, .. } = entry { - let mut target = target.clone(); - target.extend(path_components); - path = target; - continue 'outer; - } + if (path_components.peek().is_some() || follow_symlink) + && let FakeFsEntry::Symlink { target, .. } = entry + { + let mut target = target.clone(); + target.extend(path_components); + path = target; + continue 'outer; } entry_stack.push(entry); canonical_path = canonical_path.join(name); @@ -1566,10 +1566,10 @@ impl FakeFs { pub fn insert_branches(&self, dot_git: &Path, branches: &[&str]) { self.with_git_state(dot_git, true, |state| { - if let Some(first) = branches.first() { - if state.current_branch_name.is_none() { - state.current_branch_name = Some(first.to_string()) - } + if let Some(first) = branches.first() + && state.current_branch_name.is_none() + { + state.current_branch_name = Some(first.to_string()) } state .branches diff --git a/crates/fs/src/mac_watcher.rs b/crates/fs/src/mac_watcher.rs index aa75ad31d9beadada32b62ed4d21a612631d31c3..7bd176639f1dccef2da4c4ae8dcb317d0be602cb 100644 --- a/crates/fs/src/mac_watcher.rs +++ b/crates/fs/src/mac_watcher.rs @@ -41,10 +41,9 @@ impl Watcher for MacWatcher { if let Some((watched_path, _)) = handles .range::<Path, _>((Bound::Unbounded, Bound::Included(path))) .next_back() + && path.starts_with(watched_path) { - if path.starts_with(watched_path) { - return Ok(()); - } + return Ok(()); } let (stream, handle) = EventStream::new(&[path], self.latency); diff --git a/crates/fsevent/src/fsevent.rs b/crates/fsevent/src/fsevent.rs index 81ca0a4114253fc38b5d120d1c37dfc9233f7fd1..c97ab5f35d1b1e8463e895da7a309dc7ef3be998 100644 --- a/crates/fsevent/src/fsevent.rs +++ b/crates/fsevent/src/fsevent.rs @@ -178,40 +178,39 @@ impl EventStream { flags.contains(StreamFlags::USER_DROPPED) || flags.contains(StreamFlags::KERNEL_DROPPED) }) + && let Some(last_valid_event_id) = state.last_valid_event_id.take() { - if let Some(last_valid_event_id) = state.last_valid_event_id.take() { - fs::FSEventStreamStop(state.stream); - fs::FSEventStreamInvalidate(state.stream); - fs::FSEventStreamRelease(state.stream); - - let stream_context = fs::FSEventStreamContext { - version: 0, - info, - retain: None, - release: None, - copy_description: None, - }; - let stream = fs::FSEventStreamCreate( - cf::kCFAllocatorDefault, - Self::trampoline, - &stream_context, - state.paths, - last_valid_event_id, - state.latency.as_secs_f64(), - fs::kFSEventStreamCreateFlagFileEvents - | fs::kFSEventStreamCreateFlagNoDefer - | fs::kFSEventStreamCreateFlagWatchRoot, - ); - - state.stream = stream; - fs::FSEventStreamScheduleWithRunLoop( - state.stream, - cf::CFRunLoopGetCurrent(), - cf::kCFRunLoopDefaultMode, - ); - fs::FSEventStreamStart(state.stream); - stream_restarted = true; - } + fs::FSEventStreamStop(state.stream); + fs::FSEventStreamInvalidate(state.stream); + fs::FSEventStreamRelease(state.stream); + + let stream_context = fs::FSEventStreamContext { + version: 0, + info, + retain: None, + release: None, + copy_description: None, + }; + let stream = fs::FSEventStreamCreate( + cf::kCFAllocatorDefault, + Self::trampoline, + &stream_context, + state.paths, + last_valid_event_id, + state.latency.as_secs_f64(), + fs::kFSEventStreamCreateFlagFileEvents + | fs::kFSEventStreamCreateFlagNoDefer + | fs::kFSEventStreamCreateFlagWatchRoot, + ); + + state.stream = stream; + fs::FSEventStreamScheduleWithRunLoop( + state.stream, + cf::CFRunLoopGetCurrent(), + cf::kCFRunLoopDefaultMode, + ); + fs::FSEventStreamStart(state.stream); + stream_restarted = true; } if !stream_restarted { diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index 6f12681ea08956b53d9ce298593ce08f0e2a74a9..24b2c44218120b1237fb42e04edc9b6784356c57 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -289,14 +289,12 @@ fn parse_git_blame(output: &str) -> Result<Vec<BlameEntry>> { } }; - if done { - if let Some(entry) = current_entry.take() { - index.insert(entry.sha, entries.len()); + if done && let Some(entry) = current_entry.take() { + index.insert(entry.sha, entries.len()); - // We only want annotations that have a commit. - if !entry.sha.is_zero() { - entries.push(entry); - } + // We only want annotations that have a commit. + if !entry.sha.is_zero() { + entries.push(entry); } } } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index ae8c5f849cba1c4c8ffe6a9bf56b3b6328e13171..c30b789d9ff9957bfb00d57bab45da27c1e0a433 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -1447,12 +1447,11 @@ impl GitRepository for RealGitRepository { let mut remote_branches = vec![]; let mut add_if_matching = async |remote_head: &str| { - if let Ok(merge_base) = git_cmd(&["merge-base", &head, remote_head]).await { - if merge_base.trim() == head { - if let Some(s) = remote_head.strip_prefix("refs/remotes/") { - remote_branches.push(s.to_owned().into()); - } - } + if let Ok(merge_base) = git_cmd(&["merge-base", &head, remote_head]).await + && merge_base.trim() == head + && let Some(s) = remote_head.strip_prefix("refs/remotes/") + { + remote_branches.push(s.to_owned().into()); } }; @@ -1574,10 +1573,9 @@ impl GitRepository for RealGitRepository { Err(error) => { if let Some(GitBinaryCommandError { status, .. }) = error.downcast_ref::<GitBinaryCommandError>() + && status.code() == Some(1) { - if status.code() == Some(1) { - return Ok(false); - } + return Ok(false); } Err(error) diff --git a/crates/git_hosting_providers/src/git_hosting_providers.rs b/crates/git_hosting_providers/src/git_hosting_providers.rs index d4b3a59375f42db4a21c811ca6c4f94c912a4b3b..1d88c47f2e26fc9ad4e27b1e36351198c4365caf 100644 --- a/crates/git_hosting_providers/src/git_hosting_providers.rs +++ b/crates/git_hosting_providers/src/git_hosting_providers.rs @@ -49,10 +49,10 @@ pub fn register_additional_providers( pub fn get_host_from_git_remote_url(remote_url: &str) -> Result<String> { maybe!({ - if let Some(remote_url) = remote_url.strip_prefix("git@") { - if let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':') { - return Some(host.to_string()); - } + if let Some(remote_url) = remote_url.strip_prefix("git@") + && let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':') + { + return Some(host.to_string()); } Url::parse(remote_url) diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 5e7430ebc693458e6df9a41513138ae993b9097c..4303f53275d5c402d5ff2663629ab8a772052e64 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -135,11 +135,10 @@ impl CommitModal { .as_ref() .and_then(|repo| repo.read(cx).head_commit.as_ref()) .is_some() + && !git_panel.amend_pending() { - if !git_panel.amend_pending() { - git_panel.set_amend_pending(true, cx); - git_panel.load_last_commit_message_if_empty(cx); - } + git_panel.set_amend_pending(true, cx); + git_panel.load_last_commit_message_if_empty(cx); } } ForceMode::Commit => { @@ -195,12 +194,12 @@ impl CommitModal { let commit_message = commit_editor.read(cx).text(cx); - if let Some(suggested_commit_message) = suggested_commit_message { - if commit_message.is_empty() { - commit_editor.update(cx, |editor, cx| { - editor.set_placeholder_text(suggested_commit_message, cx); - }); - } + if let Some(suggested_commit_message) = suggested_commit_message + && commit_message.is_empty() + { + commit_editor.update(cx, |editor, cx| { + editor.set_placeholder_text(suggested_commit_message, cx); + }); } let focus_handle = commit_editor.focus_handle(cx); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index b1bdcdc3e0b8895426354ea9b50e04faef462dcb..82870b4e756eea76f269006ce35191d6958cb97e 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -926,19 +926,17 @@ impl GitPanel { let workspace = self.workspace.upgrade()?; let git_repo = self.active_repository.as_ref()?; - if let Some(project_diff) = workspace.read(cx).active_item_as::<ProjectDiff>(cx) { - if let Some(project_path) = project_diff.read(cx).active_path(cx) { - if Some(&entry.repo_path) - == git_repo - .read(cx) - .project_path_to_repo_path(&project_path, cx) - .as_ref() - { - project_diff.focus_handle(cx).focus(window); - project_diff.update(cx, |project_diff, cx| project_diff.autoscroll(cx)); - return None; - } - } + if let Some(project_diff) = workspace.read(cx).active_item_as::<ProjectDiff>(cx) + && let Some(project_path) = project_diff.read(cx).active_path(cx) + && Some(&entry.repo_path) + == git_repo + .read(cx) + .project_path_to_repo_path(&project_path, cx) + .as_ref() + { + project_diff.focus_handle(cx).focus(window); + project_diff.update(cx, |project_diff, cx| project_diff.autoscroll(cx)); + return None; }; self.workspace @@ -2514,10 +2512,11 @@ impl GitPanel { new_co_authors.push((name.clone(), email.clone())) } } - if !project.is_local() && !project.is_read_only(cx) { - if let Some(local_committer) = self.local_committer(room, cx) { - new_co_authors.push(local_committer); - } + if !project.is_local() + && !project.is_read_only(cx) + && let Some(local_committer) = self.local_committer(room, cx) + { + new_co_authors.push(local_committer); } new_co_authors } @@ -2758,14 +2757,13 @@ impl GitPanel { pending_staged_count += pending.entries.len(); last_pending_staged = pending.entries.first().cloned(); } - if let Some(single_staged) = &single_staged_entry { - if pending + if let Some(single_staged) = &single_staged_entry + && pending .entries .iter() .any(|entry| entry.repo_path == single_staged.repo_path) - { - pending_status_for_single_staged = Some(pending.target_status); - } + { + pending_status_for_single_staged = Some(pending.target_status); } } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 09c5ce11523aee66bd9d09e8f23ca5494ca1979c..3c0898fabfe3832cc548beffab61b87c85c87b80 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -363,10 +363,10 @@ impl ProjectDiff { } _ => {} } - if editor.focus_handle(cx).contains_focused(window, cx) { - if self.multibuffer.read(cx).is_empty() { - self.focus_handle.focus(window) - } + if editor.focus_handle(cx).contains_focused(window, cx) + && self.multibuffer.read(cx).is_empty() + { + self.focus_handle.focus(window) } } diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index af92621378bdd1635af147d845ab809fe3326828..9d918048fafc6c4d2abdc4979e873affb54b94ff 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -95,10 +95,8 @@ impl CursorPosition { .ok() .unwrap_or(true); - if !is_singleton { - if let Some(debounce) = debounce { - cx.background_executor().timer(debounce).await; - } + if !is_singleton && let Some(debounce) = debounce { + cx.background_executor().timer(debounce).await; } editor @@ -234,13 +232,11 @@ impl Render for CursorPosition { if let Some(editor) = workspace .active_item(cx) .and_then(|item| item.act_as::<Editor>(cx)) + && let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx) { - if let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx) - { - workspace.toggle_modal(window, cx, |window, cx| { - crate::GoToLine::new(editor, buffer, window, cx) - }) - } + workspace.toggle_modal(window, cx, |window, cx| { + crate::GoToLine::new(editor, buffer, window, cx) + }) } }); } diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 1ac933e316bcde24384139c851a8bedb63388611..908e61cac73849601dbca6338e585b96617ace9e 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -103,11 +103,11 @@ impl GoToLine { return; }; editor.update(cx, |editor, cx| { - if let Some(placeholder_text) = editor.placeholder_text() { - if editor.text(cx).is_empty() { - let placeholder_text = placeholder_text.to_string(); - editor.set_text(placeholder_text, window, cx); - } + if let Some(placeholder_text) = editor.placeholder_text() + && editor.text(cx).is_empty() + { + let placeholder_text = placeholder_text.to_string(); + editor.set_text(placeholder_text, window, cx); } }); } diff --git a/crates/google_ai/src/google_ai.rs b/crates/google_ai/src/google_ai.rs index dfa51d024c46c2acc15744cd366c2fd723d59046..95a6daa1d93669d7793ea1296ecb1fe872882262 100644 --- a/crates/google_ai/src/google_ai.rs +++ b/crates/google_ai/src/google_ai.rs @@ -106,10 +106,9 @@ pub fn validate_generate_content_request(request: &GenerateContentRequest) -> Re .contents .iter() .find(|content| content.role == Role::User) + && user_content.parts.is_empty() { - if user_content.parts.is_empty() { - bail!("User content must contain at least one part"); - } + bail!("User content must contain at least one part"); } Ok(()) diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index 3a80ee12a00ec24db64b2371637324b4c2393277..0040046f90554ecd3cb4c010faf18e7b98b62159 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -327,10 +327,10 @@ mod windows { /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler. fn find_fxc_compiler() -> String { // Check environment variable - if let Ok(path) = std::env::var("GPUI_FXC_PATH") { - if Path::new(&path).exists() { - return path; - } + if let Ok(path) = std::env::var("GPUI_FXC_PATH") + && Path::new(&path).exists() + { + return path; } // Try to find in PATH @@ -338,11 +338,10 @@ mod windows { if let Ok(output) = std::process::Command::new("where.exe") .arg("fxc.exe") .output() + && output.status.success() { - if output.status.success() { - let path = String::from_utf8_lossy(&output.stdout); - return path.trim().to_string(); - } + let path = String::from_utf8_lossy(&output.stdout); + return path.trim().to_string(); } // Check the default path diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index 170df3cad726605d10dbc42b34060d75e72ff20e..ae635c94b81aa2b452d1aff00939108cf0d34bbb 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -549,10 +549,10 @@ impl Element for TextElement { line.paint(bounds.origin, window.line_height(), window, cx) .unwrap(); - if focus_handle.is_focused(window) { - if let Some(cursor) = prepaint.cursor.take() { - window.paint_quad(cursor); - } + if focus_handle.is_focused(window) + && let Some(cursor) = prepaint.cursor.take() + { + window.paint_quad(cursor); } self.input.update(cx, |input, _cx| { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index ed1b935c58b93a591867a357be1f75499567889f..c4499aff0769f2ae2d7e6029cfd5caf6820bd6ed 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1516,12 +1516,11 @@ impl App { /// the bindings in the element tree, and any global action listeners. pub fn is_action_available(&mut self, action: &dyn Action) -> bool { let mut action_available = false; - if let Some(window) = self.active_window() { - if let Ok(window_action_available) = + if let Some(window) = self.active_window() + && let Ok(window_action_available) = window.update(self, |_, window, cx| window.is_action_available(action, cx)) - { - action_available = window_action_available; - } + { + action_available = window_action_available; } action_available @@ -1606,27 +1605,26 @@ impl App { .insert(action.as_any().type_id(), global_listeners); } - if self.propagate_event { - if let Some(mut global_listeners) = self + if self.propagate_event + && let Some(mut global_listeners) = self .global_action_listeners .remove(&action.as_any().type_id()) - { - for listener in global_listeners.iter().rev() { - listener(action.as_any(), DispatchPhase::Bubble, self); - if !self.propagate_event { - break; - } + { + for listener in global_listeners.iter().rev() { + listener(action.as_any(), DispatchPhase::Bubble, self); + if !self.propagate_event { + break; } + } - global_listeners.extend( - self.global_action_listeners - .remove(&action.as_any().type_id()) - .unwrap_or_default(), - ); - + global_listeners.extend( self.global_action_listeners - .insert(action.as_any().type_id(), global_listeners); - } + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); } } diff --git a/crates/gpui/src/app/context.rs b/crates/gpui/src/app/context.rs index 68c41592b3872addef6381d9f5e8a3f611611bd0..a6ab02677093b25d37ee5e608f299cdf9d86dc2f 100644 --- a/crates/gpui/src/app/context.rs +++ b/crates/gpui/src/app/context.rs @@ -610,16 +610,16 @@ impl<'a, T: 'static> Context<'a, T> { let (subscription, activate) = window.new_focus_listener(Box::new(move |event, window, cx| { view.update(cx, |view, cx| { - if let Some(blurred_id) = event.previous_focus_path.last().copied() { - if event.is_focus_out(focus_id) { - let event = FocusOutEvent { - blurred: WeakFocusHandle { - id: blurred_id, - handles: Arc::downgrade(&cx.focus_handles), - }, - }; - listener(view, event, window, cx) - } + if let Some(blurred_id) = event.previous_focus_path.last().copied() + && event.is_focus_out(focus_id) + { + let event = FocusOutEvent { + blurred: WeakFocusHandle { + id: blurred_id, + handles: Arc::downgrade(&cx.focus_handles), + }, + }; + listener(view, event, window, cx) } }) .is_ok() diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index e5f49c7be141a3620e52599bcc2b151acc1f7319..f537bc5ac840432732ae8c9fb608ba74ffefa168 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -603,10 +603,8 @@ impl AnyElement { self.0.prepaint(window, cx); - if !focus_assigned { - if let Some(focus_id) = window.next_frame.focus { - return FocusHandle::for_id(focus_id, &cx.focus_handles); - } + if !focus_assigned && let Some(focus_id) = window.next_frame.focus { + return FocusHandle::for_id(focus_id, &cx.focus_handles); } None diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index f553bf55f652daa0907983c18481a1d897923108..7b689ca0adf6b1df2a7e83bb197da3135cf0d759 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -286,21 +286,20 @@ impl Interactivity { { self.mouse_move_listeners .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Capture { - if let Some(drag) = &cx.active_drag { - if drag.value.as_ref().type_id() == TypeId::of::<T>() { - (listener)( - &DragMoveEvent { - event: event.clone(), - bounds: hitbox.bounds, - drag: PhantomData, - dragged_item: Arc::clone(&drag.value), - }, - window, - cx, - ); - } - } + if phase == DispatchPhase::Capture + && let Some(drag) = &cx.active_drag + && drag.value.as_ref().type_id() == TypeId::of::<T>() + { + (listener)( + &DragMoveEvent { + event: event.clone(), + bounds: hitbox.bounds, + drag: PhantomData, + dragged_item: Arc::clone(&drag.value), + }, + window, + cx, + ); } })); } @@ -1514,15 +1513,14 @@ impl Interactivity { let mut element_state = element_state.map(|element_state| element_state.unwrap_or_default()); - if let Some(element_state) = element_state.as_ref() { - if cx.has_active_drag() { - if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref() - { - *pending_mouse_down.borrow_mut() = None; - } - if let Some(clicked_state) = element_state.clicked_state.as_ref() { - *clicked_state.borrow_mut() = ElementClickedState::default(); - } + if let Some(element_state) = element_state.as_ref() + && cx.has_active_drag() + { + if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref() { + *pending_mouse_down.borrow_mut() = None; + } + if let Some(clicked_state) = element_state.clicked_state.as_ref() { + *clicked_state.borrow_mut() = ElementClickedState::default(); } } @@ -1530,35 +1528,35 @@ impl Interactivity { // If there's an explicit focus handle we're tracking, use that. Otherwise // create a new handle and store it in the element state, which lives for as // as frames contain an element with this id. - if self.focusable && self.tracked_focus_handle.is_none() { - if let Some(element_state) = element_state.as_mut() { - let mut handle = element_state - .focus_handle - .get_or_insert_with(|| cx.focus_handle()) - .clone() - .tab_stop(false); - - if let Some(index) = self.tab_index { - handle = handle.tab_index(index).tab_stop(true); - } - - self.tracked_focus_handle = Some(handle); + if self.focusable + && self.tracked_focus_handle.is_none() + && let Some(element_state) = element_state.as_mut() + { + let mut handle = element_state + .focus_handle + .get_or_insert_with(|| cx.focus_handle()) + .clone() + .tab_stop(false); + + if let Some(index) = self.tab_index { + handle = handle.tab_index(index).tab_stop(true); } + + self.tracked_focus_handle = Some(handle); } if let Some(scroll_handle) = self.tracked_scroll_handle.as_ref() { self.scroll_offset = Some(scroll_handle.0.borrow().offset.clone()); - } else if self.base_style.overflow.x == Some(Overflow::Scroll) - || self.base_style.overflow.y == Some(Overflow::Scroll) + } else if (self.base_style.overflow.x == Some(Overflow::Scroll) + || self.base_style.overflow.y == Some(Overflow::Scroll)) + && let Some(element_state) = element_state.as_mut() { - if let Some(element_state) = element_state.as_mut() { - self.scroll_offset = Some( - element_state - .scroll_offset - .get_or_insert_with(Rc::default) - .clone(), - ); - } + self.scroll_offset = Some( + element_state + .scroll_offset + .get_or_insert_with(Rc::default) + .clone(), + ); } let style = self.compute_style_internal(None, element_state.as_mut(), window, cx); @@ -2031,26 +2029,27 @@ impl Interactivity { let hitbox = hitbox.clone(); window.on_mouse_event({ move |_: &MouseUpEvent, phase, window, cx| { - if let Some(drag) = &cx.active_drag { - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { - let drag_state_type = drag.value.as_ref().type_id(); - for (drop_state_type, listener) in &drop_listeners { - if *drop_state_type == drag_state_type { - let drag = cx - .active_drag - .take() - .expect("checked for type drag state type above"); - - let mut can_drop = true; - if let Some(predicate) = &can_drop_predicate { - can_drop = predicate(drag.value.as_ref(), window, cx); - } + if let Some(drag) = &cx.active_drag + && phase == DispatchPhase::Bubble + && hitbox.is_hovered(window) + { + let drag_state_type = drag.value.as_ref().type_id(); + for (drop_state_type, listener) in &drop_listeners { + if *drop_state_type == drag_state_type { + let drag = cx + .active_drag + .take() + .expect("checked for type drag state type above"); + + let mut can_drop = true; + if let Some(predicate) = &can_drop_predicate { + can_drop = predicate(drag.value.as_ref(), window, cx); + } - if can_drop { - listener(drag.value.as_ref(), window, cx); - window.refresh(); - cx.stop_propagation(); - } + if can_drop { + listener(drag.value.as_ref(), window, cx); + window.refresh(); + cx.stop_propagation(); } } } @@ -2094,31 +2093,24 @@ impl Interactivity { } let mut pending_mouse_down = pending_mouse_down.borrow_mut(); - if let Some(mouse_down) = pending_mouse_down.clone() { - if !cx.has_active_drag() - && (event.position - mouse_down.position).magnitude() - > DRAG_THRESHOLD - { - if let Some((drag_value, drag_listener)) = drag_listener.take() { - *clicked_state.borrow_mut() = ElementClickedState::default(); - let cursor_offset = event.position - hitbox.origin; - let drag = (drag_listener)( - drag_value.as_ref(), - cursor_offset, - window, - cx, - ); - cx.active_drag = Some(AnyDrag { - view: drag, - value: drag_value, - cursor_offset, - cursor_style: drag_cursor_style, - }); - pending_mouse_down.take(); - window.refresh(); - cx.stop_propagation(); - } - } + if let Some(mouse_down) = pending_mouse_down.clone() + && !cx.has_active_drag() + && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD + && let Some((drag_value, drag_listener)) = drag_listener.take() + { + *clicked_state.borrow_mut() = ElementClickedState::default(); + let cursor_offset = event.position - hitbox.origin; + let drag = + (drag_listener)(drag_value.as_ref(), cursor_offset, window, cx); + cx.active_drag = Some(AnyDrag { + view: drag, + value: drag_value, + cursor_offset, + cursor_style: drag_cursor_style, + }); + pending_mouse_down.take(); + window.refresh(); + cx.stop_propagation(); } } }); @@ -2428,33 +2420,32 @@ impl Interactivity { style.refine(&self.base_style); if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { - if let Some(in_focus_style) = self.in_focus_style.as_ref() { - if focus_handle.within_focused(window, cx) { - style.refine(in_focus_style); - } + if let Some(in_focus_style) = self.in_focus_style.as_ref() + && focus_handle.within_focused(window, cx) + { + style.refine(in_focus_style); } - if let Some(focus_style) = self.focus_style.as_ref() { - if focus_handle.is_focused(window) { - style.refine(focus_style); - } + if let Some(focus_style) = self.focus_style.as_ref() + && focus_handle.is_focused(window) + { + style.refine(focus_style); } } if let Some(hitbox) = hitbox { if !cx.has_active_drag() { - if let Some(group_hover) = self.group_hover_style.as_ref() { - if let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) { - if group_hitbox_id.is_hovered(window) { - style.refine(&group_hover.style); - } - } + if let Some(group_hover) = self.group_hover_style.as_ref() + && let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) + && group_hitbox_id.is_hovered(window) + { + style.refine(&group_hover.style); } - if let Some(hover_style) = self.hover_style.as_ref() { - if hitbox.is_hovered(window) { - style.refine(hover_style); - } + if let Some(hover_style) = self.hover_style.as_ref() + && hitbox.is_hovered(window) + { + style.refine(hover_style); } } @@ -2468,12 +2459,10 @@ impl Interactivity { for (state_type, group_drag_style) in &self.group_drag_over_styles { if let Some(group_hitbox_id) = GroupHitboxes::get(&group_drag_style.group, cx) + && *state_type == drag.value.as_ref().type_id() + && group_hitbox_id.is_hovered(window) { - if *state_type == drag.value.as_ref().type_id() - && group_hitbox_id.is_hovered(window) - { - style.refine(&group_drag_style.style); - } + style.refine(&group_drag_style.style); } } @@ -2495,16 +2484,16 @@ impl Interactivity { .clicked_state .get_or_insert_with(Default::default) .borrow(); - if clicked_state.group { - if let Some(group) = self.group_active_style.as_ref() { - style.refine(&group.style) - } + if clicked_state.group + && let Some(group) = self.group_active_style.as_ref() + { + style.refine(&group.style) } - if let Some(active_style) = self.active_style.as_ref() { - if clicked_state.element { - style.refine(active_style) - } + if let Some(active_style) = self.active_style.as_ref() + && clicked_state.element + { + style.refine(active_style) } } diff --git a/crates/gpui/src/elements/image_cache.rs b/crates/gpui/src/elements/image_cache.rs index e7bdeaf9eb4d26913718a9b235cee4fcb0ca85ff..263f0aafc2c45739c2f7843454aaa8bccd335131 100644 --- a/crates/gpui/src/elements/image_cache.rs +++ b/crates/gpui/src/elements/image_cache.rs @@ -297,10 +297,10 @@ impl RetainAllImageCache { /// Remove the image from the cache by the given source. pub fn remove(&mut self, source: &Resource, window: &mut Window, cx: &mut App) { let hash = hash(source); - if let Some(mut item) = self.0.remove(&hash) { - if let Some(Ok(image)) = item.get() { - cx.drop_image(image, Some(window)); - } + if let Some(mut item) = self.0.remove(&hash) + && let Some(Ok(image)) = item.get() + { + cx.drop_image(image, Some(window)); } } diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 993b319b697ece386ad8af6d6164c1b85bf3a1c7..ae63819ca202bae5efe5829512c80b0fb754a567 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -379,13 +379,12 @@ impl Element for Img { None => { if let Some(state) = &mut state { if let Some((started_loading, _)) = state.started_loading { - if started_loading.elapsed() > LOADING_DELAY { - if let Some(loading) = self.style.loading.as_ref() { - let mut element = loading(); - replacement_id = - Some(element.request_layout(window, cx)); - layout_state.replacement = Some(element); - } + if started_loading.elapsed() > LOADING_DELAY + && let Some(loading) = self.style.loading.as_ref() + { + let mut element = loading(); + replacement_id = Some(element.request_layout(window, cx)); + layout_state.replacement = Some(element); } } else { let current_view = window.current_view(); diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 39f38bdc69d6a5d4c9ce8c7c349707e906124cca..98b63ef907f144add4758ff69b0fd6bcef2706a9 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -732,46 +732,44 @@ impl StateInner { item.element.prepaint_at(item_origin, window, cx); }); - if let Some(autoscroll_bounds) = window.take_autoscroll() { - if autoscroll { - if autoscroll_bounds.top() < bounds.top() { - return Err(ListOffset { - item_ix: item.index, - offset_in_item: autoscroll_bounds.top() - item_origin.y, - }); - } else if autoscroll_bounds.bottom() > bounds.bottom() { - let mut cursor = self.items.cursor::<Count>(&()); - cursor.seek(&Count(item.index), Bias::Right); - let mut height = bounds.size.height - padding.top - padding.bottom; - - // Account for the height of the element down until the autoscroll bottom. - height -= autoscroll_bounds.bottom() - item_origin.y; - - // Keep decreasing the scroll top until we fill all the available space. - while height > Pixels::ZERO { - cursor.prev(); - let Some(item) = cursor.item() else { break }; - - let size = item.size().unwrap_or_else(|| { - let mut item = render_item(cursor.start().0, window, cx); - let item_available_size = size( - bounds.size.width.into(), - AvailableSpace::MinContent, - ); - item.layout_as_root(item_available_size, window, cx) - }); - height -= size.height; - } - - return Err(ListOffset { - item_ix: cursor.start().0, - offset_in_item: if height < Pixels::ZERO { - -height - } else { - Pixels::ZERO - }, + if let Some(autoscroll_bounds) = window.take_autoscroll() + && autoscroll + { + if autoscroll_bounds.top() < bounds.top() { + return Err(ListOffset { + item_ix: item.index, + offset_in_item: autoscroll_bounds.top() - item_origin.y, + }); + } else if autoscroll_bounds.bottom() > bounds.bottom() { + let mut cursor = self.items.cursor::<Count>(&()); + cursor.seek(&Count(item.index), Bias::Right); + let mut height = bounds.size.height - padding.top - padding.bottom; + + // Account for the height of the element down until the autoscroll bottom. + height -= autoscroll_bounds.bottom() - item_origin.y; + + // Keep decreasing the scroll top until we fill all the available space. + while height > Pixels::ZERO { + cursor.prev(); + let Some(item) = cursor.item() else { break }; + + let size = item.size().unwrap_or_else(|| { + let mut item = render_item(cursor.start().0, window, cx); + let item_available_size = + size(bounds.size.width.into(), AvailableSpace::MinContent); + item.layout_as_root(item_available_size, window, cx) }); + height -= size.height; } + + return Err(ListOffset { + item_ix: cursor.start().0, + offset_in_item: if height < Pixels::ZERO { + -height + } else { + Pixels::ZERO + }, + }); } } diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 014f617e2cfc74755908368f57060aeaeb38aa74..c58f72267c281bd66d844c184f1c56756141371b 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -356,12 +356,11 @@ impl TextLayout { (None, "".into()) }; - if let Some(text_layout) = element_state.0.borrow().as_ref() { - if text_layout.size.is_some() - && (wrap_width.is_none() || wrap_width == text_layout.wrap_width) - { - return text_layout.size.unwrap(); - } + if let Some(text_layout) = element_state.0.borrow().as_ref() + && text_layout.size.is_some() + && (wrap_width.is_none() || wrap_width == text_layout.wrap_width) + { + return text_layout.size.unwrap(); } let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size); @@ -763,14 +762,13 @@ impl Element for InteractiveText { let mut interactive_state = interactive_state.unwrap_or_default(); if let Some(click_listener) = self.click_listener.take() { let mouse_position = window.mouse_position(); - if let Ok(ix) = text_layout.index_for_position(mouse_position) { - if self + if let Ok(ix) = text_layout.index_for_position(mouse_position) + && self .clickable_ranges .iter() .any(|range| range.contains(&ix)) - { - window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox) - } + { + window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox) } let text_layout = text_layout.clone(); @@ -803,13 +801,13 @@ impl Element for InteractiveText { } else { let hitbox = hitbox.clone(); window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| { - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { - if let Ok(mouse_down_index) = + if phase == DispatchPhase::Bubble + && hitbox.is_hovered(window) + && let Ok(mouse_down_index) = text_layout.index_for_position(event.position) - { - mouse_down.set(Some(mouse_down_index)); - window.refresh(); - } + { + mouse_down.set(Some(mouse_down_index)); + window.refresh(); } }); } diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index 1d3f612c5bef76d75cb1bd8ee9d9c686190c3fd7..6d36cbb4e0951c49caa095f6f2e8a9c6a147da56 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -53,10 +53,10 @@ impl KeyBinding { if let Some(equivalents) = key_equivalents { for keystroke in keystrokes.iter_mut() { - if keystroke.key.chars().count() == 1 { - if let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) { - keystroke.key = key.to_string(); - } + if keystroke.key.chars().count() == 1 + && let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) + { + keystroke.key = key.to_string(); } } } diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index 46d3c16c72a9c10c0e686aff425fcc236c253ce7..cc1df7748ba6b7947ab53a86baa8ab31644ac05d 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/crates/gpui/src/platform/blade/blade_renderer.rs @@ -434,24 +434,24 @@ impl BladeRenderer { } fn wait_for_gpu(&mut self) { - if let Some(last_sp) = self.last_sync_point.take() { - if !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) { - log::error!("GPU hung"); - #[cfg(target_os = "linux")] - if self.gpu.device_information().driver_name == "radv" { - log::error!( - "there's a known bug with amdgpu/radv, try setting ZED_PATH_SAMPLE_COUNT=0 as a workaround" - ); - log::error!( - "if that helps you're running into https://github.com/zed-industries/zed/issues/26143" - ); - } + if let Some(last_sp) = self.last_sync_point.take() + && !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) + { + log::error!("GPU hung"); + #[cfg(target_os = "linux")] + if self.gpu.device_information().driver_name == "radv" { log::error!( - "your device information is: {:?}", - self.gpu.device_information() + "there's a known bug with amdgpu/radv, try setting ZED_PATH_SAMPLE_COUNT=0 as a workaround" + ); + log::error!( + "if that helps you're running into https://github.com/zed-industries/zed/issues/26143" ); - while !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) {} } + log::error!( + "your device information is: {:?}", + self.gpu.device_information() + ); + while !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) {} } } diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 0ab61fbf0c19bfc6390962742fd1ec8fc25d5f62..d1aa590192aad06df50525d5a63debb0d82f9e81 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -359,13 +359,13 @@ impl WaylandClientStatePtr { } changed }; - if changed { - if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() { - drop(state); - callback(); - state = client.borrow_mut(); - state.common.callbacks.keyboard_layout_change = Some(callback); - } + + if changed && let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() + { + drop(state); + callback(); + state = client.borrow_mut(); + state.common.callbacks.keyboard_layout_change = Some(callback); } } @@ -373,15 +373,15 @@ impl WaylandClientStatePtr { let mut client = self.get_client(); let mut state = client.borrow_mut(); let closed_window = state.windows.remove(surface_id).unwrap(); - if let Some(window) = state.mouse_focused_window.take() { - if !window.ptr_eq(&closed_window) { - state.mouse_focused_window = Some(window); - } + if let Some(window) = state.mouse_focused_window.take() + && !window.ptr_eq(&closed_window) + { + state.mouse_focused_window = Some(window); } - if let Some(window) = state.keyboard_focused_window.take() { - if !window.ptr_eq(&closed_window) { - state.keyboard_focused_window = Some(window); - } + if let Some(window) = state.keyboard_focused_window.take() + && !window.ptr_eq(&closed_window) + { + state.keyboard_focused_window = Some(window); } if state.windows.is_empty() { state.common.signal.stop(); @@ -1784,17 +1784,17 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr { drop(state); window.handle_input(input); } - } else if let Some(discrete) = discrete { - if let Some(window) = state.mouse_focused_window.clone() { - let input = PlatformInput::ScrollWheel(ScrollWheelEvent { - position: state.mouse_location.unwrap(), - delta: ScrollDelta::Lines(discrete), - modifiers: state.modifiers, - touch_phase: TouchPhase::Moved, - }); - drop(state); - window.handle_input(input); - } + } else if let Some(discrete) = discrete + && let Some(window) = state.mouse_focused_window.clone() + { + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: state.mouse_location.unwrap(), + delta: ScrollDelta::Lines(discrete), + modifiers: state.modifiers, + touch_phase: TouchPhase::Moved, + }); + drop(state); + window.handle_input(input); } } } diff --git a/crates/gpui/src/platform/linux/wayland/cursor.rs b/crates/gpui/src/platform/linux/wayland/cursor.rs index bfbedf234dc66c3f82040ce08a6eb0e99f04add9..a21263ccfe9c6e654b4f03e781c37398886a281a 100644 --- a/crates/gpui/src/platform/linux/wayland/cursor.rs +++ b/crates/gpui/src/platform/linux/wayland/cursor.rs @@ -45,10 +45,11 @@ impl Cursor { } fn set_theme_internal(&mut self, theme_name: Option<String>) { - if let Some(loaded_theme) = self.loaded_theme.as_ref() { - if loaded_theme.name == theme_name && loaded_theme.scaled_size == self.scaled_size { - return; - } + if let Some(loaded_theme) = self.loaded_theme.as_ref() + && loaded_theme.name == theme_name + && loaded_theme.scaled_size == self.scaled_size + { + return; } let result = if let Some(theme_name) = theme_name.as_ref() { CursorTheme::load_from_name( diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 2b2207e22c86fc25e6387581bb92b9c304f4bc9d..7cf2d02d3b1c3f5356a46fd74a2a149807afba6f 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -713,21 +713,20 @@ impl WaylandWindowStatePtr { } pub fn handle_input(&self, input: PlatformInput) { - if let Some(ref mut fun) = self.callbacks.borrow_mut().input { - if !fun(input.clone()).propagate { - return; - } + if let Some(ref mut fun) = self.callbacks.borrow_mut().input + && !fun(input.clone()).propagate + { + return; } - if let PlatformInput::KeyDown(event) = input { - if event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) { - if let Some(key_char) = &event.keystroke.key_char { - let mut state = self.state.borrow_mut(); - if let Some(mut input_handler) = state.input_handler.take() { - drop(state); - input_handler.replace_text_in_range(None, key_char); - self.state.borrow_mut().input_handler = Some(input_handler); - } - } + if let PlatformInput::KeyDown(event) = input + && event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) + && let Some(key_char) = &event.keystroke.key_char + { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.replace_text_in_range(None, key_char); + self.state.borrow_mut().input_handler = Some(input_handler); } } } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index dd0cea32902046d204cb70c2eb1f4bdf6d48cfe2..b4914c9dd29b8d66d5ee11da9ccc31d2aa885052 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -565,10 +565,10 @@ impl X11Client { events.push(last_keymap_change_event); } - if let Some(last_press) = last_key_press.as_ref() { - if last_press.detail == key_press.detail { - continue; - } + if let Some(last_press) = last_key_press.as_ref() + && last_press.detail == key_press.detail + { + continue; } if let Some(Event::KeyRelease(key_release)) = @@ -2035,12 +2035,11 @@ fn xdnd_get_supported_atom( ), ) .log_with_level(Level::Warn) + && let Some(atoms) = reply.value32() { - if let Some(atoms) = reply.value32() { - for atom in atoms { - if xdnd_is_atom_supported(atom, supported_atoms) { - return atom; - } + for atom in atoms { + if xdnd_is_atom_supported(atom, supported_atoms) { + return atom; } } } @@ -2411,11 +2410,13 @@ fn legacy_get_randr_scale_factor(connection: &XCBConnection, root: u32) -> Optio let mut crtc_infos: HashMap<randr::Crtc, randr::GetCrtcInfoReply> = HashMap::default(); let mut valid_outputs: HashSet<randr::Output> = HashSet::new(); for (crtc, cookie) in crtc_cookies { - if let Ok(reply) = cookie.reply() { - if reply.width > 0 && reply.height > 0 && !reply.outputs.is_empty() { - crtc_infos.insert(crtc, reply.clone()); - valid_outputs.extend(&reply.outputs); - } + if let Ok(reply) = cookie.reply() + && reply.width > 0 + && reply.height > 0 + && !reply.outputs.is_empty() + { + crtc_infos.insert(crtc, reply.clone()); + valid_outputs.extend(&reply.outputs); } } diff --git a/crates/gpui/src/platform/linux/x11/clipboard.rs b/crates/gpui/src/platform/linux/x11/clipboard.rs index 5d42eadaaf04e0ad7811b980e6d31b4bca935139..5b32f2c93eb762400c21b07c72c322aee554c7ae 100644 --- a/crates/gpui/src/platform/linux/x11/clipboard.rs +++ b/crates/gpui/src/platform/linux/x11/clipboard.rs @@ -1120,25 +1120,25 @@ impl Drop for Clipboard { log::error!("Failed to flush the clipboard window. Error: {}", e); return; } - if let Some(global_cb) = global_cb { - if let Err(e) = global_cb.server_handle.join() { - // Let's try extracting the error message - let message; - if let Some(msg) = e.downcast_ref::<&'static str>() { - message = Some((*msg).to_string()); - } else if let Some(msg) = e.downcast_ref::<String>() { - message = Some(msg.clone()); - } else { - message = None; - } - if let Some(message) = message { - log::error!( - "The clipboard server thread panicked. Panic message: '{}'", - message, - ); - } else { - log::error!("The clipboard server thread panicked."); - } + if let Some(global_cb) = global_cb + && let Err(e) = global_cb.server_handle.join() + { + // Let's try extracting the error message + let message; + if let Some(msg) = e.downcast_ref::<&'static str>() { + message = Some((*msg).to_string()); + } else if let Some(msg) = e.downcast_ref::<String>() { + message = Some(msg.clone()); + } else { + message = None; + } + if let Some(message) = message { + log::error!( + "The clipboard server thread panicked. Panic message: '{}'", + message, + ); + } else { + log::error!("The clipboard server thread panicked."); } } } diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 2bf58d6184e1b542adabd72eaddd11dc091ec28c..c33d6fa4621a924d3a808a54cce1c0abe44e3ef4 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -515,19 +515,19 @@ impl X11WindowState { xcb.configure_window(x_window, &xproto::ConfigureWindowAux::new().x(x).y(y)), )?; } - if let Some(titlebar) = params.titlebar { - if let Some(title) = titlebar.title { - check_reply( - || "X11 ChangeProperty8 on window title failed.", - xcb.change_property8( - xproto::PropMode::REPLACE, - x_window, - xproto::AtomEnum::WM_NAME, - xproto::AtomEnum::STRING, - title.as_bytes(), - ), - )?; - } + if let Some(titlebar) = params.titlebar + && let Some(title) = titlebar.title + { + check_reply( + || "X11 ChangeProperty8 on window title failed.", + xcb.change_property8( + xproto::PropMode::REPLACE, + x_window, + xproto::AtomEnum::WM_NAME, + xproto::AtomEnum::STRING, + title.as_bytes(), + ), + )?; } if params.kind == WindowKind::PopUp { check_reply( @@ -956,10 +956,10 @@ impl X11WindowStatePtr { } pub fn handle_input(&self, input: PlatformInput) { - if let Some(ref mut fun) = self.callbacks.borrow_mut().input { - if !fun(input.clone()).propagate { - return; - } + if let Some(ref mut fun) = self.callbacks.borrow_mut().input + && !fun(input.clone()).propagate + { + return; } if let PlatformInput::KeyDown(event) = input { // only allow shift modifier when inserting text @@ -1068,15 +1068,14 @@ impl X11WindowStatePtr { } let mut callbacks = self.callbacks.borrow_mut(); - if let Some((content_size, scale_factor)) = resize_args { - if let Some(ref mut fun) = callbacks.resize { - fun(content_size, scale_factor) - } + if let Some((content_size, scale_factor)) = resize_args + && let Some(ref mut fun) = callbacks.resize + { + fun(content_size, scale_factor) } - if !is_resize { - if let Some(ref mut fun) = callbacks.moved { - fun(); - } + + if !is_resize && let Some(ref mut fun) = callbacks.moved { + fun(); } Ok(()) diff --git a/crates/gpui/src/platform/mac/open_type.rs b/crates/gpui/src/platform/mac/open_type.rs index 2ae5e8f87ab78a70e423a4645c96e69f098828a6..37a29559fdfbc284ffd1021cc6c2c6ed717ca228 100644 --- a/crates/gpui/src/platform/mac/open_type.rs +++ b/crates/gpui/src/platform/mac/open_type.rs @@ -35,14 +35,14 @@ pub fn apply_features_and_fallbacks( unsafe { let mut keys = vec![kCTFontFeatureSettingsAttribute]; let mut values = vec![generate_feature_array(features)]; - if let Some(fallbacks) = fallbacks { - if !fallbacks.fallback_list().is_empty() { - keys.push(kCTFontCascadeListAttribute); - values.push(generate_fallback_array( - fallbacks, - font.native_font().as_concrete_TypeRef(), - )); - } + if let Some(fallbacks) = fallbacks + && !fallbacks.fallback_list().is_empty() + { + keys.push(kCTFontCascadeListAttribute); + values.push(generate_fallback_array( + fallbacks, + font.native_font().as_concrete_TypeRef(), + )); } let attrs = CFDictionaryCreate( kCFAllocatorDefault, diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index f094ed9f30bed2f54aa0698c13ad3454e4a7e677..57dfa9c6036cc4ddb8c9131712a9d675957f19ad 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -715,10 +715,10 @@ impl Platform for MacPlatform { let urls = panel.URLs(); for i in 0..urls.count() { let url = urls.objectAtIndex(i); - if url.isFileURL() == YES { - if let Ok(path) = ns_url_to_path(url) { - result.push(path) - } + if url.isFileURL() == YES + && let Ok(path) = ns_url_to_path(url) + { + result.push(path) } } Some(result) @@ -786,15 +786,16 @@ impl Platform for MacPlatform { // This is conditional on OS version because I'd like to get rid of it, so that // you can manually create a file called `a.sql.s`. That said it seems better // to break that use-case than breaking `a.sql`. - if chunks.len() == 3 && chunks[1].starts_with(chunks[2]) { - if Self::os_version() >= SemanticVersion::new(15, 0, 0) { - let new_filename = OsStr::from_bytes( - &filename.as_bytes() - [..chunks[0].len() + 1 + chunks[1].len()], - ) - .to_owned(); - result.set_file_name(&new_filename); - } + if chunks.len() == 3 + && chunks[1].starts_with(chunks[2]) + && Self::os_version() >= SemanticVersion::new(15, 0, 0) + { + let new_filename = OsStr::from_bytes( + &filename.as_bytes() + [..chunks[0].len() + 1 + chunks[1].len()], + ) + .to_owned(); + result.set_file_name(&new_filename); } return result; }) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 40a03b6c4a7eb41620f0909ba3f58a26d8cfdffb..b6f684a72c7eed2f75670401a9bbf51118c162db 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1478,18 +1478,18 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: return YES; } - if key_down_event.is_held { - if let Some(key_char) = key_down_event.keystroke.key_char.as_ref() { - let handled = with_input_handler(this, |input_handler| { - if !input_handler.apple_press_and_hold_enabled() { - input_handler.replace_text_in_range(None, key_char); - return YES; - } - NO - }); - if handled == Some(YES) { + if key_down_event.is_held + && let Some(key_char) = key_down_event.keystroke.key_char.as_ref() + { + let handled = with_input_handler(this, |input_handler| { + if !input_handler.apple_press_and_hold_enabled() { + input_handler.replace_text_in_range(None, key_char); return YES; } + NO + }); + if handled == Some(YES) { + return YES; } } @@ -1624,10 +1624,10 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { modifiers: prev_modifiers, capslock: prev_capslock, })) = &lock.previous_modifiers_changed_event + && prev_modifiers == modifiers + && prev_capslock == capslock { - if prev_modifiers == modifiers && prev_capslock == capslock { - return; - } + return; } lock.previous_modifiers_changed_event = Some(event.clone()); @@ -1995,10 +1995,10 @@ extern "C" fn attributed_substring_for_proposed_range( let mut adjusted: Option<Range<usize>> = None; let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?; - if let Some(adjusted) = adjusted { - if adjusted != range { - unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) }; - } + if let Some(adjusted) = adjusted + && adjusted != range + { + unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) }; } unsafe { let string: id = msg_send![class!(NSAttributedString), alloc]; @@ -2073,11 +2073,10 @@ extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDr let paths = external_paths_from_event(dragging_info); if let Some(event) = paths.map(|paths| PlatformInput::FileDrop(FileDropEvent::Entered { position, paths })) + && send_new_event(&window_state, event) { - if send_new_event(&window_state, event) { - window_state.lock().external_files_dragged = true; - return NSDragOperationCopy; - } + window_state.lock().external_files_dragged = true; + return NSDragOperationCopy; } NSDragOperationNone } diff --git a/crates/gpui/src/platform/test/dispatcher.rs b/crates/gpui/src/platform/test/dispatcher.rs index 16edabfa4bfee9c66dcf6ed8abc5eeb7957a7fa0..bdc783493160b0acf83852d515925f28df555527 100644 --- a/crates/gpui/src/platform/test/dispatcher.rs +++ b/crates/gpui/src/platform/test/dispatcher.rs @@ -78,11 +78,11 @@ impl TestDispatcher { let state = self.state.lock(); let next_due_time = state.delayed.first().map(|(time, _)| *time); drop(state); - if let Some(due_time) = next_due_time { - if due_time <= new_now { - self.state.lock().time = due_time; - continue; - } + if let Some(due_time) = next_due_time + && due_time <= new_now + { + self.state.lock().time = due_time; + continue; } break; } diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 69371bc8c4aae38c48e1f14ae223fd9c8b1fb75e..2b4914baedfbc33a60df3fef0282535e0a9b6b3d 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -201,10 +201,10 @@ impl TestPlatform { executor .spawn(async move { if let Some(previous_window) = previous_window { - if let Some(window) = window.as_ref() { - if Rc::ptr_eq(&previous_window.0, &window.0) { - return; - } + if let Some(window) = window.as_ref() + && Rc::ptr_eq(&previous_window.0, &window.0) + { + return; } previous_window.simulate_active_status_change(false); } diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 9b25ab360eaa0e9238f231fadfb2e0c7c86573ee..c3bb8bb22babe6d07e1c01cdbe07706deb7bcf03 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -701,29 +701,28 @@ impl WindowsWindowInner { // 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 + if is_maximized + && 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 - } + { + // 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 } } } @@ -1125,28 +1124,26 @@ impl WindowsWindowInner { // 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); - } + if unsafe { !parameter.is_null() && !parameter.is_empty() } + && 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) diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 856187fa5719cfc95364ec89b521074820b046c7..b13b9915f135b7ff6c4aafe2d04f670f7416aca1 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -821,14 +821,14 @@ fn file_save_dialog( window: Option<HWND>, ) -> Result<Option<PathBuf>> { let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? }; - if !directory.to_string_lossy().is_empty() { - if let Some(full_path) = directory.canonicalize().log_err() { - let full_path = SanitizedPath::from(full_path); - let full_path_string = full_path.to_string(); - let path_item: IShellItem = - unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? }; - unsafe { dialog.SetFolder(&path_item).log_err() }; - } + if !directory.to_string_lossy().is_empty() + && let Some(full_path) = directory.canonicalize().log_err() + { + let full_path = SanitizedPath::from(full_path); + let full_path_string = full_path.to_string(); + let path_item: IShellItem = + unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? }; + unsafe { dialog.SetFolder(&path_item).log_err() }; } if let Some(suggested_name) = suggested_name { diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index b48c3a29350ab3c770c5eca765f7019ae1afd8f3..29af900b66a7b41fe69c7d4c03712ce0391a61c2 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -366,15 +366,14 @@ impl WindowTextSystem { let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); for run in runs { - if let Some(last_run) = decoration_runs.last_mut() { - if last_run.color == run.color - && last_run.underline == run.underline - && last_run.strikethrough == run.strikethrough - && last_run.background_color == run.background_color - { - last_run.len += run.len as u32; - continue; - } + if let Some(last_run) = decoration_runs.last_mut() + && last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + && last_run.background_color == run.background_color + { + last_run.len += run.len as u32; + continue; } decoration_runs.push(DecorationRun { len: run.len as u32, @@ -492,14 +491,14 @@ impl WindowTextSystem { let mut split_lines = text.split('\n'); let mut processed = false; - if let Some(first_line) = split_lines.next() { - if let Some(second_line) = split_lines.next() { - processed = true; - process_line(first_line.to_string().into()); - process_line(second_line.to_string().into()); - for line_text in split_lines { - process_line(line_text.to_string().into()); - } + if let Some(first_line) = split_lines.next() + && let Some(second_line) = split_lines.next() + { + processed = true; + process_line(first_line.to_string().into()); + process_line(second_line.to_string().into()); + for line_text in split_lines { + process_line(line_text.to_string().into()); } } @@ -534,11 +533,11 @@ impl WindowTextSystem { let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); for run in runs.iter() { let font_id = self.resolve_font(&run.font); - if let Some(last_run) = font_runs.last_mut() { - if last_run.font_id == font_id { - last_run.len += run.len; - continue; - } + if let Some(last_run) = font_runs.last_mut() + && last_run.font_id == font_id + { + last_run.len += run.len; + continue; } font_runs.push(FontRun { len: run.len, diff --git a/crates/gpui/src/text_system/line.rs b/crates/gpui/src/text_system/line.rs index 3813393d81deaff4ed9adb1d96e204b75953233f..8d559f981581858990fa545b8e0ba65bccdf80a8 100644 --- a/crates/gpui/src/text_system/line.rs +++ b/crates/gpui/src/text_system/line.rs @@ -292,10 +292,10 @@ fn paint_line( } if let Some(style_run) = style_run { - if let Some((_, underline_style)) = &mut current_underline { - if style_run.underline.as_ref() != Some(underline_style) { - finished_underline = current_underline.take(); - } + if let Some((_, underline_style)) = &mut current_underline + && style_run.underline.as_ref() != Some(underline_style) + { + finished_underline = current_underline.take(); } if let Some(run_underline) = style_run.underline.as_ref() { current_underline.get_or_insert(( @@ -310,10 +310,10 @@ fn paint_line( }, )); } - if let Some((_, strikethrough_style)) = &mut current_strikethrough { - if style_run.strikethrough.as_ref() != Some(strikethrough_style) { - finished_strikethrough = current_strikethrough.take(); - } + if let Some((_, strikethrough_style)) = &mut current_strikethrough + && style_run.strikethrough.as_ref() != Some(strikethrough_style) + { + finished_strikethrough = current_strikethrough.take(); } if let Some(run_strikethrough) = style_run.strikethrough.as_ref() { current_strikethrough.get_or_insert(( @@ -509,10 +509,10 @@ fn paint_line_background( } if let Some(style_run) = style_run { - if let Some((_, background_color)) = &mut current_background { - if style_run.background_color.as_ref() != Some(background_color) { - finished_background = current_background.take(); - } + if let Some((_, background_color)) = &mut current_background + && style_run.background_color.as_ref() != Some(background_color) + { + finished_background = current_background.take(); } if let Some(run_background) = style_run.background_color { current_background.get_or_insert(( diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index 9c2dd7f0871e5b67bd15d3a419c1c03496e2afaa..43694702a82566b8f84199dcfc4ff996da93588e 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -185,10 +185,10 @@ impl LineLayout { if width > wrap_width && boundary > last_boundary { // When used line_clamp, we should limit the number of lines. - if let Some(max_lines) = max_lines { - if boundaries.len() >= max_lines - 1 { - break; - } + if let Some(max_lines) = max_lines + && boundaries.len() >= max_lines - 1 + { + break; } if let Some(last_candidate_ix) = last_candidate_ix.take() { diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index f461e2f7d01a1dc2cdc93cda4f5854c8e958feaf..217971792ee978307a19f7e40374cb337e38a625 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -205,22 +205,21 @@ impl Element for AnyView { let content_mask = window.content_mask(); let text_style = window.text_style(); - if let Some(mut element_state) = element_state { - if element_state.cache_key.bounds == bounds - && element_state.cache_key.content_mask == content_mask - && element_state.cache_key.text_style == text_style - && !window.dirty_views.contains(&self.entity_id()) - && !window.refreshing - { - let prepaint_start = window.prepaint_index(); - window.reuse_prepaint(element_state.prepaint_range.clone()); - cx.entities - .extend_accessed(&element_state.accessed_entities); - let prepaint_end = window.prepaint_index(); - element_state.prepaint_range = prepaint_start..prepaint_end; - - return (None, element_state); - } + if let Some(mut element_state) = element_state + && element_state.cache_key.bounds == bounds + && element_state.cache_key.content_mask == content_mask + && element_state.cache_key.text_style == text_style + && !window.dirty_views.contains(&self.entity_id()) + && !window.refreshing + { + let prepaint_start = window.prepaint_index(); + window.reuse_prepaint(element_state.prepaint_range.clone()); + cx.entities + .extend_accessed(&element_state.accessed_entities); + let prepaint_end = window.prepaint_index(); + element_state.prepaint_range = prepaint_start..prepaint_end; + + return (None, element_state); } let refreshing = mem::replace(&mut window.refreshing, true); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index c0ffd34a0d99f78f9388927ba7857ebb9661baa1..62aeb0df118a7ae929c53cdeba17b2231c147600 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3401,16 +3401,16 @@ impl Window { let focus_id = handle.id; let (subscription, activate) = self.new_focus_listener(Box::new(move |event, window, cx| { - if let Some(blurred_id) = event.previous_focus_path.last().copied() { - if event.is_focus_out(focus_id) { - let event = FocusOutEvent { - blurred: WeakFocusHandle { - id: blurred_id, - handles: Arc::downgrade(&cx.focus_handles), - }, - }; - listener(event, window, cx) - } + if let Some(blurred_id) = event.previous_focus_path.last().copied() + && event.is_focus_out(focus_id) + { + let event = FocusOutEvent { + blurred: WeakFocusHandle { + id: blurred_id, + handles: Arc::downgrade(&cx.focus_handles), + }, + }; + listener(event, window, cx) } true })); @@ -3444,12 +3444,12 @@ impl Window { return true; } - if let Some(input) = keystroke.key_char { - if let Some(mut input_handler) = self.platform_window.take_input_handler() { - input_handler.dispatch_input(&input, self, cx); - self.platform_window.set_input_handler(input_handler); - return true; - } + if let Some(input) = keystroke.key_char + && let Some(mut input_handler) = self.platform_window.take_input_handler() + { + input_handler.dispatch_input(&input, self, cx); + self.platform_window.set_input_handler(input_handler); + return true; } false @@ -3864,11 +3864,11 @@ impl Window { if !cx.propagate_event { continue 'replay; } - if let Some(input) = replay.keystroke.key_char.as_ref().cloned() { - if let Some(mut input_handler) = self.platform_window.take_input_handler() { - input_handler.dispatch_input(&input, self, cx); - self.platform_window.set_input_handler(input_handler) - } + if let Some(input) = replay.keystroke.key_char.as_ref().cloned() + && let Some(mut input_handler) = self.platform_window.take_input_handler() + { + input_handler.dispatch_input(&input, self, cx); + self.platform_window.set_input_handler(input_handler) } } } @@ -4309,15 +4309,15 @@ impl Window { cx: &mut App, f: impl FnOnce(&mut Option<T>, &mut Self) -> R, ) -> R { - if let Some(inspector_id) = _inspector_id { - if let Some(inspector) = &self.inspector { - let inspector = inspector.clone(); - let active_element_id = inspector.read(cx).active_element_id(); - if Some(inspector_id) == active_element_id { - return inspector.update(cx, |inspector, _cx| { - inspector.with_active_element_state(self, f) - }); - } + if let Some(inspector_id) = _inspector_id + && let Some(inspector) = &self.inspector + { + let inspector = inspector.clone(); + let active_element_id = inspector.read(cx).active_element_id(); + if Some(inspector_id) == active_element_id { + return inspector.update(cx, |inspector, _cx| { + inspector.with_active_element_state(self, f) + }); } } f(&mut None, self) @@ -4389,15 +4389,13 @@ impl Window { if let Some(inspector) = self.inspector.as_ref() { let inspector = inspector.read(cx); if let Some((hitbox_id, _)) = self.hovered_inspector_hitbox(inspector, &self.next_frame) - { - if let Some(hitbox) = self + && let Some(hitbox) = self .next_frame .hitboxes .iter() .find(|hitbox| hitbox.id == hitbox_id) - { - self.paint_quad(crate::fill(hitbox.bounds, crate::rgba(0x61afef4d))); - } + { + self.paint_quad(crate::fill(hitbox.bounds, crate::rgba(0x61afef4d))); } } } diff --git a/crates/gpui_macros/src/derive_inspector_reflection.rs b/crates/gpui_macros/src/derive_inspector_reflection.rs index fa22f95f9a1c274d193a6985a84bf3cdecfcc17f..5415807ea08d63344375efc8a96f70424f0dd1ce 100644 --- a/crates/gpui_macros/src/derive_inspector_reflection.rs +++ b/crates/gpui_macros/src/derive_inspector_reflection.rs @@ -160,16 +160,14 @@ fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> { let mut doc_lines = Vec::new(); for attr in attrs { - if attr.path().is_ident("doc") { - if let Meta::NameValue(meta) = &attr.meta { - if let Expr::Lit(expr_lit) = &meta.value { - if let Lit::Str(lit_str) = &expr_lit.lit { - let line = lit_str.value(); - let line = line.strip_prefix(' ').unwrap_or(&line); - doc_lines.push(line.to_string()); - } - } - } + if attr.path().is_ident("doc") + && let Meta::NameValue(meta) = &attr.meta + && let Expr::Lit(expr_lit) = &meta.value + && let Lit::Str(lit_str) = &expr_lit.lit + { + let line = lit_str.value(); + let line = line.strip_prefix(' ').unwrap_or(&line); + doc_lines.push(line.to_string()); } } diff --git a/crates/gpui_macros/src/test.rs b/crates/gpui_macros/src/test.rs index 5a8b1cf7fca771813d2ac5c18a49a643c55c205c..0153c5889adf53f8a95b5876726d70230aad587d 100644 --- a/crates/gpui_macros/src/test.rs +++ b/crates/gpui_macros/src/test.rs @@ -152,28 +152,28 @@ fn generate_test_function( } _ => {} } - } else if let Type::Reference(ty) = &*arg.ty { - if let Type::Path(ty) = &*ty.elem { - let last_segment = ty.path.segments.last(); - if let Some("TestAppContext") = - last_segment.map(|s| s.ident.to_string()).as_deref() - { - let cx_varname = format_ident!("cx_{}", ix); - cx_vars.extend(quote!( - let mut #cx_varname = gpui::TestAppContext::build( - dispatcher.clone(), - Some(stringify!(#outer_fn_name)), - ); - )); - cx_teardowns.extend(quote!( - dispatcher.run_until_parked(); - #cx_varname.executor().forbid_parking(); - #cx_varname.quit(); - dispatcher.run_until_parked(); - )); - inner_fn_args.extend(quote!(&mut #cx_varname,)); - continue; - } + } else if let Type::Reference(ty) = &*arg.ty + && let Type::Path(ty) = &*ty.elem + { + let last_segment = ty.path.segments.last(); + if let Some("TestAppContext") = + last_segment.map(|s| s.ident.to_string()).as_deref() + { + let cx_varname = format_ident!("cx_{}", ix); + cx_vars.extend(quote!( + let mut #cx_varname = gpui::TestAppContext::build( + dispatcher.clone(), + Some(stringify!(#outer_fn_name)), + ); + )); + cx_teardowns.extend(quote!( + dispatcher.run_until_parked(); + #cx_varname.executor().forbid_parking(); + #cx_varname.quit(); + dispatcher.run_until_parked(); + )); + inner_fn_args.extend(quote!(&mut #cx_varname,)); + continue; } } } @@ -215,48 +215,48 @@ fn generate_test_function( inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),)); continue; } - } else if let Type::Reference(ty) = &*arg.ty { - if let Type::Path(ty) = &*ty.elem { - let last_segment = ty.path.segments.last(); - match last_segment.map(|s| s.ident.to_string()).as_deref() { - Some("App") => { - let cx_varname = format_ident!("cx_{}", ix); - let cx_varname_lock = format_ident!("cx_{}_lock", ix); - cx_vars.extend(quote!( - let mut #cx_varname = gpui::TestAppContext::build( - dispatcher.clone(), - Some(stringify!(#outer_fn_name)) - ); - let mut #cx_varname_lock = #cx_varname.app.borrow_mut(); - )); - inner_fn_args.extend(quote!(&mut #cx_varname_lock,)); - cx_teardowns.extend(quote!( + } else if let Type::Reference(ty) = &*arg.ty + && let Type::Path(ty) = &*ty.elem + { + let last_segment = ty.path.segments.last(); + match last_segment.map(|s| s.ident.to_string()).as_deref() { + Some("App") => { + let cx_varname = format_ident!("cx_{}", ix); + let cx_varname_lock = format_ident!("cx_{}_lock", ix); + cx_vars.extend(quote!( + let mut #cx_varname = gpui::TestAppContext::build( + dispatcher.clone(), + Some(stringify!(#outer_fn_name)) + ); + let mut #cx_varname_lock = #cx_varname.app.borrow_mut(); + )); + inner_fn_args.extend(quote!(&mut #cx_varname_lock,)); + cx_teardowns.extend(quote!( drop(#cx_varname_lock); dispatcher.run_until_parked(); #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); }); dispatcher.run_until_parked(); )); - continue; - } - Some("TestAppContext") => { - let cx_varname = format_ident!("cx_{}", ix); - cx_vars.extend(quote!( - let mut #cx_varname = gpui::TestAppContext::build( - dispatcher.clone(), - Some(stringify!(#outer_fn_name)) - ); - )); - cx_teardowns.extend(quote!( - dispatcher.run_until_parked(); - #cx_varname.executor().forbid_parking(); - #cx_varname.quit(); - dispatcher.run_until_parked(); - )); - inner_fn_args.extend(quote!(&mut #cx_varname,)); - continue; - } - _ => {} + continue; + } + Some("TestAppContext") => { + let cx_varname = format_ident!("cx_{}", ix); + cx_vars.extend(quote!( + let mut #cx_varname = gpui::TestAppContext::build( + dispatcher.clone(), + Some(stringify!(#outer_fn_name)) + ); + )); + cx_teardowns.extend(quote!( + dispatcher.run_until_parked(); + #cx_varname.executor().forbid_parking(); + #cx_varname.quit(); + dispatcher.run_until_parked(); + )); + inner_fn_args.extend(quote!(&mut #cx_varname,)); + continue; } + _ => {} } } } diff --git a/crates/html_to_markdown/src/markdown.rs b/crates/html_to_markdown/src/markdown.rs index b9ffbac79c6b6af64222e6447392aa3a75440dda..bb3b3563bcdff8692c80b1b79e7c94d4184bf1cb 100644 --- a/crates/html_to_markdown/src/markdown.rs +++ b/crates/html_to_markdown/src/markdown.rs @@ -34,15 +34,14 @@ impl HandleTag for ParagraphHandler { tag: &HtmlElement, writer: &mut MarkdownWriter, ) -> StartTagOutcome { - if tag.is_inline() && writer.is_inside("p") { - if let Some(parent) = writer.current_element_stack().iter().last() { - if !(parent.is_inline() - || writer.markdown.ends_with(' ') - || writer.markdown.ends_with('\n')) - { - writer.push_str(" "); - } - } + if tag.is_inline() + && writer.is_inside("p") + && let Some(parent) = writer.current_element_stack().iter().last() + && !(parent.is_inline() + || writer.markdown.ends_with(' ') + || writer.markdown.ends_with('\n')) + { + writer.push_str(" "); } if tag.tag() == "p" { diff --git a/crates/http_client/src/github.rs b/crates/http_client/src/github.rs index 89309ff344c2a64127ee8b2603d10d029a82f6bf..32efed8e727330d3ac1c2fb6d8ea5d57fdd66dd4 100644 --- a/crates/http_client/src/github.rs +++ b/crates/http_client/src/github.rs @@ -77,10 +77,10 @@ pub async fn latest_github_release( .find(|release| release.pre_release == pre_release) .context("finding a prerelease")?; release.assets.iter_mut().for_each(|asset| { - if let Some(digest) = &mut asset.digest { - if let Some(stripped) = digest.strip_prefix("sha256:") { - *digest = stripped.to_owned(); - } + if let Some(digest) = &mut asset.digest + && let Some(stripped) = digest.strip_prefix("sha256:") + { + *digest = stripped.to_owned(); } }); Ok(release) diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 0335a746cd23eb2654dac7f8960a649aa3c269ff..53887eb7366c844796da5505923ba74fdfb0e4c7 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -170,23 +170,23 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap .await }; - if let Some(Some(Ok(item))) = opened.first() { - if let Some(editor) = item.downcast::<Editor>().map(|editor| editor.downgrade()) { - editor.update_in(cx, |editor, window, cx| { - let len = editor.buffer().read(cx).len(cx); - editor.change_selections( - SelectionEffects::scroll(Autoscroll::center()), - window, - cx, - |s| s.select_ranges([len..len]), - ); - if len > 0 { - editor.insert("\n\n", window, cx); - } - editor.insert(&entry_heading, window, cx); + if let Some(Some(Ok(item))) = opened.first() + && let Some(editor) = item.downcast::<Editor>().map(|editor| editor.downgrade()) + { + editor.update_in(cx, |editor, window, cx| { + let len = editor.buffer().read(cx).len(cx); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::center()), + window, + cx, + |s| s.select_ranges([len..len]), + ); + if len > 0 { editor.insert("\n\n", window, cx); - })?; - } + } + editor.insert(&entry_heading, window, cx); + editor.insert("\n\n", window, cx); + })?; } anyhow::Ok(()) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index abb8d3b151c471a658a00895408c37e9b2ab111d..9227d35a504458deeea4ab8b8030f9b247891742 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1158,13 +1158,12 @@ impl Buffer { base_buffer.edit(edits, None, cx) }); - if let Some(operation) = operation { - if let Some(BufferBranchState { + if let Some(operation) = operation + && let Some(BufferBranchState { merged_operations, .. }) = &mut self.branch_state - { - merged_operations.push(operation); - } + { + merged_operations.push(operation); } } @@ -1185,11 +1184,11 @@ impl Buffer { }; let mut operation_to_undo = None; - if let Operation::Buffer(text::Operation::Edit(operation)) = &operation { - if let Ok(ix) = merged_operations.binary_search(&operation.timestamp) { - merged_operations.remove(ix); - operation_to_undo = Some(operation.timestamp); - } + if let Operation::Buffer(text::Operation::Edit(operation)) = &operation + && let Ok(ix) = merged_operations.binary_search(&operation.timestamp) + { + merged_operations.remove(ix); + operation_to_undo = Some(operation.timestamp); } self.apply_ops([operation.clone()], cx); @@ -1424,10 +1423,10 @@ impl Buffer { .map(|info| info.language.clone()) .collect(); - if languages.is_empty() { - if let Some(buffer_language) = self.language() { - languages.push(buffer_language.clone()); - } + if languages.is_empty() + && let Some(buffer_language) = self.language() + { + languages.push(buffer_language.clone()); } languages @@ -2589,10 +2588,10 @@ impl Buffer { line_mode, cursor_shape, } => { - if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id) { - if set.lamport_timestamp > lamport_timestamp { - return; - } + if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id) + && set.lamport_timestamp > lamport_timestamp + { + return; } self.remote_selections.insert( @@ -3365,8 +3364,8 @@ impl BufferSnapshot { } } - if let Some(range) = range { - if smallest_range_and_depth.as_ref().map_or( + if let Some(range) = range + && smallest_range_and_depth.as_ref().map_or( true, |(smallest_range, smallest_range_depth)| { if layer.depth > *smallest_range_depth { @@ -3377,13 +3376,13 @@ impl BufferSnapshot { false } }, - ) { - smallest_range_and_depth = Some((range, layer.depth)); - scope = Some(LanguageScope { - language: layer.language.clone(), - override_id: layer.override_id(offset, &self.text), - }); - } + ) + { + smallest_range_and_depth = Some((range, layer.depth)); + scope = Some(LanguageScope { + language: layer.language.clone(), + override_id: layer.override_id(offset, &self.text), + }); } } @@ -3499,17 +3498,17 @@ impl BufferSnapshot { // If there is a candidate node on both sides of the (empty) range, then // decide between the two by favoring a named node over an anonymous token. // If both nodes are the same in that regard, favor the right one. - if let Some(right_node) = right_node { - if right_node.is_named() || !left_node.is_named() { - layer_result = right_node; - } + if let Some(right_node) = right_node + && (right_node.is_named() || !left_node.is_named()) + { + layer_result = right_node; } } - if let Some(previous_result) = &result { - if previous_result.byte_range().len() < layer_result.byte_range().len() { - continue; - } + if let Some(previous_result) = &result + && previous_result.byte_range().len() < layer_result.byte_range().len() + { + continue; } result = Some(layer_result); } @@ -4081,10 +4080,10 @@ impl BufferSnapshot { let mut result: Option<(Range<usize>, Range<usize>)> = None; for pair in self.enclosing_bracket_ranges(range.clone()) { - if let Some(range_filter) = range_filter { - if !range_filter(pair.open_range.clone(), pair.close_range.clone()) { - continue; - } + if let Some(range_filter) = range_filter + && !range_filter(pair.open_range.clone(), pair.close_range.clone()) + { + continue; } let len = pair.close_range.end - pair.open_range.start; @@ -4474,27 +4473,26 @@ impl BufferSnapshot { current_word_start_ix = Some(ix); } - if let Some(query_chars) = &query_chars { - if query_ix < query_len { - if c.to_lowercase().eq(query_chars[query_ix].to_lowercase()) { - query_ix += 1; - } - } + if let Some(query_chars) = &query_chars + && query_ix < query_len + && c.to_lowercase().eq(query_chars[query_ix].to_lowercase()) + { + query_ix += 1; } continue; - } else if let Some(word_start) = current_word_start_ix.take() { - if query_ix == query_len { - let word_range = self.anchor_before(word_start)..self.anchor_after(ix); - let mut word_text = self.text_for_range(word_start..ix).peekable(); - let first_char = word_text - .peek() - .and_then(|first_chunk| first_chunk.chars().next()); - // Skip empty and "words" starting with digits as a heuristic to reduce useless completions - if !query.skip_digits - || first_char.map_or(true, |first_char| !first_char.is_digit(10)) - { - words.insert(word_text.collect(), word_range); - } + } else if let Some(word_start) = current_word_start_ix.take() + && query_ix == query_len + { + let word_range = self.anchor_before(word_start)..self.anchor_after(ix); + let mut word_text = self.text_for_range(word_start..ix).peekable(); + let first_char = word_text + .peek() + .and_then(|first_chunk| first_chunk.chars().next()); + // Skip empty and "words" starting with digits as a heuristic to reduce useless completions + if !query.skip_digits + || first_char.map_or(true, |first_char| !first_char.is_digit(10)) + { + words.insert(word_text.collect(), word_range); } } query_ix = 0; @@ -4607,17 +4605,17 @@ impl<'a> BufferChunks<'a> { highlights .stack .retain(|(end_offset, _)| *end_offset > range.start); - if let Some(capture) = &highlights.next_capture { - if range.start >= capture.node.start_byte() { - let next_capture_end = capture.node.end_byte(); - if range.start < next_capture_end { - highlights.stack.push(( - next_capture_end, - highlights.highlight_maps[capture.grammar_index].get(capture.index), - )); - } - highlights.next_capture.take(); + if let Some(capture) = &highlights.next_capture + && range.start >= capture.node.start_byte() + { + let next_capture_end = capture.node.end_byte(); + if range.start < next_capture_end { + highlights.stack.push(( + next_capture_end, + highlights.highlight_maps[capture.grammar_index].get(capture.index), + )); } + highlights.next_capture.take(); } } else if let Some(snapshot) = self.buffer_snapshot { let (captures, highlight_maps) = snapshot.get_highlights(self.range.clone()); @@ -4642,33 +4640,33 @@ impl<'a> BufferChunks<'a> { } fn initialize_diagnostic_endpoints(&mut self) { - if let Some(diagnostics) = self.diagnostic_endpoints.as_mut() { - if let Some(buffer) = self.buffer_snapshot { - let mut diagnostic_endpoints = Vec::new(); - for entry in buffer.diagnostics_in_range::<_, usize>(self.range.clone(), false) { - diagnostic_endpoints.push(DiagnosticEndpoint { - offset: entry.range.start, - is_start: true, - severity: entry.diagnostic.severity, - is_unnecessary: entry.diagnostic.is_unnecessary, - underline: entry.diagnostic.underline, - }); - diagnostic_endpoints.push(DiagnosticEndpoint { - offset: entry.range.end, - is_start: false, - severity: entry.diagnostic.severity, - is_unnecessary: entry.diagnostic.is_unnecessary, - underline: entry.diagnostic.underline, - }); - } - diagnostic_endpoints - .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start)); - *diagnostics = diagnostic_endpoints.into_iter().peekable(); - self.hint_depth = 0; - self.error_depth = 0; - self.warning_depth = 0; - self.information_depth = 0; + if let Some(diagnostics) = self.diagnostic_endpoints.as_mut() + && let Some(buffer) = self.buffer_snapshot + { + let mut diagnostic_endpoints = Vec::new(); + for entry in buffer.diagnostics_in_range::<_, usize>(self.range.clone(), false) { + diagnostic_endpoints.push(DiagnosticEndpoint { + offset: entry.range.start, + is_start: true, + severity: entry.diagnostic.severity, + is_unnecessary: entry.diagnostic.is_unnecessary, + underline: entry.diagnostic.underline, + }); + diagnostic_endpoints.push(DiagnosticEndpoint { + offset: entry.range.end, + is_start: false, + severity: entry.diagnostic.severity, + is_unnecessary: entry.diagnostic.is_unnecessary, + underline: entry.diagnostic.underline, + }); } + diagnostic_endpoints + .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start)); + *diagnostics = diagnostic_endpoints.into_iter().peekable(); + self.hint_depth = 0; + self.error_depth = 0; + self.warning_depth = 0; + self.information_depth = 0; } } @@ -4779,11 +4777,11 @@ impl<'a> Iterator for BufferChunks<'a> { .min(next_capture_start) .min(next_diagnostic_endpoint); let mut highlight_id = None; - if let Some(highlights) = self.highlights.as_ref() { - if let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last() { - chunk_end = chunk_end.min(*parent_capture_end); - highlight_id = Some(*parent_highlight_id); - } + if let Some(highlights) = self.highlights.as_ref() + && let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last() + { + chunk_end = chunk_end.min(*parent_capture_end); + highlight_id = Some(*parent_highlight_id); } let slice = @@ -4977,11 +4975,12 @@ pub(crate) fn contiguous_ranges( std::iter::from_fn(move || { loop { if let Some(value) = values.next() { - if let Some(range) = &mut current_range { - if value == range.end && range.len() < max_len { - range.end += 1; - continue; - } + if let Some(range) = &mut current_range + && value == range.end + && range.len() < max_len + { + range.end += 1; + continue; } let prev_range = current_range.clone(); @@ -5049,10 +5048,10 @@ impl CharClassifier { } else { scope.word_characters() }; - if let Some(characters) = characters { - if characters.contains(&c) { - return CharKind::Word; - } + if let Some(characters) = characters + && characters.contains(&c) + { + return CharKind::Word; } } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 3a417331912707939572a91e00b982aa7064c75d..68addc804eddd009b9b0bc523b8df25daabdd6a0 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -329,8 +329,8 @@ pub trait LspAdapter: 'static + Send + Sync { // We only want to cache when we fall back to the global one, // because we don't want to download and overwrite our global one // for each worktree we might have open. - if binary_options.allow_path_lookup { - if let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await { + if binary_options.allow_path_lookup + && let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await { log::info!( "found user-installed language server for {}. path: {:?}, arguments: {:?}", self.name().0, @@ -339,7 +339,6 @@ pub trait LspAdapter: 'static + Send + Sync { ); return Ok(binary); } - } anyhow::ensure!(binary_options.allow_binary_download, "downloading language servers disabled"); @@ -1776,10 +1775,10 @@ impl Language { BufferChunks::new(text, range, Some((captures, highlight_maps)), false, None) { let end_offset = offset + chunk.text.len(); - if let Some(highlight_id) = chunk.syntax_highlight_id { - if !highlight_id.is_default() { - result.push((offset..end_offset, highlight_id)); - } + if let Some(highlight_id) = chunk.syntax_highlight_id + && !highlight_id.is_default() + { + result.push((offset..end_offset, highlight_id)); } offset = end_offset; } @@ -1796,11 +1795,11 @@ impl Language { } pub fn set_theme(&self, theme: &SyntaxTheme) { - if let Some(grammar) = self.grammar.as_ref() { - if let Some(highlights_query) = &grammar.highlights_query { - *grammar.highlight_map.lock() = - HighlightMap::new(highlights_query.capture_names(), theme); - } + if let Some(grammar) = self.grammar.as_ref() + && let Some(highlights_query) = &grammar.highlights_query + { + *grammar.highlight_map.lock() = + HighlightMap::new(highlights_query.capture_names(), theme); } } @@ -1920,11 +1919,11 @@ impl LanguageScope { .enumerate() .map(move |(ix, bracket)| { let mut is_enabled = true; - if let Some(next_disabled_ix) = disabled_ids.first() { - if ix == *next_disabled_ix as usize { - disabled_ids = &disabled_ids[1..]; - is_enabled = false; - } + if let Some(next_disabled_ix) = disabled_ids.first() + && ix == *next_disabled_ix as usize + { + disabled_ids = &disabled_ids[1..]; + is_enabled = false; } (bracket, is_enabled) }) diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 30bbc88f7e7240551ee9784da6a389b08c11b5f5..1e1060c843a444834ecd6bc39352b9415629b0b5 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -414,42 +414,42 @@ impl SyntaxSnapshot { .collect::<Vec<_>>(); self.reparse_with_ranges(text, root_language.clone(), edit_ranges, registry.as_ref()); - if let Some(registry) = registry { - if registry.version() != self.language_registry_version { - let mut resolved_injection_ranges = Vec::new(); - let mut cursor = self - .layers - .filter::<_, ()>(text, |summary| summary.contains_unknown_injections); - cursor.next(); - while let Some(layer) = cursor.item() { - let SyntaxLayerContent::Pending { language_name } = &layer.content else { - unreachable!() - }; - if registry - .language_for_name_or_extension(language_name) - .now_or_never() - .and_then(|language| language.ok()) - .is_some() - { - let range = layer.range.to_offset(text); - log::trace!("reparse range {range:?} for language {language_name:?}"); - resolved_injection_ranges.push(range); - } - - cursor.next(); - } - drop(cursor); - - if !resolved_injection_ranges.is_empty() { - self.reparse_with_ranges( - text, - root_language, - resolved_injection_ranges, - Some(®istry), - ); + if let Some(registry) = registry + && registry.version() != self.language_registry_version + { + let mut resolved_injection_ranges = Vec::new(); + let mut cursor = self + .layers + .filter::<_, ()>(text, |summary| summary.contains_unknown_injections); + cursor.next(); + while let Some(layer) = cursor.item() { + let SyntaxLayerContent::Pending { language_name } = &layer.content else { + unreachable!() + }; + if registry + .language_for_name_or_extension(language_name) + .now_or_never() + .and_then(|language| language.ok()) + .is_some() + { + let range = layer.range.to_offset(text); + log::trace!("reparse range {range:?} for language {language_name:?}"); + resolved_injection_ranges.push(range); } - self.language_registry_version = registry.version(); + + cursor.next(); + } + drop(cursor); + + if !resolved_injection_ranges.is_empty() { + self.reparse_with_ranges( + text, + root_language, + resolved_injection_ranges, + Some(®istry), + ); } + self.language_registry_version = registry.version(); } self.update_count += 1; @@ -1065,10 +1065,10 @@ impl<'a> SyntaxMapCaptures<'a> { pub fn set_byte_range(&mut self, range: Range<usize>) { for layer in &mut self.layers { layer.captures.set_byte_range(range.clone()); - if let Some(capture) = &layer.next_capture { - if capture.node.end_byte() > range.start { - continue; - } + if let Some(capture) = &layer.next_capture + && capture.node.end_byte() > range.start + { + continue; } layer.advance(); } @@ -1277,11 +1277,11 @@ fn join_ranges( (None, None) => break, }; - if let Some(last) = result.last_mut() { - if range.start <= last.end { - last.end = last.end.max(range.end); - continue; - } + if let Some(last) = result.last_mut() + && range.start <= last.end + { + last.end = last.end.max(range.end); + continue; } result.push(range); } @@ -1330,14 +1330,13 @@ fn get_injections( // if there currently no matches for that injection. combined_injection_ranges.clear(); for pattern in &config.patterns { - if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) { - if let Some(language) = language_registry + if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) + && let Some(language) = language_registry .language_for_name_or_extension(language_name) .now_or_never() .and_then(|language| language.ok()) - { - combined_injection_ranges.insert(language.id, (language, Vec::new())); - } + { + combined_injection_ranges.insert(language.id, (language, Vec::new())); } } @@ -1357,10 +1356,11 @@ fn get_injections( content_ranges.first().unwrap().start_byte..content_ranges.last().unwrap().end_byte; // Avoid duplicate matches if two changed ranges intersect the same injection. - if let Some((prev_pattern_ix, prev_range)) = &prev_match { - if mat.pattern_index == *prev_pattern_ix && content_range == *prev_range { - continue; - } + if let Some((prev_pattern_ix, prev_range)) = &prev_match + && mat.pattern_index == *prev_pattern_ix + && content_range == *prev_range + { + continue; } prev_match = Some((mat.pattern_index, content_range.clone())); diff --git a/crates/language/src/text_diff.rs b/crates/language/src/text_diff.rs index af8ce608818cf66dec6d6db7a7556636276ad9bc..1e3e12758dde70127a5006025cb8153e645fdb0a 100644 --- a/crates/language/src/text_diff.rs +++ b/crates/language/src/text_diff.rs @@ -189,11 +189,11 @@ fn tokenize(text: &str, language_scope: Option<LanguageScope>) -> impl Iterator< while let Some((ix, c)) = chars.next() { let mut token = None; let kind = classifier.kind(c); - if let Some((prev_char, prev_kind)) = prev { - if kind != prev_kind || (kind == CharKind::Punctuation && c != prev_char) { - token = Some(&text[start_ix..ix]); - start_ix = ix; - } + if let Some((prev_char, prev_kind)) = prev + && (kind != prev_kind || (kind == CharKind::Punctuation && c != prev_char)) + { + token = Some(&text[start_ix..ix]); + start_ix = ix; } prev = Some((c, kind)); if token.is_some() { diff --git a/crates/language_model/src/request.rs b/crates/language_model/src/request.rs index 8c2d169973729c7e3891f45548a71e5dc72a377d..1182e0f7a8f1952a62832970ca63f3684eea5b17 100644 --- a/crates/language_model/src/request.rs +++ b/crates/language_model/src/request.rs @@ -221,36 +221,33 @@ impl<'de> Deserialize<'de> for LanguageModelToolResultContent { // Accept wrapped text format: { "type": "text", "text": "..." } if let (Some(type_value), Some(text_value)) = (get_field(obj, "type"), get_field(obj, "text")) + && let Some(type_str) = type_value.as_str() + && type_str.to_lowercase() == "text" + && let Some(text) = text_value.as_str() { - if let Some(type_str) = type_value.as_str() { - if type_str.to_lowercase() == "text" { - if let Some(text) = text_value.as_str() { - return Ok(Self::Text(Arc::from(text))); - } - } - } + return Ok(Self::Text(Arc::from(text))); } // Check for wrapped Text variant: { "text": "..." } - if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "text") { - if obj.len() == 1 { - // Only one field, and it's "text" (case-insensitive) - if let Some(text) = value.as_str() { - return Ok(Self::Text(Arc::from(text))); - } + if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "text") + && obj.len() == 1 + { + // Only one field, and it's "text" (case-insensitive) + if let Some(text) = value.as_str() { + return Ok(Self::Text(Arc::from(text))); } } // Check for wrapped Image variant: { "image": { "source": "...", "size": ... } } - if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "image") { - if obj.len() == 1 { - // Only one field, and it's "image" (case-insensitive) - // Try to parse the nested image object - if let Some(image_obj) = value.as_object() { - if let Some(image) = LanguageModelImage::from_json(image_obj) { - return Ok(Self::Image(image)); - } - } + if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "image") + && obj.len() == 1 + { + // Only one field, and it's "image" (case-insensitive) + // Try to parse the nested image object + if let Some(image_obj) = value.as_object() + && let Some(image) = LanguageModelImage::from_json(image_obj) + { + return Ok(Self::Image(image)); } } diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 7ba56ec7755afd8ab5e2c8528435c14132fbd293..b16be36ea1baefbe3d54e395e21b5ae9b2cb7fb4 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -633,11 +633,11 @@ pub fn into_anthropic( Role::Assistant => anthropic::Role::Assistant, Role::System => unreachable!("System role should never occur here"), }; - if let Some(last_message) = new_messages.last_mut() { - if last_message.role == anthropic_role { - last_message.content.extend(anthropic_message_content); - continue; - } + if let Some(last_message) = new_messages.last_mut() + && last_message.role == anthropic_role + { + last_message.content.extend(anthropic_message_content); + continue; } // Mark the last segment of the message as cached diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index f33a00972ddf6593252eb6c71cc3d2c417298bb2..193d218094eed5a86d466a73701ffc00ff27b94e 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -412,10 +412,10 @@ impl BedrockModel { .region(Region::new(region)) .timeout_config(TimeoutConfig::disabled()); - if let Some(endpoint_url) = endpoint { - if !endpoint_url.is_empty() { - config_builder = config_builder.endpoint_url(endpoint_url); - } + if let Some(endpoint_url) = endpoint + && !endpoint_url.is_empty() + { + config_builder = config_builder.endpoint_url(endpoint_url); } match auth_method { @@ -728,11 +728,11 @@ pub fn into_bedrock( Role::Assistant => bedrock::BedrockRole::Assistant, Role::System => unreachable!("System role should never occur here"), }; - if let Some(last_message) = new_messages.last_mut() { - if last_message.role == bedrock_role { - last_message.content.extend(bedrock_message_content); - continue; - } + if let Some(last_message) = new_messages.last_mut() + && last_message.role == bedrock_role + { + last_message.content.extend(bedrock_message_content); + continue; } new_messages.push( BedrockMessage::builder() diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index f226d0c6a8eac5382e7ac38cccfaf83975dcd908..e99dadc28de9fa236318b46ecd609997a8405e4d 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -597,15 +597,13 @@ impl CloudLanguageModel { .headers() .get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME) .and_then(|resource| resource.to_str().ok()) - { - if let Some(plan) = response + && let Some(plan) = response .headers() .get(CURRENT_PLAN_HEADER_NAME) .and_then(|plan| plan.to_str().ok()) .and_then(|plan| cloud_llm_client::Plan::from_str(plan).ok()) - { - return Err(anyhow!(ModelRequestLimitReachedError { plan })); - } + { + return Err(anyhow!(ModelRequestLimitReachedError { plan })); } } else if status == StatusCode::PAYMENT_REQUIRED { return Err(anyhow!(PaymentRequiredError)); @@ -662,29 +660,29 @@ where impl From<ApiError> for LanguageModelCompletionError { fn from(error: ApiError) -> Self { - if let Ok(cloud_error) = serde_json::from_str::<CloudApiError>(&error.body) { - if cloud_error.code.starts_with("upstream_http_") { - let status = if let Some(status) = cloud_error.upstream_status { - status - } else if cloud_error.code.ends_with("_error") { - error.status - } else { - // If there's a status code in the code string (e.g. "upstream_http_429") - // then use that; otherwise, see if the JSON contains a status code. - cloud_error - .code - .strip_prefix("upstream_http_") - .and_then(|code_str| code_str.parse::<u16>().ok()) - .and_then(|code| StatusCode::from_u16(code).ok()) - .unwrap_or(error.status) - }; + if let Ok(cloud_error) = serde_json::from_str::<CloudApiError>(&error.body) + && cloud_error.code.starts_with("upstream_http_") + { + let status = if let Some(status) = cloud_error.upstream_status { + status + } else if cloud_error.code.ends_with("_error") { + error.status + } else { + // If there's a status code in the code string (e.g. "upstream_http_429") + // then use that; otherwise, see if the JSON contains a status code. + cloud_error + .code + .strip_prefix("upstream_http_") + .and_then(|code_str| code_str.parse::<u16>().ok()) + .and_then(|code| StatusCode::from_u16(code).ok()) + .unwrap_or(error.status) + }; - return LanguageModelCompletionError::UpstreamProviderError { - message: cloud_error.message, - status, - retry_after: cloud_error.retry_after.map(Duration::from_secs_f64), - }; - } + return LanguageModelCompletionError::UpstreamProviderError { + message: cloud_error.message, + status, + retry_after: cloud_error.retry_after.map(Duration::from_secs_f64), + }; } let retry_after = None; diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index c5c5eceab54f2c34f4b1e2aae1b04f85fc5d9ab6..56924c4cd2d54c64436a5ccaa7dabfe4c53ff0ec 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -28,10 +28,10 @@ impl ActiveBufferLanguage { self.active_language = Some(None); let editor = editor.read(cx); - if let Some((_, buffer, _)) = editor.active_excerpt(cx) { - if let Some(language) = buffer.read(cx).language() { - self.active_language = Some(Some(language.name())); - } + if let Some((_, buffer, _)) = editor.active_excerpt(cx) + && let Some(language) = buffer.read(cx).language() + { + self.active_language = Some(Some(language.name())); } cx.notify(); diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index c303a8c305af4f09c535b40fc30c0a185efab3da..3285efaaef53d6f31a8db5b3e18240db9027d905 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -254,35 +254,35 @@ impl LogStore { let copilot_subscription = Copilot::global(cx).map(|copilot| { let copilot = &copilot; 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(); - this.copilot_log_subscription = - Some(server.on_notification::<copilot::request::LogMessage, _>( - move |params, cx| { - weak_this - .update(cx, |this, cx| { - this.add_language_server_log( - server_id, - MessageType::LOG, - ¶ms.message, - cx, - ); - }) - .ok(); - }, - )); - let name = LanguageServerName::new_static("copilot"); - this.add_language_server( - LanguageServerKind::Global, - server.server_id(), - Some(name), - None, - Some(server.clone()), - cx, - ); - } + if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event + && let Some(server) = copilot.read(cx).language_server() + { + let server_id = server.server_id(); + let weak_this = cx.weak_entity(); + this.copilot_log_subscription = + Some(server.on_notification::<copilot::request::LogMessage, _>( + move |params, cx| { + weak_this + .update(cx, |this, cx| { + this.add_language_server_log( + server_id, + MessageType::LOG, + ¶ms.message, + cx, + ); + }) + .ok(); + }, + )); + let name = LanguageServerName::new_static("copilot"); + this.add_language_server( + LanguageServerKind::Global, + server.server_id(), + Some(name), + None, + Some(server.clone()), + cx, + ); } }) }); @@ -733,16 +733,14 @@ impl LspLogView { let first_server_id_for_project = store.read(cx).server_ids_for_project(&weak_project).next(); if let Some(current_lsp) = this.current_server_id { - if !store.read(cx).language_servers.contains_key(¤t_lsp) { - if let Some(server_id) = first_server_id_for_project { - match this.active_entry_kind { - LogKind::Rpc => { - this.show_rpc_trace_for_server(server_id, window, cx) - } - LogKind::Trace => this.show_trace_for_server(server_id, window, cx), - LogKind::Logs => this.show_logs_for_server(server_id, window, cx), - LogKind::ServerInfo => this.show_server_info(server_id, window, cx), - } + if !store.read(cx).language_servers.contains_key(¤t_lsp) + && let Some(server_id) = first_server_id_for_project + { + match this.active_entry_kind { + LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx), + LogKind::Trace => this.show_trace_for_server(server_id, window, cx), + LogKind::Logs => this.show_logs_for_server(server_id, window, cx), + LogKind::ServerInfo => this.show_server_info(server_id, window, cx), } } } else if let Some(server_id) = first_server_id_for_project { @@ -776,21 +774,17 @@ impl LspLogView { ], cx, ); - if text.len() > 1024 { - if let Some((fold_offset, _)) = + if text.len() > 1024 + && let Some((fold_offset, _)) = text.char_indices().dropping(1024).next() - { - if fold_offset < text.len() { - editor.fold_ranges( - vec![ - last_offset + fold_offset..last_offset + text.len(), - ], - false, - window, - cx, - ); - } - } + && fold_offset < text.len() + { + editor.fold_ranges( + vec![last_offset + fold_offset..last_offset + text.len()], + false, + window, + cx, + ); } if newest_cursor_is_at_end { @@ -1311,14 +1305,14 @@ impl ToolbarItemView for LspLogToolbarItemView { _: &mut Window, cx: &mut Context<Self>, ) -> workspace::ToolbarItemLocation { - if let Some(item) = active_pane_item { - if let Some(log_view) = item.downcast::<LspLogView>() { - self.log_view = Some(log_view.clone()); - self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| { - cx.notify(); - })); - return ToolbarItemLocation::PrimaryLeft; - } + if let Some(item) = active_pane_item + && let Some(log_view) = item.downcast::<LspLogView>() + { + self.log_view = Some(log_view.clone()); + self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| { + cx.notify(); + })); + return ToolbarItemLocation::PrimaryLeft; } self.log_view = None; self._log_view_subscription = None; diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 9946442ec88bf5aa2d1c1d5678ab39c08144591f..4fe8e11f94b7623882b4e5dbe79b80ed060a99c8 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -103,12 +103,11 @@ impl SyntaxTreeView { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(item) = active_item { - if item.item_id() != cx.entity_id() { - if let Some(editor) = item.act_as::<Editor>(cx) { - self.set_editor(editor, window, cx); - } - } + if let Some(item) = active_item + && item.item_id() != cx.entity_id() + && let Some(editor) = item.act_as::<Editor>(cx) + { + self.set_editor(editor, window, cx); } } @@ -537,12 +536,12 @@ impl ToolbarItemView for SyntaxTreeToolbarItemView { window: &mut Window, cx: &mut Context<Self>, ) -> ToolbarItemLocation { - if let Some(item) = active_pane_item { - if let Some(view) = item.downcast::<SyntaxTreeView>() { - self.tree_view = Some(view.clone()); - self.subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify())); - return ToolbarItemLocation::PrimaryLeft; - } + if let Some(item) = active_pane_item + && let Some(view) = item.downcast::<SyntaxTreeView>() + { + self.tree_view = Some(view.clone()); + self.subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify())); + return ToolbarItemLocation::PrimaryLeft; } self.tree_view = None; self.subscription = None; diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index f739c5c4c696c52db338c6cea3cf036a4f245be7..00e3cad4360f72b1e428144dcb99905e7be4fdb4 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -131,19 +131,19 @@ impl super::LspAdapter for GoLspAdapter { if let Some(version) = *version { let binary_path = container_dir.join(format!("gopls_{version}_go_{go_version}")); - if let Ok(metadata) = fs::metadata(&binary_path).await { - if metadata.is_file() { - remove_matching(&container_dir, |entry| { - entry != binary_path && entry.file_name() != Some(OsStr::new("gobin")) - }) - .await; + if let Ok(metadata) = fs::metadata(&binary_path).await + && metadata.is_file() + { + remove_matching(&container_dir, |entry| { + entry != binary_path && entry.file_name() != Some(OsStr::new("gobin")) + }) + .await; - return Ok(LanguageServerBinary { - path: binary_path.to_path_buf(), - arguments: server_binary_arguments(), - env: None, - }); - } + return Ok(LanguageServerBinary { + path: binary_path.to_path_buf(), + arguments: server_binary_arguments(), + env: None, + }); } } else if let Some(path) = this .cached_server_binary(container_dir.clone(), delegate) diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index e446f22713d860d9574303d91579c305bcc44391..75289dd59daa85de5c0c6ab3e7ec71736212a5ae 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -244,11 +244,8 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { cx.observe_flag::<BasedPyrightFeatureFlag, _>({ let languages = languages.clone(); move |enabled, _| { - if enabled { - if let Some(adapter) = basedpyright_lsp_adapter.take() { - languages - .register_available_lsp_adapter(adapter.name(), move || adapter.clone()); - } + if enabled && let Some(adapter) = basedpyright_lsp_adapter.take() { + languages.register_available_lsp_adapter(adapter.name(), move || adapter.clone()); } } }) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 17d0d98fad07de0b21d5b950fdb8f56cf0a59cc3..89a091797e49103dc4538f8d8a82d560b96fc637 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -338,31 +338,31 @@ impl LspAdapter for PythonLspAdapter { let interpreter_path = toolchain.path.to_string(); // Detect if this is a virtual environment - if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() { - if let Some(venv_dir) = interpreter_dir.parent() { - // Check if this looks like a virtual environment - if venv_dir.join("pyvenv.cfg").exists() - || venv_dir.join("bin/activate").exists() - || venv_dir.join("Scripts/activate.bat").exists() - { - // Set venvPath and venv at the root level - // This matches the format of a pyrightconfig.json file - if let Some(parent) = venv_dir.parent() { - // Use relative path if the venv is inside the workspace - let venv_path = if parent == adapter.worktree_root_path() { - ".".to_string() - } else { - parent.to_string_lossy().into_owned() - }; - object.insert("venvPath".to_string(), Value::String(venv_path)); - } + if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() + && let Some(venv_dir) = interpreter_dir.parent() + { + // Check if this looks like a virtual environment + if venv_dir.join("pyvenv.cfg").exists() + || venv_dir.join("bin/activate").exists() + || venv_dir.join("Scripts/activate.bat").exists() + { + // Set venvPath and venv at the root level + // This matches the format of a pyrightconfig.json file + if let Some(parent) = venv_dir.parent() { + // Use relative path if the venv is inside the workspace + let venv_path = if parent == adapter.worktree_root_path() { + ".".to_string() + } else { + parent.to_string_lossy().into_owned() + }; + object.insert("venvPath".to_string(), Value::String(venv_path)); + } - if let Some(venv_name) = venv_dir.file_name() { - object.insert( - "venv".to_owned(), - Value::String(venv_name.to_string_lossy().into_owned()), - ); - } + if let Some(venv_name) = venv_dir.file_name() { + object.insert( + "venv".to_owned(), + Value::String(venv_name.to_string_lossy().into_owned()), + ); } } } @@ -1519,31 +1519,31 @@ impl LspAdapter for BasedPyrightLspAdapter { let interpreter_path = toolchain.path.to_string(); // Detect if this is a virtual environment - if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() { - if let Some(venv_dir) = interpreter_dir.parent() { - // Check if this looks like a virtual environment - if venv_dir.join("pyvenv.cfg").exists() - || venv_dir.join("bin/activate").exists() - || venv_dir.join("Scripts/activate.bat").exists() - { - // Set venvPath and venv at the root level - // This matches the format of a pyrightconfig.json file - if let Some(parent) = venv_dir.parent() { - // Use relative path if the venv is inside the workspace - let venv_path = if parent == adapter.worktree_root_path() { - ".".to_string() - } else { - parent.to_string_lossy().into_owned() - }; - object.insert("venvPath".to_string(), Value::String(venv_path)); - } + if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() + && let Some(venv_dir) = interpreter_dir.parent() + { + // Check if this looks like a virtual environment + if venv_dir.join("pyvenv.cfg").exists() + || venv_dir.join("bin/activate").exists() + || venv_dir.join("Scripts/activate.bat").exists() + { + // Set venvPath and venv at the root level + // This matches the format of a pyrightconfig.json file + if let Some(parent) = venv_dir.parent() { + // Use relative path if the venv is inside the workspace + let venv_path = if parent == adapter.worktree_root_path() { + ".".to_string() + } else { + parent.to_string_lossy().into_owned() + }; + object.insert("venvPath".to_string(), Value::String(venv_path)); + } - if let Some(venv_name) = venv_dir.file_name() { - object.insert( - "venv".to_owned(), - Value::String(venv_name.to_string_lossy().into_owned()), - ); - } + if let Some(venv_name) = venv_dir.file_name() { + object.insert( + "venv".to_owned(), + Value::String(venv_name.to_string_lossy().into_owned()), + ); } } } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index bbdfcdb4990d0ab02aadfa976669639c69615d64..f9b23ed9f4e4ea0f157c65f20afd710c4a91199b 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -598,12 +598,10 @@ impl ContextProvider for RustContextProvider { if let Some(path) = local_abs_path .as_deref() .and_then(|local_abs_path| local_abs_path.parent()) - { - if let Some(package_name) = + && let Some(package_name) = human_readable_package_name(path, project_env.as_ref()).await - { - variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name); - } + { + variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name); } if let Some(path) = local_abs_path.as_ref() && let Some((target, manifest_path)) = diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 7937adbc09925176256044dbe766e159400b4b2c..afc84c3affb9964cd2cf00c9a15e3ca8ef6d68e4 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -341,10 +341,10 @@ async fn detect_package_manager( fs: Arc<dyn Fs>, package_json_data: Option<PackageJsonData>, ) -> &'static str { - if let Some(package_json_data) = package_json_data { - if let Some(package_manager) = package_json_data.package_manager { - return package_manager; - } + if let Some(package_json_data) = package_json_data + && let Some(package_manager) = package_json_data.package_manager + { + return package_manager; } if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await { return "pnpm"; diff --git a/crates/livekit_client/src/test.rs b/crates/livekit_client/src/test.rs index e0058d1163d9d0a66d598ed3e09c9dcda221a90b..873e0222d013c20c5f4aff2a263b925c7d21aff6 100644 --- a/crates/livekit_client/src/test.rs +++ b/crates/livekit_client/src/test.rs @@ -736,14 +736,14 @@ impl Room { impl Drop for RoomState { fn drop(&mut self) { - if self.connection_state == ConnectionState::Connected { - if let Ok(server) = TestServer::get(&self.url) { - let executor = server.executor.clone(); - let token = self.token.clone(); - executor - .spawn(async move { server.leave_room(token).await.ok() }) - .detach(); - } + if self.connection_state == ConnectionState::Connected + && let Ok(server) = TestServer::get(&self.url) + { + let executor = server.executor.clone(); + let token = self.token.clone(); + executor + .spawn(async move { server.leave_room(token).await.ok() }) + .detach(); } } } diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index e5709bc07c5aaf76800226fcc6594b33031e17bf..7939e97e48117ce7b23834697007e27b5f79fcc6 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -340,27 +340,26 @@ impl Markdown { } for (range, event) in &events { - if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event { - if let Some(data_url) = dest_url.strip_prefix("data:") { - let Some((mime_info, data)) = data_url.split_once(',') else { - continue; - }; - let Some((mime_type, encoding)) = mime_info.split_once(';') else { - continue; - }; - let Some(format) = ImageFormat::from_mime_type(mime_type) else { - continue; - }; - let is_base64 = encoding == "base64"; - if is_base64 { - if let Some(bytes) = base64::prelude::BASE64_STANDARD - .decode(data) - .log_with_level(Level::Debug) - { - let image = Arc::new(Image::from_bytes(format, bytes)); - images_by_source_offset.insert(range.start, image); - } - } + if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event + && let Some(data_url) = dest_url.strip_prefix("data:") + { + let Some((mime_info, data)) = data_url.split_once(',') else { + continue; + }; + let Some((mime_type, encoding)) = mime_info.split_once(';') else { + continue; + }; + let Some(format) = ImageFormat::from_mime_type(mime_type) else { + continue; + }; + let is_base64 = encoding == "base64"; + if is_base64 + && let Some(bytes) = base64::prelude::BASE64_STANDARD + .decode(data) + .log_with_level(Level::Debug) + { + let image = Arc::new(Image::from_bytes(format, bytes)); + images_by_source_offset.insert(range.start, image); } } } @@ -659,13 +658,13 @@ impl MarkdownElement { let rendered_text = rendered_text.clone(); move |markdown, event: &MouseUpEvent, phase, window, cx| { if phase.bubble() { - if let Some(pressed_link) = markdown.pressed_link.take() { - if Some(&pressed_link) == rendered_text.link_for_position(event.position) { - if let Some(open_url) = on_open_url.as_ref() { - open_url(pressed_link.destination_url, window, cx); - } else { - cx.open_url(&pressed_link.destination_url); - } + if let Some(pressed_link) = markdown.pressed_link.take() + && Some(&pressed_link) == rendered_text.link_for_position(event.position) + { + if let Some(open_url) = on_open_url.as_ref() { + open_url(pressed_link.destination_url, window, cx); + } else { + cx.open_url(&pressed_link.destination_url); } } } else if markdown.selection.pending { @@ -758,10 +757,10 @@ impl Element for MarkdownElement { let mut current_img_block_range: Option<Range<usize>> = None; for (range, event) in parsed_markdown.events.iter() { // Skip alt text for images that rendered - if let Some(current_img_block_range) = ¤t_img_block_range { - if current_img_block_range.end > range.end { - continue; - } + if let Some(current_img_block_range) = ¤t_img_block_range + && current_img_block_range.end > range.end + { + continue; } match event { @@ -1696,10 +1695,10 @@ impl RenderedText { while let Some(line) = lines.next() { let line_bounds = line.layout.bounds(); if position.y > line_bounds.bottom() { - if let Some(next_line) = lines.peek() { - if position.y < next_line.layout.bounds().top() { - return Err(line.source_end); - } + if let Some(next_line) = lines.peek() + && position.y < next_line.layout.bounds().top() + { + return Err(line.source_end); } continue; diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 27691f2ecffadb7a7df1e9647e7d1d6487135974..890d564b7a1f4a66f8a587abdaae265d6b10e03b 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -300,13 +300,12 @@ impl<'a> MarkdownParser<'a> { if style != MarkdownHighlightStyle::default() && last_run_len < text.len() { let mut new_highlight = true; - if let Some((last_range, last_style)) = highlights.last_mut() { - if last_range.end == last_run_len - && last_style == &MarkdownHighlight::Style(style.clone()) - { - last_range.end = text.len(); - new_highlight = false; - } + if let Some((last_range, last_style)) = highlights.last_mut() + && last_range.end == last_run_len + && last_style == &MarkdownHighlight::Style(style.clone()) + { + last_range.end = text.len(); + new_highlight = false; } if new_highlight { highlights.push(( @@ -579,10 +578,10 @@ impl<'a> MarkdownParser<'a> { } } else { let block = self.parse_block().await; - if let Some(block) = block { - if let Some(list_item) = items_stack.last_mut() { - list_item.content.extend(block); - } + if let Some(block) = block + && let Some(list_item) = items_stack.last_mut() + { + list_item.content.extend(block); } } } diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index a0c8819991d68336a306af85a4dd709353222fa1..c2b98f69c853798c09376c66294835f7b3e2c30d 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -151,10 +151,9 @@ impl MarkdownPreviewView { if let Some(editor) = workspace .active_item(cx) .and_then(|item| item.act_as::<Editor>(cx)) + && Self::is_markdown_file(&editor, cx) { - if Self::is_markdown_file(&editor, cx) { - return Some(editor); - } + return Some(editor); } None } @@ -243,32 +242,30 @@ impl MarkdownPreviewView { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(item) = active_item { - if item.item_id() != cx.entity_id() { - if let Some(editor) = item.act_as::<Editor>(cx) { - if Self::is_markdown_file(&editor, cx) { - self.set_editor(editor, window, cx); - } - } - } + if let Some(item) = active_item + && item.item_id() != cx.entity_id() + && let Some(editor) = item.act_as::<Editor>(cx) + && Self::is_markdown_file(&editor, cx) + { + self.set_editor(editor, window, cx); } } pub fn is_markdown_file<V>(editor: &Entity<Editor>, cx: &mut Context<V>) -> bool { let buffer = editor.read(cx).buffer().read(cx); - if let Some(buffer) = buffer.as_singleton() { - if let Some(language) = buffer.read(cx).language() { - return language.name() == "Markdown".into(); - } + if let Some(buffer) = buffer.as_singleton() + && let Some(language) = buffer.read(cx).language() + { + return language.name() == "Markdown".into(); } false } fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) { - if let Some(active) = &self.active_editor { - if active.editor == editor { - return; - } + if let Some(active) = &self.active_editor + && active.editor == editor + { + return; } let subscription = cx.subscribe_in( @@ -552,21 +549,20 @@ impl Render for MarkdownPreviewView { .group("markdown-block") .on_click(cx.listener( move |this, event: &ClickEvent, window, cx| { - if event.click_count() == 2 { - if let Some(source_range) = this + if event.click_count() == 2 + && let Some(source_range) = this .contents .as_ref() .and_then(|c| c.children.get(ix)) .and_then(|block: &ParsedMarkdownElement| { block.source_range() }) - { - this.move_cursor_to_block( - window, - cx, - source_range.start..source_range.start, - ); - } + { + this.move_cursor_to_block( + window, + cx, + source_range.start..source_range.start, + ); } }, )) diff --git a/crates/migrator/src/migrations/m_2025_06_16/settings.rs b/crates/migrator/src/migrations/m_2025_06_16/settings.rs index cce407e21b81bf9064c1261c142b216b622712a8..cd79eae2048ca9809b720b7913eba12b3e6cb1ce 100644 --- a/crates/migrator/src/migrations/m_2025_06_16/settings.rs +++ b/crates/migrator/src/migrations/m_2025_06_16/settings.rs @@ -40,20 +40,20 @@ fn migrate_context_server_settings( // Parse the server settings to check what keys it contains let mut cursor = server_settings.walk(); for child in server_settings.children(&mut cursor) { - if child.kind() == "pair" { - if let Some(key_node) = child.child_by_field_name("key") { - if let (None, Some(quote_content)) = (column, key_node.child(0)) { - column = Some(quote_content.start_position().column); - } - if let Some(string_content) = key_node.child(1) { - let key = &contents[string_content.byte_range()]; - match key { - // If it already has a source key, don't modify it - "source" => return None, - "command" => has_command = true, - "settings" => has_settings = true, - _ => other_keys += 1, - } + if child.kind() == "pair" + && let Some(key_node) = child.child_by_field_name("key") + { + if let (None, Some(quote_content)) = (column, key_node.child(0)) { + column = Some(quote_content.start_position().column); + } + if let Some(string_content) = key_node.child(1) { + let key = &contents[string_content.byte_range()]; + match key { + // If it already has a source key, don't modify it + "source" => return None, + "command" => has_command = true, + "settings" => has_settings = true, + _ => other_keys += 1, } } } diff --git a/crates/migrator/src/migrations/m_2025_06_25/settings.rs b/crates/migrator/src/migrations/m_2025_06_25/settings.rs index 5dd6c3093a43b00acff3db6c1e316a3fc6664175..2bf7658eeb9036c0b1d08d2af446c0aba788d402 100644 --- a/crates/migrator/src/migrations/m_2025_06_25/settings.rs +++ b/crates/migrator/src/migrations/m_2025_06_25/settings.rs @@ -84,10 +84,10 @@ fn remove_pair_with_whitespace( } } else { // If no next sibling, check if there's a comma before - if let Some(prev_sibling) = pair_node.prev_sibling() { - if prev_sibling.kind() == "," { - range_to_remove.start = prev_sibling.start_byte(); - } + if let Some(prev_sibling) = pair_node.prev_sibling() + && prev_sibling.kind() == "," + { + range_to_remove.start = prev_sibling.start_byte(); } } @@ -123,10 +123,10 @@ fn remove_pair_with_whitespace( // Also check if we need to include trailing whitespace up to the next line let text_after = &contents[range_to_remove.end..]; - if let Some(newline_pos) = text_after.find('\n') { - if text_after[..newline_pos].chars().all(|c| c.is_whitespace()) { - range_to_remove.end += newline_pos + 1; - } + if let Some(newline_pos) = text_after.find('\n') + && text_after[..newline_pos].chars().all(|c| c.is_whitespace()) + { + range_to_remove.end += newline_pos + 1; } Some((range_to_remove, String::new())) diff --git a/crates/migrator/src/migrations/m_2025_06_27/settings.rs b/crates/migrator/src/migrations/m_2025_06_27/settings.rs index 6156308fcec05dfb10b5b258d31077e5d4b09adc..e3e951b1a69e39d19e93a152a264750caf51a81e 100644 --- a/crates/migrator/src/migrations/m_2025_06_27/settings.rs +++ b/crates/migrator/src/migrations/m_2025_06_27/settings.rs @@ -56,19 +56,18 @@ fn flatten_context_server_command( let mut cursor = command_object.walk(); for child in command_object.children(&mut cursor) { - if child.kind() == "pair" { - if let Some(key_node) = child.child_by_field_name("key") { - if let Some(string_content) = key_node.child(1) { - let key = &contents[string_content.byte_range()]; - if let Some(value_node) = child.child_by_field_name("value") { - let value_range = value_node.byte_range(); - match key { - "path" => path_value = Some(&contents[value_range]), - "args" => args_value = Some(&contents[value_range]), - "env" => env_value = Some(&contents[value_range]), - _ => {} - } - } + if child.kind() == "pair" + && let Some(key_node) = child.child_by_field_name("key") + && let Some(string_content) = key_node.child(1) + { + let key = &contents[string_content.byte_range()]; + if let Some(value_node) = child.child_by_field_name("value") { + let value_range = value_node.byte_range(); + match key { + "path" => path_value = Some(&contents[value_range]), + "args" => args_value = Some(&contents[value_range]), + "env" => env_value = Some(&contents[value_range]), + _ => {} } } } diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs index 8584519d56dfd49f2a8e43eaea32c11d38d2c25c..6bed0a4028c5c4b0816355a397046560fb6b8618 100644 --- a/crates/multi_buffer/src/anchor.rs +++ b/crates/multi_buffer/src/anchor.rs @@ -76,27 +76,26 @@ impl Anchor { if text_cmp.is_ne() { return text_cmp; } - if self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some() { - if let Some(base_text) = snapshot + if (self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some()) + && let Some(base_text) = snapshot .diffs .get(&excerpt.buffer_id) .map(|diff| diff.base_text()) - { - let self_anchor = self.diff_base_anchor.filter(|a| base_text.can_resolve(a)); - let other_anchor = other.diff_base_anchor.filter(|a| base_text.can_resolve(a)); - return match (self_anchor, other_anchor) { - (Some(a), Some(b)) => a.cmp(&b, base_text), - (Some(_), None) => match other.text_anchor.bias { - Bias::Left => Ordering::Greater, - Bias::Right => Ordering::Less, - }, - (None, Some(_)) => match self.text_anchor.bias { - Bias::Left => Ordering::Less, - Bias::Right => Ordering::Greater, - }, - (None, None) => Ordering::Equal, - }; - } + { + let self_anchor = self.diff_base_anchor.filter(|a| base_text.can_resolve(a)); + let other_anchor = other.diff_base_anchor.filter(|a| base_text.can_resolve(a)); + return match (self_anchor, other_anchor) { + (Some(a), Some(b)) => a.cmp(&b, base_text), + (Some(_), None) => match other.text_anchor.bias { + Bias::Left => Ordering::Greater, + Bias::Right => Ordering::Less, + }, + (None, Some(_)) => match self.text_anchor.bias { + Bias::Left => Ordering::Less, + Bias::Right => Ordering::Greater, + }, + (None, None) => Ordering::Equal, + }; } } Ordering::Equal @@ -107,51 +106,49 @@ impl Anchor { } pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor { - if self.text_anchor.bias != Bias::Left { - if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { - return Self { - buffer_id: self.buffer_id, - excerpt_id: self.excerpt_id, - text_anchor: self.text_anchor.bias_left(&excerpt.buffer), - diff_base_anchor: self.diff_base_anchor.map(|a| { - if let Some(base_text) = snapshot - .diffs - .get(&excerpt.buffer_id) - .map(|diff| diff.base_text()) - { - if a.buffer_id == Some(base_text.remote_id()) { - return a.bias_left(base_text); - } - } - a - }), - }; - } + if self.text_anchor.bias != Bias::Left + && let Some(excerpt) = snapshot.excerpt(self.excerpt_id) + { + return Self { + buffer_id: self.buffer_id, + excerpt_id: self.excerpt_id, + text_anchor: self.text_anchor.bias_left(&excerpt.buffer), + diff_base_anchor: self.diff_base_anchor.map(|a| { + if let Some(base_text) = snapshot + .diffs + .get(&excerpt.buffer_id) + .map(|diff| diff.base_text()) + && a.buffer_id == Some(base_text.remote_id()) + { + return a.bias_left(base_text); + } + a + }), + }; } *self } pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor { - if self.text_anchor.bias != Bias::Right { - if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { - return Self { - buffer_id: self.buffer_id, - excerpt_id: self.excerpt_id, - text_anchor: self.text_anchor.bias_right(&excerpt.buffer), - diff_base_anchor: self.diff_base_anchor.map(|a| { - if let Some(base_text) = snapshot - .diffs - .get(&excerpt.buffer_id) - .map(|diff| diff.base_text()) - { - if a.buffer_id == Some(base_text.remote_id()) { - return a.bias_right(base_text); - } - } - a - }), - }; - } + if self.text_anchor.bias != Bias::Right + && let Some(excerpt) = snapshot.excerpt(self.excerpt_id) + { + return Self { + buffer_id: self.buffer_id, + excerpt_id: self.excerpt_id, + text_anchor: self.text_anchor.bias_right(&excerpt.buffer), + diff_base_anchor: self.diff_base_anchor.map(|a| { + if let Some(base_text) = snapshot + .diffs + .get(&excerpt.buffer_id) + .map(|diff| diff.base_text()) + && a.buffer_id == Some(base_text.remote_id()) + { + return a.bias_right(base_text); + } + a + }), + }; } *self } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 59eaa9934dc5432418c5758dbeb700d4658fdc93..ab5f148d6cad0cc2cfa3a42f8888ad2cd350315f 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1082,11 +1082,11 @@ impl MultiBuffer { let mut ranges: Vec<Range<usize>> = Vec::new(); for edit in edits { - if let Some(last_range) = ranges.last_mut() { - if edit.range.start <= last_range.end { - last_range.end = last_range.end.max(edit.range.end); - continue; - } + if let Some(last_range) = ranges.last_mut() + && edit.range.start <= last_range.end + { + last_range.end = last_range.end.max(edit.range.end); + continue; } ranges.push(edit.range); } @@ -1212,25 +1212,24 @@ impl MultiBuffer { for range in buffer.edited_ranges_for_transaction_id::<D>(*buffer_transaction) { for excerpt_id in &buffer_state.excerpts { cursor.seek(excerpt_id, Bias::Left); - if let Some(excerpt) = cursor.item() { - if excerpt.locator == *excerpt_id { - let excerpt_buffer_start = - excerpt.range.context.start.summary::<D>(buffer); - let excerpt_buffer_end = excerpt.range.context.end.summary::<D>(buffer); - let excerpt_range = excerpt_buffer_start..excerpt_buffer_end; - if excerpt_range.contains(&range.start) - && excerpt_range.contains(&range.end) - { - let excerpt_start = D::from_text_summary(&cursor.start().text); + if let Some(excerpt) = cursor.item() + && excerpt.locator == *excerpt_id + { + let excerpt_buffer_start = excerpt.range.context.start.summary::<D>(buffer); + let excerpt_buffer_end = excerpt.range.context.end.summary::<D>(buffer); + let excerpt_range = excerpt_buffer_start..excerpt_buffer_end; + if excerpt_range.contains(&range.start) + && excerpt_range.contains(&range.end) + { + let excerpt_start = D::from_text_summary(&cursor.start().text); - let mut start = excerpt_start; - start.add_assign(&(range.start - excerpt_buffer_start)); - let mut end = excerpt_start; - end.add_assign(&(range.end - excerpt_buffer_start)); + let mut start = excerpt_start; + start.add_assign(&(range.start - excerpt_buffer_start)); + let mut end = excerpt_start; + end.add_assign(&(range.end - excerpt_buffer_start)); - ranges.push(start..end); - break; - } + ranges.push(start..end); + break; } } } @@ -1251,25 +1250,25 @@ impl MultiBuffer { buffer.update(cx, |buffer, _| { buffer.merge_transactions(transaction, destination) }); - } else if let Some(transaction) = self.history.forget(transaction) { - if let Some(destination) = self.history.transaction_mut(destination) { - for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { - if let Some(destination_buffer_transaction_id) = - destination.buffer_transactions.get(&buffer_id) - { - if let Some(state) = self.buffers.borrow().get(&buffer_id) { - state.buffer.update(cx, |buffer, _| { - buffer.merge_transactions( - buffer_transaction_id, - *destination_buffer_transaction_id, - ) - }); - } - } else { - destination - .buffer_transactions - .insert(buffer_id, buffer_transaction_id); + } else if let Some(transaction) = self.history.forget(transaction) + && let Some(destination) = self.history.transaction_mut(destination) + { + for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { + if let Some(destination_buffer_transaction_id) = + destination.buffer_transactions.get(&buffer_id) + { + if let Some(state) = self.buffers.borrow().get(&buffer_id) { + state.buffer.update(cx, |buffer, _| { + buffer.merge_transactions( + buffer_transaction_id, + *destination_buffer_transaction_id, + ) + }); } + } else { + destination + .buffer_transactions + .insert(buffer_id, buffer_transaction_id); } } } @@ -1562,11 +1561,11 @@ impl MultiBuffer { }); let mut merged_ranges: Vec<ExcerptRange<Point>> = Vec::new(); for range in expanded_ranges { - if let Some(last_range) = merged_ranges.last_mut() { - if last_range.context.end >= range.context.start { - last_range.context.end = range.context.end; - continue; - } + if let Some(last_range) = merged_ranges.last_mut() + && last_range.context.end >= range.context.start + { + last_range.context.end = range.context.end; + continue; } merged_ranges.push(range) } @@ -1794,25 +1793,25 @@ impl MultiBuffer { }; if let Some((last_id, last)) = to_insert.last_mut() { - if let Some(new) = new { - if last.context.end >= new.context.start { - last.context.end = last.context.end.max(new.context.end); - excerpt_ids.push(*last_id); - new_iter.next(); - continue; - } + if let Some(new) = new + && last.context.end >= new.context.start + { + last.context.end = last.context.end.max(new.context.end); + excerpt_ids.push(*last_id); + new_iter.next(); + continue; } - if let Some((existing_id, existing_range)) = &existing { - if last.context.end >= existing_range.start { - last.context.end = last.context.end.max(existing_range.end); - to_remove.push(*existing_id); - self.snapshot - .borrow_mut() - .replaced_excerpts - .insert(*existing_id, *last_id); - existing_iter.next(); - continue; - } + if let Some((existing_id, existing_range)) = &existing + && last.context.end >= existing_range.start + { + last.context.end = last.context.end.max(existing_range.end); + to_remove.push(*existing_id); + self.snapshot + .borrow_mut() + .replaced_excerpts + .insert(*existing_id, *last_id); + existing_iter.next(); + continue; } } @@ -2105,10 +2104,10 @@ impl MultiBuffer { .flatten() { cursor.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = cursor.item() { - if excerpt.locator == *locator { - excerpts.push((excerpt.id, excerpt.range.clone())); - } + if let Some(excerpt) = cursor.item() + && excerpt.locator == *locator + { + excerpts.push((excerpt.id, excerpt.range.clone())); } } @@ -2132,22 +2131,21 @@ impl MultiBuffer { let mut result = Vec::new(); for locator in locators { excerpts.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = excerpts.item() { - if excerpt.locator == *locator { - let excerpt_start = excerpts.start().1.clone(); - let excerpt_end = - ExcerptDimension(excerpt_start.0 + excerpt.text_summary.lines); + if let Some(excerpt) = excerpts.item() + && excerpt.locator == *locator + { + let excerpt_start = excerpts.start().1.clone(); + let excerpt_end = ExcerptDimension(excerpt_start.0 + excerpt.text_summary.lines); - diff_transforms.seek_forward(&excerpt_start, Bias::Left); - let overshoot = excerpt_start.0 - diff_transforms.start().0.0; - let start = diff_transforms.start().1.0 + overshoot; + diff_transforms.seek_forward(&excerpt_start, Bias::Left); + let overshoot = excerpt_start.0 - diff_transforms.start().0.0; + let start = diff_transforms.start().1.0 + overshoot; - diff_transforms.seek_forward(&excerpt_end, Bias::Right); - let overshoot = excerpt_end.0 - diff_transforms.start().0.0; - let end = diff_transforms.start().1.0 + overshoot; + diff_transforms.seek_forward(&excerpt_end, Bias::Right); + let overshoot = excerpt_end.0 - diff_transforms.start().0.0; + let end = diff_transforms.start().1.0 + overshoot; - result.push(start..end) - } + result.push(start..end) } } result @@ -2316,12 +2314,12 @@ impl MultiBuffer { // Skip over any subsequent excerpts that are also removed. if let Some(&next_excerpt_id) = excerpt_ids.peek() { let next_locator = snapshot.excerpt_locator_for_id(next_excerpt_id); - if let Some(next_excerpt) = cursor.item() { - if next_excerpt.locator == *next_locator { - excerpt_ids.next(); - excerpt = next_excerpt; - continue 'remove_excerpts; - } + if let Some(next_excerpt) = cursor.item() + && next_excerpt.locator == *next_locator + { + excerpt_ids.next(); + excerpt = next_excerpt; + continue 'remove_excerpts; } } @@ -2494,33 +2492,33 @@ impl MultiBuffer { .excerpts .cursor::<Dimensions<Option<&Locator>, ExcerptOffset>>(&()); cursor.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = cursor.item() { - if excerpt.locator == *locator { - let excerpt_buffer_range = excerpt.range.context.to_offset(&excerpt.buffer); - if diff_change_range.end < excerpt_buffer_range.start - || diff_change_range.start > excerpt_buffer_range.end - { - continue; - } - let excerpt_start = cursor.start().1; - let excerpt_len = ExcerptOffset::new(excerpt.text_summary.len); - let diff_change_start_in_excerpt = ExcerptOffset::new( - diff_change_range - .start - .saturating_sub(excerpt_buffer_range.start), - ); - let diff_change_end_in_excerpt = ExcerptOffset::new( - diff_change_range - .end - .saturating_sub(excerpt_buffer_range.start), - ); - let edit_start = excerpt_start + diff_change_start_in_excerpt.min(excerpt_len); - let edit_end = excerpt_start + diff_change_end_in_excerpt.min(excerpt_len); - excerpt_edits.push(Edit { - old: edit_start..edit_end, - new: edit_start..edit_end, - }); + if let Some(excerpt) = cursor.item() + && excerpt.locator == *locator + { + let excerpt_buffer_range = excerpt.range.context.to_offset(&excerpt.buffer); + if diff_change_range.end < excerpt_buffer_range.start + || diff_change_range.start > excerpt_buffer_range.end + { + continue; } + let excerpt_start = cursor.start().1; + let excerpt_len = ExcerptOffset::new(excerpt.text_summary.len); + let diff_change_start_in_excerpt = ExcerptOffset::new( + diff_change_range + .start + .saturating_sub(excerpt_buffer_range.start), + ); + let diff_change_end_in_excerpt = ExcerptOffset::new( + diff_change_range + .end + .saturating_sub(excerpt_buffer_range.start), + ); + let edit_start = excerpt_start + diff_change_start_in_excerpt.min(excerpt_len); + let edit_end = excerpt_start + diff_change_end_in_excerpt.min(excerpt_len); + excerpt_edits.push(Edit { + old: edit_start..edit_end, + new: edit_start..edit_end, + }); } } @@ -3155,13 +3153,12 @@ impl MultiBuffer { at_transform_boundary = false; let transforms_before_edit = old_diff_transforms.slice(&edit.old.start, Bias::Left); self.append_diff_transforms(&mut new_diff_transforms, transforms_before_edit); - if let Some(transform) = old_diff_transforms.item() { - if old_diff_transforms.end().0 == edit.old.start - && old_diff_transforms.start().0 < edit.old.start - { - self.push_diff_transform(&mut new_diff_transforms, transform.clone()); - old_diff_transforms.next(); - } + if let Some(transform) = old_diff_transforms.item() + && old_diff_transforms.end().0 == edit.old.start + && old_diff_transforms.start().0 < edit.old.start + { + self.push_diff_transform(&mut new_diff_transforms, transform.clone()); + old_diff_transforms.next(); } } @@ -3431,18 +3428,17 @@ impl MultiBuffer { inserted_hunk_info, summary, }) = subtree.first() - { - if self.extend_last_buffer_content_transform( + && self.extend_last_buffer_content_transform( new_transforms, *inserted_hunk_info, *summary, - ) { - let mut cursor = subtree.cursor::<()>(&()); - cursor.next(); - cursor.next(); - new_transforms.append(cursor.suffix(), &()); - return; - } + ) + { + let mut cursor = subtree.cursor::<()>(&()); + cursor.next(); + cursor.next(); + new_transforms.append(cursor.suffix(), &()); + return; } new_transforms.append(subtree, &()); } @@ -3456,14 +3452,13 @@ impl MultiBuffer { inserted_hunk_info: inserted_hunk_anchor, summary, } = transform - { - if self.extend_last_buffer_content_transform( + && self.extend_last_buffer_content_transform( new_transforms, inserted_hunk_anchor, summary, - ) { - return; - } + ) + { + return; } new_transforms.push(transform, &()); } @@ -3518,11 +3513,10 @@ impl MultiBuffer { summary, inserted_hunk_info: inserted_hunk_anchor, } = last_transform + && *inserted_hunk_anchor == new_inserted_hunk_info { - if *inserted_hunk_anchor == new_inserted_hunk_info { - *summary += summary_to_add; - did_extend = true; - } + *summary += summary_to_add; + did_extend = true; } }, &(), @@ -4037,10 +4031,10 @@ impl MultiBufferSnapshot { cursor.seek(&query_range.start); - if let Some(region) = cursor.region().filter(|region| !region.is_main_buffer) { - if region.range.start > D::zero(&()) { - cursor.prev() - } + if let Some(region) = cursor.region().filter(|region| !region.is_main_buffer) + && region.range.start > D::zero(&()) + { + cursor.prev() } iter::from_fn(move || { @@ -4070,10 +4064,10 @@ impl MultiBufferSnapshot { buffer_start = cursor.main_buffer_position()?; }; let mut buffer_end = excerpt.range.context.end.summary::<D>(&excerpt.buffer); - if let Some((end_excerpt_id, end_buffer_offset)) = range_end { - if excerpt.id == end_excerpt_id { - buffer_end = buffer_end.min(end_buffer_offset); - } + if let Some((end_excerpt_id, end_buffer_offset)) = range_end + && excerpt.id == end_excerpt_id + { + buffer_end = buffer_end.min(end_buffer_offset); } if let Some(iterator) = @@ -4144,10 +4138,10 @@ impl MultiBufferSnapshot { // When there are no more metadata items for this excerpt, move to the next excerpt. else { current_excerpt_metadata.take(); - if let Some((end_excerpt_id, _)) = range_end { - if excerpt.id == end_excerpt_id { - return None; - } + if let Some((end_excerpt_id, _)) = range_end + && excerpt.id == end_excerpt_id + { + return None; } cursor.next_excerpt(); } @@ -4622,20 +4616,20 @@ impl MultiBufferSnapshot { pub fn indent_and_comment_for_line(&self, row: MultiBufferRow, cx: &App) -> String { let mut indent = self.indent_size_for_line(row).chars().collect::<String>(); - if self.language_settings(cx).extend_comment_on_newline { - if let Some(language_scope) = self.language_scope_at(Point::new(row.0, 0)) { - let delimiters = language_scope.line_comment_prefixes(); - for delimiter in delimiters { - if *self - .chars_at(Point::new(row.0, indent.len() as u32)) - .take(delimiter.chars().count()) - .collect::<String>() - .as_str() - == **delimiter - { - indent.push_str(delimiter); - break; - } + if self.language_settings(cx).extend_comment_on_newline + && let Some(language_scope) = self.language_scope_at(Point::new(row.0, 0)) + { + let delimiters = language_scope.line_comment_prefixes(); + for delimiter in delimiters { + if *self + .chars_at(Point::new(row.0, indent.len() as u32)) + .take(delimiter.chars().count()) + .collect::<String>() + .as_str() + == **delimiter + { + indent.push_str(delimiter); + break; } } } @@ -4893,25 +4887,22 @@ impl MultiBufferSnapshot { base_text_byte_range, .. }) => { - if let Some(diff_base_anchor) = &anchor.diff_base_anchor { - if let Some(base_text) = + if let Some(diff_base_anchor) = &anchor.diff_base_anchor + && let Some(base_text) = self.diffs.get(buffer_id).map(|diff| diff.base_text()) + && base_text.can_resolve(diff_base_anchor) + { + let base_text_offset = diff_base_anchor.to_offset(base_text); + if base_text_offset >= base_text_byte_range.start + && base_text_offset <= base_text_byte_range.end { - if base_text.can_resolve(diff_base_anchor) { - let base_text_offset = diff_base_anchor.to_offset(base_text); - if base_text_offset >= base_text_byte_range.start - && base_text_offset <= base_text_byte_range.end - { - let position_in_hunk = base_text - .text_summary_for_range::<D, _>( - base_text_byte_range.start..base_text_offset, - ); - position.add_assign(&position_in_hunk); - } else if at_transform_end { - diff_transforms.next(); - continue; - } - } + let position_in_hunk = base_text.text_summary_for_range::<D, _>( + base_text_byte_range.start..base_text_offset, + ); + position.add_assign(&position_in_hunk); + } else if at_transform_end { + diff_transforms.next(); + continue; } } } @@ -4941,20 +4932,19 @@ impl MultiBufferSnapshot { } let mut position = cursor.start().1; - if let Some(excerpt) = cursor.item() { - if excerpt.id == anchor.excerpt_id { - let excerpt_buffer_start = excerpt - .buffer - .offset_for_anchor(&excerpt.range.context.start); - let excerpt_buffer_end = - excerpt.buffer.offset_for_anchor(&excerpt.range.context.end); - let buffer_position = cmp::min( - excerpt_buffer_end, - excerpt.buffer.offset_for_anchor(&anchor.text_anchor), - ); - if buffer_position > excerpt_buffer_start { - position.value += buffer_position - excerpt_buffer_start; - } + if let Some(excerpt) = cursor.item() + && excerpt.id == anchor.excerpt_id + { + let excerpt_buffer_start = excerpt + .buffer + .offset_for_anchor(&excerpt.range.context.start); + let excerpt_buffer_end = excerpt.buffer.offset_for_anchor(&excerpt.range.context.end); + let buffer_position = cmp::min( + excerpt_buffer_end, + excerpt.buffer.offset_for_anchor(&anchor.text_anchor), + ); + if buffer_position > excerpt_buffer_start { + position.value += buffer_position - excerpt_buffer_start; } } position @@ -5211,14 +5201,15 @@ impl MultiBufferSnapshot { .cursor::<Dimensions<usize, ExcerptOffset>>(&()); diff_transforms.seek(&offset, Bias::Right); - if offset == diff_transforms.start().0 && bias == Bias::Left { - if let Some(prev_item) = diff_transforms.prev_item() { - match prev_item { - DiffTransform::DeletedHunk { .. } => { - diff_transforms.prev(); - } - _ => {} + if offset == diff_transforms.start().0 + && bias == Bias::Left + && let Some(prev_item) = diff_transforms.prev_item() + { + match prev_item { + DiffTransform::DeletedHunk { .. } => { + diff_transforms.prev(); } + _ => {} } } let offset_in_transform = offset - diff_transforms.start().0; @@ -5296,17 +5287,17 @@ impl MultiBufferSnapshot { let locator = self.excerpt_locator_for_id(excerpt_id); let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&()); cursor.seek(locator, Bias::Left); - if let Some(excerpt) = cursor.item() { - if excerpt.id == excerpt_id { - let text_anchor = excerpt.clip_anchor(text_anchor); - drop(cursor); - return Some(Anchor { - buffer_id: Some(excerpt.buffer_id), - excerpt_id, - text_anchor, - diff_base_anchor: None, - }); - } + if let Some(excerpt) = cursor.item() + && excerpt.id == excerpt_id + { + let text_anchor = excerpt.clip_anchor(text_anchor); + drop(cursor); + return Some(Anchor { + buffer_id: Some(excerpt.buffer_id), + excerpt_id, + text_anchor, + diff_base_anchor: None, + }); } None } @@ -5860,10 +5851,10 @@ impl MultiBufferSnapshot { let current_depth = indent_stack.len() as u32; // Avoid retrieving the language settings repeatedly for every buffer row. - if let Some((prev_buffer_id, _)) = &prev_settings { - if prev_buffer_id != &buffer.remote_id() { - prev_settings.take(); - } + if let Some((prev_buffer_id, _)) = &prev_settings + && prev_buffer_id != &buffer.remote_id() + { + prev_settings.take(); } let settings = &prev_settings .get_or_insert_with(|| { @@ -6192,10 +6183,10 @@ impl MultiBufferSnapshot { } else { let mut cursor = self.excerpt_ids.cursor::<ExcerptId>(&()); cursor.seek(&id, Bias::Left); - if let Some(entry) = cursor.item() { - if entry.id == id { - return &entry.locator; - } + if let Some(entry) = cursor.item() + && entry.id == id + { + return &entry.locator; } panic!("invalid excerpt id {id:?}") } @@ -6272,10 +6263,10 @@ impl MultiBufferSnapshot { pub fn buffer_range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<Range<text::Anchor>> { let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); - if cursor.seek(&Some(locator), Bias::Left) { - if let Some(excerpt) = cursor.item() { - return Some(excerpt.range.context.clone()); - } + if cursor.seek(&Some(locator), Bias::Left) + && let Some(excerpt) = cursor.item() + { + return Some(excerpt.range.context.clone()); } None } @@ -6284,10 +6275,10 @@ impl MultiBufferSnapshot { let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); cursor.seek(&Some(locator), Bias::Left); - if let Some(excerpt) = cursor.item() { - if excerpt.id == excerpt_id { - return Some(excerpt); - } + if let Some(excerpt) = cursor.item() + && excerpt.id == excerpt_id + { + return Some(excerpt); } None } @@ -6446,13 +6437,12 @@ impl MultiBufferSnapshot { inserted_hunk_info: prev_inserted_hunk_info, .. }) = prev_transform + && *inserted_hunk_info == *prev_inserted_hunk_info { - if *inserted_hunk_info == *prev_inserted_hunk_info { - panic!( - "multiple adjacent buffer content transforms with is_inserted_hunk = {inserted_hunk_info:?}. transforms: {:+?}", - self.diff_transforms.items(&()) - ); - } + panic!( + "multiple adjacent buffer content transforms with is_inserted_hunk = {inserted_hunk_info:?}. transforms: {:+?}", + self.diff_transforms.items(&()) + ); } if summary.len == 0 && !self.is_empty() { panic!("empty buffer content transform"); @@ -6552,14 +6542,12 @@ where self.excerpts.next(); } else if let Some(DiffTransform::DeletedHunk { hunk_info, .. }) = self.diff_transforms.item() - { - if self + && self .excerpts .item() .map_or(false, |excerpt| excerpt.id != hunk_info.excerpt_id) - { - self.excerpts.next(); - } + { + self.excerpts.next(); } } } @@ -7855,10 +7843,11 @@ impl io::Read for ReversedMultiBufferBytes<'_> { if len > 0 { self.range.end -= len; self.chunk = &self.chunk[..self.chunk.len() - len]; - if !self.range.is_empty() && self.chunk.is_empty() { - if let Some(chunk) = self.chunks.next() { - self.chunk = chunk.as_bytes(); - } + if !self.range.is_empty() + && self.chunk.is_empty() + && let Some(chunk) = self.chunks.next() + { + self.chunk = chunk.as_bytes(); } } Ok(len) diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index fefeddb4da049ccf26e76fdb7075dede99fb10e4..598ee0f9cba32fa13760e14092a423e56860aabe 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -3592,24 +3592,20 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) { for (anchors, bias) in [(&left_anchors, Bias::Left), (&right_anchors, Bias::Right)] { for (ix, (offset, anchor)) in offsets.iter().zip(anchors).enumerate() { - if ix > 0 { - if *offset == 252 { - if offset > &offsets[ix - 1] { - let prev_anchor = left_anchors[ix - 1]; - assert!( - anchor.cmp(&prev_anchor, snapshot).is_gt(), - "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_gt()", - offsets[ix], - offsets[ix - 1], - ); - assert!( - prev_anchor.cmp(anchor, snapshot).is_lt(), - "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()", - offsets[ix - 1], - offsets[ix], - ); - } - } + if ix > 0 && *offset == 252 && offset > &offsets[ix - 1] { + let prev_anchor = left_anchors[ix - 1]; + assert!( + anchor.cmp(&prev_anchor, snapshot).is_gt(), + "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_gt()", + offsets[ix], + offsets[ix - 1], + ); + assert!( + prev_anchor.cmp(anchor, snapshot).is_lt(), + "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()", + offsets[ix - 1], + offsets[ix], + ); } } } diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 29653748e4873a271f58f932ee71c820aa755b9a..af2601bd181089a4952529ab4f315aa148e25121 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -138,10 +138,10 @@ impl NotificationStore { pub fn notification_for_id(&self, id: u64) -> Option<&NotificationEntry> { let mut cursor = self.notifications.cursor::<NotificationId>(&()); cursor.seek(&NotificationId(id), Bias::Left); - if let Some(item) = cursor.item() { - if item.id == id { - return Some(item); - } + if let Some(item) = cursor.item() + && item.id == id + { + return Some(item); } None } @@ -229,25 +229,24 @@ impl NotificationStore { mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, cx| { - if let Some(notification) = envelope.payload.notification { - if let Some(rpc::Notification::ChannelMessageMention { message_id, .. }) = + if let Some(notification) = envelope.payload.notification + && let Some(rpc::Notification::ChannelMessageMention { message_id, .. }) = Notification::from_proto(¬ification) - { - let fetch_message_task = this.channel_store.update(cx, |this, cx| { - this.fetch_channel_messages(vec![message_id], cx) - }); - - cx.spawn(async move |this, cx| { - let messages = fetch_message_task.await?; - this.update(cx, move |this, cx| { - for message in messages { - this.channel_messages.insert(message_id, message); - } - cx.notify(); - }) + { + let fetch_message_task = this.channel_store.update(cx, |this, cx| { + this.fetch_channel_messages(vec![message_id], cx) + }); + + cx.spawn(async move |this, cx| { + let messages = fetch_message_task.await?; + this.update(cx, move |this, cx| { + for message in messages { + this.channel_messages.insert(message_id, message); + } + cx.notify(); }) - .detach_and_log_err(cx) - } + }) + .detach_and_log_err(cx) } Ok(()) })? @@ -390,12 +389,12 @@ impl NotificationStore { }); } } - } else if let Some(new_notification) = &new_notification { - if is_new { - cx.emit(NotificationEvent::NewNotification { - entry: new_notification.clone(), - }); - } + } else if let Some(new_notification) = &new_notification + && is_new + { + cx.emit(NotificationEvent::NewNotification { + entry: new_notification.clone(), + }); } if let Some(notification) = new_notification { diff --git a/crates/open_router/src/open_router.rs b/crates/open_router/src/open_router.rs index 3e6e406d9842d5996f2e866d534094ded23fd61c..7c304bad642e22849fb42f34b69c5b80f6f261ad 100644 --- a/crates/open_router/src/open_router.rs +++ b/crates/open_router/src/open_router.rs @@ -240,10 +240,10 @@ impl MessageContent { impl From<Vec<MessagePart>> for MessageContent { fn from(parts: Vec<MessagePart>) -> Self { - if parts.len() == 1 { - if let MessagePart::Text { text } = &parts[0] { - return Self::Plain(text.clone()); - } + if parts.len() == 1 + && let MessagePart::Text { text } = &parts[0] + { + return Self::Plain(text.clone()); } Self::Multipart(parts) } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 9514fd7e364d7969bad33e5e4a7e4bc0d70124aa..9b7ec473fdce9e53d41421580b60084741112edc 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -1170,12 +1170,11 @@ impl OutlinePanel { }); } else { let mut offset = Point::default(); - if let Some(buffer_id) = scroll_to_buffer { - if multi_buffer_snapshot.as_singleton().is_none() - && !active_editor.read(cx).is_buffer_folded(buffer_id, cx) - { - offset.y = -(active_editor.read(cx).file_header_size() as f32); - } + if let Some(buffer_id) = scroll_to_buffer + && multi_buffer_snapshot.as_singleton().is_none() + && !active_editor.read(cx).is_buffer_folded(buffer_id, cx) + { + offset.y = -(active_editor.read(cx).file_header_size() as f32); } active_editor.update(cx, |editor, cx| { @@ -1606,16 +1605,14 @@ impl OutlinePanel { } PanelEntry::FoldedDirs(folded_dirs) => { let mut folded = false; - if let Some(dir_entry) = folded_dirs.entries.last() { - if self + if let Some(dir_entry) = folded_dirs.entries.last() + && self .collapsed_entries .insert(CollapsedEntry::Dir(folded_dirs.worktree_id, dir_entry.id)) - { - folded = true; - buffers_to_fold.extend( - self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry), - ); - } + { + folded = true; + buffers_to_fold + .extend(self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry)); } folded } @@ -2108,11 +2105,11 @@ impl OutlinePanel { dirs_to_expand.push(current_entry.id); } - if traversal.back_to_parent() { - if let Some(parent_entry) = traversal.entry() { - current_entry = parent_entry.clone(); - continue; - } + if traversal.back_to_parent() + && let Some(parent_entry) = traversal.entry() + { + current_entry = parent_entry.clone(); + continue; } break; } @@ -2475,17 +2472,17 @@ impl OutlinePanel { let search_data = match render_data.get() { Some(search_data) => search_data, None => { - if let ItemsDisplayMode::Search(search_state) = &mut self.mode { - if let Some(multi_buffer_snapshot) = multi_buffer_snapshot { - search_state - .highlight_search_match_tx - .try_send(HighlightArguments { - multi_buffer_snapshot: multi_buffer_snapshot.clone(), - match_range: match_range.clone(), - search_data: Arc::clone(render_data), - }) - .ok(); - } + if let ItemsDisplayMode::Search(search_state) = &mut self.mode + && let Some(multi_buffer_snapshot) = multi_buffer_snapshot + { + search_state + .highlight_search_match_tx + .try_send(HighlightArguments { + multi_buffer_snapshot: multi_buffer_snapshot.clone(), + match_range: match_range.clone(), + search_data: Arc::clone(render_data), + }) + .ok(); } return None; } @@ -2833,11 +2830,12 @@ impl OutlinePanel { let new_entry_added = entries_to_add .insert(current_entry.id, current_entry) .is_none(); - if new_entry_added && traversal.back_to_parent() { - if let Some(parent_entry) = traversal.entry() { - current_entry = parent_entry.to_owned(); - continue; - } + if new_entry_added + && traversal.back_to_parent() + && let Some(parent_entry) = traversal.entry() + { + current_entry = parent_entry.to_owned(); + continue; } break; } @@ -2878,18 +2876,17 @@ impl OutlinePanel { entries .into_iter() .filter_map(|entry| { - if auto_fold_dirs { - if let Some(parent) = entry.path.parent() { - let children = new_children_count - .entry(worktree_id) - .or_default() - .entry(Arc::from(parent)) - .or_default(); - if entry.is_dir() { - children.dirs += 1; - } else { - children.files += 1; - } + if auto_fold_dirs && let Some(parent) = entry.path.parent() + { + let children = new_children_count + .entry(worktree_id) + .or_default() + .entry(Arc::from(parent)) + .or_default(); + if entry.is_dir() { + children.dirs += 1; + } else { + children.files += 1; } } @@ -3409,30 +3406,29 @@ impl OutlinePanel { { excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines); - if let Some(default_depth) = pending_default_depth { - if let ExcerptOutlines::Outlines(outlines) = + if let Some(default_depth) = pending_default_depth + && let ExcerptOutlines::Outlines(outlines) = &excerpt.outlines - { - outlines - .iter() - .filter(|outline| { - (default_depth == 0 - || outline.depth >= default_depth) - && outlines_with_children.contains(&( - outline.range.clone(), - outline.depth, - )) - }) - .for_each(|outline| { - outline_panel.collapsed_entries.insert( - CollapsedEntry::Outline( - buffer_id, - excerpt_id, - outline.range.clone(), - ), - ); - }); - } + { + outlines + .iter() + .filter(|outline| { + (default_depth == 0 + || outline.depth >= default_depth) + && outlines_with_children.contains(&( + outline.range.clone(), + outline.depth, + )) + }) + .for_each(|outline| { + outline_panel.collapsed_entries.insert( + CollapsedEntry::Outline( + buffer_id, + excerpt_id, + outline.range.clone(), + ), + ); + }); } // Even if no outlines to check, we still need to update cached entries @@ -3611,10 +3607,9 @@ impl OutlinePanel { .update_in(cx, |outline_panel, window, cx| { outline_panel.cached_entries = new_cached_entries; outline_panel.max_width_item_index = max_width_item_index; - if outline_panel.selected_entry.is_invalidated() - || matches!(outline_panel.selected_entry, SelectedEntry::None) - { - if let Some(new_selected_entry) = + if (outline_panel.selected_entry.is_invalidated() + || matches!(outline_panel.selected_entry, SelectedEntry::None)) + && let Some(new_selected_entry) = outline_panel.active_editor().and_then(|active_editor| { outline_panel.location_for_editor_selection( &active_editor, @@ -3622,9 +3617,8 @@ impl OutlinePanel { cx, ) }) - { - outline_panel.select_entry(new_selected_entry, false, window, cx); - } + { + outline_panel.select_entry(new_selected_entry, false, window, cx); } outline_panel.autoscroll(cx); @@ -3921,19 +3915,19 @@ impl OutlinePanel { } else { None }; - if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider { - if !active_editor.read(cx).is_buffer_folded(buffer_id, cx) { - outline_panel.add_excerpt_entries( - &mut generation_state, - buffer_id, - entry_excerpts, - depth, - track_matches, - is_singleton, - query.as_deref(), - cx, - ); - } + if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider + && !active_editor.read(cx).is_buffer_folded(buffer_id, cx) + { + outline_panel.add_excerpt_entries( + &mut generation_state, + buffer_id, + entry_excerpts, + depth, + track_matches, + is_singleton, + query.as_deref(), + cx, + ); } } } @@ -4404,15 +4398,15 @@ impl OutlinePanel { }) .filter(|(match_range, _)| { let editor = active_editor.read(cx); - if let Some(buffer_id) = match_range.start.buffer_id { - if editor.is_buffer_folded(buffer_id, cx) { - return false; - } + if let Some(buffer_id) = match_range.start.buffer_id + && editor.is_buffer_folded(buffer_id, cx) + { + return false; } - if let Some(buffer_id) = match_range.start.buffer_id { - if editor.is_buffer_folded(buffer_id, cx) { - return false; - } + if let Some(buffer_id) = match_range.start.buffer_id + && editor.is_buffer_folded(buffer_id, cx) + { + return false; } true }); @@ -4456,16 +4450,14 @@ impl OutlinePanel { cx: &mut Context<Self>, ) { self.pinned = !self.pinned; - if !self.pinned { - if let Some((active_item, active_editor)) = self + if !self.pinned + && let Some((active_item, active_editor)) = self .workspace .upgrade() .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx)) - { - if self.should_replace_active_item(active_item.as_ref()) { - self.replace_active_editor(active_item, active_editor, window, cx); - } - } + && self.should_replace_active_item(active_item.as_ref()) + { + self.replace_active_editor(active_item, active_editor, window, cx); } cx.notify(); @@ -5067,24 +5059,23 @@ impl Panel for OutlinePanel { let old_active = outline_panel.active; outline_panel.active = active; if old_active != active { - if active { - if let Some((active_item, active_editor)) = + if active + && let Some((active_item, active_editor)) = outline_panel.workspace.upgrade().and_then(|workspace| { workspace_active_editor(workspace.read(cx), cx) }) - { - if outline_panel.should_replace_active_item(active_item.as_ref()) { - outline_panel.replace_active_editor( - active_item, - active_editor, - window, - cx, - ); - } else { - outline_panel.update_fs_entries(active_editor, None, window, cx) - } - return; + { + if outline_panel.should_replace_active_item(active_item.as_ref()) { + outline_panel.replace_active_editor( + active_item, + active_editor, + window, + cx, + ); + } else { + outline_panel.update_fs_entries(active_editor, None, window, cx) } + return; } if !outline_panel.pinned { @@ -5319,8 +5310,8 @@ fn subscribe_for_editor_events( }) .copied(), ); - if !ignore_selections_change { - if let Some(entry_to_select) = latest_unfolded_buffer_id + if !ignore_selections_change + && let Some(entry_to_select) = latest_unfolded_buffer_id .or(latest_folded_buffer_id) .and_then(|toggled_buffer_id| { outline_panel.fs_entries.iter().find_map( @@ -5344,9 +5335,8 @@ fn subscribe_for_editor_events( ) }) .map(PanelEntry::Fs) - { - outline_panel.select_entry(entry_to_select, true, window, cx); - } + { + outline_panel.select_entry(entry_to_select, true, window, cx); } outline_panel.update_fs_entries(editor.clone(), debounce, window, cx); diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 33320e6845964932aa7bfe051f3ffe4fba1a6168..8e1485dc9ac2bc6088692ec7a257ecfb333e3dfa 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -185,11 +185,11 @@ impl Prettier { .metadata(&ignore_path) .await .with_context(|| format!("fetching metadata for {ignore_path:?}"))? + && !metadata.is_dir + && !metadata.is_symlink { - if !metadata.is_dir && !metadata.is_symlink { - log::info!("Found prettier ignore at {ignore_path:?}"); - return Ok(ControlFlow::Continue(Some(path_to_check))); - } + log::info!("Found prettier ignore at {ignore_path:?}"); + return Ok(ControlFlow::Continue(Some(path_to_check))); } match &closest_package_json_path { None => closest_package_json_path = Some(path_to_check.clone()), @@ -223,13 +223,13 @@ impl Prettier { }) { let workspace_ignore = path_to_check.join(".prettierignore"); - if let Some(metadata) = fs.metadata(&workspace_ignore).await? { - if !metadata.is_dir { - log::info!( - "Found prettier ignore at workspace root {workspace_ignore:?}" - ); - return Ok(ControlFlow::Continue(Some(path_to_check))); - } + if let Some(metadata) = fs.metadata(&workspace_ignore).await? + && !metadata.is_dir + { + log::info!( + "Found prettier ignore at workspace root {workspace_ignore:?}" + ); + return Ok(ControlFlow::Continue(Some(path_to_check))); } } } @@ -549,18 +549,16 @@ async fn read_package_json( .metadata(&possible_package_json) .await .with_context(|| format!("fetching metadata for package json {possible_package_json:?}"))? + && !package_json_metadata.is_dir + && !package_json_metadata.is_symlink { - if !package_json_metadata.is_dir && !package_json_metadata.is_symlink { - let package_json_contents = fs - .load(&possible_package_json) - .await - .with_context(|| format!("reading {possible_package_json:?} file contents"))?; - return serde_json::from_str::<HashMap<String, serde_json::Value>>( - &package_json_contents, - ) + let package_json_contents = fs + .load(&possible_package_json) + .await + .with_context(|| format!("reading {possible_package_json:?} file contents"))?; + return serde_json::from_str::<HashMap<String, serde_json::Value>>(&package_json_contents) .map(Some) .with_context(|| format!("parsing {possible_package_json:?} file contents")); - } } Ok(None) } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index b8101e14f39b4faf54b76eaab955864e4ef82ae5..1522376e9a43f0eb3d9fc218b4ed6cfd52f1bebe 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -1094,10 +1094,10 @@ impl BufferStore { .collect::<Vec<_>>() })?; for buffer_task in buffers { - if let Some(buffer) = buffer_task.await.log_err() { - if tx.send(buffer).await.is_err() { - return anyhow::Ok(()); - } + if let Some(buffer) = buffer_task.await.log_err() + && tx.send(buffer).await.is_err() + { + return anyhow::Ok(()); } } } @@ -1173,11 +1173,11 @@ impl BufferStore { buffer_id: BufferId, handle: OpenLspBufferHandle, ) { - if let Some(shared_buffers) = self.shared_buffers.get_mut(&peer_id) { - if let Some(buffer) = shared_buffers.get_mut(&buffer_id) { - buffer.lsp_handle = Some(handle); - return; - } + if let Some(shared_buffers) = self.shared_buffers.get_mut(&peer_id) + && let Some(buffer) = shared_buffers.get_mut(&buffer_id) + { + buffer.lsp_handle = Some(handle); + return; } debug_panic!("tried to register shared lsp handle, but buffer was not shared") } @@ -1388,14 +1388,14 @@ impl BufferStore { let peer_id = envelope.sender_id; let buffer_id = BufferId::new(envelope.payload.buffer_id)?; this.update(&mut cx, |this, cx| { - if let Some(shared) = this.shared_buffers.get_mut(&peer_id) { - if shared.remove(&buffer_id).is_some() { - cx.emit(BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id)); - if shared.is_empty() { - this.shared_buffers.remove(&peer_id); - } - return; + if let Some(shared) = this.shared_buffers.get_mut(&peer_id) + && shared.remove(&buffer_id).is_some() + { + cx.emit(BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id)); + if shared.is_empty() { + this.shared_buffers.remove(&peer_id); } + return; } debug_panic!( "peer_id {} closed buffer_id {} which was either not open or already closed", diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 091189db7c2345e33d5a830669b0169d1d2b0ff2..faa9948596247b70186a1990cb8a34eca61ad716 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -623,12 +623,11 @@ impl BreakpointStore { file_breakpoints.breakpoints.iter().filter_map({ let range = range.clone(); move |bp| { - if let Some(range) = &range { - if bp.position().cmp(&range.start, buffer_snapshot).is_lt() - || bp.position().cmp(&range.end, buffer_snapshot).is_gt() - { - return None; - } + if let Some(range) = &range + && (bp.position().cmp(&range.start, buffer_snapshot).is_lt() + || bp.position().cmp(&range.end, buffer_snapshot).is_gt()) + { + return None; } let session_state = active_session_id .and_then(|id| bp.session_state.get(&id)) diff --git a/crates/project/src/debugger/memory.rs b/crates/project/src/debugger/memory.rs index fec3c344c5a433eebb3a1f314a8fd911bd603022..092435fda7bbaf0f33a67a78801b77687e099b0e 100644 --- a/crates/project/src/debugger/memory.rs +++ b/crates/project/src/debugger/memory.rs @@ -318,14 +318,13 @@ impl Iterator for MemoryIterator { return None; } if let Some((current_page_address, current_memory_chunk)) = self.current_known_page.as_mut() + && current_page_address.0 <= self.start { - if current_page_address.0 <= self.start { - if let Some(next_cell) = current_memory_chunk.next() { - self.start += 1; - return Some(next_cell); - } else { - self.current_known_page.take(); - } + if let Some(next_cell) = current_memory_chunk.next() { + self.start += 1; + return Some(next_cell); + } else { + self.current_known_page.take(); } } if !self.fetch_next_page() { diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 9539008530cd16d479f32f6b9cf60aa8241dae40..ebc29a0a4b7f1329206e092d2fe67e2eb91d27bd 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -570,23 +570,22 @@ impl GitStore { cx: &mut Context<Self>, ) -> Task<Result<Entity<BufferDiff>>> { let buffer_id = buffer.read(cx).remote_id(); - if let Some(diff_state) = self.diffs.get(&buffer_id) { - if let Some(unstaged_diff) = diff_state + if let Some(diff_state) = self.diffs.get(&buffer_id) + && let Some(unstaged_diff) = diff_state .read(cx) .unstaged_diff .as_ref() .and_then(|weak| weak.upgrade()) + { + if let Some(task) = + diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) { - if let Some(task) = - diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) - { - return cx.background_executor().spawn(async move { - task.await; - Ok(unstaged_diff) - }); - } - return Task::ready(Ok(unstaged_diff)); + return cx.background_executor().spawn(async move { + task.await; + Ok(unstaged_diff) + }); } + return Task::ready(Ok(unstaged_diff)); } let Some((repo, repo_path)) = @@ -627,23 +626,22 @@ impl GitStore { ) -> Task<Result<Entity<BufferDiff>>> { let buffer_id = buffer.read(cx).remote_id(); - if let Some(diff_state) = self.diffs.get(&buffer_id) { - if let Some(uncommitted_diff) = diff_state + if let Some(diff_state) = self.diffs.get(&buffer_id) + && let Some(uncommitted_diff) = diff_state .read(cx) .uncommitted_diff .as_ref() .and_then(|weak| weak.upgrade()) + { + if let Some(task) = + diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) { - if let Some(task) = - diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) - { - return cx.background_executor().spawn(async move { - task.await; - Ok(uncommitted_diff) - }); - } - return Task::ready(Ok(uncommitted_diff)); + return cx.background_executor().spawn(async move { + task.await; + Ok(uncommitted_diff) + }); } + return Task::ready(Ok(uncommitted_diff)); } let Some((repo, repo_path)) = @@ -764,22 +762,21 @@ impl GitStore { log::debug!("open conflict set"); let buffer_id = buffer.read(cx).remote_id(); - if let Some(git_state) = self.diffs.get(&buffer_id) { - if let Some(conflict_set) = git_state + if let Some(git_state) = self.diffs.get(&buffer_id) + && let Some(conflict_set) = git_state .read(cx) .conflict_set .as_ref() .and_then(|weak| weak.upgrade()) - { - let conflict_set = conflict_set.clone(); - let buffer_snapshot = buffer.read(cx).text_snapshot(); + { + let conflict_set = conflict_set.clone(); + let buffer_snapshot = buffer.read(cx).text_snapshot(); - git_state.update(cx, |state, cx| { - let _ = state.reparse_conflict_markers(buffer_snapshot, cx); - }); + git_state.update(cx, |state, cx| { + let _ = state.reparse_conflict_markers(buffer_snapshot, cx); + }); - return conflict_set; - } + return conflict_set; } let is_unmerged = self @@ -1151,29 +1148,26 @@ impl GitStore { for (buffer_id, diff) in self.diffs.iter() { if let Some((buffer_repo, repo_path)) = self.repository_and_path_for_buffer_id(*buffer_id, cx) + && buffer_repo == repo { - if buffer_repo == repo { - diff.update(cx, |diff, cx| { - if let Some(conflict_set) = &diff.conflict_set { - let conflict_status_changed = - conflict_set.update(cx, |conflict_set, cx| { - let has_conflict = repo_snapshot.has_conflict(&repo_path); - conflict_set.set_has_conflict(has_conflict, cx) - })?; - if conflict_status_changed { - let buffer_store = self.buffer_store.read(cx); - if let Some(buffer) = buffer_store.get(*buffer_id) { - let _ = diff.reparse_conflict_markers( - buffer.read(cx).text_snapshot(), - cx, - ); - } + diff.update(cx, |diff, cx| { + if let Some(conflict_set) = &diff.conflict_set { + let conflict_status_changed = + conflict_set.update(cx, |conflict_set, cx| { + let has_conflict = repo_snapshot.has_conflict(&repo_path); + conflict_set.set_has_conflict(has_conflict, cx) + })?; + if conflict_status_changed { + let buffer_store = self.buffer_store.read(cx); + if let Some(buffer) = buffer_store.get(*buffer_id) { + let _ = diff + .reparse_conflict_markers(buffer.read(cx).text_snapshot(), cx); } } - anyhow::Ok(()) - }) - .ok(); - } + } + anyhow::Ok(()) + }) + .ok(); } } cx.emit(GitStoreEvent::RepositoryUpdated( @@ -2231,13 +2225,13 @@ impl GitStore { ) -> Result<()> { let buffer_id = BufferId::new(request.payload.buffer_id)?; this.update(&mut cx, |this, cx| { - if let Some(diff_state) = this.diffs.get_mut(&buffer_id) { - if let Some(buffer) = this.buffer_store.read(cx).get(buffer_id) { - let buffer = buffer.read(cx).text_snapshot(); - diff_state.update(cx, |diff_state, cx| { - diff_state.handle_base_texts_updated(buffer, request.payload, cx); - }) - } + if let Some(diff_state) = this.diffs.get_mut(&buffer_id) + && let Some(buffer) = this.buffer_store.read(cx).get(buffer_id) + { + let buffer = buffer.read(cx).text_snapshot(); + diff_state.update(cx, |diff_state, cx| { + diff_state.handle_base_texts_updated(buffer, request.payload, cx); + }) } }) } @@ -3533,14 +3527,13 @@ impl Repository { let Some(project_path) = self.repo_path_to_project_path(path, cx) else { continue; }; - if let Some(buffer) = buffer_store.get_by_path(&project_path) { - if buffer + if let Some(buffer) = buffer_store.get_by_path(&project_path) + && buffer .read(cx) .file() .map_or(false, |file| file.disk_state().exists()) - { - save_futures.push(buffer_store.save_buffer(buffer, cx)); - } + { + save_futures.push(buffer_store.save_buffer(buffer, cx)); } } }) @@ -3600,14 +3593,13 @@ impl Repository { let Some(project_path) = self.repo_path_to_project_path(path, cx) else { continue; }; - if let Some(buffer) = buffer_store.get_by_path(&project_path) { - if buffer + if let Some(buffer) = buffer_store.get_by_path(&project_path) + && buffer .read(cx) .file() .map_or(false, |file| file.disk_state().exists()) - { - save_futures.push(buffer_store.save_buffer(buffer, cx)); - } + { + save_futures.push(buffer_store.save_buffer(buffer, cx)); } } }) @@ -4421,14 +4413,13 @@ impl Repository { } if let Some(job) = jobs.pop_front() { - if let Some(current_key) = &job.key { - if jobs + if let Some(current_key) = &job.key + && jobs .iter() .any(|other_job| other_job.key.as_ref() == Some(current_key)) { continue; } - } (job.job)(state.clone(), cx).await; } else if let Some(job) = job_rx.next().await { jobs.push_back(job); @@ -4459,13 +4450,12 @@ impl Repository { } if let Some(job) = jobs.pop_front() { - if let Some(current_key) = &job.key { - if jobs + if let Some(current_key) = &job.key + && jobs .iter() .any(|other_job| other_job.key.as_ref() == Some(current_key)) - { - continue; - } + { + continue; } (job.job)(state.clone(), cx).await; } else if let Some(job) = job_rx.next().await { @@ -4589,10 +4579,10 @@ impl Repository { for (repo_path, status) in &*statuses.entries { changed_paths.remove(repo_path); - if cursor.seek_forward(&PathTarget::Path(repo_path), Bias::Left) { - if cursor.item().is_some_and(|entry| entry.status == *status) { - continue; - } + if cursor.seek_forward(&PathTarget::Path(repo_path), Bias::Left) + && cursor.item().is_some_and(|entry| entry.status == *status) + { + continue; } changed_path_statuses.push(Edit::Insert(StatusEntry { diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index de5ff9b93509a26023eb814076c86e0867f05257..4594e8d14061a651fd69c4f20557216445e0db4e 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -182,11 +182,11 @@ impl<'a> Iterator for ChildEntriesGitIter<'a> { type Item = GitEntryRef<'a>; fn next(&mut self) -> Option<Self::Item> { - if let Some(item) = self.traversal.entry() { - if item.path.starts_with(self.parent_path) { - self.traversal.advance_to_sibling(); - return Some(item); - } + if let Some(item) = self.traversal.entry() + && item.path.starts_with(self.parent_path) + { + self.traversal.advance_to_sibling(); + return Some(item); } None } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index d5c3cc424f7521c8b31e740c991501e8404f4b93..2a1facd3c0f58be066b54739326797c59ad87b25 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2341,15 +2341,14 @@ impl LspCommand for GetCompletions { .zip(completion_edits) .map(|(mut lsp_completion, mut edit)| { LineEnding::normalize(&mut edit.new_text); - if lsp_completion.data.is_none() { - if let Some(default_data) = lsp_defaults + if lsp_completion.data.is_none() + && let Some(default_data) = lsp_defaults .as_ref() .and_then(|item_defaults| item_defaults.data.clone()) - { - // Servers (e.g. JDTLS) prefer unchanged completions, when resolving the items later, - // so we do not insert the defaults here, but `data` is needed for resolving, so this is an exception. - lsp_completion.data = Some(default_data); - } + { + // Servers (e.g. JDTLS) prefer unchanged completions, when resolving the items later, + // so we do not insert the defaults here, but `data` is needed for resolving, so this is an exception. + lsp_completion.data = Some(default_data); } CoreCompletion { replace_range: edit.replace_range, @@ -2623,10 +2622,10 @@ impl LspCommand for GetCodeActions { .filter_map(|entry| { let (lsp_action, resolved) = match entry { lsp::CodeActionOrCommand::CodeAction(lsp_action) => { - if let Some(command) = lsp_action.command.as_ref() { - if !available_commands.contains(&command.command) { - return None; - } + if let Some(command) = lsp_action.command.as_ref() + && !available_commands.contains(&command.command) + { + return None; } (LspAction::Action(Box::new(lsp_action)), false) } @@ -2641,10 +2640,9 @@ impl LspCommand for GetCodeActions { if let Some((requested_kinds, kind)) = requested_kinds_set.as_ref().zip(lsp_action.action_kind()) + && !requested_kinds.contains(&kind) { - if !requested_kinds.contains(&kind) { - return None; - } + return None; } Some(CodeAction { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 9410eea74258b16ccfc9c8b19bb92580a32a1670..23061149bfd2112303d28c7f2b8f2b0fb8e620c8 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -701,10 +701,9 @@ impl LocalLspStore { async move { this.update(&mut cx, |this, _| { if let Some(status) = this.language_server_statuses.get_mut(&server_id) + && let lsp::NumberOrString::String(token) = params.token { - if let lsp::NumberOrString::String(token) = params.token { - status.progress_tokens.insert(token); - } + status.progress_tokens.insert(token); } })?; @@ -1015,10 +1014,10 @@ impl LocalLspStore { } } LanguageServerState::Starting { startup, .. } => { - if let Some(server) = startup.await { - if let Some(shutdown) = server.shutdown() { - shutdown.await; - } + if let Some(server) = startup.await + && let Some(shutdown) = server.shutdown() + { + shutdown.await; } } } @@ -2384,15 +2383,15 @@ impl LocalLspStore { return None; } if !only_register_servers.is_empty() { - if let Some(server_id) = server_node.server_id() { - if !only_register_servers.contains(&LanguageServerSelector::Id(server_id)) { - return None; - } + if let Some(server_id) = server_node.server_id() + && !only_register_servers.contains(&LanguageServerSelector::Id(server_id)) + { + return None; } - if let Some(name) = server_node.name() { - if !only_register_servers.contains(&LanguageServerSelector::Name(name)) { - return None; - } + if let Some(name) = server_node.name() + && !only_register_servers.contains(&LanguageServerSelector::Name(name)) + { + return None; } } @@ -2410,11 +2409,11 @@ impl LocalLspStore { cx, ); - if let Some(state) = self.language_servers.get(&server_id) { - if let Ok(uri) = uri { - state.add_workspace_folder(uri); - }; - } + if let Some(state) = self.language_servers.get(&server_id) + && let Ok(uri) = uri + { + state.add_workspace_folder(uri); + }; server_id }; @@ -3844,13 +3843,13 @@ impl LspStore { } BufferStoreEvent::BufferChangedFilePath { buffer, old_file } => { let buffer_id = buffer.read(cx).remote_id(); - if let Some(local) = self.as_local_mut() { - if let Some(old_file) = File::from_dyn(old_file.as_ref()) { - local.reset_buffer(buffer, old_file, cx); + if let Some(local) = self.as_local_mut() + && let Some(old_file) = File::from_dyn(old_file.as_ref()) + { + local.reset_buffer(buffer, old_file, cx); - if local.registered_buffers.contains_key(&buffer_id) { - local.unregister_old_buffer_from_language_servers(buffer, old_file, cx); - } + if local.registered_buffers.contains_key(&buffer_id) { + local.unregister_old_buffer_from_language_servers(buffer, old_file, cx); } } @@ -4201,14 +4200,12 @@ impl LspStore { if local .registered_buffers .contains_key(&buffer.read(cx).remote_id()) - { - if let Some(file_url) = + && let Some(file_url) = file_path_to_lsp_url(&f.abs_path(cx)).log_err() - { - local.unregister_buffer_from_language_servers( - &buffer, &file_url, cx, - ); - } + { + local.unregister_buffer_from_language_servers( + &buffer, &file_url, cx, + ); } } } @@ -4306,20 +4303,13 @@ impl LspStore { let buffer = buffer_entity.read(cx); let buffer_file = buffer.file().cloned(); let buffer_id = buffer.remote_id(); - if let Some(local_store) = self.as_local_mut() { - if local_store.registered_buffers.contains_key(&buffer_id) { - if let Some(abs_path) = - File::from_dyn(buffer_file.as_ref()).map(|file| file.abs_path(cx)) - { - if let Some(file_url) = file_path_to_lsp_url(&abs_path).log_err() { - local_store.unregister_buffer_from_language_servers( - buffer_entity, - &file_url, - cx, - ); - } - } - } + if let Some(local_store) = self.as_local_mut() + && local_store.registered_buffers.contains_key(&buffer_id) + && let Some(abs_path) = + File::from_dyn(buffer_file.as_ref()).map(|file| file.abs_path(cx)) + && let Some(file_url) = file_path_to_lsp_url(&abs_path).log_err() + { + local_store.unregister_buffer_from_language_servers(buffer_entity, &file_url, cx); } buffer_entity.update(cx, |buffer, cx| { if buffer.language().map_or(true, |old_language| { @@ -4336,33 +4326,28 @@ impl LspStore { let worktree_id = if let Some(file) = buffer_file { let worktree = file.worktree.clone(); - if let Some(local) = self.as_local_mut() { - if local.registered_buffers.contains_key(&buffer_id) { - local.register_buffer_with_language_servers( - buffer_entity, - HashSet::default(), - cx, - ); - } + if let Some(local) = self.as_local_mut() + && local.registered_buffers.contains_key(&buffer_id) + { + local.register_buffer_with_language_servers(buffer_entity, HashSet::default(), cx); } Some(worktree.read(cx).id()) } else { None }; - if settings.prettier.allowed { - if let Some(prettier_plugins) = prettier_store::prettier_plugins_for_language(&settings) - { - let prettier_store = self.as_local().map(|s| s.prettier_store.clone()); - if let Some(prettier_store) = prettier_store { - prettier_store.update(cx, |prettier_store, cx| { - prettier_store.install_default_prettier( - worktree_id, - prettier_plugins.iter().map(|s| Arc::from(s.as_str())), - cx, - ) - }) - } + if settings.prettier.allowed + && let Some(prettier_plugins) = prettier_store::prettier_plugins_for_language(&settings) + { + let prettier_store = self.as_local().map(|s| s.prettier_store.clone()); + if let Some(prettier_store) = prettier_store { + prettier_store.update(cx, |prettier_store, cx| { + prettier_store.install_default_prettier( + worktree_id, + prettier_plugins.iter().map(|s| Arc::from(s.as_str())), + cx, + ) + }) } } @@ -4381,26 +4366,25 @@ impl LspStore { } pub(crate) fn send_diagnostic_summaries(&self, worktree: &mut Worktree) { - if let Some((client, downstream_project_id)) = self.downstream_client.clone() { - if let Some(diangostic_summaries) = self.diagnostic_summaries.get(&worktree.id()) { - let mut summaries = - diangostic_summaries + if let Some((client, downstream_project_id)) = self.downstream_client.clone() + && let Some(diangostic_summaries) = self.diagnostic_summaries.get(&worktree.id()) + { + let mut summaries = diangostic_summaries + .into_iter() + .flat_map(|(path, summaries)| { + summaries .into_iter() - .flat_map(|(path, summaries)| { - summaries - .into_iter() - .map(|(server_id, summary)| summary.to_proto(*server_id, path)) - }); - if let Some(summary) = summaries.next() { - client - .send(proto::UpdateDiagnosticSummary { - project_id: downstream_project_id, - worktree_id: worktree.id().to_proto(), - summary: Some(summary), - more_summaries: summaries.collect(), - }) - .log_err(); - } + .map(|(server_id, summary)| summary.to_proto(*server_id, path)) + }); + if let Some(summary) = summaries.next() { + client + .send(proto::UpdateDiagnosticSummary { + project_id: downstream_project_id, + worktree_id: worktree.id().to_proto(), + summary: Some(summary), + more_summaries: summaries.collect(), + }) + .log_err(); } } } @@ -4730,11 +4714,11 @@ impl LspStore { &language.name(), cx, ); - if let Some(state) = local.language_servers.get(&server_id) { - if let Ok(uri) = uri { - state.add_workspace_folder(uri); - }; - } + if let Some(state) = local.language_servers.get(&server_id) + && let Ok(uri) = uri + { + state.add_workspace_folder(uri); + }; server_id }); @@ -4805,8 +4789,8 @@ impl LspStore { LocalLspStore::try_resolve_code_action(&lang_server, &mut action) .await .context("resolving a code action")?; - if let Some(edit) = action.lsp_action.edit() { - if edit.changes.is_some() || edit.document_changes.is_some() { + if let Some(edit) = action.lsp_action.edit() + && (edit.changes.is_some() || edit.document_changes.is_some()) { return LocalLspStore::deserialize_workspace_edit( this.upgrade().context("no app present")?, edit.clone(), @@ -4817,7 +4801,6 @@ impl LspStore { ) .await; } - } if let Some(command) = action.lsp_action.command() { let server_capabilities = lang_server.capabilities(); @@ -5736,28 +5719,28 @@ impl LspStore { let version_queried_for = buffer.read(cx).version(); let buffer_id = buffer.read(cx).remote_id(); - if let Some(cached_data) = self.lsp_code_lens.get(&buffer_id) { - if !version_queried_for.changed_since(&cached_data.lens_for_version) { - let has_different_servers = self.as_local().is_some_and(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - != cached_data.lens.keys().copied().collect() - }); - if !has_different_servers { - return Task::ready(Ok(cached_data.lens.values().flatten().cloned().collect())) - .shared(); - } + if let Some(cached_data) = self.lsp_code_lens.get(&buffer_id) + && !version_queried_for.changed_since(&cached_data.lens_for_version) + { + let has_different_servers = self.as_local().is_some_and(|local| { + local + .buffers_opened_in_servers + .get(&buffer_id) + .cloned() + .unwrap_or_default() + != cached_data.lens.keys().copied().collect() + }); + if !has_different_servers { + return Task::ready(Ok(cached_data.lens.values().flatten().cloned().collect())) + .shared(); } } let lsp_data = self.lsp_code_lens.entry(buffer_id).or_default(); - if let Some((updating_for, running_update)) = &lsp_data.update { - if !version_queried_for.changed_since(updating_for) { - return running_update.clone(); - } + if let Some((updating_for, running_update)) = &lsp_data.update + && !version_queried_for.changed_since(updating_for) + { + return running_update.clone(); } let buffer = buffer.clone(); let query_version_queried_for = version_queried_for.clone(); @@ -6372,11 +6355,11 @@ impl LspStore { .old_replace_start .and_then(deserialize_anchor) .zip(response.old_replace_end.and_then(deserialize_anchor)); - if let Some((old_replace_start, old_replace_end)) = replace_range { - if !response.new_text.is_empty() { - completion.new_text = response.new_text; - completion.replace_range = old_replace_start..old_replace_end; - } + if let Some((old_replace_start, old_replace_end)) = replace_range + && !response.new_text.is_empty() + { + completion.new_text = response.new_text; + completion.replace_range = old_replace_start..old_replace_end; } Ok(()) @@ -6751,33 +6734,33 @@ impl LspStore { LspFetchStrategy::UseCache { known_cache_version, } => { - if let Some(cached_data) = self.lsp_document_colors.get(&buffer_id) { - if !version_queried_for.changed_since(&cached_data.colors_for_version) { - let has_different_servers = self.as_local().is_some_and(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - != cached_data.colors.keys().copied().collect() - }); - if !has_different_servers { - if Some(cached_data.cache_version) == known_cache_version { - return None; - } else { - return Some( - Task::ready(Ok(DocumentColors { - colors: cached_data - .colors - .values() - .flatten() - .cloned() - .collect(), - cache_version: Some(cached_data.cache_version), - })) - .shared(), - ); - } + if let Some(cached_data) = self.lsp_document_colors.get(&buffer_id) + && !version_queried_for.changed_since(&cached_data.colors_for_version) + { + let has_different_servers = self.as_local().is_some_and(|local| { + local + .buffers_opened_in_servers + .get(&buffer_id) + .cloned() + .unwrap_or_default() + != cached_data.colors.keys().copied().collect() + }); + if !has_different_servers { + if Some(cached_data.cache_version) == known_cache_version { + return None; + } else { + return Some( + Task::ready(Ok(DocumentColors { + colors: cached_data + .colors + .values() + .flatten() + .cloned() + .collect(), + cache_version: Some(cached_data.cache_version), + })) + .shared(), + ); } } } @@ -6785,10 +6768,10 @@ impl LspStore { } let lsp_data = self.lsp_document_colors.entry(buffer_id).or_default(); - if let Some((updating_for, running_update)) = &lsp_data.colors_update { - if !version_queried_for.changed_since(updating_for) { - return Some(running_update.clone()); - } + if let Some((updating_for, running_update)) = &lsp_data.colors_update + && !version_queried_for.changed_since(updating_for) + { + return Some(running_update.clone()); } let query_version_queried_for = version_queried_for.clone(); let new_task = cx @@ -8785,12 +8768,11 @@ impl LspStore { if summary.is_empty() { if let Some(worktree_summaries) = lsp_store.diagnostic_summaries.get_mut(&worktree_id) + && let Some(summaries) = worktree_summaries.get_mut(&path) { - if let Some(summaries) = worktree_summaries.get_mut(&path) { - summaries.remove(&server_id); - if summaries.is_empty() { - worktree_summaries.remove(&path); - } + summaries.remove(&server_id); + if summaries.is_empty() { + worktree_summaries.remove(&path); } } } else { @@ -9491,10 +9473,10 @@ impl LspStore { cx: &mut Context<Self>, ) { if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - if let Some(work) = status.pending_work.remove(&token) { - if !work.is_disk_based_diagnostics_progress { - cx.emit(LspStoreEvent::RefreshInlayHints); - } + if let Some(work) = status.pending_work.remove(&token) + && !work.is_disk_based_diagnostics_progress + { + cx.emit(LspStoreEvent::RefreshInlayHints); } cx.notify(); } @@ -10288,10 +10270,10 @@ impl LspStore { None => None, }; - if let Some(server) = server { - if let Some(shutdown) = server.shutdown() { - shutdown.await; - } + if let Some(server) = server + && let Some(shutdown) = server.shutdown() + { + shutdown.await; } } @@ -10565,18 +10547,18 @@ impl LspStore { for buffer in buffers { buffer.update(cx, |buffer, cx| { language_servers_to_stop.extend(local.language_server_ids_for_buffer(buffer, cx)); - if let Some(worktree_id) = buffer.file().map(|f| f.worktree_id(cx)) { - if covered_worktrees.insert(worktree_id) { - language_server_names_to_stop.retain(|name| { - let old_ids_count = language_servers_to_stop.len(); - let all_language_servers_with_this_name = local - .language_server_ids - .iter() - .filter_map(|(seed, state)| seed.name.eq(name).then(|| state.id)); - language_servers_to_stop.extend(all_language_servers_with_this_name); - old_ids_count == language_servers_to_stop.len() - }); - } + if let Some(worktree_id) = buffer.file().map(|f| f.worktree_id(cx)) + && covered_worktrees.insert(worktree_id) + { + language_server_names_to_stop.retain(|name| { + let old_ids_count = language_servers_to_stop.len(); + let all_language_servers_with_this_name = local + .language_server_ids + .iter() + .filter_map(|(seed, state)| seed.name.eq(name).then(|| state.id)); + language_servers_to_stop.extend(all_language_servers_with_this_name); + old_ids_count == language_servers_to_stop.len() + }); } }); } @@ -11081,10 +11063,10 @@ impl LspStore { if let Some((LanguageServerState::Running { server, .. }, status)) = server.zip(status) { for (token, progress) in &status.pending_work { - if let Some(token_to_cancel) = token_to_cancel.as_ref() { - if token != token_to_cancel { - continue; - } + if let Some(token_to_cancel) = token_to_cancel.as_ref() + && token != token_to_cancel + { + continue; } if progress.is_cancellable { server @@ -11191,38 +11173,36 @@ impl LspStore { for server_id in &language_server_ids { if let Some(LanguageServerState::Running { server, .. }) = local.language_servers.get(server_id) - { - if let Some(watched_paths) = local + && let Some(watched_paths) = local .language_server_watched_paths .get(server_id) .and_then(|paths| paths.worktree_paths.get(&worktree_id)) - { - let params = lsp::DidChangeWatchedFilesParams { - changes: changes - .iter() - .filter_map(|(path, _, change)| { - if !watched_paths.is_match(path) { - return None; - } - let typ = match change { - PathChange::Loaded => return None, - PathChange::Added => lsp::FileChangeType::CREATED, - PathChange::Removed => lsp::FileChangeType::DELETED, - PathChange::Updated => lsp::FileChangeType::CHANGED, - PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED, - }; - Some(lsp::FileEvent { - uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(), - typ, - }) + { + let params = lsp::DidChangeWatchedFilesParams { + changes: changes + .iter() + .filter_map(|(path, _, change)| { + if !watched_paths.is_match(path) { + return None; + } + let typ = match change { + PathChange::Loaded => return None, + PathChange::Added => lsp::FileChangeType::CREATED, + PathChange::Removed => lsp::FileChangeType::DELETED, + PathChange::Updated => lsp::FileChangeType::CHANGED, + PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED, + }; + Some(lsp::FileEvent { + uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(), + typ, }) - .collect(), - }; - if !params.changes.is_empty() { - server - .notify::<lsp::notification::DidChangeWatchedFiles>(¶ms) - .ok(); - } + }) + .collect(), + }; + if !params.changes.is_empty() { + server + .notify::<lsp::notification::DidChangeWatchedFiles>(¶ms) + .ok(); } } } diff --git a/crates/project/src/manifest_tree/path_trie.rs b/crates/project/src/manifest_tree/path_trie.rs index 1a0736765a43b9e1365334de95eacbe9dbf64382..16110463ac096ae7f29ab1e46a51cfde2629774f 100644 --- a/crates/project/src/manifest_tree/path_trie.rs +++ b/crates/project/src/manifest_tree/path_trie.rs @@ -84,11 +84,11 @@ impl<Label: Ord + Clone> RootPathTrie<Label> { ) { let mut current = self; for key in path.0.iter() { - if !current.labels.is_empty() { - if (callback)(¤t.worktree_relative_path, ¤t.labels).is_break() { - return; - }; - } + if !current.labels.is_empty() + && (callback)(¤t.worktree_relative_path, ¤t.labels).is_break() + { + return; + }; current = match current.children.get(key) { Some(child) => child, None => return, diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index 29997545cd484d0bacbd489b3c5fa058daa2f017..3ae5dc24ae22e4b42138843a0f4b85b7ce2e7ebd 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -590,8 +590,8 @@ impl PrettierStore { new_plugins.clear(); } let mut needs_install = should_write_prettier_server_file(fs.as_ref()).await; - if let Some(previous_installation_task) = previous_installation_task { - if let Err(e) = previous_installation_task.await { + if let Some(previous_installation_task) = previous_installation_task + && let Err(e) = previous_installation_task.await { log::error!("Failed to install default prettier: {e:#}"); prettier_store.update(cx, |prettier_store, _| { if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut prettier_store.default_prettier.prettier { @@ -601,8 +601,7 @@ impl PrettierStore { needs_install = true; }; })?; - } - }; + }; if installation_attempt > prettier::FAIL_THRESHOLD { prettier_store.update(cx, |prettier_store, _| { if let PrettierInstallation::NotInstalled { installation_task, .. } = &mut prettier_store.default_prettier.prettier { @@ -679,13 +678,13 @@ impl PrettierStore { ) { let mut prettier_plugins_by_worktree = HashMap::default(); for (worktree, language_settings) in language_formatters_to_check { - if language_settings.prettier.allowed { - if let Some(plugins) = prettier_plugins_for_language(&language_settings) { - prettier_plugins_by_worktree - .entry(worktree) - .or_insert_with(HashSet::default) - .extend(plugins.iter().cloned()); - } + if language_settings.prettier.allowed + && let Some(plugins) = prettier_plugins_for_language(&language_settings) + { + prettier_plugins_by_worktree + .entry(worktree) + .or_insert_with(HashSet::default) + .extend(plugins.iter().cloned()); } } for (worktree, prettier_plugins) in prettier_plugins_by_worktree { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 17997850b630dbc8dd05c97d24c904198ceb7b75..3906befee23f5fe2d4c3761d8ae91b37b98daa43 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -489,67 +489,63 @@ impl CompletionSource { .. } = self { - if apply_defaults { - if let Some(lsp_defaults) = lsp_defaults { - let mut completion_with_defaults = *lsp_completion.clone(); - let default_commit_characters = lsp_defaults.commit_characters.as_ref(); - let default_edit_range = lsp_defaults.edit_range.as_ref(); - let default_insert_text_format = lsp_defaults.insert_text_format.as_ref(); - let default_insert_text_mode = lsp_defaults.insert_text_mode.as_ref(); - - if default_commit_characters.is_some() - || default_edit_range.is_some() - || default_insert_text_format.is_some() - || default_insert_text_mode.is_some() + if apply_defaults && let Some(lsp_defaults) = lsp_defaults { + let mut completion_with_defaults = *lsp_completion.clone(); + let default_commit_characters = lsp_defaults.commit_characters.as_ref(); + let default_edit_range = lsp_defaults.edit_range.as_ref(); + let default_insert_text_format = lsp_defaults.insert_text_format.as_ref(); + let default_insert_text_mode = lsp_defaults.insert_text_mode.as_ref(); + + if default_commit_characters.is_some() + || default_edit_range.is_some() + || default_insert_text_format.is_some() + || default_insert_text_mode.is_some() + { + if completion_with_defaults.commit_characters.is_none() + && default_commit_characters.is_some() { - if completion_with_defaults.commit_characters.is_none() - && default_commit_characters.is_some() - { - completion_with_defaults.commit_characters = - default_commit_characters.cloned() - } - if completion_with_defaults.text_edit.is_none() { - match default_edit_range { - Some(lsp::CompletionListItemDefaultsEditRange::Range(range)) => { - completion_with_defaults.text_edit = - Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: *range, + completion_with_defaults.commit_characters = + default_commit_characters.cloned() + } + if completion_with_defaults.text_edit.is_none() { + match default_edit_range { + Some(lsp::CompletionListItemDefaultsEditRange::Range(range)) => { + completion_with_defaults.text_edit = + Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: *range, + new_text: completion_with_defaults.label.clone(), + })) + } + Some(lsp::CompletionListItemDefaultsEditRange::InsertAndReplace { + insert, + replace, + }) => { + completion_with_defaults.text_edit = + Some(lsp::CompletionTextEdit::InsertAndReplace( + lsp::InsertReplaceEdit { new_text: completion_with_defaults.label.clone(), - })) - } - Some( - lsp::CompletionListItemDefaultsEditRange::InsertAndReplace { - insert, - replace, - }, - ) => { - completion_with_defaults.text_edit = - Some(lsp::CompletionTextEdit::InsertAndReplace( - lsp::InsertReplaceEdit { - new_text: completion_with_defaults.label.clone(), - insert: *insert, - replace: *replace, - }, - )) - } - None => {} + insert: *insert, + replace: *replace, + }, + )) } - } - if completion_with_defaults.insert_text_format.is_none() - && default_insert_text_format.is_some() - { - completion_with_defaults.insert_text_format = - default_insert_text_format.cloned() - } - if completion_with_defaults.insert_text_mode.is_none() - && default_insert_text_mode.is_some() - { - completion_with_defaults.insert_text_mode = - default_insert_text_mode.cloned() + None => {} } } - return Some(Cow::Owned(completion_with_defaults)); + if completion_with_defaults.insert_text_format.is_none() + && default_insert_text_format.is_some() + { + completion_with_defaults.insert_text_format = + default_insert_text_format.cloned() + } + if completion_with_defaults.insert_text_mode.is_none() + && default_insert_text_mode.is_some() + { + completion_with_defaults.insert_text_mode = + default_insert_text_mode.cloned() + } } + return Some(Cow::Owned(completion_with_defaults)); } Some(Cow::Borrowed(lsp_completion)) } else { @@ -2755,11 +2751,12 @@ impl Project { operations, })) })?; - if let Some(request) = request { - if request.await.is_err() && !is_local { - *needs_resync_with_host = true; - break; - } + if let Some(request) = request + && request.await.is_err() + && !is_local + { + *needs_resync_with_host = true; + break; } } Ok(()) @@ -3939,10 +3936,10 @@ impl Project { if let Some(entry) = b .entry_id(cx) .and_then(|entry_id| worktree_store.entry_for_id(entry_id, cx)) + && entry.is_ignored + && !search_query.include_ignored() { - if entry.is_ignored && !search_query.include_ignored() { - return false; - } + return false; } } true @@ -4151,11 +4148,11 @@ impl Project { ) -> Task<Option<ResolvedPath>> { let mut candidates = vec![path.clone()]; - if let Some(file) = buffer.read(cx).file() { - if let Some(dir) = file.path().parent() { - let joined = dir.to_path_buf().join(path); - candidates.push(joined); - } + if let Some(file) = buffer.read(cx).file() + && let Some(dir) = file.path().parent() + { + let joined = dir.to_path_buf().join(path); + candidates.push(joined); } let buffer_worktree_id = buffer.read(cx).file().map(|file| file.worktree_id(cx)); @@ -4168,16 +4165,14 @@ impl Project { .collect(); cx.spawn(async move |_, cx| { - if let Some(buffer_worktree_id) = buffer_worktree_id { - if let Some((worktree, _)) = worktrees_with_ids + if let Some(buffer_worktree_id) = buffer_worktree_id + && let Some((worktree, _)) = worktrees_with_ids .iter() .find(|(_, id)| *id == buffer_worktree_id) - { - for candidate in candidates.iter() { - if let Some(path) = Self::resolve_path_in_worktree(worktree, candidate, cx) - { - return Some(path); - } + { + for candidate in candidates.iter() { + if let Some(path) = Self::resolve_path_in_worktree(worktree, candidate, cx) { + return Some(path); } } } diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 4f024837c8be8946c8feb00f398779b604afbbf0..ee216a99763282d8d30a0b17c3df3b8da3213db7 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -155,16 +155,16 @@ impl SearchQuery { let initial_query = Arc::from(query.as_str()); if whole_word { let mut word_query = String::new(); - if let Some(first) = query.get(0..1) { - if WORD_MATCH_TEST.is_match(first).is_ok_and(|x| !x) { - word_query.push_str("\\b"); - } + if let Some(first) = query.get(0..1) + && WORD_MATCH_TEST.is_match(first).is_ok_and(|x| !x) + { + word_query.push_str("\\b"); } word_query.push_str(&query); - if let Some(last) = query.get(query.len() - 1..) { - if WORD_MATCH_TEST.is_match(last).is_ok_and(|x| !x) { - word_query.push_str("\\b"); - } + if let Some(last) = query.get(query.len() - 1..) + && WORD_MATCH_TEST.is_match(last).is_ok_and(|x| !x) + { + word_query.push_str("\\b"); } query = word_query } diff --git a/crates/project/src/search_history.rs b/crates/project/src/search_history.rs index 90b169bb0c5c83a9eb722c964bfb549ead1d5494..401f375094ea1052ce5a38252b9fa4f0943810b4 100644 --- a/crates/project/src/search_history.rs +++ b/crates/project/src/search_history.rs @@ -45,20 +45,19 @@ impl SearchHistory { } pub fn add(&mut self, cursor: &mut SearchHistoryCursor, search_string: String) { - if self.insertion_behavior == QueryInsertionBehavior::ReplacePreviousIfContains { - if let Some(previously_searched) = self.history.back_mut() { - if search_string.contains(previously_searched.as_str()) { - *previously_searched = search_string; - cursor.selection = Some(self.history.len() - 1); - return; - } - } + if self.insertion_behavior == QueryInsertionBehavior::ReplacePreviousIfContains + && let Some(previously_searched) = self.history.back_mut() + && search_string.contains(previously_searched.as_str()) + { + *previously_searched = search_string; + cursor.selection = Some(self.history.len() - 1); + return; } - if let Some(max_history_len) = self.max_history_len { - if self.history.len() >= max_history_len { - self.history.pop_front(); - } + if let Some(max_history_len) = self.max_history_len + && self.history.len() >= max_history_len + { + self.history.pop_front(); } self.history.push_back(search_string); diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index f5d08990b5723b43749ec01de4100432f803191f..5f98a10c75d5e66dff428e25a3c368cc8b2ac519 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -119,13 +119,13 @@ impl Project { }; let mut settings_location = None; - if let Some(path) = path.as_ref() { - if let Some((worktree, _)) = self.find_worktree(path, cx) { - settings_location = Some(SettingsLocation { - worktree_id: worktree.read(cx).id(), - path, - }); - } + if let Some(path) = path.as_ref() + && let Some((worktree, _)) = self.find_worktree(path, cx) + { + settings_location = Some(SettingsLocation { + worktree_id: worktree.read(cx).id(), + path, + }); } let venv = TerminalSettings::get(settings_location, cx) .detect_venv @@ -151,13 +151,13 @@ impl Project { cx: &'a App, ) -> &'a TerminalSettings { let mut settings_location = None; - if let Some(path) = path.as_ref() { - if let Some((worktree, _)) = self.find_worktree(path, cx) { - settings_location = Some(SettingsLocation { - worktree_id: worktree.read(cx).id(), - path, - }); - } + if let Some(path) = path.as_ref() + && let Some((worktree, _)) = self.find_worktree(path, cx) + { + settings_location = Some(SettingsLocation { + worktree_id: worktree.read(cx).id(), + path, + }); } TerminalSettings::get(settings_location, cx) } @@ -239,13 +239,13 @@ impl Project { let is_ssh_terminal = ssh_details.is_some(); let mut settings_location = None; - if let Some(path) = path.as_ref() { - if let Some((worktree, _)) = this.find_worktree(path, cx) { - settings_location = Some(SettingsLocation { - worktree_id: worktree.read(cx).id(), - path, - }); - } + if let Some(path) = path.as_ref() + && let Some((worktree, _)) = this.find_worktree(path, cx) + { + settings_location = Some(SettingsLocation { + worktree_id: worktree.read(cx).id(), + path, + }); } let settings = TerminalSettings::get(settings_location, cx).clone(); @@ -665,11 +665,11 @@ pub fn wrap_for_ssh( env_changes.push_str(&format!("{}={} ", k, v)); } } - if let Some(venv_directory) = venv_directory { - if let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref()) { - let path = RemotePathBuf::new(PathBuf::from(str.to_string()), path_style).to_string(); - env_changes.push_str(&format!("PATH={}:$PATH ", path)); - } + if let Some(venv_directory) = venv_directory + && let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref()) + { + let path = RemotePathBuf::new(PathBuf::from(str.to_string()), path_style).to_string(); + env_changes.push_str(&format!("PATH={}:$PATH ", path)); } let commands = if let Some(path) = path { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 892847a380bf1522750720ec59f74ed8db6e99dc..dd6b081e98f0fc3b5b88e905c21e85512d0a7eea 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -652,8 +652,8 @@ impl ProjectPanel { focus_opened_item, allow_preview, } => { - if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { - if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) + && let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { let file_path = entry.path.clone(); let worktree_id = worktree.read(cx).id(); let entry_id = entry.id; @@ -703,11 +703,10 @@ impl ProjectPanel { } } } - } } &Event::SplitEntry { entry_id } => { - if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { - if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) + && let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { workspace .split_path( ProjectPath { @@ -718,7 +717,6 @@ impl ProjectPanel { ) .detach_and_log_err(cx); } - } } _ => {} @@ -1002,10 +1000,10 @@ impl ProjectPanel { if let Some(parent_path) = entry.path.parent() { let snapshot = worktree.snapshot(); let mut child_entries = snapshot.child_entries(parent_path); - if let Some(child) = child_entries.next() { - if child_entries.next().is_none() { - return child.kind.is_dir(); - } + if let Some(child) = child_entries.next() + && child_entries.next().is_none() + { + return child.kind.is_dir(); } }; false @@ -1016,10 +1014,10 @@ impl ProjectPanel { let snapshot = worktree.snapshot(); let mut child_entries = snapshot.child_entries(&entry.path); - if let Some(child) = child_entries.next() { - if child_entries.next().is_none() { - return child.kind.is_dir(); - } + if let Some(child) = child_entries.next() + && child_entries.next().is_none() + { + return child.kind.is_dir(); } } false @@ -1032,12 +1030,12 @@ impl ProjectPanel { cx: &mut Context<Self>, ) { if let Some((worktree, entry)) = self.selected_entry(cx) { - if let Some(folded_ancestors) = self.ancestors.get_mut(&entry.id) { - if folded_ancestors.current_ancestor_depth > 0 { - folded_ancestors.current_ancestor_depth -= 1; - cx.notify(); - return; - } + if let Some(folded_ancestors) = self.ancestors.get_mut(&entry.id) + && folded_ancestors.current_ancestor_depth > 0 + { + folded_ancestors.current_ancestor_depth -= 1; + cx.notify(); + return; } if entry.is_dir() { let worktree_id = worktree.id(); @@ -1079,12 +1077,12 @@ impl ProjectPanel { fn collapse_entry(&mut self, entry: Entry, worktree: Entity<Worktree>, cx: &mut Context<Self>) { let worktree = worktree.read(cx); - if let Some(folded_ancestors) = self.ancestors.get_mut(&entry.id) { - if folded_ancestors.current_ancestor_depth + 1 < folded_ancestors.max_ancestor_depth() { - folded_ancestors.current_ancestor_depth += 1; - cx.notify(); - return; - } + if let Some(folded_ancestors) = self.ancestors.get_mut(&entry.id) + && folded_ancestors.current_ancestor_depth + 1 < folded_ancestors.max_ancestor_depth() + { + folded_ancestors.current_ancestor_depth += 1; + cx.notify(); + return; } let worktree_id = worktree.id(); let expanded_dir_ids = @@ -1137,23 +1135,23 @@ impl ProjectPanel { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { - if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { - self.project.update(cx, |project, cx| { - match expanded_dir_ids.binary_search(&entry_id) { - Ok(ix) => { - expanded_dir_ids.remove(ix); - } - Err(ix) => { - project.expand_entry(worktree_id, entry_id, cx); - expanded_dir_ids.insert(ix, entry_id); - } + if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) + && let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) + { + self.project.update(cx, |project, cx| { + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); } - }); - self.update_visible_entries(Some((worktree_id, entry_id)), cx); - window.focus(&self.focus_handle); - cx.notify(); - } + Err(ix) => { + project.expand_entry(worktree_id, entry_id, cx); + expanded_dir_ids.insert(ix, entry_id); + } + } + }); + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + window.focus(&self.focus_handle); + cx.notify(); } } @@ -1163,20 +1161,20 @@ impl ProjectPanel { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { - if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { - match expanded_dir_ids.binary_search(&entry_id) { - Ok(_ix) => { - self.collapse_all_for_entry(worktree_id, entry_id, cx); - } - Err(_ix) => { - self.expand_all_for_entry(worktree_id, entry_id, cx); - } + if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) + && let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) + { + match expanded_dir_ids.binary_search(&entry_id) { + Ok(_ix) => { + self.collapse_all_for_entry(worktree_id, entry_id, cx); + } + Err(_ix) => { + self.expand_all_for_entry(worktree_id, entry_id, cx); } - self.update_visible_entries(Some((worktree_id, entry_id)), cx); - window.focus(&self.focus_handle); - cx.notify(); } + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + window.focus(&self.focus_handle); + cx.notify(); } } @@ -1251,20 +1249,20 @@ impl ProjectPanel { } fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) { - if let Some(edit_state) = &self.edit_state { - if edit_state.processing_filename.is_none() { - self.filename_editor.update(cx, |editor, cx| { - editor.move_to_beginning_of_line( - &editor::actions::MoveToBeginningOfLine { - stop_at_soft_wraps: false, - stop_at_indent: false, - }, - window, - cx, - ); - }); - return; - } + if let Some(edit_state) = &self.edit_state + && edit_state.processing_filename.is_none() + { + self.filename_editor.update(cx, |editor, cx| { + editor.move_to_beginning_of_line( + &editor::actions::MoveToBeginningOfLine { + stop_at_soft_wraps: false, + stop_at_indent: false, + }, + window, + cx, + ); + }); + return; } if let Some(selection) = self.selection { let (mut worktree_ix, mut entry_ix, _) = @@ -1341,39 +1339,37 @@ impl ProjectPanel { .project .read(cx) .worktree_for_id(edit_state.worktree_id, cx) + && let Some(entry) = worktree.read(cx).entry_for_id(edit_state.entry_id) { - if let Some(entry) = worktree.read(cx).entry_for_id(edit_state.entry_id) { - let mut already_exists = false; - if edit_state.is_new_entry() { - let new_path = entry.path.join(filename.trim_start_matches('/')); - if worktree - .read(cx) - .entry_for_path(new_path.as_path()) - .is_some() - { - already_exists = true; - } + let mut already_exists = false; + if edit_state.is_new_entry() { + let new_path = entry.path.join(filename.trim_start_matches('/')); + if worktree + .read(cx) + .entry_for_path(new_path.as_path()) + .is_some() + { + already_exists = true; + } + } else { + let new_path = if let Some(parent) = entry.path.clone().parent() { + parent.join(&filename) } else { - let new_path = if let Some(parent) = entry.path.clone().parent() { - parent.join(&filename) - } else { - filename.clone().into() - }; - if let Some(existing) = worktree.read(cx).entry_for_path(new_path.as_path()) - { - if existing.id != entry.id { - already_exists = true; - } - } + filename.clone().into() }; - if already_exists { - edit_state.validation_state = ValidationState::Error(format!( - "File or directory '{}' already exists at location. Please choose a different name.", - filename - )); - cx.notify(); - return; + if let Some(existing) = worktree.read(cx).entry_for_path(new_path.as_path()) + && existing.id != entry.id + { + already_exists = true; } + }; + if already_exists { + edit_state.validation_state = ValidationState::Error(format!( + "File or directory '{}' already exists at location. Please choose a different name.", + filename + )); + cx.notify(); + return; } } let trimmed_filename = filename.trim(); @@ -1477,14 +1473,13 @@ impl ProjectPanel { } Ok(CreatedEntry::Included(new_entry)) => { project_panel.update( cx, |project_panel, cx| { - if let Some(selection) = &mut project_panel.selection { - if selection.entry_id == edited_entry_id { + if let Some(selection) = &mut project_panel.selection + && selection.entry_id == edited_entry_id { selection.worktree_id = worktree_id; selection.entry_id = new_entry.id; project_panel.marked_entries.clear(); project_panel.expand_to_selection(cx); } - } project_panel.update_visible_entries(None, cx); if is_new_entry && !is_dir { project_panel.open_entry(new_entry.id, true, false, cx); @@ -1617,11 +1612,11 @@ impl ProjectPanel { directory_id = entry.id; break; } else { - if let Some(parent_path) = entry.path.parent() { - if let Some(parent_entry) = worktree.entry_for_path(parent_path) { - entry = parent_entry; - continue; - } + if let Some(parent_path) = entry.path.parent() + && let Some(parent_entry) = worktree.entry_for_path(parent_path) + { + entry = parent_entry; + continue; } return; } @@ -1675,57 +1670,56 @@ impl ProjectPanel { worktree_id, entry_id, }) = self.selection + && let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) { - if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) { - let sub_entry_id = self.unflatten_entry_id(entry_id); - if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) { - #[cfg(target_os = "windows")] - if Some(entry) == worktree.read(cx).root_entry() { - return; - } + let sub_entry_id = self.unflatten_entry_id(entry_id); + if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) { + #[cfg(target_os = "windows")] + if Some(entry) == worktree.read(cx).root_entry() { + return; + } - if Some(entry) == worktree.read(cx).root_entry() { - let settings = ProjectPanelSettings::get_global(cx); - let visible_worktrees_count = - self.project.read(cx).visible_worktrees(cx).count(); - if settings.hide_root && visible_worktrees_count == 1 { - return; - } + if Some(entry) == worktree.read(cx).root_entry() { + let settings = ProjectPanelSettings::get_global(cx); + let visible_worktrees_count = + self.project.read(cx).visible_worktrees(cx).count(); + if settings.hide_root && visible_worktrees_count == 1 { + return; } + } - self.edit_state = Some(EditState { - worktree_id, - entry_id: sub_entry_id, - leaf_entry_id: Some(entry_id), - is_dir: entry.is_dir(), - processing_filename: None, - previously_focused: None, - depth: 0, - validation_state: ValidationState::None, - }); - let file_name = entry - .path - .file_name() - .map(|s| s.to_string_lossy()) - .unwrap_or_default() - .to_string(); - let selection = selection.unwrap_or_else(|| { - let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy()); - let selection_end = - file_stem.map_or(file_name.len(), |file_stem| file_stem.len()); - 0..selection_end - }); - self.filename_editor.update(cx, |editor, cx| { - editor.set_text(file_name, window, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges([selection]) - }); - window.focus(&editor.focus_handle(cx)); + self.edit_state = Some(EditState { + worktree_id, + entry_id: sub_entry_id, + leaf_entry_id: Some(entry_id), + is_dir: entry.is_dir(), + processing_filename: None, + previously_focused: None, + depth: 0, + validation_state: ValidationState::None, + }); + let file_name = entry + .path + .file_name() + .map(|s| s.to_string_lossy()) + .unwrap_or_default() + .to_string(); + let selection = selection.unwrap_or_else(|| { + let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy()); + let selection_end = + file_stem.map_or(file_name.len(), |file_stem| file_stem.len()); + 0..selection_end + }); + self.filename_editor.update(cx, |editor, cx| { + editor.set_text(file_name, window, cx); + editor.change_selections(Default::default(), window, cx, |s| { + s.select_ranges([selection]) }); - self.update_visible_entries(None, cx); - self.autoscroll(cx); - cx.notify(); - } + window.focus(&editor.focus_handle(cx)); + }); + self.update_visible_entries(None, cx); + self.autoscroll(cx); + cx.notify(); } } } @@ -1831,10 +1825,10 @@ impl ProjectPanel { }; let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx); cx.spawn_in(window, async move |panel, cx| { - if let Some(answer) = answer { - if answer.await != Ok(0) { - return anyhow::Ok(()); - } + if let Some(answer) = answer + && answer.await != Ok(0) + { + return anyhow::Ok(()); } for (entry_id, _) in file_paths { panel @@ -1999,19 +1993,19 @@ impl ProjectPanel { } fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) { - if let Some(edit_state) = &self.edit_state { - if edit_state.processing_filename.is_none() { - self.filename_editor.update(cx, |editor, cx| { - editor.move_to_end_of_line( - &editor::actions::MoveToEndOfLine { - stop_at_soft_wraps: false, - }, - window, - cx, - ); - }); - return; - } + if let Some(edit_state) = &self.edit_state + && edit_state.processing_filename.is_none() + { + self.filename_editor.update(cx, |editor, cx| { + editor.move_to_end_of_line( + &editor::actions::MoveToEndOfLine { + stop_at_soft_wraps: false, + }, + window, + cx, + ); + }); + return; } if let Some(selection) = self.selection { let (mut worktree_ix, mut entry_ix, _) = @@ -2032,20 +2026,19 @@ impl ProjectPanel { entries, .. }) = self.visible_entries.get(worktree_ix) + && let Some(entry) = entries.get(entry_ix) { - if let Some(entry) = entries.get(entry_ix) { - let selection = SelectedEntry { - worktree_id: *worktree_id, - entry_id: entry.id, - }; - self.selection = Some(selection); - if window.modifiers().shift { - self.marked_entries.push(selection); - } - - self.autoscroll(cx); - cx.notify(); + let selection = SelectedEntry { + worktree_id: *worktree_id, + entry_id: entry.id, + }; + self.selection = Some(selection); + if window.modifiers().shift { + self.marked_entries.push(selection); } + + self.autoscroll(cx); + cx.notify(); } } else { self.select_first(&SelectFirst {}, window, cx); @@ -2274,19 +2267,18 @@ impl ProjectPanel { entries, .. }) = self.visible_entries.first() + && let Some(entry) = entries.first() { - if let Some(entry) = entries.first() { - let selection = SelectedEntry { - worktree_id: *worktree_id, - entry_id: entry.id, - }; - self.selection = Some(selection); - if window.modifiers().shift { - self.marked_entries.push(selection); - } - self.autoscroll(cx); - cx.notify(); + let selection = SelectedEntry { + worktree_id: *worktree_id, + entry_id: entry.id, + }; + self.selection = Some(selection); + if window.modifiers().shift { + self.marked_entries.push(selection); } + self.autoscroll(cx); + cx.notify(); } } @@ -2947,10 +2939,10 @@ impl ProjectPanel { let Some(entry) = worktree.entry_for_path(path) else { continue; }; - if entry.is_dir() { - if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) { - expanded_dir_ids.insert(idx, entry.id); - } + if entry.is_dir() + && let Err(idx) = expanded_dir_ids.binary_search(&entry.id) + { + expanded_dir_ids.insert(idx, entry.id); } } @@ -3024,15 +3016,16 @@ impl ProjectPanel { let mut new_entry_parent_id = None; let mut new_entry_kind = EntryKind::Dir; - if let Some(edit_state) = &self.edit_state { - if edit_state.worktree_id == worktree_id && edit_state.is_new_entry() { - new_entry_parent_id = Some(edit_state.entry_id); - new_entry_kind = if edit_state.is_dir { - EntryKind::Dir - } else { - EntryKind::File - }; - } + if let Some(edit_state) = &self.edit_state + && edit_state.worktree_id == worktree_id + && edit_state.is_new_entry() + { + new_entry_parent_id = Some(edit_state.entry_id); + new_entry_kind = if edit_state.is_dir { + EntryKind::Dir + } else { + EntryKind::File + }; } let mut visible_worktree_entries = Vec::new(); @@ -3054,19 +3047,18 @@ impl ProjectPanel { } if auto_collapse_dirs && entry.kind.is_dir() { auto_folded_ancestors.push(entry.id); - if !self.unfolded_dir_ids.contains(&entry.id) { - if let Some(root_path) = worktree_snapshot.root_entry() { - let mut child_entries = worktree_snapshot.child_entries(&entry.path); - if let Some(child) = child_entries.next() { - if entry.path != root_path.path - && child_entries.next().is_none() - && child.kind.is_dir() - { - entry_iter.advance(); + if !self.unfolded_dir_ids.contains(&entry.id) + && let Some(root_path) = worktree_snapshot.root_entry() + { + let mut child_entries = worktree_snapshot.child_entries(&entry.path); + if let Some(child) = child_entries.next() + && entry.path != root_path.path + && child_entries.next().is_none() + && child.kind.is_dir() + { + entry_iter.advance(); - continue; - } - } + continue; } } let depth = old_ancestors @@ -3074,10 +3066,10 @@ impl ProjectPanel { .map(|ancestor| ancestor.current_ancestor_depth) .unwrap_or_default() .min(auto_folded_ancestors.len()); - if let Some(edit_state) = &mut self.edit_state { - if edit_state.entry_id == entry.id { - edit_state.depth = depth; - } + if let Some(edit_state) = &mut self.edit_state + && edit_state.entry_id == entry.id + { + edit_state.depth = depth; } let mut ancestors = std::mem::take(&mut auto_folded_ancestors); if ancestors.len() > 1 { @@ -3314,11 +3306,10 @@ impl ProjectPanel { ) })?.await?; - if answer == 1 { - if let Some(item_idx) = paths.iter().position(|p| p == original_path) { + if answer == 1 + && let Some(item_idx) = paths.iter().position(|p| p == original_path) { paths.remove(item_idx); } - } } if paths.is_empty() { @@ -4309,8 +4300,8 @@ impl ProjectPanel { } } else if kind.is_dir() { project_panel.marked_entries.clear(); - if is_sticky { - if let Some((_, _, index)) = project_panel.index_for_entry(entry_id, worktree_id) { + if is_sticky + && let Some((_, _, index)) = project_panel.index_for_entry(entry_id, worktree_id) { project_panel.scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, sticky_index.unwrap_or(0)); cx.notify(); // move down by 1px so that clicked item @@ -4325,7 +4316,6 @@ impl ProjectPanel { }); return; } - } if event.modifiers().alt { project_panel.toggle_expand_all(entry_id, window, cx); } else { @@ -4547,15 +4537,14 @@ impl ProjectPanel { }) }) .on_click(cx.listener(move |this, _, _, cx| { - if index != active_index { - if let Some(folds) = + if index != active_index + && let Some(folds) = this.ancestors.get_mut(&entry_id) { folds.current_ancestor_depth = components_len - 1 - index; cx.notify(); } - } })) .child( Label::new(component) @@ -5034,12 +5023,12 @@ impl ProjectPanel { 'outer: loop { if let Some(parent_path) = current_path.parent() { for ancestor_path in parent_path.ancestors() { - if paths.contains(ancestor_path) { - if let Some(parent_entry) = worktree.entry_for_path(ancestor_path) { - sticky_parents.push(parent_entry.clone()); - current_path = parent_entry.path.clone(); - continue 'outer; - } + if paths.contains(ancestor_path) + && let Some(parent_entry) = worktree.entry_for_path(ancestor_path) + { + sticky_parents.push(parent_entry.clone()); + current_path = parent_entry.path.clone(); + continue 'outer; } } } @@ -5291,25 +5280,25 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::paste)) .on_action(cx.listener(Self::duplicate)) .on_click(cx.listener(|this, event: &gpui::ClickEvent, window, cx| { - if event.click_count() > 1 { - if let Some(entry_id) = this.last_worktree_root_id { - let project = this.project.read(cx); + if event.click_count() > 1 + && let Some(entry_id) = this.last_worktree_root_id + { + let project = this.project.read(cx); - let worktree_id = if let Some(worktree) = - project.worktree_for_entry(entry_id, cx) - { - worktree.read(cx).id() - } else { - return; - }; + let worktree_id = if let Some(worktree) = + project.worktree_for_entry(entry_id, cx) + { + worktree.read(cx).id() + } else { + return; + }; - this.selection = Some(SelectedEntry { - worktree_id, - entry_id, - }); + this.selection = Some(SelectedEntry { + worktree_id, + entry_id, + }); - this.new_file(&NewFile, window, cx); - } + this.new_file(&NewFile, window, cx); } })) }) diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs index 7eb63eec5ea559432724622a7dc4ea5410cff62f..526d2c6a3428982429e9090ac70f3cb7f0021d74 100644 --- a/crates/prompt_store/src/prompts.rs +++ b/crates/prompt_store/src/prompts.rs @@ -261,13 +261,12 @@ impl PromptBuilder { // Initial scan of the prompt overrides directory if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await { while let Some(Ok(file_path)) = entries.next().await { - if file_path.to_string_lossy().ends_with(".hbs") { - if let Ok(content) = params.fs.load(&file_path).await { + if file_path.to_string_lossy().ends_with(".hbs") + && let Ok(content) = params.fs.load(&file_path).await { let file_name = file_path.file_stem().unwrap().to_string_lossy(); log::debug!("Registering prompt template override: {}", file_name); handlebars.lock().register_template_string(&file_name, content).log_err(); } - } } } @@ -280,13 +279,12 @@ impl PromptBuilder { let mut combined_changes = futures::stream::select(changes, parent_changes); while let Some(changed_paths) = combined_changes.next().await { - if changed_paths.iter().any(|p| &p.path == &templates_dir) { - if !params.fs.is_dir(&templates_dir).await { + if changed_paths.iter().any(|p| &p.path == &templates_dir) + && !params.fs.is_dir(&templates_dir).await { log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates."); Self::register_built_in_templates(&mut handlebars.lock()).log_err(); break; } - } for event in changed_paths { if event.path.starts_with(&templates_dir) && event.path.extension().map_or(false, |ext| ext == "hbs") { log::info!("Reloading prompt template override: {}", event.path.display()); @@ -311,12 +309,11 @@ impl PromptBuilder { .split('/') .next_back() .and_then(|s| s.strip_suffix(".hbs")) + && let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() { - if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() { - log::debug!("Registering built-in prompt template: {}", id); - let prompt = String::from_utf8_lossy(prompt.as_ref()); - handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))? - } + log::debug!("Registering built-in prompt template: {}", id); + let prompt = String::from_utf8_lossy(prompt.as_ref()); + handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))? } } diff --git a/crates/proto/src/error.rs b/crates/proto/src/error.rs index 7ba08df57d37b7d4a8b683eb08fce43acc084744..1724a7021750c2fb13c5dab504aad24a75a74c12 100644 --- a/crates/proto/src/error.rs +++ b/crates/proto/src/error.rs @@ -190,10 +190,10 @@ impl ErrorExt for RpcError { fn error_tag(&self, k: &str) -> Option<&str> { for tag in &self.tags { let mut parts = tag.split('='); - if let Some(key) = parts.next() { - if key == k { - return parts.next(); - } + if let Some(key) = parts.next() + && key == k + { + return parts.next(); } } None diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index bc837b1a1ea0cc5724f9e9d852f1dac978ee1d2c..0fd6d5af8c7396fa7bcc8d6fc5e9f7ba9f5f8b1a 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -664,10 +664,10 @@ impl RemoteServerProjects { let text = Some(state.editor.read(cx).text(cx)).filter(|text| !text.is_empty()); let index = state.index; self.update_settings_file(cx, move |setting, _| { - if let Some(connections) = setting.ssh_connections.as_mut() { - if let Some(connection) = connections.get_mut(index) { - connection.nickname = text; - } + if let Some(connections) = setting.ssh_connections.as_mut() + && let Some(connection) = connections.get_mut(index) + { + connection.nickname = text; } }); self.mode = Mode::default_mode(&self.ssh_config_servers, cx); diff --git a/crates/refineable/derive_refineable/src/derive_refineable.rs b/crates/refineable/derive_refineable/src/derive_refineable.rs index 3f6b45cc12c246dfd007b32be0d9734e41947e17..ddf3855a4dc5ae6917309ced57391bd244f1b465 100644 --- a/crates/refineable/derive_refineable/src/derive_refineable.rs +++ b/crates/refineable/derive_refineable/src/derive_refineable.rs @@ -510,12 +510,12 @@ fn is_refineable_field(f: &Field) -> bool { } fn is_optional_field(f: &Field) -> bool { - if let Type::Path(typepath) = &f.ty { - if typepath.qself.is_none() { - let segments = &typepath.path.segments; - if segments.len() == 1 && segments.iter().any(|s| s.ident == "Option") { - return true; - } + if let Type::Path(typepath) = &f.ty + && typepath.qself.is_none() + { + let segments = &typepath.path.segments; + if segments.len() == 1 && segments.iter().any(|s| s.ident == "Option") { + return true; } } false diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 71e8f6e8e758f47b82031cf2a817fd8d90569d60..2180fbb5ee68fa8f9960345d874c3f244019bd21 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -1310,10 +1310,10 @@ impl ConnectionPool { return task.clone(); } Some(ConnectionPoolEntry::Connected(ssh)) => { - if let Some(ssh) = ssh.upgrade() { - if !ssh.has_been_killed() { - return Task::ready(Ok(ssh)).shared(); - } + if let Some(ssh) = ssh.upgrade() + && !ssh.has_been_killed() + { + return Task::ready(Ok(ssh)).shared(); } self.connections.remove(&opts); } @@ -1840,26 +1840,25 @@ impl SshRemoteConnection { )), self.ssh_path_style, ); - if !self.socket.connection_options.upload_binary_over_ssh { - if let Some((url, body)) = delegate + if !self.socket.connection_options.upload_binary_over_ssh + && let Some((url, body)) = delegate .get_download_params(self.ssh_platform, release_channel, wanted_version, cx) .await? + { + match self + .download_binary_on_server(&url, &body, &tmp_path_gz, delegate, cx) + .await { - match self - .download_binary_on_server(&url, &body, &tmp_path_gz, delegate, cx) - .await - { - Ok(_) => { - self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) - .await?; - return Ok(dst_path); - } - Err(e) => { - log::error!( - "Failed to download binary on server, attempting to upload server: {}", - e - ) - } + Ok(_) => { + self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) + .await?; + return Ok(dst_path); + } + Err(e) => { + log::error!( + "Failed to download binary on server, attempting to upload server: {}", + e + ) } } } diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 4daacb3eec5d63987a3a9576153ba29a82e4fa32..76e74b75bdcf2b5cd14b6226f4c05f8e4c3ad12e 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -951,13 +951,13 @@ fn cleanup_old_binaries() -> Result<()> { for entry in std::fs::read_dir(server_dir)? { let path = entry?.path(); - if let Some(file_name) = path.file_name() { - if let Some(version) = file_name.to_string_lossy().strip_prefix(&prefix) { - if !is_new_version(version) && !is_file_in_use(file_name) { - log::info!("removing old remote server binary: {:?}", path); - std::fs::remove_file(&path)?; - } - } + if let Some(file_name) = path.file_name() + && let Some(version) = file_name.to_string_lossy().strip_prefix(&prefix) + && !is_new_version(version) + && !is_file_in_use(file_name) + { + log::info!("removing old remote server binary: {:?}", path); + std::fs::remove_file(&path)?; } } diff --git a/crates/repl/src/kernels/native_kernel.rs b/crates/repl/src/kernels/native_kernel.rs index aa6a81280940cd2510d1239ffdca75b04428b86b..83271fae16fd2d22f37fd03d86dae277070e20eb 100644 --- a/crates/repl/src/kernels/native_kernel.rs +++ b/crates/repl/src/kernels/native_kernel.rs @@ -399,10 +399,10 @@ async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> Result<Vec<LocalKernelS while let Some(path) = kernelspec_dirs.next().await { match path { Ok(path) => { - if fs.is_dir(path.as_path()).await { - if let Ok(kernelspec) = read_kernelspec_at(path, fs).await { - valid_kernelspecs.push(kernelspec); - } + if fs.is_dir(path.as_path()).await + && let Ok(kernelspec) = read_kernelspec_at(path, fs).await + { + valid_kernelspecs.push(kernelspec); } } Err(err) => log::warn!("Error reading kernelspec directory: {err:?}"), @@ -429,14 +429,14 @@ pub async fn local_kernel_specifications(fs: Arc<dyn Fs>) -> Result<Vec<LocalKer .output() .await; - if let Ok(command) = command { - if command.status.success() { - let python_prefix = String::from_utf8(command.stdout); - if let Ok(python_prefix) = python_prefix { - let python_prefix = PathBuf::from(python_prefix.trim()); - let python_data_dir = python_prefix.join("share").join("jupyter"); - data_dirs.push(python_data_dir); - } + if let Ok(command) = command + && command.status.success() + { + let python_prefix = String::from_utf8(command.stdout); + if let Ok(python_prefix) = python_prefix { + let python_prefix = PathBuf::from(python_prefix.trim()); + let python_data_dir = python_prefix.join("share").join("jupyter"); + data_dirs.push(python_data_dir); } } diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index ed252b239f61ba6726d25f2223d4cca3e981e865..1508c2b531cfe846ea74015342f4dc55d92c1e0c 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -412,10 +412,10 @@ impl ExecutionView { }; // Check for a clear output marker as the previous output, so we can clear it out - if let Some(output) = self.outputs.last() { - if let Output::ClearOutputWaitMarker = output { - self.outputs.clear(); - } + if let Some(output) = self.outputs.last() + && let Output::ClearOutputWaitMarker = output + { + self.outputs.clear(); } self.outputs.push(output); @@ -433,11 +433,11 @@ impl ExecutionView { let mut any = false; self.outputs.iter_mut().for_each(|output| { - if let Some(other_display_id) = output.display_id().as_ref() { - if other_display_id == display_id { - *output = Output::new(data, Some(display_id.to_owned()), window, cx); - any = true; - } + if let Some(other_display_id) = output.display_id().as_ref() + && other_display_id == display_id + { + *output = Output::new(data, Some(display_id.to_owned()), window, cx); + any = true; } }); @@ -452,19 +452,18 @@ impl ExecutionView { window: &mut Window, cx: &mut Context<Self>, ) -> Option<Output> { - if let Some(last_output) = self.outputs.last_mut() { - if let Output::Stream { + if let Some(last_output) = self.outputs.last_mut() + && let Output::Stream { content: last_stream, } = last_output - { - // Don't need to add a new output, we already have a terminal output - // and can just update the most recent terminal output - last_stream.update(cx, |last_stream, cx| { - last_stream.append_text(text, cx); - cx.notify(); - }); - return None; - } + { + // Don't need to add a new output, we already have a terminal output + // and can just update the most recent terminal output + last_stream.update(cx, |last_stream, cx| { + last_stream.append_text(text, cx); + cx.notify(); + }); + return None; } Some(Output::Stream { diff --git a/crates/repl/src/repl_editor.rs b/crates/repl/src/repl_editor.rs index 32b59d639dfe903ab9df520d441b1c9c736b4b25..f5dd6595979f4c2fbe805d3a8d00d2dc876c31ea 100644 --- a/crates/repl/src/repl_editor.rs +++ b/crates/repl/src/repl_editor.rs @@ -417,10 +417,10 @@ fn runnable_ranges( range: Range<Point>, cx: &mut App, ) -> (Vec<Range<Point>>, Option<Point>) { - if let Some(language) = buffer.language() { - if language.name() == "Markdown".into() { - return (markdown_code_blocks(buffer, range.clone(), cx), None); - } + if let Some(language) = buffer.language() + && language.name() == "Markdown".into() + { + return (markdown_code_blocks(buffer, range.clone(), cx), None); } let (jupytext_snippets, next_cursor) = jupytext_cells(buffer, range.clone()); diff --git a/crates/repl/src/repl_sessions_ui.rs b/crates/repl/src/repl_sessions_ui.rs index 2f4c1f86fc5d9d4baaa005e745d70161327473ca..f57dd64770f27e6e7b7360a16c3a9ec21912bc86 100644 --- a/crates/repl/src/repl_sessions_ui.rs +++ b/crates/repl/src/repl_sessions_ui.rs @@ -102,21 +102,16 @@ pub fn init(cx: &mut App) { let editor_handle = cx.entity().downgrade(); - if let Some(language) = language { - if language.name() == "Python".into() { - if let (Some(project_path), Some(project)) = (project_path, project) { - let store = ReplStore::global(cx); - store.update(cx, |store, cx| { - store - .refresh_python_kernelspecs( - project_path.worktree_id, - &project, - cx, - ) - .detach_and_log_err(cx); - }); - } - } + if let Some(language) = language + && language.name() == "Python".into() + && let (Some(project_path), Some(project)) = (project_path, project) + { + let store = ReplStore::global(cx); + store.update(cx, |store, cx| { + store + .refresh_python_kernelspecs(project_path.worktree_id, &project, cx) + .detach_and_log_err(cx); + }); } editor diff --git a/crates/repl/src/repl_store.rs b/crates/repl/src/repl_store.rs index 1a3c9fa49a5e6951949281ae8020a500b5293cd2..b9a36a18aec44a3460e099858cc33360b76ee4f9 100644 --- a/crates/repl/src/repl_store.rs +++ b/crates/repl/src/repl_store.rs @@ -171,10 +171,10 @@ impl ReplStore { .map(KernelSpecification::Jupyter) .collect::<Vec<_>>(); - if let Some(remote_task) = remote_kernel_specifications { - if let Ok(remote_specs) = remote_task.await { - all_specs.extend(remote_specs); - } + if let Some(remote_task) = remote_kernel_specifications + && let Ok(remote_specs) = remote_task.await + { + all_specs.extend(remote_specs); } anyhow::Ok(all_specs) diff --git a/crates/reqwest_client/src/reqwest_client.rs b/crates/reqwest_client/src/reqwest_client.rs index 6461a0ae17d288a9fe282cda39ea1af9ba297e21..9053f4e452790dfe06fd4f3559b32be72740d56b 100644 --- a/crates/reqwest_client/src/reqwest_client.rs +++ b/crates/reqwest_client/src/reqwest_client.rs @@ -201,12 +201,11 @@ pub fn poll_read_buf( } fn redact_error(mut error: reqwest::Error) -> reqwest::Error { - if let Some(url) = error.url_mut() { - if let Some(query) = url.query() { - if let Cow::Owned(redacted) = REDACT_REGEX.replace_all(query, "key=REDACTED") { - url.set_query(Some(redacted.as_str())); - } - } + if let Some(url) = error.url_mut() + && let Some(query) = url.query() + && let Cow::Owned(redacted) = REDACT_REGEX.replace_all(query, "key=REDACTED") + { + url.set_query(Some(redacted.as_str())); } error } diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs index 575e4318c30ba5685fd5a869519ef61bb9e2591f..2af9988f032c5dc9651e1da6e8c3b52c6c668866 100644 --- a/crates/rich_text/src/rich_text.rs +++ b/crates/rich_text/src/rich_text.rs @@ -162,10 +162,10 @@ impl RichText { } } for range in &custom_tooltip_ranges { - if range.contains(&idx) { - if let Some(f) = &custom_tooltip_fn { - return f(idx, range.clone(), window, cx); - } + if range.contains(&idx) + && let Some(f) = &custom_tooltip_fn + { + return f(idx, range.clone(), window, cx); } } None @@ -281,13 +281,12 @@ pub fn render_markdown_mut( if style != HighlightStyle::default() && last_run_len < text.len() { let mut new_highlight = true; - if let Some((last_range, last_style)) = highlights.last_mut() { - if last_range.end == last_run_len - && last_style == &Highlight::Highlight(style) - { - last_range.end = text.len(); - new_highlight = false; - } + if let Some((last_range, last_style)) = highlights.last_mut() + && last_range.end == last_run_len + && last_style == &Highlight::Highlight(style) + { + last_range.end = text.len(); + new_highlight = false; } if new_highlight { highlights diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index d8ed3bfac86bdbb360f6a161242aa874a0fd51af..78ce6f78a22b786bec4f8880282e25fbb2c9fc93 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -31,22 +31,21 @@ impl Rope { } pub fn append(&mut self, rope: Rope) { - if let Some(chunk) = rope.chunks.first() { - if self + if let Some(chunk) = rope.chunks.first() + && (self .chunks .last() .map_or(false, |c| c.text.len() < chunk::MIN_BASE) - || chunk.text.len() < chunk::MIN_BASE - { - self.push_chunk(chunk.as_slice()); - - let mut chunks = rope.chunks.cursor::<()>(&()); - chunks.next(); - chunks.next(); - self.chunks.append(chunks.suffix(), &()); - self.check_invariants(); - return; - } + || chunk.text.len() < chunk::MIN_BASE) + { + self.push_chunk(chunk.as_slice()); + + let mut chunks = rope.chunks.cursor::<()>(&()); + chunks.next(); + chunks.next(); + self.chunks.append(chunks.suffix(), &()); + self.check_invariants(); + return; } self.chunks.append(rope.chunks.clone(), &()); @@ -735,16 +734,16 @@ impl<'a> Chunks<'a> { self.chunks .search_backward(|summary| summary.text.lines.row > 0); self.offset = *self.chunks.start(); - if let Some(chunk) = self.chunks.item() { - if let Some(newline_ix) = chunk.text.rfind('\n') { - self.offset += newline_ix + 1; - if self.offset_is_valid() { - if self.offset == self.chunks.end() { - self.chunks.next(); - } - - return true; + if let Some(chunk) = self.chunks.item() + && let Some(newline_ix) = chunk.text.rfind('\n') + { + self.offset += newline_ix + 1; + if self.offset_is_valid() { + if self.offset == self.chunks.end() { + self.chunks.next(); } + + return true; } } diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index cb405b63cafaaa76e86820518263e0c9c5544a39..338ef33c8abf7bc694a07ecacfbdb94711a6924b 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -48,10 +48,10 @@ impl Notification { let Some(Value::String(kind)) = value.remove(KIND) else { unreachable!("kind is the enum tag") }; - if let map::Entry::Occupied(e) = value.entry(ENTITY_ID) { - if e.get().is_u64() { - entity_id = e.remove().as_u64(); - } + if let map::Entry::Occupied(e) = value.entry(ENTITY_ID) + && e.get().is_u64() + { + entity_id = e.remove().as_u64(); } proto::Notification { kind, diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 80a104641ff92888c5a21ec60e7f77a927427cd5..8b77788d22677eea4691f8bfbf9af64412b095ec 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -520,10 +520,10 @@ impl Peer { &response.payload { // Remove the transmitting end of the response channel to end the stream. - if let Some(channels) = stream_response_channels.upgrade() { - if let Some(channels) = channels.lock().as_mut() { - channels.remove(&message_id); - } + if let Some(channels) = stream_response_channels.upgrade() + && let Some(channels) = channels.lock().as_mut() + { + channels.remove(&message_id); } None } else { diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index ebec96dd7b6298720653768cfbe1b4515cd376fd..ec83993e5f91f970b3336a5c2ef008a4c436c3b0 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -456,11 +456,11 @@ impl RulesLibrary { pub fn new_rule(&mut self, window: &mut Window, cx: &mut Context<Self>) { // If we already have an untitled rule, use that instead // of creating a new one. - if let Some(metadata) = self.store.read(cx).first() { - if metadata.title.is_none() { - self.load_rule(metadata.id, true, window, cx); - return; - } + if let Some(metadata) = self.store.read(cx).first() + && metadata.title.is_none() + { + self.load_rule(metadata.id, true, window, cx); + return; } let prompt_id = PromptId::new(); @@ -706,15 +706,13 @@ impl RulesLibrary { .map_or(true, |old_selected_prompt| { old_selected_prompt.id != prompt_id }) - { - if let Some(ix) = picker + && let Some(ix) = picker .delegate .matches .iter() .position(|mat| mat.id == prompt_id) - { - picker.set_selected_index(ix, None, true, window, cx); - } + { + picker.set_selected_index(ix, None, true, window, cx); } } else { picker.focus(window, cx); @@ -869,10 +867,10 @@ impl RulesLibrary { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(rule_id) = self.active_rule_id { - if let Some(rule_editor) = self.rule_editors.get(&rule_id) { - window.focus(&rule_editor.body_editor.focus_handle(cx)); - } + if let Some(rule_id) = self.active_rule_id + && let Some(rule_editor) = self.rule_editors.get(&rule_id) + { + window.focus(&rule_editor.body_editor.focus_handle(cx)); } } @@ -882,10 +880,10 @@ impl RulesLibrary { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(rule_id) = self.active_rule_id { - if let Some(rule_editor) = self.rule_editors.get(&rule_id) { - window.focus(&rule_editor.title_editor.focus_handle(cx)); - } + if let Some(rule_id) = self.active_rule_id + && let Some(rule_editor) = self.rule_editors.get(&rule_id) + { + window.focus(&rule_editor.title_editor.focus_handle(cx)); } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 189f48e6b6f5d46e442f25b75671136163194872..78e4da7bc61d15daf9ab46d9e3d2f34c617767a6 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -794,15 +794,13 @@ impl BufferSearchBar { } pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) { - if let Some(match_ix) = self.active_match_index { - if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&active_searchable_item.downgrade()) - { - active_searchable_item.activate_match(match_ix, matches, window, cx) - } - } + if let Some(match_ix) = self.active_match_index + && let Some(active_searchable_item) = self.active_searchable_item.as_ref() + && let Some(matches) = self + .searchable_items_with_matches + .get(&active_searchable_item.downgrade()) + { + active_searchable_item.activate_match(match_ix, matches, window, cx) } } @@ -951,16 +949,15 @@ impl BufferSearchBar { window: &mut Window, cx: &mut Context<Self>, ) { - if !self.dismissed && self.active_match_index.is_some() { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - searchable_item.select_matches(matches, window, cx); - self.focus_editor(&FocusEditor, window, cx); - } - } + if !self.dismissed + && self.active_match_index.is_some() + && let Some(searchable_item) = self.active_searchable_item.as_ref() + && let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + searchable_item.select_matches(matches, window, cx); + self.focus_editor(&FocusEditor, window, cx); } } @@ -971,59 +968,55 @@ impl BufferSearchBar { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(index) = self.active_match_index { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - .filter(|matches| !matches.is_empty()) - { - // If 'wrapscan' is disabled, searches do not wrap around the end of the file. - if !EditorSettings::get_global(cx).search_wrap - && ((direction == Direction::Next && index + count >= matches.len()) - || (direction == Direction::Prev && index < count)) - { - crate::show_no_more_matches(window, cx); - return; - } - let new_match_index = searchable_item - .match_index_for_direction(matches, index, direction, count, window, cx); - - searchable_item.update_matches(matches, window, cx); - searchable_item.activate_match(new_match_index, matches, window, cx); - } + if let Some(index) = self.active_match_index + && let Some(searchable_item) = self.active_searchable_item.as_ref() + && let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + .filter(|matches| !matches.is_empty()) + { + // If 'wrapscan' is disabled, searches do not wrap around the end of the file. + if !EditorSettings::get_global(cx).search_wrap + && ((direction == Direction::Next && index + count >= matches.len()) + || (direction == Direction::Prev && index < count)) + { + crate::show_no_more_matches(window, cx); + return; } + let new_match_index = searchable_item + .match_index_for_direction(matches, index, direction, count, window, cx); + + searchable_item.update_matches(matches, window, cx); + searchable_item.activate_match(new_match_index, matches, window, cx); } } pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self + if let Some(searchable_item) = self.active_searchable_item.as_ref() + && let Some(matches) = self .searchable_items_with_matches .get(&searchable_item.downgrade()) - { - if matches.is_empty() { - return; - } - searchable_item.update_matches(matches, window, cx); - searchable_item.activate_match(0, matches, window, cx); + { + if matches.is_empty() { + return; } + searchable_item.update_matches(matches, window, cx); + searchable_item.activate_match(0, matches, window, cx); } } pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self + if let Some(searchable_item) = self.active_searchable_item.as_ref() + && let Some(matches) = self .searchable_items_with_matches .get(&searchable_item.downgrade()) - { - if matches.is_empty() { - return; - } - let new_match_index = matches.len() - 1; - searchable_item.update_matches(matches, window, cx); - searchable_item.activate_match(new_match_index, matches, window, cx); + { + if matches.is_empty() { + return; } + let new_match_index = matches.len() - 1; + searchable_item.update_matches(matches, window, cx); + searchable_item.activate_match(new_match_index, matches, window, cx); } } @@ -1344,15 +1337,14 @@ impl BufferSearchBar { window: &mut Window, cx: &mut Context<Self>, ) { - if self.query(cx).is_empty() { - if let Some(new_query) = self + if self.query(cx).is_empty() + && let Some(new_query) = self .search_history .current(&mut self.search_history_cursor) .map(str::to_string) - { - drop(self.search(&new_query, Some(self.search_options), window, cx)); - return; - } + { + drop(self.search(&new_query, Some(self.search_options), window, cx)); + return; } if let Some(new_query) = self @@ -1384,25 +1376,23 @@ impl BufferSearchBar { fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) { let mut should_propagate = true; - if !self.dismissed && self.active_search.is_some() { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(query) = self.active_search.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - if let Some(active_index) = self.active_match_index { - let query = query - .as_ref() - .clone() - .with_replacement(self.replacement(cx)); - searchable_item.replace(matches.at(active_index), &query, window, cx); - self.select_next_match(&SelectNextMatch, window, cx); - } - should_propagate = false; - } - } + if !self.dismissed + && self.active_search.is_some() + && let Some(searchable_item) = self.active_searchable_item.as_ref() + && let Some(query) = self.active_search.as_ref() + && let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + if let Some(active_index) = self.active_match_index { + let query = query + .as_ref() + .clone() + .with_replacement(self.replacement(cx)); + searchable_item.replace(matches.at(active_index), &query, window, cx); + self.select_next_match(&SelectNextMatch, window, cx); } + should_propagate = false; } if !should_propagate { cx.stop_propagation(); @@ -1410,21 +1400,19 @@ impl BufferSearchBar { } pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) { - if !self.dismissed && self.active_search.is_some() { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(query) = self.active_search.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - let query = query - .as_ref() - .clone() - .with_replacement(self.replacement(cx)); - searchable_item.replace_all(&mut matches.iter(), &query, window, cx); - } - } - } + if !self.dismissed + && self.active_search.is_some() + && let Some(searchable_item) = self.active_searchable_item.as_ref() + && let Some(query) = self.active_search.as_ref() + && let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + let query = query + .as_ref() + .clone() + .with_replacement(self.replacement(cx)); + searchable_item.replace_all(&mut matches.iter(), &query, window, cx); } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 443bbb0427cab3a9b47c457fce25e5dfde7f3b53..51cb1fdb26ddf644755bf995a4ec8ac0cfddd3a4 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -775,15 +775,15 @@ impl ProjectSearchView { // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes subscriptions.push( cx.subscribe(&query_editor, |this, _, event: &EditorEvent, cx| { - if let EditorEvent::Edited { .. } = event { - if EditorSettings::get_global(cx).use_smartcase_search { - let query = this.search_query_text(cx); - if !query.is_empty() - && this.search_options.contains(SearchOptions::CASE_SENSITIVE) - != contains_uppercase(&query) - { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); - } + if let EditorEvent::Edited { .. } = event + && EditorSettings::get_global(cx).use_smartcase_search + { + let query = this.search_query_text(cx); + if !query.is_empty() + && this.search_options.contains(SearchOptions::CASE_SENSITIVE) + != contains_uppercase(&query) + { + this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); } } cx.emit(ViewEvent::EditorEvent(event.clone())) @@ -947,14 +947,14 @@ impl ProjectSearchView { { let new_query = search_view.update(cx, |search_view, cx| { let new_query = search_view.build_search_query(cx); - if new_query.is_some() { - if let Some(old_query) = search_view.entity.read(cx).active_query.clone() { - search_view.query_editor.update(cx, |editor, cx| { - editor.set_text(old_query.as_str(), window, cx); - }); - search_view.search_options = SearchOptions::from_query(&old_query); - search_view.adjust_query_regex_language(cx); - } + if new_query.is_some() + && let Some(old_query) = search_view.entity.read(cx).active_query.clone() + { + search_view.query_editor.update(cx, |editor, cx| { + editor.set_text(old_query.as_str(), window, cx); + }); + search_view.search_options = SearchOptions::from_query(&old_query); + search_view.adjust_query_regex_language(cx); } new_query }); @@ -1844,8 +1844,8 @@ impl ProjectSearchBar { ), ] { if editor.focus_handle(cx).is_focused(window) { - if editor.read(cx).text(cx).is_empty() { - if let Some(new_query) = search_view + if editor.read(cx).text(cx).is_empty() + && let Some(new_query) = search_view .entity .read(cx) .project @@ -1853,10 +1853,9 @@ impl ProjectSearchBar { .search_history(kind) .current(search_view.entity.read(cx).cursor(kind)) .map(str::to_string) - { - search_view.set_search_editor(kind, &new_query, window, cx); - return; - } + { + search_view.set_search_editor(kind, &new_query, window, cx); + return; } if let Some(new_query) = search_view.entity.update(cx, |model, cx| { diff --git a/crates/semantic_index/src/embedding_index.rs b/crates/semantic_index/src/embedding_index.rs index d2d10ad0ad8b7c0e743d5cccce647eb48a2bc1c6..eeb3c91fcd855da99e645a62a7c86fd4a66b72b1 100644 --- a/crates/semantic_index/src/embedding_index.rs +++ b/crates/semantic_index/src/embedding_index.rs @@ -194,11 +194,11 @@ impl EmbeddingIndex { project::PathChange::Added | project::PathChange::Updated | project::PathChange::AddedOrUpdated => { - if let Some(entry) = worktree.entry_for_id(*entry_id) { - if entry.is_file() { - let handle = entries_being_indexed.insert(entry.id); - updated_entries_tx.send((entry.clone(), handle)).await?; - } + if let Some(entry) = worktree.entry_for_id(*entry_id) + && entry.is_file() + { + let handle = entries_being_indexed.insert(entry.id); + updated_entries_tx.send((entry.clone(), handle)).await?; } } project::PathChange::Removed => { diff --git a/crates/semantic_index/src/project_index.rs b/crates/semantic_index/src/project_index.rs index 5e852327dd5bb53fedd99786b7665922d7ae978c..60b2770dd39b91b606b9c982c894bfc94952a179 100644 --- a/crates/semantic_index/src/project_index.rs +++ b/crates/semantic_index/src/project_index.rs @@ -384,10 +384,10 @@ impl ProjectIndex { cx: &App, ) -> Option<Entity<WorktreeIndex>> { for index in self.worktree_indices.values() { - if let WorktreeIndexHandle::Loaded { index, .. } = index { - if index.read(cx).worktree().read(cx).id() == worktree_id { - return Some(index.clone()); - } + if let WorktreeIndexHandle::Loaded { index, .. } = index + && index.read(cx).worktree().read(cx).id() == worktree_id + { + return Some(index.clone()); } } None diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index a9cc08382b5cdead489aa436c5793164eeda27af..1dafeb072fc944f4356894d7a74197cac3de55f6 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -174,14 +174,13 @@ impl SemanticDb { file_content[start_line_byte_offset..end_line_byte_offset].to_string(); LineEnding::normalize(&mut excerpt_content); - if let Some(prev_result) = loaded_results.last_mut() { - if prev_result.full_path == full_path { - if *prev_result.row_range.end() + 1 == start_row { - prev_result.row_range = *prev_result.row_range.start()..=end_row; - prev_result.excerpt_content.push_str(&excerpt_content); - continue; - } - } + if let Some(prev_result) = loaded_results.last_mut() + && prev_result.full_path == full_path + && *prev_result.row_range.end() + 1 == start_row + { + prev_result.row_range = *prev_result.row_range.start()..=end_row; + prev_result.excerpt_content.push_str(&excerpt_content); + continue; } loaded_results.push(LoadedSearchResult { diff --git a/crates/semantic_index/src/summary_index.rs b/crates/semantic_index/src/summary_index.rs index 20858c8d3f0d2b908c19f852c90da3d259dd7b2d..d1c9a3abaca4be36f18c6aab81565067f6ba032b 100644 --- a/crates/semantic_index/src/summary_index.rs +++ b/crates/semantic_index/src/summary_index.rs @@ -379,18 +379,14 @@ impl SummaryIndex { | project::PathChange::Added | project::PathChange::Updated | project::PathChange::AddedOrUpdated => { - if let Some(entry) = worktree.entry_for_id(*entry_id) { - if entry.is_file() { - let needs_summary = Self::add_to_backlog( - Arc::clone(&backlog), - digest_db, - &txn, - entry, - ); - - if !needs_summary.is_empty() { - tx.send(needs_summary).await?; - } + if let Some(entry) = worktree.entry_for_id(*entry_id) + && entry.is_file() + { + let needs_summary = + Self::add_to_backlog(Arc::clone(&backlog), digest_db, &txn, entry); + + if !needs_summary.is_empty() { + tx.send(needs_summary).await?; } } } diff --git a/crates/session/src/session.rs b/crates/session/src/session.rs index f027df876236c2b570b4c0384f5d07d4816a7fa7..438059fef78adb6b83853335bfe1d38113a768d9 100644 --- a/crates/session/src/session.rs +++ b/crates/session/src/session.rs @@ -70,11 +70,11 @@ impl AppSession { let _serialization_task = cx.spawn(async move |_, cx| { let mut current_window_stack = Vec::new(); loop { - if let Some(windows) = cx.update(|cx| window_stack(cx)).ok().flatten() { - if windows != current_window_stack { - store_window_stack(&windows).await; - current_window_stack = windows; - } + if let Some(windows) = cx.update(|cx| window_stack(cx)).ok().flatten() + && windows != current_window_stack + { + store_window_stack(&windows).await; + current_window_stack = windows; } cx.background_executor() diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index b0f7d2449e51bdfd8877d6e4d57e6c524d493279..e95617512deffbe63d5094186e9b503900d516dc 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -543,27 +543,27 @@ impl KeymapFile { // // When a struct with no deserializable fields is added by deriving `Action`, an empty // object schema is produced. The action should be invoked without data in this case. - if let Some(schema) = action_schema { - if schema != empty_object { - let mut matches_action_name = json_schema!({ - "const": name - }); - if let Some(desc) = description.clone() { - add_description(&mut matches_action_name, desc); - } - if let Some(message) = deprecation_messages.get(name) { - add_deprecation(&mut matches_action_name, message.to_string()); - } else if let Some(new_name) = deprecation { - add_deprecation_preferred_name(&mut matches_action_name, new_name); - } - let action_with_input = json_schema!({ - "type": "array", - "items": [matches_action_name, schema], - "minItems": 2, - "maxItems": 2 - }); - keymap_action_alternatives.push(action_with_input); + if let Some(schema) = action_schema + && schema != empty_object + { + let mut matches_action_name = json_schema!({ + "const": name + }); + if let Some(desc) = description.clone() { + add_description(&mut matches_action_name, desc); } + if let Some(message) = deprecation_messages.get(name) { + add_deprecation(&mut matches_action_name, message.to_string()); + } else if let Some(new_name) = deprecation { + add_deprecation_preferred_name(&mut matches_action_name, new_name); + } + let action_with_input = json_schema!({ + "type": "array", + "items": [matches_action_name, schema], + "minItems": 2, + "maxItems": 2 + }); + keymap_action_alternatives.push(action_with_input); } } @@ -593,10 +593,10 @@ impl KeymapFile { match fs.load(paths::keymap_file()).await { result @ Ok(_) => result, Err(err) => { - if let Some(e) = err.downcast_ref::<std::io::Error>() { - if e.kind() == std::io::ErrorKind::NotFound { - return Ok(crate::initial_keymap_content().to_string()); - } + if let Some(e) = err.downcast_ref::<std::io::Error>() + && e.kind() == std::io::ErrorKind::NotFound + { + return Ok(crate::initial_keymap_content().to_string()); } Err(err) } diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index c43f3e79e8cf2edcee9d49e5dc3268295ff41439..d31dd82da475744d9658bc8ecdc6ec2ad17732fb 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -67,10 +67,10 @@ pub fn watch_config_file( break; } - if let Ok(contents) = fs.load(&path).await { - if tx.unbounded_send(contents).is_err() { - break; - } + if let Ok(contents) = fs.load(&path).await + && tx.unbounded_send(contents).is_err() + { + break; } } }) @@ -88,12 +88,11 @@ pub fn watch_config_dir( executor .spawn(async move { for file_path in &config_paths { - if fs.metadata(file_path).await.is_ok_and(|v| v.is_some()) { - if let Ok(contents) = fs.load(file_path).await { - if tx.unbounded_send(contents).is_err() { - return; - } - } + if fs.metadata(file_path).await.is_ok_and(|v| v.is_some()) + && let Ok(contents) = fs.load(file_path).await + && tx.unbounded_send(contents).is_err() + { + return; } } @@ -110,10 +109,10 @@ pub fn watch_config_dir( } } Some(PathEventKind::Created) | Some(PathEventKind::Changed) => { - if let Ok(contents) = fs.load(&event.path).await { - if tx.unbounded_send(contents).is_err() { - return; - } + if let Ok(contents) = fs.load(&event.path).await + && tx.unbounded_send(contents).is_err() + { + return; } } _ => {} diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index e6683857e778e0c9cd052fc9f72407ee5d7787be..8e7e11dc827c676a425860fa8b8b8633156b1d0d 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -369,13 +369,12 @@ pub fn replace_top_level_array_value_in_json_text( if cursor.node().kind() == "," { remove_range.end = cursor.node().range().end_byte; } - if let Some(next_newline) = &text[remove_range.end + 1..].find('\n') { - if text[remove_range.end + 1..remove_range.end + next_newline] + if let Some(next_newline) = &text[remove_range.end + 1..].find('\n') + && text[remove_range.end + 1..remove_range.end + next_newline] .chars() .all(|c| c.is_ascii_whitespace()) - { - remove_range.end = remove_range.end + next_newline; - } + { + remove_range.end = remove_range.end + next_newline; } } else { while cursor.goto_previous_sibling() @@ -508,10 +507,10 @@ pub fn append_top_level_array_value_in_json_text( replace_value.insert(0, ','); } } else { - if let Some(prev_newline) = text[..replace_range.start].rfind('\n') { - if text[prev_newline..replace_range.start].trim().is_empty() { - replace_range.start = prev_newline; - } + if let Some(prev_newline) = text[..replace_range.start].rfind('\n') + && text[prev_newline..replace_range.start].trim().is_empty() + { + replace_range.start = prev_newline; } let indent = format!("\n{space:width$}", width = tab_size); replace_value = replace_value.replace('\n', &indent); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index bfdafbffe8e4daf276f76f98a2ab7c535f4e1212..23f495d850d9d06148fa449bd39995347cf895df 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -346,14 +346,13 @@ impl SettingsStore { } let mut profile_value = None; - if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() { - if let Some(profiles) = self.raw_user_settings.get("profiles") { - if let Some(profile_settings) = profiles.get(&active_profile.0) { - profile_value = setting_value - .deserialize_setting(profile_settings) - .log_err(); - } - } + if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() + && let Some(profiles) = self.raw_user_settings.get("profiles") + && let Some(profile_settings) = profiles.get(&active_profile.0) + { + profile_value = setting_value + .deserialize_setting(profile_settings) + .log_err(); } let server_value = self @@ -482,10 +481,10 @@ impl SettingsStore { match fs.load(paths::settings_file()).await { result @ Ok(_) => result, Err(err) => { - if let Some(e) = err.downcast_ref::<std::io::Error>() { - if e.kind() == std::io::ErrorKind::NotFound { - return Ok(crate::initial_user_settings_content().to_string()); - } + if let Some(e) = err.downcast_ref::<std::io::Error>() + && e.kind() == std::io::ErrorKind::NotFound + { + return Ok(crate::initial_user_settings_content().to_string()); } Err(err) } @@ -496,10 +495,10 @@ impl SettingsStore { match fs.load(paths::global_settings_file()).await { result @ Ok(_) => result, Err(err) => { - if let Some(e) = err.downcast_ref::<std::io::Error>() { - if e.kind() == std::io::ErrorKind::NotFound { - return Ok("{}".to_string()); - } + if let Some(e) = err.downcast_ref::<std::io::Error>() + && e.kind() == std::io::ErrorKind::NotFound + { + return Ok("{}".to_string()); } Err(err) } @@ -955,13 +954,13 @@ impl SettingsStore { let mut setting_schema = setting_value.json_schema(&mut generator); if let Some(key) = setting_value.key() { - if let Some(properties) = combined_schema.get_mut("properties") { - if let Some(properties_obj) = properties.as_object_mut() { - if let Some(target) = properties_obj.get_mut(key) { - merge_schema(target, setting_schema.to_value()); - } else { - properties_obj.insert(key.to_string(), setting_schema.to_value()); - } + if let Some(properties) = combined_schema.get_mut("properties") + && let Some(properties_obj) = properties.as_object_mut() + { + if let Some(target) = properties_obj.get_mut(key) { + merge_schema(target, setting_schema.to_value()); + } else { + properties_obj.insert(key.to_string(), setting_schema.to_value()); } } } else { @@ -1038,16 +1037,15 @@ impl SettingsStore { | "additionalProperties" => { if let Some(old_value) = target_obj.insert(source_key.clone(), source_value.clone()) + && old_value != source_value { - if old_value != source_value { - log::error!( - "bug: while merging JSON schemas, \ + log::error!( + "bug: while merging JSON schemas, \ mismatch `\"{}\": {}` (before was `{}`)", - source_key, - old_value, - source_value - ); - } + source_key, + old_value, + source_value + ); } } _ => { @@ -1168,35 +1166,31 @@ impl SettingsStore { if let Some(release_settings) = &self .raw_user_settings .get(release_channel::RELEASE_CHANNEL.dev_name()) - { - if let Some(release_settings) = setting_value + && let Some(release_settings) = setting_value .deserialize_setting(release_settings) .log_err() - { - release_channel_settings = Some(release_settings); - } + { + release_channel_settings = Some(release_settings); } let mut os_settings = None; - if let Some(settings) = &self.raw_user_settings.get(env::consts::OS) { - if let Some(settings) = setting_value.deserialize_setting(settings).log_err() { - os_settings = Some(settings); - } + if let Some(settings) = &self.raw_user_settings.get(env::consts::OS) + && let Some(settings) = setting_value.deserialize_setting(settings).log_err() + { + os_settings = Some(settings); } let mut profile_settings = None; - if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() { - if let Some(profiles) = self.raw_user_settings.get("profiles") { - if let Some(profile_json) = profiles.get(&active_profile.0) { - profile_settings = - setting_value.deserialize_setting(profile_json).log_err(); - } - } + if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() + && let Some(profiles) = self.raw_user_settings.get("profiles") + && let Some(profile_json) = profiles.get(&active_profile.0) + { + profile_settings = setting_value.deserialize_setting(profile_json).log_err(); } // If the global settings file changed, reload the global value for the field. - if changed_local_path.is_none() { - if let Some(value) = setting_value + if changed_local_path.is_none() + && let Some(value) = setting_value .load_setting( SettingsSources { default: &default_settings, @@ -1212,9 +1206,8 @@ impl SettingsStore { cx, ) .log_err() - { - setting_value.set_global_value(value); - } + { + setting_value.set_global_value(value); } // Reload the local values for the setting. @@ -1223,12 +1216,12 @@ impl SettingsStore { for ((root_id, directory_path), local_settings) in &self.raw_local_settings { // Build a stack of all of the local values for that setting. while let Some(prev_entry) = paths_stack.last() { - if let Some((prev_root_id, prev_path)) = prev_entry { - if root_id != prev_root_id || !directory_path.starts_with(prev_path) { - paths_stack.pop(); - project_settings_stack.pop(); - continue; - } + if let Some((prev_root_id, prev_path)) = prev_entry + && (root_id != prev_root_id || !directory_path.starts_with(prev_path)) + { + paths_stack.pop(); + project_settings_stack.pop(); + continue; } break; } diff --git a/crates/snippets_ui/src/snippets_ui.rs b/crates/snippets_ui/src/snippets_ui.rs index bf0ef63bffde0586b889c41bdadd161529fdf636..db76ab6f9a503b0611910f1271d5b4773bfbf63e 100644 --- a/crates/snippets_ui/src/snippets_ui.rs +++ b/crates/snippets_ui/src/snippets_ui.rs @@ -163,13 +163,12 @@ impl ScopeSelectorDelegate { for entry in read_dir { if let Some(entry) = entry.log_err() { let path = entry.path(); - if let (Some(stem), Some(extension)) = (path.file_stem(), path.extension()) { - if extension.to_os_string().to_str() == Some("json") { - if let Ok(file_name) = stem.to_os_string().into_string() { - existing_scopes - .insert(ScopeName::from(ScopeFileName(Cow::Owned(file_name)))); - } - } + if let (Some(stem), Some(extension)) = (path.file_stem(), path.extension()) + && extension.to_os_string().to_str() == Some("json") + && let Ok(file_name) = stem.to_os_string().into_string() + { + existing_scopes + .insert(ScopeName::from(ScopeFileName(Cow::Owned(file_name)))); } } } diff --git a/crates/sqlez/src/connection.rs b/crates/sqlez/src/connection.rs index f56ae2427df3314f3b0c7ef989df9d74c51efea7..228bd4c6a2df31f41dc1988596fc87323063d78c 100644 --- a/crates/sqlez/src/connection.rs +++ b/crates/sqlez/src/connection.rs @@ -213,38 +213,37 @@ impl Connection { fn parse_alter_table(remaining_sql_str: &str) -> Option<(String, String)> { let remaining_sql_str = remaining_sql_str.to_lowercase(); - if remaining_sql_str.starts_with("alter") { - if let Some(table_offset) = remaining_sql_str.find("table") { - let after_table_offset = table_offset + "table".len(); - let table_to_alter = remaining_sql_str - .chars() - .skip(after_table_offset) - .skip_while(|c| c.is_whitespace()) - .take_while(|c| !c.is_whitespace()) - .collect::<String>(); - if !table_to_alter.is_empty() { - let column_name = - if let Some(rename_offset) = remaining_sql_str.find("rename column") { - let after_rename_offset = rename_offset + "rename column".len(); - remaining_sql_str - .chars() - .skip(after_rename_offset) - .skip_while(|c| c.is_whitespace()) - .take_while(|c| !c.is_whitespace()) - .collect::<String>() - } else if let Some(drop_offset) = remaining_sql_str.find("drop column") { - let after_drop_offset = drop_offset + "drop column".len(); - remaining_sql_str - .chars() - .skip(after_drop_offset) - .skip_while(|c| c.is_whitespace()) - .take_while(|c| !c.is_whitespace()) - .collect::<String>() - } else { - "__place_holder_column_for_syntax_checking".to_string() - }; - return Some((table_to_alter, column_name)); - } + if remaining_sql_str.starts_with("alter") + && let Some(table_offset) = remaining_sql_str.find("table") + { + let after_table_offset = table_offset + "table".len(); + let table_to_alter = remaining_sql_str + .chars() + .skip(after_table_offset) + .skip_while(|c| c.is_whitespace()) + .take_while(|c| !c.is_whitespace()) + .collect::<String>(); + if !table_to_alter.is_empty() { + let column_name = if let Some(rename_offset) = remaining_sql_str.find("rename column") { + let after_rename_offset = rename_offset + "rename column".len(); + remaining_sql_str + .chars() + .skip(after_rename_offset) + .skip_while(|c| c.is_whitespace()) + .take_while(|c| !c.is_whitespace()) + .collect::<String>() + } else if let Some(drop_offset) = remaining_sql_str.find("drop column") { + let after_drop_offset = drop_offset + "drop column".len(); + remaining_sql_str + .chars() + .skip(after_drop_offset) + .skip_while(|c| c.is_whitespace()) + .take_while(|c| !c.is_whitespace()) + .collect::<String>() + } else { + "__place_holder_column_for_syntax_checking".to_string() + }; + return Some((table_to_alter, column_name)); } } None diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index 50a556a6d279d0b7f733d0d80c6c2e7e3d6c61cd..53458b65ec313ca56c5decfcd4650825285d0733 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -530,10 +530,10 @@ where debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf()); let mut end = self.position.clone(); - if bias == Bias::Left { - if let Some(summary) = self.item_summary() { - end.add_summary(summary, self.cx); - } + if bias == Bias::Left + && let Some(summary) = self.item_summary() + { + end.add_summary(summary, self.cx); } target.cmp(&end, self.cx) == Ordering::Equal diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 3a12e3a681f7bd289e4e4a9fa9036d5f307aa1d7..f551bb32e6fd92bfb388166602e27b14329faa29 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -674,11 +674,11 @@ impl<T: KeyedItem> SumTree<T> { *self = { let mut cursor = self.cursor::<T::Key>(cx); let mut new_tree = cursor.slice(&item.key(), Bias::Left); - if let Some(cursor_item) = cursor.item() { - if cursor_item.key() == item.key() { - replaced = Some(cursor_item.clone()); - cursor.next(); - } + if let Some(cursor_item) = cursor.item() + && cursor_item.key() == item.key() + { + replaced = Some(cursor_item.clone()); + cursor.next(); } new_tree.push(item, cx); new_tree.append(cursor.suffix(), cx); @@ -692,11 +692,11 @@ impl<T: KeyedItem> SumTree<T> { *self = { let mut cursor = self.cursor::<T::Key>(cx); let mut new_tree = cursor.slice(key, Bias::Left); - if let Some(item) = cursor.item() { - if item.key() == *key { - removed = Some(item.clone()); - cursor.next(); - } + if let Some(item) = cursor.item() + && item.key() == *key + { + removed = Some(item.clone()); + cursor.next(); } new_tree.append(cursor.suffix(), cx); new_tree @@ -736,11 +736,11 @@ impl<T: KeyedItem> SumTree<T> { old_item = cursor.item(); } - if let Some(old_item) = old_item { - if old_item.key() == new_key { - removed.push(old_item.clone()); - cursor.next(); - } + if let Some(old_item) = old_item + && old_item.key() == new_key + { + removed.push(old_item.clone()); + cursor.next(); } match edit { diff --git a/crates/svg_preview/src/svg_preview_view.rs b/crates/svg_preview/src/svg_preview_view.rs index 327856d74989ba5cabab631486fd27133e3f684e..4e4c83c8de58cba07dde3c75513c98139a4e31ad 100644 --- a/crates/svg_preview/src/svg_preview_view.rs +++ b/crates/svg_preview/src/svg_preview_view.rs @@ -32,79 +32,74 @@ pub enum SvgPreviewMode { impl SvgPreviewView { pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>) { workspace.register_action(move |workspace, _: &OpenPreview, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) { - if Self::is_svg_file(&editor, cx) { - let view = Self::create_svg_view( - SvgPreviewMode::Default, - workspace, - editor.clone(), - window, - cx, - ); - workspace.active_pane().update(cx, |pane, cx| { - if let Some(existing_view_idx) = - Self::find_existing_preview_item_idx(pane, &editor, cx) - { - pane.activate_item(existing_view_idx, true, true, window, cx); - } else { - pane.add_item(Box::new(view), true, true, None, window, cx) - } - }); - cx.notify(); - } + if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) + && Self::is_svg_file(&editor, cx) + { + let view = Self::create_svg_view( + SvgPreviewMode::Default, + workspace, + editor.clone(), + window, + cx, + ); + workspace.active_pane().update(cx, |pane, cx| { + if let Some(existing_view_idx) = + Self::find_existing_preview_item_idx(pane, &editor, cx) + { + pane.activate_item(existing_view_idx, true, true, window, cx); + } else { + pane.add_item(Box::new(view), true, true, None, window, cx) + } + }); + cx.notify(); } }); workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) { - if Self::is_svg_file(&editor, cx) { - let editor_clone = editor.clone(); - let view = Self::create_svg_view( - SvgPreviewMode::Default, - workspace, - editor_clone, - window, - cx, - ); - let pane = workspace - .find_pane_in_direction(workspace::SplitDirection::Right, cx) - .unwrap_or_else(|| { - workspace.split_pane( - workspace.active_pane().clone(), - workspace::SplitDirection::Right, - window, - cx, - ) - }); - pane.update(cx, |pane, cx| { - if let Some(existing_view_idx) = - Self::find_existing_preview_item_idx(pane, &editor, cx) - { - pane.activate_item(existing_view_idx, true, true, window, cx); - } else { - pane.add_item(Box::new(view), false, false, None, window, cx) - } + if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) + && Self::is_svg_file(&editor, cx) + { + let editor_clone = editor.clone(); + let view = Self::create_svg_view( + SvgPreviewMode::Default, + workspace, + editor_clone, + window, + cx, + ); + let pane = workspace + .find_pane_in_direction(workspace::SplitDirection::Right, cx) + .unwrap_or_else(|| { + workspace.split_pane( + workspace.active_pane().clone(), + workspace::SplitDirection::Right, + window, + cx, + ) }); - cx.notify(); - } + pane.update(cx, |pane, cx| { + if let Some(existing_view_idx) = + Self::find_existing_preview_item_idx(pane, &editor, cx) + { + pane.activate_item(existing_view_idx, true, true, window, cx); + } else { + pane.add_item(Box::new(view), false, false, None, window, cx) + } + }); + cx.notify(); } }); workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) { - if Self::is_svg_file(&editor, cx) { - let view = Self::create_svg_view( - SvgPreviewMode::Follow, - workspace, - editor, - window, - cx, - ); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item(Box::new(view), true, true, None, window, cx) - }); - cx.notify(); - } + if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) + && Self::is_svg_file(&editor, cx) + { + let view = + Self::create_svg_view(SvgPreviewMode::Follow, workspace, editor, window, cx); + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item(Box::new(view), true, true, None, window, cx) + }); + cx.notify(); } }); } @@ -192,18 +187,15 @@ impl SvgPreviewView { match event { workspace::Event::ActiveItemChanged => { let workspace_read = workspace.read(cx); - if let Some(active_item) = workspace_read.active_item(cx) { - if let Some(editor_entity) = + if let Some(active_item) = workspace_read.active_item(cx) + && let Some(editor_entity) = active_item.downcast::<Editor>() - { - if Self::is_svg_file(&editor_entity, cx) { - let new_path = - Self::get_svg_path(&editor_entity, cx); - if this.svg_path != new_path { - this.svg_path = new_path; - cx.notify(); - } - } + && Self::is_svg_file(&editor_entity, cx) + { + let new_path = Self::get_svg_path(&editor_entity, cx); + if this.svg_path != new_path { + this.svg_path = new_path; + cx.notify(); } } } @@ -232,15 +224,15 @@ impl SvgPreviewView { { let app = cx.borrow(); let buffer = editor.read(app).buffer().read(app); - if let Some(buffer) = buffer.as_singleton() { - if let Some(file) = buffer.read(app).file() { - return file - .path() - .extension() - .and_then(|ext| ext.to_str()) - .map(|ext| ext.eq_ignore_ascii_case("svg")) - .unwrap_or(false); - } + if let Some(buffer) = buffer.as_singleton() + && let Some(file) = buffer.read(app).file() + { + return file + .path() + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("svg")) + .unwrap_or(false); } false } diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index f20f55975e7a1d178b899b7cc89d19676d7f27cf..38089670e23f815221c274a2ccc4619b9e8bb327 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -299,13 +299,12 @@ impl DebugTaskFile { if let Some(properties) = template_object .get_mut("properties") .and_then(|value| value.as_object_mut()) + && properties.remove("label").is_none() { - if properties.remove("label").is_none() { - debug_panic!( - "Generated TaskTemplate json schema did not have expected 'label' field. \ + debug_panic!( + "Generated TaskTemplate json schema did not have expected 'label' field. \ Schema of 2nd alternative is: {template_object:?}" - ); - } + ); } if let Some(arr) = template_object diff --git a/crates/task/src/vscode_debug_format.rs b/crates/task/src/vscode_debug_format.rs index e688760a5e67d00d111c798d90499e78c5a3fe17..5e21cd65302a11a8106881077b0f08c118ed215e 100644 --- a/crates/task/src/vscode_debug_format.rs +++ b/crates/task/src/vscode_debug_format.rs @@ -30,12 +30,12 @@ impl VsCodeDebugTaskDefinition { let label = replacer.replace(&self.name); let mut config = replacer.replace_value(self.other_attributes); let adapter = task_type_to_adapter_name(&self.r#type); - if let Some(config) = config.as_object_mut() { - if adapter == "JavaScript" { - config.insert("type".to_owned(), self.r#type.clone().into()); - if let Some(port) = self.port.take() { - config.insert("port".to_owned(), port.into()); - } + if let Some(config) = config.as_object_mut() + && adapter == "JavaScript" + { + config.insert("type".to_owned(), self.r#type.clone().into()); + if let Some(port) = self.port.take() { + config.insert("port".to_owned(), port.into()); } } let definition = DebugScenario { diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 0b3f70e6bcc5402bae3af09effb5bebc1a574977..90e6ea88788495b4ee933acf8c752cc4d44c51cb 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -227,10 +227,10 @@ where tasks.retain_mut(|(task_source_kind, target_task)| { if predicate((task_source_kind, target_task)) { - if let Some(overrides) = &overrides { - if let Some(target_override) = overrides.reveal_target { - target_task.reveal_target = target_override; - } + if let Some(overrides) = &overrides + && let Some(target_override) = overrides.reveal_target + { + target_task.reveal_target = target_override; } workspace.schedule_task( task_source_kind.clone(), @@ -343,11 +343,10 @@ pub fn task_contexts( task_contexts.lsp_task_sources = lsp_task_sources; task_contexts.latest_selection = latest_selection; - if let Some(editor_context_task) = editor_context_task { - if let Some(editor_context) = editor_context_task.await { - task_contexts.active_item_context = - Some((active_worktree, location, editor_context)); - } + if let Some(editor_context_task) = editor_context_task + && let Some(editor_context) = editor_context_task.await + { + task_contexts.active_item_context = Some((active_worktree, location, editor_context)); } if let Some(active_worktree) = active_worktree { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 3dfde8a9afaaff788c4e7b9d0a59fcd9edaa04cc..42b3694789cfa3fa463f2390cce274b4d3a84fb6 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1051,15 +1051,16 @@ impl Terminal { navigation_target: MaybeNavigationTarget, cx: &mut Context<Self>, ) { - if let Some(prev_word) = prev_word { - if prev_word.word == word && prev_word.word_match == word_match { - self.last_content.last_hovered_word = Some(HoveredWord { - word, - word_match, - id: prev_word.id, - }); - return; - } + if let Some(prev_word) = prev_word + && prev_word.word == word + && prev_word.word_match == word_match + { + self.last_content.last_hovered_word = Some(HoveredWord { + word, + word_match, + id: prev_word.id, + }); + return; } self.last_content.last_hovered_word = Some(HoveredWord { @@ -1517,12 +1518,11 @@ impl Terminal { self.last_content.display_offset, ); - if self.mouse_changed(point, side) { - if let Some(bytes) = + if self.mouse_changed(point, side) + && let Some(bytes) = mouse_moved_report(point, e.pressed_button, e.modifiers, self.last_content.mode) - { - self.pty_tx.notify(bytes); - } + { + self.pty_tx.notify(bytes); } } else if e.modifiers.secondary() { self.word_from_position(e.position); @@ -1864,10 +1864,10 @@ impl Terminal { } pub fn kill_active_task(&mut self) { - if let Some(task) = self.task() { - if task.status == TaskStatus::Running { - self.pty_info.kill_current_process(); - } + if let Some(task) = self.task() + && task.status == TaskStatus::Running + { + self.pty_info.kill_current_process(); } } diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 3f89afffab766126d5f1ef33f7d12b109d0198ca..635e3e2ca5895562c7981d89169bf6f0632a223f 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -325,10 +325,10 @@ impl settings::Settings for TerminalSettings { .and_then(|v| v.as_object()) { for (k, v) in env { - if v.is_null() { - if let Some(zed_env) = current.env.as_mut() { - zed_env.remove(k); - } + if v.is_null() + && let Some(zed_env) = current.env.as_mut() + { + zed_env.remove(k); } let Some(v) = v.as_str() else { continue }; if let Some(zed_env) = current.env.as_mut() { diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 6c1be9d5e790e9e3b83aa843c1bd9b6ed9153b77..7575706db005987dc0647ddda9f0b01d3ab8a539 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -583,15 +583,15 @@ impl TerminalElement { strikethrough, }; - if let Some((style, range)) = hyperlink { - if range.contains(&indexed.point) { - if let Some(underline) = style.underline { - result.underline = Some(underline); - } + if let Some((style, range)) = hyperlink + && range.contains(&indexed.point) + { + if let Some(underline) = style.underline { + result.underline = Some(underline); + } - if let Some(color) = style.color { - result.color = color; - } + if let Some(color) = style.color { + result.color = color; } } @@ -1275,9 +1275,9 @@ impl Element for TerminalElement { } let text_paint_time = text_paint_start.elapsed(); - if let Some(text_to_mark) = &marked_text_cloned { - if !text_to_mark.is_empty() { - if let Some(cursor_layout) = &original_cursor { + if let Some(text_to_mark) = &marked_text_cloned + && !text_to_mark.is_empty() + && let Some(cursor_layout) = &original_cursor { let ime_position = cursor_layout.bounding_rect(origin).origin; let mut ime_style = layout.base_text_style.clone(); ime_style.underline = Some(UnderlineStyle { @@ -1303,14 +1303,11 @@ impl Element for TerminalElement { .paint(ime_position, layout.dimensions.line_height, window, cx) .log_err(); } - } - } - if self.cursor_visible && marked_text_cloned.is_none() { - if let Some(mut cursor) = original_cursor { + if self.cursor_visible && marked_text_cloned.is_none() + && let Some(mut cursor) = original_cursor { cursor.paint(origin, window, cx); } - } if let Some(mut element) = block_below_cursor_element { element.paint(window, cx); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index cdf405b642d3a85d46d40138a83c6df681724b59..b161a8ea8918a913ba4a83dde970ee325dd25dd7 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -255,8 +255,7 @@ impl TerminalPanel { .transpose() .log_err() .flatten() - { - if let Ok(serialized) = workspace + && let Ok(serialized) = workspace .update_in(&mut cx, |workspace, window, cx| { deserialize_terminal_panel( workspace.weak_handle(), @@ -268,9 +267,8 @@ impl TerminalPanel { ) })? .await - { - terminal_panel = Some(serialized); - } + { + terminal_panel = Some(serialized); } } _ => {} @@ -1077,11 +1075,10 @@ pub fn new_terminal_pane( return ControlFlow::Break(()); } }; - } else if let Some(project_path) = item.project_path(cx) { - if let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx) - { - add_paths_to_terminal(pane, &[entry_path], window, cx); - } + } else if let Some(project_path) = item.project_path(cx) + && let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx) + { + add_paths_to_terminal(pane, &[entry_path], window, cx); } } } else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>() { @@ -1103,10 +1100,8 @@ pub fn new_terminal_pane( { add_paths_to_terminal(pane, &[entry_path], window, cx); } - } else if is_local { - if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() { - add_paths_to_terminal(pane, paths.paths(), window, cx); - } + } else if is_local && let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() { + add_paths_to_terminal(pane, paths.paths(), window, cx); } ControlFlow::Break(()) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 559faea42a4503fa81f60f7cffba3de9dbe6972e..14b642bc123f3209778bc5cc806e669f1d1f1034 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -308,10 +308,10 @@ impl TerminalView { } else { let mut displayed_lines = total_lines; - if !self.focus_handle.is_focused(window) { - if let Some(max_lines) = max_lines_when_unfocused { - displayed_lines = displayed_lines.min(*max_lines) - } + if !self.focus_handle.is_focused(window) + && let Some(max_lines) = max_lines_when_unfocused + { + displayed_lines = displayed_lines.min(*max_lines) } ContentMode::Inline { @@ -1156,26 +1156,26 @@ fn subscribe_for_terminal_events( if let Some(opened_item) = opened_items.first() { if open_target.is_file() { - if let Some(Ok(opened_item)) = opened_item { - if let Some(row) = path_to_open.row { - let col = path_to_open.column.unwrap_or(0); - if let Some(active_editor) = - opened_item.downcast::<Editor>() - { - active_editor - .downgrade() - .update_in(cx, |editor, window, cx| { - editor.go_to_singleton_buffer_point( - language::Point::new( - row.saturating_sub(1), - col.saturating_sub(1), - ), - window, - cx, - ) - }) - .log_err(); - } + if let Some(Ok(opened_item)) = opened_item + && let Some(row) = path_to_open.row + { + let col = path_to_open.column.unwrap_or(0); + if let Some(active_editor) = + opened_item.downcast::<Editor>() + { + active_editor + .downgrade() + .update_in(cx, |editor, window, cx| { + editor.go_to_singleton_buffer_point( + language::Point::new( + row.saturating_sub(1), + col.saturating_sub(1), + ), + window, + cx, + ) + }) + .log_err(); } } } else if open_target.is_dir() { @@ -1321,17 +1321,17 @@ fn possible_open_target( } }; - if path_to_check.path.is_relative() { - if let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) { - return Task::ready(Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree_root.join(&entry.path), - row: path_to_check.row, - column: path_to_check.column, - }, - entry.clone(), - ))); - } + if path_to_check.path.is_relative() + && let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) + { + return Task::ready(Some(OpenTarget::Worktree( + PathWithPosition { + path: worktree_root.join(&entry.path), + row: path_to_check.row, + column: path_to_check.column, + }, + entry.clone(), + ))); } paths_to_check.push(path_to_check); @@ -1428,11 +1428,11 @@ fn possible_open_target( let fs = workspace.read(cx).project().read(cx).fs().clone(); cx.background_spawn(async move { for mut path_to_check in fs_paths_to_check { - if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() { - if let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() { - path_to_check.path = fs_path_to_check; - return Some(OpenTarget::File(path_to_check, metadata)); - } + if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() + && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() + { + path_to_check.path = fs_path_to_check; + return Some(OpenTarget::File(path_to_check, metadata)); } } diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 9f7e49d24dfe8b14ccae5cffb18bb1c65382e3cb..8e37567738f90ff702b2ac565b3c3985c5b3c72e 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -446,10 +446,10 @@ impl History { } fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) { - if let Some(transaction) = self.forget(transaction) { - if let Some(destination) = self.transaction_mut(destination) { - destination.edit_ids.extend(transaction.edit_ids); - } + if let Some(transaction) = self.forget(transaction) + && let Some(destination) = self.transaction_mut(destination) + { + destination.edit_ids.extend(transaction.edit_ids); } } @@ -1585,11 +1585,11 @@ impl Buffer { .map(Some) .chain([None]) .filter_map(move |range| { - if let Some((range, prev_range)) = range.as_ref().zip(prev_range.as_mut()) { - if prev_range.end == range.start { - prev_range.end = range.end; - return None; - } + if let Some((range, prev_range)) = range.as_ref().zip(prev_range.as_mut()) + && prev_range.end == range.start + { + prev_range.end = range.end; + return None; } let result = prev_range.clone(); prev_range = range; @@ -1685,10 +1685,10 @@ impl Buffer { rx = Some(channel.1); } async move { - if let Some(mut rx) = rx { - if rx.recv().await.is_none() { - anyhow::bail!("gave up waiting for version"); - } + if let Some(mut rx) = rx + && rx.recv().await.is_none() + { + anyhow::bail!("gave up waiting for version"); } Ok(()) } diff --git a/crates/theme/src/fallback_themes.rs b/crates/theme/src/fallback_themes.rs index e9e8e2d0db9320a4ec6bf95c53a11c84d1887777..13786aca57aab50b503da48d0d4f54fdd78b88c2 100644 --- a/crates/theme/src/fallback_themes.rs +++ b/crates/theme/src/fallback_themes.rs @@ -34,10 +34,10 @@ pub(crate) fn apply_status_color_defaults(status: &mut StatusColorsRefinement) { (&status.error, &mut status.error_background), (&status.hidden, &mut status.hidden_background), ] { - if bg_color.is_none() { - if let Some(fg_color) = fg_color { - *bg_color = Some(fg_color.opacity(0.25)); - } + if bg_color.is_none() + && let Some(fg_color) = fg_color + { + *bg_color = Some(fg_color.opacity(0.25)); } } } diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index 98f0eeb6cc1696e4fd8a46376b17063dc3144ac4..d8b0b8dc6bb62b7547dcf945a73dc45bed9a86fc 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -269,32 +269,31 @@ impl Render for ApplicationMenu { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let all_menus_shown = self.all_menus_shown(cx); - if let Some(pending_menu_open) = self.pending_menu_open.take() { - if let Some(entry) = self + if let Some(pending_menu_open) = self.pending_menu_open.take() + && let Some(entry) = self .entries .iter() .find(|entry| entry.menu.name == pending_menu_open && !entry.handle.is_deployed()) - { - let handle_to_show = entry.handle.clone(); - let handles_to_hide: Vec<_> = self - .entries - .iter() - .filter(|e| e.menu.name != pending_menu_open && e.handle.is_deployed()) - .map(|e| e.handle.clone()) - .collect(); - - if handles_to_hide.is_empty() { - // We need to wait for the next frame to show all menus first, - // before we can handle show/hide operations - window.on_next_frame(move |window, cx| { - handles_to_hide.iter().for_each(|handle| handle.hide(cx)); - window.defer(cx, move |window, cx| handle_to_show.show(window, cx)); - }); - } else { - // Since menus are already shown, we can directly handle show/hide operations + { + let handle_to_show = entry.handle.clone(); + let handles_to_hide: Vec<_> = self + .entries + .iter() + .filter(|e| e.menu.name != pending_menu_open && e.handle.is_deployed()) + .map(|e| e.handle.clone()) + .collect(); + + if handles_to_hide.is_empty() { + // We need to wait for the next frame to show all menus first, + // before we can handle show/hide operations + window.on_next_frame(move |window, cx| { handles_to_hide.iter().for_each(|handle| handle.hide(cx)); - cx.defer_in(window, move |_, window, cx| handle_to_show.show(window, cx)); - } + window.defer(cx, move |window, cx| handle_to_show.show(window, cx)); + }); + } else { + // Since menus are already shown, we can directly handle show/hide operations + handles_to_hide.iter().for_each(|handle| handle.hide(cx)); + cx.defer_in(window, move |_, window, cx| handle_to_show.show(window, cx)); } } diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 84622888f1ba7b9d7ddd2760f394c6f0a747a19e..5bd6a17e4b86657097c221705c3c48966e09fa41 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -593,11 +593,11 @@ impl TitleBar { Button::new("connection-status", label) .label_size(LabelSize::Small) .on_click(|_, window, cx| { - if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) { - if auto_updater.read(cx).status().is_updated() { - workspace::reload(cx); - return; - } + if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) + && auto_updater.read(cx).status().is_updated() + { + workspace::reload(cx); + return; } auto_update::check(&Default::default(), window, cx); }) diff --git a/crates/toolchain_selector/src/active_toolchain.rs b/crates/toolchain_selector/src/active_toolchain.rs index 01bd7b0a9cfd3b36c229b9082780ccaa355e452f..ea5dcc2a1964a3330069e45b04e5ede8fa5a6972 100644 --- a/crates/toolchain_selector/src/active_toolchain.rs +++ b/crates/toolchain_selector/src/active_toolchain.rs @@ -121,31 +121,30 @@ impl ActiveToolchain { cx: &mut Context<Self>, ) { let editor = editor.read(cx); - if let Some((_, buffer, _)) = editor.active_excerpt(cx) { - if let Some(worktree_id) = buffer.read(cx).file().map(|file| file.worktree_id(cx)) { - if self - .active_buffer - .as_ref() - .is_some_and(|(old_worktree_id, old_buffer, _)| { - (old_worktree_id, old_buffer.entity_id()) - == (&worktree_id, buffer.entity_id()) - }) - { - return; - } - - let subscription = cx.subscribe_in( - &buffer, - window, - |this, _, event: &BufferEvent, window, cx| { - if matches!(event, BufferEvent::LanguageChanged) { - this._update_toolchain_task = Self::spawn_tracker_task(window, cx); - } - }, - ); - self.active_buffer = Some((worktree_id, buffer.downgrade(), subscription)); - self._update_toolchain_task = Self::spawn_tracker_task(window, cx); + if let Some((_, buffer, _)) = editor.active_excerpt(cx) + && let Some(worktree_id) = buffer.read(cx).file().map(|file| file.worktree_id(cx)) + { + if self + .active_buffer + .as_ref() + .is_some_and(|(old_worktree_id, old_buffer, _)| { + (old_worktree_id, old_buffer.entity_id()) == (&worktree_id, buffer.entity_id()) + }) + { + return; } + + let subscription = cx.subscribe_in( + &buffer, + window, + |this, _, event: &BufferEvent, window, cx| { + if matches!(event, BufferEvent::LanguageChanged) { + this._update_toolchain_task = Self::spawn_tracker_task(window, cx); + } + }, + ); + self.active_buffer = Some((worktree_id, buffer.downgrade(), subscription)); + self._update_toolchain_task = Self::spawn_tracker_task(window, cx); } cx.notify(); diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs index 21d95a66dea3e8cf8c999142baea352bd139a54e..cdd3db99e0679a30bbdf679459ba26b76f62d383 100644 --- a/crates/toolchain_selector/src/toolchain_selector.rs +++ b/crates/toolchain_selector/src/toolchain_selector.rs @@ -211,16 +211,15 @@ impl ToolchainSelectorDelegate { let _ = this.update_in(cx, move |this, window, cx| { this.delegate.candidates = available_toolchains; - if let Some(active_toolchain) = active_toolchain { - if let Some(position) = this + if let Some(active_toolchain) = active_toolchain + && let Some(position) = this .delegate .candidates .toolchains .iter() .position(|toolchain| *toolchain == active_toolchain) - { - this.delegate.set_selected_index(position, window, cx); - } + { + this.delegate.set_selected_index(position, window, cx); } this.update_matches(this.query(cx), window, cx); }); diff --git a/crates/ui/src/components/label/highlighted_label.rs b/crates/ui/src/components/label/highlighted_label.rs index 72f54e08da88f111ab135518515b1ffd57a61079..576d47eda5b802ce4cf2fc97b8dac0b20b43e883 100644 --- a/crates/ui/src/components/label/highlighted_label.rs +++ b/crates/ui/src/components/label/highlighted_label.rs @@ -98,12 +98,12 @@ pub fn highlight_ranges( loop { end_ix = end_ix + text[end_ix..].chars().next().unwrap().len_utf8(); - if let Some(&next_ix) = highlight_indices.peek() { - if next_ix == end_ix { - end_ix = next_ix; - highlight_indices.next(); - continue; - } + if let Some(&next_ix) = highlight_indices.peek() + && next_ix == end_ix + { + end_ix = next_ix; + highlight_indices.next(); + continue; } break; } diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index 55ce0218c75d4450067a5c09c2ea523f7d86ca3c..f77eea4bdc7df950d0bce17d906cbbfda9233b6d 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -72,10 +72,10 @@ impl<M: ManagedView> PopoverMenuHandle<M> { } pub fn hide(&self, cx: &mut App) { - if let Some(state) = self.0.borrow().as_ref() { - if let Some(menu) = state.menu.borrow().as_ref() { - menu.update(cx, |_, cx| cx.emit(DismissEvent)); - } + if let Some(state) = self.0.borrow().as_ref() + && let Some(menu) = state.menu.borrow().as_ref() + { + menu.update(cx, |_, cx| cx.emit(DismissEvent)); } } @@ -278,10 +278,10 @@ fn show_menu<M: ManagedView>( window .subscribe(&new_menu, cx, move |modal, _: &DismissEvent, window, cx| { - if modal.focus_handle(cx).contains_focused(window, cx) { - if let Some(previous_focus_handle) = previous_focus_handle.as_ref() { - window.focus(previous_focus_handle); - } + if modal.focus_handle(cx).contains_focused(window, cx) + && let Some(previous_focus_handle) = previous_focus_handle.as_ref() + { + window.focus(previous_focus_handle); } *menu2.borrow_mut() = None; window.refresh(); @@ -373,14 +373,14 @@ impl<M: ManagedView> Element for PopoverMenu<M> { (child_builder)(element_state.menu.clone(), self.menu_builder.clone()) }); - if let Some(trigger_handle) = self.trigger_handle.take() { - if let Some(menu_builder) = self.menu_builder.clone() { - *trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState { - menu_builder, - menu: element_state.menu.clone(), - on_open: self.on_open.clone(), - }); - } + if let Some(trigger_handle) = self.trigger_handle.take() + && let Some(menu_builder) = self.menu_builder.clone() + { + *trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState { + menu_builder, + menu: element_state.menu.clone(), + on_open: self.on_open.clone(), + }); } let child_layout_id = child_element diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index 85ef549bc017eb0708fe81c41693232a2d6deaa5..761189671b935bf1f3d9e3f7d4d547528cf20196 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -250,12 +250,11 @@ impl<M: ManagedView> Element for RightClickMenu<M> { window .subscribe(&new_menu, cx, move |modal, _: &DismissEvent, window, cx| { - if modal.focus_handle(cx).contains_focused(window, cx) { - if let Some(previous_focus_handle) = + if modal.focus_handle(cx).contains_focused(window, cx) + && let Some(previous_focus_handle) = previous_focus_handle.as_ref() - { - window.focus(previous_focus_handle); - } + { + window.focus(previous_focus_handle); } *menu2.borrow_mut() = None; window.refresh(); diff --git a/crates/util/src/fs.rs b/crates/util/src/fs.rs index 3e96594f85caf801aa80d18ee423f3d94e23b426..60aab4a2e746862c2e9b5b4933c0cf32c9d34310 100644 --- a/crates/util/src/fs.rs +++ b/crates/util/src/fs.rs @@ -13,13 +13,13 @@ where while let Some(entry) = entries.next().await { if let Some(entry) = entry.log_err() { let entry_path = entry.path(); - if predicate(entry_path.as_path()) { - if let Ok(metadata) = fs::metadata(&entry_path).await { - if metadata.is_file() { - fs::remove_file(&entry_path).await.log_err(); - } else { - fs::remove_dir_all(&entry_path).await.log_err(); - } + if predicate(entry_path.as_path()) + && let Ok(metadata) = fs::metadata(&entry_path).await + { + if metadata.is_file() { + fs::remove_file(&entry_path).await.log_err(); + } else { + fs::remove_dir_all(&entry_path).await.log_err(); } } } @@ -35,10 +35,10 @@ where if let Some(mut entries) = fs::read_dir(dir).await.log_err() { while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - if predicate(entry.path().as_path()) { - matching.push(entry.path()); - } + if let Some(entry) = entry.log_err() + && predicate(entry.path().as_path()) + { + matching.push(entry.path()); } } } @@ -58,10 +58,9 @@ where if let Some(file_name) = entry_path .file_name() .map(|file_name| file_name.to_string_lossy()) + && predicate(&file_name) { - if predicate(&file_name) { - return Some(entry_path); - } + return Some(entry_path); } } } diff --git a/crates/util/src/schemars.rs b/crates/util/src/schemars.rs index e162b41933117eb603d36601aaf4b87b0e3d1d85..a59d24c3251b6aebfa8adb9a3dfa809c34627c73 100644 --- a/crates/util/src/schemars.rs +++ b/crates/util/src/schemars.rs @@ -44,13 +44,12 @@ pub struct DefaultDenyUnknownFields; impl schemars::transform::Transform for DefaultDenyUnknownFields { fn transform(&mut self, schema: &mut schemars::Schema) { - if let Some(object) = schema.as_object_mut() { - if object.contains_key("properties") - && !object.contains_key("additionalProperties") - && !object.contains_key("unevaluatedProperties") - { - object.insert("additionalProperties".to_string(), false.into()); - } + if let Some(object) = schema.as_object_mut() + && object.contains_key("properties") + && !object.contains_key("additionalProperties") + && !object.contains_key("unevaluatedProperties") + { + object.insert("additionalProperties".to_string(), false.into()); } transform_subschemas(self, schema); } diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index e1b25f4dbad0499d2336f87bef9962548f0d3d0b..187678f8af524d0f938bdcb5cc8b1b356ae5cde7 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -128,11 +128,9 @@ pub fn truncate_lines_to_byte_limit(s: &str, max_bytes: usize) -> &str { } for i in (0..max_bytes).rev() { - if s.is_char_boundary(i) { - if s.as_bytes()[i] == b'\n' { - // Since the i-th character is \n, valid to slice at i + 1. - return &s[..i + 1]; - } + if s.is_char_boundary(i) && s.as_bytes()[i] == b'\n' { + // Since the i-th character is \n, valid to slice at i + 1. + return &s[..i + 1]; } } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index fe1537684c897ef4a03a073edaf700fa23f0c19d..00d3bde750a7fc22eeafd5c7d1771ee47368b2e8 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -510,17 +510,16 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) { vim.switch_mode(Mode::Normal, true, window, cx); } vim.update_editor(cx, |_, editor, cx| { - if let Some(first_sel) = initial_selections { - if let Some(tx_id) = editor + if let Some(first_sel) = initial_selections + && let Some(tx_id) = editor .buffer() .update(cx, |multi, cx| multi.last_transaction_id(cx)) - { - let last_sel = editor.selections.disjoint_anchors(); - editor.modify_transaction_selection_history(tx_id, |old| { - old.0 = first_sel; - old.1 = Some(last_sel); - }); - } + { + let last_sel = editor.selections.disjoint_anchors(); + editor.modify_transaction_selection_history(tx_id, |old| { + old.0 = first_sel; + old.1 = Some(last_sel); + }); } }); }) @@ -1713,14 +1712,12 @@ impl Vim { match c { '%' => { self.update_editor(cx, |_, editor, cx| { - if let Some((_, buffer, _)) = editor.active_excerpt(cx) { - if let Some(file) = buffer.read(cx).file() { - if let Some(local) = file.as_local() { - if let Some(str) = local.path().to_str() { - ret.push_str(str) - } - } - } + if let Some((_, buffer, _)) = editor.active_excerpt(cx) + && let Some(file) = buffer.read(cx).file() + && let Some(local) = file.as_local() + && let Some(str) = local.path().to_str() + { + ret.push_str(str) } }); } @@ -1954,19 +1951,19 @@ impl ShellExec { return; }; - if let Some(mut stdin) = running.stdin.take() { - if let Some(snapshot) = input_snapshot { - let range = range.clone(); - cx.background_spawn(async move { - for chunk in snapshot.text_for_range(range) { - if stdin.write_all(chunk.as_bytes()).log_err().is_none() { - return; - } + if let Some(mut stdin) = running.stdin.take() + && let Some(snapshot) = input_snapshot + { + let range = range.clone(); + cx.background_spawn(async move { + for chunk in snapshot.text_for_range(range) { + if stdin.write_all(chunk.as_bytes()).log_err().is_none() { + return; } - stdin.flush().log_err(); - }) - .detach(); - } + } + stdin.flush().log_err(); + }) + .detach(); }; let output = cx diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs index c555b781b1e01d7a32dd84a8b78f7dab638e48a3..beb3bd54bae2b6ef26126bd314db60af84f2dc94 100644 --- a/crates/vim/src/digraph.rs +++ b/crates/vim/src/digraph.rs @@ -63,15 +63,15 @@ impl Vim { } fn literal(&mut self, action: &Literal, window: &mut Window, cx: &mut Context<Self>) { - if let Some(Operator::Literal { prefix }) = self.active_operator() { - if let Some(prefix) = prefix { - if let Some(keystroke) = Keystroke::parse(&action.0).ok() { - window.defer(cx, |window, cx| { - window.dispatch_keystroke(keystroke, cx); - }); - } - return self.handle_literal_input(prefix, "", window, cx); + if let Some(Operator::Literal { prefix }) = self.active_operator() + && let Some(prefix) = prefix + { + if let Some(keystroke) = Keystroke::parse(&action.0).ok() { + window.defer(cx, |window, cx| { + window.dispatch_keystroke(keystroke, cx); + }); } + return self.handle_literal_input(prefix, "", window, cx); } self.insert_literal(Some(action.1), "", window, cx); diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 367b5130b64b1c37f856079b4b57349f674587ea..e703b181172c01aa889626b13bbe8dc0acde0d67 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1811,10 +1811,10 @@ fn previous_word_end( .ignore_punctuation(ignore_punctuation); let mut point = point.to_point(map); - if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) { - if let Some(ch) = map.buffer_snapshot.chars_at(point).next() { - point.column += ch.len_utf8() as u32; - } + if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) + && let Some(ch) = map.buffer_snapshot.chars_at(point).next() + { + point.column += ch.len_utf8() as u32; } for _ in 0..times { let new_point = movement::find_preceding_boundary_point( @@ -1986,10 +1986,10 @@ fn previous_subword_end( .ignore_punctuation(ignore_punctuation); let mut point = point.to_point(map); - if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) { - if let Some(ch) = map.buffer_snapshot.chars_at(point).next() { - point.column += ch.len_utf8() as u32; - } + if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) + && let Some(ch) = map.buffer_snapshot.chars_at(point).next() + { + point.column += ch.len_utf8() as u32; } for _ in 0..times { let new_point = movement::find_preceding_boundary_point( @@ -2054,10 +2054,10 @@ pub(crate) fn last_non_whitespace( let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map)); // NOTE: depending on clip_at_line_end we may already be one char back from the end. - if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() { - if classifier.kind(ch) != CharKind::Whitespace { - return end_of_line.to_display_point(map); - } + if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() + && classifier.kind(ch) != CharKind::Whitespace + { + return end_of_line.to_display_point(map); } for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) { diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index d7a6932baa8e141a7c8ffd64f7c34d75db29fe97..6f406d0c44ab45189ddf39bbf5e905f2950d36aa 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -74,10 +74,10 @@ impl Vim { editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { let mut cursor = selection.head(); - if kind.linewise() { - if let Some(column) = original_columns.get(&selection.id) { - *cursor.column_mut() = *column - } + if kind.linewise() + && let Some(column) = original_columns.get(&selection.id) + { + *cursor.column_mut() = *column } cursor = map.clip_point(cursor, Bias::Left); selection.collapse_to(cursor, selection.goal) diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs index 1d6264d593f2bea3326b6953310a5f51f363d775..80d94def0544fffad6d9eaa39a05d8ee932b610c 100644 --- a/crates/vim/src/normal/mark.rs +++ b/crates/vim/src/normal/mark.rs @@ -256,10 +256,8 @@ impl Vim { } }); - if should_jump { - if let Some(anchor) = anchor { - self.motion(Motion::Jump { anchor, line }, window, cx) - } + if should_jump && let Some(anchor) = anchor { + self.motion(Motion::Jump { anchor, line }, window, cx) } } } diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 5cc37629905138ab7b1e651fbf3a1de3047a60a3..2d7927480869f7a14cff7e2051ec421268df1d97 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -221,14 +221,14 @@ impl Vim { if actions.is_empty() { return None; } - if globals.replayer.is_none() { - if let Some(recording_register) = globals.recording_register { - globals - .recordings - .entry(recording_register) - .or_default() - .push(ReplayableAction::Action(Repeat.boxed_clone())); - } + if globals.replayer.is_none() + && let Some(recording_register) = globals.recording_register + { + globals + .recordings + .entry(recording_register) + .or_default() + .push(ReplayableAction::Action(Repeat.boxed_clone())); } let mut mode = None; @@ -320,10 +320,10 @@ impl Vim { // vim doesn't treat 3a1 as though you literally repeated a1 // 3 times, instead it inserts the content thrice at the insert position. if let Some(to_repeat) = repeatable_insert(&actions[0]) { - if let Some(ReplayableAction::Action(action)) = actions.last() { - if NormalBefore.partial_eq(&**action) { - actions.pop(); - } + if let Some(ReplayableAction::Action(action)) = actions.last() + && NormalBefore.partial_eq(&**action) + { + actions.pop(); } let mut new_actions = actions.clone(); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index cff23c4bd4afd02fddc1942cb52179a6beb0e60f..c65da4f90babc04d2ebe733848f2b94e0114ca9e 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -100,10 +100,10 @@ fn cover_or_next<I: Iterator<Item = (Range<usize>, Range<usize>)>>( for (open_range, close_range) in ranges { let start_off = open_range.start; let end_off = close_range.end; - if let Some(range_filter) = range_filter { - if !range_filter(open_range.clone(), close_range.clone()) { - continue; - } + if let Some(range_filter) = range_filter + && !range_filter(open_range.clone(), close_range.clone()) + { + continue; } let candidate = CandidateWithRanges { candidate: CandidateRange { @@ -1060,11 +1060,11 @@ fn text_object( .filter_map(|(r, m)| if m == target { Some(r) } else { None }) .collect(); matches.sort_by_key(|r| r.start); - if let Some(buffer_range) = matches.first() { - if !buffer_range.is_empty() { - let range = excerpt.map_range_from_buffer(buffer_range.clone()); - return Some(range.start.to_display_point(map)..range.end.to_display_point(map)); - } + if let Some(buffer_range) = matches.first() + && !buffer_range.is_empty() + { + let range = excerpt.map_range_from_buffer(buffer_range.clone()); + return Some(range.start.to_display_point(map)..range.end.to_display_point(map)); } let buffer_range = excerpt.map_range_from_buffer(around_range.clone()); return Some(buffer_range.start.to_display_point(map)..buffer_range.end.to_display_point(map)); @@ -1529,25 +1529,25 @@ fn surrounding_markers( Some((ch, _)) => ch, _ => '\0', }; - if let Some((ch, range)) = movement::chars_after(map, point).next() { - if ch == open_marker && before_ch != '\\' { - if open_marker == close_marker { - let mut total = 0; - for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() - { - if ch == '\n' { - break; - } - if ch == open_marker && before_ch != '\\' { - total += 1; - } + if let Some((ch, range)) = movement::chars_after(map, point).next() + && ch == open_marker + && before_ch != '\\' + { + if open_marker == close_marker { + let mut total = 0; + for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() { + if ch == '\n' { + break; } - if total % 2 == 0 { - opening = Some(range) + if ch == open_marker && before_ch != '\\' { + total += 1; } - } else { + } + if total % 2 == 0 { opening = Some(range) } + } else { + opening = Some(range) } } @@ -1558,10 +1558,10 @@ fn surrounding_markers( break; } - if let Some((before_ch, _)) = chars_before.peek() { - if *before_ch == '\\' { - continue; - } + if let Some((before_ch, _)) = chars_before.peek() + && *before_ch == '\\' + { + continue; } if ch == open_marker { diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 2e8e2f76bd3de363a4554004786bd6fb535b4631..db19562f027973f978578aa3c5a1ba6f82e970f2 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -412,20 +412,20 @@ impl MarksState { let mut to_write = HashMap::default(); for (key, value) in &new_points { - if self.is_global_mark(key) { - if self.global_marks.get(key) != Some(&MarkLocation::Path(path.clone())) { - if let Some(workspace_id) = self.workspace_id(cx) { - let path = path.clone(); - let key = key.clone(); - cx.background_spawn(async move { - DB.set_global_mark_path(workspace_id, key, path).await - }) - .detach_and_log_err(cx); - } - - self.global_marks - .insert(key.clone(), MarkLocation::Path(path.clone())); + if self.is_global_mark(key) + && self.global_marks.get(key) != Some(&MarkLocation::Path(path.clone())) + { + if let Some(workspace_id) = self.workspace_id(cx) { + let path = path.clone(); + let key = key.clone(); + cx.background_spawn(async move { + DB.set_global_mark_path(workspace_id, key, path).await + }) + .detach_and_log_err(cx); } + + self.global_marks + .insert(key.clone(), MarkLocation::Path(path.clone())); } if old_points.and_then(|o| o.get(key)) != Some(value) { to_write.insert(key.clone(), value.clone()); @@ -456,15 +456,15 @@ impl MarksState { buffer: &Entity<Buffer>, cx: &mut Context<Self>, ) { - if let MarkLocation::Buffer(entity_id) = old_path { - if let Some(old_marks) = self.multibuffer_marks.remove(&entity_id) { - let buffer_marks = old_marks - .into_iter() - .map(|(k, v)| (k, v.into_iter().map(|anchor| anchor.text_anchor).collect())) - .collect(); - self.buffer_marks - .insert(buffer.read(cx).remote_id(), buffer_marks); - } + if let MarkLocation::Buffer(entity_id) = old_path + && let Some(old_marks) = self.multibuffer_marks.remove(&entity_id) + { + let buffer_marks = old_marks + .into_iter() + .map(|(k, v)| (k, v.into_iter().map(|anchor| anchor.text_anchor).collect())) + .collect(); + self.buffer_marks + .insert(buffer.read(cx).remote_id(), buffer_marks); } self.watch_buffer(MarkLocation::Path(new_path.clone()), buffer, cx); self.serialize_buffer_marks(new_path, buffer, cx); @@ -512,10 +512,9 @@ impl MarksState { .watched_buffers .get(&buffer_id.clone()) .map(|(path, _, _)| path.clone()) + && let Some(new_path) = this.path_for_buffer(&buffer, cx) { - if let Some(new_path) = this.path_for_buffer(&buffer, cx) { - this.rename_buffer(old_path, new_path, &buffer, cx) - } + this.rename_buffer(old_path, new_path, &buffer, cx) } } _ => {} @@ -897,13 +896,13 @@ impl VimGlobals { self.stop_recording_after_next_action = false; } } - if self.replayer.is_none() { - if let Some(recording_register) = self.recording_register { - self.recordings - .entry(recording_register) - .or_default() - .push(ReplayableAction::Action(action)); - } + if self.replayer.is_none() + && let Some(recording_register) = self.recording_register + { + self.recordings + .entry(recording_register) + .or_default() + .push(ReplayableAction::Action(action)); } } @@ -1330,10 +1329,10 @@ impl MarksMatchInfo { let mut offset = 0; for chunk in chunks { line.push_str(chunk.text); - if let Some(highlight_style) = chunk.syntax_highlight_id { - if let Some(highlight) = highlight_style.style(cx.theme().syntax()) { - highlights.push((offset..offset + chunk.text.len(), highlight)) - } + if let Some(highlight_style) = chunk.syntax_highlight_id + && let Some(highlight) = highlight_style.style(cx.theme().syntax()) + { + highlights.push((offset..offset + chunk.text.len(), highlight)) } offset += chunk.text.len(); } diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index 63cd21e88cbd621385cb99e8310819180ebc6db9..ca65204fab2a8ddd21e4788ca5c1e5cbe325447c 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -174,12 +174,11 @@ impl Vim { if ch.to_string() == pair.start { let start = offset; let mut end = start + 1; - if surround { - if let Some((next_ch, _)) = chars_and_offset.peek() { - if next_ch.eq(&' ') { - end += 1; - } - } + if surround + && let Some((next_ch, _)) = chars_and_offset.peek() + && next_ch.eq(&' ') + { + end += 1; } edits.push((start..end, "")); anchors.push(start..start); @@ -193,12 +192,11 @@ impl Vim { if ch.to_string() == pair.end { let mut start = offset; let end = start + 1; - if surround { - if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() { - if next_ch.eq(&' ') { - start -= 1; - } - } + if surround + && let Some((next_ch, _)) = reverse_chars_and_offsets.peek() + && next_ch.eq(&' ') + { + start -= 1; } edits.push((start..end, "")); break; diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 45cef3a2b9f134f6fe4fe82299da4bbe55ead92d..98dabb83163569c5b1cf9ecce49a7e793b5209a6 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -217,10 +217,11 @@ impl NeovimConnection { .expect("Could not set nvim cursor position"); } - if let Some(NeovimData::Get { mode, state }) = self.data.back() { - if *mode == Mode::Normal && *state == marked_text { - return; - } + if let Some(NeovimData::Get { mode, state }) = self.data.back() + && *mode == Mode::Normal + && *state == marked_text + { + return; } self.data.push_back(NeovimData::Put { state: marked_text.to_string(), diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 15b0b443b5556c9dcf9d24c096fb4f2654352166..81c1a6b0b388227c236449de36f43f6b0287e48b 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -788,10 +788,10 @@ impl Vim { editor.selections.line_mode = false; editor.unregister_addon::<VimAddon>(); editor.set_relative_line_number(None, cx); - if let Some(vim) = Vim::globals(cx).focused_vim() { - if vim.entity_id() == cx.entity().entity_id() { - Vim::globals(cx).focused_vim = None; - } + if let Some(vim) = Vim::globals(cx).focused_vim() + && vim.entity_id() == cx.entity().entity_id() + { + Vim::globals(cx).focused_vim = None; } } @@ -833,10 +833,10 @@ impl Vim { if self.exit_temporary_mode { self.exit_temporary_mode = false; // Don't switch to insert mode if the action is temporary_normal. - if let Some(action) = keystroke_event.action.as_ref() { - if action.as_any().downcast_ref::<TemporaryNormal>().is_some() { - return; - } + if let Some(action) = keystroke_event.action.as_ref() + && action.as_any().downcast_ref::<TemporaryNormal>().is_some() + { + return; } self.switch_mode(Mode::Insert, false, window, cx) } @@ -1006,10 +1006,10 @@ impl Vim { Some((point, goal)) }) } - if last_mode == Mode::Insert || last_mode == Mode::Replace { - if let Some(prior_tx) = prior_tx { - editor.group_until_transaction(prior_tx, cx) - } + if (last_mode == Mode::Insert || last_mode == Mode::Replace) + && let Some(prior_tx) = prior_tx + { + editor.group_until_transaction(prior_tx, cx) } editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { @@ -1031,14 +1031,16 @@ impl Vim { } let snapshot = s.display_map(); - if let Some(pending) = s.pending.as_mut() { - if pending.selection.reversed && mode.is_visual() && !last_mode.is_visual() { - let mut end = pending.selection.end.to_point(&snapshot.buffer_snapshot); - end = snapshot - .buffer_snapshot - .clip_point(end + Point::new(0, 1), Bias::Right); - pending.selection.end = snapshot.buffer_snapshot.anchor_before(end); - } + if let Some(pending) = s.pending.as_mut() + && pending.selection.reversed + && mode.is_visual() + && !last_mode.is_visual() + { + let mut end = pending.selection.end.to_point(&snapshot.buffer_snapshot); + end = snapshot + .buffer_snapshot + .clip_point(end + Point::new(0, 1), Bias::Right); + pending.selection.end = snapshot.buffer_snapshot.anchor_before(end); } s.move_with(|map, selection| { @@ -1536,12 +1538,12 @@ impl Vim { if self.mode == Mode::Insert && self.current_tx.is_some() { if self.current_anchor.is_none() { self.current_anchor = Some(newest); - } else if self.current_anchor.as_ref().unwrap() != &newest { - if let Some(tx_id) = self.current_tx.take() { - self.update_editor(cx, |_, editor, cx| { - editor.group_until_transaction(tx_id, cx) - }); - } + } else if self.current_anchor.as_ref().unwrap() != &newest + && let Some(tx_id) = self.current_tx.take() + { + self.update_editor(cx, |_, editor, cx| { + editor.group_until_transaction(tx_id, cx) + }); } } else if self.mode == Mode::Normal && newest.start != newest.end { if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index ae72df397113904ab2762e182d57bc833b268753..079f66ae9de39e122b762268c7db664c9ffe498f 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -305,15 +305,14 @@ impl Dock { .detach(); cx.observe_in(&dock, window, move |workspace, dock, window, cx| { - if dock.read(cx).is_open() { - if let Some(panel) = dock.read(cx).active_panel() { - if panel.is_zoomed(window, cx) { - workspace.zoomed = Some(panel.to_any().downgrade()); - workspace.zoomed_position = Some(position); - cx.emit(Event::ZoomChanged); - return; - } - } + if dock.read(cx).is_open() + && let Some(panel) = dock.read(cx).active_panel() + && panel.is_zoomed(window, cx) + { + workspace.zoomed = Some(panel.to_any().downgrade()); + workspace.zoomed_position = Some(position); + cx.emit(Event::ZoomChanged); + return; } if workspace.zoomed_position == Some(position) { workspace.zoomed = None; @@ -541,10 +540,10 @@ impl Dock { Ok(ix) => ix, Err(ix) => ix, }; - if let Some(active_index) = self.active_panel_index.as_mut() { - if *active_index >= index { - *active_index += 1; - } + if let Some(active_index) = self.active_panel_index.as_mut() + && *active_index >= index + { + *active_index += 1; } self.panel_entries.insert( index, @@ -566,16 +565,16 @@ impl Dock { pub fn restore_state(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool { if let Some(serialized) = self.serialized_dock.clone() { - if let Some(active_panel) = serialized.active_panel.filter(|_| serialized.visible) { - if let Some(idx) = self.panel_index_for_persistent_name(active_panel.as_str(), cx) { - self.activate_panel(idx, window, cx); - } + if let Some(active_panel) = serialized.active_panel.filter(|_| serialized.visible) + && let Some(idx) = self.panel_index_for_persistent_name(active_panel.as_str(), cx) + { + self.activate_panel(idx, window, cx); } - if serialized.zoom { - if let Some(panel) = self.active_panel() { - panel.set_zoomed(true, window, cx) - } + if serialized.zoom + && let Some(panel) = self.active_panel() + { + panel.set_zoomed(true, window, cx) } self.set_open(serialized.visible, window, cx); return true; diff --git a/crates/workspace/src/history_manager.rs b/crates/workspace/src/history_manager.rs index e63b1823ea08aab67a48ad3766da853afcddf4d2..a8387369f424c17dd667b029b67c9d2e7b73cd3f 100644 --- a/crates/workspace/src/history_manager.rs +++ b/crates/workspace/src/history_manager.rs @@ -101,11 +101,11 @@ impl HistoryManager { } let mut deleted_ids = Vec::new(); for idx in (0..self.history.len()).rev() { - if let Some(entry) = self.history.get(idx) { - if user_removed.contains(&entry.path) { - deleted_ids.push(entry.id); - self.history.remove(idx); - } + if let Some(entry) = self.history.get(idx) + && user_removed.contains(&entry.path) + { + deleted_ids.push(entry.id); + self.history.remove(idx); } } cx.spawn(async move |_| { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 0c5543650ee56f718674964efd02f57adeee7774..014af7b0bc7a7080289617fcb64dddcec731da2e 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -832,10 +832,10 @@ impl<T: Item> ItemHandle for Entity<T> { if let Some(item) = item.to_followable_item_handle(cx) { let leader_id = workspace.leader_for_pane(&pane); - if let Some(leader_id) = leader_id { - if let Some(FollowEvent::Unfollow) = item.to_follow_event(event) { - workspace.unfollow(leader_id, window, cx); - } + if let Some(leader_id) = leader_id + && let Some(FollowEvent::Unfollow) = item.to_follow_event(event) + { + workspace.unfollow(leader_id, window, cx); } if item.item_focus_handle(cx).contains_focused(window, cx) { @@ -863,10 +863,10 @@ impl<T: Item> ItemHandle for Entity<T> { } } - if let Some(item) = item.to_serializable_item_handle(cx) { - if item.should_serialize(event, cx) { - workspace.enqueue_item_serialization(item).ok(); - } + if let Some(item) = item.to_serializable_item_handle(cx) + && item.should_serialize(event, cx) + { + workspace.enqueue_item_serialization(item).ok(); } T::to_item_events(event, |event| match event { @@ -948,11 +948,11 @@ impl<T: Item> ItemHandle for Entity<T> { &self.read(cx).focus_handle(cx), window, move |workspace, window, cx| { - if let Some(item) = weak_item.upgrade() { - if item.workspace_settings(cx).autosave == AutosaveSetting::OnFocusChange { - Pane::autosave_item(&item, workspace.project.clone(), window, cx) - .detach_and_log_err(cx); - } + if let Some(item) = weak_item.upgrade() + && item.workspace_settings(cx).autosave == AutosaveSetting::OnFocusChange + { + Pane::autosave_item(&item, workspace.project.clone(), window, cx) + .detach_and_log_err(cx); } }, ) diff --git a/crates/workspace/src/modal_layer.rs b/crates/workspace/src/modal_layer.rs index 7e92c7b8e94bfc584f900a53329dd8e2eb832eab..bcd7db3a82aec46405927e118af86cf4a0d4912b 100644 --- a/crates/workspace/src/modal_layer.rs +++ b/crates/workspace/src/modal_layer.rs @@ -141,10 +141,10 @@ impl ModalLayer { } if let Some(active_modal) = self.active_modal.take() { - if let Some(previous_focus) = active_modal.previous_focus_handle { - if active_modal.focus_handle.contains_focused(window, cx) { - previous_focus.focus(window); - } + if let Some(previous_focus) = active_modal.previous_focus_handle + && active_modal.focus_handle.contains_focused(window, cx) + { + previous_focus.focus(window); } cx.notify(); } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 0a40dbc12c2e94856c74e66b18b28759c5f2da8b..a1affc5362a2c6f2ed8603bd7e956845b712038c 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -580,19 +580,18 @@ impl Pane { // or focus the active item itself if let Some(weak_last_focus_handle) = self.last_focus_handle_by_item.get(&active_item.item_id()) + && let Some(focus_handle) = weak_last_focus_handle.upgrade() { - if let Some(focus_handle) = weak_last_focus_handle.upgrade() { - focus_handle.focus(window); - return; - } + focus_handle.focus(window); + return; } active_item.item_focus_handle(cx).focus(window); - } else if let Some(focused) = window.focused(cx) { - if !self.context_menu_focused(window, cx) { - self.last_focus_handle_by_item - .insert(active_item.item_id(), focused.downgrade()); - } + } else if let Some(focused) = window.focused(cx) + && !self.context_menu_focused(window, cx) + { + self.last_focus_handle_by_item + .insert(active_item.item_id(), focused.downgrade()); } } } @@ -858,10 +857,11 @@ impl Pane { } pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &App) { - if let Some(preview_item) = self.preview_item() { - if preview_item.item_id() == item_id && !preview_item.preserve_preview(cx) { - self.set_preview_item_id(None, cx); - } + if let Some(preview_item) = self.preview_item() + && preview_item.item_id() == item_id + && !preview_item.preserve_preview(cx) + { + self.set_preview_item_id(None, cx); } } @@ -900,12 +900,12 @@ impl Pane { if let Some((index, existing_item)) = existing_item { // If the item is already open, and the item is a preview item // and we are not allowing items to open as preview, mark the item as persistent. - if let Some(preview_item_id) = self.preview_item_id { - if let Some(tab) = self.items.get(index) { - if tab.item_id() == preview_item_id && !allow_preview { - self.set_preview_item_id(None, cx); - } - } + if let Some(preview_item_id) = self.preview_item_id + && let Some(tab) = self.items.get(index) + && tab.item_id() == preview_item_id + && !allow_preview + { + self.set_preview_item_id(None, cx); } if activate { self.activate_item(index, focus_item, focus_item, window, cx); @@ -977,21 +977,21 @@ impl Pane { self.close_items_on_item_open(window, cx); } - if item.is_singleton(cx) { - if let Some(&entry_id) = item.project_entry_ids(cx).first() { - let Some(project) = self.project.upgrade() else { - return; - }; + if item.is_singleton(cx) + && let Some(&entry_id) = item.project_entry_ids(cx).first() + { + let Some(project) = self.project.upgrade() else { + return; + }; - let project = project.read(cx); - if let Some(project_path) = project.path_for_entry(entry_id, cx) { - let abs_path = project.absolute_path(&project_path, cx); - self.nav_history - .0 - .lock() - .paths_by_item - .insert(item.item_id(), (project_path, abs_path)); - } + let project = project.read(cx); + if let Some(project_path) = project.path_for_entry(entry_id, cx) { + let abs_path = project.absolute_path(&project_path, cx); + self.nav_history + .0 + .lock() + .paths_by_item + .insert(item.item_id(), (project_path, abs_path)); } } // If no destination index is specified, add or move the item after the @@ -1192,12 +1192,11 @@ impl Pane { use NavigationMode::{GoingBack, GoingForward}; if index < self.items.len() { let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); - if prev_active_item_ix != self.active_item_index - || matches!(self.nav_history.mode(), GoingBack | GoingForward) + if (prev_active_item_ix != self.active_item_index + || matches!(self.nav_history.mode(), GoingBack | GoingForward)) + && let Some(prev_item) = self.items.get(prev_active_item_ix) { - if let Some(prev_item) = self.items.get(prev_active_item_ix) { - prev_item.deactivated(window, cx); - } + prev_item.deactivated(window, cx); } self.update_history(index); self.update_toolbar(window, cx); @@ -2462,10 +2461,11 @@ impl Pane { .on_mouse_down( MouseButton::Left, cx.listener(move |pane, event: &MouseDownEvent, _, cx| { - if let Some(id) = pane.preview_item_id { - if id == item_id && event.click_count > 1 { - pane.set_preview_item_id(None, cx); - } + if let Some(id) = pane.preview_item_id + && id == item_id + && event.click_count > 1 + { + pane.set_preview_item_id(None, cx); } }), ) @@ -3048,18 +3048,18 @@ impl Pane { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() { - if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) { - return; - } + if let Some(custom_drop_handle) = self.custom_drop_handle.clone() + && let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) + { + return; } let mut to_pane = cx.entity(); let split_direction = self.drag_split_direction; let item_id = dragged_tab.item.item_id(); - if let Some(preview_item_id) = self.preview_item_id { - if item_id == preview_item_id { - self.set_preview_item_id(None, cx); - } + if let Some(preview_item_id) = self.preview_item_id + && item_id == preview_item_id + { + self.set_preview_item_id(None, cx); } let is_clone = cfg!(target_os = "macos") && window.modifiers().alt @@ -3136,11 +3136,10 @@ impl Pane { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() { - if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx) - { - return; - } + if let Some(custom_drop_handle) = self.custom_drop_handle.clone() + && let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx) + { + return; } self.handle_project_entry_drop( &dragged_selection.active_selection.entry_id, @@ -3157,10 +3156,10 @@ impl Pane { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() { - if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) { - return; - } + if let Some(custom_drop_handle) = self.custom_drop_handle.clone() + && let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) + { + return; } let mut to_pane = cx.entity(); let split_direction = self.drag_split_direction; @@ -3233,10 +3232,10 @@ impl Pane { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() { - if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) { - return; - } + if let Some(custom_drop_handle) = self.custom_drop_handle.clone() + && let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) + { + return; } let mut to_pane = cx.entity(); let mut split_direction = self.drag_split_direction; @@ -3790,10 +3789,10 @@ impl NavHistory { borrowed_history.paths_by_item.get(&entry.item.id()) { f(entry, project_and_abs_path.clone()); - } else if let Some(item) = entry.item.upgrade() { - if let Some(path) = item.project_path(cx) { - f(entry, (path, None)); - } + } else if let Some(item) = entry.item.upgrade() + && let Some(path) = item.project_path(cx) + { + f(entry, (path, None)); } }) } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 5c87206e9e96cf3866b183684d981b02692d039f..bd2aafb7f4994cbd44ae5cb4cb737d07c1b205c8 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -619,15 +619,15 @@ impl PaneAxis { let mut found_axis_index: Option<usize> = None; if !found_pane { for (i, pa) in self.members.iter_mut().enumerate() { - if let Member::Axis(pa) = pa { - if let Some(done) = pa.resize(pane, axis, amount, bounds) { - if done { - return Some(true); // pane found and operations already done - } else if self.axis != axis { - return Some(false); // pane found but this is not the correct axis direction - } else { - found_axis_index = Some(i); // pane found and this is correct direction - } + if let Member::Axis(pa) = pa + && let Some(done) = pa.resize(pane, axis, amount, bounds) + { + if done { + return Some(true); // pane found and operations already done + } else if self.axis != axis { + return Some(false); // pane found but this is not the correct axis direction + } else { + found_axis_index = Some(i); // pane found and this is correct direction } } } @@ -743,13 +743,13 @@ impl PaneAxis { let bounding_boxes = self.bounding_boxes.lock(); for (idx, member) in self.members.iter().enumerate() { - if let Some(coordinates) = bounding_boxes[idx] { - if coordinates.contains(&coordinate) { - return match member { - Member::Pane(found) => Some(found), - Member::Axis(axis) => axis.pane_at_pixel_position(coordinate), - }; - } + if let Some(coordinates) = bounding_boxes[idx] + && coordinates.contains(&coordinate) + { + return match member { + Member::Pane(found) => Some(found), + Member::Axis(axis) => axis.pane_at_pixel_position(coordinate), + }; } } None @@ -1273,17 +1273,18 @@ mod element { window.paint_quad(gpui::fill(overlay_bounds, overlay_background)); } - if let Some(border) = overlay_border { - if self.active_pane_ix == Some(ix) && child.is_leaf_pane { - window.paint_quad(gpui::quad( - overlay_bounds, - 0., - gpui::transparent_black(), - border, - cx.theme().colors().border_selected, - BorderStyle::Solid, - )); - } + if let Some(border) = overlay_border + && self.active_pane_ix == Some(ix) + && child.is_leaf_pane + { + window.paint_quad(gpui::quad( + overlay_bounds, + 0., + gpui::transparent_black(), + border, + cx.theme().colors().border_selected, + BorderStyle::Solid, + )); } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index babf2ac1d56b1507133c1eb1cc81f16e6cd4d394..4a22107c425016c764842bb97112e39463d3360f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1345,18 +1345,18 @@ impl Workspace { .timer(Duration::from_millis(100)) .await; this.update_in(cx, |this, window, cx| { - if let Some(display) = window.display(cx) { - if let Ok(display_uuid) = display.uuid() { - let window_bounds = window.inner_window_bounds(); - if let Some(database_id) = workspace_id { - cx.background_executor() - .spawn(DB.set_window_open_status( - database_id, - SerializedWindowBounds(window_bounds), - display_uuid, - )) - .detach_and_log_err(cx); - } + if let Some(display) = window.display(cx) + && let Ok(display_uuid) = display.uuid() + { + let window_bounds = window.inner_window_bounds(); + if let Some(database_id) = workspace_id { + cx.background_executor() + .spawn(DB.set_window_open_status( + database_id, + SerializedWindowBounds(window_bounds), + display_uuid, + )) + .detach_and_log_err(cx); } } this.bounds_save_task_queued.take(); @@ -1729,13 +1729,12 @@ impl Workspace { let item_map: HashMap<EntityId, &Box<dyn ItemHandle>> = pane.items().map(|item| (item.item_id(), item)).collect(); for entry in pane.activation_history() { - if entry.timestamp > recent_timestamp { - if let Some(&item) = item_map.get(&entry.entity_id) { - if let Some(typed_item) = item.act_as::<T>(cx) { - recent_timestamp = entry.timestamp; - recent_item = Some(typed_item); - } - } + if entry.timestamp > recent_timestamp + && let Some(&item) = item_map.get(&entry.entity_id) + && let Some(typed_item) = item.act_as::<T>(cx) + { + recent_timestamp = entry.timestamp; + recent_item = Some(typed_item); } } } @@ -1774,19 +1773,19 @@ impl Workspace { } }); - if let Some(item) = pane.active_item() { - if let Some(project_path) = item.project_path(cx) { - let fs_path = self.project.read(cx).absolute_path(&project_path, cx); - - if let Some(fs_path) = &fs_path { - abs_paths_opened - .entry(fs_path.clone()) - .or_default() - .insert(project_path.clone()); - } + if let Some(item) = pane.active_item() + && let Some(project_path) = item.project_path(cx) + { + let fs_path = self.project.read(cx).absolute_path(&project_path, cx); - history.insert(project_path, (fs_path, std::usize::MAX)); + if let Some(fs_path) = &fs_path { + abs_paths_opened + .entry(fs_path.clone()) + .or_default() + .insert(project_path.clone()); } + + history.insert(project_path, (fs_path, std::usize::MAX)); } } @@ -2250,29 +2249,28 @@ impl Workspace { .count() })?; - if let Some(active_call) = active_call { - if close_intent != CloseIntent::Quit - && workspace_count == 1 - && active_call.read_with(cx, |call, _| call.room().is_some())? - { - let answer = cx.update(|window, cx| { - window.prompt( - PromptLevel::Warning, - "Do you want to leave the current call?", - None, - &["Close window and hang up", "Cancel"], - cx, - ) - })?; + if let Some(active_call) = active_call + && close_intent != CloseIntent::Quit + && workspace_count == 1 + && active_call.read_with(cx, |call, _| call.room().is_some())? + { + let answer = cx.update(|window, cx| { + window.prompt( + PromptLevel::Warning, + "Do you want to leave the current call?", + None, + &["Close window and hang up", "Cancel"], + cx, + ) + })?; - if answer.await.log_err() == Some(1) { - return anyhow::Ok(false); - } else { - active_call - .update(cx, |call, cx| call.hang_up(cx))? - .await - .log_err(); - } + if answer.await.log_err() == Some(1) { + return anyhow::Ok(false); + } else { + active_call + .update(cx, |call, cx| call.hang_up(cx))? + .await + .log_err(); } } @@ -2448,10 +2446,10 @@ impl Workspace { for (pane, item) in dirty_items { let (singleton, project_entry_ids) = cx.update(|_, cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?; - if singleton || !project_entry_ids.is_empty() { - if !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await? { - return Ok(false); - } + if (singleton || !project_entry_ids.is_empty()) + && !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await? + { + return Ok(false); } } Ok(true) @@ -3080,14 +3078,12 @@ impl Workspace { let mut focus_center = false; for dock in self.all_docks() { dock.update(cx, |dock, cx| { - if Some(dock.position()) != dock_to_reveal { - if let Some(panel) = dock.active_panel() { - if panel.is_zoomed(window, cx) { - focus_center |= - panel.panel_focus_handle(cx).contains_focused(window, cx); - dock.set_open(false, window, cx); - } - } + if Some(dock.position()) != dock_to_reveal + && let Some(panel) = dock.active_panel() + && panel.is_zoomed(window, cx) + { + focus_center |= panel.panel_focus_handle(cx).contains_focused(window, cx); + dock.set_open(false, window, cx); } }); } @@ -3328,10 +3324,10 @@ impl Workspace { .downgrade() }); - if let Member::Pane(center_pane) = &self.center.root { - if center_pane.read(cx).items_len() == 0 { - return self.open_path(path, Some(pane), true, window, cx); - } + if let Member::Pane(center_pane) = &self.center.root + && center_pane.read(cx).items_len() == 0 + { + return self.open_path(path, Some(pane), true, window, cx); } let project_path = path.into(); @@ -3393,10 +3389,10 @@ impl Workspace { if let Some(entry_id) = entry_id { item = pane.read(cx).item_for_entry(entry_id, cx); } - if item.is_none() { - if let Some(project_path) = project_path { - item = pane.read(cx).item_for_path(project_path, cx); - } + if item.is_none() + && let Some(project_path) = project_path + { + item = pane.read(cx).item_for_path(project_path, cx); } item.and_then(|item| item.downcast::<T>()) @@ -3440,12 +3436,11 @@ impl Workspace { let item_id = item.item_id(); let mut destination_index = None; pane.update(cx, |pane, cx| { - if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { - if let Some(preview_item_id) = pane.preview_item_id() { - if preview_item_id != item_id { - destination_index = pane.close_current_preview_item(window, cx); - } - } + if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation + && let Some(preview_item_id) = pane.preview_item_id() + && preview_item_id != item_id + { + destination_index = pane.close_current_preview_item(window, cx); } pane.set_preview_item_id(Some(item.item_id()), cx) }); @@ -3912,10 +3907,10 @@ impl Workspace { pane::Event::RemovedItem { item } => { cx.emit(Event::ActiveItemChanged); self.update_window_edited(window, cx); - if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id()) { - if entry.get().entity_id() == pane.entity_id() { - entry.remove(); - } + if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id()) + && entry.get().entity_id() == pane.entity_id() + { + entry.remove(); } } pane::Event::Focus => { @@ -4105,14 +4100,13 @@ impl Workspace { pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> { for dock in self.all_docks() { - if dock.focus_handle(cx).contains_focused(window, cx) { - if let Some(pane) = dock + if dock.focus_handle(cx).contains_focused(window, cx) + && let Some(pane) = dock .read(cx) .active_panel() .and_then(|panel| panel.pane(cx)) - { - return pane; - } + { + return pane; } } self.active_pane().clone() @@ -4393,10 +4387,10 @@ impl Workspace { title.push_str(" ↗"); } - if let Some(last_title) = self.last_window_title.as_ref() { - if &title == last_title { - return; - } + if let Some(last_title) = self.last_window_title.as_ref() + && &title == last_title + { + return; } window.set_window_title(&title); self.last_window_title = Some(title); @@ -4575,10 +4569,8 @@ impl Workspace { } })??; - if should_add_view { - if let Some(view) = update_active_view.view { - Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await? - } + if should_add_view && let Some(view) = update_active_view.view { + Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await? } } proto::update_followers::Variant::UpdateView(update_view) => { @@ -4774,40 +4766,40 @@ impl Workspace { if window.is_window_active() { let (active_item, panel_id) = self.active_item_for_followers(window, cx); - if let Some(item) = active_item { - if item.item_focus_handle(cx).contains_focused(window, cx) { - let leader_id = self - .pane_for(&*item) - .and_then(|pane| self.leader_for_pane(&pane)); - let leader_peer_id = match leader_id { - Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id), - Some(CollaboratorId::Agent) | None => None, - }; + if let Some(item) = active_item + && item.item_focus_handle(cx).contains_focused(window, cx) + { + let leader_id = self + .pane_for(&*item) + .and_then(|pane| self.leader_for_pane(&pane)); + let leader_peer_id = match leader_id { + Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id), + Some(CollaboratorId::Agent) | None => None, + }; - if let Some(item) = item.to_followable_item_handle(cx) { - let id = item - .remote_id(&self.app_state.client, window, cx) - .map(|id| id.to_proto()); - - if let Some(id) = id { - if let Some(variant) = item.to_state_proto(window, cx) { - let view = Some(proto::View { - id: id.clone(), - leader_id: leader_peer_id, - variant: Some(variant), - panel_id: panel_id.map(|id| id as i32), - }); - - is_project_item = item.is_project_item(window, cx); - update = proto::UpdateActiveView { - view, - // TODO: Remove after version 0.145.x stabilizes. - id, - leader_id: leader_peer_id, - }; - } + if let Some(item) = item.to_followable_item_handle(cx) { + let id = item + .remote_id(&self.app_state.client, window, cx) + .map(|id| id.to_proto()); + + if let Some(id) = id + && let Some(variant) = item.to_state_proto(window, cx) + { + let view = Some(proto::View { + id: id.clone(), + leader_id: leader_peer_id, + variant: Some(variant), + panel_id: panel_id.map(|id| id as i32), + }); + + is_project_item = item.is_project_item(window, cx); + update = proto::UpdateActiveView { + view, + // TODO: Remove after version 0.145.x stabilizes. + id, + leader_id: leader_peer_id, }; - } + }; } } } @@ -4832,16 +4824,14 @@ impl Workspace { let mut active_item = None; let mut panel_id = None; for dock in self.all_docks() { - if dock.focus_handle(cx).contains_focused(window, cx) { - if let Some(panel) = dock.read(cx).active_panel() { - if let Some(pane) = panel.pane(cx) { - if let Some(item) = pane.read(cx).active_item() { - active_item = Some(item); - panel_id = panel.remote_id(); - break; - } - } - } + if dock.focus_handle(cx).contains_focused(window, cx) + && let Some(panel) = dock.read(cx).active_panel() + && let Some(pane) = panel.pane(cx) + && let Some(item) = pane.read(cx).active_item() + { + active_item = Some(item); + panel_id = panel.remote_id(); + break; } } @@ -4969,10 +4959,10 @@ impl Workspace { let state = self.follower_states.get(&peer_id.into())?; let mut item_to_activate = None; if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) { - if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) { - if leader_in_this_project || !item.view.is_project_item(window, cx) { - item_to_activate = Some((item.location, item.view.boxed_clone())); - } + if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) + && (leader_in_this_project || !item.view.is_project_item(window, cx)) + { + item_to_activate = Some((item.location, item.view.boxed_clone())); } } else if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx) @@ -6079,10 +6069,10 @@ fn open_items( project_paths_to_open .iter_mut() .for_each(|(_, project_path)| { - if let Some(project_path_to_open) = project_path { - if restored_project_paths.contains(project_path_to_open) { - *project_path = None; - } + if let Some(project_path_to_open) = project_path + && restored_project_paths.contains(project_path_to_open) + { + *project_path = None; } }); } else { @@ -6109,24 +6099,24 @@ fn open_items( // We only want to open file paths here. If one of the items // here is a directory, it was already opened further above // with a `find_or_create_worktree`. - if let Ok(task) = abs_path_task { - if task.await.map_or(true, |p| p.is_file()) { - return Some(( - ix, - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_path( - file_project_path, - None, - true, - window, - cx, - ) - }) - .log_err()? - .await, - )); - } + if let Ok(task) = abs_path_task + && task.await.map_or(true, |p| p.is_file()) + { + return Some(( + ix, + workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_path( + file_project_path, + None, + true, + window, + cx, + ) + }) + .log_err()? + .await, + )); } None }) @@ -6728,10 +6718,10 @@ impl WorkspaceStore { .update(cx, |workspace, window, cx| { let handler_response = workspace.handle_follow(follower.project_id, window, cx); - if let Some(active_view) = handler_response.active_view.clone() { - if workspace.project.read(cx).remote_id() == follower.project_id { - response.active_view = Some(active_view) - } + if let Some(active_view) = handler_response.active_view.clone() + && workspace.project.read(cx).remote_id() == follower.project_id + { + response.active_view = Some(active_view) } }) .is_ok() @@ -6965,34 +6955,35 @@ async fn join_channel_internal( } // If you are the first to join a channel, see if you should share your project. - if room.remote_participants().is_empty() && !room.local_participant_is_guest() { - if let Some(workspace) = requesting_window { - let project = workspace.update(cx, |workspace, _, cx| { - let project = workspace.project.read(cx); + if room.remote_participants().is_empty() + && !room.local_participant_is_guest() + && let Some(workspace) = requesting_window + { + let project = workspace.update(cx, |workspace, _, cx| { + let project = workspace.project.read(cx); - if !CallSettings::get_global(cx).share_on_join { - return None; - } + if !CallSettings::get_global(cx).share_on_join { + return None; + } - if (project.is_local() || project.is_via_ssh()) - && project.visible_worktrees(cx).any(|tree| { - tree.read(cx) - .root_entry() - .map_or(false, |entry| entry.is_dir()) - }) - { - Some(workspace.project.clone()) - } else { - None - } - }); - if let Ok(Some(project)) = project { - return Some(cx.spawn(async move |room, cx| { - room.update(cx, |room, cx| room.share_project(project, cx))? - .await?; - Ok(()) - })); + if (project.is_local() || project.is_via_ssh()) + && project.visible_worktrees(cx).any(|tree| { + tree.read(cx) + .root_entry() + .map_or(false, |entry| entry.is_dir()) + }) + { + Some(workspace.project.clone()) + } else { + None } + }); + if let Ok(Some(project)) = project { + return Some(cx.spawn(async move |room, cx| { + room.update(cx, |room, cx| room.share_project(project, cx))? + .await?; + Ok(()) + })); } } @@ -7189,35 +7180,35 @@ pub fn open_paths( } })?; - if open_options.open_new_workspace.is_none() && existing.is_none() { - if all_metadatas.iter().all(|file| !file.is_dir) { - cx.update(|cx| { - if let Some(window) = cx - .active_window() - .and_then(|window| window.downcast::<Workspace>()) - { - if let Ok(workspace) = window.read(cx) { - let project = workspace.project().read(cx); - if project.is_local() && !project.is_via_collab() { - existing = Some(window); - open_visible = OpenVisible::None; - return; - } - } + if open_options.open_new_workspace.is_none() + && existing.is_none() + && all_metadatas.iter().all(|file| !file.is_dir) + { + cx.update(|cx| { + if let Some(window) = cx + .active_window() + .and_then(|window| window.downcast::<Workspace>()) + && let Ok(workspace) = window.read(cx) + { + let project = workspace.project().read(cx); + if project.is_local() && !project.is_via_collab() { + existing = Some(window); + open_visible = OpenVisible::None; + return; } - for window in local_workspace_windows(cx) { - if let Ok(workspace) = window.read(cx) { - let project = workspace.project().read(cx); - if project.is_via_collab() { - continue; - } - existing = Some(window); - open_visible = OpenVisible::None; - break; + } + for window in local_workspace_windows(cx) { + if let Ok(workspace) = window.read(cx) { + let project = workspace.project().read(cx); + if project.is_via_collab() { + continue; } + existing = Some(window); + open_visible = OpenVisible::None; + break; } - })?; - } + } + })?; } } @@ -7651,10 +7642,9 @@ pub fn reload(cx: &mut App) { for window in workspace_windows { if let Ok(should_close) = window.update(cx, |workspace, window, cx| { workspace.prepare_to_close(CloseIntent::Quit, window, cx) - }) { - if !should_close.await? { - return Ok(()); - } + }) && !should_close.await? + { + return Ok(()); } } cx.update(|cx| cx.restart()) diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 4a8c9d466670873c87d05ea41f410ecaf41cc60b..56353475148b5606d306201075fdfc6d59ffcf6f 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -282,19 +282,17 @@ impl Settings for WorkspaceSettings { if vscode .read_bool("accessibility.dimUnfocused.enabled") .unwrap_or_default() - { - if let Some(opacity) = vscode + && let Some(opacity) = vscode .read_value("accessibility.dimUnfocused.opacity") .and_then(|v| v.as_f64()) - { - if let Some(settings) = current.active_pane_modifiers.as_mut() { - settings.inactive_opacity = Some(opacity as f32) - } else { - current.active_pane_modifiers = Some(ActivePanelModifiers { - inactive_opacity: Some(opacity as f32), - ..Default::default() - }) - } + { + if let Some(settings) = current.active_pane_modifiers.as_mut() { + settings.inactive_opacity = Some(opacity as f32) + } else { + current.active_pane_modifiers = Some(ActivePanelModifiers { + inactive_opacity: Some(opacity as f32), + ..Default::default() + }) } } @@ -345,13 +343,11 @@ impl Settings for WorkspaceSettings { .read_value("workbench.editor.limit.value") .and_then(|v| v.as_u64()) .and_then(|n| NonZeroUsize::new(n as usize)) - { - if vscode + && vscode .read_bool("workbench.editor.limit.enabled") .unwrap_or_default() - { - current.max_tabs = Some(n) - } + { + current.max_tabs = Some(n) } // some combination of "window.restoreWindows" and "workbench.startupEditor" might diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index f110726afddc6a3445ef5e5c6973fd336b23119c..9e1832721fcdf1577537de5e90dccace661db5ee 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1522,10 +1522,10 @@ impl LocalWorktree { // reasonable limit { const FILE_SIZE_MAX: u64 = 6 * 1024 * 1024 * 1024; // 6GB - if let Ok(Some(metadata)) = fs.metadata(&abs_path).await { - if metadata.len >= FILE_SIZE_MAX { - anyhow::bail!("File is too large to load"); - } + if let Ok(Some(metadata)) = fs.metadata(&abs_path).await + && metadata.len >= FILE_SIZE_MAX + { + anyhow::bail!("File is too large to load"); } } let text = fs.load(&abs_path).await?; @@ -2503,10 +2503,10 @@ impl Snapshot { if let Some(PathEntry { path, .. }) = self.entries_by_id.get(&entry.id, &()) { entries_by_path_edits.push(Edit::Remove(PathKey(path.clone()))); } - if let Some(old_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), &()) { - if old_entry.id != entry.id { - entries_by_id_edits.push(Edit::Remove(old_entry.id)); - } + if let Some(old_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), &()) + && old_entry.id != entry.id + { + entries_by_id_edits.push(Edit::Remove(old_entry.id)); } entries_by_id_edits.push(Edit::Insert(PathEntry { id: entry.id, @@ -2747,20 +2747,19 @@ impl LocalSnapshot { } } - if entry.kind == EntryKind::PendingDir { - if let Some(existing_entry) = + if entry.kind == EntryKind::PendingDir + && let Some(existing_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), &()) - { - entry.kind = existing_entry.kind; - } + { + entry.kind = existing_entry.kind; } let scan_id = self.scan_id; let removed = self.entries_by_path.insert_or_replace(entry.clone(), &()); - if let Some(removed) = removed { - if removed.id != entry.id { - self.entries_by_id.remove(&removed.id, &()); - } + if let Some(removed) = removed + && removed.id != entry.id + { + self.entries_by_id.remove(&removed.id, &()); } self.entries_by_id.insert_or_replace( PathEntry { @@ -4138,13 +4137,13 @@ impl BackgroundScanner { let root_path = state.snapshot.abs_path.clone(); for path in paths { for ancestor in path.ancestors() { - if let Some(entry) = state.snapshot.entry_for_path(ancestor) { - if entry.kind == EntryKind::UnloadedDir { - let abs_path = root_path.as_path().join(ancestor); - state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx); - state.paths_to_scan.insert(path.clone()); - break; - } + if let Some(entry) = state.snapshot.entry_for_path(ancestor) + && entry.kind == EntryKind::UnloadedDir + { + let abs_path = root_path.as_path().join(ancestor); + state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx); + state.paths_to_scan.insert(path.clone()); + break; } } } @@ -4214,11 +4213,10 @@ impl BackgroundScanner { // Recursively load directories from the file system. job = scan_jobs_rx.recv().fuse() => { let Ok(job) = job else { break }; - if let Err(err) = self.scan_dir(&job).await { - if job.path.as_ref() != Path::new("") { + if let Err(err) = self.scan_dir(&job).await + && job.path.as_ref() != Path::new("") { log::error!("error scanning directory {:?}: {}", job.abs_path, err); } - } } } } @@ -4554,18 +4552,18 @@ impl BackgroundScanner { state.insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()); - if path.as_ref() == Path::new("") { - if let Some((ignores, repo)) = new_ancestor_repo.take() { - log::trace!("updating ancestor git repository"); - state.snapshot.ignores_by_parent_abs_path.extend(ignores); - if let Some((ancestor_dot_git, work_directory)) = repo { - state.insert_git_repository_for_path( - work_directory, - ancestor_dot_git.as_path().into(), - self.fs.as_ref(), - self.watcher.as_ref(), - ); - } + if path.as_ref() == Path::new("") + && let Some((ignores, repo)) = new_ancestor_repo.take() + { + log::trace!("updating ancestor git repository"); + state.snapshot.ignores_by_parent_abs_path.extend(ignores); + if let Some((ancestor_dot_git, work_directory)) = repo { + state.insert_git_repository_for_path( + work_directory, + ancestor_dot_git.as_path().into(), + self.fs.as_ref(), + self.watcher.as_ref(), + ); } } } @@ -4590,13 +4588,12 @@ impl BackgroundScanner { if !path .components() .any(|component| component.as_os_str() == *DOT_GIT) + && let Some(local_repo) = snapshot.local_repo_for_work_directory_path(path) { - if let Some(local_repo) = snapshot.local_repo_for_work_directory_path(path) { - let id = local_repo.work_directory_id; - log::debug!("remove repo path: {:?}", path); - snapshot.git_repositories.remove(&id); - return Some(()); - } + let id = local_repo.work_directory_id; + log::debug!("remove repo path: {:?}", path); + snapshot.git_repositories.remove(&id); + return Some(()); } Some(()) @@ -4738,10 +4735,10 @@ impl BackgroundScanner { let state = &mut self.state.lock(); for edit in &entries_by_path_edits { - if let Edit::Insert(entry) = edit { - if let Err(ix) = state.changed_paths.binary_search(&entry.path) { - state.changed_paths.insert(ix, entry.path.clone()); - } + if let Edit::Insert(entry) = edit + && let Err(ix) = state.changed_paths.binary_search(&entry.path) + { + state.changed_paths.insert(ix, entry.path.clone()); } } @@ -5287,13 +5284,12 @@ impl<'a> Traversal<'a> { while let Some(entry) = self.cursor.item() { self.cursor .seek_forward(&TraversalTarget::successor(&entry.path), Bias::Left); - if let Some(entry) = self.cursor.item() { - if (self.include_files || !entry.is_file()) - && (self.include_dirs || !entry.is_dir()) - && (self.include_ignored || !entry.is_ignored || entry.is_always_included) - { - return true; - } + if let Some(entry) = self.cursor.item() + && (self.include_files || !entry.is_file()) + && (self.include_dirs || !entry.is_dir()) + && (self.include_ignored || !entry.is_ignored || entry.is_always_included) + { + return true; } } false @@ -5437,11 +5433,11 @@ impl<'a> Iterator for ChildEntriesIter<'a> { type Item = &'a Entry; fn next(&mut self) -> Option<Self::Item> { - if let Some(item) = self.traversal.entry() { - if item.path.starts_with(self.parent_path) { - self.traversal.advance_to_sibling(); - return Some(item); - } + if let Some(item) = self.traversal.entry() + && item.path.starts_with(self.parent_path) + { + self.traversal.advance_to_sibling(); + return Some(item); } None } @@ -5564,12 +5560,10 @@ fn discover_git_paths(dot_git_abs_path: &Arc<Path>, fs: &dyn Fs) -> (Arc<Path>, repository_dir_abs_path = Path::new(&path).into(); common_dir_abs_path = repository_dir_abs_path.clone(); if let Some(commondir_contents) = smol::block_on(fs.load(&path.join("commondir"))).ok() - { - if let Some(commondir_path) = + && let Some(commondir_path) = smol::block_on(fs.canonicalize(&path.join(commondir_contents.trim()))).log_err() - { - common_dir_abs_path = commondir_path.as_path().into(); - } + { + common_dir_abs_path = commondir_path.as_path().into(); } } }; diff --git a/crates/zed/build.rs b/crates/zed/build.rs index eb18617adde491908e690495917fd55974635642..c6d943a4593f0341dfef7869582e58980ae4a5ea 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -23,22 +23,20 @@ fn main() { "cargo:rustc-env=TARGET={}", std::env::var("TARGET").unwrap() ); - if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() { - if output.status.success() { - let git_sha = String::from_utf8_lossy(&output.stdout); - let git_sha = git_sha.trim(); + if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() + && output.status.success() + { + let git_sha = String::from_utf8_lossy(&output.stdout); + let git_sha = git_sha.trim(); - println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}"); + println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}"); - if let Ok(build_profile) = std::env::var("PROFILE") { - if build_profile == "release" { - // This is currently the best way to make `cargo build ...`'s build script - // to print something to stdout without extra verbosity. - println!( - "cargo:warning=Info: using '{git_sha}' hash for ZED_COMMIT_SHA env var" - ); - } - } + if let Ok(build_profile) = std::env::var("PROFILE") + && build_profile == "release" + { + // This is currently the best way to make `cargo build ...`'s build script + // to print something to stdout without extra verbosity. + println!("cargo:warning=Info: using '{git_sha}' hash for ZED_COMMIT_SHA env var"); } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index a66b30c44a6a87a4294396dd6ba9342bd90d0622..df30d4dd7beba769d5ce9f44446fdf6f82efd881 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1026,18 +1026,18 @@ async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: &mut AsyncApp // Try to find an active workspace to show the toast let toast_shown = cx .update(|cx| { - if let Some(window) = cx.active_window() { - if let Some(workspace) = window.downcast::<Workspace>() { - workspace - .update(cx, |workspace, _, cx| { - workspace.show_toast( - Toast::new(NotificationId::unique::<()>(), message), - cx, - ) - }) - .ok(); - return true; - } + if let Some(window) = cx.active_window() + && let Some(workspace) = window.downcast::<Workspace>() + { + workspace + .update(cx, |workspace, _, cx| { + workspace.show_toast( + Toast::new(NotificationId::unique::<()>(), message), + cx, + ) + }) + .ok(); + return true; } false }) @@ -1117,10 +1117,8 @@ pub(crate) async fn restorable_workspace_locations( // Since last_session_window_order returns the windows ordered front-to-back // we need to open the window that was frontmost last. - if ordered { - if let Some(locations) = locations.as_mut() { - locations.reverse(); - } + if ordered && let Some(locations) = locations.as_mut() { + locations.reverse(); } locations @@ -1290,21 +1288,21 @@ fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &App) { if let Some(theme_selection) = theme_settings.theme_selection.as_ref() { let theme_name = theme_selection.theme(appearance); - if matches!(theme_registry.get(theme_name), Err(ThemeNotFoundError(_))) { - if let Some(theme_path) = extension_store.read(cx).path_to_extension_theme(theme_name) { - cx.spawn({ - let theme_registry = theme_registry.clone(); - let fs = fs.clone(); - async move |cx| { - theme_registry.load_user_theme(&theme_path, fs).await?; + if matches!(theme_registry.get(theme_name), Err(ThemeNotFoundError(_))) + && let Some(theme_path) = extension_store.read(cx).path_to_extension_theme(theme_name) + { + cx.spawn({ + let theme_registry = theme_registry.clone(); + let fs = fs.clone(); + async move |cx| { + theme_registry.load_user_theme(&theme_path, fs).await?; - cx.update(|cx| { - ThemeSettings::reload_current_theme(cx); - }) - } - }) - .detach_and_log_err(cx); - } + cx.update(|cx| { + ThemeSettings::reload_current_theme(cx); + }) + } + }) + .detach_and_log_err(cx); } } @@ -1313,26 +1311,24 @@ fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &App) { if matches!( theme_registry.get_icon_theme(icon_theme_name), Err(IconThemeNotFoundError(_)) - ) { - if let Some((icon_theme_path, icons_root_path)) = extension_store - .read(cx) - .path_to_extension_icon_theme(icon_theme_name) - { - cx.spawn({ - let theme_registry = theme_registry.clone(); - let fs = fs.clone(); - async move |cx| { - theme_registry - .load_icon_theme(&icon_theme_path, &icons_root_path, fs) - .await?; + ) && let Some((icon_theme_path, icons_root_path)) = extension_store + .read(cx) + .path_to_extension_icon_theme(icon_theme_name) + { + cx.spawn({ + let theme_registry = theme_registry.clone(); + let fs = fs.clone(); + async move |cx| { + theme_registry + .load_icon_theme(&icon_theme_path, &icons_root_path, fs) + .await?; - cx.update(|cx| { - ThemeSettings::reload_current_icon_theme(cx); - }) - } - }) - .detach_and_log_err(cx); - } + cx.update(|cx| { + ThemeSettings::reload_current_icon_theme(cx); + }) + } + }) + .detach_and_log_err(cx); } } } @@ -1381,18 +1377,15 @@ fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut App) { while let Some(paths) = events.next().await { for event in paths { - if fs.metadata(&event.path).await.ok().flatten().is_some() { - if let Some(theme_registry) = + if fs.metadata(&event.path).await.ok().flatten().is_some() + && let Some(theme_registry) = cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err() - { - if let Some(()) = theme_registry - .load_user_theme(&event.path, fs.clone()) - .await - .log_err() - { - cx.update(ThemeSettings::reload_current_theme).log_err(); - } - } + && let Some(()) = theme_registry + .load_user_theme(&event.path, fs.clone()) + .await + .log_err() + { + cx.update(ThemeSettings::reload_current_theme).log_err(); } } } diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index f2e65b4f53907858392920a8d54ceb0a08b29844..cbd31c2e26471292cf1ccbac45720082f672f7d9 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -146,19 +146,17 @@ pub fn init_panic_hook( } zlog::flush(); - if !is_pty { - 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 = fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&panic_file_path) - .log_err(); - if let Some(mut panic_file) = panic_file { - writeln!(&mut panic_file, "{panic_data_json}").log_err(); - panic_file.flush().log_err(); - } + if !is_pty && 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 = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&panic_file_path) + .log_err(); + if let Some(mut panic_file) = panic_file { + writeln!(&mut panic_file, "{panic_data_json}").log_err(); + panic_file.flush().log_err(); } } @@ -459,10 +457,10 @@ pub fn monitor_main_thread_hangs( continue; }; - if let Some(response) = http_client.send(request).await.log_err() { - if response.status() != 200 { - log::error!("Failed to send hang report: HTTP {:?}", response.status()); - } + if let Some(response) = http_client.send(request).await.log_err() + && response.status() != 200 + { + log::error!("Failed to send hang report: HTTP {:?}", response.status()); } } } @@ -563,8 +561,8 @@ pub async fn upload_previous_minidumps(http: Arc<HttpClientWithUrl>) -> anyhow:: } let mut json_path = child_path.clone(); json_path.set_extension("json"); - if let Ok(metadata) = serde_json::from_slice(&smol::fs::read(&json_path).await?) { - if upload_minidump( + if let Ok(metadata) = serde_json::from_slice(&smol::fs::read(&json_path).await?) + && upload_minidump( http.clone(), minidump_endpoint, smol::fs::read(&child_path) @@ -575,10 +573,9 @@ pub async fn upload_previous_minidumps(http: Arc<HttpClientWithUrl>) -> anyhow:: .await .log_err() .is_some() - { - fs::remove_file(child_path).ok(); - fs::remove_file(json_path).ok(); - } + { + fs::remove_file(child_path).ok(); + fs::remove_file(json_path).ok(); } } Ok(()) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 535cb12e1ad4c78913dbce6565605e0b340999fa..93a62afc6f4468212709e612d635a899d100f203 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1054,27 +1054,25 @@ fn quit(_: &Quit, cx: &mut App) { }) .log_err(); - if should_confirm { - if let Some(workspace) = workspace_windows.first() { - let answer = workspace - .update(cx, |_, window, cx| { - window.prompt( - PromptLevel::Info, - "Are you sure you want to quit?", - None, - &["Quit", "Cancel"], - cx, - ) - }) - .log_err(); + if should_confirm && let Some(workspace) = workspace_windows.first() { + let answer = workspace + .update(cx, |_, window, cx| { + window.prompt( + PromptLevel::Info, + "Are you sure you want to quit?", + None, + &["Quit", "Cancel"], + cx, + ) + }) + .log_err(); - if let Some(answer) = answer { - WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release); - let answer = answer.await.ok(); - WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release); - if answer != Some(0) { - return Ok(()); - } + if let Some(answer) = answer { + WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release); + let answer = answer.await.ok(); + WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release); + if answer != Some(0) { + return Ok(()); } } } @@ -1086,10 +1084,9 @@ fn quit(_: &Quit, cx: &mut App) { workspace.prepare_to_close(CloseIntent::Quit, window, cx) }) .log_err() + && !should_close.await? { - if !should_close.await? { - return Ok(()); - } + return Ok(()); } } cx.update(|cx| cx.quit())?; @@ -1633,15 +1630,15 @@ fn open_local_file( }; if !file_exists { - if let Some(dir_path) = settings_relative_path.parent() { - if worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())? { - project - .update(cx, |project, cx| { - project.create_entry((tree_id, dir_path), true, cx) - })? - .await - .context("worktree was removed")?; - } + if let Some(dir_path) = settings_relative_path.parent() + && worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())? + { + project + .update(cx, |project, cx| { + project.create_entry((tree_id, dir_path), true, cx) + })? + .await + .context("worktree was removed")?; } if worktree.read_with(cx, |tree, _| { @@ -1667,12 +1664,12 @@ fn open_local_file( editor .downgrade() .update(cx, |editor, cx| { - if let Some(buffer) = editor.buffer().read(cx).as_singleton() { - if buffer.read(cx).is_empty() { - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, initial_contents)], None, cx) - }); - } + if let Some(buffer) = editor.buffer().read(cx).as_singleton() + && buffer.read(cx).is_empty() + { + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, initial_contents)], None, cx) + }); } }) .ok(); diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index 915c40030a26240cc24d3dd79e7c716010abb469..d855fc3af799016bc634b4c013eb7302c6300ab9 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -318,26 +318,26 @@ impl ComponentPreview { let lowercase_scope = scope_name.to_lowercase(); let lowercase_desc = description.to_lowercase(); - if lowercase_scopeless.contains(&lowercase_filter) { - if let Some(index) = lowercase_scopeless.find(&lowercase_filter) { - let end = index + lowercase_filter.len(); - - if end <= scopeless_name.len() { - let mut positions = Vec::new(); - for i in index..end { - if scopeless_name.is_char_boundary(i) { - positions.push(i); - } - } + if lowercase_scopeless.contains(&lowercase_filter) + && let Some(index) = lowercase_scopeless.find(&lowercase_filter) + { + let end = index + lowercase_filter.len(); - if !positions.is_empty() { - scope_groups - .entry(component.scope()) - .or_insert_with(Vec::new) - .push((component.clone(), Some(positions))); - continue; + if end <= scopeless_name.len() { + let mut positions = Vec::new(); + for i in index..end { + if scopeless_name.is_char_boundary(i) { + positions.push(i); } } + + if !positions.is_empty() { + scope_groups + .entry(component.scope()) + .or_insert_with(Vec::new) + .push((component.clone(), Some(positions))); + continue; + } } } @@ -372,32 +372,32 @@ impl ComponentPreview { scopes.sort_by_key(|s| s.to_string()); for scope in scopes { - if let Some(components) = scope_groups.remove(&scope) { - if !components.is_empty() { - entries.push(PreviewEntry::Separator); - entries.push(PreviewEntry::SectionHeader(scope.to_string().into())); + if let Some(components) = scope_groups.remove(&scope) + && !components.is_empty() + { + entries.push(PreviewEntry::Separator); + entries.push(PreviewEntry::SectionHeader(scope.to_string().into())); - let mut sorted_components = components; - sorted_components.sort_by_key(|(component, _)| component.sort_name()); + let mut sorted_components = components; + sorted_components.sort_by_key(|(component, _)| component.sort_name()); - for (component, positions) in sorted_components { - entries.push(PreviewEntry::Component(component, positions)); - } + for (component, positions) in sorted_components { + entries.push(PreviewEntry::Component(component, positions)); } } } // Add uncategorized components last - if let Some(components) = scope_groups.get(&ComponentScope::None) { - if !components.is_empty() { - entries.push(PreviewEntry::Separator); - entries.push(PreviewEntry::SectionHeader("Uncategorized".into())); - let mut sorted_components = components.clone(); - sorted_components.sort_by_key(|(c, _)| c.sort_name()); - - for (component, positions) in sorted_components { - entries.push(PreviewEntry::Component(component, positions)); - } + if let Some(components) = scope_groups.get(&ComponentScope::None) + && !components.is_empty() + { + entries.push(PreviewEntry::Separator); + entries.push(PreviewEntry::SectionHeader("Uncategorized".into())); + let mut sorted_components = components.clone(); + sorted_components.sort_by_key(|(c, _)| c.sort_name()); + + for (component, positions) in sorted_components { + entries.push(PreviewEntry::Component(component, positions)); } } @@ -415,19 +415,20 @@ impl ComponentPreview { let filtered_components = self.filtered_components(); - if !self.filter_text.is_empty() && !matches!(self.active_page, PreviewPage::AllComponents) { - if let PreviewPage::Component(ref component_id) = self.active_page { - let component_still_visible = filtered_components - .iter() - .any(|component| component.id() == *component_id); - - if !component_still_visible { - if !filtered_components.is_empty() { - let first_component = &filtered_components[0]; - self.set_active_page(PreviewPage::Component(first_component.id()), cx); - } else { - self.set_active_page(PreviewPage::AllComponents, cx); - } + if !self.filter_text.is_empty() + && !matches!(self.active_page, PreviewPage::AllComponents) + && let PreviewPage::Component(ref component_id) = self.active_page + { + let component_still_visible = filtered_components + .iter() + .any(|component| component.id() == *component_id); + + if !component_still_visible { + if !filtered_components.is_empty() { + let first_component = &filtered_components[0]; + self.set_active_page(PreviewPage::Component(first_component.id()), cx); + } else { + self.set_active_page(PreviewPage::AllComponents, cx); } } } diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index 587786fe8f248536796909911022bdf3b97de3eb..8d12a5bfad746492286ff4198818e8b84fc21111 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -204,12 +204,12 @@ fn assign_edit_prediction_provider( } EditPredictionProvider::Copilot => { if let Some(copilot) = Copilot::global(cx) { - if let Some(buffer) = singleton_buffer { - if buffer.read(cx).file().is_some() { - copilot.update(cx, |copilot, cx| { - copilot.register_buffer(&buffer, cx); - }); - } + if let Some(buffer) = singleton_buffer + && buffer.read(cx).file().is_some() + { + copilot.update(cx, |copilot, cx| { + copilot.register_buffer(&buffer, cx); + }); } let provider = cx.new(|_| CopilotCompletionProvider::new(copilot)); editor.set_edit_prediction_provider(Some(provider), window, cx); @@ -225,15 +225,15 @@ fn assign_edit_prediction_provider( if user_store.read(cx).current_user().is_some() { let mut worktree = None; - if let Some(buffer) = &singleton_buffer { - if let Some(file) = buffer.read(cx).file() { - let id = file.worktree_id(cx); - if let Some(inner_worktree) = editor - .project() - .and_then(|project| project.read(cx).worktree_for_id(id, cx)) - { - worktree = Some(inner_worktree); - } + if let Some(buffer) = &singleton_buffer + && let Some(file) = buffer.read(cx).file() + { + let id = file.worktree_id(cx); + if let Some(inner_worktree) = editor + .project() + .and_then(|project| project.read(cx).worktree_for_id(id, cx)) + { + worktree = Some(inner_worktree); } } @@ -245,12 +245,12 @@ fn assign_edit_prediction_provider( let zeta = zeta::Zeta::register(workspace, worktree, client.clone(), user_store, cx); - if let Some(buffer) = &singleton_buffer { - if buffer.read(cx).file().is_some() { - zeta.update(cx, |zeta, cx| { - zeta.register_buffer(buffer, cx); - }); - } + if let Some(buffer) = &singleton_buffer + && buffer.read(cx).file().is_some() + { + zeta.update(cx, |zeta, cx| { + zeta.register_buffer(buffer, cx); + }); } let data_collection = diff --git a/crates/zed/src/zed/mac_only_instance.rs b/crates/zed/src/zed/mac_only_instance.rs index 716c2224e319244b6870cf9d09ed13f14650b675..cb9641e9dfe55660e301faa46d47e1a4b8511466 100644 --- a/crates/zed/src/zed/mac_only_instance.rs +++ b/crates/zed/src/zed/mac_only_instance.rs @@ -37,20 +37,19 @@ fn address() -> SocketAddr { let mut user_port = port; let mut sys = System::new_all(); sys.refresh_all(); - if let Ok(current_pid) = sysinfo::get_current_pid() { - if let Some(uid) = sys + if let Ok(current_pid) = sysinfo::get_current_pid() + && let Some(uid) = sys .process(current_pid) .and_then(|process| process.user_id()) - { - let uid_u32 = get_uid_as_u32(uid); - // Ensure that the user ID is not too large to avoid overflow when - // calculating the port number. This seems unlikely but it doesn't - // hurt to be safe. - let max_port = 65535; - let max_uid: u32 = max_port - port as u32; - let wrapped_uid: u16 = (uid_u32 % max_uid) as u16; - user_port += wrapped_uid; - } + { + let uid_u32 = get_uid_as_u32(uid); + // Ensure that the user ID is not too large to avoid overflow when + // calculating the port number. This seems unlikely but it doesn't + // hurt to be safe. + let max_port = 65535; + let max_uid: u32 = max_port - port as u32; + let wrapped_uid: u16 = (uid_u32 % max_uid) as u16; + user_port += wrapped_uid; } SocketAddr::V4(SocketAddrV4::new(LOCALHOST, user_port)) diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index f282860e2cdc6ffc343724718f386054418338af..5baf76b64c6a4d60c2cc631c54fc0facddb02098 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -123,26 +123,24 @@ impl OpenRequest { fn parse_request_path(&mut self, request_path: &str) -> Result<()> { let mut parts = request_path.split('/'); - if parts.next() == Some("channel") { - if let Some(slug) = parts.next() { - if let Some(id_str) = slug.split('-').next_back() { - if let Ok(channel_id) = id_str.parse::<u64>() { - let Some(next) = parts.next() else { - self.join_channel = Some(channel_id); - return Ok(()); - }; - - if let Some(heading) = next.strip_prefix("notes#") { - self.open_channel_notes - .push((channel_id, Some(heading.to_string()))); - return Ok(()); - } - if next == "notes" { - self.open_channel_notes.push((channel_id, None)); - return Ok(()); - } - } - } + if parts.next() == Some("channel") + && let Some(slug) = parts.next() + && let Some(id_str) = slug.split('-').next_back() + && let Ok(channel_id) = id_str.parse::<u64>() + { + let Some(next) = parts.next() else { + self.join_channel = Some(channel_id); + return Ok(()); + }; + + if let Some(heading) = next.strip_prefix("notes#") { + self.open_channel_notes + .push((channel_id, Some(heading.to_string()))); + return Ok(()); + } + if next == "notes" { + self.open_channel_notes.push((channel_id, None)); + return Ok(()); } } anyhow::bail!("invalid zed url: {request_path}") @@ -181,10 +179,10 @@ pub fn listen_for_cli_connections(opener: OpenListener) -> Result<()> { let sock_path = paths::data_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL_NAME)); // remove the socket if the process listening on it has died - if let Err(e) = UnixDatagram::unbound()?.connect(&sock_path) { - if e.kind() == std::io::ErrorKind::ConnectionRefused { - std::fs::remove_file(&sock_path)?; - } + if let Err(e) = UnixDatagram::unbound()?.connect(&sock_path) + && e.kind() == std::io::ErrorKind::ConnectionRefused + { + std::fs::remove_file(&sock_path)?; } let listener = UnixDatagram::bind(&sock_path)?; thread::spawn(move || { @@ -244,12 +242,12 @@ pub async fn open_paths_with_positions( .iter() .map(|path_with_position| { let path = path_with_position.path.clone(); - if let Some(row) = path_with_position.row { - if path.is_file() { - let row = row.saturating_sub(1); - let col = path_with_position.column.unwrap_or(0).saturating_sub(1); - caret_positions.insert(path.clone(), Point::new(row, col)); - } + if let Some(row) = path_with_position.row + && path.is_file() + { + let row = row.saturating_sub(1); + let col = path_with_position.column.unwrap_or(0).saturating_sub(1); + caret_positions.insert(path.clone(), Point::new(row, col)); } path }) @@ -264,10 +262,9 @@ pub async fn open_paths_with_positions( let new_path = Path::new(&diff_pair[1]).canonicalize()?; if let Ok(diff_view) = workspace.update(cx, |workspace, window, cx| { FileDiffView::open(old_path, new_path, workspace, window, cx) - }) { - if let Some(diff_view) = diff_view.await.log_err() { - items.push(Some(Ok(Box::new(diff_view)))) - } + }) && let Some(diff_view) = diff_view.await.log_err() + { + items.push(Some(Ok(Box::new(diff_view)))) } } diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index ac7fcade9137d4a60b22e88273b0d625371028e1..313e4c377984d1b4cf3e3ab72004e29bba9c705b 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/crates/zeta/src/rate_completion_modal.rs @@ -267,13 +267,13 @@ impl RateCompletionModal { .unwrap_or(self.selected_index); cx.notify(); - if let Some(prev_completion) = self.active_completion.as_ref() { - if completion.id == prev_completion.completion.id { - if focus { - window.focus(&prev_completion.feedback_editor.focus_handle(cx)); - } - return; + if let Some(prev_completion) = self.active_completion.as_ref() + && completion.id == prev_completion.completion.id + { + if focus { + window.focus(&prev_completion.feedback_editor.focus_handle(cx)); } + return; } } diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 956e416fe98087168bb61932e5d0211443a0ea4e..2a121c407c5456f3cdfb5cc8eb6206f8cd985f41 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -836,12 +836,11 @@ and then another .headers() .get(MINIMUM_REQUIRED_VERSION_HEADER_NAME) .and_then(|version| SemanticVersion::from_str(version.to_str().ok()?).ok()) + && app_version < minimum_required_version { - if app_version < minimum_required_version { - return Err(anyhow!(ZedUpdateRequiredError { - minimum_version: minimum_required_version - })); - } + return Err(anyhow!(ZedUpdateRequiredError { + minimum_version: minimum_required_version + })); } if response.status().is_success() { diff --git a/crates/zlog/src/sink.rs b/crates/zlog/src/sink.rs index 17aa08026e6dea4bbc98946e044ce3828e5aa28f..3ac85d4bbfc8aaa5d8568cb14b50e04a94708f1c 100644 --- a/crates/zlog/src/sink.rs +++ b/crates/zlog/src/sink.rs @@ -194,10 +194,10 @@ pub fn flush() { ENABLED_SINKS_FILE.clear_poison(); handle.into_inner() }); - if let Some(file) = file.as_mut() { - if let Err(err) = file.flush() { - eprintln!("Failed to flush log file: {}", err); - } + if let Some(file) = file.as_mut() + && let Err(err) = file.flush() + { + eprintln!("Failed to flush log file: {}", err); } } diff --git a/crates/zlog/src/zlog.rs b/crates/zlog/src/zlog.rs index 5b40278f3fb0adbafe1815608765aa4ab3d44e57..df3a2102317775288f11650320bdc26d7a34d1af 100644 --- a/crates/zlog/src/zlog.rs +++ b/crates/zlog/src/zlog.rs @@ -28,10 +28,8 @@ pub fn try_init() -> anyhow::Result<()> { } pub fn init_test() { - if get_env_config().is_some() { - if try_init().is_ok() { - init_output_stdout(); - } + if get_env_config().is_some() && try_init().is_ok() { + init_output_stdout(); } } @@ -344,18 +342,18 @@ impl Timer { return; } let elapsed = self.start_time.elapsed(); - if let Some(warn_limit) = self.warn_if_longer_than { - if elapsed > warn_limit { - crate::warn!( - self.logger => - "Timer '{}' took {:?}. Which was longer than the expected limit of {:?}", - self.name, - elapsed, - warn_limit - ); - self.done = true; - return; - } + if let Some(warn_limit) = self.warn_if_longer_than + && elapsed > warn_limit + { + crate::warn!( + self.logger => + "Timer '{}' took {:?}. Which was longer than the expected limit of {:?}", + self.name, + elapsed, + warn_limit + ); + self.done = true; + return; } crate::trace!( self.logger => diff --git a/extensions/glsl/src/glsl.rs b/extensions/glsl/src/glsl.rs index a42403ebefeff89b749d5091e84ceea2528aba79..ba506d2b11fca935fe327279421259d284dd8e3d 100644 --- a/extensions/glsl/src/glsl.rs +++ b/extensions/glsl/src/glsl.rs @@ -16,10 +16,10 @@ impl GlslExtension { return Ok(path); } - if let Some(path) = &self.cached_binary_path { - if fs::metadata(path).map_or(false, |stat| stat.is_file()) { - return Ok(path.clone()); - } + if let Some(path) = &self.cached_binary_path + && fs::metadata(path).map_or(false, |stat| stat.is_file()) + { + return Ok(path.clone()); } zed::set_language_server_installation_status( diff --git a/extensions/ruff/src/ruff.rs b/extensions/ruff/src/ruff.rs index da9b6c0bf1a03cd69f0e29f2a7b2282c30149fe5..7b811db21202cd0bb7c3f6b54248144ae8a2f6e6 100644 --- a/extensions/ruff/src/ruff.rs +++ b/extensions/ruff/src/ruff.rs @@ -38,13 +38,13 @@ impl RuffExtension { }); } - if let Some(path) = &self.cached_binary_path { - if fs::metadata(path).map_or(false, |stat| stat.is_file()) { - return Ok(RuffBinary { - path: path.clone(), - args: binary_args, - }); - } + if let Some(path) = &self.cached_binary_path + && fs::metadata(path).map_or(false, |stat| stat.is_file()) + { + return Ok(RuffBinary { + path: path.clone(), + args: binary_args, + }); } zed::set_language_server_installation_status( diff --git a/extensions/snippets/src/snippets.rs b/extensions/snippets/src/snippets.rs index 46ba7469301699d423ecd5ef35dbf015901b2bfe..682709a28a4857e854cda66787025a20569a5cca 100644 --- a/extensions/snippets/src/snippets.rs +++ b/extensions/snippets/src/snippets.rs @@ -17,10 +17,10 @@ impl SnippetExtension { return Ok(path); } - if let Some(path) = &self.cached_binary_path { - if fs::metadata(path).map_or(false, |stat| stat.is_file()) { - return Ok(path.clone()); - } + if let Some(path) = &self.cached_binary_path + && fs::metadata(path).map_or(false, |stat| stat.is_file()) + { + return Ok(path.clone()); } zed::set_language_server_installation_status( diff --git a/extensions/test-extension/src/test_extension.rs b/extensions/test-extension/src/test_extension.rs index 5b6a3f920a136362edcfdd5488ae2dc17341aa47..0ef522bd51277f45897d3455a274d132683dd32c 100644 --- a/extensions/test-extension/src/test_extension.rs +++ b/extensions/test-extension/src/test_extension.rs @@ -18,10 +18,10 @@ impl TestExtension { println!("{}", String::from_utf8_lossy(&echo_output.stdout)); - if let Some(path) = &self.cached_binary_path { - if fs::metadata(path).map_or(false, |stat| stat.is_file()) { - return Ok(path.clone()); - } + if let Some(path) = &self.cached_binary_path + && fs::metadata(path).map_or(false, |stat| stat.is_file()) + { + return Ok(path.clone()); } zed::set_language_server_installation_status( diff --git a/extensions/toml/src/toml.rs b/extensions/toml/src/toml.rs index 20f27b6d97ee2793d00152aacc37997be6f404a9..30a2cd6ce3b9c83fcc25ed2c38adc1623a5fb353 100644 --- a/extensions/toml/src/toml.rs +++ b/extensions/toml/src/toml.rs @@ -39,13 +39,13 @@ impl TomlExtension { }); } - if let Some(path) = &self.cached_binary_path { - if fs::metadata(path).map_or(false, |stat| stat.is_file()) { - return Ok(TaploBinary { - path: path.clone(), - args: binary_args, - }); - } + if let Some(path) = &self.cached_binary_path + && fs::metadata(path).map_or(false, |stat| stat.is_file()) + { + return Ok(TaploBinary { + path: path.clone(), + args: binary_args, + }); } zed::set_language_server_installation_status( From e3b593efbdfab2609a44ce3dee14be143d341155 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Tue, 19 Aug 2025 19:04:48 +0530 Subject: [PATCH 139/744] project: Take 2 on Handle textDocument/didSave and textDocument/didChange (un)registration and usage correctly (#36485) Relands https://github.com/zed-industries/zed/pull/36441 with a deserialization fix. Previously, deserializing `"includeText"` into `lsp::TextDocumentSyncSaveOptions` resulted in a `Supported(false)` type instead of `SaveOptions(SaveOptions { include_text: Option<bool> })`. ```rs impl From<bool> for TextDocumentSyncSaveOptions { fn from(from: bool) -> Self { Self::Supported(from) } } ``` Looks like, while dynamic registartion we only get `SaveOptions` type and never `Supported` type. (https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentSaveRegistrationOptions) Release Notes: - N/A --------- Co-authored-by: Lukas Wirth <lukas@zed.dev> --- crates/project/src/lsp_store.rs | 88 ++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 18 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 23061149bfd2112303d28c7f2b8f2b0fb8e620c8..75609b3187bc99c963ac97099a02e89ffd93de63 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -74,8 +74,8 @@ use lsp::{ FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, - OneOf, RenameFilesParams, SymbolKind, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, - WorkspaceFolder, notification::DidRenameFiles, + OneOf, RenameFilesParams, SymbolKind, TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, + WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; use parking_lot::Mutex; @@ -11800,8 +11800,40 @@ impl LspStore { .transpose()? { server.update_capabilities(|capabilities| { + let mut sync_options = + Self::take_text_document_sync_options(capabilities); + sync_options.change = Some(sync_kind); capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)); + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + }); + notify_server_capabilities_updated(&server, cx); + } + } + "textDocument/didSave" => { + if let Some(include_text) = reg + .register_options + .map(|opts| { + let transpose = opts + .get("includeText") + .cloned() + .map(serde_json::from_value::<Option<bool>>) + .transpose(); + match transpose { + Ok(value) => Ok(value.flatten()), + Err(e) => Err(e), + } + }) + .transpose()? + { + server.update_capabilities(|capabilities| { + let mut sync_options = + Self::take_text_document_sync_options(capabilities); + sync_options.save = + Some(TextDocumentSyncSaveOptions::SaveOptions(lsp::SaveOptions { + include_text, + })); + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); }); notify_server_capabilities_updated(&server, cx); } @@ -11953,7 +11985,19 @@ impl LspStore { } "textDocument/didChange" => { server.update_capabilities(|capabilities| { - capabilities.text_document_sync = None; + let mut sync_options = Self::take_text_document_sync_options(capabilities); + sync_options.change = None; + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/didSave" => { + server.update_capabilities(|capabilities| { + let mut sync_options = Self::take_text_document_sync_options(capabilities); + sync_options.save = None; + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); }); notify_server_capabilities_updated(&server, cx); } @@ -11981,6 +12025,20 @@ impl LspStore { Ok(()) } + + fn take_text_document_sync_options( + capabilities: &mut lsp::ServerCapabilities, + ) -> lsp::TextDocumentSyncOptions { + match capabilities.text_document_sync.take() { + Some(lsp::TextDocumentSyncCapability::Options(sync_options)) => sync_options, + Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)) => { + let mut sync_options = lsp::TextDocumentSyncOptions::default(); + sync_options.change = Some(sync_kind); + sync_options + } + None => lsp::TextDocumentSyncOptions::default(), + } + } } // Registration with empty capabilities should be ignored. @@ -13083,24 +13141,18 @@ async fn populate_labels_for_symbols( fn include_text(server: &lsp::LanguageServer) -> Option<bool> { match server.capabilities().text_document_sync.as_ref()? { - lsp::TextDocumentSyncCapability::Kind(kind) => match *kind { - lsp::TextDocumentSyncKind::NONE => None, - lsp::TextDocumentSyncKind::FULL => Some(true), - lsp::TextDocumentSyncKind::INCREMENTAL => Some(false), - _ => None, - }, - lsp::TextDocumentSyncCapability::Options(options) => match options.save.as_ref()? { - lsp::TextDocumentSyncSaveOptions::Supported(supported) => { - if *supported { - Some(true) - } else { - None - } - } + lsp::TextDocumentSyncCapability::Options(opts) => match opts.save.as_ref()? { + // Server wants didSave but didn't specify includeText. + lsp::TextDocumentSyncSaveOptions::Supported(true) => Some(false), + // Server doesn't want didSave at all. + lsp::TextDocumentSyncSaveOptions::Supported(false) => None, + // Server provided SaveOptions. lsp::TextDocumentSyncSaveOptions::SaveOptions(save_options) => { Some(save_options.include_text.unwrap_or(false)) } }, + // We do not have any save info. Kind affects didChange only. + lsp::TextDocumentSyncCapability::Kind(_) => None, } } From c4083b9b63efddd093126a2b613e44ceb9e7e505 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:20:01 +0200 Subject: [PATCH 140/744] Fix unnecessary-mut-passed lint (#36490) Release Notes: - N/A --- Cargo.toml | 1 + crates/agent_ui/src/acp/message_editor.rs | 2 +- crates/assistant_context/src/context_store.rs | 2 +- crates/auto_update/src/auto_update.rs | 2 +- crates/call/src/call_impl/mod.rs | 2 +- crates/channel/src/channel_buffer.rs | 4 +- crates/channel/src/channel_chat.rs | 4 +- crates/channel/src/channel_store.rs | 4 +- crates/client/src/client.rs | 10 ++-- crates/client/src/user.rs | 4 +- crates/editor/src/hover_links.rs | 8 ++-- crates/gpui/src/app.rs | 6 +-- crates/gpui/src/app/context.rs | 2 +- crates/gpui/src/app/test_context.rs | 6 +-- crates/project/src/buffer_store.rs | 6 +-- .../project/src/debugger/breakpoint_store.rs | 2 +- crates/project/src/lsp_command.rs | 46 +++++++++---------- crates/project/src/lsp_store.rs | 24 +++++----- .../project/src/lsp_store/lsp_ext_command.rs | 12 ++--- crates/project/src/project.rs | 32 ++++++------- crates/project/src/search_history.rs | 2 +- crates/project/src/task_store.rs | 2 +- crates/remote_server/src/headless_project.rs | 8 ++-- crates/remote_server/src/unix.rs | 2 +- crates/search/src/buffer_search.rs | 2 +- crates/search/src/project_search.rs | 2 +- .../src/ui_components/keystroke_input.rs | 4 +- crates/snippet_provider/src/lib.rs | 6 +-- 28 files changed, 103 insertions(+), 104 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 89aadbcba0a74bc0ed43ebedc419ad9533216ccc..603897084c50fc4dc9380b9d5a6aa08f6cced23f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -832,6 +832,7 @@ redundant_closure = { level = "deny" } declare_interior_mutable_const = { level = "deny" } collapsible_if = { level = "warn"} needless_borrow = { level = "warn"} +unnecessary_mut_passed = {level = "warn"} # Individual rules that have violations in the codebase: type_complexity = "allow" # We often return trait objects from `new` functions. diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index a32d0ce6ce8c3e0b292d56fb9fccbdd598c75888..00368d6087a510bc1f471330e12f136fb05b6fdd 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -2173,7 +2173,7 @@ mod tests { cx.run_until_parked(); - editor.read_with(&mut cx, |editor, cx| { + editor.read_with(&cx, |editor, cx| { assert_eq!( editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) [@MySymbol](file:///dir/a/one.txt?symbol=MySymbol#L1:1) " diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index af43b912e94ec8d64ebdef2f538a224751b8d648..a2b3adc68657caca42b5f27332e1bb21a601fb48 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -320,7 +320,7 @@ impl ContextStore { .client .subscribe_to_entity(remote_id) .log_err() - .map(|subscription| subscription.set_entity(&cx.entity(), &mut cx.to_async())); + .map(|subscription| subscription.set_entity(&cx.entity(), &cx.to_async())); self.advertise_contexts(cx); } else { self.client_subscription = None; diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 4d0d2d59843d4cde885340319d261a4e7315765e..2150873cadd0a84b4a2894ebbe373d9bd0e007f0 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -543,7 +543,7 @@ impl AutoUpdater { async fn update(this: Entity<Self>, mut cx: AsyncApp) -> Result<()> { let (client, installed_version, previous_status, release_channel) = - this.read_with(&mut cx, |this, cx| { + this.read_with(&cx, |this, cx| { ( this.http_client.clone(), this.current_version, diff --git a/crates/call/src/call_impl/mod.rs b/crates/call/src/call_impl/mod.rs index 71c314932419e1228c74e2d3de547a4e21b152c6..6cc94a5dd536f61a1809899cb9f7d13a9c7e3381 100644 --- a/crates/call/src/call_impl/mod.rs +++ b/crates/call/src/call_impl/mod.rs @@ -116,7 +116,7 @@ impl ActiveCall { envelope: TypedEnvelope<proto::IncomingCall>, mut cx: AsyncApp, ) -> Result<proto::Ack> { - let user_store = this.read_with(&mut cx, |this, _| this.user_store.clone())?; + let user_store = this.read_with(&cx, |this, _| this.user_store.clone())?; let call = IncomingCall { room_id: envelope.payload.room_id, participants: user_store diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index a367ffbf099f07ba83c8bc902c8463c4372cf464..943e819ad6ddf9b8da3b7a224b13c33024bd86ba 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -82,7 +82,7 @@ impl ChannelBuffer { collaborators: Default::default(), acknowledge_task: None, channel_id: channel.id, - subscription: Some(subscription.set_entity(&cx.entity(), &mut cx.to_async())), + subscription: Some(subscription.set_entity(&cx.entity(), &cx.to_async())), user_store, channel_store, }; @@ -110,7 +110,7 @@ impl ChannelBuffer { let Ok(subscription) = self.client.subscribe_to_entity(self.channel_id.0) else { return; }; - self.subscription = Some(subscription.set_entity(&cx.entity(), &mut cx.to_async())); + self.subscription = Some(subscription.set_entity(&cx.entity(), &cx.to_async())); cx.emit(ChannelBufferEvent::Connected); } } diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 02b5ccec6809d068912dba410b7c2b21d59ae261..86f307717c03695d68f17f0c54346abeeaf225fd 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -532,7 +532,7 @@ impl ChannelChat { message: TypedEnvelope<proto::ChannelMessageSent>, mut cx: AsyncApp, ) -> Result<()> { - let user_store = this.read_with(&mut cx, |this, _| this.user_store.clone())?; + let user_store = this.read_with(&cx, |this, _| this.user_store.clone())?; let message = message.payload.message.context("empty message")?; let message_id = message.id; @@ -564,7 +564,7 @@ impl ChannelChat { message: TypedEnvelope<proto::ChannelMessageUpdate>, mut cx: AsyncApp, ) -> Result<()> { - let user_store = this.read_with(&mut cx, |this, _| this.user_store.clone())?; + let user_store = this.read_with(&cx, |this, _| this.user_store.clone())?; let message = message.payload.message.context("empty message")?; let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?; diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 6d1716a7eaf5e0a4498fc7e80f2ec026232b034d..42a1851408f6cf7c5add3f4c1c29a2998d6ec5af 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -908,9 +908,9 @@ impl ChannelStore { async fn handle_update_channels( this: Entity<Self>, message: TypedEnvelope<proto::UpdateChannels>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<()> { - this.read_with(&mut cx, |this, _| { + this.read_with(&cx, |this, _| { this.update_channels_tx .unbounded_send(message.payload) .unwrap(); diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index d7d8b602119f8f11627f47832b8dc5511b1f3220..218cf2b0797576956cc14a60a2575ebb00a40660 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -2073,8 +2073,8 @@ mod tests { let (done_tx1, done_rx1) = smol::channel::unbounded(); let (done_tx2, done_rx2) = smol::channel::unbounded(); AnyProtoClient::from(client.clone()).add_entity_message_handler( - move |entity: Entity<TestEntity>, _: TypedEnvelope<proto::JoinProject>, mut cx| { - match entity.read_with(&mut cx, |entity, _| entity.id).unwrap() { + move |entity: Entity<TestEntity>, _: TypedEnvelope<proto::JoinProject>, cx| { + match entity.read_with(&cx, |entity, _| entity.id).unwrap() { 1 => done_tx1.try_send(()).unwrap(), 2 => done_tx2.try_send(()).unwrap(), _ => unreachable!(), @@ -2098,17 +2098,17 @@ mod tests { let _subscription1 = client .subscribe_to_entity(1) .unwrap() - .set_entity(&entity1, &mut cx.to_async()); + .set_entity(&entity1, &cx.to_async()); let _subscription2 = client .subscribe_to_entity(2) .unwrap() - .set_entity(&entity2, &mut cx.to_async()); + .set_entity(&entity2, &cx.to_async()); // Ensure dropping a subscription for the same entity type still allows receiving of // messages for other entity IDs of the same type. let subscription3 = client .subscribe_to_entity(3) .unwrap() - .set_entity(&entity3, &mut cx.to_async()); + .set_entity(&entity3, &cx.to_async()); drop(subscription3); server.send(proto::JoinProject { diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 3509a8c57fe5c114fe66d9ded7ddf7c204c06086..722d6861ff59ba7b65ff67044e18c02c5576476b 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -332,9 +332,9 @@ impl UserStore { async fn handle_update_contacts( this: Entity<Self>, message: TypedEnvelope<proto::UpdateContacts>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<()> { - this.read_with(&mut cx, |this, _| { + this.read_with(&cx, |this, _| { this.update_contacts_tx .unbounded_send(UpdateContacts::Update(message.payload)) .unwrap(); diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index b431834d350a17bd097fa7a4f04f17ea12922451..358d8683fe48a3c4b95d44b0818296b2ca0f5b43 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -655,11 +655,11 @@ pub fn show_link_definition( pub(crate) fn find_url( buffer: &Entity<language::Buffer>, position: text::Anchor, - mut cx: AsyncWindowContext, + cx: AsyncWindowContext, ) -> Option<(Range<text::Anchor>, String)> { const LIMIT: usize = 2048; - let Ok(snapshot) = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot()) else { + let Ok(snapshot) = buffer.read_with(&cx, |buffer, _| buffer.snapshot()) else { return None; }; @@ -717,11 +717,11 @@ pub(crate) fn find_url( pub(crate) fn find_url_from_range( buffer: &Entity<language::Buffer>, range: Range<text::Anchor>, - mut cx: AsyncWindowContext, + cx: AsyncWindowContext, ) -> Option<String> { const LIMIT: usize = 2048; - let Ok(snapshot) = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot()) else { + let Ok(snapshot) = buffer.read_with(&cx, |buffer, _| buffer.snapshot()) else { return None; }; diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index c4499aff0769f2ae2d7e6029cfd5caf6820bd6ed..2be1a34e4993c50d1d42da1a5d86db9a354a272e 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -368,7 +368,7 @@ impl App { }), }); - init_app_menus(platform.as_ref(), &mut app.borrow_mut()); + init_app_menus(platform.as_ref(), &app.borrow()); platform.on_keyboard_layout_change(Box::new({ let app = Rc::downgrade(&app); @@ -1332,7 +1332,7 @@ impl App { } inner( - &mut self.keystroke_observers, + &self.keystroke_observers, Box::new(move |event, window, cx| { f(event, window, cx); true @@ -1358,7 +1358,7 @@ impl App { } inner( - &mut self.keystroke_interceptors, + &self.keystroke_interceptors, Box::new(move |event, window, cx| { f(event, window, cx); true diff --git a/crates/gpui/src/app/context.rs b/crates/gpui/src/app/context.rs index a6ab02677093b25d37ee5e608f299cdf9d86dc2f..1112878a66b07c133031086c3b14aa8427617bea 100644 --- a/crates/gpui/src/app/context.rs +++ b/crates/gpui/src/app/context.rs @@ -472,7 +472,7 @@ impl<'a, T: 'static> Context<'a, T> { let view = self.weak_entity(); inner( - &mut self.keystroke_observers, + &self.keystroke_observers, Box::new(move |event, window, cx| { if let Some(view) = view.upgrade() { view.update(cx, |view, cx| f(view, event, window, cx)); diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index a96c24432a8851cb8b7dbadb3f7e794971dfca0b..43adacf7ddb7aa3a08d3a11bc8d0fade3b34a073 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -219,7 +219,7 @@ impl TestAppContext { let mut cx = self.app.borrow_mut(); // Some tests rely on the window size matching the bounds of the test display - let bounds = Bounds::maximized(None, &mut cx); + let bounds = Bounds::maximized(None, &cx); cx.open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), @@ -233,7 +233,7 @@ impl TestAppContext { /// Adds a new window with no content. pub fn add_empty_window(&mut self) -> &mut VisualTestContext { let mut cx = self.app.borrow_mut(); - let bounds = Bounds::maximized(None, &mut cx); + let bounds = Bounds::maximized(None, &cx); let window = cx .open_window( WindowOptions { @@ -261,7 +261,7 @@ impl TestAppContext { V: 'static + Render, { let mut cx = self.app.borrow_mut(); - let bounds = Bounds::maximized(None, &mut cx); + let bounds = Bounds::maximized(None, &cx); let window = cx .open_window( WindowOptions { diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 1522376e9a43f0eb3d9fc218b4ed6cfd52f1bebe..96e87b1fe0ac30112543fd79498128179a9bb55e 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -1345,7 +1345,7 @@ impl BufferStore { mut cx: AsyncApp, ) -> Result<proto::BufferSaved> { let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let (buffer, project_id) = this.read_with(&mut cx, |this, _| { + let (buffer, project_id) = this.read_with(&cx, |this, _| { anyhow::Ok(( this.get_existing(buffer_id)?, this.downstream_client @@ -1359,7 +1359,7 @@ impl BufferStore { buffer.wait_for_version(deserialize_version(&envelope.payload.version)) })? .await?; - let buffer_id = buffer.read_with(&mut cx, |buffer, _| buffer.remote_id())?; + let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?; if let Some(new_path) = envelope.payload.new_path { let new_path = ProjectPath::from_proto(new_path); @@ -1372,7 +1372,7 @@ impl BufferStore { .await?; } - buffer.read_with(&mut cx, |buffer, _| proto::BufferSaved { + buffer.read_with(&cx, |buffer, _| proto::BufferSaved { project_id, buffer_id: buffer_id.into(), version: serialize_version(buffer.saved_version()), diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index faa9948596247b70186a1990cb8a34eca61ad716..38d8b4cfc65300931dd7045cfce53fc240eb8de2 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -267,7 +267,7 @@ impl BreakpointStore { message: TypedEnvelope<proto::ToggleBreakpoint>, mut cx: AsyncApp, ) -> Result<proto::Ack> { - let breakpoints = this.read_with(&mut cx, |this, _| this.breakpoint_store())?; + let breakpoints = this.read_with(&cx, |this, _| this.breakpoint_store())?; let path = this .update(&mut cx, |this, cx| { this.project_path_for_absolute_path(message.payload.path.as_ref(), cx) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 2a1facd3c0f58be066b54739326797c59ad87b25..64414c654534d41fd9e420eb220319588fbcdeb4 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -332,9 +332,9 @@ impl LspCommand for PrepareRename { _: Entity<LspStore>, buffer: Entity<Buffer>, _: LanguageServerId, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<PrepareRenameResponse> { - buffer.read_with(&mut cx, |buffer, _| match message { + buffer.read_with(&cx, |buffer, _| match message { Some(lsp::PrepareRenameResponse::Range(range)) | Some(lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. }) => { let Range { start, end } = range_from_lsp(range); @@ -386,7 +386,7 @@ impl LspCommand for PrepareRename { .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -543,7 +543,7 @@ impl LspCommand for PerformRename { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, new_name: message.new_name, push_to_history: false, }) @@ -658,7 +658,7 @@ impl LspCommand for GetDefinitions { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -761,7 +761,7 @@ impl LspCommand for GetDeclarations { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -863,7 +863,7 @@ impl LspCommand for GetImplementations { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -962,7 +962,7 @@ impl LspCommand for GetTypeDefinitions { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -1330,7 +1330,7 @@ impl LspCommand for GetReferences { target_buffer_handle .clone() - .read_with(&mut cx, |target_buffer, _| { + .read_with(&cx, |target_buffer, _| { let target_start = target_buffer .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); let target_end = target_buffer @@ -1374,7 +1374,7 @@ impl LspCommand for GetReferences { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -1484,9 +1484,9 @@ impl LspCommand for GetDocumentHighlights { _: Entity<LspStore>, buffer: Entity<Buffer>, _: LanguageServerId, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<Vec<DocumentHighlight>> { - buffer.read_with(&mut cx, |buffer, _| { + buffer.read_with(&cx, |buffer, _| { let mut lsp_highlights = lsp_highlights.unwrap_or_default(); lsp_highlights.sort_unstable_by_key(|h| (h.range.start, Reverse(h.range.end))); lsp_highlights @@ -1534,7 +1534,7 @@ impl LspCommand for GetDocumentHighlights { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -1865,7 +1865,7 @@ impl LspCommand for GetSignatureHelp { })? .await .with_context(|| format!("waiting for version for buffer {}", buffer.entity_id()))?; - let buffer_snapshot = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot())?; + let buffer_snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?; Ok(Self { position: payload .position @@ -1947,13 +1947,13 @@ impl LspCommand for GetHover { _: Entity<LspStore>, buffer: Entity<Buffer>, _: LanguageServerId, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<Self::Response> { let Some(hover) = message else { return Ok(None); }; - let (language, range) = buffer.read_with(&mut cx, |buffer, _| { + let (language, range) = buffer.read_with(&cx, |buffer, _| { ( buffer.language().cloned(), hover.range.map(|range| { @@ -2039,7 +2039,7 @@ impl LspCommand for GetHover { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -2113,7 +2113,7 @@ impl LspCommand for GetHover { return Ok(None); } - let language = buffer.read_with(&mut cx, |buffer, _| buffer.language().cloned())?; + let language = buffer.read_with(&cx, |buffer, _| buffer.language().cloned())?; let range = if let (Some(start), Some(end)) = (message.start, message.end) { language::proto::deserialize_anchor(start) .and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end)) @@ -2208,7 +2208,7 @@ impl LspCommand for GetCompletions { let unfiltered_completions_count = completions.len(); let language_server_adapter = lsp_store - .read_with(&mut cx, |lsp_store, _| { + .read_with(&cx, |lsp_store, _| { lsp_store.language_server_adapter_for_id(server_id) })? .with_context(|| format!("no language server with id {server_id}"))?; @@ -2394,7 +2394,7 @@ impl LspCommand for GetCompletions { .position .and_then(language::proto::deserialize_anchor) .map(|p| { - buffer.read_with(&mut cx, |buffer, _| { + buffer.read_with(&cx, |buffer, _| { buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left) }) }) @@ -2862,7 +2862,7 @@ impl LspCommand for OnTypeFormatting { })?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, trigger: message.trigger.clone(), options, push_to_history: false, @@ -3474,9 +3474,9 @@ impl LspCommand for GetCodeLens { lsp_store: Entity<LspStore>, buffer: Entity<Buffer>, server_id: LanguageServerId, - mut cx: AsyncApp, + cx: AsyncApp, ) -> anyhow::Result<Vec<CodeAction>> { - let snapshot = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot())?; + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?; let language_server = cx.update(|cx| { lsp_store .read(cx) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 75609b3187bc99c963ac97099a02e89ffd93de63..e93e859dcf917a5b5fa75c3cec804118e0e71fac 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -669,10 +669,10 @@ impl LocalLspStore { let this = this.clone(); move |_, cx| { let this = this.clone(); - let mut cx = cx.clone(); + let cx = cx.clone(); async move { - let Some(server) = this - .read_with(&mut cx, |this, _| this.language_server_for_id(server_id))? + let Some(server) = + this.read_with(&cx, |this, _| this.language_server_for_id(server_id))? else { return Ok(None); }; @@ -8154,7 +8154,7 @@ impl LspStore { envelope: TypedEnvelope<proto::MultiLspQuery>, mut cx: AsyncApp, ) -> Result<proto::MultiLspQueryResponse> { - let response_from_ssh = lsp_store.read_with(&mut cx, |this, _| { + let response_from_ssh = lsp_store.read_with(&cx, |this, _| { let (upstream_client, project_id) = this.upstream_client()?; let mut payload = envelope.payload.clone(); payload.project_id = project_id; @@ -8176,7 +8176,7 @@ impl LspStore { buffer.wait_for_version(version.clone()) })? .await?; - let buffer_version = buffer.read_with(&mut cx, |buffer, _| buffer.version())?; + let buffer_version = buffer.read_with(&cx, |buffer, _| buffer.version())?; match envelope .payload .strategy @@ -8717,7 +8717,7 @@ impl LspStore { })? .context("worktree not found")?; let (old_abs_path, new_abs_path) = { - let root_path = worktree.read_with(&mut cx, |this, _| this.abs_path())?; + let root_path = worktree.read_with(&cx, |this, _| this.abs_path())?; let new_path = PathBuf::from_proto(envelope.payload.new_path.clone()); (root_path.join(&old_path), root_path.join(&new_path)) }; @@ -8732,7 +8732,7 @@ impl LspStore { ) .await; let response = Worktree::handle_rename_entry(worktree, envelope.payload, cx.clone()).await; - this.read_with(&mut cx, |this, _| { + this.read_with(&cx, |this, _| { this.did_rename_entry(worktree_id, &old_abs_path, &new_abs_path, is_dir); }) .ok(); @@ -8966,10 +8966,10 @@ impl LspStore { async fn handle_lsp_ext_cancel_flycheck( lsp_store: Entity<Self>, envelope: TypedEnvelope<proto::LspExtCancelFlycheck>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<proto::Ack> { let server_id = LanguageServerId(envelope.payload.language_server_id as usize); - lsp_store.read_with(&mut cx, |lsp_store, _| { + lsp_store.read_with(&cx, |lsp_store, _| { if let Some(server) = lsp_store.language_server_for_id(server_id) { server .notify::<lsp_store::lsp_ext_command::LspExtCancelFlycheck>(&()) @@ -9018,10 +9018,10 @@ impl LspStore { async fn handle_lsp_ext_clear_flycheck( lsp_store: Entity<Self>, envelope: TypedEnvelope<proto::LspExtClearFlycheck>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<proto::Ack> { let server_id = LanguageServerId(envelope.payload.language_server_id as usize); - lsp_store.read_with(&mut cx, |lsp_store, _| { + lsp_store.read_with(&cx, |lsp_store, _| { if let Some(server) = lsp_store.language_server_for_id(server_id) { server .notify::<lsp_store::lsp_ext_command::LspExtClearFlycheck>(&()) @@ -9789,7 +9789,7 @@ impl LspStore { let peer_id = envelope.original_sender_id().unwrap_or_default(); let symbol = envelope.payload.symbol.context("invalid symbol")?; let symbol = Self::deserialize_symbol(symbol)?; - let symbol = this.read_with(&mut cx, |this, _| { + let symbol = this.read_with(&cx, |this, _| { let signature = this.symbol_signature(&symbol.path); anyhow::ensure!(signature == symbol.signature, "invalid symbol signature"); Ok(symbol) diff --git a/crates/project/src/lsp_store/lsp_ext_command.rs b/crates/project/src/lsp_store/lsp_ext_command.rs index cb13fa5efcfd753e0ffb12fbcc0f3d84e09ff370..1c969f8114eb7647e7c109baf2a7b70339997b41 100644 --- a/crates/project/src/lsp_store/lsp_ext_command.rs +++ b/crates/project/src/lsp_store/lsp_ext_command.rs @@ -115,14 +115,14 @@ impl LspCommand for ExpandMacro { message: Self::ProtoRequest, _: Entity<LspStore>, buffer: Entity<Buffer>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> anyhow::Result<Self> { let position = message .position .and_then(deserialize_anchor) .context("invalid position")?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -249,14 +249,14 @@ impl LspCommand for OpenDocs { message: Self::ProtoRequest, _: Entity<LspStore>, buffer: Entity<Buffer>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> anyhow::Result<Self> { let position = message .position .and_then(deserialize_anchor) .context("invalid position")?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -462,14 +462,14 @@ impl LspCommand for GoToParentModule { request: Self::ProtoRequest, _: Entity<LspStore>, buffer: Entity<Buffer>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> anyhow::Result<Self> { let position = request .position .and_then(deserialize_anchor) .context("bad request with bad position")?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3906befee23f5fe2d4c3761d8ae91b37b98daa43..f825cd8c47ddf71e3e894ab84cf9f1d1891bc714 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1613,25 +1613,23 @@ impl Project { .into_iter() .map(|s| match s { EntitySubscription::BufferStore(subscription) => { - subscription.set_entity(&buffer_store, &mut cx) + subscription.set_entity(&buffer_store, &cx) } EntitySubscription::WorktreeStore(subscription) => { - subscription.set_entity(&worktree_store, &mut cx) + subscription.set_entity(&worktree_store, &cx) } EntitySubscription::GitStore(subscription) => { - subscription.set_entity(&git_store, &mut cx) + subscription.set_entity(&git_store, &cx) } EntitySubscription::SettingsObserver(subscription) => { - subscription.set_entity(&settings_observer, &mut cx) - } - EntitySubscription::Project(subscription) => { - subscription.set_entity(&this, &mut cx) + subscription.set_entity(&settings_observer, &cx) } + EntitySubscription::Project(subscription) => subscription.set_entity(&this, &cx), EntitySubscription::LspStore(subscription) => { - subscription.set_entity(&lsp_store, &mut cx) + subscription.set_entity(&lsp_store, &cx) } EntitySubscription::DapStore(subscription) => { - subscription.set_entity(&dap_store, &mut cx) + subscription.set_entity(&dap_store, &cx) } }) .collect::<Vec<_>>(); @@ -2226,28 +2224,28 @@ impl Project { self.client_subscriptions.extend([ self.client .subscribe_to_entity(project_id)? - .set_entity(&cx.entity(), &mut cx.to_async()), + .set_entity(&cx.entity(), &cx.to_async()), self.client .subscribe_to_entity(project_id)? - .set_entity(&self.worktree_store, &mut cx.to_async()), + .set_entity(&self.worktree_store, &cx.to_async()), self.client .subscribe_to_entity(project_id)? - .set_entity(&self.buffer_store, &mut cx.to_async()), + .set_entity(&self.buffer_store, &cx.to_async()), self.client .subscribe_to_entity(project_id)? - .set_entity(&self.lsp_store, &mut cx.to_async()), + .set_entity(&self.lsp_store, &cx.to_async()), self.client .subscribe_to_entity(project_id)? - .set_entity(&self.settings_observer, &mut cx.to_async()), + .set_entity(&self.settings_observer, &cx.to_async()), self.client .subscribe_to_entity(project_id)? - .set_entity(&self.dap_store, &mut cx.to_async()), + .set_entity(&self.dap_store, &cx.to_async()), self.client .subscribe_to_entity(project_id)? - .set_entity(&self.breakpoint_store, &mut cx.to_async()), + .set_entity(&self.breakpoint_store, &cx.to_async()), self.client .subscribe_to_entity(project_id)? - .set_entity(&self.git_store, &mut cx.to_async()), + .set_entity(&self.git_store, &cx.to_async()), ]); self.buffer_store.update(cx, |buffer_store, cx| { diff --git a/crates/project/src/search_history.rs b/crates/project/src/search_history.rs index 401f375094ea1052ce5a38252b9fa4f0943810b4..4b2a7a065b12c222dbc3a984d95ce9b83a52ecba 100644 --- a/crates/project/src/search_history.rs +++ b/crates/project/src/search_history.rs @@ -202,7 +202,7 @@ mod tests { assert_eq!(search_history.current(&cursor), Some("TypeScript")); cursor.reset(); - assert_eq!(search_history.current(&mut cursor), None); + assert_eq!(search_history.current(&cursor), None); assert_eq!( search_history.previous(&mut cursor), Some("TypeScript"), diff --git a/crates/project/src/task_store.rs b/crates/project/src/task_store.rs index f6718a3f3cd65297913be288ef97ef50dcdb52f4..ae49ce5b4d708171e28ef6ade5b05267638c6a31 100644 --- a/crates/project/src/task_store.rs +++ b/crates/project/src/task_store.rs @@ -71,7 +71,7 @@ impl TaskStore { .payload .location .context("no location given for task context handling")?; - let (buffer_store, is_remote) = store.read_with(&mut cx, |store, _| { + let (buffer_store, is_remote) = store.read_with(&cx, |store, _| { Ok(match store { TaskStore::Functional(state) => ( state.buffer_store.clone(), diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 6b0cc2219f205667a3b36c5ea41cd22a4892d421..85150f629ee8ab775b812fa7c0f53671ce37c7ec 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -372,7 +372,7 @@ impl HeadlessProject { mut cx: AsyncApp, ) -> Result<proto::AddWorktreeResponse> { use client::ErrorCodeExt; - let fs = this.read_with(&mut cx, |this, _| this.fs.clone())?; + let fs = this.read_with(&cx, |this, _| this.fs.clone())?; let path = PathBuf::from_proto(shellexpand::tilde(&message.payload.path).to_string()); let canonicalized = match fs.canonicalize(&path).await { @@ -396,7 +396,7 @@ impl HeadlessProject { }; let worktree = this - .read_with(&mut cx.clone(), |this, _| { + .read_with(&cx.clone(), |this, _| { Worktree::local( Arc::from(canonicalized.as_path()), message.payload.visible, @@ -407,7 +407,7 @@ impl HeadlessProject { })? .await?; - let response = this.read_with(&mut cx, |_, cx| { + let response = this.read_with(&cx, |_, cx| { let worktree = worktree.read(cx); proto::AddWorktreeResponse { worktree_id: worktree.id().to_proto(), @@ -586,7 +586,7 @@ impl HeadlessProject { let buffer_store = this.read_with(&cx, |this, _| this.buffer_store.clone())?; while let Ok(buffer) = results.recv().await { - let buffer_id = buffer.read_with(&mut cx, |this, _| this.remote_id())?; + let buffer_id = buffer.read_with(&cx, |this, _| this.remote_id())?; response.buffer_ids.push(buffer_id.to_proto()); buffer_store .update(&mut cx, |buffer_store, cx| { diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 76e74b75bdcf2b5cd14b6226f4c05f8e4c3ad12e..9315536e6b4b942d1bf5d572c930b0ea4e736931 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -622,7 +622,7 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { Err(anyhow!(error))?; } n => { - stderr.write_all(&mut stderr_buffer[..n]).await?; + stderr.write_all(&stderr_buffer[..n]).await?; stderr.flush().await?; } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 78e4da7bc61d15daf9ab46d9e3d2f34c617767a6..75042f184f3d3fe285a700a87d430f384df0127d 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1340,7 +1340,7 @@ impl BufferSearchBar { if self.query(cx).is_empty() && let Some(new_query) = self .search_history - .current(&mut self.search_history_cursor) + .current(&self.search_history_cursor) .map(str::to_string) { drop(self.search(&new_query, Some(self.search_options), window, cx)); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 51cb1fdb26ddf644755bf995a4ec8ac0cfddd3a4..b6836324db10305454763b142cb51d150290fb7a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -4111,7 +4111,7 @@ pub mod tests { }); cx.run_until_parked(); let project_search_view = pane - .read_with(&mut cx, |pane, _| { + .read_with(&cx, |pane, _| { pane.active_item() .and_then(|item| item.downcast::<ProjectSearchView>()) }) diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index f23d80931c4e3e509731836bd1230df1f64e4423..de133d406b04d87508433dbd199ced01f38ef069 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -811,7 +811,7 @@ mod tests { pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self { let actual = self .input - .read_with(&mut self.cx, |input, _| input.keystrokes.clone()); + .read_with(&self.cx, |input, _| input.keystrokes.clone()); Self::expect_keystrokes_equal(&actual, expected); self } @@ -820,7 +820,7 @@ mod tests { pub fn expect_close_keystrokes(&mut self, expected: &[&str]) -> &mut Self { let actual = self .input - .read_with(&mut self.cx, |input, _| input.close_keystrokes.clone()) + .read_with(&self.cx, |input, _| input.close_keystrokes.clone()) .unwrap_or_default(); Self::expect_keystrokes_equal(&actual, expected); self diff --git a/crates/snippet_provider/src/lib.rs b/crates/snippet_provider/src/lib.rs index d1112a8d00f60ec5faba4d8fdeb4f1721b3ab976..c8d2555df2798c3f3e33dbef9257e6ef3411033e 100644 --- a/crates/snippet_provider/src/lib.rs +++ b/crates/snippet_provider/src/lib.rs @@ -69,7 +69,7 @@ async fn process_updates( entries: Vec<PathBuf>, mut cx: AsyncApp, ) -> Result<()> { - let fs = this.read_with(&mut cx, |this, _| this.fs.clone())?; + let fs = this.read_with(&cx, |this, _| this.fs.clone())?; for entry_path in entries { if !entry_path .extension() @@ -118,9 +118,9 @@ async fn process_updates( async fn initial_scan( this: WeakEntity<SnippetProvider>, path: Arc<Path>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<()> { - let fs = this.read_with(&mut cx, |this, _| this.fs.clone())?; + let fs = this.read_with(&cx, |this, _| this.fs.clone())?; let entries = fs.read_dir(&path).await; if let Ok(entries) = entries { let entries = entries From 6c255c19736389916e8862f339464ae319dc9019 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra <me@as-cii.com> Date: Tue, 19 Aug 2025 16:24:23 +0200 Subject: [PATCH 141/744] Lay the groundwork to support history in agent2 (#36483) This pull request introduces title generation and history replaying. We still need to wire up the rest of the history but this gets us very close. I extracted a lot of this code from `agent2-history` because that branch was starting to get long-lived and there were lots of changes since we started. Release Notes: - N/A --- Cargo.lock | 3 + crates/acp_thread/src/acp_thread.rs | 39 +- crates/acp_thread/src/connection.rs | 16 +- crates/acp_thread/src/diff.rs | 13 +- crates/acp_thread/src/mention.rs | 3 +- crates/agent2/Cargo.toml | 4 + crates/agent2/src/agent.rs | 104 +-- crates/agent2/src/tests/mod.rs | 94 ++- crates/agent2/src/thread.rs | 669 ++++++++++++++---- .../src/tools/context_server_registry.rs | 10 + crates/agent2/src/tools/edit_file_tool.rs | 137 ++-- crates/agent2/src/tools/terminal_tool.rs | 4 +- crates/agent2/src/tools/web_search_tool.rs | 67 +- crates/agent_servers/Cargo.toml | 1 + crates/agent_servers/src/acp/v0.rs | 4 +- crates/agent_servers/src/acp/v1.rs | 7 +- crates/agent_servers/src/claude.rs | 12 +- crates/agent_ui/src/acp/thread_view.rs | 31 +- crates/agent_ui/src/agent_diff.rs | 41 +- 19 files changed, 930 insertions(+), 329 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7edc54257b2248feda0c642d2afede7dd3961e3..dc9d074f01bb75c6af91fb98b3fcecd554f0ec5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,7 @@ version = "0.1.0" dependencies = [ "acp_thread", "action_log", + "agent", "agent-client-protocol", "agent_servers", "agent_settings", @@ -208,6 +209,7 @@ dependencies = [ "env_logger 0.11.8", "fs", "futures 0.3.31", + "git", "gpui", "gpui_tokio", "handlebars 4.5.0", @@ -256,6 +258,7 @@ name = "agent_servers" version = "0.1.0" dependencies = [ "acp_thread", + "action_log", "agent-client-protocol", "agent_settings", "agentic-coding-protocol", diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 227ca984d466d92a263add2011599f3af1677808..7d70727252abc5196f65cbc9cd712bf356063208 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -537,9 +537,15 @@ impl ToolCallContent { acp::ToolCallContent::Content { content } => { Self::ContentBlock(ContentBlock::new(content, &language_registry, cx)) } - acp::ToolCallContent::Diff { diff } => { - Self::Diff(cx.new(|cx| Diff::from_acp(diff, language_registry, cx))) - } + acp::ToolCallContent::Diff { diff } => Self::Diff(cx.new(|cx| { + Diff::finalized( + diff.path, + diff.old_text, + diff.new_text, + language_registry, + cx, + ) + })), } } @@ -682,6 +688,7 @@ pub struct AcpThread { #[derive(Debug)] pub enum AcpThreadEvent { NewEntry, + TitleUpdated, EntryUpdated(usize), EntriesRemoved(Range<usize>), ToolAuthorizationRequired, @@ -728,11 +735,9 @@ impl AcpThread { title: impl Into<SharedString>, connection: Rc<dyn AgentConnection>, project: Entity<Project>, + action_log: Entity<ActionLog>, session_id: acp::SessionId, - cx: &mut Context<Self>, ) -> Self { - let action_log = cx.new(|_| ActionLog::new(project.clone())); - Self { action_log, shared_buffers: Default::default(), @@ -926,6 +931,12 @@ impl AcpThread { cx.emit(AcpThreadEvent::NewEntry); } + pub fn update_title(&mut self, title: SharedString, cx: &mut Context<Self>) -> Result<()> { + self.title = title; + cx.emit(AcpThreadEvent::TitleUpdated); + Ok(()) + } + pub fn update_retry_status(&mut self, status: RetryStatus, cx: &mut Context<Self>) { cx.emit(AcpThreadEvent::Retry(status)); } @@ -1657,7 +1668,7 @@ mod tests { use super::*; use anyhow::anyhow; use futures::{channel::mpsc, future::LocalBoxFuture, select}; - use gpui::{AsyncApp, TestAppContext, WeakEntity}; + use gpui::{App, AsyncApp, TestAppContext, WeakEntity}; use indoc::indoc; use project::{FakeFs, Fs}; use rand::Rng as _; @@ -2327,7 +2338,7 @@ mod tests { self: Rc<Self>, project: Entity<Project>, _cwd: &Path, - cx: &mut gpui::App, + cx: &mut App, ) -> Task<gpui::Result<Entity<AcpThread>>> { let session_id = acp::SessionId( rand::thread_rng() @@ -2337,8 +2348,16 @@ mod tests { .collect::<String>() .into(), ); - let thread = - cx.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)); + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let thread = cx.new(|_cx| { + AcpThread::new( + "Test", + self.clone(), + project, + action_log, + session_id.clone(), + ) + }); self.sessions.lock().insert(session_id, thread.downgrade()); Task::ready(Ok(thread)) } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 0d4116321d00d2ac650a8fe2b43d7406a40b520b..b09f383029d0d2e635076a729204b5eddadd5ad5 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -5,11 +5,12 @@ use collections::IndexMap; use gpui::{Entity, SharedString, Task}; use language_model::LanguageModelProviderId; use project::Project; +use serde::{Deserialize, Serialize}; use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc}; use ui::{App, IconName}; use uuid::Uuid; -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct UserMessageId(Arc<str>); impl UserMessageId { @@ -208,6 +209,7 @@ impl AgentModelList { mod test_support { use std::sync::Arc; + use action_log::ActionLog; use collections::HashMap; use futures::{channel::oneshot, future::try_join_all}; use gpui::{AppContext as _, WeakEntity}; @@ -295,8 +297,16 @@ mod test_support { cx: &mut gpui::App, ) -> Task<gpui::Result<Entity<AcpThread>>> { let session_id = acp::SessionId(self.sessions.lock().len().to_string().into()); - let thread = - cx.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)); + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let thread = cx.new(|_cx| { + AcpThread::new( + "Test", + self.clone(), + project, + action_log, + session_id.clone(), + ) + }); self.sessions.lock().insert( session_id, Session { diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index e5f71d21098b83c149cedf25c0d1f0c76687a1e5..4b779931c5d333089fa8055c0403e0ef2cf8dbaa 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -1,4 +1,3 @@ -use agent_client_protocol as acp; use anyhow::Result; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; use editor::{MultiBuffer, PathKey}; @@ -21,17 +20,13 @@ pub enum Diff { } impl Diff { - pub fn from_acp( - diff: acp::Diff, + pub fn finalized( + path: PathBuf, + old_text: Option<String>, + new_text: String, language_registry: Arc<LanguageRegistry>, cx: &mut Context<Self>, ) -> Self { - let acp::Diff { - path, - old_text, - new_text, - } = diff; - let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly)); let new_buffer = cx.new(|cx| Buffer::local(new_text, cx)); diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 25e64acbee697b23f9c25a006bf709c48ef8d60b..350785ec1ed98521fc6e63e645d0f60b4c31d8f1 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -2,6 +2,7 @@ use agent::ThreadId; use anyhow::{Context as _, Result, bail}; use file_icons::FileIcons; use prompt_store::{PromptId, UserPromptId}; +use serde::{Deserialize, Serialize}; use std::{ fmt, ops::Range, @@ -11,7 +12,7 @@ use std::{ use ui::{App, IconName, SharedString}; use url::Url; -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum MentionUri { File { abs_path: PathBuf, diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index ac1840e5e53f7812a792fa207de3ba24a64e355b..8129341545f38a44656924f8a1282c6a0346de55 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -14,6 +14,7 @@ workspace = true [dependencies] acp_thread.workspace = true action_log.workspace = true +agent.workspace = true agent-client-protocol.workspace = true agent_servers.workspace = true agent_settings.workspace = true @@ -26,6 +27,7 @@ collections.workspace = true context_server.workspace = true fs.workspace = true futures.workspace = true +git.workspace = true gpui.workspace = true handlebars = { workspace = true, features = ["rust-embed"] } html_to_markdown.workspace = true @@ -59,6 +61,7 @@ which.workspace = true workspace-hack.workspace = true [dev-dependencies] +agent = { workspace = true, "features" = ["test-support"] } ctor.workspace = true client = { workspace = true, "features" = ["test-support"] } clock = { workspace = true, "features" = ["test-support"] } @@ -66,6 +69,7 @@ context_server = { workspace = true, "features" = ["test-support"] } editor = { workspace = true, "features" = ["test-support"] } env_logger.workspace = true fs = { workspace = true, "features" = ["test-support"] } +git = { workspace = true, "features" = ["test-support"] } gpui = { workspace = true, "features" = ["test-support"] } gpui_tokio.workspace = true language = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 480b2baa95d2d4e42cb6970045f85b8763ff7fe1..9cf0c3b60398b600783364455a0a4b9dd650b127 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,10 +1,11 @@ use crate::{ - AgentResponseEvent, ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool, - DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, - MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool, ThinkingTool, Thread, - ToolCallAuthorization, UserMessageContent, WebSearchTool, templates::Templates, + ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool, DiagnosticsTool, + EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, + OpenTool, ReadFileTool, TerminalTool, ThinkingTool, Thread, ThreadEvent, ToolCallAuthorization, + UserMessageContent, WebSearchTool, templates::Templates, }; use acp_thread::AgentModelSelector; +use action_log::ActionLog; use agent_client_protocol as acp; use agent_settings::AgentSettings; use anyhow::{Context as _, Result, anyhow}; @@ -427,18 +428,19 @@ impl NativeAgent { ) { self.models.refresh_list(cx); - let default_model = LanguageModelRegistry::read_global(cx) - .default_model() - .map(|m| m.model.clone()); + let registry = LanguageModelRegistry::read_global(cx); + let default_model = registry.default_model().map(|m| m.model.clone()); + let summarization_model = registry.thread_summary_model().map(|m| m.model.clone()); for session in self.sessions.values_mut() { session.thread.update(cx, |thread, cx| { if thread.model().is_none() && let Some(model) = default_model.clone() { - thread.set_model(model); + thread.set_model(model, cx); cx.notify(); } + thread.set_summarization_model(summarization_model.clone(), cx); }); } } @@ -462,10 +464,7 @@ impl NativeAgentConnection { session_id: acp::SessionId, cx: &mut App, f: impl 'static - + FnOnce( - Entity<Thread>, - &mut App, - ) -> Result<mpsc::UnboundedReceiver<Result<AgentResponseEvent>>>, + + FnOnce(Entity<Thread>, &mut App) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>>, ) -> Task<Result<acp::PromptResponse>> { let Some((thread, acp_thread)) = self.0.update(cx, |agent, _cx| { agent @@ -489,7 +488,18 @@ impl NativeAgentConnection { log::trace!("Received completion event: {:?}", event); match event { - AgentResponseEvent::Text(text) => { + ThreadEvent::UserMessage(message) => { + acp_thread.update(cx, |thread, cx| { + for content in message.content { + thread.push_user_content_block( + Some(message.id.clone()), + content.into(), + cx, + ); + } + })?; + } + ThreadEvent::AgentText(text) => { acp_thread.update(cx, |thread, cx| { thread.push_assistant_content_block( acp::ContentBlock::Text(acp::TextContent { @@ -501,7 +511,7 @@ impl NativeAgentConnection { ) })?; } - AgentResponseEvent::Thinking(text) => { + ThreadEvent::AgentThinking(text) => { acp_thread.update(cx, |thread, cx| { thread.push_assistant_content_block( acp::ContentBlock::Text(acp::TextContent { @@ -513,7 +523,7 @@ impl NativeAgentConnection { ) })?; } - AgentResponseEvent::ToolCallAuthorization(ToolCallAuthorization { + ThreadEvent::ToolCallAuthorization(ToolCallAuthorization { tool_call, options, response, @@ -536,22 +546,26 @@ impl NativeAgentConnection { }) .detach(); } - AgentResponseEvent::ToolCall(tool_call) => { + ThreadEvent::ToolCall(tool_call) => { acp_thread.update(cx, |thread, cx| { thread.upsert_tool_call(tool_call, cx) })??; } - AgentResponseEvent::ToolCallUpdate(update) => { + ThreadEvent::ToolCallUpdate(update) => { acp_thread.update(cx, |thread, cx| { thread.update_tool_call(update, cx) })??; } - AgentResponseEvent::Retry(status) => { + ThreadEvent::TitleUpdate(title) => { + acp_thread + .update(cx, |thread, cx| thread.update_title(title, cx))??; + } + ThreadEvent::Retry(status) => { acp_thread.update(cx, |thread, cx| { thread.update_retry_status(status, cx) })?; } - AgentResponseEvent::Stop(stop_reason) => { + ThreadEvent::Stop(stop_reason) => { log::debug!("Assistant message complete: {:?}", stop_reason); return Ok(acp::PromptResponse { stop_reason }); } @@ -604,8 +618,8 @@ impl AgentModelSelector for NativeAgentConnection { return Task::ready(Err(anyhow!("Invalid model ID {}", model_id))); }; - thread.update(cx, |thread, _cx| { - thread.set_model(model.clone()); + thread.update(cx, |thread, cx| { + thread.set_model(model.clone(), cx); }); update_settings_file::<AgentSettings>( @@ -665,30 +679,14 @@ impl acp_thread::AgentConnection for NativeAgentConnection { cx.spawn(async move |cx| { log::debug!("Starting thread creation in async context"); - // Generate session ID - let session_id = acp::SessionId(uuid::Uuid::new_v4().to_string().into()); - log::info!("Created session with ID: {}", session_id); - - // Create AcpThread - let acp_thread = cx.update(|cx| { - cx.new(|cx| { - acp_thread::AcpThread::new( - "agent2", - self.clone(), - project.clone(), - session_id.clone(), - cx, - ) - }) - })?; - let action_log = cx.update(|cx| acp_thread.read(cx).action_log().clone())?; - + let action_log = cx.new(|_cx| ActionLog::new(project.clone()))?; // Create Thread let thread = agent.update( cx, |agent, cx: &mut gpui::Context<NativeAgent>| -> Result<_> { // Fetch default model from registry settings let registry = LanguageModelRegistry::read_global(cx); + let language_registry = project.read(cx).languages().clone(); // Log available models for debugging let available_count = registry.available_models(cx).count(); @@ -699,6 +697,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .models .model_from_id(&LanguageModels::model_id(&default_model.model)) }); + let summarization_model = registry.thread_summary_model().map(|c| c.model); let thread = cx.new(|cx| { let mut thread = Thread::new( @@ -708,13 +707,14 @@ impl acp_thread::AgentConnection for NativeAgentConnection { action_log.clone(), agent.templates.clone(), default_model, + summarization_model, cx, ); thread.add_tool(CopyPathTool::new(project.clone())); thread.add_tool(CreateDirectoryTool::new(project.clone())); thread.add_tool(DeletePathTool::new(project.clone(), action_log.clone())); thread.add_tool(DiagnosticsTool::new(project.clone())); - thread.add_tool(EditFileTool::new(cx.entity())); + thread.add_tool(EditFileTool::new(cx.weak_entity(), language_registry)); thread.add_tool(FetchTool::new(project.read(cx).client().http_client())); thread.add_tool(FindPathTool::new(project.clone())); thread.add_tool(GrepTool::new(project.clone())); @@ -722,7 +722,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { thread.add_tool(MovePathTool::new(project.clone())); thread.add_tool(NowTool); thread.add_tool(OpenTool::new(project.clone())); - thread.add_tool(ReadFileTool::new(project.clone(), action_log)); + thread.add_tool(ReadFileTool::new(project.clone(), action_log.clone())); thread.add_tool(TerminalTool::new(project.clone(), cx)); thread.add_tool(ThinkingTool); thread.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model. @@ -733,6 +733,21 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }, )??; + let session_id = thread.read_with(cx, |thread, _| thread.id().clone())?; + log::info!("Created session with ID: {}", session_id); + // Create AcpThread + let acp_thread = cx.update(|cx| { + cx.new(|_cx| { + acp_thread::AcpThread::new( + "agent2", + self.clone(), + project.clone(), + action_log.clone(), + session_id.clone(), + ) + }) + })?; + // Store the session agent.update(cx, |agent, cx| { agent.sessions.insert( @@ -803,7 +818,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { log::info!("Cancelling on session: {}", session_id); self.0.update(cx, |agent, cx| { if let Some(agent) = agent.sessions.get(session_id) { - agent.thread.update(cx, |thread, _cx| thread.cancel()); + agent.thread.update(cx, |thread, cx| thread.cancel(cx)); } }); } @@ -830,7 +845,10 @@ struct NativeAgentSessionEditor(Entity<Thread>); impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor { fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> { - Task::ready(self.0.update(cx, |thread, _cx| thread.truncate(message_id))) + Task::ready( + self.0 + .update(cx, |thread, cx| thread.truncate(message_id, cx)), + ) } } diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index c83479f2cf8e0378ef10a36662206b8ac61b2d87..33706b05dea6b52b044faf8fd6833a7e0f8119f2 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -345,7 +345,7 @@ async fn test_streaming_tool_calls(cx: &mut TestAppContext) { let mut saw_partial_tool_use = false; while let Some(event) = events.next().await { - if let Ok(AgentResponseEvent::ToolCall(tool_call)) = event { + if let Ok(ThreadEvent::ToolCall(tool_call)) = event { thread.update(cx, |thread, _cx| { // Look for a tool use in the thread's last message let message = thread.last_message().unwrap(); @@ -735,16 +735,14 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) { ); } -async fn expect_tool_call( - events: &mut UnboundedReceiver<Result<AgentResponseEvent>>, -) -> acp::ToolCall { +async fn expect_tool_call(events: &mut UnboundedReceiver<Result<ThreadEvent>>) -> acp::ToolCall { let event = events .next() .await .expect("no tool call authorization event received") .unwrap(); match event { - AgentResponseEvent::ToolCall(tool_call) => return tool_call, + ThreadEvent::ToolCall(tool_call) => return tool_call, event => { panic!("Unexpected event {event:?}"); } @@ -752,7 +750,7 @@ async fn expect_tool_call( } async fn expect_tool_call_update_fields( - events: &mut UnboundedReceiver<Result<AgentResponseEvent>>, + events: &mut UnboundedReceiver<Result<ThreadEvent>>, ) -> acp::ToolCallUpdate { let event = events .next() @@ -760,7 +758,7 @@ async fn expect_tool_call_update_fields( .expect("no tool call authorization event received") .unwrap(); match event { - AgentResponseEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => { + ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => { return update; } event => { @@ -770,7 +768,7 @@ async fn expect_tool_call_update_fields( } async fn next_tool_call_authorization( - events: &mut UnboundedReceiver<Result<AgentResponseEvent>>, + events: &mut UnboundedReceiver<Result<ThreadEvent>>, ) -> ToolCallAuthorization { loop { let event = events @@ -778,7 +776,7 @@ async fn next_tool_call_authorization( .await .expect("no tool call authorization event received") .unwrap(); - if let AgentResponseEvent::ToolCallAuthorization(tool_call_authorization) = event { + if let ThreadEvent::ToolCallAuthorization(tool_call_authorization) = event { let permission_kinds = tool_call_authorization .options .iter() @@ -945,13 +943,13 @@ async fn test_cancellation(cx: &mut TestAppContext) { let mut echo_completed = false; while let Some(event) = events.next().await { match event.unwrap() { - AgentResponseEvent::ToolCall(tool_call) => { + ThreadEvent::ToolCall(tool_call) => { assert_eq!(tool_call.title, expected_tools.remove(0)); if tool_call.title == "Echo" { echo_id = Some(tool_call.id); } } - AgentResponseEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( + ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( acp::ToolCallUpdate { id, fields: @@ -973,13 +971,13 @@ async fn test_cancellation(cx: &mut TestAppContext) { // Cancel the current send and ensure that the event stream is closed, even // if one of the tools is still running. - thread.update(cx, |thread, _cx| thread.cancel()); + thread.update(cx, |thread, cx| thread.cancel(cx)); let events = events.collect::<Vec<_>>().await; let last_event = events.last(); assert!( matches!( last_event, - Some(Ok(AgentResponseEvent::Stop(acp::StopReason::Canceled))) + Some(Ok(ThreadEvent::Stop(acp::StopReason::Canceled))) ), "unexpected event {last_event:?}" ); @@ -1161,7 +1159,7 @@ async fn test_truncate(cx: &mut TestAppContext) { }); thread - .update(cx, |thread, _cx| thread.truncate(message_id)) + .update(cx, |thread, cx| thread.truncate(message_id, cx)) .unwrap(); cx.run_until_parked(); thread.read_with(cx, |thread, _| { @@ -1203,6 +1201,51 @@ async fn test_truncate(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_title_generation(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let summary_model = Arc::new(FakeLanguageModel::default()); + thread.update(cx, |thread, cx| { + thread.set_summarization_model(Some(summary_model.clone()), cx) + }); + + let send = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + fake_model.send_last_completion_stream_text_chunk("Hey!"); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "New Thread")); + + // Ensure the summary model has been invoked to generate a title. + summary_model.send_last_completion_stream_text_chunk("Hello "); + summary_model.send_last_completion_stream_text_chunk("world\nG"); + summary_model.send_last_completion_stream_text_chunk("oodnight Moon"); + summary_model.end_last_completion_stream(); + send.collect::<Vec<_>>().await; + thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); + + // Send another message, ensuring no title is generated this time. + let send = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello again"], cx) + }) + .unwrap(); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hey again!"); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + assert_eq!(summary_model.pending_completions(), Vec::new()); + send.collect::<Vec<_>>().await; + thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); +} + #[gpui::test] async fn test_agent_connection(cx: &mut TestAppContext) { cx.update(settings::init); @@ -1442,7 +1485,7 @@ async fn test_send_no_retry_on_success(cx: &mut TestAppContext) { let mut events = thread .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn); + thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); thread.send(UserMessageId::new(), ["Hello!"], cx) }) .unwrap(); @@ -1454,10 +1497,10 @@ async fn test_send_no_retry_on_success(cx: &mut TestAppContext) { let mut retry_events = Vec::new(); while let Some(Ok(event)) = events.next().await { match event { - AgentResponseEvent::Retry(retry_status) => { + ThreadEvent::Retry(retry_status) => { retry_events.push(retry_status); } - AgentResponseEvent::Stop(..) => break, + ThreadEvent::Stop(..) => break, _ => {} } } @@ -1486,7 +1529,7 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { let mut events = thread .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn); + thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); thread.send(UserMessageId::new(), ["Hello!"], cx) }) .unwrap(); @@ -1507,10 +1550,10 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { let mut retry_events = Vec::new(); while let Some(Ok(event)) = events.next().await { match event { - AgentResponseEvent::Retry(retry_status) => { + ThreadEvent::Retry(retry_status) => { retry_events.push(retry_status); } - AgentResponseEvent::Stop(..) => break, + ThreadEvent::Stop(..) => break, _ => {} } } @@ -1543,7 +1586,7 @@ async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { let mut events = thread .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn); + thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); thread.send(UserMessageId::new(), ["Hello!"], cx) }) .unwrap(); @@ -1565,10 +1608,10 @@ async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { let mut retry_events = Vec::new(); while let Some(event) = events.next().await { match event { - Ok(AgentResponseEvent::Retry(retry_status)) => { + Ok(ThreadEvent::Retry(retry_status)) => { retry_events.push(retry_status); } - Ok(AgentResponseEvent::Stop(..)) => break, + Ok(ThreadEvent::Stop(..)) => break, Err(error) => errors.push(error), _ => {} } @@ -1592,11 +1635,11 @@ async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { } /// Filters out the stop events for asserting against in tests -fn stop_events(result_events: Vec<Result<AgentResponseEvent>>) -> Vec<acp::StopReason> { +fn stop_events(result_events: Vec<Result<ThreadEvent>>) -> Vec<acp::StopReason> { result_events .into_iter() .filter_map(|event| match event.unwrap() { - AgentResponseEvent::Stop(stop_reason) => Some(stop_reason), + ThreadEvent::Stop(stop_reason) => Some(stop_reason), _ => None, }) .collect() @@ -1713,6 +1756,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { action_log, templates, Some(model.clone()), + None, cx, ) }); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 856e70ce593e91bb48c652c61701977e969b88b7..aeb600e2328332335dca6f4a59b58f65a9ca91f9 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,25 +1,34 @@ use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates}; use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; +use agent::thread::{DetailedSummaryState, GitState, ProjectSnapshot, WorktreeSnapshot}; use agent_client_protocol as acp; -use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; +use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT}; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; +use chrono::{DateTime, Utc}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus}; use collections::IndexMap; use fs::Fs; use futures::{ + FutureExt, channel::{mpsc, oneshot}, + future::Shared, stream::FuturesUnordered, }; +use git::repository::DiffType; use gpui::{App, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, LanguageModelProviderId, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, + TokenUsage, +}; +use project::{ + Project, + git_store::{GitStore, RepositoryState}, }; -use project::Project; use prompt_store::ProjectContext; use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; @@ -35,28 +44,7 @@ use std::{fmt::Write, ops::Range}; use util::{ResultExt, markdown::MarkdownCodeBlock}; use uuid::Uuid; -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema, -)] -pub struct ThreadId(Arc<str>); - -impl ThreadId { - pub fn new() -> Self { - Self(Uuid::new_v4().to_string().into()) - } -} - -impl std::fmt::Display for ThreadId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From<&str> for ThreadId { - fn from(value: &str) -> Self { - Self(value.into()) - } -} +const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user"; /// The ID of the user prompt that initiated a request. /// @@ -91,7 +79,7 @@ enum RetryStrategy { }, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Message { User(UserMessage), Agent(AgentMessage), @@ -106,6 +94,18 @@ impl Message { } } + pub fn to_request(&self) -> Vec<LanguageModelRequestMessage> { + match self { + Message::User(message) => vec![message.to_request()], + Message::Agent(message) => message.to_request(), + Message::Resume => vec![LanguageModelRequestMessage { + role: Role::User, + content: vec!["Continue where you left off".into()], + cache: false, + }], + } + } + pub fn to_markdown(&self) -> String { match self { Message::User(message) => message.to_markdown(), @@ -113,15 +113,22 @@ impl Message { Message::Resume => "[resumed after tool use limit was reached]".into(), } } + + pub fn role(&self) -> Role { + match self { + Message::User(_) | Message::Resume => Role::User, + Message::Agent(_) => Role::Assistant, + } + } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct UserMessage { pub id: UserMessageId, pub content: Vec<UserMessageContent>, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum UserMessageContent { Text(String), Mention { uri: MentionUri, content: String }, @@ -345,9 +352,6 @@ impl AgentMessage { AgentMessageContent::RedactedThinking(_) => { markdown.push_str("<redacted_thinking />\n") } - AgentMessageContent::Image(_) => { - markdown.push_str("<image />\n"); - } AgentMessageContent::ToolUse(tool_use) => { markdown.push_str(&format!( "**Tool Use**: {} (ID: {})\n", @@ -418,9 +422,6 @@ impl AgentMessage { AgentMessageContent::ToolUse(value) => { language_model::MessageContent::ToolUse(value.clone()) } - AgentMessageContent::Image(value) => { - language_model::MessageContent::Image(value.clone()) - } }; assistant_message.content.push(chunk); } @@ -450,13 +451,13 @@ impl AgentMessage { } } -#[derive(Default, Debug, Clone, PartialEq, Eq)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AgentMessage { pub content: Vec<AgentMessageContent>, pub tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum AgentMessageContent { Text(String), Thinking { @@ -464,17 +465,18 @@ pub enum AgentMessageContent { signature: Option<String>, }, RedactedThinking(String), - Image(LanguageModelImage), ToolUse(LanguageModelToolUse), } #[derive(Debug)] -pub enum AgentResponseEvent { - Text(String), - Thinking(String), +pub enum ThreadEvent { + UserMessage(UserMessage), + AgentText(String), + AgentThinking(String), ToolCall(acp::ToolCall), ToolCallUpdate(acp_thread::ToolCallUpdate), ToolCallAuthorization(ToolCallAuthorization), + TitleUpdate(SharedString), Retry(acp_thread::RetryStatus), Stop(acp::StopReason), } @@ -487,8 +489,12 @@ pub struct ToolCallAuthorization { } pub struct Thread { - id: ThreadId, + id: acp::SessionId, prompt_id: PromptId, + updated_at: DateTime<Utc>, + title: Option<SharedString>, + #[allow(unused)] + summary: DetailedSummaryState, messages: Vec<Message>, completion_mode: CompletionMode, /// Holds the task that handles agent interaction until the end of the turn. @@ -498,11 +504,18 @@ pub struct Thread { pending_message: Option<AgentMessage>, tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>, tool_use_limit_reached: bool, + #[allow(unused)] + request_token_usage: Vec<TokenUsage>, + #[allow(unused)] + cumulative_token_usage: TokenUsage, + #[allow(unused)] + initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>, context_server_registry: Entity<ContextServerRegistry>, profile_id: AgentProfileId, project_context: Entity<ProjectContext>, templates: Arc<Templates>, model: Option<Arc<dyn LanguageModel>>, + summarization_model: Option<Arc<dyn LanguageModel>>, project: Entity<Project>, action_log: Entity<ActionLog>, } @@ -515,36 +528,254 @@ impl Thread { action_log: Entity<ActionLog>, templates: Arc<Templates>, model: Option<Arc<dyn LanguageModel>>, + summarization_model: Option<Arc<dyn LanguageModel>>, cx: &mut Context<Self>, ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); Self { - id: ThreadId::new(), + id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()), prompt_id: PromptId::new(), + updated_at: Utc::now(), + title: None, + summary: DetailedSummaryState::default(), messages: Vec::new(), completion_mode: AgentSettings::get_global(cx).preferred_completion_mode, running_turn: None, pending_message: None, tools: BTreeMap::default(), tool_use_limit_reached: false, + request_token_usage: Vec::new(), + cumulative_token_usage: TokenUsage::default(), + initial_project_snapshot: { + let project_snapshot = Self::project_snapshot(project.clone(), cx); + cx.foreground_executor() + .spawn(async move { Some(project_snapshot.await) }) + .shared() + }, context_server_registry, profile_id, project_context, templates, model, + summarization_model, project, action_log, } } - pub fn project(&self) -> &Entity<Project> { - &self.project + pub fn id(&self) -> &acp::SessionId { + &self.id + } + + pub fn replay( + &mut self, + cx: &mut Context<Self>, + ) -> mpsc::UnboundedReceiver<Result<ThreadEvent>> { + let (tx, rx) = mpsc::unbounded(); + let stream = ThreadEventStream(tx); + for message in &self.messages { + match message { + Message::User(user_message) => stream.send_user_message(user_message), + Message::Agent(assistant_message) => { + for content in &assistant_message.content { + match content { + AgentMessageContent::Text(text) => stream.send_text(text), + AgentMessageContent::Thinking { text, .. } => { + stream.send_thinking(text) + } + AgentMessageContent::RedactedThinking(_) => {} + AgentMessageContent::ToolUse(tool_use) => { + self.replay_tool_call( + tool_use, + assistant_message.tool_results.get(&tool_use.id), + &stream, + cx, + ); + } + } + } + } + Message::Resume => {} + } + } + rx + } + + fn replay_tool_call( + &self, + tool_use: &LanguageModelToolUse, + tool_result: Option<&LanguageModelToolResult>, + stream: &ThreadEventStream, + cx: &mut Context<Self>, + ) { + let Some(tool) = self.tools.get(tool_use.name.as_ref()) else { + stream + .0 + .unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall { + id: acp::ToolCallId(tool_use.id.to_string().into()), + title: tool_use.name.to_string(), + kind: acp::ToolKind::Other, + status: acp::ToolCallStatus::Failed, + content: Vec::new(), + locations: Vec::new(), + raw_input: Some(tool_use.input.clone()), + raw_output: None, + }))) + .ok(); + return; + }; + + let title = tool.initial_title(tool_use.input.clone()); + let kind = tool.kind(); + stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone()); + + let output = tool_result + .as_ref() + .and_then(|result| result.output.clone()); + if let Some(output) = output.clone() { + let tool_event_stream = ToolCallEventStream::new( + tool_use.id.clone(), + stream.clone(), + Some(self.project.read(cx).fs().clone()), + ); + tool.replay(tool_use.input.clone(), output, tool_event_stream, cx) + .log_err(); + } + + stream.update_tool_call_fields( + &tool_use.id, + acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::Completed), + raw_output: output, + ..Default::default() + }, + ); + } + + /// Create a snapshot of the current project state including git information and unsaved buffers. + fn project_snapshot( + project: Entity<Project>, + cx: &mut Context<Self>, + ) -> Task<Arc<agent::thread::ProjectSnapshot>> { + let git_store = project.read(cx).git_store().clone(); + let worktree_snapshots: Vec<_> = project + .read(cx) + .visible_worktrees(cx) + .map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx)) + .collect(); + + cx.spawn(async move |_, cx| { + let worktree_snapshots = futures::future::join_all(worktree_snapshots).await; + + let mut unsaved_buffers = Vec::new(); + cx.update(|app_cx| { + let buffer_store = project.read(app_cx).buffer_store(); + for buffer_handle in buffer_store.read(app_cx).buffers() { + let buffer = buffer_handle.read(app_cx); + if buffer.is_dirty() + && let Some(file) = buffer.file() + { + let path = file.path().to_string_lossy().to_string(); + unsaved_buffers.push(path); + } + } + }) + .ok(); + + Arc::new(ProjectSnapshot { + worktree_snapshots, + unsaved_buffer_paths: unsaved_buffers, + timestamp: Utc::now(), + }) + }) + } + + fn worktree_snapshot( + worktree: Entity<project::Worktree>, + git_store: Entity<GitStore>, + cx: &App, + ) -> Task<agent::thread::WorktreeSnapshot> { + cx.spawn(async move |cx| { + // Get worktree path and snapshot + let worktree_info = cx.update(|app_cx| { + let worktree = worktree.read(app_cx); + let path = worktree.abs_path().to_string_lossy().to_string(); + let snapshot = worktree.snapshot(); + (path, snapshot) + }); + + let Ok((worktree_path, _snapshot)) = worktree_info else { + return WorktreeSnapshot { + worktree_path: String::new(), + git_state: None, + }; + }; + + let git_state = git_store + .update(cx, |git_store, cx| { + git_store + .repositories() + .values() + .find(|repo| { + repo.read(cx) + .abs_path_to_repo_path(&worktree.read(cx).abs_path()) + .is_some() + }) + .cloned() + }) + .ok() + .flatten() + .map(|repo| { + repo.update(cx, |repo, _| { + let current_branch = + repo.branch.as_ref().map(|branch| branch.name().to_owned()); + repo.send_job(None, |state, _| async move { + let RepositoryState::Local { backend, .. } = state else { + return GitState { + remote_url: None, + head_sha: None, + current_branch, + diff: None, + }; + }; + + let remote_url = backend.remote_url("origin"); + let head_sha = backend.head_sha().await; + let diff = backend.diff(DiffType::HeadToWorktree).await.ok(); + + GitState { + remote_url, + head_sha, + current_branch, + diff, + } + }) + }) + }); + + let git_state = match git_state { + Some(git_state) => match git_state.ok() { + Some(git_state) => git_state.await.ok(), + None => None, + }, + None => None, + }; + + WorktreeSnapshot { + worktree_path, + git_state, + } + }) } pub fn project_context(&self) -> &Entity<ProjectContext> { &self.project_context } + pub fn project(&self) -> &Entity<Project> { + &self.project + } + pub fn action_log(&self) -> &Entity<ActionLog> { &self.action_log } @@ -553,16 +784,27 @@ impl Thread { self.model.as_ref() } - pub fn set_model(&mut self, model: Arc<dyn LanguageModel>) { + pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) { self.model = Some(model); + cx.notify() + } + + pub fn set_summarization_model( + &mut self, + model: Option<Arc<dyn LanguageModel>>, + cx: &mut Context<Self>, + ) { + self.summarization_model = model; + cx.notify() } pub fn completion_mode(&self) -> CompletionMode { self.completion_mode } - pub fn set_completion_mode(&mut self, mode: CompletionMode) { + pub fn set_completion_mode(&mut self, mode: CompletionMode, cx: &mut Context<Self>) { self.completion_mode = mode; + cx.notify() } #[cfg(any(test, feature = "test-support"))] @@ -590,29 +832,29 @@ impl Thread { self.profile_id = profile_id; } - pub fn cancel(&mut self) { + pub fn cancel(&mut self, cx: &mut Context<Self>) { if let Some(running_turn) = self.running_turn.take() { running_turn.cancel(); } - self.flush_pending_message(); + self.flush_pending_message(cx); } - pub fn truncate(&mut self, message_id: UserMessageId) -> Result<()> { - self.cancel(); + pub fn truncate(&mut self, message_id: UserMessageId, cx: &mut Context<Self>) -> Result<()> { + self.cancel(cx); let Some(position) = self.messages.iter().position( |msg| matches!(msg, Message::User(UserMessage { id, .. }) if id == &message_id), ) else { return Err(anyhow!("Message not found")); }; self.messages.truncate(position); + cx.notify(); Ok(()) } pub fn resume( &mut self, cx: &mut Context<Self>, - ) -> Result<mpsc::UnboundedReceiver<Result<AgentResponseEvent>>> { - anyhow::ensure!(self.model.is_some(), "Model not set"); + ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> { anyhow::ensure!( self.tool_use_limit_reached, "can only resume after tool use limit is reached" @@ -633,7 +875,7 @@ impl Thread { id: UserMessageId, content: impl IntoIterator<Item = T>, cx: &mut Context<Self>, - ) -> Result<mpsc::UnboundedReceiver<Result<AgentResponseEvent>>> + ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> where T: Into<UserMessageContent>, { @@ -656,22 +898,19 @@ impl Thread { fn run_turn( &mut self, cx: &mut Context<Self>, - ) -> Result<mpsc::UnboundedReceiver<Result<AgentResponseEvent>>> { - self.cancel(); - - let model = self - .model() - .cloned() - .context("No language model configured")?; - let (events_tx, events_rx) = mpsc::unbounded::<Result<AgentResponseEvent>>(); - let event_stream = AgentResponseEventStream(events_tx); + ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> { + self.cancel(cx); + + let model = self.model.clone().context("No language model configured")?; + let (events_tx, events_rx) = mpsc::unbounded::<Result<ThreadEvent>>(); + let event_stream = ThreadEventStream(events_tx); let message_ix = self.messages.len().saturating_sub(1); self.tool_use_limit_reached = false; self.running_turn = Some(RunningTurn { event_stream: event_stream.clone(), _task: cx.spawn(async move |this, cx| { log::info!("Starting agent turn execution"); - let turn_result: Result<()> = async { + let turn_result: Result<StopReason> = async { let mut completion_intent = CompletionIntent::UserPrompt; loop { log::debug!( @@ -685,18 +924,27 @@ impl Thread { log::info!("Calling model.stream_completion"); let mut tool_use_limit_reached = false; + let mut refused = false; + let mut reached_max_tokens = false; let mut tool_uses = Self::stream_completion_with_retries( this.clone(), model.clone(), request, - message_ix, &event_stream, &mut tool_use_limit_reached, + &mut refused, + &mut reached_max_tokens, cx, ) .await?; - let used_tools = tool_uses.is_empty(); + if refused { + return Ok(StopReason::Refusal); + } else if reached_max_tokens { + return Ok(StopReason::MaxTokens); + } + + let end_turn = tool_uses.is_empty(); while let Some(tool_result) = tool_uses.next().await { log::info!("Tool finished {:?}", tool_result); @@ -724,29 +972,42 @@ impl Thread { log::info!("Tool use limit reached, completing turn"); this.update(cx, |this, _cx| this.tool_use_limit_reached = true)?; return Err(language_model::ToolUseLimitReachedError.into()); - } else if used_tools { + } else if end_turn { log::info!("No tool uses found, completing turn"); - return Ok(()); + return Ok(StopReason::EndTurn); } else { - this.update(cx, |this, _| this.flush_pending_message())?; + this.update(cx, |this, cx| this.flush_pending_message(cx))?; completion_intent = CompletionIntent::ToolResults; } } } .await; + _ = this.update(cx, |this, cx| this.flush_pending_message(cx)); + + match turn_result { + Ok(reason) => { + log::info!("Turn execution completed: {:?}", reason); + + let update_title = this + .update(cx, |this, cx| this.update_title(&event_stream, cx)) + .ok() + .flatten(); + if let Some(update_title) = update_title { + update_title.await.context("update title failed").log_err(); + } - if let Err(error) = turn_result { - log::error!("Turn execution failed: {:?}", error); - event_stream.send_error(error); - } else { - log::info!("Turn execution completed successfully"); + event_stream.send_stop(reason); + if reason == StopReason::Refusal { + _ = this.update(cx, |this, _| this.messages.truncate(message_ix)); + } + } + Err(error) => { + log::error!("Turn execution failed: {:?}", error); + event_stream.send_error(error); + } } - this.update(cx, |this, _| { - this.flush_pending_message(); - this.running_turn.take(); - }) - .ok(); + _ = this.update(cx, |this, _| this.running_turn.take()); }), }); Ok(events_rx) @@ -756,9 +1017,10 @@ impl Thread { this: WeakEntity<Self>, model: Arc<dyn LanguageModel>, request: LanguageModelRequest, - message_ix: usize, - event_stream: &AgentResponseEventStream, + event_stream: &ThreadEventStream, tool_use_limit_reached: &mut bool, + refusal: &mut bool, + max_tokens_reached: &mut bool, cx: &mut AsyncApp, ) -> Result<FuturesUnordered<Task<LanguageModelToolResult>>> { log::debug!("Stream completion started successfully"); @@ -774,16 +1036,17 @@ impl Thread { )) => { *tool_use_limit_reached = true; } - Ok(LanguageModelCompletionEvent::Stop(reason)) => { - event_stream.send_stop(reason); - if reason == StopReason::Refusal { - this.update(cx, |this, _cx| { - this.flush_pending_message(); - this.messages.truncate(message_ix); - })?; - return Ok(tool_uses); - } + Ok(LanguageModelCompletionEvent::Stop(StopReason::Refusal)) => { + *refusal = true; + return Ok(FuturesUnordered::default()); + } + Ok(LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)) => { + *max_tokens_reached = true; + return Ok(FuturesUnordered::default()); } + Ok(LanguageModelCompletionEvent::Stop( + StopReason::ToolUse | StopReason::EndTurn, + )) => break, Ok(event) => { log::trace!("Received completion event: {:?}", event); this.update(cx, |this, cx| { @@ -843,6 +1106,7 @@ impl Thread { } } } + return Ok(tool_uses); } } @@ -870,7 +1134,7 @@ impl Thread { fn handle_streamed_completion_event( &mut self, event: LanguageModelCompletionEvent, - event_stream: &AgentResponseEventStream, + event_stream: &ThreadEventStream, cx: &mut Context<Self>, ) -> Option<Task<LanguageModelToolResult>> { log::trace!("Handling streamed completion event: {:?}", event); @@ -878,7 +1142,7 @@ impl Thread { match event { StartMessage { .. } => { - self.flush_pending_message(); + self.flush_pending_message(cx); self.pending_message = Some(AgentMessage::default()); } Text(new_text) => self.handle_text_event(new_text, event_stream, cx), @@ -912,7 +1176,7 @@ impl Thread { fn handle_text_event( &mut self, new_text: String, - event_stream: &AgentResponseEventStream, + event_stream: &ThreadEventStream, cx: &mut Context<Self>, ) { event_stream.send_text(&new_text); @@ -933,7 +1197,7 @@ impl Thread { &mut self, new_text: String, new_signature: Option<String>, - event_stream: &AgentResponseEventStream, + event_stream: &ThreadEventStream, cx: &mut Context<Self>, ) { event_stream.send_thinking(&new_text); @@ -965,7 +1229,7 @@ impl Thread { fn handle_tool_use_event( &mut self, tool_use: LanguageModelToolUse, - event_stream: &AgentResponseEventStream, + event_stream: &ThreadEventStream, cx: &mut Context<Self>, ) -> Option<Task<LanguageModelToolResult>> { cx.notify(); @@ -1083,11 +1347,85 @@ impl Thread { } } + pub fn title(&self) -> SharedString { + self.title.clone().unwrap_or("New Thread".into()) + } + + fn update_title( + &mut self, + event_stream: &ThreadEventStream, + cx: &mut Context<Self>, + ) -> Option<Task<Result<()>>> { + if self.title.is_some() { + log::debug!("Skipping title generation because we already have one."); + return None; + } + + log::info!( + "Generating title with model: {:?}", + self.summarization_model.as_ref().map(|model| model.name()) + ); + let model = self.summarization_model.clone()?; + let event_stream = event_stream.clone(); + let mut request = LanguageModelRequest { + intent: Some(CompletionIntent::ThreadSummarization), + temperature: AgentSettings::temperature_for_model(&model, cx), + ..Default::default() + }; + + for message in &self.messages { + request.messages.extend(message.to_request()); + } + + request.messages.push(LanguageModelRequestMessage { + role: Role::User, + content: vec![SUMMARIZE_THREAD_PROMPT.into()], + cache: false, + }); + Some(cx.spawn(async move |this, cx| { + let mut title = String::new(); + let mut messages = model.stream_completion(request, cx).await?; + while let Some(event) = messages.next().await { + let event = event?; + let text = match event { + LanguageModelCompletionEvent::Text(text) => text, + LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::UsageUpdated { .. }, + ) => { + // this.update(cx, |thread, cx| { + // thread.update_model_request_usage(amount as u32, limit, cx); + // })?; + // TODO: handle usage update + continue; + } + _ => continue, + }; + + let mut lines = text.lines(); + title.extend(lines.next()); + + // Stop if the LLM generated multiple lines. + if lines.next().is_some() { + break; + } + } + + log::info!("Setting title: {}", title); + + this.update(cx, |this, cx| { + let title = SharedString::from(title); + event_stream.send_title_update(title.clone()); + this.title = Some(title); + cx.notify(); + }) + })) + } + fn pending_message(&mut self) -> &mut AgentMessage { self.pending_message.get_or_insert_default() } - fn flush_pending_message(&mut self) { + fn flush_pending_message(&mut self, cx: &mut Context<Self>) { let Some(mut message) = self.pending_message.take() else { return; }; @@ -1104,9 +1442,7 @@ impl Thread { tool_use_id: tool_use.id.clone(), tool_name: tool_use.name.clone(), is_error: true, - content: LanguageModelToolResultContent::Text( - "Tool canceled by user".into(), - ), + content: LanguageModelToolResultContent::Text(TOOL_CANCELED_MESSAGE.into()), output: None, }, ); @@ -1114,6 +1450,8 @@ impl Thread { } self.messages.push(Message::Agent(message)); + self.updated_at = Utc::now(); + cx.notify() } pub(crate) fn build_completion_request( @@ -1205,15 +1543,7 @@ impl Thread { ); let mut messages = vec![self.build_system_message(cx)]; for message in &self.messages { - match message { - Message::User(message) => messages.push(message.to_request()), - Message::Agent(message) => messages.extend(message.to_request()), - Message::Resume => messages.push(LanguageModelRequestMessage { - role: Role::User, - content: vec!["Continue where you left off".into()], - cache: false, - }), - } + messages.extend(message.to_request()); } if let Some(message) = self.pending_message.as_ref() { @@ -1367,7 +1697,7 @@ struct RunningTurn { _task: Task<()>, /// The current event stream for the running turn. Used to report a final /// cancellation event if we cancel the turn. - event_stream: AgentResponseEventStream, + event_stream: ThreadEventStream, } impl RunningTurn { @@ -1420,6 +1750,17 @@ where cx: &mut App, ) -> Task<Result<Self::Output>>; + /// Emits events for a previous execution of the tool. + fn replay( + &self, + _input: Self::Input, + _output: Self::Output, + _event_stream: ToolCallEventStream, + _cx: &mut App, + ) -> Result<()> { + Ok(()) + } + fn erase(self) -> Arc<dyn AnyAgentTool> { Arc::new(Erased(Arc::new(self))) } @@ -1447,6 +1788,13 @@ pub trait AnyAgentTool { event_stream: ToolCallEventStream, cx: &mut App, ) -> Task<Result<AgentToolOutput>>; + fn replay( + &self, + input: serde_json::Value, + output: serde_json::Value, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Result<()>; } impl<T> AnyAgentTool for Erased<Arc<T>> @@ -1498,21 +1846,45 @@ where }) }) } + + fn replay( + &self, + input: serde_json::Value, + output: serde_json::Value, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Result<()> { + let input = serde_json::from_value(input)?; + let output = serde_json::from_value(output)?; + self.0.replay(input, output, event_stream, cx) + } } #[derive(Clone)] -struct AgentResponseEventStream(mpsc::UnboundedSender<Result<AgentResponseEvent>>); +struct ThreadEventStream(mpsc::UnboundedSender<Result<ThreadEvent>>); + +impl ThreadEventStream { + fn send_title_update(&self, text: SharedString) { + self.0 + .unbounded_send(Ok(ThreadEvent::TitleUpdate(text))) + .ok(); + } + + fn send_user_message(&self, message: &UserMessage) { + self.0 + .unbounded_send(Ok(ThreadEvent::UserMessage(message.clone()))) + .ok(); + } -impl AgentResponseEventStream { fn send_text(&self, text: &str) { self.0 - .unbounded_send(Ok(AgentResponseEvent::Text(text.to_string()))) + .unbounded_send(Ok(ThreadEvent::AgentText(text.to_string()))) .ok(); } fn send_thinking(&self, text: &str) { self.0 - .unbounded_send(Ok(AgentResponseEvent::Thinking(text.to_string()))) + .unbounded_send(Ok(ThreadEvent::AgentThinking(text.to_string()))) .ok(); } @@ -1524,7 +1896,7 @@ impl AgentResponseEventStream { input: serde_json::Value, ) { self.0 - .unbounded_send(Ok(AgentResponseEvent::ToolCall(Self::initial_tool_call( + .unbounded_send(Ok(ThreadEvent::ToolCall(Self::initial_tool_call( id, title.to_string(), kind, @@ -1557,7 +1929,7 @@ impl AgentResponseEventStream { fields: acp::ToolCallUpdateFields, ) { self.0 - .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( + .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( acp::ToolCallUpdate { id: acp::ToolCallId(tool_use_id.to_string().into()), fields, @@ -1568,26 +1940,24 @@ impl AgentResponseEventStream { } fn send_retry(&self, status: acp_thread::RetryStatus) { - self.0 - .unbounded_send(Ok(AgentResponseEvent::Retry(status))) - .ok(); + self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); } fn send_stop(&self, reason: StopReason) { match reason { StopReason::EndTurn => { self.0 - .unbounded_send(Ok(AgentResponseEvent::Stop(acp::StopReason::EndTurn))) + .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::EndTurn))) .ok(); } StopReason::MaxTokens => { self.0 - .unbounded_send(Ok(AgentResponseEvent::Stop(acp::StopReason::MaxTokens))) + .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::MaxTokens))) .ok(); } StopReason::Refusal => { self.0 - .unbounded_send(Ok(AgentResponseEvent::Stop(acp::StopReason::Refusal))) + .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Refusal))) .ok(); } StopReason::ToolUse => {} @@ -1596,7 +1966,7 @@ impl AgentResponseEventStream { fn send_canceled(&self) { self.0 - .unbounded_send(Ok(AgentResponseEvent::Stop(acp::StopReason::Canceled))) + .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Canceled))) .ok(); } @@ -1608,24 +1978,23 @@ impl AgentResponseEventStream { #[derive(Clone)] pub struct ToolCallEventStream { tool_use_id: LanguageModelToolUseId, - stream: AgentResponseEventStream, + stream: ThreadEventStream, fs: Option<Arc<dyn Fs>>, } impl ToolCallEventStream { #[cfg(test)] pub fn test() -> (Self, ToolCallEventStreamReceiver) { - let (events_tx, events_rx) = mpsc::unbounded::<Result<AgentResponseEvent>>(); + let (events_tx, events_rx) = mpsc::unbounded::<Result<ThreadEvent>>(); - let stream = - ToolCallEventStream::new("test_id".into(), AgentResponseEventStream(events_tx), None); + let stream = ToolCallEventStream::new("test_id".into(), ThreadEventStream(events_tx), None); (stream, ToolCallEventStreamReceiver(events_rx)) } fn new( tool_use_id: LanguageModelToolUseId, - stream: AgentResponseEventStream, + stream: ThreadEventStream, fs: Option<Arc<dyn Fs>>, ) -> Self { Self { @@ -1643,7 +2012,7 @@ impl ToolCallEventStream { pub fn update_diff(&self, diff: Entity<acp_thread::Diff>) { self.stream .0 - .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( + .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( acp_thread::ToolCallUpdateDiff { id: acp::ToolCallId(self.tool_use_id.to_string().into()), diff, @@ -1656,7 +2025,7 @@ impl ToolCallEventStream { pub fn update_terminal(&self, terminal: Entity<acp_thread::Terminal>) { self.stream .0 - .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( + .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( acp_thread::ToolCallUpdateTerminal { id: acp::ToolCallId(self.tool_use_id.to_string().into()), terminal, @@ -1674,7 +2043,7 @@ impl ToolCallEventStream { let (response_tx, response_rx) = oneshot::channel(); self.stream .0 - .unbounded_send(Ok(AgentResponseEvent::ToolCallAuthorization( + .unbounded_send(Ok(ThreadEvent::ToolCallAuthorization( ToolCallAuthorization { tool_call: acp::ToolCallUpdate { id: acp::ToolCallId(self.tool_use_id.to_string().into()), @@ -1724,13 +2093,13 @@ impl ToolCallEventStream { } #[cfg(test)] -pub struct ToolCallEventStreamReceiver(mpsc::UnboundedReceiver<Result<AgentResponseEvent>>); +pub struct ToolCallEventStreamReceiver(mpsc::UnboundedReceiver<Result<ThreadEvent>>); #[cfg(test)] impl ToolCallEventStreamReceiver { pub async fn expect_authorization(&mut self) -> ToolCallAuthorization { let event = self.0.next().await; - if let Some(Ok(AgentResponseEvent::ToolCallAuthorization(auth))) = event { + if let Some(Ok(ThreadEvent::ToolCallAuthorization(auth))) = event { auth } else { panic!("Expected ToolCallAuthorization but got: {:?}", event); @@ -1739,9 +2108,9 @@ impl ToolCallEventStreamReceiver { pub async fn expect_terminal(&mut self) -> Entity<acp_thread::Terminal> { let event = self.0.next().await; - if let Some(Ok(AgentResponseEvent::ToolCallUpdate( - acp_thread::ToolCallUpdate::UpdateTerminal(update), - ))) = event + if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal( + update, + )))) = event { update.terminal } else { @@ -1752,7 +2121,7 @@ impl ToolCallEventStreamReceiver { #[cfg(test)] impl std::ops::Deref for ToolCallEventStreamReceiver { - type Target = mpsc::UnboundedReceiver<Result<AgentResponseEvent>>; + type Target = mpsc::UnboundedReceiver<Result<ThreadEvent>>; fn deref(&self) -> &Self::Target { &self.0 @@ -1821,6 +2190,38 @@ impl From<acp::ContentBlock> for UserMessageContent { } } +impl From<UserMessageContent> for acp::ContentBlock { + fn from(content: UserMessageContent) -> Self { + match content { + UserMessageContent::Text(text) => acp::ContentBlock::Text(acp::TextContent { + text, + annotations: None, + }), + UserMessageContent::Image(image) => acp::ContentBlock::Image(acp::ImageContent { + data: image.source.to_string(), + mime_type: "image/png".to_string(), + annotations: None, + uri: None, + }), + UserMessageContent::Mention { uri, content } => { + acp::ContentBlock::ResourceLink(acp::ResourceLink { + uri: uri.to_uri().to_string(), + name: uri.name(), + annotations: None, + description: if content.is_empty() { + None + } else { + Some(content) + }, + mime_type: None, + size: None, + title: None, + }) + } + } + } +} + fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage { LanguageModelImage { source: image_content.data.into(), diff --git a/crates/agent2/src/tools/context_server_registry.rs b/crates/agent2/src/tools/context_server_registry.rs index ddeb08a046124e698bc4931018f26dbe54efa0bc..69c4221a815eba4c9e5b04dfc97e04ef90bc62b7 100644 --- a/crates/agent2/src/tools/context_server_registry.rs +++ b/crates/agent2/src/tools/context_server_registry.rs @@ -228,4 +228,14 @@ impl AnyAgentTool for ContextServerTool { }) }) } + + fn replay( + &self, + _input: serde_json::Value, + _output: serde_json::Value, + _event_stream: ToolCallEventStream, + _cx: &mut App, + ) -> Result<()> { + Ok(()) + } } diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 7687d687026705d467d0f471275ace2717d12576..b3b1a428bff1b21744952148dc6196de62f8546c 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -5,10 +5,10 @@ use anyhow::{Context as _, Result, anyhow}; use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}; use cloud_llm_client::CompletionIntent; use collections::HashSet; -use gpui::{App, AppContext, AsyncApp, Entity, Task}; +use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use indoc::formatdoc; -use language::ToPoint; use language::language_settings::{self, FormatOnSave}; +use language::{LanguageRegistry, ToPoint}; use language_model::LanguageModelToolResultContent; use paths; use project::lsp_store::{FormatTrigger, LspFormatTarget}; @@ -98,11 +98,13 @@ pub enum EditFileMode { #[derive(Debug, Serialize, Deserialize)] pub struct EditFileToolOutput { + #[serde(alias = "original_path")] input_path: PathBuf, - project_path: PathBuf, new_text: String, old_text: Arc<String>, + #[serde(default)] diff: String, + #[serde(alias = "raw_output")] edit_agent_output: EditAgentOutput, } @@ -122,12 +124,16 @@ impl From<EditFileToolOutput> for LanguageModelToolResultContent { } pub struct EditFileTool { - thread: Entity<Thread>, + thread: WeakEntity<Thread>, + language_registry: Arc<LanguageRegistry>, } impl EditFileTool { - pub fn new(thread: Entity<Thread>) -> Self { - Self { thread } + pub fn new(thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>) -> Self { + Self { + thread, + language_registry, + } } fn authorize( @@ -167,8 +173,11 @@ impl EditFileTool { // Check if path is inside the global config directory // First check if it's already inside project - if not, try to canonicalize - let thread = self.thread.read(cx); - let project_path = thread.project().read(cx).find_project_path(&input.path, cx); + let Ok(project_path) = self.thread.read_with(cx, |thread, cx| { + thread.project().read(cx).find_project_path(&input.path, cx) + }) else { + return Task::ready(Err(anyhow!("thread was dropped"))); + }; // If the path is inside the project, and it's not one of the above edge cases, // then no confirmation is necessary. Otherwise, confirmation is necessary. @@ -221,7 +230,12 @@ impl AgentTool for EditFileTool { event_stream: ToolCallEventStream, cx: &mut App, ) -> Task<Result<Self::Output>> { - let project = self.thread.read(cx).project().clone(); + let Ok(project) = self + .thread + .read_with(cx, |thread, _cx| thread.project().clone()) + else { + return Task::ready(Err(anyhow!("thread was dropped"))); + }; let project_path = match resolve_path(&input, project.clone(), cx) { Ok(path) => path, Err(err) => return Task::ready(Err(anyhow!(err))), @@ -237,23 +251,17 @@ impl AgentTool for EditFileTool { }); } - let Some(request) = self.thread.update(cx, |thread, cx| { - thread - .build_completion_request(CompletionIntent::ToolResults, cx) - .ok() - }) else { - return Task::ready(Err(anyhow!("Failed to build completion request"))); - }; - let thread = self.thread.read(cx); - let Some(model) = thread.model().cloned() else { - return Task::ready(Err(anyhow!("No language model configured"))); - }; - let action_log = thread.action_log().clone(); - let authorize = self.authorize(&input, &event_stream, cx); cx.spawn(async move |cx: &mut AsyncApp| { authorize.await?; + let (request, model, action_log) = self.thread.update(cx, |thread, cx| { + let request = thread.build_completion_request(CompletionIntent::ToolResults, cx); + (request, thread.model().cloned(), thread.action_log().clone()) + })?; + let request = request?; + let model = model.context("No language model configured")?; + let edit_format = EditFormat::from_model(model.clone())?; let edit_agent = EditAgent::new( model, @@ -419,7 +427,6 @@ impl AgentTool for EditFileTool { Ok(EditFileToolOutput { input_path: input.path, - project_path: project_path.path.to_path_buf(), new_text: new_text.clone(), old_text, diff: unified_diff, @@ -427,6 +434,25 @@ impl AgentTool for EditFileTool { }) }) } + + fn replay( + &self, + _input: Self::Input, + output: Self::Output, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Result<()> { + event_stream.update_diff(cx.new(|cx| { + Diff::finalized( + output.input_path, + Some(output.old_text.to_string()), + output.new_text, + self.language_registry.clone(), + cx, + ) + })); + Ok(()) + } } /// Validate that the file path is valid, meaning: @@ -515,6 +541,7 @@ mod tests { let fs = project::FakeFs::new(cx.executor()); fs.insert_tree("/root", json!({})).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); @@ -527,6 +554,7 @@ mod tests { action_log, Templates::new(), Some(model), + None, cx, ) }); @@ -537,7 +565,11 @@ mod tests { path: "root/nonexistent_file.txt".into(), mode: EditFileMode::Edit, }; - Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx) + Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run( + input, + ToolCallEventStream::test().0, + cx, + ) }) .await; assert_eq!( @@ -724,6 +756,7 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); @@ -750,9 +783,10 @@ mod tests { path: "root/src/main.rs".into(), mode: EditFileMode::Overwrite, }; - Arc::new(EditFileTool { - thread: thread.clone(), - }) + Arc::new(EditFileTool::new( + thread.downgrade(), + language_registry.clone(), + )) .run(input, ToolCallEventStream::test().0, cx) }); @@ -806,7 +840,11 @@ mod tests { path: "root/src/main.rs".into(), mode: EditFileMode::Overwrite, }; - Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx) + Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run( + input, + ToolCallEventStream::test().0, + cx, + ) }); // Stream the unformatted content @@ -850,6 +888,7 @@ mod tests { let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { @@ -860,6 +899,7 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); @@ -887,9 +927,10 @@ mod tests { path: "root/src/main.rs".into(), mode: EditFileMode::Overwrite, }; - Arc::new(EditFileTool { - thread: thread.clone(), - }) + Arc::new(EditFileTool::new( + thread.downgrade(), + language_registry.clone(), + )) .run(input, ToolCallEventStream::test().0, cx) }); @@ -938,10 +979,11 @@ mod tests { path: "root/src/main.rs".into(), mode: EditFileMode::Overwrite, }; - Arc::new(EditFileTool { - thread: thread.clone(), - }) - .run(input, ToolCallEventStream::test().0, cx) + Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run( + input, + ToolCallEventStream::test().0, + cx, + ) }); // Stream the content with trailing whitespace @@ -976,6 +1018,7 @@ mod tests { let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { @@ -986,10 +1029,11 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); - let tool = Arc::new(EditFileTool { thread }); + let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); fs.insert_tree("/root", json!({})).await; // Test 1: Path with .zed component should require confirmation @@ -1111,6 +1155,7 @@ mod tests { let fs = project::FakeFs::new(cx.executor()); fs.insert_tree("/project", json!({})).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let action_log = cx.new(|_| ActionLog::new(project.clone())); @@ -1123,10 +1168,11 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); - let tool = Arc::new(EditFileTool { thread }); + let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); // Test global config paths - these should require confirmation if they exist and are outside the project let test_cases = vec![ @@ -1220,7 +1266,7 @@ mod tests { cx, ) .await; - + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); @@ -1233,10 +1279,11 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); - let tool = Arc::new(EditFileTool { thread }); + let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); // Test files in different worktrees let test_cases = vec![ @@ -1302,6 +1349,7 @@ mod tests { ) .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); @@ -1314,10 +1362,11 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); - let tool = Arc::new(EditFileTool { thread }); + let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); // Test edge cases let test_cases = vec![ @@ -1386,6 +1435,7 @@ mod tests { ) .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); @@ -1398,10 +1448,11 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); - let tool = Arc::new(EditFileTool { thread }); + let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); // Test different EditFileMode values let modes = vec![ @@ -1467,6 +1518,7 @@ mod tests { init_test(cx); let fs = project::FakeFs::new(cx.executor()); let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); @@ -1479,10 +1531,11 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); - let tool = Arc::new(EditFileTool { thread }); + let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); assert_eq!( tool.initial_title(Err(json!({ diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index ac79874c365eb42d3aaead5cb705ee4127389cd1..1804d0ab3047c41dfb9842a10ed1001346a468cb 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -319,7 +319,7 @@ mod tests { use theme::ThemeSettings; use util::test::TempTree; - use crate::AgentResponseEvent; + use crate::ThreadEvent; use super::*; @@ -396,7 +396,7 @@ mod tests { }); cx.run_until_parked(); let event = stream_rx.try_next(); - if let Ok(Some(Ok(AgentResponseEvent::ToolCallAuthorization(auth)))) = event { + if let Ok(Some(Ok(ThreadEvent::ToolCallAuthorization(auth)))) = event { auth.response.send(auth.options[0].id.clone()).unwrap(); } diff --git a/crates/agent2/src/tools/web_search_tool.rs b/crates/agent2/src/tools/web_search_tool.rs index c1c09707426431bf8a3ad4c59a012a567366d392..d71a128bfe4f70a95aa71d776b76bd4f5426800a 100644 --- a/crates/agent2/src/tools/web_search_tool.rs +++ b/crates/agent2/src/tools/web_search_tool.rs @@ -80,33 +80,48 @@ impl AgentTool for WebSearchTool { } }; - let result_text = if response.results.len() == 1 { - "1 result".to_string() - } else { - format!("{} results", response.results.len()) - }; - event_stream.update_fields(acp::ToolCallUpdateFields { - title: Some(format!("Searched the web: {result_text}")), - content: Some( - response - .results - .iter() - .map(|result| acp::ToolCallContent::Content { - content: acp::ContentBlock::ResourceLink(acp::ResourceLink { - name: result.title.clone(), - uri: result.url.clone(), - title: Some(result.title.clone()), - description: Some(result.text.clone()), - mime_type: None, - annotations: None, - size: None, - }), - }) - .collect(), - ), - ..Default::default() - }); + emit_update(&response, &event_stream); Ok(WebSearchToolOutput(response)) }) } + + fn replay( + &self, + _input: Self::Input, + output: Self::Output, + event_stream: ToolCallEventStream, + _cx: &mut App, + ) -> Result<()> { + emit_update(&output.0, &event_stream); + Ok(()) + } +} + +fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream) { + let result_text = if response.results.len() == 1 { + "1 result".to_string() + } else { + format!("{} results", response.results.len()) + }; + event_stream.update_fields(acp::ToolCallUpdateFields { + title: Some(format!("Searched the web: {result_text}")), + content: Some( + response + .results + .iter() + .map(|result| acp::ToolCallContent::Content { + content: acp::ContentBlock::ResourceLink(acp::ResourceLink { + name: result.title.clone(), + uri: result.url.clone(), + title: Some(result.title.clone()), + description: Some(result.text.clone()), + mime_type: None, + annotations: None, + size: None, + }), + }) + .collect(), + ), + ..Default::default() + }); } diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 886f650470185ff6e5d4b70e5873037329f08b21..cbc874057a088124475da7507b5e090cfa3a5509 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -18,6 +18,7 @@ doctest = false [dependencies] acp_thread.workspace = true +action_log.workspace = true agent-client-protocol.workspace = true agent_settings.workspace = true agentic-coding-protocol.workspace = true diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 551e9fa01a6c79c4a8c4d67b9af6996a0235086f..aa80f01c15a4c2da96973356ab1dbf838a8eb28a 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -1,4 +1,5 @@ // Translates old acp agents into the new schema +use action_log::ActionLog; use agent_client_protocol as acp; use agentic_coding_protocol::{self as acp_old, AgentRequest as _}; use anyhow::{Context as _, Result, anyhow}; @@ -443,7 +444,8 @@ impl AgentConnection for AcpConnection { cx.update(|cx| { let thread = cx.new(|cx| { let session_id = acp::SessionId("acp-old-no-id".into()); - AcpThread::new(self.name, self.clone(), project, session_id, cx) + let action_log = cx.new(|_| ActionLog::new(project.clone())); + AcpThread::new(self.name, self.clone(), project, action_log, session_id) }); current_thread.replace(thread.downgrade()); thread diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 93a5ae757a3fde11660db24e182b453e0fbb9850..d749537c4c3ff60affe05b745a12a0baac69f722 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -1,3 +1,4 @@ +use action_log::ActionLog; use agent_client_protocol::{self as acp, Agent as _}; use anyhow::anyhow; use collections::HashMap; @@ -153,14 +154,14 @@ impl AgentConnection for AcpConnection { })?; let session_id = response.session_id; - - let thread = cx.new(|cx| { + let action_log = cx.new(|_| ActionLog::new(project.clone()))?; + let thread = cx.new(|_cx| { AcpThread::new( self.server_name, self.clone(), project, + action_log, session_id.clone(), - cx, ) })?; diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 34d55f39dc99f5d01d47556a8c9315646f8b1a68..f27c973ad62c6205d9d4b01103ecf41aa9b2157e 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1,6 +1,7 @@ mod mcp_server; pub mod tools; +use action_log::ActionLog; use collections::HashMap; use context_server::listener::McpServerTool; use language_models::provider::anthropic::AnthropicLanguageModelProvider; @@ -215,8 +216,15 @@ impl AgentConnection for ClaudeAgentConnection { } }); - let thread = cx.new(|cx| { - AcpThread::new("Claude Code", self.clone(), project, session_id.clone(), cx) + let action_log = cx.new(|_| ActionLog::new(project.clone()))?; + let thread = cx.new(|_cx| { + AcpThread::new( + "Claude Code", + self.clone(), + project, + action_log, + session_id.clone(), + ) })?; thread_tx.send(thread.downgrade())?; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index ad0920bc4a9cd5bc3bf046c04cd8cab16726834f..150f1ea73bf8018a09fd169a408914adb052c0fc 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -303,8 +303,13 @@ impl AcpThreadView { let action_log_subscription = cx.observe(&action_log, |_, _, cx| cx.notify()); - this.list_state - .splice(0..0, thread.read(cx).entries().len()); + let count = thread.read(cx).entries().len(); + this.list_state.splice(0..0, count); + this.entry_view_state.update(cx, |view_state, cx| { + for ix in 0..count { + view_state.sync_entry(ix, &thread, window, cx); + } + }); AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx); @@ -808,6 +813,7 @@ impl AcpThreadView { self.thread_retry_status.take(); self.thread_state = ThreadState::ServerExited { status: *status }; } + AcpThreadEvent::TitleUpdated => {} } cx.notify(); } @@ -2816,12 +2822,15 @@ impl AcpThreadView { return; }; - thread.update(cx, |thread, _cx| { + thread.update(cx, |thread, cx| { let current_mode = thread.completion_mode(); - thread.set_completion_mode(match current_mode { - CompletionMode::Burn => CompletionMode::Normal, - CompletionMode::Normal => CompletionMode::Burn, - }); + thread.set_completion_mode( + match current_mode { + CompletionMode::Burn => CompletionMode::Normal, + CompletionMode::Normal => CompletionMode::Burn, + }, + cx, + ); }); } @@ -3572,8 +3581,9 @@ impl AcpThreadView { )) .on_click({ cx.listener(move |this, _, _window, cx| { - thread.update(cx, |thread, _cx| { - thread.set_completion_mode(CompletionMode::Burn); + thread.update(cx, |thread, cx| { + thread + .set_completion_mode(CompletionMode::Burn, cx); }); this.resume_chat(cx); }) @@ -4156,12 +4166,13 @@ pub(crate) mod tests { cx: &mut gpui::App, ) -> Task<gpui::Result<Entity<AcpThread>>> { Task::ready(Ok(cx.new(|cx| { + let action_log = cx.new(|_| ActionLog::new(project.clone())); AcpThread::new( "SaboteurAgentConnection", self, project, + action_log, SessionId("test".into()), - cx, ) }))) } diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index f474fdf3ae85d30a2d36dc9483e01fda89ad09b6..5b4f1038e2dbd9285bceeb6beb6f3197cbb983eb 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -199,24 +199,21 @@ impl AgentDiffPane { let action_log = thread.action_log(cx).clone(); let mut this = Self { - _subscriptions: [ - Some( - cx.observe_in(&action_log, window, |this, _action_log, window, cx| { - this.update_excerpts(window, cx) - }), - ), + _subscriptions: vec![ + cx.observe_in(&action_log, window, |this, _action_log, window, cx| { + this.update_excerpts(window, cx) + }), match &thread { - AgentDiffThread::Native(thread) => { - Some(cx.subscribe(thread, |this, _thread, event, cx| { - this.handle_thread_event(event, cx) - })) - } - AgentDiffThread::AcpThread(_) => None, + AgentDiffThread::Native(thread) => cx + .subscribe(thread, |this, _thread, event, cx| { + this.handle_native_thread_event(event, cx) + }), + AgentDiffThread::AcpThread(thread) => cx + .subscribe(thread, |this, _thread, event, cx| { + this.handle_acp_thread_event(event, cx) + }), }, - ] - .into_iter() - .flatten() - .collect(), + ], title: SharedString::default(), multibuffer, editor, @@ -324,13 +321,20 @@ impl AgentDiffPane { } } - fn handle_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) { + fn handle_native_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) { match event { ThreadEvent::SummaryGenerated => self.update_title(cx), _ => {} } } + fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context<Self>) { + match event { + AcpThreadEvent::TitleUpdated => self.update_title(cx), + _ => {} + } + } + pub fn move_to_path(&self, path_key: PathKey, window: &mut Window, cx: &mut App) { if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) { self.editor.update(cx, |editor, cx| { @@ -1523,7 +1527,8 @@ impl AgentDiff { AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::ServerExited(_) => { self.update_reviewing_editors(workspace, window, cx); } - AcpThreadEvent::EntriesRemoved(_) + AcpThreadEvent::TitleUpdated + | AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::ToolAuthorizationRequired | AcpThreadEvent::Retry(_) => {} } From 1444cd9839dcd04f60bb3ba2284be2183cae567d Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Tue, 19 Aug 2025 10:53:10 -0400 Subject: [PATCH 142/744] Fix Windows test failures not being detected in CI (#36446) Bug introduced in #35926 Release Notes: - N/A --- .github/actions/run_tests_windows/action.yml | 1 - crates/acp_thread/src/acp_thread.rs | 14 ++- crates/acp_thread/src/mention.rs | 83 ++++++++--------- crates/agent_ui/src/acp/message_editor.rs | 93 +++++++------------- crates/fs/src/fake_git_repo.rs | 6 +- crates/fs/src/fs.rs | 4 +- 6 files changed, 87 insertions(+), 114 deletions(-) diff --git a/.github/actions/run_tests_windows/action.yml b/.github/actions/run_tests_windows/action.yml index e3e3b7142e2223e2b5a7524205dbe21fb963ed86..0a550c7d32b823db22cf205cf991020182a7d3b5 100644 --- a/.github/actions/run_tests_windows/action.yml +++ b/.github/actions/run_tests_windows/action.yml @@ -56,7 +56,6 @@ runs: $env:COMPlus_CreateDumpDiagnostics = "1" cargo nextest run --workspace --no-fail-fast - continue-on-error: true - name: Analyze crash dumps if: always() diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 7d70727252abc5196f65cbc9cd712bf356063208..1de8110f07a1586634b5a0e8e01aa0f597f1238e 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -2155,7 +2155,7 @@ mod tests { "} ); }); - assert_eq!(fs.files(), vec![Path::new("/test/file-0")]); + assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]); cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["ipsum".into()], cx))) .await @@ -2185,7 +2185,10 @@ mod tests { }); assert_eq!( fs.files(), - vec![Path::new("/test/file-0"), Path::new("/test/file-1")] + vec![ + Path::new(path!("/test/file-0")), + Path::new(path!("/test/file-1")) + ] ); // Checkpoint isn't stored when there are no changes. @@ -2226,7 +2229,10 @@ mod tests { }); assert_eq!( fs.files(), - vec![Path::new("/test/file-0"), Path::new("/test/file-1")] + vec![ + Path::new(path!("/test/file-0")), + Path::new(path!("/test/file-1")) + ] ); // Rewinding the conversation truncates the history and restores the checkpoint. @@ -2254,7 +2260,7 @@ mod tests { "} ); }); - assert_eq!(fs.files(), vec![Path::new("/test/file-0")]); + assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]); } async fn run_until_first_tool_call( diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 350785ec1ed98521fc6e63e645d0f60b4c31d8f1..fcf50b0fd72591a20ed2d52415f286f29100f796 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -52,6 +52,7 @@ impl MentionUri { let path = url.path(); match url.scheme() { "file" => { + let path = url.to_file_path().ok().context("Extracting file path")?; if let Some(fragment) = url.fragment() { let range = fragment .strip_prefix("L") @@ -72,23 +73,17 @@ impl MentionUri { if let Some(name) = single_query_param(&url, "symbol")? { Ok(Self::Symbol { name, - path: path.into(), + path, line_range, }) } else { - Ok(Self::Selection { - path: path.into(), - line_range, - }) + Ok(Self::Selection { path, line_range }) } } else { - let abs_path = - PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path)); - if input.ends_with("/") { - Ok(Self::Directory { abs_path }) + Ok(Self::Directory { abs_path: path }) } else { - Ok(Self::File { abs_path }) + Ok(Self::File { abs_path: path }) } } } @@ -162,27 +157,17 @@ impl MentionUri { pub fn to_uri(&self) -> Url { match self { MentionUri::File { abs_path } => { - let mut url = Url::parse("file:///").unwrap(); - let path = abs_path.to_string_lossy(); - url.set_path(&path); - url + Url::from_file_path(abs_path).expect("mention path should be absolute") } MentionUri::Directory { abs_path } => { - let mut url = Url::parse("file:///").unwrap(); - let mut path = abs_path.to_string_lossy().to_string(); - if !path.ends_with("/") { - path.push_str("/"); - } - url.set_path(&path); - url + Url::from_directory_path(abs_path).expect("mention path should be absolute") } MentionUri::Symbol { path, name, line_range, } => { - let mut url = Url::parse("file:///").unwrap(); - url.set_path(&path.to_string_lossy()); + let mut url = Url::from_file_path(path).expect("mention path should be absolute"); url.query_pairs_mut().append_pair("symbol", name); url.set_fragment(Some(&format!( "L{}:{}", @@ -192,8 +177,7 @@ impl MentionUri { url } MentionUri::Selection { path, line_range } => { - let mut url = Url::parse("file:///").unwrap(); - url.set_path(&path.to_string_lossy()); + let mut url = Url::from_file_path(path).expect("mention path should be absolute"); url.set_fragment(Some(&format!( "L{}:{}", line_range.start + 1, @@ -266,15 +250,17 @@ pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String { #[cfg(test)] mod tests { + use util::{path, uri}; + use super::*; #[test] fn test_parse_file_uri() { - let file_uri = "file:///path/to/file.rs"; + let file_uri = uri!("file:///path/to/file.rs"); let parsed = MentionUri::parse(file_uri).unwrap(); match &parsed { MentionUri::File { abs_path } => { - assert_eq!(abs_path.to_str().unwrap(), "/path/to/file.rs"); + assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/file.rs")); } _ => panic!("Expected File variant"), } @@ -283,11 +269,11 @@ mod tests { #[test] fn test_parse_directory_uri() { - let file_uri = "file:///path/to/dir/"; + let file_uri = uri!("file:///path/to/dir/"); let parsed = MentionUri::parse(file_uri).unwrap(); match &parsed { MentionUri::Directory { abs_path } => { - assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir/"); + assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/dir/")); } _ => panic!("Expected Directory variant"), } @@ -297,22 +283,24 @@ mod tests { #[test] fn test_to_directory_uri_with_slash() { let uri = MentionUri::Directory { - abs_path: PathBuf::from("/path/to/dir/"), + abs_path: PathBuf::from(path!("/path/to/dir/")), }; - assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/"); + let expected = uri!("file:///path/to/dir/"); + assert_eq!(uri.to_uri().to_string(), expected); } #[test] fn test_to_directory_uri_without_slash() { let uri = MentionUri::Directory { - abs_path: PathBuf::from("/path/to/dir"), + abs_path: PathBuf::from(path!("/path/to/dir")), }; - assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/"); + let expected = uri!("file:///path/to/dir/"); + assert_eq!(uri.to_uri().to_string(), expected); } #[test] fn test_parse_symbol_uri() { - let symbol_uri = "file:///path/to/file.rs?symbol=MySymbol#L10:20"; + let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20"); let parsed = MentionUri::parse(symbol_uri).unwrap(); match &parsed { MentionUri::Symbol { @@ -320,7 +308,7 @@ mod tests { name, line_range, } => { - assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"); + assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs")); assert_eq!(name, "MySymbol"); assert_eq!(line_range.start, 9); assert_eq!(line_range.end, 19); @@ -332,11 +320,11 @@ mod tests { #[test] fn test_parse_selection_uri() { - let selection_uri = "file:///path/to/file.rs#L5:15"; + let selection_uri = uri!("file:///path/to/file.rs#L5:15"); let parsed = MentionUri::parse(selection_uri).unwrap(); match &parsed { MentionUri::Selection { path, line_range } => { - assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"); + assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs")); assert_eq!(line_range.start, 4); assert_eq!(line_range.end, 14); } @@ -418,32 +406,35 @@ mod tests { #[test] fn test_invalid_line_range_format() { // Missing L prefix - assert!(MentionUri::parse("file:///path/to/file.rs#10:20").is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#10:20")).is_err()); // Missing colon separator - assert!(MentionUri::parse("file:///path/to/file.rs#L1020").is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L1020")).is_err()); // Invalid numbers - assert!(MentionUri::parse("file:///path/to/file.rs#L10:abc").is_err()); - assert!(MentionUri::parse("file:///path/to/file.rs#Labc:20").is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L10:abc")).is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#Labc:20")).is_err()); } #[test] fn test_invalid_query_parameters() { // Invalid query parameter name - assert!(MentionUri::parse("file:///path/to/file.rs#L10:20?invalid=test").is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L10:20?invalid=test")).is_err()); // Too many query parameters assert!( - MentionUri::parse("file:///path/to/file.rs#L10:20?symbol=test&another=param").is_err() + MentionUri::parse(uri!( + "file:///path/to/file.rs#L10:20?symbol=test&another=param" + )) + .is_err() ); } #[test] fn test_zero_based_line_numbers() { // Test that 0-based line numbers are rejected (should be 1-based) - assert!(MentionUri::parse("file:///path/to/file.rs#L0:10").is_err()); - assert!(MentionUri::parse("file:///path/to/file.rs#L1:0").is_err()); - assert!(MentionUri::parse("file:///path/to/file.rs#L0:0").is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L0:10")).is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L1:0")).is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L0:0")).is_err()); } } diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 00368d6087a510bc1f471330e12f136fb05b6fdd..afb1512e5d529dd41453dcedcfd8d9fbf3f566e8 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1640,7 +1640,7 @@ mod tests { use serde_json::json; use text::Point; use ui::{App, Context, IntoElement, Render, SharedString, Window}; - use util::path; + use util::{path, uri}; use workspace::{AppState, Item, Workspace}; use crate::acp::{ @@ -1950,13 +1950,12 @@ mod tests { editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); }); + let url_one = uri!("file:///dir/a/one.txt"); editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) "); + let text = editor.text(cx); + assert_eq!(text, format!("Lorem [@one.txt]({url_one}) ")); assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); + assert_eq!(fold_ranges(editor, cx).len(), 1); }); let contents = message_editor @@ -1977,47 +1976,35 @@ mod tests { contents, [Mention::Text { content: "1".into(), - uri: "file:///dir/a/one.txt".parse().unwrap() + uri: url_one.parse().unwrap() }] ); cx.simulate_input(" "); editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) "); + let text = editor.text(cx); + assert_eq!(text, format!("Lorem [@one.txt]({url_one}) ")); assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); + assert_eq!(fold_ranges(editor, cx).len(), 1); }); cx.simulate_input("Ipsum "); editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum ", - ); + let text = editor.text(cx); + assert_eq!(text, format!("Lorem [@one.txt]({url_one}) Ipsum "),); assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); + assert_eq!(fold_ranges(editor, cx).len(), 1); }); cx.simulate_input("@file "); editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum @file ", - ); + let text = editor.text(cx); + assert_eq!(text, format!("Lorem [@one.txt]({url_one}) Ipsum @file "),); assert!(editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); + assert_eq!(fold_ranges(editor, cx).len(), 1); }); editor.update_in(&mut cx, |editor, window, cx| { @@ -2041,28 +2028,23 @@ mod tests { .collect::<Vec<_>>(); assert_eq!(contents.len(), 2); + let url_eight = uri!("file:///dir/b/eight.txt"); pretty_assertions::assert_eq!( contents[1], Mention::Text { content: "8".to_string(), - uri: "file:///dir/b/eight.txt".parse().unwrap(), + uri: url_eight.parse().unwrap(), } ); editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) " - ); - assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![ - Point::new(0, 6)..Point::new(0, 39), - Point::new(0, 47)..Point::new(0, 84) - ] - ); - }); + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) ") + ); + assert!(!editor.has_visible_completions_menu()); + assert_eq!(fold_ranges(editor, cx).len(), 2); + }); let plain_text_language = Arc::new(language::Language::new( language::LanguageConfig { @@ -2108,7 +2090,7 @@ mod tests { let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>( - |_, _| async move { + move |_, _| async move { Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![ #[allow(deprecated)] lsp::SymbolInformation { @@ -2132,18 +2114,13 @@ mod tests { cx.simulate_input("@symbol "); editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) @symbol " - ); - assert!(editor.has_visible_completions_menu()); - assert_eq!( - current_completion_labels(editor), - &[ - "MySymbol", - ] - ); - }); + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) @symbol ") + ); + assert!(editor.has_visible_completions_menu()); + assert_eq!(current_completion_labels(editor), &["MySymbol"]); + }); editor.update_in(&mut cx, |editor, window, cx| { editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); @@ -2165,9 +2142,7 @@ mod tests { contents[2], Mention::Text { content: "1".into(), - uri: "file:///dir/a/one.txt?symbol=MySymbol#L1:1" - .parse() - .unwrap(), + uri: format!("{url_one}?symbol=MySymbol#L1:1").parse().unwrap(), } ); @@ -2176,7 +2151,7 @@ mod tests { editor.read_with(&cx, |editor, cx| { assert_eq!( editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) [@MySymbol](file:///dir/a/one.txt?symbol=MySymbol#L1:1) " + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") ); }); } diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index f0936d400a98eba5fe8c37d946f704b831dfb876..5b093ac6a0f6cb5c5ca5da7262ad17dd4059ce6d 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -590,9 +590,9 @@ mod tests { assert_eq!( fs.files_with_contents(Path::new("")), [ - (Path::new("/bar/baz").into(), b"qux".into()), - (Path::new("/foo/a").into(), b"lorem".into()), - (Path::new("/foo/b").into(), b"ipsum".into()) + (Path::new(path!("/bar/baz")).into(), b"qux".into()), + (Path::new(path!("/foo/a")).into(), b"lorem".into()), + (Path::new(path!("/foo/b")).into(), b"ipsum".into()) ] ); } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 847e98d6c4ef090b5905a4fd06ddaf16359eb12f..399c0f3e320b5653ee262acb370bbaab442a9a18 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1101,7 +1101,9 @@ impl FakeFsState { ) -> Option<(&mut FakeFsEntry, PathBuf)> { let canonical_path = self.canonicalize(target, follow_symlink)?; - let mut components = canonical_path.components(); + let mut components = canonical_path + .components() + .skip_while(|component| matches!(component, Component::Prefix(_))); let Some(Component::RootDir) = components.next() else { panic!( "the path {:?} was not canonicalized properly {:?}", From 43b4363b34ceb5070ab80343cecd83c55be1e942 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Tue, 19 Aug 2025 20:30:25 +0530 Subject: [PATCH 143/744] lsp: Enable dynamic registration for TextDocumentSyncClientCapabilities post revert (#36494) Follow up: https://github.com/zed-industries/zed/pull/36485 Release Notes: - N/A --- crates/lsp/src/lsp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 366005a4abf3f984f6e68d527da3fcf10da4f6cf..ce9e2fe229c0aded6fac31c260e334445f987f03 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -827,7 +827,7 @@ impl LanguageServer { }), synchronization: Some(TextDocumentSyncClientCapabilities { did_save: Some(true), - dynamic_registration: Some(false), + dynamic_registration: Some(true), ..TextDocumentSyncClientCapabilities::default() }), code_lens: Some(CodeLensClientCapabilities { From 013eaaeadd9952a8bf3b546a271b7d8e08368e1b Mon Sep 17 00:00:00 2001 From: Lukas Wirth <lukas@zed.dev> Date: Tue, 19 Aug 2025 18:43:42 +0200 Subject: [PATCH 144/744] editor: Render dirty and conflict markers in multibuffer headers (#36489) Release Notes: - Added rendering of status indicators for multi buffer headers --- crates/editor/src/element.rs | 19 +++++++++++++++---- crates/inspector_ui/src/div_inspector.rs | 12 ++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 915a3cdc381c4cc949561f85c24e13477dd0f906..0922752e44d5ac85cd638ca8ea119312dc6a0c67 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -82,7 +82,7 @@ use sum_tree::Bias; use text::{BufferId, SelectionGoal}; use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor}; use ui::{ - ButtonLike, ContextMenu, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*, + ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*, right_click_menu, }; use unicode_segmentation::UnicodeSegmentation; @@ -3563,9 +3563,8 @@ impl EditorElement { cx: &mut App, ) -> impl IntoElement { let editor = self.editor.read(cx); - let file_status = editor - .buffer - .read(cx) + let multi_buffer = editor.buffer.read(cx); + let file_status = multi_buffer .all_diff_hunks_expanded() .then(|| { editor @@ -3575,6 +3574,17 @@ impl EditorElement { .status_for_buffer_id(for_excerpt.buffer_id, cx) }) .flatten(); + let indicator = multi_buffer + .buffer(for_excerpt.buffer_id) + .and_then(|buffer| { + let buffer = buffer.read(cx); + let indicator_color = match (buffer.has_conflict(), buffer.is_dirty()) { + (true, _) => Some(Color::Warning), + (_, true) => Some(Color::Accent), + (false, false) => None, + }; + indicator_color.map(|indicator_color| Indicator::dot().color(indicator_color)) + }); let include_root = editor .project @@ -3683,6 +3693,7 @@ impl EditorElement { }) .take(1), ) + .children(indicator) .child( h_flex() .cursor_pointer() diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs index bd395aa01bca42ce923073ee6f80472abc7820eb..e9460cc9cca7420b67368184dcd36d8ecb079f4d 100644 --- a/crates/inspector_ui/src/div_inspector.rs +++ b/crates/inspector_ui/src/div_inspector.rs @@ -395,11 +395,11 @@ impl DivInspector { .zip(self.rust_completion_replace_range.as_ref()) { let before_text = snapshot - .text_for_range(0..completion_range.start.to_offset(&snapshot)) + .text_for_range(0..completion_range.start.to_offset(snapshot)) .collect::<String>(); let after_text = snapshot .text_for_range( - completion_range.end.to_offset(&snapshot) + completion_range.end.to_offset(snapshot) ..snapshot.clip_offset(usize::MAX, Bias::Left), ) .collect::<String>(); @@ -702,10 +702,10 @@ impl CompletionProvider for RustStyleCompletionProvider { } fn completion_replace_range(snapshot: &BufferSnapshot, anchor: &Anchor) -> Option<Range<Anchor>> { - let point = anchor.to_point(&snapshot); - let offset = point.to_offset(&snapshot); - let line_start = Point::new(point.row, 0).to_offset(&snapshot); - let line_end = Point::new(point.row, snapshot.line_len(point.row)).to_offset(&snapshot); + let point = anchor.to_point(snapshot); + let offset = point.to_offset(snapshot); + let line_start = Point::new(point.row, 0).to_offset(snapshot); + let line_end = Point::new(point.row, snapshot.line_len(point.row)).to_offset(snapshot); let mut lines = snapshot.text_for_range(line_start..line_end).lines(); let line = lines.next()?; From d1cabef2bfbe37bea8415d6b32835be9ed108249 Mon Sep 17 00:00:00 2001 From: Lukas Wirth <lukas@zed.dev> Date: Tue, 19 Aug 2025 18:53:45 +0200 Subject: [PATCH 145/744] editor: Fix inline diagnostics min column inaccuracy (#36501) Closes https://github.com/zed-industries/zed/issues/33346 Release Notes: - Fixed `diagnostic.inline.min_column` being inaccurate --- crates/editor/src/element.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 0922752e44d5ac85cd638ca8ea119312dc6a0c67..d8fe3ccf158a7b8b20fc4fcd56030ad24d066ad7 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2173,11 +2173,13 @@ impl EditorElement { }; let padding = ProjectSettings::get_global(cx).diagnostics.inline.padding as f32 * em_width; - let min_x = ProjectSettings::get_global(cx) - .diagnostics - .inline - .min_column as f32 - * em_width; + let min_x = self.column_pixels( + ProjectSettings::get_global(cx) + .diagnostics + .inline + .min_column as usize, + window, + ); let mut elements = HashMap::default(); for (row, mut diagnostics) in diagnostics_by_rows { From e092aed253a7814f3fb04b4b700e9b65c80ec993 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Tue, 19 Aug 2025 14:25:25 -0300 Subject: [PATCH 146/744] Split external agent flags (#36499) Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 132 +++++++++++++--------- crates/feature_flags/src/feature_flags.rs | 6 + 2 files changed, 82 insertions(+), 56 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 55d07ed495800b098f1f32b050072d5a61e1a6ab..995bf771e266c1057406c8b98f99fcc8d6b2349b 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -4,7 +4,6 @@ use std::rc::Rc; use std::sync::Arc; use std::time::Duration; -use agent_servers::AgentServer; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; @@ -44,7 +43,7 @@ use assistant_tool::ToolWorkingSet; 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}; +use feature_flags::{self, AcpFeatureFlag, ClaudeCodeFeatureFlag, FeatureFlagAppExt}; use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem, @@ -971,7 +970,7 @@ impl AgentPanel { let text_thread_store = self.context_store.clone(); cx.spawn_in(window, async move |this, cx| { - let server: Rc<dyn AgentServer> = match agent_choice { + let ext_agent = match agent_choice { Some(agent) => { cx.background_spawn(async move { if let Some(serialized) = @@ -985,10 +984,10 @@ impl AgentPanel { }) .detach(); - agent.server(fs) + agent } - None => cx - .background_spawn(async move { + None => { + cx.background_spawn(async move { KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY) }) .await @@ -999,10 +998,25 @@ impl AgentPanel { }) .unwrap_or_default() .agent - .server(fs), + } }; + let server = ext_agent.server(fs); + this.update_in(cx, |this, window, cx| { + match ext_agent { + crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => { + if !cx.has_flag::<AcpFeatureFlag>() { + return; + } + } + crate::ExternalAgent::ClaudeCode => { + if !cx.has_flag::<ClaudeCodeFeatureFlag>() { + return; + } + } + } + let thread_view = cx.new(|cx| { crate::acp::AcpThreadView::new( server, @@ -2320,56 +2334,60 @@ impl AgentPanel { ) .separator() .header("External Agents") - .item( - ContextMenuEntry::new("New Gemini Thread") - .icon(IconName::AiGemini) - .icon_color(Color::Muted) - .handler({ - let workspace = workspace.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::<AgentPanel>(cx) - { - panel.update(cx, |panel, cx| { - panel.set_selected_agent( - AgentType::Gemini, - window, - cx, - ); - }); - } - }); + .when(cx.has_flag::<AcpFeatureFlag>(), |menu| { + menu.item( + ContextMenuEntry::new("New Gemini Thread") + .icon(IconName::AiGemini) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::<AgentPanel>(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::Gemini, + window, + cx, + ); + }); + } + }); + } } - } - }), - ) - .item( - ContextMenuEntry::new("New Claude Code Thread") - .icon(IconName::AiClaude) - .icon_color(Color::Muted) - .handler({ - let workspace = workspace.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::<AgentPanel>(cx) - { - panel.update(cx, |panel, cx| { - panel.set_selected_agent( - AgentType::ClaudeCode, - window, - cx, - ); - }); - } - }); + }), + ) + }) + .when(cx.has_flag::<ClaudeCodeFeatureFlag>(), |menu| { + menu.item( + ContextMenuEntry::new("New Claude Code Thread") + .icon(IconName::AiClaude) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::<AgentPanel>(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::ClaudeCode, + window, + cx, + ); + }); + } + }); + } } - } - }), - ); + }), + ) + }); menu })) } @@ -2439,7 +2457,9 @@ impl AgentPanel { } fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { - if cx.has_flag::<feature_flags::AcpFeatureFlag>() { + if cx.has_flag::<feature_flags::AcpFeatureFlag>() + || cx.has_flag::<feature_flags::ClaudeCodeFeatureFlag>() + { self.render_toolbar_new(window, cx).into_any_element() } else { self.render_toolbar_old(window, cx).into_any_element() diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index f87932bfaf99411120de61edf95535ea46e1117c..7c12571f24bfcfe7823d5e291afbeec6ccc43d9f 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -95,6 +95,12 @@ impl FeatureFlag for AcpFeatureFlag { const NAME: &'static str = "acp"; } +pub struct ClaudeCodeFeatureFlag; + +impl FeatureFlag for ClaudeCodeFeatureFlag { + const NAME: &'static str = "claude-code"; +} + pub trait FeatureFlagViewExt<V: 'static> { fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription where From 1af47a563fd11ac83d676dee07f87e2b46fe3649 Mon Sep 17 00:00:00 2001 From: fantacell <ghub@giggo.de> Date: Tue, 19 Aug 2025 19:52:29 +0200 Subject: [PATCH 147/744] helix: Uncomment one test (#36328) There are two tests commented out in the helix file, but one of them works again. I don't know if this is too little a change to be merged, but I wanted to suggest it. The other test might be more complicated though, so I didn't touch it. Release Notes: - N/A --- crates/vim/src/helix.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 0c8c06d8ab66f422df7c79c33e0f179db59f716f..3cc9772d42c9efe5ef937995aee84fbdd60bb09d 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -547,27 +547,27 @@ mod test { ); } - // #[gpui::test] - // async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) { - // let mut cx = VimTestContext::new(cx, true).await; + #[gpui::test] + async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; - // cx.set_state( - // indoc! {" - // The quick brownˇ - // fox jumps over - // the lazy dog."}, - // Mode::HelixNormal, - // ); + cx.set_state( + indoc! {" + The quick brownˇ + fox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); - // cx.simulate_keystrokes("d"); + cx.simulate_keystrokes("d"); - // cx.assert_state( - // indoc! {" - // The quick brownˇfox jumps over - // the lazy dog."}, - // Mode::HelixNormal, - // ); - // } + cx.assert_state( + indoc! {" + The quick brownˇfox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); + } // #[gpui::test] // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) { From 6b6eb116438f055cb6344d510e37138d8b998ccb Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Tue, 19 Aug 2025 20:06:09 +0200 Subject: [PATCH 148/744] agent2: Fix tool schemas for Gemini (#36507) Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <agus@zed.dev> --- crates/agent2/src/agent2.rs | 1 + crates/agent2/src/thread.rs | 6 ++--- crates/agent2/src/tool_schema.rs | 43 +++++++++++++++++++++++++++++++ crates/google_ai/src/google_ai.rs | 2 +- 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 crates/agent2/src/tool_schema.rs diff --git a/crates/agent2/src/agent2.rs b/crates/agent2/src/agent2.rs index f13cd1bd673b5e122333264ac3cbcbe83edd7627..8d18da7fe16e6c3238a8816cace1f3b3968cced2 100644 --- a/crates/agent2/src/agent2.rs +++ b/crates/agent2/src/agent2.rs @@ -2,6 +2,7 @@ mod agent; mod native_agent_server; mod templates; mod thread; +mod tool_schema; mod tools; #[cfg(test)] diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index aeb600e2328332335dca6f4a59b58f65a9ca91f9..d90d0bd4f84664f4c8912e22ab9865d94a0ccf68 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1732,8 +1732,8 @@ where fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString; /// Returns the JSON schema that describes the tool's input. - fn input_schema(&self) -> Schema { - schemars::schema_for!(Self::Input) + fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Schema { + crate::tool_schema::root_schema_for::<Self::Input>(format) } /// Some tools rely on a provider for the underlying billing or other reasons. @@ -1819,7 +1819,7 @@ where } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> { - let mut json = serde_json::to_value(self.0.input_schema())?; + let mut json = serde_json::to_value(self.0.input_schema(format))?; adapt_schema_to_format(&mut json, format)?; Ok(json) } diff --git a/crates/agent2/src/tool_schema.rs b/crates/agent2/src/tool_schema.rs new file mode 100644 index 0000000000000000000000000000000000000000..f608336b416a72885e52abba58ef472029421e4f --- /dev/null +++ b/crates/agent2/src/tool_schema.rs @@ -0,0 +1,43 @@ +use language_model::LanguageModelToolSchemaFormat; +use schemars::{ + JsonSchema, Schema, + generate::SchemaSettings, + transform::{Transform, transform_subschemas}, +}; + +pub(crate) fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema { + let mut generator = match format { + LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(), + LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3() + .with(|settings| { + settings.meta_schema = None; + settings.inline_subschemas = true; + }) + .with_transform(ToJsonSchemaSubsetTransform) + .into_generator(), + }; + generator.root_schema_for::<T>() +} + +#[derive(Debug, Clone)] +struct ToJsonSchemaSubsetTransform; + +impl Transform for ToJsonSchemaSubsetTransform { + fn transform(&mut self, schema: &mut Schema) { + // Ensure that the type field is not an array, this happens when we use + // Option<T>, the type will be [T, "null"]. + if let Some(type_field) = schema.get_mut("type") + && let Some(types) = type_field.as_array() + && let Some(first_type) = types.first() + { + *type_field = first_type.clone(); + } + + // oneOf is not supported, use anyOf instead + if let Some(one_of) = schema.remove("oneOf") { + schema.insert("anyOf".to_string(), one_of); + } + + transform_subschemas(self, schema); + } +} diff --git a/crates/google_ai/src/google_ai.rs b/crates/google_ai/src/google_ai.rs index 95a6daa1d93669d7793ea1296ecb1fe872882262..a1b5ca3a03d67fe63032df8f3996298ea64f169c 100644 --- a/crates/google_ai/src/google_ai.rs +++ b/crates/google_ai/src/google_ai.rs @@ -266,7 +266,7 @@ pub struct CitationMetadata { pub struct PromptFeedback { #[serde(skip_serializing_if = "Option::is_none")] pub block_reason: Option<String>, - pub safety_ratings: Vec<SafetyRating>, + pub safety_ratings: Option<Vec<SafetyRating>>, #[serde(skip_serializing_if = "Option::is_none")] pub block_reason_message: Option<String>, } From 6ba52a3a42cbbb9dc4daa3d3e283ca1f98e11d30 Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Tue, 19 Aug 2025 12:08:11 -0600 Subject: [PATCH 149/744] Re-add history entries for native agent threads (#36500) Closes #ISSUE Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> --- Cargo.lock | 4 + crates/agent/src/thread_store.rs | 2 +- crates/agent2/Cargo.toml | 4 + crates/agent2/src/agent.rs | 200 ++++-- crates/agent2/src/agent2.rs | 4 + crates/agent2/src/db.rs | 470 +++++++++++++ crates/agent2/src/history_store.rs | 314 +++++++++ crates/agent2/src/native_agent_server.rs | 11 +- crates/agent2/src/tests/mod.rs | 4 +- crates/agent2/src/thread.rs | 132 +++- crates/agent2/src/tools/edit_file_tool.rs | 9 - crates/agent_ui/src/acp.rs | 2 + crates/agent_ui/src/acp/thread_history.rs | 766 ++++++++++++++++++++++ crates/agent_ui/src/acp/thread_view.rs | 42 +- crates/agent_ui/src/agent_panel.rs | 154 ++++- crates/agent_ui/src/agent_ui.rs | 8 +- 16 files changed, 2007 insertions(+), 119 deletions(-) create mode 100644 crates/agent2/src/db.rs create mode 100644 crates/agent2/src/history_store.rs create mode 100644 crates/agent_ui/src/acp/thread_history.rs diff --git a/Cargo.lock b/Cargo.lock index dc9d074f01bb75c6af91fb98b3fcecd554f0ec5a..4a5dec4734e8bbb0ad82bc9f0e5d027c1bcf19ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,6 +196,7 @@ dependencies = [ "agent_servers", "agent_settings", "anyhow", + "assistant_context", "assistant_tool", "assistant_tools", "chrono", @@ -223,6 +224,7 @@ dependencies = [ "log", "lsp", "open", + "parking_lot", "paths", "portable-pty", "pretty_assertions", @@ -235,6 +237,7 @@ dependencies = [ "serde_json", "settings", "smol", + "sqlez", "task", "tempfile", "terminal", @@ -251,6 +254,7 @@ dependencies = [ "workspace-hack", "worktree", "zlog", + "zstd", ] [[package]] diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 96bf6393068d9949123b3da92fdeee86b4c41dc6..ed1605aacfe613c4ecfb2921d7d585400ed334c4 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -893,7 +893,7 @@ impl ThreadsDatabase { let needs_migration_from_heed = mdb_path.exists(); - let connection = if *ZED_STATELESS { + let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { Connection::open_memory(Some("THREAD_FALLBACK_DB")) } else { Connection::open_file(&sqlite_path.to_string_lossy()) diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 8129341545f38a44656924f8a1282c6a0346de55..890f7e774b417a68bbe3d9acbeff1f90fd40782a 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -19,6 +19,7 @@ agent-client-protocol.workspace = true agent_servers.workspace = true agent_settings.workspace = true anyhow.workspace = true +assistant_context.workspace = true assistant_tool.workspace = true assistant_tools.workspace = true chrono.workspace = true @@ -39,6 +40,7 @@ language_model.workspace = true language_models.workspace = true log.workspace = true open.workspace = true +parking_lot.workspace = true paths.workspace = true portable-pty.workspace = true project.workspace = true @@ -49,6 +51,7 @@ serde.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true +sqlez.workspace = true task.workspace = true terminal.workspace = true text.workspace = true @@ -59,6 +62,7 @@ watch.workspace = true web_search.workspace = true which.workspace = true workspace-hack.workspace = true +zstd.workspace = true [dev-dependencies] agent = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 9cf0c3b60398b600783364455a0a4b9dd650b127..bc46ad1657870d0445d9cb23f84898a1f3f08999 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,10 +1,9 @@ use crate::{ - ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool, DiagnosticsTool, - EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, - OpenTool, ReadFileTool, TerminalTool, ThinkingTool, Thread, ThreadEvent, ToolCallAuthorization, - UserMessageContent, WebSearchTool, templates::Templates, + ContextServerRegistry, Thread, ThreadEvent, ToolCallAuthorization, UserMessageContent, + templates::Templates, }; -use acp_thread::AgentModelSelector; +use crate::{HistoryStore, ThreadsDatabase}; +use acp_thread::{AcpThread, AgentModelSelector}; use action_log::ActionLog; use agent_client_protocol as acp; use agent_settings::AgentSettings; @@ -51,7 +50,8 @@ struct Session { thread: Entity<Thread>, /// The ACP thread that handles protocol communication acp_thread: WeakEntity<acp_thread::AcpThread>, - _subscription: Subscription, + pending_save: Task<()>, + _subscriptions: Vec<Subscription>, } pub struct LanguageModels { @@ -155,6 +155,7 @@ impl LanguageModels { pub struct NativeAgent { /// Session ID -> Session mapping sessions: HashMap<acp::SessionId, Session>, + history: Entity<HistoryStore>, /// Shared project context for all threads project_context: Entity<ProjectContext>, project_context_needs_refresh: watch::Sender<()>, @@ -173,6 +174,7 @@ pub struct NativeAgent { impl NativeAgent { pub async fn new( project: Entity<Project>, + history: Entity<HistoryStore>, templates: Arc<Templates>, prompt_store: Option<Entity<PromptStore>>, fs: Arc<dyn Fs>, @@ -200,6 +202,7 @@ impl NativeAgent { watch::channel(()); Self { sessions: HashMap::new(), + history, project_context: cx.new(|_| project_context), project_context_needs_refresh: project_context_needs_refresh_tx, _maintain_project_context: cx.spawn(async move |this, cx| { @@ -218,6 +221,55 @@ impl NativeAgent { }) } + fn register_session( + &mut self, + thread_handle: Entity<Thread>, + cx: &mut Context<Self>, + ) -> Entity<AcpThread> { + let connection = Rc::new(NativeAgentConnection(cx.entity())); + let registry = LanguageModelRegistry::read_global(cx); + let summarization_model = registry.thread_summary_model().map(|c| c.model); + + thread_handle.update(cx, |thread, cx| { + thread.set_summarization_model(summarization_model, cx); + thread.add_default_tools(cx) + }); + + let thread = thread_handle.read(cx); + let session_id = thread.id().clone(); + let title = thread.title(); + let project = thread.project.clone(); + let action_log = thread.action_log.clone(); + let acp_thread = cx.new(|_cx| { + acp_thread::AcpThread::new( + title, + connection, + project.clone(), + action_log.clone(), + session_id.clone(), + ) + }); + let subscriptions = vec![ + cx.observe_release(&acp_thread, |this, acp_thread, _cx| { + this.sessions.remove(acp_thread.session_id()); + }), + cx.observe(&thread_handle, move |this, thread, cx| { + this.save_thread(thread.clone(), cx) + }), + ]; + + self.sessions.insert( + session_id, + Session { + thread: thread_handle, + acp_thread: acp_thread.downgrade(), + _subscriptions: subscriptions, + pending_save: Task::ready(()), + }, + ); + acp_thread + } + pub fn models(&self) -> &LanguageModels { &self.models } @@ -444,6 +496,63 @@ impl NativeAgent { }); } } + + pub fn open_thread( + &mut self, + id: acp::SessionId, + cx: &mut Context<Self>, + ) -> Task<Result<Entity<AcpThread>>> { + let database_future = ThreadsDatabase::connect(cx); + cx.spawn(async move |this, cx| { + let database = database_future.await.map_err(|err| anyhow!(err))?; + let db_thread = database + .load_thread(id.clone()) + .await? + .with_context(|| format!("no thread found with ID: {id:?}"))?; + + let thread = this.update(cx, |this, cx| { + let action_log = cx.new(|_cx| ActionLog::new(this.project.clone())); + cx.new(|cx| { + Thread::from_db( + id.clone(), + db_thread, + this.project.clone(), + this.project_context.clone(), + this.context_server_registry.clone(), + action_log.clone(), + this.templates.clone(), + cx, + ) + }) + })?; + let acp_thread = + this.update(cx, |this, cx| this.register_session(thread.clone(), cx))?; + let events = thread.update(cx, |thread, cx| thread.replay(cx))?; + cx.update(|cx| { + NativeAgentConnection::handle_thread_events(events, acp_thread.downgrade(), cx) + })? + .await?; + Ok(acp_thread) + }) + } + + fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) { + let database_future = ThreadsDatabase::connect(cx); + let (id, db_thread) = + thread.update(cx, |thread, cx| (thread.id().clone(), thread.to_db(cx))); + let Some(session) = self.sessions.get_mut(&id) else { + return; + }; + let history = self.history.clone(); + session.pending_save = cx.spawn(async move |_, cx| { + let Some(database) = database_future.await.map_err(|err| anyhow!(err)).log_err() else { + return; + }; + let db_thread = db_thread.await; + database.save_thread(id, db_thread).await.log_err(); + history.update(cx, |history, cx| history.reload(cx)).ok(); + }); + } } /// Wrapper struct that implements the AgentConnection trait @@ -476,13 +585,21 @@ impl NativeAgentConnection { }; log::debug!("Found session for: {}", session_id); - let mut response_stream = match f(thread, cx) { + let response_stream = match f(thread, cx) { Ok(stream) => stream, Err(err) => return Task::ready(Err(err)), }; + Self::handle_thread_events(response_stream, acp_thread, cx) + } + + fn handle_thread_events( + mut events: mpsc::UnboundedReceiver<Result<ThreadEvent>>, + acp_thread: WeakEntity<AcpThread>, + cx: &App, + ) -> Task<Result<acp::PromptResponse>> { cx.spawn(async move |cx| { // Handle response stream and forward to session.acp_thread - while let Some(result) = response_stream.next().await { + while let Some(result) = events.next().await { match result { Ok(event) => { log::trace!("Received completion event: {:?}", event); @@ -686,8 +803,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection { |agent, cx: &mut gpui::Context<NativeAgent>| -> Result<_> { // Fetch default model from registry settings let registry = LanguageModelRegistry::read_global(cx); - let language_registry = project.read(cx).languages().clone(); - // Log available models for debugging let available_count = registry.available_models(cx).count(); log::debug!("Total available models: {}", available_count); @@ -697,72 +812,23 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .models .model_from_id(&LanguageModels::model_id(&default_model.model)) }); - let summarization_model = registry.thread_summary_model().map(|c| c.model); let thread = cx.new(|cx| { - let mut thread = Thread::new( + Thread::new( project.clone(), agent.project_context.clone(), agent.context_server_registry.clone(), action_log.clone(), agent.templates.clone(), default_model, - summarization_model, cx, - ); - thread.add_tool(CopyPathTool::new(project.clone())); - thread.add_tool(CreateDirectoryTool::new(project.clone())); - thread.add_tool(DeletePathTool::new(project.clone(), action_log.clone())); - thread.add_tool(DiagnosticsTool::new(project.clone())); - thread.add_tool(EditFileTool::new(cx.weak_entity(), language_registry)); - thread.add_tool(FetchTool::new(project.read(cx).client().http_client())); - thread.add_tool(FindPathTool::new(project.clone())); - thread.add_tool(GrepTool::new(project.clone())); - thread.add_tool(ListDirectoryTool::new(project.clone())); - thread.add_tool(MovePathTool::new(project.clone())); - thread.add_tool(NowTool); - thread.add_tool(OpenTool::new(project.clone())); - thread.add_tool(ReadFileTool::new(project.clone(), action_log.clone())); - thread.add_tool(TerminalTool::new(project.clone(), cx)); - thread.add_tool(ThinkingTool); - thread.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model. - thread + ) }); Ok(thread) }, )??; - - let session_id = thread.read_with(cx, |thread, _| thread.id().clone())?; - log::info!("Created session with ID: {}", session_id); - // Create AcpThread - let acp_thread = cx.update(|cx| { - cx.new(|_cx| { - acp_thread::AcpThread::new( - "agent2", - self.clone(), - project.clone(), - action_log.clone(), - session_id.clone(), - ) - }) - })?; - - // Store the session - agent.update(cx, |agent, cx| { - agent.sessions.insert( - session_id, - Session { - thread, - acp_thread: acp_thread.downgrade(), - _subscription: cx.observe_release(&acp_thread, |this, acp_thread, _cx| { - this.sessions.remove(acp_thread.session_id()); - }), - }, - ); - })?; - - Ok(acp_thread) + agent.update(cx, |agent, cx| agent.register_session(thread, cx)) }) } @@ -887,8 +953,11 @@ mod tests { ) .await; let project = Project::test(fs.clone(), [], cx).await; + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); let agent = NativeAgent::new( project.clone(), + history_store, Templates::new(), None, fs.clone(), @@ -942,9 +1011,12 @@ mod tests { let fs = FakeFs::new(cx.executor()); fs.insert_tree("/", json!({ "a": {} })).await; let project = Project::test(fs.clone(), [], cx).await; + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); let connection = NativeAgentConnection( NativeAgent::new( project.clone(), + history_store, Templates::new(), None, fs.clone(), @@ -995,9 +1067,13 @@ mod tests { .await; let project = Project::test(fs.clone(), [], cx).await; + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); + // Create the agent and connection let agent = NativeAgent::new( project.clone(), + history_store, Templates::new(), None, fs.clone(), diff --git a/crates/agent2/src/agent2.rs b/crates/agent2/src/agent2.rs index 8d18da7fe16e6c3238a8816cace1f3b3968cced2..1fc9c1cb956d1676c42713b5d9bb2a0b51e8ac90 100644 --- a/crates/agent2/src/agent2.rs +++ b/crates/agent2/src/agent2.rs @@ -1,4 +1,6 @@ mod agent; +mod db; +mod history_store; mod native_agent_server; mod templates; mod thread; @@ -9,6 +11,8 @@ mod tools; mod tests; pub use agent::*; +pub use db::*; +pub use history_store::*; pub use native_agent_server::NativeAgentServer; pub use templates::*; pub use thread::*; diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs new file mode 100644 index 0000000000000000000000000000000000000000..c3e6352ef66007c193e53c343dd30d0b31492730 --- /dev/null +++ b/crates/agent2/src/db.rs @@ -0,0 +1,470 @@ +use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent}; +use agent::thread_store; +use agent_client_protocol as acp; +use agent_settings::{AgentProfileId, CompletionMode}; +use anyhow::{Result, anyhow}; +use chrono::{DateTime, Utc}; +use collections::{HashMap, IndexMap}; +use futures::{FutureExt, future::Shared}; +use gpui::{BackgroundExecutor, Global, Task}; +use indoc::indoc; +use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; +use sqlez::{ + bindable::{Bind, Column}, + connection::Connection, + statement::Statement, +}; +use std::sync::Arc; +use ui::{App, SharedString}; + +pub type DbMessage = crate::Message; +pub type DbSummary = agent::thread::DetailedSummaryState; +pub type DbLanguageModel = thread_store::SerializedLanguageModel; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DbThreadMetadata { + pub id: acp::SessionId, + #[serde(alias = "summary")] + pub title: SharedString, + pub updated_at: DateTime<Utc>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DbThread { + pub title: SharedString, + pub messages: Vec<DbMessage>, + pub updated_at: DateTime<Utc>, + #[serde(default)] + pub summary: DbSummary, + #[serde(default)] + pub initial_project_snapshot: Option<Arc<agent::thread::ProjectSnapshot>>, + #[serde(default)] + pub cumulative_token_usage: language_model::TokenUsage, + #[serde(default)] + pub request_token_usage: Vec<language_model::TokenUsage>, + #[serde(default)] + pub model: Option<DbLanguageModel>, + #[serde(default)] + pub completion_mode: Option<CompletionMode>, + #[serde(default)] + pub profile: Option<AgentProfileId>, +} + +impl DbThread { + pub const VERSION: &'static str = "0.3.0"; + + pub fn from_json(json: &[u8]) -> Result<Self> { + let saved_thread_json = serde_json::from_slice::<serde_json::Value>(json)?; + match saved_thread_json.get("version") { + Some(serde_json::Value::String(version)) => match version.as_str() { + Self::VERSION => Ok(serde_json::from_value(saved_thread_json)?), + _ => Self::upgrade_from_agent_1(agent::SerializedThread::from_json(json)?), + }, + _ => Self::upgrade_from_agent_1(agent::SerializedThread::from_json(json)?), + } + } + + fn upgrade_from_agent_1(thread: agent::SerializedThread) -> Result<Self> { + let mut messages = Vec::new(); + for msg in thread.messages { + let message = match msg.role { + language_model::Role::User => { + let mut content = Vec::new(); + + // Convert segments to content + for segment in msg.segments { + match segment { + thread_store::SerializedMessageSegment::Text { text } => { + content.push(UserMessageContent::Text(text)); + } + thread_store::SerializedMessageSegment::Thinking { text, .. } => { + // User messages don't have thinking segments, but handle gracefully + content.push(UserMessageContent::Text(text)); + } + thread_store::SerializedMessageSegment::RedactedThinking { .. } => { + // User messages don't have redacted thinking, skip. + } + } + } + + // If no content was added, add context as text if available + if content.is_empty() && !msg.context.is_empty() { + content.push(UserMessageContent::Text(msg.context)); + } + + crate::Message::User(UserMessage { + // MessageId from old format can't be meaningfully converted, so generate a new one + id: acp_thread::UserMessageId::new(), + content, + }) + } + language_model::Role::Assistant => { + let mut content = Vec::new(); + + // Convert segments to content + for segment in msg.segments { + match segment { + thread_store::SerializedMessageSegment::Text { text } => { + content.push(AgentMessageContent::Text(text)); + } + thread_store::SerializedMessageSegment::Thinking { + text, + signature, + } => { + content.push(AgentMessageContent::Thinking { text, signature }); + } + thread_store::SerializedMessageSegment::RedactedThinking { data } => { + content.push(AgentMessageContent::RedactedThinking(data)); + } + } + } + + // Convert tool uses + let mut tool_names_by_id = HashMap::default(); + for tool_use in msg.tool_uses { + tool_names_by_id.insert(tool_use.id.clone(), tool_use.name.clone()); + content.push(AgentMessageContent::ToolUse( + language_model::LanguageModelToolUse { + id: tool_use.id, + name: tool_use.name.into(), + raw_input: serde_json::to_string(&tool_use.input) + .unwrap_or_default(), + input: tool_use.input, + is_input_complete: true, + }, + )); + } + + // Convert tool results + let mut tool_results = IndexMap::default(); + for tool_result in msg.tool_results { + let name = tool_names_by_id + .remove(&tool_result.tool_use_id) + .unwrap_or_else(|| SharedString::from("unknown")); + tool_results.insert( + tool_result.tool_use_id.clone(), + language_model::LanguageModelToolResult { + tool_use_id: tool_result.tool_use_id, + tool_name: name.into(), + is_error: tool_result.is_error, + content: tool_result.content, + output: tool_result.output, + }, + ); + } + + crate::Message::Agent(AgentMessage { + content, + tool_results, + }) + } + language_model::Role::System => { + // Skip system messages as they're not supported in the new format + continue; + } + }; + + messages.push(message); + } + + Ok(Self { + title: thread.summary, + messages, + updated_at: thread.updated_at, + summary: thread.detailed_summary_state, + initial_project_snapshot: thread.initial_project_snapshot, + cumulative_token_usage: thread.cumulative_token_usage, + request_token_usage: thread.request_token_usage, + model: thread.model, + completion_mode: thread.completion_mode, + profile: thread.profile, + }) + } +} + +pub static ZED_STATELESS: std::sync::LazyLock<bool> = + std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty())); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum DataType { + #[serde(rename = "json")] + Json, + #[serde(rename = "zstd")] + Zstd, +} + +impl Bind for DataType { + fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> { + let value = match self { + DataType::Json => "json", + DataType::Zstd => "zstd", + }; + value.bind(statement, start_index) + } +} + +impl Column for DataType { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let (value, next_index) = String::column(statement, start_index)?; + let data_type = match value.as_str() { + "json" => DataType::Json, + "zstd" => DataType::Zstd, + _ => anyhow::bail!("Unknown data type: {}", value), + }; + Ok((data_type, next_index)) + } +} + +pub(crate) struct ThreadsDatabase { + executor: BackgroundExecutor, + connection: Arc<Mutex<Connection>>, +} + +struct GlobalThreadsDatabase(Shared<Task<Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>>); + +impl Global for GlobalThreadsDatabase {} + +impl ThreadsDatabase { + pub fn connect(cx: &mut App) -> Shared<Task<Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>> { + if cx.has_global::<GlobalThreadsDatabase>() { + return cx.global::<GlobalThreadsDatabase>().0.clone(); + } + let executor = cx.background_executor().clone(); + let task = executor + .spawn({ + let executor = executor.clone(); + async move { + match ThreadsDatabase::new(executor) { + Ok(db) => Ok(Arc::new(db)), + Err(err) => Err(Arc::new(err)), + } + } + }) + .shared(); + + cx.set_global(GlobalThreadsDatabase(task.clone())); + task + } + + pub fn new(executor: BackgroundExecutor) -> Result<Self> { + let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { + Connection::open_memory(Some("THREAD_FALLBACK_DB")) + } else { + let threads_dir = paths::data_dir().join("threads"); + std::fs::create_dir_all(&threads_dir)?; + let sqlite_path = threads_dir.join("threads.db"); + Connection::open_file(&sqlite_path.to_string_lossy()) + }; + + connection.exec(indoc! {" + CREATE TABLE IF NOT EXISTS threads ( + id TEXT PRIMARY KEY, + summary TEXT NOT NULL, + updated_at TEXT NOT NULL, + data_type TEXT NOT NULL, + data BLOB NOT NULL + ) + "})?() + .map_err(|e| anyhow!("Failed to create threads table: {}", e))?; + + let db = Self { + executor: executor.clone(), + connection: Arc::new(Mutex::new(connection)), + }; + + Ok(db) + } + + fn save_thread_sync( + connection: &Arc<Mutex<Connection>>, + id: acp::SessionId, + thread: DbThread, + ) -> Result<()> { + const COMPRESSION_LEVEL: i32 = 3; + + #[derive(Serialize)] + struct SerializedThread { + #[serde(flatten)] + thread: DbThread, + version: &'static str, + } + + let title = thread.title.to_string(); + let updated_at = thread.updated_at.to_rfc3339(); + let json_data = serde_json::to_string(&SerializedThread { + thread, + version: DbThread::VERSION, + })?; + + let connection = connection.lock(); + + let compressed = zstd::encode_all(json_data.as_bytes(), COMPRESSION_LEVEL)?; + let data_type = DataType::Zstd; + let data = compressed; + + let mut insert = connection.exec_bound::<(Arc<str>, String, String, DataType, Vec<u8>)>(indoc! {" + INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?) + "})?; + + insert((id.0.clone(), title, updated_at, data_type, data))?; + + Ok(()) + } + + pub fn list_threads(&self) -> Task<Result<Vec<DbThreadMetadata>>> { + let connection = self.connection.clone(); + + self.executor.spawn(async move { + let connection = connection.lock(); + + let mut select = + connection.select_bound::<(), (Arc<str>, String, String)>(indoc! {" + SELECT id, summary, updated_at FROM threads ORDER BY updated_at DESC + "})?; + + let rows = select(())?; + let mut threads = Vec::new(); + + for (id, summary, updated_at) in rows { + threads.push(DbThreadMetadata { + id: acp::SessionId(id), + title: summary.into(), + updated_at: DateTime::parse_from_rfc3339(&updated_at)?.with_timezone(&Utc), + }); + } + + Ok(threads) + }) + } + + pub fn load_thread(&self, id: acp::SessionId) -> Task<Result<Option<DbThread>>> { + let connection = self.connection.clone(); + + self.executor.spawn(async move { + let connection = connection.lock(); + let mut select = connection.select_bound::<Arc<str>, (DataType, Vec<u8>)>(indoc! {" + SELECT data_type, data FROM threads WHERE id = ? LIMIT 1 + "})?; + + let rows = select(id.0)?; + if let Some((data_type, data)) = rows.into_iter().next() { + let json_data = match data_type { + DataType::Zstd => { + let decompressed = zstd::decode_all(&data[..])?; + String::from_utf8(decompressed)? + } + DataType::Json => String::from_utf8(data)?, + }; + let thread = DbThread::from_json(json_data.as_bytes())?; + Ok(Some(thread)) + } else { + Ok(None) + } + }) + } + + pub fn save_thread(&self, id: acp::SessionId, thread: DbThread) -> Task<Result<()>> { + let connection = self.connection.clone(); + + self.executor + .spawn(async move { Self::save_thread_sync(&connection, id, thread) }) + } + + pub fn delete_thread(&self, id: acp::SessionId) -> Task<Result<()>> { + let connection = self.connection.clone(); + + self.executor.spawn(async move { + let connection = connection.lock(); + + let mut delete = connection.exec_bound::<Arc<str>>(indoc! {" + DELETE FROM threads WHERE id = ? + "})?; + + delete(id.0)?; + + Ok(()) + }) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use agent::MessageSegment; + use agent::context::LoadedContext; + use client::Client; + use fs::FakeFs; + use gpui::AppContext; + use gpui::TestAppContext; + use http_client::FakeHttpClient; + use language_model::Role; + use project::Project; + use settings::SettingsStore; + + fn init_test(cx: &mut TestAppContext) { + env_logger::try_init().ok(); + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + Project::init_settings(cx); + language::init(cx); + + let http_client = FakeHttpClient::with_404_response(); + let clock = Arc::new(clock::FakeSystemClock::new()); + let client = Client::new(clock, http_client, cx); + agent::init(cx); + agent_settings::init(cx); + language_model::init(client.clone(), cx); + }); + } + + #[gpui::test] + async fn test_retrieving_old_thread(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + + // Save a thread using the old agent. + let thread_store = cx.new(|cx| agent::ThreadStore::fake(project, cx)); + let thread = thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx)); + thread.update(cx, |thread, cx| { + thread.insert_message( + Role::User, + vec![MessageSegment::Text("Hey!".into())], + LoadedContext::default(), + vec![], + false, + cx, + ); + thread.insert_message( + Role::Assistant, + vec![MessageSegment::Text("How're you doing?".into())], + LoadedContext::default(), + vec![], + false, + cx, + ) + }); + thread_store + .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx)) + .await + .unwrap(); + + // Open that same thread using the new agent. + let db = cx.update(ThreadsDatabase::connect).await.unwrap(); + let threads = db.list_threads().await.unwrap(); + assert_eq!(threads.len(), 1); + let thread = db + .load_thread(threads[0].id.clone()) + .await + .unwrap() + .unwrap(); + assert_eq!(thread.messages[0].to_markdown(), "## User\n\nHey!\n"); + assert_eq!( + thread.messages[1].to_markdown(), + "## Assistant\n\nHow're you doing?\n" + ); + } +} diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs new file mode 100644 index 0000000000000000000000000000000000000000..34a5e7b4efe313291dcd04b6c8c2c8f4e1e22e53 --- /dev/null +++ b/crates/agent2/src/history_store.rs @@ -0,0 +1,314 @@ +use crate::{DbThreadMetadata, ThreadsDatabase}; +use agent_client_protocol as acp; +use anyhow::{Context as _, Result, anyhow}; +use assistant_context::SavedContextMetadata; +use chrono::{DateTime, Utc}; +use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*}; +use itertools::Itertools; +use paths::contexts_dir; +use serde::{Deserialize, Serialize}; +use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration}; +use util::ResultExt as _; + +const MAX_RECENTLY_OPENED_ENTRIES: usize = 6; +const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json"; +const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50); + +const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread"); + +#[derive(Clone, Debug)] +pub enum HistoryEntry { + AcpThread(DbThreadMetadata), + TextThread(SavedContextMetadata), +} + +impl HistoryEntry { + pub fn updated_at(&self) -> DateTime<Utc> { + match self { + HistoryEntry::AcpThread(thread) => thread.updated_at, + HistoryEntry::TextThread(context) => context.mtime.to_utc(), + } + } + + pub fn id(&self) -> HistoryEntryId { + match self { + HistoryEntry::AcpThread(thread) => HistoryEntryId::AcpThread(thread.id.clone()), + HistoryEntry::TextThread(context) => HistoryEntryId::TextThread(context.path.clone()), + } + } + + pub fn title(&self) -> &SharedString { + match self { + HistoryEntry::AcpThread(thread) if thread.title.is_empty() => DEFAULT_TITLE, + HistoryEntry::AcpThread(thread) => &thread.title, + HistoryEntry::TextThread(context) => &context.title, + } + } +} + +/// Generic identifier for a history entry. +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum HistoryEntryId { + AcpThread(acp::SessionId), + TextThread(Arc<Path>), +} + +#[derive(Serialize, Deserialize)] +enum SerializedRecentOpen { + Thread(String), + ContextName(String), + /// Old format which stores the full path + Context(String), +} + +pub struct HistoryStore { + threads: Vec<DbThreadMetadata>, + context_store: Entity<assistant_context::ContextStore>, + recently_opened_entries: VecDeque<HistoryEntryId>, + _subscriptions: Vec<gpui::Subscription>, + _save_recently_opened_entries_task: Task<()>, +} + +impl HistoryStore { + pub fn new( + context_store: Entity<assistant_context::ContextStore>, + initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>, + cx: &mut Context<Self>, + ) -> Self { + let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())]; + + cx.spawn(async move |this, cx| { + let entries = Self::load_recently_opened_entries(cx).await.log_err()?; + this.update(cx, |this, _| { + this.recently_opened_entries + .extend( + entries.into_iter().take( + MAX_RECENTLY_OPENED_ENTRIES + .saturating_sub(this.recently_opened_entries.len()), + ), + ); + }) + .ok() + }) + .detach(); + + Self { + context_store, + recently_opened_entries: initial_recent_entries.into_iter().collect(), + threads: Vec::default(), + _subscriptions: subscriptions, + _save_recently_opened_entries_task: Task::ready(()), + } + } + + pub fn delete_thread( + &mut self, + id: acp::SessionId, + cx: &mut Context<Self>, + ) -> Task<Result<()>> { + let database_future = ThreadsDatabase::connect(cx); + cx.spawn(async move |this, cx| { + let database = database_future.await.map_err(|err| anyhow!(err))?; + database.delete_thread(id.clone()).await?; + this.update(cx, |this, cx| this.reload(cx)) + }) + } + + pub fn delete_text_thread( + &mut self, + path: Arc<Path>, + cx: &mut Context<Self>, + ) -> Task<Result<()>> { + self.context_store.update(cx, |context_store, cx| { + context_store.delete_local_context(path, cx) + }) + } + + pub fn reload(&self, cx: &mut Context<Self>) { + let database_future = ThreadsDatabase::connect(cx); + cx.spawn(async move |this, cx| { + let threads = database_future + .await + .map_err(|err| anyhow!(err))? + .list_threads() + .await?; + + this.update(cx, |this, cx| { + this.threads = threads; + cx.notify(); + }) + }) + .detach_and_log_err(cx); + } + + pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> { + let mut history_entries = Vec::new(); + + #[cfg(debug_assertions)] + if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { + return history_entries; + } + + history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread)); + history_entries.extend( + self.context_store + .read(cx) + .unordered_contexts() + .cloned() + .map(HistoryEntry::TextThread), + ); + + history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at())); + history_entries + } + + pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> { + self.entries(cx).into_iter().take(limit).collect() + } + + pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> { + #[cfg(debug_assertions)] + if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { + return Vec::new(); + } + + let thread_entries = self.threads.iter().flat_map(|thread| { + self.recently_opened_entries + .iter() + .enumerate() + .flat_map(|(index, entry)| match entry { + HistoryEntryId::AcpThread(id) if &thread.id == id => { + Some((index, HistoryEntry::AcpThread(thread.clone()))) + } + _ => None, + }) + }); + + let context_entries = + self.context_store + .read(cx) + .unordered_contexts() + .flat_map(|context| { + self.recently_opened_entries + .iter() + .enumerate() + .flat_map(|(index, entry)| match entry { + HistoryEntryId::TextThread(path) if &context.path == path => { + Some((index, HistoryEntry::TextThread(context.clone()))) + } + _ => None, + }) + }); + + thread_entries + .chain(context_entries) + // optimization to halt iteration early + .take(self.recently_opened_entries.len()) + .sorted_unstable_by_key(|(index, _)| *index) + .map(|(_, entry)| entry) + .collect() + } + + fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) { + let serialized_entries = self + .recently_opened_entries + .iter() + .filter_map(|entry| match entry { + HistoryEntryId::TextThread(path) => path.file_name().map(|file| { + SerializedRecentOpen::ContextName(file.to_string_lossy().to_string()) + }), + HistoryEntryId::AcpThread(id) => Some(SerializedRecentOpen::Thread(id.to_string())), + }) + .collect::<Vec<_>>(); + + self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| { + cx.background_executor() + .timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE) + .await; + cx.background_spawn(async move { + let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH); + let content = serde_json::to_string(&serialized_entries)?; + std::fs::write(path, content)?; + anyhow::Ok(()) + }) + .await + .log_err(); + }); + } + + fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<Vec<HistoryEntryId>>> { + cx.background_spawn(async move { + let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH); + let contents = match smol::fs::read_to_string(path).await { + Ok(it) => it, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + return Ok(Vec::new()); + } + Err(e) => { + return Err(e) + .context("deserializing persisted agent panel navigation history"); + } + }; + let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&contents) + .context("deserializing persisted agent panel navigation history")? + .into_iter() + .take(MAX_RECENTLY_OPENED_ENTRIES) + .flat_map(|entry| match entry { + SerializedRecentOpen::Thread(id) => Some(HistoryEntryId::AcpThread( + acp::SessionId(id.as_str().into()), + )), + SerializedRecentOpen::ContextName(file_name) => Some( + HistoryEntryId::TextThread(contexts_dir().join(file_name).into()), + ), + SerializedRecentOpen::Context(path) => { + Path::new(&path).file_name().map(|file_name| { + HistoryEntryId::TextThread(contexts_dir().join(file_name).into()) + }) + } + }) + .collect::<Vec<_>>(); + Ok(entries) + }) + } + + pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context<Self>) { + self.recently_opened_entries + .retain(|old_entry| old_entry != &entry); + self.recently_opened_entries.push_front(entry); + self.recently_opened_entries + .truncate(MAX_RECENTLY_OPENED_ENTRIES); + self.save_recently_opened_entries(cx); + } + + pub fn remove_recently_opened_thread(&mut self, id: acp::SessionId, cx: &mut Context<Self>) { + self.recently_opened_entries.retain(|entry| match entry { + HistoryEntryId::AcpThread(thread_id) if thread_id == &id => false, + _ => true, + }); + self.save_recently_opened_entries(cx); + } + + pub fn replace_recently_opened_text_thread( + &mut self, + old_path: &Path, + new_path: &Arc<Path>, + cx: &mut Context<Self>, + ) { + for entry in &mut self.recently_opened_entries { + match entry { + HistoryEntryId::TextThread(path) if path.as_ref() == old_path => { + *entry = HistoryEntryId::TextThread(new_path.clone()); + break; + } + _ => {} + } + } + self.save_recently_opened_entries(cx); + } + + pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context<Self>) { + self.recently_opened_entries + .retain(|old_entry| old_entry != entry); + self.save_recently_opened_entries(cx); + } +} diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 6f09ee117574c23c22761e3b7ef3d011d0a11f6a..f8cf3dd602c2a27a0062bdb1ad71901e920f9387 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -7,16 +7,17 @@ use gpui::{App, Entity, Task}; use project::Project; use prompt_store::PromptStore; -use crate::{NativeAgent, NativeAgentConnection, templates::Templates}; +use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates}; #[derive(Clone)] pub struct NativeAgentServer { fs: Arc<dyn Fs>, + history: Entity<HistoryStore>, } impl NativeAgentServer { - pub fn new(fs: Arc<dyn Fs>) -> Self { - Self { fs } + pub fn new(fs: Arc<dyn Fs>, history: Entity<HistoryStore>) -> Self { + Self { fs, history } } } @@ -50,6 +51,7 @@ impl AgentServer for NativeAgentServer { ); let project = project.clone(); let fs = self.fs.clone(); + let history = self.history.clone(); let prompt_store = PromptStore::global(cx); cx.spawn(async move |cx| { log::debug!("Creating templates for native agent"); @@ -57,7 +59,8 @@ impl AgentServer for NativeAgentServer { let prompt_store = prompt_store.await?; log::debug!("Creating native agent entity"); - let agent = NativeAgent::new(project, templates, Some(prompt_store), fs, cx).await?; + let agent = + NativeAgent::new(project, history, templates, Some(prompt_store), fs, cx).await?; // Create the connection wrapper let connection = NativeAgentConnection(agent); diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 33706b05dea6b52b044faf8fd6833a7e0f8119f2..f01873cfc109c1191f062402a03565541ab18851 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1273,10 +1273,13 @@ async fn test_agent_connection(cx: &mut TestAppContext) { fake_fs.insert_tree(path!("/test"), json!({})).await; let project = Project::test(fake_fs.clone(), [Path::new("/test")], cx).await; let cwd = Path::new("/test"); + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); // Create agent and connection let agent = NativeAgent::new( project.clone(), + history_store, templates.clone(), None, fake_fs.clone(), @@ -1756,7 +1759,6 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { action_log, templates, Some(model.clone()), - None, cx, ) }); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index d90d0bd4f84664f4c8912e22ab9865d94a0ccf68..66b4485f72981cf2a40a5fcee1a590462b8295a0 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,4 +1,9 @@ -use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates}; +use crate::{ + ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread, + DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool, + ListDirectoryTool, MovePathTool, NowTool, OpenTool, ReadFileTool, SystemPromptTemplate, + Template, Templates, TerminalTool, ThinkingTool, WebSearchTool, +}; use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; use agent::thread::{DetailedSummaryState, GitState, ProjectSnapshot, WorktreeSnapshot}; @@ -17,13 +22,13 @@ use futures::{ stream::FuturesUnordered, }; use git::repository::DiffType; -use gpui::{App, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; +use gpui::{App, AppContext, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, - LanguageModelProviderId, LanguageModelRequest, LanguageModelRequestMessage, - LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, - LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, - TokenUsage, + LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, + LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse, + LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, }; use project::{ Project, @@ -516,8 +521,8 @@ pub struct Thread { templates: Arc<Templates>, model: Option<Arc<dyn LanguageModel>>, summarization_model: Option<Arc<dyn LanguageModel>>, - project: Entity<Project>, - action_log: Entity<ActionLog>, + pub(crate) project: Entity<Project>, + pub(crate) action_log: Entity<ActionLog>, } impl Thread { @@ -528,7 +533,6 @@ impl Thread { action_log: Entity<ActionLog>, templates: Arc<Templates>, model: Option<Arc<dyn LanguageModel>>, - summarization_model: Option<Arc<dyn LanguageModel>>, cx: &mut Context<Self>, ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); @@ -557,7 +561,7 @@ impl Thread { project_context, templates, model, - summarization_model, + summarization_model: None, project, action_log, } @@ -652,6 +656,88 @@ impl Thread { ); } + pub fn from_db( + id: acp::SessionId, + db_thread: DbThread, + project: Entity<Project>, + project_context: Entity<ProjectContext>, + context_server_registry: Entity<ContextServerRegistry>, + action_log: Entity<ActionLog>, + templates: Arc<Templates>, + cx: &mut Context<Self>, + ) -> Self { + let profile_id = db_thread + .profile + .unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone()); + let model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| { + db_thread + .model + .and_then(|model| { + let model = SelectedModel { + provider: model.provider.clone().into(), + model: model.model.clone().into(), + }; + registry.select_model(&model, cx) + }) + .or_else(|| registry.default_model()) + .map(|model| model.model) + }); + + Self { + id, + prompt_id: PromptId::new(), + title: if db_thread.title.is_empty() { + None + } else { + Some(db_thread.title.clone()) + }, + summary: db_thread.summary, + messages: db_thread.messages, + completion_mode: db_thread.completion_mode.unwrap_or_default(), + running_turn: None, + pending_message: None, + tools: BTreeMap::default(), + tool_use_limit_reached: false, + request_token_usage: db_thread.request_token_usage.clone(), + cumulative_token_usage: db_thread.cumulative_token_usage, + initial_project_snapshot: Task::ready(db_thread.initial_project_snapshot).shared(), + context_server_registry, + profile_id, + project_context, + templates, + model, + summarization_model: None, + project, + action_log, + updated_at: db_thread.updated_at, + } + } + + pub fn to_db(&self, cx: &App) -> Task<DbThread> { + let initial_project_snapshot = self.initial_project_snapshot.clone(); + let mut thread = DbThread { + title: self.title.clone().unwrap_or_default(), + messages: self.messages.clone(), + updated_at: self.updated_at, + summary: self.summary.clone(), + initial_project_snapshot: None, + cumulative_token_usage: self.cumulative_token_usage, + request_token_usage: self.request_token_usage.clone(), + model: self.model.as_ref().map(|model| DbLanguageModel { + provider: model.provider_id().to_string(), + model: model.name().0.to_string(), + }), + completion_mode: Some(self.completion_mode), + profile: Some(self.profile_id.clone()), + }; + + cx.background_spawn(async move { + let initial_project_snapshot = initial_project_snapshot.await; + thread.initial_project_snapshot = initial_project_snapshot; + thread + }) + } + /// Create a snapshot of the current project state including git information and unsaved buffers. fn project_snapshot( project: Entity<Project>, @@ -816,6 +902,32 @@ impl Thread { } } + pub fn add_default_tools(&mut self, cx: &mut Context<Self>) { + let language_registry = self.project.read(cx).languages().clone(); + self.add_tool(CopyPathTool::new(self.project.clone())); + self.add_tool(CreateDirectoryTool::new(self.project.clone())); + self.add_tool(DeletePathTool::new( + self.project.clone(), + self.action_log.clone(), + )); + self.add_tool(DiagnosticsTool::new(self.project.clone())); + self.add_tool(EditFileTool::new(cx.weak_entity(), language_registry)); + self.add_tool(FetchTool::new(self.project.read(cx).client().http_client())); + self.add_tool(FindPathTool::new(self.project.clone())); + self.add_tool(GrepTool::new(self.project.clone())); + self.add_tool(ListDirectoryTool::new(self.project.clone())); + self.add_tool(MovePathTool::new(self.project.clone())); + self.add_tool(NowTool); + self.add_tool(OpenTool::new(self.project.clone())); + self.add_tool(ReadFileTool::new( + self.project.clone(), + self.action_log.clone(), + )); + self.add_tool(TerminalTool::new(self.project.clone(), cx)); + self.add_tool(ThinkingTool); + self.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model. + } + pub fn add_tool(&mut self, tool: impl AgentTool) { self.tools.insert(tool.name(), tool.erase()); } diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index b3b1a428bff1b21744952148dc6196de62f8546c..21eb282110100b89abcdce4047a0ace6797408ca 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -554,7 +554,6 @@ mod tests { action_log, Templates::new(), Some(model), - None, cx, ) }); @@ -756,7 +755,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); @@ -899,7 +897,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); @@ -1029,7 +1026,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); @@ -1168,7 +1164,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); @@ -1279,7 +1274,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); @@ -1362,7 +1356,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); @@ -1448,7 +1441,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); @@ -1531,7 +1523,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); diff --git a/crates/agent_ui/src/acp.rs b/crates/agent_ui/src/acp.rs index 831d296eebcf7edd29f3f84acbf6a7824be47a1b..6f228b91d6ed74c7cb330aecd1c9de07e386bed1 100644 --- a/crates/agent_ui/src/acp.rs +++ b/crates/agent_ui/src/acp.rs @@ -3,8 +3,10 @@ mod entry_view_state; mod message_editor; mod model_selector; mod model_selector_popover; +mod thread_history; mod thread_view; pub use model_selector::AcpModelSelector; pub use model_selector_popover::AcpModelSelectorPopover; +pub use thread_history::*; pub use thread_view::AcpThreadView; diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs new file mode 100644 index 0000000000000000000000000000000000000000..8a058011394739f46028fa46ed57244aab91cc1f --- /dev/null +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -0,0 +1,766 @@ +use crate::RemoveSelectedThread; +use agent2::{HistoryEntry, HistoryStore}; +use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; +use editor::{Editor, EditorEvent}; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, + UniformListScrollHandle, Window, uniform_list, +}; +use std::{fmt::Display, ops::Range, sync::Arc}; +use time::{OffsetDateTime, UtcOffset}; +use ui::{ + HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, + Tooltip, prelude::*, +}; +use util::ResultExt; + +pub struct AcpThreadHistory { + pub(crate) history_store: Entity<HistoryStore>, + scroll_handle: UniformListScrollHandle, + selected_index: usize, + hovered_index: Option<usize>, + search_editor: Entity<Editor>, + all_entries: Arc<Vec<HistoryEntry>>, + // When the search is empty, we display date separators between history entries + // This vector contains an enum of either a separator or an actual entry + separated_items: Vec<ListItemType>, + // Maps entry indexes to list item indexes + separated_item_indexes: Vec<u32>, + _separated_items_task: Option<Task<()>>, + search_state: SearchState, + scrollbar_visibility: bool, + scrollbar_state: ScrollbarState, + local_timezone: UtcOffset, + _subscriptions: Vec<gpui::Subscription>, +} + +enum SearchState { + Empty, + Searching { + query: SharedString, + _task: Task<()>, + }, + Searched { + query: SharedString, + matches: Vec<StringMatch>, + }, +} + +enum ListItemType { + BucketSeparator(TimeBucket), + Entry { + index: usize, + format: EntryTimeFormat, + }, +} + +pub enum ThreadHistoryEvent { + Open(HistoryEntry), +} + +impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {} + +impl AcpThreadHistory { + pub(crate) fn new( + history_store: Entity<agent2::HistoryStore>, + window: &mut Window, + cx: &mut Context<Self>, + ) -> Self { + let search_editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_placeholder_text("Search threads...", cx); + editor + }); + + let search_editor_subscription = + cx.subscribe(&search_editor, |this, search_editor, event, cx| { + if let EditorEvent::BufferEdited = event { + let query = search_editor.read(cx).text(cx); + this.search(query.into(), cx); + } + }); + + let history_store_subscription = cx.observe(&history_store, |this, _, cx| { + this.update_all_entries(cx); + }); + + let scroll_handle = UniformListScrollHandle::default(); + let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); + + let mut this = Self { + history_store, + scroll_handle, + selected_index: 0, + hovered_index: None, + search_state: SearchState::Empty, + all_entries: Default::default(), + separated_items: Default::default(), + separated_item_indexes: Default::default(), + search_editor, + scrollbar_visibility: true, + scrollbar_state, + local_timezone: UtcOffset::from_whole_seconds( + chrono::Local::now().offset().local_minus_utc(), + ) + .unwrap(), + _subscriptions: vec![search_editor_subscription, history_store_subscription], + _separated_items_task: None, + }; + this.update_all_entries(cx); + this + } + + fn update_all_entries(&mut self, cx: &mut Context<Self>) { + let new_entries: Arc<Vec<HistoryEntry>> = self + .history_store + .update(cx, |store, cx| store.entries(cx)) + .into(); + + self._separated_items_task.take(); + + let mut items = Vec::with_capacity(new_entries.len() + 1); + let mut indexes = Vec::with_capacity(new_entries.len() + 1); + + let bg_task = cx.background_spawn(async move { + let mut bucket = None; + let today = Local::now().naive_local().date(); + + for (index, entry) in new_entries.iter().enumerate() { + let entry_date = entry + .updated_at() + .with_timezone(&Local) + .naive_local() + .date(); + let entry_bucket = TimeBucket::from_dates(today, entry_date); + + if Some(entry_bucket) != bucket { + bucket = Some(entry_bucket); + items.push(ListItemType::BucketSeparator(entry_bucket)); + } + + indexes.push(items.len() as u32); + items.push(ListItemType::Entry { + index, + format: entry_bucket.into(), + }); + } + (new_entries, items, indexes) + }); + + let task = cx.spawn(async move |this, cx| { + let (new_entries, items, indexes) = bg_task.await; + this.update(cx, |this, cx| { + let previously_selected_entry = + this.all_entries.get(this.selected_index).map(|e| e.id()); + + this.all_entries = new_entries; + this.separated_items = items; + this.separated_item_indexes = indexes; + + match &this.search_state { + SearchState::Empty => { + if this.selected_index >= this.all_entries.len() { + this.set_selected_entry_index( + this.all_entries.len().saturating_sub(1), + cx, + ); + } else if let Some(prev_id) = previously_selected_entry + && let Some(new_ix) = this + .all_entries + .iter() + .position(|probe| probe.id() == prev_id) + { + this.set_selected_entry_index(new_ix, cx); + } + } + SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => { + this.search(query.clone(), cx); + } + } + + cx.notify(); + }) + .log_err(); + }); + self._separated_items_task = Some(task); + } + + fn search(&mut self, query: SharedString, cx: &mut Context<Self>) { + if query.is_empty() { + self.search_state = SearchState::Empty; + cx.notify(); + return; + } + + let all_entries = self.all_entries.clone(); + + let fuzzy_search_task = cx.background_spawn({ + let query = query.clone(); + let executor = cx.background_executor().clone(); + async move { + let mut candidates = Vec::with_capacity(all_entries.len()); + + for (idx, entry) in all_entries.iter().enumerate() { + candidates.push(StringMatchCandidate::new(idx, entry.title())); + } + + const MAX_MATCHES: usize = 100; + + fuzzy::match_strings( + &candidates, + &query, + false, + true, + MAX_MATCHES, + &Default::default(), + executor, + ) + .await + } + }); + + let task = cx.spawn({ + let query = query.clone(); + async move |this, cx| { + let matches = fuzzy_search_task.await; + + this.update(cx, |this, cx| { + let SearchState::Searching { + query: current_query, + _task, + } = &this.search_state + else { + return; + }; + + if &query == current_query { + this.search_state = SearchState::Searched { + query: query.clone(), + matches, + }; + + this.set_selected_entry_index(0, cx); + cx.notify(); + }; + }) + .log_err(); + } + }); + + self.search_state = SearchState::Searching { query, _task: task }; + cx.notify(); + } + + fn matched_count(&self) -> usize { + match &self.search_state { + SearchState::Empty => self.all_entries.len(), + SearchState::Searching { .. } => 0, + SearchState::Searched { matches, .. } => matches.len(), + } + } + + fn list_item_count(&self) -> usize { + match &self.search_state { + SearchState::Empty => self.separated_items.len(), + SearchState::Searching { .. } => 0, + SearchState::Searched { matches, .. } => matches.len(), + } + } + + fn search_produced_no_matches(&self) -> bool { + match &self.search_state { + SearchState::Empty => false, + SearchState::Searching { .. } => false, + SearchState::Searched { matches, .. } => matches.is_empty(), + } + } + + fn get_match(&self, ix: usize) -> Option<&HistoryEntry> { + match &self.search_state { + SearchState::Empty => self.all_entries.get(ix), + SearchState::Searching { .. } => None, + SearchState::Searched { matches, .. } => matches + .get(ix) + .and_then(|m| self.all_entries.get(m.candidate_id)), + } + } + + pub fn select_previous( + &mut self, + _: &menu::SelectPrevious, + _window: &mut Window, + cx: &mut Context<Self>, + ) { + let count = self.matched_count(); + if count > 0 { + if self.selected_index == 0 { + self.set_selected_entry_index(count - 1, cx); + } else { + self.set_selected_entry_index(self.selected_index - 1, cx); + } + } + } + + pub fn select_next( + &mut self, + _: &menu::SelectNext, + _window: &mut Window, + cx: &mut Context<Self>, + ) { + let count = self.matched_count(); + if count > 0 { + if self.selected_index == count - 1 { + self.set_selected_entry_index(0, cx); + } else { + self.set_selected_entry_index(self.selected_index + 1, cx); + } + } + } + + fn select_first( + &mut self, + _: &menu::SelectFirst, + _window: &mut Window, + cx: &mut Context<Self>, + ) { + let count = self.matched_count(); + if count > 0 { + self.set_selected_entry_index(0, cx); + } + } + + fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) { + let count = self.matched_count(); + if count > 0 { + self.set_selected_entry_index(count - 1, cx); + } + } + + fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context<Self>) { + self.selected_index = entry_index; + + let scroll_ix = match self.search_state { + SearchState::Empty | SearchState::Searching { .. } => self + .separated_item_indexes + .get(entry_index) + .map(|ix| *ix as usize) + .unwrap_or(entry_index + 1), + SearchState::Searched { .. } => entry_index, + }; + + self.scroll_handle + .scroll_to_item(scroll_ix, ScrollStrategy::Top); + + cx.notify(); + } + + fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> { + if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) { + return None; + } + + Some( + div() + .occlude() + .id("thread-history-scroll") + .h_full() + .bg(cx.theme().colors().panel_background.opacity(0.8)) + .border_l_1() + .border_color(cx.theme().colors().border_variant) + .absolute() + .right_1() + .top_0() + .bottom_0() + .w_4() + .pl_1() + .cursor_default() + .on_mouse_move(cx.listener(|_, _, _window, cx| { + cx.notify(); + cx.stop_propagation() + })) + .on_hover(|_, _window, cx| { + cx.stop_propagation(); + }) + .on_any_mouse_down(|_, _window, cx| { + cx.stop_propagation(); + }) + .on_scroll_wheel(cx.listener(|_, _, _window, cx| { + cx.notify(); + })) + .children(Scrollbar::vertical(self.scrollbar_state.clone())), + ) + } + + fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) { + self.confirm_entry(self.selected_index, cx); + } + + fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) { + let Some(entry) = self.get_match(ix) else { + return; + }; + cx.emit(ThreadHistoryEvent::Open(entry.clone())); + } + + fn remove_selected_thread( + &mut self, + _: &RemoveSelectedThread, + _window: &mut Window, + cx: &mut Context<Self>, + ) { + self.remove_thread(self.selected_index, cx) + } + + fn remove_thread(&mut self, ix: usize, cx: &mut Context<Self>) { + let Some(entry) = self.get_match(ix) else { + return; + }; + + let task = match entry { + HistoryEntry::AcpThread(thread) => self + .history_store + .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)), + HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| { + this.delete_text_thread(context.path.clone(), cx) + }), + }; + task.detach_and_log_err(cx); + } + + fn list_items( + &mut self, + range: Range<usize>, + _window: &mut Window, + cx: &mut Context<Self>, + ) -> Vec<AnyElement> { + match &self.search_state { + SearchState::Empty => self + .separated_items + .get(range) + .iter() + .flat_map(|items| { + items + .iter() + .map(|item| self.render_list_item(item, vec![], cx)) + }) + .collect(), + SearchState::Searched { matches, .. } => matches[range] + .iter() + .filter_map(|m| { + let entry = self.all_entries.get(m.candidate_id)?; + Some(self.render_history_entry( + entry, + EntryTimeFormat::DateAndTime, + m.candidate_id, + m.positions.clone(), + cx, + )) + }) + .collect(), + SearchState::Searching { .. } => { + vec![] + } + } + } + + fn render_list_item( + &self, + item: &ListItemType, + highlight_positions: Vec<usize>, + cx: &Context<Self>, + ) -> AnyElement { + match item { + ListItemType::Entry { index, format } => match self.all_entries.get(*index) { + Some(entry) => self + .render_history_entry(entry, *format, *index, highlight_positions, cx) + .into_any(), + None => Empty.into_any_element(), + }, + ListItemType::BucketSeparator(bucket) => div() + .px(DynamicSpacing::Base06.rems(cx)) + .pt_2() + .pb_1() + .child( + Label::new(bucket.to_string()) + .size(LabelSize::XSmall) + .color(Color::Muted), + ) + .into_any_element(), + } + } + + fn render_history_entry( + &self, + entry: &HistoryEntry, + format: EntryTimeFormat, + list_entry_ix: usize, + highlight_positions: Vec<usize>, + cx: &Context<Self>, + ) -> AnyElement { + let selected = list_entry_ix == self.selected_index; + let hovered = Some(list_entry_ix) == self.hovered_index; + let timestamp = entry.updated_at().timestamp(); + let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone); + + h_flex() + .w_full() + .pb_1() + .child( + ListItem::new(list_entry_ix) + .rounded() + .toggle_state(selected) + .spacing(ListItemSpacing::Sparse) + .start_slot( + h_flex() + .w_full() + .gap_2() + .justify_between() + .child( + HighlightedLabel::new(entry.title(), highlight_positions) + .size(LabelSize::Small) + .truncate(), + ) + .child( + Label::new(thread_timestamp) + .color(Color::Muted) + .size(LabelSize::XSmall), + ), + ) + .on_hover(cx.listener(move |this, is_hovered, _window, cx| { + if *is_hovered { + this.hovered_index = Some(list_entry_ix); + } else if this.hovered_index == Some(list_entry_ix) { + this.hovered_index = None; + } + + cx.notify(); + })) + .end_slot::<IconButton>(if hovered || selected { + Some( + IconButton::new("delete", IconName::Trash) + .shape(IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .tooltip(move |window, cx| { + Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) + }) + .on_click(cx.listener(move |this, _, _, cx| { + this.remove_thread(list_entry_ix, cx) + })), + ) + } else { + None + }) + .on_click( + cx.listener(move |this, _, _, cx| this.confirm_entry(list_entry_ix, cx)), + ), + ) + .into_any_element() + } +} + +impl Focusable for AcpThreadHistory { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.search_editor.focus_handle(cx) + } +} + +impl Render for AcpThreadHistory { + fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { + v_flex() + .key_context("ThreadHistory") + .size_full() + .on_action(cx.listener(Self::select_previous)) + .on_action(cx.listener(Self::select_next)) + .on_action(cx.listener(Self::select_first)) + .on_action(cx.listener(Self::select_last)) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::remove_selected_thread)) + .when(!self.all_entries.is_empty(), |parent| { + parent.child( + h_flex() + .h(px(41.)) // Match the toolbar perfectly + .w_full() + .py_1() + .px_2() + .gap_2() + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border) + .child( + Icon::new(IconName::MagnifyingGlass) + .color(Color::Muted) + .size(IconSize::Small), + ) + .child(self.search_editor.clone()), + ) + }) + .child({ + let view = v_flex() + .id("list-container") + .relative() + .overflow_hidden() + .flex_grow(); + + if self.all_entries.is_empty() { + view.justify_center() + .child( + h_flex().w_full().justify_center().child( + Label::new("You don't have any past threads yet.") + .size(LabelSize::Small), + ), + ) + } else if self.search_produced_no_matches() { + view.justify_center().child( + h_flex().w_full().justify_center().child( + Label::new("No threads match your search.").size(LabelSize::Small), + ), + ) + } else { + view.pr_5() + .child( + uniform_list( + "thread-history", + self.list_item_count(), + cx.processor(|this, range: Range<usize>, window, cx| { + this.list_items(range, window, cx) + }), + ) + .p_1() + .track_scroll(self.scroll_handle.clone()) + .flex_grow(), + ) + .when_some(self.render_scrollbar(cx), |div, scrollbar| { + div.child(scrollbar) + }) + } + }) + } +} + +#[derive(Clone, Copy)] +pub enum EntryTimeFormat { + DateAndTime, + TimeOnly, +} + +impl EntryTimeFormat { + fn format_timestamp(&self, timestamp: i64, timezone: UtcOffset) -> String { + let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap(); + + match self { + EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp( + timestamp, + OffsetDateTime::now_utc(), + timezone, + time_format::TimestampFormat::EnhancedAbsolute, + ), + EntryTimeFormat::TimeOnly => time_format::format_time(timestamp), + } + } +} + +impl From<TimeBucket> for EntryTimeFormat { + fn from(bucket: TimeBucket) -> Self { + match bucket { + TimeBucket::Today => EntryTimeFormat::TimeOnly, + TimeBucket::Yesterday => EntryTimeFormat::TimeOnly, + TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime, + TimeBucket::PastWeek => EntryTimeFormat::DateAndTime, + TimeBucket::All => EntryTimeFormat::DateAndTime, + } + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +enum TimeBucket { + Today, + Yesterday, + ThisWeek, + PastWeek, + All, +} + +impl TimeBucket { + fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self { + if date == reference { + return TimeBucket::Today; + } + + if date == reference - TimeDelta::days(1) { + return TimeBucket::Yesterday; + } + + let week = date.iso_week(); + + if reference.iso_week() == week { + return TimeBucket::ThisWeek; + } + + let last_week = (reference - TimeDelta::days(7)).iso_week(); + + if week == last_week { + return TimeBucket::PastWeek; + } + + TimeBucket::All + } +} + +impl Display for TimeBucket { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TimeBucket::Today => write!(f, "Today"), + TimeBucket::Yesterday => write!(f, "Yesterday"), + TimeBucket::ThisWeek => write!(f, "This Week"), + TimeBucket::PastWeek => write!(f, "Past Week"), + TimeBucket::All => write!(f, "All"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::NaiveDate; + + #[test] + fn test_time_bucket_from_dates() { + let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap(); + + let date = today; + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today); + + let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday); + + let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek); + + let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek); + + let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek); + + let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek); + + // All: not in this week or last week + let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All); + + // Test year boundary cases + let new_year = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(); + + let date = NaiveDate::from_ymd_opt(2022, 12, 31).unwrap(); + assert_eq!( + TimeBucket::from_dates(new_year, date), + TimeBucket::Yesterday + ); + + let date = NaiveDate::from_ymd_opt(2022, 12, 28).unwrap(); + assert_eq!(TimeBucket::from_dates(new_year, date), TimeBucket::ThisWeek); + } +} diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 150f1ea73bf8018a09fd169a408914adb052c0fc..bf5b8efbc80e71d0d691a0794d8eec13654a5e47 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -9,6 +9,7 @@ use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::{self as acp}; use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; +use agent2::DbThreadMetadata; use anyhow::bail; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; @@ -155,6 +156,7 @@ enum ThreadState { impl AcpThreadView { pub fn new( agent: Rc<dyn AgentServer>, + resume_thread: Option<DbThreadMetadata>, workspace: WeakEntity<Workspace>, project: Entity<Project>, thread_store: Entity<ThreadStore>, @@ -203,7 +205,7 @@ impl AcpThreadView { workspace: workspace.clone(), project: project.clone(), entry_view_state, - thread_state: Self::initial_state(agent, workspace, project, window, cx), + thread_state: Self::initial_state(agent, resume_thread, workspace, project, window, cx), message_editor, model_selector: None, profile_selector: None, @@ -228,6 +230,7 @@ impl AcpThreadView { fn initial_state( agent: Rc<dyn AgentServer>, + resume_thread: Option<DbThreadMetadata>, workspace: WeakEntity<Workspace>, project: Entity<Project>, window: &mut Window, @@ -254,28 +257,27 @@ impl AcpThreadView { } }; - // this.update_in(cx, |_this, _window, cx| { - // let status = connection.exit_status(cx); - // cx.spawn(async move |this, cx| { - // let status = status.await.ok(); - // this.update(cx, |this, cx| { - // this.thread_state = ThreadState::ServerExited { status }; - // cx.notify(); - // }) - // .ok(); - // }) - // .detach(); - // }) - // .ok(); - - let Some(result) = cx - .update(|_, cx| { + let result = if let Some(native_agent) = connection + .clone() + .downcast::<agent2::NativeAgentConnection>() + && let Some(resume) = resume_thread + { + cx.update(|_, cx| { + native_agent + .0 + .update(cx, |agent, cx| agent.open_thread(resume.id, cx)) + }) + .log_err() + } else { + cx.update(|_, cx| { connection .clone() .new_thread(project.clone(), &root_dir, cx) }) .log_err() - else { + }; + + let Some(result) = result else { return; }; @@ -382,6 +384,7 @@ impl AcpThreadView { this.update(cx, |this, cx| { this.thread_state = Self::initial_state( agent.clone(), + None, this.workspace.clone(), this.project.clone(), window, @@ -842,6 +845,7 @@ impl AcpThreadView { } else { this.thread_state = Self::initial_state( agent, + None, this.workspace.clone(), project.clone(), window, @@ -4044,6 +4048,7 @@ pub(crate) mod tests { cx.new(|cx| { AcpThreadView::new( Rc::new(agent), + None, workspace.downgrade(), project, thread_store.clone(), @@ -4248,6 +4253,7 @@ pub(crate) mod tests { cx.new(|cx| { AcpThreadView::new( Rc::new(StubAgentServer::new(connection.as_ref().clone())), + None, workspace.downgrade(), project.clone(), thread_store.clone(), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 995bf771e266c1057406c8b98f99fcc8d6b2349b..f9aea843768b755ffccfcc4564c4cf2a7b55e7ea 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -4,10 +4,11 @@ use std::rc::Rc; use std::sync::Arc; use std::time::Duration; +use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; -use crate::NewExternalAgentThread; +use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::agent_diff::AgentDiffThread; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, @@ -28,6 +29,7 @@ use crate::{ thread_history::{HistoryEntryElement, ThreadHistory}, ui::{AgentOnboardingModal, EndTrialUpsell}, }; +use crate::{ExternalAgent, NewExternalAgentThread}; use agent::{ Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio, context_store::ContextStore, @@ -117,7 +119,7 @@ pub fn init(cx: &mut App) { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| { - panel.new_external_thread(action.agent, window, cx) + panel.external_thread(action.agent, None, window, cx) }); } }) @@ -360,6 +362,7 @@ impl ActiveView { pub fn prompt_editor( context_editor: Entity<TextThreadEditor>, history_store: Entity<HistoryStore>, + acp_history_store: Entity<agent2::HistoryStore>, language_registry: Arc<LanguageRegistry>, window: &mut Window, cx: &mut App, @@ -437,6 +440,18 @@ impl ActiveView { ); } }); + + acp_history_store.update(cx, |history_store, cx| { + if let Some(old_path) = old_path { + history_store + .replace_recently_opened_text_thread(old_path, new_path, cx); + } else { + history_store.push_recently_opened_entry( + agent2::HistoryEntryId::TextThread(new_path.clone()), + cx, + ); + } + }); } _ => {} } @@ -465,6 +480,8 @@ pub struct AgentPanel { fs: Arc<dyn Fs>, language_registry: Arc<LanguageRegistry>, thread_store: Entity<ThreadStore>, + acp_history: Entity<AcpThreadHistory>, + acp_history_store: Entity<agent2::HistoryStore>, _default_model_subscription: Subscription, context_store: Entity<TextThreadStore>, prompt_store: Option<Entity<PromptStore>>, @@ -631,6 +648,29 @@ impl AgentPanel { ) }); + let acp_history_store = + cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), [], cx)); + let acp_history = cx.new(|cx| AcpThreadHistory::new(acp_history_store.clone(), window, cx)); + cx.subscribe_in( + &acp_history, + window, + |this, _, event, window, cx| match event { + ThreadHistoryEvent::Open(HistoryEntry::AcpThread(thread)) => { + this.external_thread( + Some(crate::ExternalAgent::NativeAgent), + Some(thread.clone()), + window, + cx, + ); + } + ThreadHistoryEvent::Open(HistoryEntry::TextThread(thread)) => { + this.open_saved_prompt_editor(thread.path.clone(), window, cx) + .detach_and_log_err(cx); + } + }, + ) + .detach(); + cx.observe(&history_store, |_, _, cx| cx.notify()).detach(); let active_thread = cx.new(|cx| { @@ -669,6 +709,7 @@ impl AgentPanel { ActiveView::prompt_editor( context_editor, history_store.clone(), + acp_history_store.clone(), language_registry.clone(), window, cx, @@ -685,7 +726,11 @@ impl AgentPanel { let assistant_navigation_menu = ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| { if let Some(panel) = panel.upgrade() { - menu = Self::populate_recently_opened_menu_section(menu, panel, cx); + if cx.has_flag::<AcpFeatureFlag>() { + menu = Self::populate_recently_opened_menu_section_new(menu, panel, cx); + } else { + menu = Self::populate_recently_opened_menu_section_old(menu, panel, cx); + } } menu.action("View All", Box::new(OpenHistory)) .end_slot_action(DeleteRecentlyOpenThread.boxed_clone()) @@ -773,6 +818,8 @@ impl AgentPanel { zoomed: false, pending_serialization: None, onboarding, + acp_history, + acp_history_store, selected_agent: AgentType::default(), } } @@ -939,6 +986,7 @@ impl AgentPanel { ActiveView::prompt_editor( context_editor.clone(), self.history_store.clone(), + self.acp_history_store.clone(), self.language_registry.clone(), window, cx, @@ -949,9 +997,10 @@ impl AgentPanel { context_editor.focus_handle(cx).focus(window); } - fn new_external_thread( + fn external_thread( &mut self, agent_choice: Option<crate::ExternalAgent>, + resume_thread: Option<DbThreadMetadata>, window: &mut Window, cx: &mut Context<Self>, ) { @@ -968,6 +1017,7 @@ impl AgentPanel { let thread_store = self.thread_store.clone(); let text_thread_store = self.context_store.clone(); + let history = self.acp_history_store.clone(); cx.spawn_in(window, async move |this, cx| { let ext_agent = match agent_choice { @@ -1001,7 +1051,7 @@ impl AgentPanel { } }; - let server = ext_agent.server(fs); + let server = ext_agent.server(fs, history); this.update_in(cx, |this, window, cx| { match ext_agent { @@ -1020,6 +1070,7 @@ impl AgentPanel { let thread_view = cx.new(|cx| { crate::acp::AcpThreadView::new( server, + resume_thread, workspace.clone(), project, thread_store.clone(), @@ -1114,6 +1165,7 @@ impl AgentPanel { ActiveView::prompt_editor( editor.clone(), self.history_store.clone(), + self.acp_history_store.clone(), self.language_registry.clone(), window, cx, @@ -1580,7 +1632,7 @@ impl AgentPanel { self.focus_handle(cx).focus(window); } - fn populate_recently_opened_menu_section( + fn populate_recently_opened_menu_section_old( mut menu: ContextMenu, panel: Entity<Self>, cx: &mut Context<ContextMenu>, @@ -1644,6 +1696,72 @@ impl AgentPanel { menu } + fn populate_recently_opened_menu_section_new( + mut menu: ContextMenu, + panel: Entity<Self>, + cx: &mut Context<ContextMenu>, + ) -> ContextMenu { + let entries = panel + .read(cx) + .acp_history_store + .read(cx) + .recently_opened_entries(cx); + + if entries.is_empty() { + return menu; + } + + menu = menu.header("Recently Opened"); + + for entry in entries { + let title = entry.title().clone(); + + menu = menu.entry_with_end_slot_on_hover( + title, + None, + { + let panel = panel.downgrade(); + let entry = entry.clone(); + move |window, cx| { + let entry = entry.clone(); + panel + .update(cx, move |this, cx| match &entry { + agent2::HistoryEntry::AcpThread(entry) => this.external_thread( + Some(ExternalAgent::NativeAgent), + Some(entry.clone()), + window, + cx, + ), + agent2::HistoryEntry::TextThread(entry) => this + .open_saved_prompt_editor(entry.path.clone(), window, cx) + .detach_and_log_err(cx), + }) + .ok(); + } + }, + IconName::Close, + "Close Entry".into(), + { + let panel = panel.downgrade(); + let id = entry.id(); + move |_window, cx| { + panel + .update(cx, |this, cx| { + this.acp_history_store.update(cx, |history_store, cx| { + history_store.remove_recently_opened_entry(&id, cx); + }); + }) + .ok(); + } + }, + ); + } + + menu = menu.separator(); + + menu + } + pub fn set_selected_agent( &mut self, agent: AgentType, @@ -1653,8 +1771,8 @@ impl AgentPanel { if self.selected_agent != agent { self.selected_agent = agent; self.serialize(cx); - self.new_agent_thread(agent, window, cx); } + self.new_agent_thread(agent, window, cx); } pub fn selected_agent(&self) -> AgentType { @@ -1681,13 +1799,13 @@ impl AgentPanel { window.dispatch_action(NewTextThread.boxed_clone(), cx); } AgentType::NativeAgent => { - self.new_external_thread(Some(crate::ExternalAgent::NativeAgent), window, cx) + self.external_thread(Some(crate::ExternalAgent::NativeAgent), None, window, cx) } AgentType::Gemini => { - self.new_external_thread(Some(crate::ExternalAgent::Gemini), window, cx) + self.external_thread(Some(crate::ExternalAgent::Gemini), None, window, cx) } AgentType::ClaudeCode => { - self.new_external_thread(Some(crate::ExternalAgent::ClaudeCode), window, cx) + self.external_thread(Some(crate::ExternalAgent::ClaudeCode), None, window, cx) } } } @@ -1698,7 +1816,13 @@ impl Focusable for AgentPanel { match &self.active_view { ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx), ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx), - ActiveView::History => self.history.focus_handle(cx), + ActiveView::History => { + if cx.has_flag::<feature_flags::AcpFeatureFlag>() { + self.acp_history.focus_handle(cx) + } else { + self.history.focus_handle(cx) + } + } ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx), ActiveView::Configuration => { if let Some(configuration) = self.configuration.as_ref() { @@ -3534,7 +3658,13 @@ impl Render for AgentPanel { ActiveView::ExternalAgentThread { thread_view, .. } => parent .child(thread_view.clone()) .child(self.render_drag_target(cx)), - ActiveView::History => parent.child(self.history.clone()), + ActiveView::History => { + if cx.has_flag::<feature_flags::AcpFeatureFlag>() { + parent.child(self.acp_history.clone()) + } else { + parent.child(self.history.clone()) + } + } ActiveView::TextThread { context_editor, buffer_search_bar, diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 8525d7f9e5d5af9455853256cbf4f345c70f0e0f..a1dbc77084d62ccef7848d6328b8daf2731bf01a 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -156,11 +156,15 @@ enum ExternalAgent { } impl ExternalAgent { - pub fn server(&self, fs: Arc<dyn fs::Fs>) -> Rc<dyn agent_servers::AgentServer> { + pub fn server( + &self, + fs: Arc<dyn fs::Fs>, + history: Entity<agent2::HistoryStore>, + ) -> Rc<dyn agent_servers::AgentServer> { match self { ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), - ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs)), + ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), } } } From a91acb5f4126fe650efc758a4159d0758e34b02e Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 20 Aug 2025 00:13:54 +0530 Subject: [PATCH 150/744] onboarding: Fix theme selection in system mode (#36484) Previously, selecting the "System" theme during onboarding would hardcode the theme based on the device's current mode (e.g., Light or Dark). This change ensures the "System" setting is saved correctly, allowing the app to dynamically follow the OS theme by inserting the correct theme in the config for both light and dark mode. Release Notes: - N/A Signed-off-by: Umesh Yadav <git@umesh.dev> --- crates/onboarding/src/basics_page.rs | 32 +++++++++++++++++++--------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 86ddc22a8687b5f591afb810ead541a0294dc7d9..8d89c6662ef97b6b426c6216b0afa6faf1f7a094 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -16,6 +16,23 @@ use vim_mode_setting::VimModeSetting; use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile}; +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"), +]; + +fn get_theme_family_themes(theme_name: &str) -> Option<(&'static str, &'static str)> { + for i in 0..LIGHT_THEMES.len() { + if LIGHT_THEMES[i] == theme_name || DARK_THEMES[i] == theme_name { + return Some((LIGHT_THEMES[i], DARK_THEMES[i])); + } + } + None +} + fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { let theme_selection = ThemeSettings::get_global(cx).theme_selection.clone(); let system_appearance = theme::SystemAppearance::global(cx); @@ -90,14 +107,6 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement }; 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, @@ -184,10 +193,13 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement let theme = theme.into(); update_settings_file::<ThemeSettings>(fs, cx, move |settings, cx| { if theme_mode == ThemeMode::System { + let (light_theme, dark_theme) = + get_theme_family_themes(&theme).unwrap_or((theme.as_ref(), theme.as_ref())); + settings.theme = Some(ThemeSelection::Dynamic { mode: ThemeMode::System, - light: ThemeName(theme.clone()), - dark: ThemeName(theme.clone()), + light: ThemeName(light_theme.into()), + dark: ThemeName(dark_theme.into()), }); } else { let appearance = *SystemAppearance::global(cx); From df9c2aefb1e4f589753980dbdf6622a1f2dcf52a Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Tue, 19 Aug 2025 15:14:43 -0400 Subject: [PATCH 151/744] thread_view: Fix issues with images (#36509) - Clean up failed load tasks for mentions that require async processing - When dragging and dropping files, hold onto added worktrees until any async processing has completed; this fixes a bug when dragging items from outside the project Release Notes: - N/A --- .../agent_ui/src/acp/completion_provider.rs | 18 +-- crates/agent_ui/src/acp/message_editor.rs | 125 +++++++++++------- crates/agent_ui/src/acp/thread_view.rs | 3 +- 3 files changed, 85 insertions(+), 61 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index d2af2a880ddca00842248929644963edf799efee..a8a690190a5a828288f973b7a43af422fc4d7890 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -763,14 +763,16 @@ fn confirm_completion_callback( message_editor .clone() .update(cx, |message_editor, cx| { - message_editor.confirm_completion( - crease_text, - start, - content_len, - mention_uri, - window, - cx, - ) + message_editor + .confirm_completion( + crease_text, + start, + content_len, + mention_uri, + window, + cx, + ) + .detach(); }) .ok(); }); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index afb1512e5d529dd41453dcedcfd8d9fbf3f566e8..3ed202f66a8238a1b414f3b08b71e65da85811d7 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -26,7 +26,7 @@ use gpui::{ }; use language::{Buffer, Language}; use language_model::LanguageModelImage; -use project::{CompletionIntent, Project, ProjectPath, Worktree}; +use project::{Project, ProjectPath, Worktree}; use rope::Point; use settings::Settings; use std::{ @@ -202,18 +202,18 @@ impl MessageEditor { mention_uri: MentionUri, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> Task<()> { let snapshot = self .editor .update(cx, |editor, cx| editor.snapshot(window, cx)); let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { - return; + return Task::ready(()); }; let Some(anchor) = snapshot .buffer_snapshot .anchor_in_excerpt(*excerpt_id, start) else { - return; + return Task::ready(()); }; if let MentionUri::File { abs_path, .. } = &mention_uri { @@ -228,7 +228,7 @@ impl MessageEditor { .read(cx) .project_path_for_absolute_path(abs_path, cx) else { - return; + return Task::ready(()); }; let image = cx .spawn(async move |_, cx| { @@ -252,9 +252,9 @@ impl MessageEditor { window, cx, ) else { - return; + return Task::ready(()); }; - self.confirm_mention_for_image( + return self.confirm_mention_for_image( crease_id, anchor, Some(abs_path.clone()), @@ -262,7 +262,6 @@ impl MessageEditor { window, cx, ); - return; } } @@ -276,27 +275,28 @@ impl MessageEditor { window, cx, ) else { - return; + return Task::ready(()); }; match mention_uri { MentionUri::Fetch { url } => { - self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx); + self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx) } MentionUri::Directory { abs_path } => { - self.confirm_mention_for_directory(crease_id, anchor, abs_path, window, cx); + self.confirm_mention_for_directory(crease_id, anchor, abs_path, window, cx) } MentionUri::Thread { id, name } => { - self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx); + self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx) } MentionUri::TextThread { path, name } => { - self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx); + self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx) } MentionUri::File { .. } | MentionUri::Symbol { .. } | MentionUri::Rule { .. } | MentionUri::Selection { .. } => { self.mention_set.insert_uri(crease_id, mention_uri.clone()); + Task::ready(()) } } } @@ -308,7 +308,7 @@ impl MessageEditor { abs_path: PathBuf, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> Task<()> { fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> { let mut files = Vec::new(); @@ -331,13 +331,13 @@ impl MessageEditor { .read(cx) .project_path_for_absolute_path(&abs_path, cx) else { - return; + return Task::ready(()); }; let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else { - return; + return Task::ready(()); }; let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else { - return; + return Task::ready(()); }; let project = self.project.clone(); let task = cx.spawn(async move |_, cx| { @@ -396,7 +396,9 @@ impl MessageEditor { }) .shared(); - self.mention_set.directories.insert(abs_path, task.clone()); + self.mention_set + .directories + .insert(abs_path.clone(), task.clone()); let editor = self.editor.clone(); cx.spawn_in(window, async move |this, cx| { @@ -414,9 +416,12 @@ impl MessageEditor { editor.remove_creases([crease_id], cx); }) .ok(); + this.update(cx, |this, _cx| { + this.mention_set.directories.remove(&abs_path); + }) + .ok(); } }) - .detach(); } fn confirm_mention_for_fetch( @@ -426,13 +431,13 @@ impl MessageEditor { url: url::Url, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> Task<()> { let Some(http_client) = self .workspace .update(cx, |workspace, _cx| workspace.client().http_client()) .ok() else { - return; + return Task::ready(()); }; let url_string = url.to_string(); @@ -450,9 +455,9 @@ impl MessageEditor { cx.spawn_in(window, async move |this, cx| { let fetch = fetch.await.notify_async_err(cx); this.update(cx, |this, cx| { - let mention_uri = MentionUri::Fetch { url }; if fetch.is_some() { - this.mention_set.insert_uri(crease_id, mention_uri.clone()); + this.mention_set + .insert_uri(crease_id, MentionUri::Fetch { url }); } else { // Remove crease if we failed to fetch this.editor.update(cx, |editor, cx| { @@ -461,11 +466,11 @@ impl MessageEditor { }); editor.remove_creases([crease_id], cx); }); + this.mention_set.fetch_results.remove(&url); } }) .ok(); }) - .detach(); } pub fn confirm_mention_for_selection( @@ -528,7 +533,7 @@ impl MessageEditor { name: String, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> Task<()> { let uri = MentionUri::Thread { id: id.clone(), name, @@ -546,7 +551,7 @@ impl MessageEditor { }) .shared(); - self.mention_set.insert_thread(id, task.clone()); + self.mention_set.insert_thread(id.clone(), task.clone()); let editor = self.editor.clone(); cx.spawn_in(window, async move |this, cx| { @@ -564,9 +569,12 @@ impl MessageEditor { editor.remove_creases([crease_id], cx); }) .ok(); + this.update(cx, |this, _| { + this.mention_set.thread_summaries.remove(&id); + }) + .ok(); } }) - .detach(); } fn confirm_mention_for_text_thread( @@ -577,7 +585,7 @@ impl MessageEditor { name: String, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> Task<()> { let uri = MentionUri::TextThread { path: path.clone(), name, @@ -595,7 +603,8 @@ impl MessageEditor { }) .shared(); - self.mention_set.insert_text_thread(path, task.clone()); + self.mention_set + .insert_text_thread(path.clone(), task.clone()); let editor = self.editor.clone(); cx.spawn_in(window, async move |this, cx| { @@ -613,9 +622,12 @@ impl MessageEditor { editor.remove_creases([crease_id], cx); }) .ok(); + this.update(cx, |this, _| { + this.mention_set.text_thread_summaries.remove(&path); + }) + .ok(); } }) - .detach(); } pub fn contents( @@ -784,13 +796,15 @@ impl MessageEditor { ) else { return; }; - self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx); + self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx) + .detach(); } } pub fn insert_dragged_files( - &self, + &mut self, paths: Vec<project::ProjectPath>, + added_worktrees: Vec<Entity<Worktree>>, window: &mut Window, cx: &mut Context<Self>, ) { @@ -798,6 +812,7 @@ impl MessageEditor { let Some(buffer) = buffer.read(cx).as_singleton() else { return; }; + let mut tasks = Vec::new(); for path in paths { let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else { continue; @@ -805,39 +820,44 @@ impl MessageEditor { let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else { continue; }; - - let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); let path_prefix = abs_path .file_name() .unwrap_or(path.path.as_os_str()) .display() .to_string(); - let Some(completion) = ContextPickerCompletionProvider::completion_for_path( - path, - &path_prefix, - false, - entry.is_dir(), - anchor..anchor, - cx.weak_entity(), - self.project.clone(), - cx, - ) else { - continue; + let (file_name, _) = + crate::context_picker::file_context_picker::extract_file_name_and_directory( + &path.path, + &path_prefix, + ); + + let uri = if entry.is_dir() { + MentionUri::Directory { abs_path } + } else { + MentionUri::File { abs_path } }; + let new_text = format!("{} ", uri.as_link()); + let content_len = new_text.len() - 1; + + let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); + self.editor.update(cx, |message_editor, cx| { message_editor.edit( [( multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), - completion.new_text, + new_text, )], cx, ); }); - if let Some(confirm) = completion.confirm.clone() { - confirm(CompletionIntent::Complete, window, cx); - } + tasks.push(self.confirm_completion(file_name, anchor, content_len, uri, window, cx)); } + cx.spawn(async move |_, _| { + join_all(tasks).await; + drop(added_worktrees); + }) + .detach(); } pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) { @@ -855,7 +875,7 @@ impl MessageEditor { image: Shared<Task<Result<Arc<Image>, String>>>, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> Task<()> { let editor = self.editor.clone(); let task = cx .spawn_in(window, { @@ -900,9 +920,12 @@ impl MessageEditor { editor.remove_creases([crease_id], cx); }) .ok(); + this.update(cx, |this, _cx| { + this.mention_set.images.remove(&crease_id); + }) + .ok(); } }) - .detach(); } pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index bf5b8efbc80e71d0d691a0794d8eec13654a5e47..f1d3870d6de9e91cb9c39397eebac5c5298e3749 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -3429,8 +3429,7 @@ impl AcpThreadView { cx: &mut Context<Self>, ) { self.message_editor.update(cx, |message_editor, cx| { - message_editor.insert_dragged_files(paths, window, cx); - drop(added_worktrees); + message_editor.insert_dragged_files(paths, added_worktrees, window, cx); }) } From 05fc0c432c024596e68ac5223c556d2a642ff135 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 19 Aug 2025 21:26:17 +0200 Subject: [PATCH 152/744] Fix a bunch of other low-hanging style lints (#36498) - **Fix a bunch of low hanging style lints like unnecessary-return** - **Fix single worktree violation** - **And the rest** Release Notes: - N/A --- Cargo.toml | 6 + crates/acp_thread/src/acp_thread.rs | 2 +- crates/acp_thread/src/mention.rs | 8 +- crates/action_log/src/action_log.rs | 10 +- crates/agent/src/agent_profile.rs | 2 +- crates/agent/src/thread_store.rs | 2 +- crates/agent/src/tool_use.rs | 2 +- crates/agent2/src/db.rs | 2 +- crates/agent2/src/tests/mod.rs | 6 +- crates/agent2/src/thread.rs | 4 +- crates/agent2/src/tools/read_file_tool.rs | 2 +- crates/agent2/src/tools/terminal_tool.rs | 8 +- crates/agent_servers/src/agent_servers.rs | 4 +- crates/agent_servers/src/e2e_tests.rs | 2 +- crates/agent_settings/src/agent_profile.rs | 2 +- .../agent_ui/src/acp/completion_provider.rs | 2 +- crates/agent_ui/src/acp/message_editor.rs | 4 +- crates/agent_ui/src/acp/thread_view.rs | 32 ++-- crates/agent_ui/src/active_thread.rs | 4 +- crates/agent_ui/src/agent_configuration.rs | 2 +- crates/agent_ui/src/agent_diff.rs | 12 +- crates/agent_ui/src/agent_panel.rs | 14 +- crates/agent_ui/src/context_picker.rs | 6 +- .../src/context_picker/completion_provider.rs | 2 +- .../context_picker/fetch_context_picker.rs | 7 +- .../src/context_picker/file_context_picker.rs | 4 +- .../context_picker/rules_context_picker.rs | 2 +- .../context_picker/symbol_context_picker.rs | 2 +- .../context_picker/thread_context_picker.rs | 10 +- crates/agent_ui/src/inline_assistant.rs | 2 +- crates/agent_ui/src/inline_prompt_editor.rs | 2 +- crates/agent_ui/src/message_editor.rs | 6 +- crates/agent_ui/src/slash_command_picker.rs | 10 +- crates/agent_ui/src/text_thread_editor.rs | 4 +- crates/askpass/src/askpass.rs | 6 +- .../src/assistant_context.rs | 21 ++- .../src/assistant_context_tests.rs | 8 +- crates/assistant_context/src/context_store.rs | 2 +- .../src/context_server_command.rs | 7 +- .../src/diagnostics_command.rs | 2 +- .../src/file_command.rs | 4 +- crates/assistant_tools/src/assistant_tools.rs | 2 +- .../assistant_tools/src/edit_agent/evals.rs | 2 +- crates/assistant_tools/src/read_file_tool.rs | 2 +- crates/assistant_tools/src/terminal_tool.rs | 8 +- crates/bedrock/src/bedrock.rs | 6 +- crates/call/src/call_impl/mod.rs | 2 +- crates/call/src/call_impl/participant.rs | 2 +- crates/call/src/call_impl/room.rs | 23 ++- crates/channel/src/channel_chat.rs | 6 +- crates/channel/src/channel_store.rs | 6 +- crates/cli/src/main.rs | 8 +- crates/client/src/client.rs | 2 +- crates/client/src/user.rs | 2 +- .../cloud_api_client/src/cloud_api_client.rs | 6 +- .../random_project_collaboration_tests.rs | 2 +- crates/collab_ui/src/chat_panel.rs | 4 +- crates/collab_ui/src/collab_panel.rs | 6 +- crates/collab_ui/src/notification_panel.rs | 2 +- .../src/credentials_provider.rs | 2 +- crates/dap_adapters/src/codelldb.rs | 2 +- crates/db/src/db.rs | 2 +- crates/db/src/kvp.rs | 2 +- crates/debugger_tools/src/dap_log.rs | 2 +- crates/debugger_ui/src/session/running.rs | 5 +- .../src/session/running/breakpoint_list.rs | 2 +- crates/diagnostics/src/diagnostic_renderer.rs | 16 +- crates/diagnostics/src/diagnostics.rs | 10 +- crates/docs_preprocessor/src/main.rs | 2 +- .../src/edit_prediction_button.rs | 6 +- crates/editor/src/clangd_ext.rs | 2 +- crates/editor/src/code_context_menus.rs | 6 +- crates/editor/src/display_map.rs | 2 +- crates/editor/src/display_map/block_map.rs | 27 ++- crates/editor/src/display_map/fold_map.rs | 22 +-- crates/editor/src/display_map/inlay_map.rs | 2 +- crates/editor/src/display_map/wrap_map.rs | 20 +-- crates/editor/src/editor.rs | 111 ++++++------- crates/editor/src/editor_settings.rs | 6 +- crates/editor/src/element.rs | 120 ++++++------- crates/editor/src/git/blame.rs | 2 +- crates/editor/src/hover_links.rs | 4 +- crates/editor/src/items.rs | 4 +- crates/editor/src/jsx_tag_auto_close.rs | 24 ++- crates/editor/src/mouse_context_menu.rs | 4 +- crates/editor/src/tasks.rs | 2 +- crates/eval/src/assertions.rs | 2 +- crates/eval/src/eval.rs | 2 +- .../src/examples/add_arg_to_trait_method.rs | 6 +- crates/extension/src/extension_builder.rs | 2 +- crates/extension/src/extension_events.rs | 5 +- crates/extension_host/src/extension_host.rs | 12 +- crates/feature_flags/src/feature_flags.rs | 2 +- crates/feedback/src/system_specs.rs | 6 +- crates/file_finder/src/file_finder.rs | 4 +- crates/file_finder/src/open_path_prompt.rs | 2 +- crates/file_icons/src/file_icons.rs | 2 +- crates/fs/src/fs.rs | 4 +- crates/git/src/repository.rs | 8 +- crates/git_ui/src/commit_view.rs | 3 +- crates/git_ui/src/git_panel.rs | 45 ++--- crates/git_ui/src/project_diff.rs | 4 +- crates/go_to_line/src/cursor_position.rs | 2 +- crates/google_ai/src/google_ai.rs | 4 +- crates/gpui/src/app/entity_map.rs | 2 +- crates/gpui/src/elements/div.rs | 6 +- crates/gpui/src/elements/image_cache.rs | 2 +- crates/gpui/src/elements/list.rs | 7 +- crates/gpui/src/key_dispatch.rs | 7 +- crates/gpui/src/keymap/context.rs | 4 +- crates/gpui/src/platform.rs | 2 +- .../gpui/src/platform/blade/blade_context.rs | 2 +- crates/gpui/src/platform/linux/platform.rs | 2 +- .../gpui/src/platform/linux/wayland/client.rs | 6 +- .../gpui/src/platform/linux/wayland/window.rs | 4 +- crates/gpui/src/platform/linux/x11/client.rs | 16 +- .../gpui/src/platform/linux/x11/clipboard.rs | 4 +- crates/gpui/src/platform/linux/x11/event.rs | 2 +- crates/gpui/src/platform/mac/events.rs | 5 +- crates/gpui/src/platform/mac/platform.rs | 2 +- crates/gpui/src/platform/mac/text_system.rs | 2 +- crates/gpui/src/platform/mac/window.rs | 4 +- crates/gpui/src/platform/test/dispatcher.rs | 4 +- crates/gpui/src/style.rs | 6 +- crates/gpui/src/text_system.rs | 2 +- crates/gpui/src/window.rs | 10 +- .../src/derive_inspector_reflection.rs | 2 +- crates/language/src/buffer.rs | 37 ++--- crates/language/src/language_registry.rs | 2 +- crates/language/src/language_settings.rs | 2 +- crates/language/src/syntax_map.rs | 6 +- .../language_models/src/provider/anthropic.rs | 4 +- crates/language_models/src/provider/cloud.rs | 2 +- crates/language_models/src/provider/google.rs | 2 +- crates/language_tools/src/key_context_view.rs | 8 +- crates/language_tools/src/lsp_tool.rs | 2 - crates/languages/src/go.rs | 2 +- crates/languages/src/json.rs | 2 +- crates/languages/src/python.rs | 2 +- crates/languages/src/rust.rs | 4 +- crates/livekit_client/examples/test_app.rs | 4 +- .../markdown_preview/src/markdown_parser.rs | 8 +- .../markdown_preview/src/markdown_renderer.rs | 5 +- .../src/migrations/m_2025_05_05/settings.rs | 4 +- crates/multi_buffer/src/multi_buffer.rs | 157 +++++++++--------- crates/node_runtime/src/node_runtime.rs | 2 +- crates/onboarding/src/theme_preview.rs | 8 +- crates/open_ai/src/open_ai.rs | 4 +- crates/open_router/src/open_router.rs | 2 +- crates/outline_panel/src/outline_panel.rs | 35 ++-- crates/prettier/src/prettier.rs | 4 +- crates/project/src/buffer_store.rs | 7 +- crates/project/src/color_extractor.rs | 2 +- crates/project/src/debugger/locators/cargo.rs | 2 +- .../project/src/debugger/locators/python.rs | 4 +- crates/project/src/debugger/memory.rs | 2 +- crates/project/src/debugger/session.rs | 8 +- crates/project/src/git_store.rs | 14 +- crates/project/src/lsp_command.rs | 4 +- crates/project/src/lsp_store.rs | 45 +++-- crates/project/src/manifest_tree.rs | 6 +- .../project/src/manifest_tree/server_tree.rs | 2 +- crates/project/src/project.rs | 19 ++- crates/project/src/project_settings.rs | 6 +- crates/project/src/project_tests.rs | 7 +- crates/project/src/terminals.rs | 4 +- crates/project_panel/src/project_panel.rs | 42 +++-- crates/project_symbols/src/project_symbols.rs | 2 +- crates/prompt_store/src/prompt_store.rs | 4 +- crates/prompt_store/src/prompts.rs | 2 +- .../src/disconnected_overlay.rs | 2 +- crates/recent_projects/src/remote_servers.rs | 2 +- crates/recent_projects/src/ssh_connections.rs | 2 +- crates/remote/src/ssh_session.rs | 4 +- crates/repl/src/components/kernel_options.rs | 2 +- crates/repl/src/repl_editor.rs | 2 +- crates/rope/src/rope.rs | 4 +- crates/rules_library/src/rules_library.rs | 4 +- crates/settings/src/keymap_file.rs | 8 +- crates/settings/src/settings_json.rs | 8 +- crates/settings/src/settings_store.rs | 3 +- .../src/settings_profile_selector.rs | 2 +- crates/settings_ui/src/keybindings.rs | 35 ++-- .../src/ui_components/keystroke_input.rs | 16 +- crates/settings_ui/src/ui_components/table.rs | 2 +- crates/snippet/src/snippet.rs | 2 +- crates/snippet_provider/src/lib.rs | 6 +- crates/sum_tree/src/sum_tree.rs | 6 +- crates/supermaven/src/supermaven.rs | 2 +- crates/supermaven_api/src/supermaven_api.rs | 4 +- crates/tasks_ui/src/tasks_ui.rs | 4 +- crates/telemetry/src/telemetry.rs | 1 - crates/terminal/src/pty_info.rs | 2 +- crates/terminal/src/terminal.rs | 8 +- crates/terminal/src/terminal_hyperlinks.rs | 4 +- crates/terminal_view/src/color_contrast.rs | 8 +- crates/terminal_view/src/terminal_element.rs | 2 +- crates/terminal_view/src/terminal_panel.rs | 42 +++-- crates/terminal_view/src/terminal_view.rs | 4 +- crates/text/src/anchor.rs | 2 +- crates/text/src/patch.rs | 4 +- crates/text/src/text.rs | 6 +- crates/title_bar/src/collab.rs | 2 +- crates/title_bar/src/onboarding_banner.rs | 2 +- crates/ui/src/components/popover_menu.rs | 6 +- crates/ui/src/components/sticky_items.rs | 1 - crates/util/src/paths.rs | 4 +- crates/util/src/size.rs | 12 +- crates/util/src/util.rs | 4 +- crates/vim/src/command.rs | 3 +- crates/vim/src/digraph.rs | 1 - crates/vim/src/helix.rs | 1 - crates/vim/src/motion.rs | 4 +- crates/vim/src/normal/mark.rs | 1 - crates/vim/src/normal/search.rs | 2 +- crates/vim/src/state.rs | 4 +- .../src/web_search_providers.rs | 2 +- crates/workspace/src/dock.rs | 4 +- crates/workspace/src/item.rs | 2 +- crates/workspace/src/pane.rs | 16 +- crates/workspace/src/workspace.rs | 28 ++-- crates/worktree/src/worktree.rs | 26 ++- crates/worktree/src/worktree_settings.rs | 2 +- crates/zed/src/zed.rs | 2 +- crates/zed/src/zed/migrate.rs | 2 +- crates/zed/src/zed/quick_action_bar.rs | 6 +- crates/zeta/src/init.rs | 8 +- crates/zeta/src/onboarding_modal.rs | 2 +- crates/zeta/src/rate_completion_modal.rs | 2 +- crates/zeta/src/zeta.rs | 4 +- crates/zlog/src/filter.rs | 28 ++-- crates/zlog/src/zlog.rs | 10 +- extensions/glsl/src/glsl.rs | 4 +- extensions/html/src/html.rs | 2 +- extensions/ruff/src/ruff.rs | 4 +- extensions/snippets/src/snippets.rs | 4 +- .../test-extension/src/test_extension.rs | 4 +- extensions/toml/src/toml.rs | 4 +- tooling/xtask/src/tasks/package_conformity.rs | 6 +- 239 files changed, 854 insertions(+), 1015 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 603897084c50fc4dc9380b9d5a6aa08f6cced23f..46c5646c90daf90c4cee00cf7fc66b5239e54f4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -821,6 +821,7 @@ single_range_in_vec_init = "allow" style = { level = "allow", priority = -1 } # Temporary list of style lints that we've fixed so far. +comparison_to_empty = "warn" iter_cloned_collect = "warn" iter_next_slice = "warn" iter_nth = "warn" @@ -831,8 +832,13 @@ question_mark = { level = "deny" } redundant_closure = { level = "deny" } declare_interior_mutable_const = { level = "deny" } collapsible_if = { level = "warn"} +collapsible_else_if = { level = "warn" } needless_borrow = { level = "warn"} +needless_return = { level = "warn" } unnecessary_mut_passed = {level = "warn"} +unnecessary_map_or = { level = "warn" } +unused_unit = "warn" + # Individual rules that have violations in the codebase: type_complexity = "allow" # We often return trait objects from `new` functions. diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 1de8110f07a1586634b5a0e8e01aa0f597f1238e..d4d73e1eddd6ae0015049e82d825d6ecb508a985 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -49,7 +49,7 @@ impl UserMessage { if self .checkpoint .as_ref() - .map_or(false, |checkpoint| checkpoint.show) + .is_some_and(|checkpoint| checkpoint.show) { writeln!(markdown, "## User (checkpoint)").unwrap(); } else { diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index fcf50b0fd72591a20ed2d52415f286f29100f796..4615e9a5515ef29b1dc4b9c79ff49fc018cbfd2c 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -79,12 +79,10 @@ impl MentionUri { } else { Ok(Self::Selection { path, line_range }) } + } else if input.ends_with("/") { + Ok(Self::Directory { abs_path: path }) } else { - if input.ends_with("/") { - Ok(Self::Directory { abs_path: path }) - } else { - Ok(Self::File { abs_path: path }) - } + Ok(Self::File { abs_path: path }) } } "zed" => { diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index ceced1bcdd2e8edb0e4cd950bef68b646b3a252c..602357ed2b5bfb8e52f906dfcd3de911a4068907 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -116,7 +116,7 @@ impl ActionLog { } else if buffer .read(cx) .file() - .map_or(false, |file| file.disk_state().exists()) + .is_some_and(|file| file.disk_state().exists()) { TrackedBufferStatus::Created { existing_file_content: Some(buffer.read(cx).as_rope().clone()), @@ -215,7 +215,7 @@ impl ActionLog { if buffer .read(cx) .file() - .map_or(false, |file| file.disk_state() == DiskState::Deleted) + .is_some_and(|file| file.disk_state() == DiskState::Deleted) { // If the buffer had been edited by a tool, but it got // deleted externally, we want to stop tracking it. @@ -227,7 +227,7 @@ impl ActionLog { if buffer .read(cx) .file() - .map_or(false, |file| file.disk_state() != DiskState::Deleted) + .is_some_and(|file| file.disk_state() != DiskState::Deleted) { // If the buffer had been deleted by a tool, but it got // resurrected externally, we want to clear the edits we @@ -811,7 +811,7 @@ impl ActionLog { tracked.version != buffer.version && buffer .file() - .map_or(false, |file| file.disk_state() != DiskState::Deleted) + .is_some_and(|file| file.disk_state() != DiskState::Deleted) }) .map(|(buffer, _)| buffer) } @@ -847,7 +847,7 @@ fn apply_non_conflicting_edits( conflict = true; if new_edits .peek() - .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new)) + .is_some_and(|next_edit| next_edit.old.overlaps(&old_edit.new)) { new_edit = new_edits.next().unwrap(); } else { diff --git a/crates/agent/src/agent_profile.rs b/crates/agent/src/agent_profile.rs index 38e697dd9bbd5ede89ad23575bb1e123dfb2c350..1636508df6b2012333ee90d9062c73918ddf0f35 100644 --- a/crates/agent/src/agent_profile.rs +++ b/crates/agent/src/agent_profile.rs @@ -90,7 +90,7 @@ impl AgentProfile { return false; }; - return Self::is_enabled(settings, source, tool_name); + Self::is_enabled(settings, source, tool_name) } fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool { diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index ed1605aacfe613c4ecfb2921d7d585400ed334c4..63d0f72e00b272bbe91859ffe52bce317ef1963f 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -42,7 +42,7 @@ use std::{ use util::ResultExt as _; pub static ZED_STATELESS: std::sync::LazyLock<bool> = - std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty())); + std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum DataType { diff --git a/crates/agent/src/tool_use.rs b/crates/agent/src/tool_use.rs index d109891bf2a84e3833875719f0d709123b041695..962dca591fb66f4679d44b8e8a4733c879bc2e0c 100644 --- a/crates/agent/src/tool_use.rs +++ b/crates/agent/src/tool_use.rs @@ -275,7 +275,7 @@ impl ToolUseState { pub fn message_has_tool_results(&self, assistant_message_id: MessageId) -> bool { self.tool_uses_by_assistant_message .get(&assistant_message_id) - .map_or(false, |results| !results.is_empty()) + .is_some_and(|results| !results.is_empty()) } pub fn tool_result( diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index c3e6352ef66007c193e53c343dd30d0b31492730..27a109c573d956f592ac4fd8540aab42ae414a75 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent2/src/db.rs @@ -184,7 +184,7 @@ impl DbThread { } pub static ZED_STATELESS: std::sync::LazyLock<bool> = - std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty())); + std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum DataType { diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index f01873cfc109c1191f062402a03565541ab18851..7fa12e57117c55d78f3a910188c74bb1057330bf 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -742,7 +742,7 @@ async fn expect_tool_call(events: &mut UnboundedReceiver<Result<ThreadEvent>>) - .expect("no tool call authorization event received") .unwrap(); match event { - ThreadEvent::ToolCall(tool_call) => return tool_call, + ThreadEvent::ToolCall(tool_call) => tool_call, event => { panic!("Unexpected event {event:?}"); } @@ -758,9 +758,7 @@ async fn expect_tool_call_update_fields( .expect("no tool call authorization event received") .unwrap(); match event { - ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => { - return update; - } + ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => update, event => { panic!("Unexpected event {event:?}"); } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 66b4485f72981cf2a40a5fcee1a590462b8295a0..ba5cd1f47725c1d1a5edd7855295b3cc05b4ecad 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1356,7 +1356,7 @@ impl Thread { // Ensure the last message ends in the current tool use let last_message = self.pending_message(); - let push_new_tool_use = last_message.content.last_mut().map_or(true, |content| { + let push_new_tool_use = last_message.content.last_mut().is_none_or(|content| { if let AgentMessageContent::ToolUse(last_tool_use) = content { if last_tool_use.id == tool_use.id { *last_tool_use = tool_use.clone(); @@ -1408,7 +1408,7 @@ impl Thread { status: Some(acp::ToolCallStatus::InProgress), ..Default::default() }); - let supports_images = self.model().map_or(false, |model| model.supports_images()); + let supports_images = self.model().is_some_and(|model| model.supports_images()); let tool_result = tool.run(tool_use.input, tool_event_stream, cx); log::info!("Running tool {}", tool_use.name); Some(cx.foreground_executor().spawn(async move { diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index f21643cbbbffca7a489918c3466ccc369a17156c..f37dff4f47adfa65ca8145dd88d591467f60956a 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -175,7 +175,7 @@ impl AgentTool for ReadFileTool { buffer .file() .as_ref() - .map_or(true, |file| !file.disk_state().exists()) + .is_none_or(|file| !file.disk_state().exists()) })? { anyhow::bail!("{file_path} not found"); } diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index 1804d0ab3047c41dfb9842a10ed1001346a468cb..d8f0282f4bd038c0c73b2d394c33be32c5f7b6ce 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -271,7 +271,7 @@ fn working_dir( let project = project.read(cx); let cd = &input.cd; - if cd == "." || cd == "" { + if cd == "." || cd.is_empty() { // Accept "." or "" as meaning "the one worktree" if we only have one worktree. let mut worktrees = project.worktrees(cx); @@ -296,10 +296,8 @@ fn working_dir( { return Ok(Some(input_path.into())); } - } else { - if let Some(worktree) = project.worktree_for_root_name(cd, cx) { - return Ok(Some(worktree.read(cx).abs_path().to_path_buf())); - } + } else if let Some(worktree) = project.worktree_for_root_name(cd, cx) { + return Ok(Some(worktree.read(cx).abs_path().to_path_buf())); } anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees."); diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 8f8aa1d7887b93c4ce7f7487d916d154f7d16de1..cebf82cddb00be91f7b12aca369883cd5cce9fde 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -104,7 +104,7 @@ impl AgentServerCommand { cx: &mut AsyncApp, ) -> Option<Self> { if let Some(agent_settings) = settings { - return Some(Self { + Some(Self { path: agent_settings.command.path, args: agent_settings .command @@ -113,7 +113,7 @@ impl AgentServerCommand { .chain(extra_args.iter().map(|arg| arg.to_string())) .collect(), env: agent_settings.command.env, - }); + }) } else { match find_bin_in_path(path_bin_name, project, cx).await { Some(path) => Some(Self { diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 2b32edcd4f54b2ca216e11d37cb2dd7c1ee2243a..fef80b4d42b6d49385acc5e0fae01997969da005 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -471,7 +471,7 @@ pub fn get_zed_path() -> PathBuf { while zed_path .file_name() - .map_or(true, |name| name.to_string_lossy() != "debug") + .is_none_or(|name| name.to_string_lossy() != "debug") { if !zed_path.pop() { panic!("Could not find target directory"); diff --git a/crates/agent_settings/src/agent_profile.rs b/crates/agent_settings/src/agent_profile.rs index 402cf81678e02a13c99bf4cdf225406085e3551d..04fdd4a753a3fd015f2710fb9d70770ad960c560 100644 --- a/crates/agent_settings/src/agent_profile.rs +++ b/crates/agent_settings/src/agent_profile.rs @@ -58,7 +58,7 @@ impl AgentProfileSettings { || self .context_servers .get(server_id) - .map_or(false, |preset| preset.tools.get(tool_name) == Some(&true)) + .is_some_and(|preset| preset.tools.get(tool_name) == Some(&true)) } } diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index a8a690190a5a828288f973b7a43af422fc4d7890..1a5e9c7d81c1a8db46ecc23c97c349e5ba63dd7a 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -797,7 +797,7 @@ impl MentionCompletion { && line .chars() .nth(last_mention_start - 1) - .map_or(false, |c| !c.is_whitespace()) + .is_some_and(|c| !c.is_whitespace()) { return None; } diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 3ed202f66a8238a1b414f3b08b71e65da85811d7..e7f0d4f88fd85fc6e6bd643d2e5d9ad050d019dd 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1552,14 +1552,14 @@ impl SemanticsProvider for SlashCommandSemanticsProvider { return None; } let range = snapshot.anchor_after(start)..snapshot.anchor_after(end); - return Some(Task::ready(vec![project::Hover { + Some(Task::ready(vec![project::Hover { contents: vec![project::HoverBlock { text: "Slash commands are not supported".into(), kind: project::HoverBlockKind::PlainText, }], range: Some(range), language: None, - }])); + }])) } fn inline_values( diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f1d3870d6de9e91cb9c39397eebac5c5298e3749..7b38ba93015fa8616b62780f91af60f4c5eeedd7 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -102,7 +102,7 @@ impl ProfileProvider for Entity<agent2::Thread> { fn profiles_supported(&self, cx: &App) -> bool { self.read(cx) .model() - .map_or(false, |model| model.supports_tools()) + .is_some_and(|model| model.supports_tools()) } } @@ -2843,7 +2843,7 @@ impl AcpThreadView { if thread .model() - .map_or(true, |model| !model.supports_burn_mode()) + .is_none_or(|model| !model.supports_burn_mode()) { return None; } @@ -2875,9 +2875,9 @@ impl AcpThreadView { fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement { let is_editor_empty = self.message_editor.read(cx).is_empty(cx); - let is_generating = self.thread().map_or(false, |thread| { - thread.read(cx).status() != ThreadStatus::Idle - }); + let is_generating = self + .thread() + .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); if is_generating && is_editor_empty { IconButton::new("stop-generation", IconName::Stop) @@ -3455,18 +3455,16 @@ impl AcpThreadView { } else { format!("Retrying. Next attempt in {next_attempt_in_secs} seconds.") } + } else if next_attempt_in_secs == 1 { + format!( + "Retrying. Next attempt in 1 second (Attempt {} of {}).", + state.attempt, state.max_attempts, + ) } else { - if next_attempt_in_secs == 1 { - format!( - "Retrying. Next attempt in 1 second (Attempt {} of {}).", - state.attempt, state.max_attempts, - ) - } else { - format!( - "Retrying. Next attempt in {next_attempt_in_secs} seconds (Attempt {} of {}).", - state.attempt, state.max_attempts, - ) - } + format!( + "Retrying. Next attempt in {next_attempt_in_secs} seconds (Attempt {} of {}).", + state.attempt, state.max_attempts, + ) }; Some( @@ -3552,7 +3550,7 @@ impl AcpThreadView { let supports_burn_mode = thread .read(cx) .model() - .map_or(false, |model| model.supports_burn_mode()); + .is_some_and(|model| model.supports_burn_mode()); let focus_handle = self.focus_handle(cx); diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 3defa42d1729f898f362c18d41a8e8ceac34554f..a1e51f883a4359734c8c6de8c98ec33051de9e83 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -2246,9 +2246,7 @@ impl ActiveThread { let after_editing_message = self .editing_message .as_ref() - .map_or(false, |(editing_message_id, _)| { - message_id > *editing_message_id - }); + .is_some_and(|(editing_message_id, _)| message_id > *editing_message_id); let backdrop = div() .id(("backdrop", ix)) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index a0584f9e2e67f4c08efe17c499abc9bbaf394eb9..b032201d8c72443697b154cb0338e96a496d0340 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -96,7 +96,7 @@ impl AgentConfiguration { let mut expanded_provider_configurations = HashMap::default(); if LanguageModelRegistry::read_global(cx) .provider(&ZED_CLOUD_PROVIDER_ID) - .map_or(false, |cloud_provider| cloud_provider.must_accept_terms(cx)) + .is_some_and(|cloud_provider| cloud_provider.must_accept_terms(cx)) { expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true); } diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 5b4f1038e2dbd9285bceeb6beb6f3197cbb983eb..e80cd2084693447e5d6b7bc84820f42aab8f3590 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -285,7 +285,7 @@ impl AgentDiffPane { && buffer .read(cx) .file() - .map_or(false, |file| file.disk_state() == DiskState::Deleted) + .is_some_and(|file| file.disk_state() == DiskState::Deleted) { editor.fold_buffer(snapshot.text.remote_id(), cx) } @@ -1063,7 +1063,7 @@ impl ToolbarItemView for AgentDiffToolbar { } self.active_item = None; - return self.location(cx); + self.location(cx) } fn pane_focus_update( @@ -1509,7 +1509,7 @@ impl AgentDiff { .read(cx) .entries() .last() - .map_or(false, |entry| entry.diffs().next().is_some()) + .is_some_and(|entry| entry.diffs().next().is_some()) { self.update_reviewing_editors(workspace, window, cx); } @@ -1519,7 +1519,7 @@ impl AgentDiff { .read(cx) .entries() .get(*ix) - .map_or(false, |entry| entry.diffs().next().is_some()) + .is_some_and(|entry| entry.diffs().next().is_some()) { self.update_reviewing_editors(workspace, window, cx); } @@ -1709,7 +1709,7 @@ impl AgentDiff { .read_with(cx, |editor, _cx| editor.workspace()) .ok() .flatten() - .map_or(false, |editor_workspace| { + .is_some_and(|editor_workspace| { editor_workspace.entity_id() == workspace.entity_id() }); @@ -1868,7 +1868,7 @@ impl AgentDiff { } } - return Some(Task::ready(Ok(()))); + Some(Task::ready(Ok(()))) } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index f9aea843768b755ffccfcc4564c4cf2a7b55e7ea..c79349e3a9e9467780226d956e89310efd01ae57 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1463,7 +1463,7 @@ impl AgentPanel { AssistantConfigurationEvent::NewThread(provider) => { if LanguageModelRegistry::read_global(cx) .default_model() - .map_or(true, |model| model.provider.id() != provider.id()) + .is_none_or(|model| model.provider.id() != provider.id()) && let Some(model) = provider.default_model(cx) { update_settings_file::<AgentSettings>( @@ -2708,9 +2708,7 @@ impl AgentPanel { } ActiveView::ExternalAgentThread { .. } | ActiveView::History - | ActiveView::Configuration => { - return None; - } + | ActiveView::Configuration => None, } } @@ -2726,7 +2724,7 @@ impl AgentPanel { .thread() .read(cx) .configured_model() - .map_or(false, |model| { + .is_some_and(|model| { model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID }) { @@ -2737,7 +2735,7 @@ impl AgentPanel { if LanguageModelRegistry::global(cx) .read(cx) .default_model() - .map_or(false, |model| { + .is_some_and(|model| { model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID }) { @@ -3051,9 +3049,7 @@ impl AgentPanel { let zed_provider_configured = AgentSettings::get_global(cx) .default_model .as_ref() - .map_or(false, |selection| { - selection.provider.0.as_str() == "zed.dev" - }); + .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev"); let callout = if zed_provider_configured { Callout::new() diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 131023d249852e54e508cac3165cb482860d005b..697f704991a3099e6f135c58635144edbc8794ad 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -610,9 +610,7 @@ pub(crate) fn available_context_picker_entries( .read(cx) .active_item(cx) .and_then(|item| item.downcast::<Editor>()) - .map_or(false, |editor| { - editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx)) - }); + .is_some_and(|editor| editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))); if has_selection { entries.push(ContextPickerEntry::Action( ContextPickerAction::AddSelections, @@ -680,7 +678,7 @@ pub(crate) fn recent_context_picker_entries( .filter(|(_, abs_path)| { abs_path .as_ref() - .map_or(true, |path| !exclude_paths.contains(path.as_path())) + .is_none_or(|path| !exclude_paths.contains(path.as_path())) }) .take(4) .filter_map(|(project_path, _)| { diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index 79e56acacf1ce9a1ad541de9404aed44c711ff8b..747ec46e0a03d25df32b2b4bcb9dd2e915196f8c 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -1020,7 +1020,7 @@ impl MentionCompletion { && line .chars() .nth(last_mention_start - 1) - .map_or(false, |c| !c.is_whitespace()) + .is_some_and(|c| !c.is_whitespace()) { return None; } diff --git a/crates/agent_ui/src/context_picker/fetch_context_picker.rs b/crates/agent_ui/src/context_picker/fetch_context_picker.rs index 8ff68a8365ee01ac79d707abf00197bf5175e43a..dd558b2a1c88f60e68313b208b076a0974b30f85 100644 --- a/crates/agent_ui/src/context_picker/fetch_context_picker.rs +++ b/crates/agent_ui/src/context_picker/fetch_context_picker.rs @@ -226,9 +226,10 @@ impl PickerDelegate for FetchContextPickerDelegate { _window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Option<Self::ListItem> { - let added = self.context_store.upgrade().map_or(false, |context_store| { - context_store.read(cx).includes_url(&self.url) - }); + let added = self + .context_store + .upgrade() + .is_some_and(|context_store| context_store.read(cx).includes_url(&self.url)); Some( ListItem::new(ix) diff --git a/crates/agent_ui/src/context_picker/file_context_picker.rs b/crates/agent_ui/src/context_picker/file_context_picker.rs index 4f74e2cea4f9960fdc30279fb7e9297f10675b81..6c224caf4c114c389991907fd7e382fb7e4e49d1 100644 --- a/crates/agent_ui/src/context_picker/file_context_picker.rs +++ b/crates/agent_ui/src/context_picker/file_context_picker.rs @@ -239,9 +239,7 @@ pub(crate) fn search_files( PathMatchCandidateSet { snapshot: worktree.snapshot(), - include_ignored: worktree - .root_entry() - .map_or(false, |entry| entry.is_ignored), + include_ignored: worktree.root_entry().is_some_and(|entry| entry.is_ignored), include_root_name: true, candidates: project::Candidates::Entries, } diff --git a/crates/agent_ui/src/context_picker/rules_context_picker.rs b/crates/agent_ui/src/context_picker/rules_context_picker.rs index 8ce821cfaaab0a49f4af70fca13c1ed202de20a1..f3982f61cb9822a238dc6573e81430de98c78838 100644 --- a/crates/agent_ui/src/context_picker/rules_context_picker.rs +++ b/crates/agent_ui/src/context_picker/rules_context_picker.rs @@ -159,7 +159,7 @@ pub fn render_thread_context_entry( context_store: WeakEntity<ContextStore>, cx: &mut App, ) -> Div { - let added = context_store.upgrade().map_or(false, |context_store| { + let added = context_store.upgrade().is_some_and(|context_store| { context_store .read(cx) .includes_user_rules(user_rules.prompt_id) diff --git a/crates/agent_ui/src/context_picker/symbol_context_picker.rs b/crates/agent_ui/src/context_picker/symbol_context_picker.rs index 805c10c96553afe6482793974478392c0441e350..b00d4e3693d2aff12c54c5ebda9acf93c74bfc7d 100644 --- a/crates/agent_ui/src/context_picker/symbol_context_picker.rs +++ b/crates/agent_ui/src/context_picker/symbol_context_picker.rs @@ -294,7 +294,7 @@ pub(crate) fn search_symbols( .partition(|candidate| { project .entry_for_path(&symbols[candidate.id].path, cx) - .map_or(false, |e| !e.is_ignored) + .is_some_and(|e| !e.is_ignored) }) }) .log_err() 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 e660e64ae349e2edc810fdaac68528f80a6005a7..66654f3d8c066701a9f84df4d53bd9f1d9c30d3b 100644 --- a/crates/agent_ui/src/context_picker/thread_context_picker.rs +++ b/crates/agent_ui/src/context_picker/thread_context_picker.rs @@ -236,12 +236,10 @@ pub fn render_thread_context_entry( let is_added = match entry { ThreadContextEntry::Thread { id, .. } => context_store .upgrade() - .map_or(false, |ctx_store| ctx_store.read(cx).includes_thread(id)), - ThreadContextEntry::Context { path, .. } => { - context_store.upgrade().map_or(false, |ctx_store| { - ctx_store.read(cx).includes_text_thread(path) - }) - } + .is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(id)), + ThreadContextEntry::Context { path, .. } => context_store + .upgrade() + .is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(path)), }; h_flex() diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 101eb899b232f3361170561c78e0f2e2c01ba629..90302236fb13eb9bed612affc4c40c80e2cacd3a 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -1120,7 +1120,7 @@ impl InlineAssistant { if editor_assists .scroll_lock .as_ref() - .map_or(false, |lock| lock.assist_id == assist_id) + .is_some_and(|lock| lock.assist_id == assist_id) { editor_assists.scroll_lock = None; } diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 6f12050f883636c0a3027a1342ea5fe2d0e150a9..56081434644e3b2f3ee7147bdff2874e631ee544 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -345,7 +345,7 @@ impl<T: 'static> PromptEditor<T> { let prompt = self.editor.read(cx).text(cx); if self .prompt_history_ix - .map_or(true, |ix| self.prompt_history[ix] != prompt) + .is_none_or(|ix| self.prompt_history[ix] != prompt) { self.prompt_history_ix.take(); self.pending_prompt = prompt; diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 64c9a873f56fcc6ff4f4a353543cf25a369a1fd4..6e4d2638c1fa4c3592d55c9be250c26730ee46e5 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -156,7 +156,7 @@ impl ProfileProvider for Entity<Thread> { fn profiles_supported(&self, cx: &App) -> bool { self.read(cx) .configured_model() - .map_or(false, |model| model.model.supports_tools()) + .is_some_and(|model| model.model.supports_tools()) } fn profile_id(&self, cx: &App) -> AgentProfileId { @@ -1289,7 +1289,7 @@ impl MessageEditor { self.thread .read(cx) .configured_model() - .map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID) + .is_some_and(|model| model.provider.id() == ZED_CLOUD_PROVIDER_ID) } fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> { @@ -1442,7 +1442,7 @@ impl MessageEditor { let message_text = editor.read(cx).text(cx); if message_text.is_empty() - && loaded_context.map_or(true, |loaded_context| loaded_context.is_empty()) + && loaded_context.is_none_or(|loaded_context| loaded_context.is_empty()) { return None; } diff --git a/crates/agent_ui/src/slash_command_picker.rs b/crates/agent_ui/src/slash_command_picker.rs index bab2364679b472488340e9b849681a50837011ad..03f2c97887375ad349b603ac30d876514ee5a4a1 100644 --- a/crates/agent_ui/src/slash_command_picker.rs +++ b/crates/agent_ui/src/slash_command_picker.rs @@ -140,12 +140,10 @@ impl PickerDelegate for SlashCommandDelegate { ); ret.push(index - 1); } - } else { - if let SlashCommandEntry::Advert { .. } = command { - previous_is_advert = true; - if index != 0 { - ret.push(index - 1); - } + } else if let SlashCommandEntry::Advert { .. } = command { + previous_is_advert = true; + if index != 0 { + ret.push(index - 1); } } } diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 3b5f2e5069d9980dcbb9aa6c5ec568d9a2b85ac8..b7e5d83d6d842b8463b3a856ed7e557bdb2ac001 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -373,7 +373,7 @@ impl TextThreadEditor { .map(|default| default.provider); if provider .as_ref() - .map_or(false, |provider| provider.must_accept_terms(cx)) + .is_some_and(|provider| provider.must_accept_terms(cx)) { self.show_accept_terms = true; cx.notify(); @@ -457,7 +457,7 @@ impl TextThreadEditor { || snapshot .chars_at(newest_cursor) .next() - .map_or(false, |ch| ch != '\n') + .is_some_and(|ch| ch != '\n') { editor.move_to_end_of_line( &MoveToEndOfLine { diff --git a/crates/askpass/src/askpass.rs b/crates/askpass/src/askpass.rs index f085a2be72d04d7c1d16f855230011639853ddf2..9e84a9fed03c8a620c7cb33cc76ef22c000c3fa6 100644 --- a/crates/askpass/src/askpass.rs +++ b/crates/askpass/src/askpass.rs @@ -177,11 +177,11 @@ impl AskPassSession { _ = askpass_opened_rx.fuse() => { // Note: this await can only resolve after we are dropped. askpass_kill_master_rx.await.ok(); - return AskPassResult::CancelledByUser + AskPassResult::CancelledByUser } _ = futures::FutureExt::fuse(smol::Timer::after(connection_timeout)) => { - return AskPassResult::Timedout + AskPassResult::Timedout } } } @@ -215,7 +215,7 @@ pub fn main(socket: &str) { } #[cfg(target_os = "windows")] - while buffer.last().map_or(false, |&b| b == b'\n' || b == b'\r') { + while buffer.last().is_some_and(|&b| b == b'\n' || b == b'\r') { buffer.pop(); } if buffer.last() != Some(&b'\0') { diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 151586564f159f826f8c87b6dea82d7b271fcd9a..2d71a1c08a7cff3de8db2608d762248d2b96c6b4 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -1023,9 +1023,11 @@ impl AssistantContext { summary: new_summary, .. } => { - if self.summary.timestamp().map_or(true, |current_timestamp| { - new_summary.timestamp > current_timestamp - }) { + if self + .summary + .timestamp() + .is_none_or(|current_timestamp| new_summary.timestamp > current_timestamp) + { self.summary = ContextSummary::Content(new_summary); summary_generated = true; } @@ -1339,7 +1341,7 @@ impl AssistantContext { let is_invalid = self .messages_metadata .get(&message_id) - .map_or(true, |metadata| { + .is_none_or(|metadata| { !metadata.is_cache_valid(&buffer, &message.offset_range) || *encountered_invalid }); @@ -1860,7 +1862,7 @@ impl AssistantContext { { let newline_offset = insert_position.saturating_sub(1); if buffer.contains_str_at(newline_offset, "\n") - && last_section_range.map_or(true, |last_section_range| { + && last_section_range.is_none_or(|last_section_range| { !last_section_range .to_offset(buffer) .contains(&newline_offset) @@ -2313,10 +2315,7 @@ impl AssistantContext { let mut request_message = LanguageModelRequestMessage { role: message.role, content: Vec::new(), - cache: message - .cache - .as_ref() - .map_or(false, |cache| cache.is_anchor), + cache: message.cache.as_ref().is_some_and(|cache| cache.is_anchor), }; while let Some(content) = contents.peek() { @@ -2797,7 +2796,7 @@ impl AssistantContext { let mut current_message = messages.next(); while let Some(offset) = offsets.next() { // Locate the message that contains the offset. - while current_message.as_ref().map_or(false, |message| { + while current_message.as_ref().is_some_and(|message| { !message.offset_range.contains(&offset) && messages.peek().is_some() }) { current_message = messages.next(); @@ -2807,7 +2806,7 @@ impl AssistantContext { }; // Skip offsets that are in the same message. - while offsets.peek().map_or(false, |offset| { + while offsets.peek().is_some_and(|offset| { message.offset_range.contains(offset) || messages.peek().is_none() }) { offsets.next(); diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_context/src/assistant_context_tests.rs index eae7741358be6bfae680a4be3f5fb454387093f6..28cc8ef8f058e9627e6ff73b8e40a1198f218856 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_context/src/assistant_context_tests.rs @@ -1055,7 +1055,7 @@ fn test_mark_cache_anchors(cx: &mut App) { assert_eq!( messages_cache(&context, cx) .iter() - .filter(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor)) + .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor)) .count(), 0, "Empty messages should not have any cache anchors." @@ -1083,7 +1083,7 @@ fn test_mark_cache_anchors(cx: &mut App) { assert_eq!( messages_cache(&context, cx) .iter() - .filter(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor)) + .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor)) .count(), 0, "Messages should not be marked for cache before going over the token minimum." @@ -1098,7 +1098,7 @@ fn test_mark_cache_anchors(cx: &mut App) { assert_eq!( messages_cache(&context, cx) .iter() - .map(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor)) + .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor)) .collect::<Vec<bool>>(), vec![true, true, false], "Last message should not be an anchor on speculative request." @@ -1116,7 +1116,7 @@ fn test_mark_cache_anchors(cx: &mut App) { assert_eq!( messages_cache(&context, cx) .iter() - .map(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor)) + .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor)) .collect::<Vec<bool>>(), vec![false, true, true, false], "Most recent message should also be cached if not a speculative request." diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index a2b3adc68657caca42b5f27332e1bb21a601fb48..6d13531a57de2b8b654ba4ce0c734fc575c659cb 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -789,7 +789,7 @@ impl ContextStore { let fs = self.fs.clone(); cx.spawn(async move |this, cx| { pub static ZED_STATELESS: LazyLock<bool> = - LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty())); + LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); if *ZED_STATELESS { return Ok(()); } diff --git a/crates/assistant_slash_commands/src/context_server_command.rs b/crates/assistant_slash_commands/src/context_server_command.rs index 219c3b30bc8328fd900299adfdc406167f5f341d..6caa1beb3bd82fdbc70fd516cdbef9db63978a76 100644 --- a/crates/assistant_slash_commands/src/context_server_command.rs +++ b/crates/assistant_slash_commands/src/context_server_command.rs @@ -62,9 +62,10 @@ impl SlashCommand for ContextServerSlashCommand { } fn requires_argument(&self) -> bool { - self.prompt.arguments.as_ref().map_or(false, |args| { - args.iter().any(|arg| arg.required == Some(true)) - }) + self.prompt + .arguments + .as_ref() + .is_some_and(|args| args.iter().any(|arg| arg.required == Some(true))) } fn complete_argument( diff --git a/crates/assistant_slash_commands/src/diagnostics_command.rs b/crates/assistant_slash_commands/src/diagnostics_command.rs index 45c976c82611ae36b4d3b65306c88d6d7f93e80e..536fe9f0efc0d5145663d1a88295bf849e0587ab 100644 --- a/crates/assistant_slash_commands/src/diagnostics_command.rs +++ b/crates/assistant_slash_commands/src/diagnostics_command.rs @@ -61,7 +61,7 @@ impl DiagnosticsSlashCommand { snapshot: worktree.snapshot(), include_ignored: worktree .root_entry() - .map_or(false, |entry| entry.is_ignored), + .is_some_and(|entry| entry.is_ignored), include_root_name: true, candidates: project::Candidates::Entries, } diff --git a/crates/assistant_slash_commands/src/file_command.rs b/crates/assistant_slash_commands/src/file_command.rs index c913ccc0f199cb5d03cf0a91d67459f3728b55a9..68751899272afa2ee2ef03541d99c5debf5a94d2 100644 --- a/crates/assistant_slash_commands/src/file_command.rs +++ b/crates/assistant_slash_commands/src/file_command.rs @@ -92,7 +92,7 @@ impl FileSlashCommand { snapshot: worktree.snapshot(), include_ignored: worktree .root_entry() - .map_or(false, |entry| entry.is_ignored), + .is_some_and(|entry| entry.is_ignored), include_root_name: true, candidates: project::Candidates::Entries, } @@ -536,7 +536,7 @@ mod custom_path_matcher { let path_str = path.to_string_lossy(); let separator = std::path::MAIN_SEPARATOR_STR; if path_str.ends_with(separator) { - return false; + false } else { self.glob.is_match(path_str.to_string() + separator) } diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index bf668e691885d328ecd34b22d0a4e14633be565a..f381103c278b83c87aeeb59c06939797f97c7067 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -86,7 +86,7 @@ fn register_web_search_tool(registry: &Entity<LanguageModelRegistry>, cx: &mut A let using_zed_provider = registry .read(cx) .default_model() - .map_or(false, |default| default.is_provided_by_zed()); + .is_some_and(|default| default.is_provided_by_zed()); if using_zed_provider { ToolRegistry::global(cx).register_tool(WebSearchTool); } else { diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index 0d529a55735d07f15e05fbc50ec8b1f1230b50fa..ea2fa02663674c3aaae2dc8fe3198ec110d0f42e 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -1586,7 +1586,7 @@ impl EditAgentTest { let has_system_prompt = eval .conversation .first() - .map_or(false, |msg| msg.role == Role::System); + .is_some_and(|msg| msg.role == Role::System); let messages = if has_system_prompt { eval.conversation } else { diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index 68b870e40f4af23f4eb27f68c8d45d4789f6bc48..766ee3b1611ccc9093fbe9299644413877afd991 100644 --- a/crates/assistant_tools/src/read_file_tool.rs +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -201,7 +201,7 @@ impl Tool for ReadFileTool { buffer .file() .as_ref() - .map_or(true, |file| !file.disk_state().exists()) + .is_none_or(|file| !file.disk_state().exists()) })? { anyhow::bail!("{file_path} not found"); } diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 3de22ad28da617a31c3dd7c9e361f9cbb074c82d..dd0a0c8e4c34b8e7f5f7a7dc9768c188dc66ed8a 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -387,7 +387,7 @@ fn working_dir( let project = project.read(cx); let cd = &input.cd; - if cd == "." || cd == "" { + if cd == "." || cd.is_empty() { // Accept "." or "" as meaning "the one worktree" if we only have one worktree. let mut worktrees = project.worktrees(cx); @@ -412,10 +412,8 @@ fn working_dir( { return Ok(Some(input_path.into())); } - } else { - if let Some(worktree) = project.worktree_for_root_name(cd, cx) { - return Ok(Some(worktree.read(cx).abs_path().to_path_buf())); - } + } else if let Some(worktree) = project.worktree_for_root_name(cd, cx) { + return Ok(Some(worktree.read(cx).abs_path().to_path_buf())); } anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees."); diff --git a/crates/bedrock/src/bedrock.rs b/crates/bedrock/src/bedrock.rs index 1c6a9bd0a1e745da1dd4577741fc7cb4cab771ad..c8315d4201a46d5ac47825ff40aed3829f191d87 100644 --- a/crates/bedrock/src/bedrock.rs +++ b/crates/bedrock/src/bedrock.rs @@ -54,11 +54,7 @@ pub async fn stream_completion( )]))); } - if request - .tools - .as_ref() - .map_or(false, |t| !t.tools.is_empty()) - { + if request.tools.as_ref().is_some_and(|t| !t.tools.is_empty()) { response = response.set_tool_config(request.tools); } diff --git a/crates/call/src/call_impl/mod.rs b/crates/call/src/call_impl/mod.rs index 6cc94a5dd536f61a1809899cb9f7d13a9c7e3381..156a80faba61d2a4946bafa5943c167284d14a97 100644 --- a/crates/call/src/call_impl/mod.rs +++ b/crates/call/src/call_impl/mod.rs @@ -147,7 +147,7 @@ impl ActiveCall { let mut incoming_call = this.incoming_call.0.borrow_mut(); if incoming_call .as_ref() - .map_or(false, |call| call.room_id == envelope.payload.room_id) + .is_some_and(|call| call.room_id == envelope.payload.room_id) { incoming_call.take(); } diff --git a/crates/call/src/call_impl/participant.rs b/crates/call/src/call_impl/participant.rs index 8e1e264a23d7c58c927d182bbac811a0beb4f02a..6fb6a2eb79b537aa9d7296a323f7d45221a4b05d 100644 --- a/crates/call/src/call_impl/participant.rs +++ b/crates/call/src/call_impl/participant.rs @@ -64,7 +64,7 @@ pub struct RemoteParticipant { impl RemoteParticipant { pub fn has_video_tracks(&self) -> bool { - return !self.video_tracks.is_empty(); + !self.video_tracks.is_empty() } pub fn can_write(&self) -> bool { diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index bab99cd3f32c409b9b549df6b5276868a2e3951d..ffe4c6c25191dc8f9087ccfcc77252b8e5a25a13 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -939,8 +939,7 @@ impl Room { self.client.user_id() ) })?; - if self.live_kit.as_ref().map_or(true, |kit| kit.deafened) && publication.is_audio() - { + if self.live_kit.as_ref().is_none_or(|kit| kit.deafened) && publication.is_audio() { publication.set_enabled(false, cx); } match track { @@ -1174,7 +1173,7 @@ impl Room { this.update(cx, |this, cx| { this.shared_projects.insert(project.downgrade()); let active_project = this.local_participant.active_project.as_ref(); - if active_project.map_or(false, |location| *location == project) { + if active_project.is_some_and(|location| *location == project) { this.set_location(Some(&project), cx) } else { Task::ready(Ok(())) @@ -1247,9 +1246,9 @@ impl Room { } pub fn is_sharing_screen(&self) -> bool { - self.live_kit.as_ref().map_or(false, |live_kit| { - !matches!(live_kit.screen_track, LocalTrack::None) - }) + self.live_kit + .as_ref() + .is_some_and(|live_kit| !matches!(live_kit.screen_track, LocalTrack::None)) } pub fn shared_screen_id(&self) -> Option<u64> { @@ -1262,13 +1261,13 @@ impl Room { } pub fn is_sharing_mic(&self) -> bool { - self.live_kit.as_ref().map_or(false, |live_kit| { - !matches!(live_kit.microphone_track, LocalTrack::None) - }) + self.live_kit + .as_ref() + .is_some_and(|live_kit| !matches!(live_kit.microphone_track, LocalTrack::None)) } pub fn is_muted(&self) -> bool { - self.live_kit.as_ref().map_or(false, |live_kit| { + self.live_kit.as_ref().is_some_and(|live_kit| { matches!(live_kit.microphone_track, LocalTrack::None) || live_kit.muted_by_user || live_kit.deafened @@ -1278,13 +1277,13 @@ impl Room { pub fn muted_by_user(&self) -> bool { self.live_kit .as_ref() - .map_or(false, |live_kit| live_kit.muted_by_user) + .is_some_and(|live_kit| live_kit.muted_by_user) } pub fn is_speaking(&self) -> bool { self.live_kit .as_ref() - .map_or(false, |live_kit| live_kit.speaking) + .is_some_and(|live_kit| live_kit.speaking) } pub fn is_deafened(&self) -> Option<bool> { diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 86f307717c03695d68f17f0c54346abeeaf225fd..baf23ac39f983c018da2f291bec7879913f12a58 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -340,7 +340,7 @@ impl ChannelChat { return ControlFlow::Break( if cursor .item() - .map_or(false, |message| message.id == message_id) + .is_some_and(|message| message.id == message_id) { Some(cursor.start().1.0) } else { @@ -362,7 +362,7 @@ impl ChannelChat { if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id && self .last_acknowledged_id - .map_or(true, |acknowledged_id| acknowledged_id < latest_message_id) + .is_none_or(|acknowledged_id| acknowledged_id < latest_message_id) { self.rpc .send(proto::AckChannelMessage { @@ -612,7 +612,7 @@ impl ChannelChat { while let Some(message) = old_cursor.item() { let message_ix = old_cursor.start().1.0; if nonces.contains(&message.nonce) { - if ranges.last().map_or(false, |r| r.end == message_ix) { + if ranges.last().is_some_and(|r| r.end == message_ix) { ranges.last_mut().unwrap().end += 1; } else { ranges.push(message_ix..message_ix + 1); diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 42a1851408f6cf7c5add3f4c1c29a2998d6ec5af..850a4946135305d372e9b08b0b8ecc9ad7daf407 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -568,16 +568,14 @@ impl ChannelStore { self.channel_index .by_id() .get(&channel_id) - .map_or(false, |channel| channel.is_root_channel()) + .is_some_and(|channel| channel.is_root_channel()) } pub fn is_public_channel(&self, channel_id: ChannelId) -> bool { self.channel_index .by_id() .get(&channel_id) - .map_or(false, |channel| { - channel.visibility == ChannelVisibility::Public - }) + .is_some_and(|channel| channel.visibility == ChannelVisibility::Public) } pub fn channel_capability(&self, channel_id: ChannelId) -> Capability { diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index d8b46dabb69e803aa4be2cf77b8fb11ad08d6d5f..57890628f2893e3a8e47105cc23aea14079ce307 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -363,7 +363,7 @@ fn anonymous_fd(path: &str) -> Option<fs::File> { let fd: fd::RawFd = fd_str.parse().ok()?; let file = unsafe { fs::File::from_raw_fd(fd) }; - return Some(file); + Some(file) } #[cfg(any(target_os = "macos", target_os = "freebsd"))] { @@ -381,13 +381,13 @@ fn anonymous_fd(path: &str) -> Option<fs::File> { } let fd: fd::RawFd = fd_str.parse().ok()?; let file = unsafe { fs::File::from_raw_fd(fd) }; - return Some(file); + Some(file) } #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))] { _ = path; // not implemented for bsd, windows. Could be, but isn't yet - return None; + None } } @@ -586,7 +586,7 @@ mod flatpak { pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args { if env::var(NO_ESCAPE_ENV_NAME).is_ok() - && env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed")) + && env::var("FLATPAK_ID").is_ok_and(|id| id.starts_with("dev.zed.Zed")) && args.zed.is_none() { args.zed = Some("/app/libexec/zed-editor".into()); diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 218cf2b0797576956cc14a60a2575ebb00a40660..058a12417a6f52a54f405eab655b2c9bb3b4fa5a 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -76,7 +76,7 @@ pub static ZED_APP_PATH: LazyLock<Option<PathBuf>> = LazyLock::new(|| std::env::var("ZED_APP_PATH").ok().map(PathBuf::from)); pub static ZED_ALWAYS_ACTIVE: LazyLock<bool> = - LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty())); + LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").is_ok_and(|e| !e.is_empty())); pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500); pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(30); diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 722d6861ff59ba7b65ff67044e18c02c5576476b..2599be9b1690727ca3c91b16c9d3f03923617c13 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -848,7 +848,7 @@ impl UserStore { pub fn has_accepted_terms_of_service(&self) -> bool { self.accepted_tos_at - .map_or(false, |accepted_tos_at| accepted_tos_at.is_some()) + .is_some_and(|accepted_tos_at| accepted_tos_at.is_some()) } pub fn accept_terms_of_service(&self, cx: &Context<Self>) -> Task<Result<()>> { diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index ef9a1a9a553596baf737c4e1ee60d9b3344f4ecf..92417d8319fc1ce0750f0c2b400e49a24b83e073 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -205,12 +205,12 @@ impl CloudApiClient { let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; if response.status() == StatusCode::UNAUTHORIZED { - return Ok(false); + Ok(false) } else { - return Err(anyhow!( + Err(anyhow!( "Failed to get authenticated user.\nStatus: {:?}\nBody: {body}", response.status() - )); + )) } } } diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index ca8a42d54de73c71a324f259b1c92bf23e94358c..cd4cf69f60663c90fd4525c8946aa34038f8b164 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -304,7 +304,7 @@ impl RandomizedTest for ProjectCollaborationTest { let worktree = worktree.read(cx); worktree.is_visible() && worktree.entries(false, 0).any(|e| e.is_file()) - && worktree.root_entry().map_or(false, |e| e.is_dir()) + && worktree.root_entry().is_some_and(|e| e.is_dir()) }) .choose(rng) }); diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 77ce74d58149d0df00206d49189d118db177e8e2..5ed3907f6cfdb3e5b5eba3fc4c930b6b2b0ffb18 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -890,7 +890,7 @@ impl ChatPanel { this.highlighted_message = Some((highlight_message_id, task)); } - if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) { + if this.active_chat.as_ref().is_some_and(|(c, _)| *c == chat) { this.message_list.scroll_to(ListOffset { item_ix, offset_in_item: px(0.0), @@ -1186,7 +1186,7 @@ impl Panel for ChatPanel { let is_in_call = ActiveCall::global(cx) .read(cx) .room() - .map_or(false, |room| room.read(cx).contains_guests()); + .is_some_and(|room| room.read(cx).contains_guests()); self.active || is_in_call } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 526aacf066bc2619d198e23762b1137dd2b8cad9..0f785c1f90d71f6a9826254c6918a88fcdf67b1d 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -664,9 +664,7 @@ impl CollabPanel { let has_children = channel_store .channel_at_index(mat.candidate_id + 1) - .map_or(false, |next_channel| { - next_channel.parent_path.ends_with(&[channel.id]) - }); + .is_some_and(|next_channel| next_channel.parent_path.ends_with(&[channel.id])); match &self.channel_editing_state { Some(ChannelEditingState::Create { @@ -1125,7 +1123,7 @@ impl CollabPanel { } fn has_subchannels(&self, ix: usize) -> bool { - self.entries.get(ix).map_or(false, |entry| { + self.entries.get(ix).is_some_and(|entry| { if let ListEntry::Channel { has_children, .. } = entry { *has_children } else { diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 00c3bbf623321e9648dde867934786274683a777..a900d585f8f0834c1d6be310f5a840ed939d0cf4 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -497,7 +497,7 @@ impl NotificationPanel { panel.is_scrolled_to_bottom() && panel .active_chat() - .map_or(false, |chat| chat.read(cx).channel_id.0 == *channel_id) + .is_some_and(|chat| chat.read(cx).channel_id.0 == *channel_id) } else { false }; diff --git a/crates/credentials_provider/src/credentials_provider.rs b/crates/credentials_provider/src/credentials_provider.rs index f72fd6c39b12d5d46cfa1d4f3f30900f01471e64..2c8dd6fc812aaeffd6c06c88ee2adceabdbb27a3 100644 --- a/crates/credentials_provider/src/credentials_provider.rs +++ b/crates/credentials_provider/src/credentials_provider.rs @@ -19,7 +19,7 @@ use release_channel::ReleaseChannel; /// Only works in development. Setting this environment variable in other /// release channels is a no-op. static ZED_DEVELOPMENT_USE_KEYCHAIN: LazyLock<bool> = LazyLock::new(|| { - std::env::var("ZED_DEVELOPMENT_USE_KEYCHAIN").map_or(false, |value| !value.is_empty()) + std::env::var("ZED_DEVELOPMENT_USE_KEYCHAIN").is_ok_and(|value| !value.is_empty()) }); /// A provider for credentials. diff --git a/crates/dap_adapters/src/codelldb.rs b/crates/dap_adapters/src/codelldb.rs index 842bb264a8469402fe73747356ab2e616ab08533..25dc875740e8aba87872e8dc93fa8d77062ed545 100644 --- a/crates/dap_adapters/src/codelldb.rs +++ b/crates/dap_adapters/src/codelldb.rs @@ -385,7 +385,7 @@ impl DebugAdapter for CodeLldbDebugAdapter { && let Some(source_languages) = config.get("sourceLanguages").filter(|value| { value .as_array() - .map_or(false, |array| array.iter().all(Value::is_string)) + .is_some_and(|array| array.iter().all(Value::is_string)) }) { let ret = vec![ diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 7fed761f5a05ea6638f1718cbbe115b16f6a54c5..37e347282d0181ecfd8f58af7f65c8d6e6ecef40 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -37,7 +37,7 @@ const FALLBACK_DB_NAME: &str = "FALLBACK_MEMORY_DB"; const DB_FILE_NAME: &str = "db.sqlite"; pub static ZED_STATELESS: LazyLock<bool> = - LazyLock::new(|| env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty())); + LazyLock::new(|| env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); pub static ALL_FILE_DB_FAILED: LazyLock<AtomicBool> = LazyLock::new(|| AtomicBool::new(false)); diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index daf0b136fde5bd62411c70033e8bcfcb668a5e06..256b789c9b2f2909ec5b12f6dc9dd60c04555e51 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -20,7 +20,7 @@ pub trait Dismissable { KEY_VALUE_STORE .read_kvp(Self::KEY) .log_err() - .map_or(false, |s| s.is_some()) + .is_some_and(|s| s.is_some()) } fn set_dismissed(is_dismissed: bool, cx: &mut App) { diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index e60c08cd0faffb0725fdc0a534580e9c612ce2f5..131272da6b031a04cd9c4e5421a7ff024b73cfc2 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -392,7 +392,7 @@ impl LogStore { session.label(), session .adapter_client() - .map_or(false, |client| client.has_adapter_logs()), + .is_some_and(|client| client.has_adapter_logs()), ) }); diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 3c1d35cdd33c2051a7e44f0eecf8fc8a2bc2a797..449deb4ddbd74ada3fba7e07302e6a744f7bd6f8 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -414,7 +414,7 @@ pub(crate) fn new_debugger_pane( .and_then(|item| item.downcast::<SubView>()); let is_hovered = as_subview .as_ref() - .map_or(false, |item| item.read(cx).hovered); + .is_some_and(|item| item.read(cx).hovered); h_flex() .track_focus(&focus_handle) @@ -427,7 +427,6 @@ pub(crate) fn new_debugger_pane( .bg(cx.theme().colors().tab_bar_background) .on_action(|_: &menu::Cancel, window, cx| { if cx.stop_active_drag(window) { - return; } else { cx.propagate(); } @@ -449,7 +448,7 @@ pub(crate) fn new_debugger_pane( .children(pane.items().enumerate().map(|(ix, item)| { let selected = active_pane_item .as_ref() - .map_or(false, |active| active.item_id() == item.item_id()); + .is_some_and(|active| active.item_id() == item.item_id()); let deemphasized = !pane.has_focus(window, cx); let item_ = item.boxed_clone(); div() diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 095b069fa3fe25c700ce3fb5bf9a5b8778aece24..26a26c7bef565224eb2a6de1b2e628ab8fbcec69 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -528,7 +528,7 @@ impl BreakpointList { cx.background_executor() .spawn(async move { KEY_VALUE_STORE.write_kvp(key, value?).await }) } else { - return Task::ready(Result::Ok(())); + Task::ready(Result::Ok(())) } } diff --git a/crates/diagnostics/src/diagnostic_renderer.rs b/crates/diagnostics/src/diagnostic_renderer.rs index cb1c052925cb7a3117b817406e0caec0adfc568f..e9731f84ce258bbe55dc278eb4d2ddb0d6bab9ef 100644 --- a/crates/diagnostics/src/diagnostic_renderer.rs +++ b/crates/diagnostics/src/diagnostic_renderer.rs @@ -287,15 +287,13 @@ impl DiagnosticBlock { } } } - } else { - if let Some(diagnostic) = editor - .snapshot(window, cx) - .buffer_snapshot - .diagnostic_group(buffer_id, group_id) - .nth(ix) - { - Self::jump_to(editor, diagnostic.range, window, cx) - } + } else if let Some(diagnostic) = editor + .snapshot(window, cx) + .buffer_snapshot + .diagnostic_group(buffer_id, group_id) + .nth(ix) + { + Self::jump_to(editor, diagnostic.range, window, cx) }; } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index c15c0f2493fb4ea0cabdfee963a2238416899e33..2e20118381de2e814ef2361280763eeab48606de 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -383,12 +383,10 @@ impl ProjectDiagnosticsEditor { } else { self.update_all_diagnostics(false, window, cx); } + } else if self.update_excerpts_task.is_some() { + self.update_excerpts_task = None; } else { - if self.update_excerpts_task.is_some() { - self.update_excerpts_task = None; - } else { - self.update_all_diagnostics(false, window, cx); - } + self.update_all_diagnostics(false, window, cx); } cx.notify(); } @@ -542,7 +540,7 @@ impl ProjectDiagnosticsEditor { return true; } this.diagnostics.insert(buffer_id, diagnostics.clone()); - return false; + false })?; if unchanged { return Ok(()); diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 29011352fbb565a8ead2f1e3d6e23258682fc246..6ac0f49fada427f84b55aae11bc56cb08f39a62b 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -295,7 +295,7 @@ fn dump_all_gpui_actions() -> Vec<ActionDef> { actions.sort_by_key(|a| a.name); - return actions; + actions } fn handle_postprocessing() -> Result<()> { diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 4632a03daf53460cc0f674c0bca425f6bc689f24..21c934fefaa480dd8c58e3927e21ef0a3c14a2b7 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -185,7 +185,7 @@ impl Render for EditPredictionButton { let this = cx.entity(); let fs = self.fs.clone(); - return div().child( + div().child( PopoverMenu::new("supermaven") .menu(move |window, cx| match &status { SupermavenButtonStatus::NeedsActivation(activate_url) => { @@ -230,7 +230,7 @@ impl Render for EditPredictionButton { }, ) .with_handle(self.popover_menu_handle.clone()), - ); + ) } EditPredictionProvider::Zed => { @@ -343,7 +343,7 @@ impl Render for EditPredictionButton { let is_refreshing = self .edit_prediction_provider .as_ref() - .map_or(false, |provider| provider.is_refreshing(cx)); + .is_some_and(|provider| provider.is_refreshing(cx)); if is_refreshing { popover_menu = popover_menu.trigger( diff --git a/crates/editor/src/clangd_ext.rs b/crates/editor/src/clangd_ext.rs index 07be9ea9e92d1f5db0d4cae343f83ab3a9480526..c78d4c83c01c49e6b1ff947d3cd53bc887424a16 100644 --- a/crates/editor/src/clangd_ext.rs +++ b/crates/editor/src/clangd_ext.rs @@ -13,7 +13,7 @@ use crate::{Editor, SwitchSourceHeader, element::register_action}; use project::lsp_store::clangd_ext::CLANGD_SERVER_NAME; fn is_c_language(language: &Language) -> bool { - return language.name() == "C++".into() || language.name() == "C".into(); + language.name() == "C++".into() || language.name() == "C".into() } pub fn switch_source_header( diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 24d2cfddcb09ba1ecc24b3d2702c15b693a1a3e0..4847bc25658d8ff12dc3ee6c3a74d2fae2048f36 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1111,10 +1111,8 @@ impl CompletionsMenu { let query_start_doesnt_match_split_words = query_start_lower .map(|query_char| { !split_words(&string_match.string).any(|word| { - word.chars() - .next() - .and_then(|c| c.to_lowercase().next()) - .map_or(false, |word_char| word_char == query_char) + word.chars().next().and_then(|c| c.to_lowercase().next()) + == Some(query_char) }) }) .unwrap_or(false); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index cc1cc2c44078a0f95ecff09ee7abbcc8f4143567..c16e4a6ddbb971b44d71421d6ad868e6423eb035 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -991,7 +991,7 @@ impl DisplaySnapshot { if let Some(severity) = chunk.diagnostic_severity.filter(|severity| { self.diagnostics_max_severity .into_lsp() - .map_or(false, |max_severity| severity <= &max_severity) + .is_some_and(|max_severity| severity <= &max_severity) }) { if chunk.is_unnecessary { diagnostic_highlight.fade_out = Some(editor_style.unnecessary_code_fade); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 5ae37d20fa11e47de69b4d19595681f300d62fde..5d5c9500ebcd1c1088318a9f7a2a3f4db5ddb0b1 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -528,10 +528,7 @@ impl BlockMap { if let Some(transform) = cursor.item() && transform.summary.input_rows > 0 && cursor.end() == old_start - && transform - .block - .as_ref() - .map_or(true, |b| !b.is_replacement()) + && transform.block.as_ref().is_none_or(|b| !b.is_replacement()) { // Preserve the transform (push and next) new_transforms.push(transform.clone(), &()); @@ -539,7 +536,7 @@ impl BlockMap { // Preserve below blocks at end of edit while let Some(transform) = cursor.item() { - if transform.block.as_ref().map_or(false, |b| b.place_below()) { + if transform.block.as_ref().is_some_and(|b| b.place_below()) { new_transforms.push(transform.clone(), &()); cursor.next(); } else { @@ -606,7 +603,7 @@ impl BlockMap { // Discard below blocks at the end of the edit. They'll be reconstructed. while let Some(transform) = cursor.item() { - if transform.block.as_ref().map_or(false, |b| b.place_below()) { + if transform.block.as_ref().is_some_and(|b| b.place_below()) { cursor.next(); } else { break; @@ -1328,7 +1325,7 @@ impl BlockSnapshot { let Dimensions(output_start, input_start, _) = cursor.start(); let overshoot = if cursor .item() - .map_or(false, |transform| transform.block.is_none()) + .is_some_and(|transform| transform.block.is_none()) { start_row.0 - output_start.0 } else { @@ -1358,7 +1355,7 @@ impl BlockSnapshot { && transform .block .as_ref() - .map_or(false, |block| block.height() > 0)) + .is_some_and(|block| block.height() > 0)) { break; } @@ -1511,7 +1508,7 @@ impl BlockSnapshot { pub(super) fn is_block_line(&self, row: BlockRow) -> bool { let mut cursor = self.transforms.cursor::<Dimensions<BlockRow, WrapRow>>(&()); cursor.seek(&row, Bias::Right); - cursor.item().map_or(false, |t| t.block.is_some()) + cursor.item().is_some_and(|t| t.block.is_some()) } pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool { @@ -1529,11 +1526,11 @@ impl BlockSnapshot { .make_wrap_point(Point::new(row.0, 0), Bias::Left); let mut cursor = self.transforms.cursor::<Dimensions<WrapRow, BlockRow>>(&()); cursor.seek(&WrapRow(wrap_point.row()), Bias::Right); - cursor.item().map_or(false, |transform| { + cursor.item().is_some_and(|transform| { transform .block .as_ref() - .map_or(false, |block| block.is_replacement()) + .is_some_and(|block| block.is_replacement()) }) } @@ -1653,7 +1650,7 @@ impl BlockChunks<'_> { if transform .block .as_ref() - .map_or(false, |block| block.height() == 0) + .is_some_and(|block| block.height() == 0) { self.transforms.next(); } else { @@ -1664,7 +1661,7 @@ impl BlockChunks<'_> { if self .transforms .item() - .map_or(false, |transform| transform.block.is_none()) + .is_some_and(|transform| transform.block.is_none()) { let start_input_row = self.transforms.start().1.0; let start_output_row = self.transforms.start().0.0; @@ -1774,7 +1771,7 @@ impl Iterator for BlockRows<'_> { if transform .block .as_ref() - .map_or(false, |block| block.height() == 0) + .is_some_and(|block| block.height() == 0) { self.transforms.next(); } else { @@ -1786,7 +1783,7 @@ impl Iterator for BlockRows<'_> { if transform .block .as_ref() - .map_or(true, |block| block.is_replacement()) + .is_none_or(|block| block.is_replacement()) { self.input_rows.seek(self.transforms.start().1.0); } diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 3509bcbba8d24e98d06ba8ca77a0f341909e9382..3dcd172c3c484a51456a86c4f242b959b7a732f3 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -491,14 +491,14 @@ impl FoldMap { while folds .peek() - .map_or(false, |(_, fold_range)| fold_range.start < edit.new.end) + .is_some_and(|(_, fold_range)| fold_range.start < edit.new.end) { let (fold, mut fold_range) = folds.next().unwrap(); let sum = new_transforms.summary(); assert!(fold_range.start.0 >= sum.input.len); - while folds.peek().map_or(false, |(next_fold, next_fold_range)| { + while folds.peek().is_some_and(|(next_fold, next_fold_range)| { next_fold_range.start < fold_range.end || (next_fold_range.start == fold_range.end && fold.placeholder.merge_adjacent @@ -575,14 +575,14 @@ impl FoldMap { for mut edit in inlay_edits { old_transforms.seek(&edit.old.start, Bias::Left); - if old_transforms.item().map_or(false, |t| t.is_fold()) { + if old_transforms.item().is_some_and(|t| t.is_fold()) { edit.old.start = old_transforms.start().0; } let old_start = old_transforms.start().1.0 + (edit.old.start - old_transforms.start().0).0; old_transforms.seek_forward(&edit.old.end, Bias::Right); - if old_transforms.item().map_or(false, |t| t.is_fold()) { + if old_transforms.item().is_some_and(|t| t.is_fold()) { old_transforms.next(); edit.old.end = old_transforms.start().0; } @@ -590,14 +590,14 @@ impl FoldMap { old_transforms.start().1.0 + (edit.old.end - old_transforms.start().0).0; new_transforms.seek(&edit.new.start, Bias::Left); - if new_transforms.item().map_or(false, |t| t.is_fold()) { + if new_transforms.item().is_some_and(|t| t.is_fold()) { edit.new.start = new_transforms.start().0; } let new_start = new_transforms.start().1.0 + (edit.new.start - new_transforms.start().0).0; new_transforms.seek_forward(&edit.new.end, Bias::Right); - if new_transforms.item().map_or(false, |t| t.is_fold()) { + if new_transforms.item().is_some_and(|t| t.is_fold()) { new_transforms.next(); edit.new.end = new_transforms.start().0; } @@ -709,7 +709,7 @@ impl FoldSnapshot { .transforms .cursor::<Dimensions<InlayPoint, FoldPoint>>(&()); cursor.seek(&point, Bias::Right); - if cursor.item().map_or(false, |t| t.is_fold()) { + if cursor.item().is_some_and(|t| t.is_fold()) { if bias == Bias::Left || point == cursor.start().0 { cursor.start().1 } else { @@ -788,7 +788,7 @@ impl FoldSnapshot { let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset); let mut cursor = self.transforms.cursor::<InlayOffset>(&()); cursor.seek(&inlay_offset, Bias::Right); - cursor.item().map_or(false, |t| t.placeholder.is_some()) + cursor.item().is_some_and(|t| t.placeholder.is_some()) } pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool { @@ -839,7 +839,7 @@ impl FoldSnapshot { let inlay_end = if transform_cursor .item() - .map_or(true, |transform| transform.is_fold()) + .is_none_or(|transform| transform.is_fold()) { inlay_start } else if range.end < transform_end.0 { @@ -1348,7 +1348,7 @@ impl FoldChunks<'_> { let inlay_end = if self .transform_cursor .item() - .map_or(true, |transform| transform.is_fold()) + .is_none_or(|transform| transform.is_fold()) { inlay_start } else if range.end < transform_end.0 { @@ -1463,7 +1463,7 @@ impl FoldOffset { .transforms .cursor::<Dimensions<FoldOffset, TransformSummary>>(&()); cursor.seek(&self, Bias::Right); - let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) { + let overshoot = if cursor.item().is_none_or(|t| t.is_fold()) { Point::new(0, (self.0 - cursor.start().0.0) as u32) } else { let inlay_offset = cursor.start().1.input.len + self.0 - cursor.start().0.0; diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 626dbf5cba73c5ed3865b7f51c775b62086aeedf..3db9d10fdc74f418ecd4ea682dde91185130cd46 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -625,7 +625,7 @@ impl InlayMap { // we can push its remainder. if buffer_edits_iter .peek() - .map_or(true, |edit| edit.old.start >= cursor.end().0) + .is_none_or(|edit| edit.old.start >= cursor.end().0) { let transform_start = new_transforms.summary().input.len; let transform_end = diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 7aa252a7f3b3103cfa1bc440098dfc03089c0452..500ec3a0bb77f8a8332e86485b81b357644e6d23 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -74,10 +74,10 @@ impl WrapRows<'_> { self.transforms .seek(&WrapPoint::new(start_row, 0), Bias::Left); let mut input_row = self.transforms.start().1.row(); - if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { + if self.transforms.item().is_some_and(|t| t.is_isomorphic()) { input_row += start_row - self.transforms.start().0.row(); } - self.soft_wrapped = self.transforms.item().map_or(false, |t| !t.is_isomorphic()); + self.soft_wrapped = self.transforms.item().is_some_and(|t| !t.is_isomorphic()); self.input_buffer_rows.seek(input_row); self.input_buffer_row = self.input_buffer_rows.next().unwrap(); self.output_row = start_row; @@ -603,7 +603,7 @@ impl WrapSnapshot { .cursor::<Dimensions<WrapPoint, TabPoint>>(&()); 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()) { + if transforms.item().is_some_and(|t| t.is_isomorphic()) { input_start.0 += output_start.0 - transforms.start().0.0; } let input_end = self @@ -634,7 +634,7 @@ impl WrapSnapshot { cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left); if cursor .item() - .map_or(false, |transform| transform.is_isomorphic()) + .is_some_and(|transform| transform.is_isomorphic()) { let overshoot = row - cursor.start().0.row(); let tab_row = cursor.start().1.row() + overshoot; @@ -732,10 +732,10 @@ impl WrapSnapshot { .cursor::<Dimensions<WrapPoint, TabPoint>>(&()); 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()) { + if transforms.item().is_some_and(|t| t.is_isomorphic()) { input_row += start_row - transforms.start().0.row(); } - let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic()); + let soft_wrapped = transforms.item().is_some_and(|t| !t.is_isomorphic()); let mut input_buffer_rows = self.tab_snapshot.rows(input_row); let input_buffer_row = input_buffer_rows.next().unwrap(); WrapRows { @@ -754,7 +754,7 @@ impl WrapSnapshot { .cursor::<Dimensions<WrapPoint, TabPoint>>(&()); cursor.seek(&point, Bias::Right); let mut tab_point = cursor.start().1.0; - if cursor.item().map_or(false, |t| t.is_isomorphic()) { + if cursor.item().is_some_and(|t| t.is_isomorphic()) { tab_point += point.0 - cursor.start().0.0; } TabPoint(tab_point) @@ -780,7 +780,7 @@ impl WrapSnapshot { if bias == Bias::Left { let mut cursor = self.transforms.cursor::<WrapPoint>(&()); cursor.seek(&point, Bias::Right); - if cursor.item().map_or(false, |t| !t.is_isomorphic()) { + if cursor.item().is_some_and(|t| !t.is_isomorphic()) { point = *cursor.start(); *point.column_mut() -= 1; } @@ -901,7 +901,7 @@ impl WrapChunks<'_> { let output_end = WrapPoint::new(rows.end, 0); self.transforms.seek(&output_start, Bias::Right); let mut input_start = TabPoint(self.transforms.start().1.0); - if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { + if self.transforms.item().is_some_and(|t| t.is_isomorphic()) { input_start.0 += output_start.0 - self.transforms.start().0.0; } let input_end = self @@ -993,7 +993,7 @@ impl Iterator for WrapRows<'_> { self.output_row += 1; self.transforms .seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left); - if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { + if self.transforms.item().is_some_and(|t| t.is_isomorphic()) { self.input_buffer_row = self.input_buffer_rows.next().unwrap(); self.soft_wrapped = false; } else { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ca1f1f8828a8846173454d75a20855286ecbeb76..7c36a410468da4a07d6a02d4038b2c9d048ae7f7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1429,7 +1429,7 @@ impl SelectionHistory { if self .undo_stack .back() - .map_or(true, |e| e.selections != entry.selections) + .is_none_or(|e| e.selections != entry.selections) { self.undo_stack.push_back(entry); if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN { @@ -1442,7 +1442,7 @@ impl SelectionHistory { if self .redo_stack .back() - .map_or(true, |e| e.selections != entry.selections) + .is_none_or(|e| e.selections != entry.selections) { self.redo_stack.push_back(entry); if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN { @@ -2512,9 +2512,7 @@ impl Editor { .context_menu .borrow() .as_ref() - .map_or(false, |context| { - matches!(context, CodeContextMenu::Completions(_)) - }); + .is_some_and(|context| matches!(context, CodeContextMenu::Completions(_))); showing_completions || self.edit_prediction_requires_modifier() @@ -2545,7 +2543,7 @@ impl Editor { || binding .keystrokes() .first() - .map_or(false, |keystroke| keystroke.modifiers.modified()) + .is_some_and(|keystroke| keystroke.modifiers.modified()) })) } @@ -2941,7 +2939,7 @@ impl Editor { return false; }; - scope.override_name().map_or(false, |scope_name| { + scope.override_name().is_some_and(|scope_name| { settings .edit_predictions_disabled_in .iter() @@ -4033,18 +4031,18 @@ impl Editor { let following_text_allows_autoclose = snapshot .chars_at(selection.start) .next() - .map_or(true, |c| scope.should_autoclose_before(c)); + .is_none_or(|c| scope.should_autoclose_before(c)); let preceding_text_allows_autoclose = selection.start.column == 0 - || snapshot.reversed_chars_at(selection.start).next().map_or( - true, - |c| { + || snapshot + .reversed_chars_at(selection.start) + .next() + .is_none_or(|c| { bracket_pair.start != bracket_pair.end || !snapshot .char_classifier_at(selection.start) .is_word(c) - }, - ); + }); let is_closing_quote = if bracket_pair.end == bracket_pair.start && bracket_pair.start.len() == 1 @@ -4185,7 +4183,7 @@ impl Editor { if !self.linked_edit_ranges.is_empty() { let start_anchor = snapshot.anchor_before(selection.start); - let is_word_char = text.chars().next().map_or(true, |char| { + let is_word_char = text.chars().next().is_none_or(|char| { let classifier = snapshot .char_classifier_at(start_anchor.to_offset(&snapshot)) .ignore_punctuation(true); @@ -5427,11 +5425,11 @@ impl Editor { let sort_completions = provider .as_ref() - .map_or(false, |provider| provider.sort_completions()); + .is_some_and(|provider| provider.sort_completions()); let filter_completions = provider .as_ref() - .map_or(true, |provider| provider.filter_completions()); + .is_none_or(|provider| provider.filter_completions()); let trigger_kind = match trigger { Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => { @@ -5537,7 +5535,7 @@ impl Editor { let skip_digits = query .as_ref() - .map_or(true, |query| !query.chars().any(|c| c.is_digit(10))); + .is_none_or(|query| !query.chars().any(|c| c.is_digit(10))); let (mut words, provider_responses) = match &provider { Some(provider) => { @@ -5971,7 +5969,7 @@ impl Editor { let show_new_completions_on_confirm = completion .confirm .as_ref() - .map_or(false, |confirm| confirm(intent, window, cx)); + .is_some_and(|confirm| confirm(intent, window, cx)); if show_new_completions_on_confirm { self.show_completions(&ShowCompletions { trigger: None }, window, cx); } @@ -6103,10 +6101,10 @@ impl Editor { let spawn_straight_away = quick_launch && resolved_tasks .as_ref() - .map_or(false, |tasks| tasks.templates.len() == 1) + .is_some_and(|tasks| tasks.templates.len() == 1) && code_actions .as_ref() - .map_or(true, |actions| actions.is_empty()) + .is_none_or(|actions| actions.is_empty()) && debug_scenarios.is_empty(); editor.update_in(cx, |editor, window, cx| { @@ -6720,9 +6718,9 @@ impl Editor { let buffer_id = cursor_position.buffer_id; let buffer = this.buffer.read(cx); - if !buffer + if buffer .text_anchor_for_position(cursor_position, cx) - .map_or(false, |(buffer, _)| buffer == cursor_buffer) + .is_none_or(|(buffer, _)| buffer != cursor_buffer) { return; } @@ -6972,9 +6970,7 @@ impl Editor { || self .quick_selection_highlight_task .as_ref() - .map_or(true, |(prev_anchor_range, _)| { - prev_anchor_range != &query_range - }) + .is_none_or(|(prev_anchor_range, _)| prev_anchor_range != &query_range) { let multi_buffer_visible_start = self .scroll_manager @@ -7003,9 +6999,7 @@ impl Editor { || self .debounced_selection_highlight_task .as_ref() - .map_or(true, |(prev_anchor_range, _)| { - prev_anchor_range != &query_range - }) + .is_none_or(|(prev_anchor_range, _)| prev_anchor_range != &query_range) { let multi_buffer_start = multi_buffer_snapshot .anchor_before(0) @@ -7140,9 +7134,7 @@ impl Editor { && self .edit_prediction_provider .as_ref() - .map_or(false, |provider| { - provider.provider.show_completions_in_menu() - }); + .is_some_and(|provider| provider.provider.show_completions_in_menu()); let preview_requires_modifier = all_language_settings(file, cx).edit_predictions_mode() == EditPredictionsMode::Subtle; @@ -7726,7 +7718,7 @@ impl Editor { || self .active_edit_prediction .as_ref() - .map_or(false, |completion| { + .is_some_and(|completion| { let invalidation_range = completion.invalidation_range.to_offset(&multibuffer); let invalidation_range = invalidation_range.start..=invalidation_range.end; !invalidation_range.contains(&offset_selection.head()) @@ -8427,7 +8419,7 @@ impl Editor { .context_menu .borrow() .as_ref() - .map_or(false, |menu| menu.visible()) + .is_some_and(|menu| menu.visible()) } pub fn context_menu_origin(&self) -> Option<ContextMenuOrigin> { @@ -8973,9 +8965,8 @@ impl Editor { let end_row = start_row + line_count as u32; visible_row_range.contains(&start_row) && visible_row_range.contains(&end_row) - && cursor_row.map_or(true, |cursor_row| { - !((start_row..end_row).contains(&cursor_row)) - }) + && cursor_row + .is_none_or(|cursor_row| !((start_row..end_row).contains(&cursor_row))) })?; content_origin @@ -9585,7 +9576,7 @@ impl Editor { .tabstops .iter() .map(|tabstop| { - let is_end_tabstop = tabstop.ranges.first().map_or(false, |tabstop| { + let is_end_tabstop = tabstop.ranges.first().is_some_and(|tabstop| { tabstop.is_empty() && tabstop.start == snippet.text.len() as isize }); let mut tabstop_ranges = tabstop @@ -11716,7 +11707,7 @@ impl Editor { let transpose_start = display_map .buffer_snapshot .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); - if edits.last().map_or(true, |e| e.0.end <= transpose_start) { + if edits.last().is_none_or(|e| e.0.end <= transpose_start) { let transpose_end = display_map .buffer_snapshot .clip_offset(transpose_offset + 1, Bias::Right); @@ -16229,23 +16220,21 @@ impl Editor { if split { workspace.split_item(SplitDirection::Right, item.clone(), window, cx); - } else { - if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { - let (preview_item_id, preview_item_idx) = - workspace.active_pane().read_with(cx, |pane, _| { - (pane.preview_item_id(), pane.preview_item_idx()) - }); + } else if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { + let (preview_item_id, preview_item_idx) = + workspace.active_pane().read_with(cx, |pane, _| { + (pane.preview_item_id(), pane.preview_item_idx()) + }); - workspace.add_item_to_active_pane(item.clone(), preview_item_idx, true, window, cx); + workspace.add_item_to_active_pane(item.clone(), preview_item_idx, true, window, cx); - if let Some(preview_item_id) = preview_item_id { - workspace.active_pane().update(cx, |pane, cx| { - pane.remove_item(preview_item_id, false, false, window, cx); - }); - } - } else { - workspace.add_item_to_active_pane(item.clone(), None, true, window, cx); + if let Some(preview_item_id) = preview_item_id { + workspace.active_pane().update(cx, |pane, cx| { + pane.remove_item(preview_item_id, false, false, window, cx); + }); } + } else { + workspace.add_item_to_active_pane(item.clone(), None, true, window, cx); } workspace.active_pane().update(cx, |pane, cx| { pane.set_preview_item_id(Some(item_id), cx); @@ -19010,7 +18999,7 @@ impl Editor { fn has_blame_entries(&self, cx: &App) -> bool { self.blame() - .map_or(false, |blame| blame.read(cx).has_generated_entries()) + .is_some_and(|blame| blame.read(cx).has_generated_entries()) } fn newest_selection_head_on_empty_line(&self, cx: &App) -> bool { @@ -19660,7 +19649,7 @@ impl Editor { pub fn has_background_highlights<T: 'static>(&self) -> bool { self.background_highlights .get(&HighlightKey::Type(TypeId::of::<T>())) - .map_or(false, |(_, highlights)| !highlights.is_empty()) + .is_some_and(|(_, highlights)| !highlights.is_empty()) } pub fn background_highlights_in_range( @@ -20582,7 +20571,7 @@ impl Editor { // For now, don't allow opening excerpts in buffers that aren't backed by // regular project files. fn can_open_excerpts_in_file(file: Option<&Arc<dyn language::File>>) -> bool { - file.map_or(true, |file| project::File::from_dyn(Some(file)).is_some()) + file.is_none_or(|file| project::File::from_dyn(Some(file)).is_some()) } fn marked_text_ranges(&self, cx: &App) -> Option<Vec<Range<OffsetUtf16>>> { @@ -21125,7 +21114,7 @@ impl Editor { pub fn has_visible_completions_menu(&self) -> bool { !self.edit_prediction_preview_is_active() - && self.context_menu.borrow().as_ref().map_or(false, |menu| { + && self.context_menu.borrow().as_ref().is_some_and(|menu| { menu.visible() && matches!(menu, CodeContextMenu::Completions(_)) }) } @@ -21548,9 +21537,9 @@ fn is_grapheme_whitespace(text: &str) -> bool { } fn should_stay_with_preceding_ideograph(text: &str) -> bool { - text.chars().next().map_or(false, |ch| { - matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…') - }) + text.chars() + .next() + .is_some_and(|ch| matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…')) } #[derive(PartialEq, Eq, Debug, Clone, Copy)] @@ -21589,11 +21578,11 @@ impl<'a> Iterator for WordBreakingTokenizer<'a> { } else { let mut words = self.input[offset..].split_word_bound_indices().peekable(); let mut next_word_bound = words.peek().copied(); - if next_word_bound.map_or(false, |(i, _)| i == 0) { + if next_word_bound.is_some_and(|(i, _)| i == 0) { next_word_bound = words.next(); } while let Some(grapheme) = iter.peek().copied() { - if next_word_bound.map_or(false, |(i, _)| i == offset) { + if next_word_bound.is_some_and(|(i, _)| i == offset) { break; }; if is_grapheme_whitespace(grapheme) != is_whitespace diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index d3a21c7642e2d5eb11a75e06c5466210dd68f63c..1d7e04cae021dd7b755f1f80e78fd3ea83197539 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -810,10 +810,8 @@ impl Settings for EditorSettings { if gutter.line_numbers.is_some() { old_gutter.line_numbers = gutter.line_numbers } - } else { - if gutter != GutterContent::default() { - current.gutter = Some(gutter) - } + } else if gutter != GutterContent::default() { + current.gutter = Some(gutter) } if let Some(b) = vscode.read_bool("editor.scrollBeyondLastLine") { current.scroll_beyond_last_line = Some(if b { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d8fe3ccf158a7b8b20fc4fcd56030ad24d066ad7..c14e49fc1d2c52c8285f56538de6dd7b3d368a3f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -919,6 +919,7 @@ impl EditorElement { { #[allow( clippy::collapsible_if, + clippy::needless_return, reason = "The cfg-block below makes this a false positive" )] if !text_hitbox.is_hovered(window) || editor.read_only(cx) { @@ -1126,26 +1127,24 @@ impl EditorElement { let hovered_diff_hunk_row = if let Some(control_row) = hovered_diff_control { Some(control_row) - } else { - if text_hovered { - let current_row = valid_point.row(); - position_map.display_hunks.iter().find_map(|(hunk, _)| { - if let DisplayDiffHunk::Unfolded { - display_row_range, .. - } = hunk - { - if display_row_range.contains(¤t_row) { - Some(display_row_range.start) - } else { - None - } + } else if text_hovered { + let current_row = valid_point.row(); + position_map.display_hunks.iter().find_map(|(hunk, _)| { + if let DisplayDiffHunk::Unfolded { + display_row_range, .. + } = hunk + { + if display_row_range.contains(¤t_row) { + Some(display_row_range.start) } else { None } - }) - } else { - None - } + } else { + None + } + }) + } else { + None }; if hovered_diff_hunk_row != editor.hovered_diff_hunk_row { @@ -1159,11 +1158,11 @@ impl EditorElement { .inline_blame_popover .as_ref() .and_then(|state| state.popover_bounds) - .map_or(false, |bounds| bounds.contains(&event.position)); + .is_some_and(|bounds| bounds.contains(&event.position)); let keyboard_grace = editor .inline_blame_popover .as_ref() - .map_or(false, |state| state.keyboard_grace); + .is_some_and(|state| state.keyboard_grace); if mouse_over_inline_blame || mouse_over_popover { editor.show_blame_popover(blame_entry, event.position, false, cx); @@ -1190,10 +1189,10 @@ impl EditorElement { let is_visible = editor .gutter_breakpoint_indicator .0 - .map_or(false, |indicator| indicator.is_active); + .is_some_and(|indicator| indicator.is_active); let has_existing_breakpoint = - editor.breakpoint_store.as_ref().map_or(false, |store| { + editor.breakpoint_store.as_ref().is_some_and(|store| { let Some(project) = &editor.project else { return false; }; @@ -2220,12 +2219,11 @@ impl EditorElement { cmp::max(padded_line, min_start) }; - let behind_edit_prediction_popover = edit_prediction_popover_origin.as_ref().map_or( - false, - |edit_prediction_popover_origin| { + let behind_edit_prediction_popover = edit_prediction_popover_origin + .as_ref() + .is_some_and(|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 { @@ -2291,9 +2289,7 @@ impl EditorElement { None } }) - .map_or(false, |source| { - matches!(source, CodeActionSource::Indicator(..)) - }); + .is_some_and(|source| matches!(source, CodeActionSource::Indicator(..))); Some(editor.render_inline_code_actions(icon_size, display_point.row(), active, cx)) })?; @@ -2909,7 +2905,7 @@ impl EditorElement { if multibuffer_row .0 .checked_sub(1) - .map_or(false, |previous_row| { + .is_some_and(|previous_row| { snapshot.is_line_folded(MultiBufferRow(previous_row)) }) { @@ -3900,7 +3896,7 @@ impl EditorElement { for (row, block) in fixed_blocks { let block_id = block.id(); - if focused_block.as_ref().map_or(false, |b| b.id == block_id) { + if focused_block.as_ref().is_some_and(|b| b.id == block_id) { focused_block = None; } @@ -3957,7 +3953,7 @@ impl EditorElement { }; let block_id = block.id(); - if focused_block.as_ref().map_or(false, |b| b.id == block_id) { + if focused_block.as_ref().is_some_and(|b| b.id == block_id) { focused_block = None; } @@ -4736,7 +4732,7 @@ impl EditorElement { } }; - let source_included = source_display_point.map_or(true, |source_display_point| { + let source_included = source_display_point.is_none_or(|source_display_point| { visible_range .to_inclusive() .contains(&source_display_point.row()) @@ -4916,7 +4912,7 @@ impl EditorElement { let intersects_menu = |bounds: Bounds<Pixels>| -> bool { context_menu_layout .as_ref() - .map_or(false, |menu| bounds.intersects(&menu.bounds)) + .is_some_and(|menu| bounds.intersects(&menu.bounds)) }; let can_place_above = { @@ -5101,7 +5097,7 @@ impl EditorElement { if active_positions .iter() - .any(|p| p.map_or(false, |p| display_row_range.contains(&p.row()))) + .any(|p| p.is_some_and(|p| display_row_range.contains(&p.row()))) { let y = display_row_range.start.as_f32() * line_height + text_hitbox.bounds.top() @@ -5214,7 +5210,7 @@ impl EditorElement { let intersects_menu = |bounds: Bounds<Pixels>| -> bool { context_menu_layout .as_ref() - .map_or(false, |menu| bounds.intersects(&menu.bounds)) + .is_some_and(|menu| bounds.intersects(&menu.bounds)) }; let final_origin = if popover_bounds_above.is_contained_within(hitbox) @@ -5299,7 +5295,7 @@ impl EditorElement { let mut end_row = start_row.0; while active_rows .peek() - .map_or(false, |(active_row, has_selection)| { + .is_some_and(|(active_row, has_selection)| { active_row.0 == end_row + 1 && has_selection.selection == contains_non_empty_selection.selection }) @@ -6687,25 +6683,23 @@ impl EditorElement { editor.set_scroll_position(position, window, cx); } cx.stop_propagation(); - } else { - if minimap_hitbox.is_hovered(window) { - editor.scroll_manager.set_is_hovering_minimap_thumb( - !event.dragging() - && layout - .thumb_layout - .thumb_bounds - .is_some_and(|bounds| bounds.contains(&event.position)), - cx, - ); + } else if minimap_hitbox.is_hovered(window) { + editor.scroll_manager.set_is_hovering_minimap_thumb( + !event.dragging() + && layout + .thumb_layout + .thumb_bounds + .is_some_and(|bounds| bounds.contains(&event.position)), + cx, + ); - // Stop hover events from propagating to the - // underlying editor if the minimap hitbox is hovered - if !event.dragging() { - cx.stop_propagation(); - } - } else { - editor.scroll_manager.hide_minimap_thumb(cx); + // Stop hover events from propagating to the + // underlying editor if the minimap hitbox is hovered + if !event.dragging() { + cx.stop_propagation(); } + } else { + editor.scroll_manager.hide_minimap_thumb(cx); } mouse_position = event.position; }); @@ -7084,9 +7078,7 @@ impl EditorElement { let unstaged_hollow = ProjectSettings::get_global(cx) .git .hunk_style - .map_or(false, |style| { - matches!(style, GitHunkStyleSetting::UnstagedHollow) - }); + .is_some_and(|style| matches!(style, GitHunkStyleSetting::UnstagedHollow)); unstaged == unstaged_hollow } @@ -8183,7 +8175,7 @@ impl Element for EditorElement { let is_row_soft_wrapped = |row: usize| { row_infos .get(row) - .map_or(true, |info| info.buffer_row.is_none()) + .is_none_or(|info| info.buffer_row.is_none()) }; let start_anchor = if start_row == Default::default() { @@ -9718,14 +9710,12 @@ impl PointForPosition { false } else if start_row == end_row { candidate_col >= start_col && candidate_col < end_col + } else if candidate_row == start_row { + candidate_col >= start_col + } else if candidate_row == end_row { + candidate_col < end_col } else { - if candidate_row == start_row { - candidate_col >= start_col - } else if candidate_row == end_row { - candidate_col < end_col - } else { - true - } + true } } } diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 712325f339867a8629b7f6ffca60369d7603e9cd..2f6106c86cda709f58c49daae713a56a6cbb0f68 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -415,7 +415,7 @@ impl GitBlame { let old_end = cursor.end(); if row_edits .peek() - .map_or(true, |next_edit| next_edit.old.start >= old_end) + .is_none_or(|next_edit| next_edit.old.start >= old_end) && let Some(entry) = cursor.item() { if old_end > edit.old.end { diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 358d8683fe48a3c4b95d44b0818296b2ca0f5b43..04e66a234c0b16131b492264eb9e798e76b24453 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -271,7 +271,7 @@ impl Editor { Task::ready(Ok(Navigated::No)) }; self.select(SelectPhase::End, window, cx); - return navigate_task; + navigate_task } } @@ -871,7 +871,7 @@ fn surrounding_filename( .peekable(); while let Some(ch) = forwards.next() { // Skip escaped whitespace - if ch == '\\' && forwards.peek().map_or(false, |ch| ch.is_whitespace()) { + if ch == '\\' && forwards.peek().is_some_and(|ch| ch.is_whitespace()) { token_end += ch.len_utf8(); let whitespace = forwards.next().unwrap(); token_end += whitespace.len_utf8(); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 136b0b314d9d74ca04e4de778c6855963160330c..e3d2f92c553a2f446cf837615da39a6d21d0b876 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -201,7 +201,7 @@ impl FollowableItem for Editor { if buffer .as_singleton() .and_then(|buffer| buffer.read(cx).file()) - .map_or(false, |file| file.is_private()) + .is_some_and(|file| file.is_private()) { return None; } @@ -715,7 +715,7 @@ impl Item for Editor { .read(cx) .as_singleton() .and_then(|buffer| buffer.read(cx).file()) - .map_or(false, |file| file.disk_state() == DiskState::Deleted); + .is_some_and(|file| file.disk_state() == DiskState::Deleted); h_flex() .gap_2() diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index cae4b565b4b2b7dc1462e362d072bcff9c9cfd36..13e5d0a8c75d4b329aa466405b6a3d2167e7c955 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -86,9 +86,9 @@ pub(crate) fn should_auto_close( }); } if to_auto_edit.is_empty() { - return None; + None } else { - return Some(to_auto_edit); + Some(to_auto_edit) } } @@ -186,7 +186,7 @@ pub(crate) fn generate_auto_close_edits( let range = node_name.byte_range(); return buffer.text_for_range(range).equals_str(name); } - return is_empty; + is_empty }; let tree_root_node = { @@ -227,7 +227,7 @@ pub(crate) fn generate_auto_close_edits( let has_open_tag_with_same_tag_name = ancestor .named_child(0) .filter(|n| n.kind() == config.open_tag_node_name) - .map_or(false, |element_open_tag_node| { + .is_some_and(|element_open_tag_node| { tag_node_name_equals(&element_open_tag_node, &tag_name) }); if has_open_tag_with_same_tag_name { @@ -263,8 +263,7 @@ pub(crate) fn generate_auto_close_edits( } let is_after_open_tag = |node: &Node| { - return node.start_byte() < open_tag.start_byte() - && node.end_byte() < open_tag.start_byte(); + node.start_byte() < open_tag.start_byte() && node.end_byte() < open_tag.start_byte() }; // perf: use cursor for more efficient traversal @@ -301,7 +300,7 @@ pub(crate) fn generate_auto_close_edits( let edit_range = edit_anchor..edit_anchor; edits.push((edit_range, format!("</{}>", tag_name))); } - return Ok(edits); + Ok(edits) } pub(crate) fn refresh_enabled_in_any_buffer( @@ -367,7 +366,7 @@ pub(crate) fn construct_initial_buffer_versions_map< initial_buffer_versions.insert(buffer_id, buffer_version); } } - return initial_buffer_versions; + initial_buffer_versions } pub(crate) fn handle_from( @@ -455,12 +454,9 @@ pub(crate) fn handle_from( let ensure_no_edits_since_start = || -> Option<()> { let has_edits_since_start = this .read_with(cx, |this, cx| { - this.buffer - .read(cx) - .buffer(buffer_id) - .map_or(true, |buffer| { - buffer.read(cx).has_edits_since(&buffer_version_initial) - }) + this.buffer.read(cx).buffer(buffer_id).is_none_or(|buffer| { + buffer.read(cx).has_edits_since(&buffer_version_initial) + }) }) .ok()?; diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 7f9eb374e8f3480f36b892246ac8f5c9d176c737..5cf22de537b6965085179dea522a4313799fa141 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -61,13 +61,13 @@ impl MouseContextMenu { source, offset: position - (source_position + content_origin), }; - return Some(MouseContextMenu::new( + Some(MouseContextMenu::new( editor, menu_position, context_menu, window, cx, - )); + )) } pub(crate) fn new( diff --git a/crates/editor/src/tasks.rs b/crates/editor/src/tasks.rs index 0d497e4cac779a65b7a6593d3b82f786d10321ce..8be2a3a2e14d7b815d2ca3496adc6f70ec16055e 100644 --- a/crates/editor/src/tasks.rs +++ b/crates/editor/src/tasks.rs @@ -89,7 +89,7 @@ impl Editor { .lsp_task_source()?; if lsp_settings .get(&lsp_tasks_source) - .map_or(true, |s| s.enable_lsp_tasks) + .is_none_or(|s| s.enable_lsp_tasks) { let buffer_id = buffer.read(cx).remote_id(); Some((lsp_tasks_source, buffer_id)) diff --git a/crates/eval/src/assertions.rs b/crates/eval/src/assertions.rs index 489e4aa22ecdc6633a0002238a2287ca0a5105f0..01fac186d33a8b5b156121acf924d37c90c64679 100644 --- a/crates/eval/src/assertions.rs +++ b/crates/eval/src/assertions.rs @@ -54,7 +54,7 @@ impl AssertionsReport { pub fn passed_count(&self) -> usize { self.ran .iter() - .filter(|a| a.result.as_ref().map_or(false, |result| result.passed)) + .filter(|a| a.result.as_ref().is_ok_and(|result| result.passed)) .count() } diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 809b530ed77257930cd4d6cb1c17720655529f2a..1d2bece5cc1ad99c96b0e161a1c188e3d61680af 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -112,7 +112,7 @@ fn main() { let telemetry = app_state.client.telemetry(); telemetry.start(system_id, installation_id, session_id, cx); - let enable_telemetry = env::var("ZED_EVAL_TELEMETRY").map_or(false, |value| value == "1") + let enable_telemetry = env::var("ZED_EVAL_TELEMETRY").is_ok_and(|value| value == "1") && telemetry.has_checksum_seed(); if enable_telemetry { println!("Telemetry enabled"); diff --git a/crates/eval/src/examples/add_arg_to_trait_method.rs b/crates/eval/src/examples/add_arg_to_trait_method.rs index 9c538f926059eb3998eb725168905d148dccdc9d..084f12bc6263da030d313c362cc3d051dfdb8ea8 100644 --- a/crates/eval/src/examples/add_arg_to_trait_method.rs +++ b/crates/eval/src/examples/add_arg_to_trait_method.rs @@ -70,10 +70,10 @@ impl Example for AddArgToTraitMethod { let path_str = format!("crates/assistant_tools/src/{}.rs", tool_name); let edits = edits.get(Path::new(&path_str)); - let ignored = edits.map_or(false, |edits| { + let ignored = edits.is_some_and(|edits| { edits.has_added_line(" _window: Option<gpui::AnyWindowHandle>,\n") }); - let uningored = edits.map_or(false, |edits| { + let uningored = edits.is_some_and(|edits| { edits.has_added_line(" window: Option<gpui::AnyWindowHandle>,\n") }); @@ -89,7 +89,7 @@ impl Example for AddArgToTraitMethod { let batch_tool_edits = edits.get(Path::new("crates/assistant_tools/src/batch_tool.rs")); cx.assert( - batch_tool_edits.map_or(false, |edits| { + batch_tool_edits.is_some_and(|edits| { edits.has_added_line(" window: Option<gpui::AnyWindowHandle>,\n") }), "Argument: batch_tool", diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index b80525798bff3e34bedfb5d62d4b5563691f93b1..432adaf4bc90e8b3c60b8baec14426dc4ec392b0 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -401,7 +401,7 @@ impl ExtensionBuilder { let mut clang_path = wasi_sdk_dir.clone(); clang_path.extend(["bin", &format!("clang{}", env::consts::EXE_SUFFIX)]); - if fs::metadata(&clang_path).map_or(false, |metadata| metadata.is_file()) { + if fs::metadata(&clang_path).is_ok_and(|metadata| metadata.is_file()) { return Ok(clang_path); } diff --git a/crates/extension/src/extension_events.rs b/crates/extension/src/extension_events.rs index b151b3f412ea523a1c5b97dea210adf68e5bea89..94f3277b05b76aa93717458c57d0280a15b8435f 100644 --- a/crates/extension/src/extension_events.rs +++ b/crates/extension/src/extension_events.rs @@ -19,9 +19,8 @@ pub struct ExtensionEvents; impl ExtensionEvents { /// Returns the global [`ExtensionEvents`]. pub fn try_global(cx: &App) -> Option<Entity<Self>> { - return cx - .try_global::<GlobalExtensionEvents>() - .map(|g| g.0.clone()); + cx.try_global::<GlobalExtensionEvents>() + .map(|g| g.0.clone()) } fn new(_cx: &mut Context<Self>) -> Self { diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 01edb5c033b9f9f35a93aefb9cca5c79453bf5b1..1a05dbc570e7b45963028b70cf643d93c94dd59c 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -562,12 +562,12 @@ impl ExtensionStore { extensions .into_iter() .filter(|extension| { - this.extension_index.extensions.get(&extension.id).map_or( - true, - |installed_extension| { + this.extension_index + .extensions + .get(&extension.id) + .is_none_or(|installed_extension| { installed_extension.manifest.version != extension.manifest.version - }, - ) + }) }) .collect() }) @@ -1451,7 +1451,7 @@ impl ExtensionStore { if extension_dir .file_name() - .map_or(false, |file_name| file_name == ".DS_Store") + .is_some_and(|file_name| file_name == ".DS_Store") { continue; } diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 7c12571f24bfcfe7823d5e291afbeec6ccc43d9f..49ccfcc85c09194017aeb96a3a590fb60a4a71ca 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -14,7 +14,7 @@ struct FeatureFlags { } pub static ZED_DISABLE_STAFF: LazyLock<bool> = LazyLock::new(|| { - std::env::var("ZED_DISABLE_STAFF").map_or(false, |value| !value.is_empty() && value != "0") + std::env::var("ZED_DISABLE_STAFF").is_ok_and(|value| !value.is_empty() && value != "0") }); impl FeatureFlags { diff --git a/crates/feedback/src/system_specs.rs b/crates/feedback/src/system_specs.rs index 7c002d90e94ed5c44a1076aac2788fc8d1150eaa..b5ccaca6895e4ec935e5e5200fa7828e85b3295e 100644 --- a/crates/feedback/src/system_specs.rs +++ b/crates/feedback/src/system_specs.rs @@ -135,7 +135,7 @@ impl Display for SystemSpecs { fn try_determine_available_gpus() -> Option<String> { #[cfg(any(target_os = "linux", target_os = "freebsd"))] { - return std::process::Command::new("vulkaninfo") + std::process::Command::new("vulkaninfo") .args(&["--summary"]) .output() .ok() @@ -150,11 +150,11 @@ fn try_determine_available_gpus() -> Option<String> { ] .join("\n") }) - .or(Some("Failed to run `vulkaninfo --summary`".to_string())); + .or(Some("Failed to run `vulkaninfo --summary`".to_string())) } #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] { - return None; + None } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index aebc262af05675b4a8687d4af6a1b82712001c4d..3a08ec08e0551a213c24c75d14d23f616bfe787d 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -878,9 +878,7 @@ impl FileFinderDelegate { PathMatchCandidateSet { snapshot: worktree.snapshot(), include_ignored: self.include_ignored.unwrap_or_else(|| { - worktree - .root_entry() - .map_or(false, |entry| entry.is_ignored) + worktree.root_entry().is_some_and(|entry| entry.is_ignored) }), include_root_name, candidates: project::Candidates::Files, diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 3a99afc8cbc0b1ecae384c9184fabb548318187a..77acdf8097bdd86bd26f8a0f2d8a221ea7938f03 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -728,7 +728,7 @@ impl PickerDelegate for OpenPathDelegate { .child(LabelLike::new().child(label_with_highlights)), ) } - DirectoryState::None { .. } => return None, + DirectoryState::None { .. } => None, } } diff --git a/crates/file_icons/src/file_icons.rs b/crates/file_icons/src/file_icons.rs index 82a8e05d8571b04ec177c9944a765778684fe2a4..42c00fb12d5e9f0fbb1662eb0941ed70d94382b5 100644 --- a/crates/file_icons/src/file_icons.rs +++ b/crates/file_icons/src/file_icons.rs @@ -72,7 +72,7 @@ impl FileIcons { return maybe_path; } } - return this.get_icon_for_type("default", cx); + this.get_icon_for_type("default", cx) } fn default_icon_theme(cx: &App) -> Option<Arc<IconTheme>> { diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 399c0f3e320b5653ee262acb370bbaab442a9a18..d17cbdcf51d30fea6e402419ed21c8ee70b63a2e 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -625,13 +625,13 @@ impl Fs for RealFs { async fn is_file(&self, path: &Path) -> bool { smol::fs::metadata(path) .await - .map_or(false, |metadata| metadata.is_file()) + .is_ok_and(|metadata| metadata.is_file()) } async fn is_dir(&self, path: &Path) -> bool { smol::fs::metadata(path) .await - .map_or(false, |metadata| metadata.is_dir()) + .is_ok_and(|metadata| metadata.is_dir()) } async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> { diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index c30b789d9ff9957bfb00d57bab45da27c1e0a433..edcad514bb2b232cac56d4ca87c04a8121ba1713 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -269,10 +269,8 @@ impl GitExcludeOverride { pub async fn restore_original(&mut self) -> Result<()> { if let Some(ref original) = self.original_excludes { smol::fs::write(&self.git_exclude_path, original).await?; - } else { - if self.git_exclude_path.exists() { - smol::fs::remove_file(&self.git_exclude_path).await?; - } + } else if self.git_exclude_path.exists() { + smol::fs::remove_file(&self.git_exclude_path).await?; } self.added_excludes = None; @@ -2052,7 +2050,7 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> { } fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> { - if upstream_track == "" { + if upstream_track.is_empty() { return Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead: 0, behind: 0, diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index 07896b0c011bc58747381de53db0c2996da23c0e..d428ccbb0509702ee2535fb8c8e95b059fa24499 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -88,11 +88,10 @@ impl CommitView { let ix = pane.items().position(|item| { let commit_view = item.downcast::<CommitView>(); commit_view - .map_or(false, |view| view.read(cx).commit.sha == commit.sha) + .is_some_and(|view| view.read(cx).commit.sha == commit.sha) }); if let Some(ix) = ix { pane.activate_item(ix, true, true, window, cx); - return; } else { pane.add_item(Box::new(commit_view), true, true, None, window, cx); } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 82870b4e756eea76f269006ce35191d6958cb97e..ace3a8eb15a001208fdd4977bb7b40ce0a35d8e6 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -775,7 +775,7 @@ impl GitPanel { if window .focused(cx) - .map_or(false, |focused| self.focus_handle == focused) + .is_some_and(|focused| self.focus_handle == focused) { dispatch_context.add("menu"); dispatch_context.add("ChangesList"); @@ -894,9 +894,7 @@ impl GitPanel { let have_entries = self .active_repository .as_ref() - .map_or(false, |active_repository| { - active_repository.read(cx).status_summary().count > 0 - }); + .is_some_and(|active_repository| active_repository.read(cx).status_summary().count > 0); if have_entries && self.selected_entry.is_none() { self.selected_entry = Some(1); self.scroll_to_selected_entry(cx); @@ -1207,9 +1205,7 @@ impl GitPanel { }) .ok(); } - _ => { - return; - } + _ => {} }) .detach(); } @@ -1640,13 +1636,12 @@ impl GitPanel { fn has_commit_message(&self, cx: &mut Context<Self>) -> bool { let text = self.commit_editor.read(cx).text(cx); if !text.trim().is_empty() { - return true; + true } else if text.is_empty() { - return self - .suggest_commit_message(cx) - .is_some_and(|text| !text.trim().is_empty()); + self.suggest_commit_message(cx) + .is_some_and(|text| !text.trim().is_empty()) } else { - return false; + false } } @@ -2938,8 +2933,7 @@ impl GitPanel { .matches(git::repository::REMOTE_CANCELLED_BY_USER) .next() .is_some() - { - return; // Hide the cancelled by user message + { // Hide the cancelled by user message } else { workspace.update(cx, |workspace, cx| { let workspace_weak = cx.weak_entity(); @@ -3272,12 +3266,10 @@ impl GitPanel { } else { "Amend Tracked" } + } else if self.has_staged_changes() { + "Commit" } else { - if self.has_staged_changes() { - "Commit" - } else { - "Commit Tracked" - } + "Commit Tracked" } } @@ -4498,7 +4490,7 @@ impl Render for GitPanel { let has_write_access = self.has_write_access(cx); - let has_co_authors = room.map_or(false, |room| { + let has_co_authors = room.is_some_and(|room| { self.load_local_committer(cx); let room = room.read(cx); room.remote_participants() @@ -4814,12 +4806,10 @@ impl RenderOnce for PanelRepoFooter { // ideally, show the whole branch and repo names but // when we can't, use a budget to allocate space between the two - let (repo_display_len, branch_display_len) = if branch_actual_len + repo_actual_len - <= LABEL_CHARACTER_BUDGET - { - (repo_actual_len, branch_actual_len) - } else { - if branch_actual_len <= MAX_BRANCH_LEN { + let (repo_display_len, branch_display_len) = + if branch_actual_len + repo_actual_len <= LABEL_CHARACTER_BUDGET { + (repo_actual_len, branch_actual_len) + } else if branch_actual_len <= MAX_BRANCH_LEN { let repo_space = (LABEL_CHARACTER_BUDGET - branch_actual_len).min(MAX_REPO_LEN); (repo_space, branch_actual_len) } else if repo_actual_len <= MAX_REPO_LEN { @@ -4827,8 +4817,7 @@ impl RenderOnce for PanelRepoFooter { (repo_actual_len, branch_space) } else { (MAX_REPO_LEN, MAX_BRANCH_LEN) - } - }; + }; let truncated_repo_name = if repo_actual_len <= repo_display_len { active_repo_name.to_string() diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 3c0898fabfe3832cc548beffab61b87c85c87b80..c12ef58ce26b8a1bf524605870975021259d9024 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -329,14 +329,14 @@ impl ProjectDiff { }) .ok(); - return ButtonStates { + ButtonStates { stage: has_unstaged_hunks, unstage: has_staged_hunks, prev_next, selection, stage_all, unstage_all, - }; + } } fn handle_editor_event( diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 9d918048fafc6c4d2abdc4979e873affb54b94ff..23729be062ab64c3367d58ac5e87de67a077ac7e 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -129,7 +129,7 @@ impl CursorPosition { cursor_position.selected_count.lines += 1; } } - if last_selection.as_ref().map_or(true, |last_selection| { + if last_selection.as_ref().is_none_or(|last_selection| { selection.id > last_selection.id }) { last_selection = Some(selection); diff --git a/crates/google_ai/src/google_ai.rs b/crates/google_ai/src/google_ai.rs index a1b5ca3a03d67fe63032df8f3996298ea64f169c..ca0aa309b1296e021402d921b7a7809f1e593e2b 100644 --- a/crates/google_ai/src/google_ai.rs +++ b/crates/google_ai/src/google_ai.rs @@ -477,10 +477,10 @@ impl<'de> Deserialize<'de> for ModelName { model_id: id.to_string(), }) } else { - return Err(serde::de::Error::custom(format!( + Err(serde::de::Error::custom(format!( "Expected model name to begin with {}, got: {}", MODEL_NAME_PREFIX, string - ))); + ))) } } } diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index 48b2bcaf989516c8a2c82fee3cb417a352c5c931..6099ee58579576c85ed9bd6776b2a80d8419c188 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -786,7 +786,7 @@ impl<T: 'static> PartialOrd for WeakEntity<T> { #[cfg(any(test, feature = "leak-detection"))] static LEAK_BACKTRACE: std::sync::LazyLock<bool> = - std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty())); + std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").is_ok_and(|b| !b.is_empty())); #[cfg(any(test, feature = "leak-detection"))] #[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 7b689ca0adf6b1df2a7e83bb197da3135cf0d759..c9826b704e5732424f06e951c452331cb199a0fa 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -2274,7 +2274,7 @@ impl Interactivity { window.on_mouse_event(move |_: &MouseDownEvent, phase, window, _cx| { if phase == DispatchPhase::Bubble && !window.default_prevented() { let group_hovered = active_group_hitbox - .map_or(false, |group_hitbox_id| group_hitbox_id.is_hovered(window)); + .is_some_and(|group_hitbox_id| group_hitbox_id.is_hovered(window)); let element_hovered = hitbox.is_hovered(window); if group_hovered || element_hovered { *active_state.borrow_mut() = ElementClickedState { @@ -2614,7 +2614,7 @@ pub(crate) fn register_tooltip_mouse_handlers( window.on_mouse_event({ let active_tooltip = active_tooltip.clone(); move |_: &MouseDownEvent, _phase, window: &mut Window, _cx| { - if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(window)) { + if !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) { clear_active_tooltip_if_not_hoverable(&active_tooltip, window); } } @@ -2623,7 +2623,7 @@ pub(crate) fn register_tooltip_mouse_handlers( window.on_mouse_event({ let active_tooltip = active_tooltip.clone(); move |_: &ScrollWheelEvent, _phase, window: &mut Window, _cx| { - if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(window)) { + if !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) { clear_active_tooltip_if_not_hoverable(&active_tooltip, window); } } diff --git a/crates/gpui/src/elements/image_cache.rs b/crates/gpui/src/elements/image_cache.rs index 263f0aafc2c45739c2f7843454aaa8bccd335131..ee1436134a30f70e7015ab1c86f60733e60e9164 100644 --- a/crates/gpui/src/elements/image_cache.rs +++ b/crates/gpui/src/elements/image_cache.rs @@ -64,7 +64,7 @@ mod any_image_cache { cx: &mut App, ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> { let image_cache = image_cache.clone().downcast::<I>().unwrap(); - return image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx)); + image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx)) } } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 98b63ef907f144add4758ff69b0fd6bcef2706a9..6758f4eee1e03c5b32f1dd924ed6d37fa31cf767 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -938,9 +938,10 @@ impl Element for List { let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); // If the width of the list has changed, invalidate all cached item heights - if state.last_layout_bounds.map_or(true, |last_bounds| { - last_bounds.size.width != bounds.size.width - }) { + if state + .last_layout_bounds + .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width) + { let new_items = SumTree::from_iter( state.items.iter().map(|item| ListItem::Unmeasured { focus_handle: item.focus_handle(), diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index f682b78c4115ab9a05e2e23fca22a5b1c817b5f4..95374e579fa5cc11d84c2ba7e9ec88f261d8d2b2 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -458,7 +458,7 @@ impl DispatchTree { .keymap .borrow() .bindings_for_input(input, &context_stack); - return (bindings, partial, context_stack); + (bindings, partial, context_stack) } /// dispatch_key processes the keystroke @@ -639,10 +639,7 @@ mod tests { } fn partial_eq(&self, action: &dyn Action) -> bool { - action - .as_any() - .downcast_ref::<Self>() - .map_or(false, |a| self == a) + action.as_any().downcast_ref::<Self>() == Some(self) } fn boxed_clone(&self) -> std::boxed::Box<dyn Action> { diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index 281035fe97614dd810f1057c8094b2c698984166..976f99c26efdd5de6df0fc3297ad569316ce1e54 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -287,7 +287,7 @@ impl KeyBindingContextPredicate { return false; } } - return true; + true } // Workspace > Pane > Editor // @@ -305,7 +305,7 @@ impl KeyBindingContextPredicate { return true; } } - return false; + false } Self::And(left, right) => { left.eval_inner(contexts, all_contexts) && right.eval_inner(contexts, all_contexts) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 3e002309e46395432d5cec21042c6747fcde8397..1df8a608f457da356d8f723f743ebbcc58955733 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -592,7 +592,7 @@ impl PlatformTextSystem for NoopTextSystem { } fn font_id(&self, _descriptor: &Font) -> Result<FontId> { - return Ok(FontId(1)); + Ok(FontId(1)) } fn font_metrics(&self, _font_id: FontId) -> FontMetrics { diff --git a/crates/gpui/src/platform/blade/blade_context.rs b/crates/gpui/src/platform/blade/blade_context.rs index 48872f16198a4ed2d1fc8c2a0b1cbce3eb0de477..12c68a1e70188d3ed2ab425b5abc1bac0dfe3a19 100644 --- a/crates/gpui/src/platform/blade/blade_context.rs +++ b/crates/gpui/src/platform/blade/blade_context.rs @@ -49,7 +49,7 @@ fn parse_pci_id(id: &str) -> anyhow::Result<u32> { "Expected a 4 digit PCI ID in hexadecimal format" ); - return u32::from_str_radix(id, 16).context("parsing PCI ID as hex"); + u32::from_str_radix(id, 16).context("parsing PCI ID as hex") } #[cfg(test)] diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index a1da088b757b6ad6aaabb89ab4a33587c6dbbc98..ed824744a98b97e7410699f8335480ace4f94a7c 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -441,7 +441,7 @@ impl<P: LinuxClient + 'static> Platform for P { fn app_path(&self) -> Result<PathBuf> { // get the path of the executable of the current process let app_path = env::current_exe()?; - return Ok(app_path); + Ok(app_path) } fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) { diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index d1aa590192aad06df50525d5a63debb0d82f9e81..3278dfbe385e85e40fb6b7c4e9bafdf32174a68f 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -710,9 +710,7 @@ impl LinuxClient for WaylandClient { fn set_cursor_style(&self, style: CursorStyle) { let mut state = self.0.borrow_mut(); - let need_update = state - .cursor_style - .map_or(true, |current_style| current_style != style); + let need_update = state.cursor_style != Some(style); if need_update { let serial = state.serial_tracker.get(SerialKind::MouseEnter); @@ -1577,7 +1575,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr { if state .keyboard_focused_window .as_ref() - .map_or(false, |keyboard_window| window.ptr_eq(keyboard_window)) + .is_some_and(|keyboard_window| window.ptr_eq(keyboard_window)) { state.enter_token = None; } diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 7cf2d02d3b1c3f5356a46fd74a2a149807afba6f..1d1166a56c89eaa877625099e13664b6f93be790 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -669,8 +669,8 @@ impl WaylandWindowStatePtr { pub fn set_size_and_scale(&self, size: Option<Size<Pixels>>, scale: Option<f32>) { let (size, scale) = { let mut state = self.state.borrow_mut(); - if size.map_or(true, |size| size == state.bounds.size) - && scale.map_or(true, |scale| scale == state.scale) + if size.is_none_or(|size| size == state.bounds.size) + && scale.is_none_or(|scale| scale == state.scale) { return; } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index b4914c9dd29b8d66d5ee11da9ccc31d2aa885052..e422af961fd6ed2b731fbec0f433987986cc015d 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1586,11 +1586,11 @@ impl LinuxClient for X11Client { fn read_from_primary(&self) -> Option<crate::ClipboardItem> { let state = self.0.borrow_mut(); - return state + state .clipboard .get_any(clipboard::ClipboardKind::Primary) .context("X11: Failed to read from clipboard (primary)") - .log_with_level(log::Level::Debug); + .log_with_level(log::Level::Debug) } fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> { @@ -1603,11 +1603,11 @@ impl LinuxClient for X11Client { { return state.clipboard_item.clone(); } - return state + state .clipboard .get_any(clipboard::ClipboardKind::Clipboard) .context("X11: Failed to read from clipboard (clipboard)") - .log_with_level(log::Level::Debug); + .log_with_level(log::Level::Debug) } fn run(&self) { @@ -2010,12 +2010,12 @@ fn check_gtk_frame_extents_supported( } fn xdnd_is_atom_supported(atom: u32, atoms: &XcbAtoms) -> bool { - return atom == atoms.TEXT + atom == atoms.TEXT || atom == atoms.STRING || atom == atoms.UTF8_STRING || atom == atoms.TEXT_PLAIN || atom == atoms.TEXT_PLAIN_UTF8 - || atom == atoms.TextUriList; + || atom == atoms.TextUriList } fn xdnd_get_supported_atom( @@ -2043,7 +2043,7 @@ fn xdnd_get_supported_atom( } } } - return 0; + 0 } fn xdnd_send_finished( @@ -2144,7 +2144,7 @@ fn current_pointer_device_states( if pointer_device_states.is_empty() { log::error!("Found no xinput mouse pointers."); } - return Some(pointer_device_states); + Some(pointer_device_states) } /// Returns true if the device is a pointer device. Does not include pointer device groups. diff --git a/crates/gpui/src/platform/linux/x11/clipboard.rs b/crates/gpui/src/platform/linux/x11/clipboard.rs index 5b32f2c93eb762400c21b07c72c322aee554c7ae..a6f96d38c4254da5a2f92261700126962c16e91c 100644 --- a/crates/gpui/src/platform/linux/x11/clipboard.rs +++ b/crates/gpui/src/platform/linux/x11/clipboard.rs @@ -1078,11 +1078,11 @@ impl Clipboard { } else { String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)? }; - return Ok(ClipboardItem::new_string(text)); + Ok(ClipboardItem::new_string(text)) } pub fn is_owner(&self, selection: ClipboardKind) -> bool { - return self.inner.is_owner(selection).unwrap_or(false); + self.inner.is_owner(selection).unwrap_or(false) } } diff --git a/crates/gpui/src/platform/linux/x11/event.rs b/crates/gpui/src/platform/linux/x11/event.rs index a566762c540a1a39ae2e8fcfea78f4fdd0b1d436..17bcc908d3a6bdd48f16a8f5db69f08290b9444f 100644 --- a/crates/gpui/src/platform/linux/x11/event.rs +++ b/crates/gpui/src/platform/linux/x11/event.rs @@ -104,7 +104,7 @@ fn bit_is_set_in_vec(bit_vec: &Vec<u32>, bit_index: u16) -> bool { let array_index = bit_index as usize / 32; bit_vec .get(array_index) - .map_or(false, |bits| bit_is_set(*bits, bit_index % 32)) + .is_some_and(|bits| bit_is_set(*bits, bit_index % 32)) } fn bit_is_set(bits: u32, bit_index: u16) -> bool { diff --git a/crates/gpui/src/platform/mac/events.rs b/crates/gpui/src/platform/mac/events.rs index 0dc361b9dcfdb0980561037484cf51b84dc251e8..50a516cb388a404443d2fc635f589cdc4d8c64ee 100644 --- a/crates/gpui/src/platform/mac/events.rs +++ b/crates/gpui/src/platform/mac/events.rs @@ -311,9 +311,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask) - && first_char.map_or(true, |ch| { - !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch) - }); + && first_char + .is_none_or(|ch| !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch)); #[allow(non_upper_case_globals)] let key = match first_char { diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 57dfa9c6036cc4ddb8c9131712a9d675957f19ad..832550dc46281c56749aa8f6bc4d59a041c9a00d 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -797,7 +797,7 @@ impl Platform for MacPlatform { .to_owned(); result.set_file_name(&new_filename); } - return result; + result }) } } diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index 849925c72772b70162f09fc680c0be2d6510878a..72a0f2e565d9937e3aaf4082b663c3e2ae6ac91d 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -319,7 +319,7 @@ impl MacTextSystemState { fn is_emoji(&self, font_id: FontId) -> bool { self.postscript_names_by_font_id .get(&font_id) - .map_or(false, |postscript_name| { + .is_some_and(|postscript_name| { postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI" }) } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index b6f684a72c7eed2f75670401a9bbf51118c162db..bc60e13a59355abc8520213517a89cbc56934400 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -653,7 +653,7 @@ impl MacWindow { .and_then(|titlebar| titlebar.traffic_light_position), transparent_titlebar: titlebar .as_ref() - .map_or(true, |titlebar| titlebar.appears_transparent), + .is_none_or(|titlebar| titlebar.appears_transparent), previous_modifiers_changed_event: None, keystroke_for_do_command: None, do_command_handled: None, @@ -688,7 +688,7 @@ impl MacWindow { }); } - if titlebar.map_or(true, |titlebar| titlebar.appears_transparent) { + if titlebar.is_none_or(|titlebar| titlebar.appears_transparent) { native_window.setTitlebarAppearsTransparent_(YES); native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden); } diff --git a/crates/gpui/src/platform/test/dispatcher.rs b/crates/gpui/src/platform/test/dispatcher.rs index bdc783493160b0acf83852d515925f28df555527..4ce62c4bdcae60d517dd88501cb89af8fee2c9bc 100644 --- a/crates/gpui/src/platform/test/dispatcher.rs +++ b/crates/gpui/src/platform/test/dispatcher.rs @@ -270,9 +270,7 @@ impl PlatformDispatcher for TestDispatcher { fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) { { let mut state = self.state.lock(); - if label.map_or(false, |label| { - state.deprioritized_task_labels.contains(&label) - }) { + if label.is_some_and(|label| state.deprioritized_task_labels.contains(&label)) { state.deprioritized_background.push(runnable); } else { state.background.push(runnable); diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 09985722efa1b23b3b42f4d168549c651dd6bd26..5b69ce7fa6eb06affc2f77c0d1bdfbe4165c206a 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -573,7 +573,7 @@ impl Style { if self .border_color - .map_or(false, |color| !color.is_transparent()) + .is_some_and(|color| !color.is_transparent()) { min.x += self.border_widths.left.to_pixels(rem_size); max.x -= self.border_widths.right.to_pixels(rem_size); @@ -633,7 +633,7 @@ impl Style { window.paint_shadows(bounds, corner_radii, &self.box_shadow); let background_color = self.background.as_ref().and_then(Fill::color); - if background_color.map_or(false, |color| !color.is_transparent()) { + if background_color.is_some_and(|color| !color.is_transparent()) { let mut border_color = match background_color { Some(color) => match color.tag { BackgroundTag::Solid => color.solid, @@ -729,7 +729,7 @@ impl Style { fn is_border_visible(&self) -> bool { self.border_color - .map_or(false, |color| !color.is_transparent()) + .is_some_and(|color| !color.is_transparent()) && self.border_widths.any(|length| !length.is_zero()) } } diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 29af900b66a7b41fe69c7d4c03712ce0391a61c2..53991089da94c58d0035bff0d607ad3ab57a69bd 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -435,7 +435,7 @@ impl WindowTextSystem { }); } - if decoration_runs.last().map_or(false, |last_run| { + if decoration_runs.last().is_some_and(|last_run| { last_run.color == run.color && last_run.underline == run.underline && last_run.strikethrough == run.strikethrough diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 62aeb0df118a7ae929c53cdeba17b2231c147600..89c1595a3fb700566a0ce64f86b758affcee1393 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -243,14 +243,14 @@ impl FocusId { pub fn contains_focused(&self, window: &Window, cx: &App) -> bool { window .focused(cx) - .map_or(false, |focused| self.contains(focused.id, window)) + .is_some_and(|focused| self.contains(focused.id, window)) } /// Obtains whether the element associated with this handle is contained within the /// focused element or is itself focused. pub fn within_focused(&self, window: &Window, cx: &App) -> bool { let focused = window.focused(cx); - focused.map_or(false, |focused| focused.id.contains(*self, window)) + focused.is_some_and(|focused| focused.id.contains(*self, window)) } /// Obtains whether this handle contains the given handle in the most recently rendered frame. @@ -504,7 +504,7 @@ impl HitboxId { return true; } } - return false; + false } /// Checks if the hitbox with this ID contains the mouse and should handle scroll events. @@ -634,7 +634,7 @@ impl TooltipId { window .tooltip_bounds .as_ref() - .map_or(false, |tooltip_bounds| { + .is_some_and(|tooltip_bounds| { tooltip_bounds.id == *self && tooltip_bounds.bounds.contains(&window.mouse_position()) }) @@ -4466,7 +4466,7 @@ impl Window { } } } - return None; + None } } diff --git a/crates/gpui_macros/src/derive_inspector_reflection.rs b/crates/gpui_macros/src/derive_inspector_reflection.rs index 5415807ea08d63344375efc8a96f70424f0dd1ce..9c1cb503a87e5f726ba27d1868a6c053b36c6731 100644 --- a/crates/gpui_macros/src/derive_inspector_reflection.rs +++ b/crates/gpui_macros/src/derive_inspector_reflection.rs @@ -189,7 +189,7 @@ fn extract_cfg_attributes(attrs: &[Attribute]) -> Vec<Attribute> { fn is_called_from_gpui_crate(_span: Span) -> bool { // Check if we're being called from within the gpui crate by examining the call site // This is a heuristic approach - we check if the current crate name is "gpui" - std::env::var("CARGO_PKG_NAME").map_or(false, |name| name == "gpui") + std::env::var("CARGO_PKG_NAME").is_ok_and(|name| name == "gpui") } struct MacroExpander; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 9227d35a504458deeea4ab8b8030f9b247891742..972a90ddab0386eb0d347d239f1169927e80934a 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1406,7 +1406,7 @@ impl Buffer { }) .unwrap_or(true); let result = any_sub_ranges_contain_range; - return result; + result }) .last() .map(|info| info.language.clone()) @@ -1520,12 +1520,12 @@ impl Buffer { let new_syntax_map = parse_task.await; this.update(cx, move |this, cx| { let grammar_changed = - this.language.as_ref().map_or(true, |current_language| { + this.language.as_ref().is_none_or(|current_language| { !Arc::ptr_eq(&language, current_language) }); let language_registry_changed = new_syntax_map .contains_unknown_injections() - && language_registry.map_or(false, |registry| { + && language_registry.is_some_and(|registry| { registry.version() != new_syntax_map.language_registry_version() }); let parse_again = language_registry_changed @@ -1719,8 +1719,7 @@ impl Buffer { }) .with_delta(suggestion.delta, language_indent_size); - if old_suggestions.get(&new_row).map_or( - true, + if old_suggestions.get(&new_row).is_none_or( |(old_indentation, was_within_error)| { suggested_indent != *old_indentation && (!suggestion.within_error || *was_within_error) @@ -2014,7 +2013,7 @@ impl Buffer { fn was_changed(&mut self) { self.change_bits.retain(|change_bit| { - change_bit.upgrade().map_or(false, |bit| { + change_bit.upgrade().is_some_and(|bit| { bit.replace(true); true }) @@ -2191,7 +2190,7 @@ impl Buffer { if self .remote_selections .get(&self.text.replica_id()) - .map_or(true, |set| !set.selections.is_empty()) + .is_none_or(|set| !set.selections.is_empty()) { self.set_active_selections(Arc::default(), false, Default::default(), cx); } @@ -2839,7 +2838,7 @@ impl Buffer { let mut edits: Vec<(Range<usize>, String)> = Vec::new(); let mut last_end = None; for _ in 0..old_range_count { - if last_end.map_or(false, |last_end| last_end >= self.len()) { + if last_end.is_some_and(|last_end| last_end >= self.len()) { break; } @@ -3059,14 +3058,14 @@ impl BufferSnapshot { if config .decrease_indent_pattern .as_ref() - .map_or(false, |regex| regex.is_match(line)) + .is_some_and(|regex| regex.is_match(line)) { indent_change_rows.push((row, Ordering::Less)); } if config .increase_indent_pattern .as_ref() - .map_or(false, |regex| regex.is_match(line)) + .is_some_and(|regex| regex.is_match(line)) { indent_change_rows.push((row + 1, Ordering::Greater)); } @@ -3082,7 +3081,7 @@ impl BufferSnapshot { } } for rule in &config.decrease_indent_patterns { - if rule.pattern.as_ref().map_or(false, |r| r.is_match(line)) { + if rule.pattern.as_ref().is_some_and(|r| r.is_match(line)) { let row_start_column = self.indent_size_for_line(row).len; let basis_row = rule .valid_after @@ -3295,8 +3294,7 @@ impl BufferSnapshot { range: Range<D>, ) -> Option<SyntaxLayer<'_>> { let range = range.to_offset(self); - return self - .syntax + self.syntax .layers_for_range(range, &self.text, false) .max_by(|a, b| { if a.depth != b.depth { @@ -3306,7 +3304,7 @@ impl BufferSnapshot { } else { a.node().end_byte().cmp(&b.node().end_byte()).reverse() } - }); + }) } /// Returns the main [`Language`]. @@ -3365,8 +3363,7 @@ impl BufferSnapshot { } if let Some(range) = range - && smallest_range_and_depth.as_ref().map_or( - true, + && smallest_range_and_depth.as_ref().is_none_or( |(smallest_range, smallest_range_depth)| { if layer.depth > *smallest_range_depth { true @@ -3543,7 +3540,7 @@ impl BufferSnapshot { } } - return Some(cursor.node()); + Some(cursor.node()) } /// Returns the outline for the buffer. @@ -3572,7 +3569,7 @@ impl BufferSnapshot { )?; let mut prev_depth = None; items.retain(|item| { - let result = prev_depth.map_or(true, |prev_depth| item.depth > prev_depth); + let result = prev_depth.is_none_or(|prev_depth| item.depth > prev_depth); prev_depth = Some(item.depth); result }); @@ -4449,7 +4446,7 @@ impl BufferSnapshot { pub fn words_in_range(&self, query: WordsQuery) -> BTreeMap<String, Range<Anchor>> { let query_str = query.fuzzy_contents; - if query_str.map_or(false, |query| query.is_empty()) { + if query_str.is_some_and(|query| query.is_empty()) { return BTreeMap::default(); } @@ -4490,7 +4487,7 @@ impl BufferSnapshot { .and_then(|first_chunk| first_chunk.chars().next()); // Skip empty and "words" starting with digits as a heuristic to reduce useless completions if !query.skip_digits - || first_char.map_or(true, |first_char| !first_char.is_digit(10)) + || first_char.is_none_or(|first_char| !first_char.is_digit(10)) { words.insert(word_text.collect(), word_range); } diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 83c16f4558c66b2ab3c72315e52369237e50397b..589fc68e99cd05e0a11c776895c2e8f06b610199 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -773,7 +773,7 @@ impl LanguageRegistry { }; let content_matches = || { - config.first_line_pattern.as_ref().map_or(false, |pattern| { + config.first_line_pattern.as_ref().is_some_and(|pattern| { content .as_ref() .is_some_and(|content| pattern.is_match(content)) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 62fe75b6a8aff79f3c7069d6a06c5c1214c688a5..fbb67a98181c8dd19e5b49a59ff17854ad9a9019 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -253,7 +253,7 @@ impl EditPredictionSettings { !self.disabled_globs.iter().any(|glob| { if glob.is_absolute { file.as_local() - .map_or(false, |local| glob.matcher.is_match(local.abs_path(cx))) + .is_some_and(|local| glob.matcher.is_match(local.abs_path(cx))) } else { glob.matcher.is_match(file.path()) } diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 1e1060c843a444834ecd6bc39352b9415629b0b5..f10056af13f4b0881388e455df43eb1b9530dd6f 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -1630,10 +1630,8 @@ impl<'a> SyntaxLayer<'a> { if offset < range.start || offset > range.end { continue; } - } else { - if offset <= range.start || offset >= range.end { - continue; - } + } else if offset <= range.start || offset >= range.end { + continue; } if let Some((_, smallest_range)) = &smallest_match { diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index b16be36ea1baefbe3d54e395e21b5ae9b2cb7fb4..0d061c058765cd507c3785b82da3d055d0bc12be 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -554,7 +554,7 @@ pub fn into_anthropic( .into_iter() .filter_map(|content| match content { MessageContent::Text(text) => { - let text = if text.chars().last().map_or(false, |c| c.is_whitespace()) { + let text = if text.chars().last().is_some_and(|c| c.is_whitespace()) { text.trim_end().to_string() } else { text @@ -813,7 +813,7 @@ impl AnthropicEventMapper { ))]; } } - return vec![]; + vec![] } }, Event::ContentBlockStop { index } => { diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index e99dadc28de9fa236318b46ecd609997a8405e4d..d3fee7b63b3689fc815f135af870d85eca6998ec 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -270,7 +270,7 @@ impl State { if response.status().is_success() { let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; - return Ok(serde_json::from_str(&body)?); + Ok(serde_json::from_str(&body)?) } else { let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index 1bb9f3fa00b25f663797df46e2a1ede2fcc73710..a36ce949b177bf49de7bbcd914d9e68955fa4ef4 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -530,7 +530,7 @@ pub fn into_google( let system_instructions = if request .messages .first() - .map_or(false, |msg| matches!(msg.role, Role::System)) + .is_some_and(|msg| matches!(msg.role, Role::System)) { let message = request.messages.remove(0); Some(SystemInstruction { diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 320668cfc28ab659fa7f7b688c5639de898ccc08..057259d114f88f785c1a016d82f443b2ee2be644 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -71,12 +71,10 @@ impl KeyContextView { } else { None } + } else if this.action_matches(&e.action, binding.action()) { + Some(true) } else { - if this.action_matches(&e.action, binding.action()) { - Some(true) - } else { - Some(false) - } + Some(false) }; let predicate = if let Some(predicate) = binding.predicate() { format!("{}", predicate) diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index 3244350a34e275a33f9b9a5c2d3841c34884d1df..dd3e80212fda08f43718a664d2cfd6d377182273 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -349,7 +349,6 @@ impl LanguageServerState { .detach(); } else { cx.propagate(); - return; } } }, @@ -523,7 +522,6 @@ impl LspTool { if ProjectSettings::get_global(cx).global_lsp_settings.button { if lsp_tool.lsp_menu.is_none() { lsp_tool.refresh_lsp_menu(true, window, cx); - return; } } else if lsp_tool.lsp_menu.take().is_some() { cx.notify(); diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 00e3cad4360f72b1e428144dcb99905e7be4fdb4..d6f9538ee4851bb29a38f4108ddbce18929b94e5 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -452,7 +452,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ && entry .file_name() .to_str() - .map_or(false, |name| name.starts_with("gopls_")) + .is_some_and(|name| name.starts_with("gopls_")) { last_binary_path = Some(entry.path()); } diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 6f57ace4889e4f190f4993e4d7cbf0d4f51f6188..2c490b45cfce50cd2dc1a43162da84e1d8586094 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -280,7 +280,7 @@ impl JsonLspAdapter { ) })?; writer.replace(config.clone()); - return Ok(config); + Ok(config) } } diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 89a091797e49103dc4538f8d8a82d560b96fc637..906e45bb3a5bc1b149efb433d907624eb9a796af 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -828,7 +828,7 @@ impl ToolchainLister for PythonToolchainProvider { .get_env_var("CONDA_PREFIX".to_string()) .map(|conda_prefix| { let is_match = |exe: &Option<PathBuf>| { - exe.as_ref().map_or(false, |e| e.starts_with(&conda_prefix)) + exe.as_ref().is_some_and(|e| e.starts_with(&conda_prefix)) }; match (is_match(&lhs.executable), is_match(&rhs.executable)) { (true, false) => Ordering::Less, diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index f9b23ed9f4e4ea0f157c65f20afd710c4a91199b..eb5e0cee7ceb423983b24f37eb15440370bd7670 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -403,7 +403,7 @@ impl LspAdapter for RustLspAdapter { } else if completion .detail .as_ref() - .map_or(false, |detail| detail.starts_with("macro_rules! ")) + .is_some_and(|detail| detail.starts_with("macro_rules! ")) { let text = completion.label.clone(); let len = text.len(); @@ -496,7 +496,7 @@ impl LspAdapter for RustLspAdapter { let enable_lsp_tasks = ProjectSettings::get_global(cx) .lsp .get(&SERVER_NAME) - .map_or(false, |s| s.enable_lsp_tasks); + .is_some_and(|s| s.enable_lsp_tasks); if enable_lsp_tasks { let experimental = json!({ "runnables": { diff --git a/crates/livekit_client/examples/test_app.rs b/crates/livekit_client/examples/test_app.rs index e1d01df534e142502abb5f17392e19299f8ae158..51f335c2dbefc93a565ce2d6f6c467ca11e5b1eb 100644 --- a/crates/livekit_client/examples/test_app.rs +++ b/crates/livekit_client/examples/test_app.rs @@ -159,14 +159,14 @@ impl LivekitWindow { if output .audio_output_stream .as_ref() - .map_or(false, |(track, _)| track.sid() == unpublish_sid) + .is_some_and(|(track, _)| track.sid() == unpublish_sid) { output.audio_output_stream.take(); } if output .screen_share_output_view .as_ref() - .map_or(false, |(track, _)| track.sid() == unpublish_sid) + .is_some_and(|(track, _)| track.sid() == unpublish_sid) { output.screen_share_output_view.take(); } diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 890d564b7a1f4a66f8a587abdaae265d6b10e03b..8c8d9e177fba440acda8555d33ba9679269a4f1e 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -76,22 +76,22 @@ impl<'a> MarkdownParser<'a> { if self.eof() || (steps + self.cursor) >= self.tokens.len() { return self.tokens.last(); } - return self.tokens.get(self.cursor + steps); + self.tokens.get(self.cursor + steps) } fn previous(&self) -> Option<&(Event<'_>, Range<usize>)> { if self.cursor == 0 || self.cursor > self.tokens.len() { return None; } - return self.tokens.get(self.cursor - 1); + self.tokens.get(self.cursor - 1) } fn current(&self) -> Option<&(Event<'_>, Range<usize>)> { - return self.peek(0); + self.peek(0) } fn current_event(&self) -> Option<&Event<'_>> { - return self.current().map(|(event, _)| event); + self.current().map(|(event, _)| event) } fn is_text_like(event: &Event) -> bool { diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 3acc4b560025cc3c8f5c98ddd2a53f646955a57b..b0b10e927cb3bbc4f0b8366cc77b091c9df773d2 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -111,11 +111,10 @@ impl RenderContext { /// buffer font size changes. The callees of this function should be reimplemented to use real /// relative sizing once that is implemented in GPUI pub fn scaled_rems(&self, rems: f32) -> Rems { - return self - .buffer_text_style + self.buffer_text_style .font_size .to_rems(self.window_rem_size) - .mul(rems); + .mul(rems) } /// This ensures that children inside of block quotes diff --git a/crates/migrator/src/migrations/m_2025_05_05/settings.rs b/crates/migrator/src/migrations/m_2025_05_05/settings.rs index 88c6c338d18bc9c648a6c09e8fe1755bc3f77cd9..77da1b9a077b4acc2e6df6d47713f8e15f0fd090 100644 --- a/crates/migrator/src/migrations/m_2025_05_05/settings.rs +++ b/crates/migrator/src/migrations/m_2025_05_05/settings.rs @@ -24,7 +24,7 @@ fn rename_assistant( .nodes_for_capture_index(key_capture_ix) .next()? .byte_range(); - return Some((key_range, "agent".to_string())); + Some((key_range, "agent".to_string())) } fn rename_edit_prediction_assistant( @@ -37,5 +37,5 @@ fn rename_edit_prediction_assistant( .nodes_for_capture_index(key_capture_ix) .next()? .byte_range(); - return Some((key_range, "enabled_in_text_threads".to_string())); + Some((key_range, "enabled_in_text_threads".to_string())) } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index ab5f148d6cad0cc2cfa3a42f8888ad2cd350315f..7f65ccf5ea4ed7a96ce5421cba96f782dbaf2093 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1146,13 +1146,13 @@ impl MultiBuffer { pub fn last_transaction_id(&self, cx: &App) -> Option<TransactionId> { if let Some(buffer) = self.as_singleton() { - return buffer + buffer .read(cx) .peek_undo_stack() - .map(|history_entry| history_entry.transaction_id()); + .map(|history_entry| history_entry.transaction_id()) } else { let last_transaction = self.history.undo_stack.last()?; - return Some(last_transaction.id); + Some(last_transaction.id) } } @@ -1725,7 +1725,7 @@ impl MultiBuffer { merged_ranges.push(range.clone()); counts.push(1); } - return (merged_ranges, counts); + (merged_ranges, counts) } fn update_path_excerpts( @@ -2482,7 +2482,7 @@ impl MultiBuffer { let base_text_changed = snapshot .diffs .get(&buffer_id) - .map_or(true, |old_diff| !new_diff.base_texts_eq(old_diff)); + .is_none_or(|old_diff| !new_diff.base_texts_eq(old_diff)); snapshot.diffs.insert(buffer_id, new_diff); @@ -2776,7 +2776,7 @@ impl MultiBuffer { if diff_hunk.excerpt_id.cmp(&end_excerpt_id, &snapshot).is_gt() { continue; } - if last_hunk_row.map_or(false, |row| row >= diff_hunk.row_range.start) { + if last_hunk_row.is_some_and(|row| row >= diff_hunk.row_range.start) { continue; } let start = Anchor::in_buffer( @@ -3040,7 +3040,7 @@ impl MultiBuffer { is_dirty |= buffer.is_dirty(); has_deleted_file |= buffer .file() - .map_or(false, |file| file.disk_state() == DiskState::Deleted); + .is_some_and(|file| file.disk_state() == DiskState::Deleted); has_conflict |= buffer.has_conflict(); } if edited { @@ -3198,9 +3198,10 @@ impl MultiBuffer { // If this is the last edit that intersects the current diff transform, // then recreate the content up to the end of this transform, to prepare // for reusing additional slices of the old transforms. - if excerpt_edits.peek().map_or(true, |next_edit| { - next_edit.old.start >= old_diff_transforms.end().0 - }) { + if excerpt_edits + .peek() + .is_none_or(|next_edit| next_edit.old.start >= old_diff_transforms.end().0) + { let keep_next_old_transform = (old_diff_transforms.start().0 >= edit.old.end) && match old_diff_transforms.item() { Some(DiffTransform::BufferContent { @@ -3595,7 +3596,7 @@ impl MultiBuffer { let mut edits: Vec<(Range<usize>, Arc<str>)> = Vec::new(); let mut last_end = None; for _ in 0..edit_count { - if last_end.map_or(false, |last_end| last_end >= snapshot.len()) { + if last_end.is_some_and(|last_end| last_end >= snapshot.len()) { break; } @@ -4649,7 +4650,7 @@ impl MultiBufferSnapshot { return true; } } - return true; + true } pub fn prev_non_blank_row(&self, mut row: MultiBufferRow) -> Option<MultiBufferRow> { @@ -4954,7 +4955,7 @@ impl MultiBufferSnapshot { while let Some(replacement) = self.replaced_excerpts.get(&excerpt_id) { excerpt_id = *replacement; } - return excerpt_id; + excerpt_id } pub fn summaries_for_anchors<'a, D, I>(&'a self, anchors: I) -> Vec<D> @@ -5072,9 +5073,9 @@ impl MultiBufferSnapshot { if point == region.range.end.key && region.has_trailing_newline { position.add_assign(&D::from_text_summary(&TextSummary::newline())); } - return Some(position); + Some(position) } else { - return Some(D::from_text_summary(&self.text_summary())); + Some(D::from_text_summary(&self.text_summary())) } }) } @@ -5114,7 +5115,7 @@ impl MultiBufferSnapshot { // Leave min and max anchors unchanged if invalid or // if the old excerpt still exists at this location let mut kept_position = next_excerpt - .map_or(false, |e| e.id == old_excerpt_id && e.contains(&anchor)) + .is_some_and(|e| e.id == old_excerpt_id && e.contains(&anchor)) || old_excerpt_id == ExcerptId::max() || old_excerpt_id == ExcerptId::min(); @@ -5482,7 +5483,7 @@ impl MultiBufferSnapshot { let range_filter = |open: Range<usize>, close: Range<usize>| -> bool { excerpt_buffer_range.contains(&open.start) && excerpt_buffer_range.contains(&close.end) - && range_filter.map_or(true, |filter| filter(buffer, open, close)) + && range_filter.is_none_or(|filter| filter(buffer, open, close)) }; let (open, close) = excerpt.buffer().innermost_enclosing_bracket_ranges( @@ -5642,10 +5643,10 @@ impl MultiBufferSnapshot { .buffer .line_indents_in_row_range(buffer_start_row..buffer_end_row); cursor.next(); - return Some(line_indents.map(move |(buffer_row, indent)| { + Some(line_indents.map(move |(buffer_row, indent)| { let row = region.range.start.row + (buffer_row - region.buffer_range.start.row); (MultiBufferRow(row), indent, ®ion.excerpt.buffer) - })); + })) }) .flatten() } @@ -5682,10 +5683,10 @@ impl MultiBufferSnapshot { .buffer .reversed_line_indents_in_row_range(buffer_start_row..buffer_end_row); cursor.prev(); - return Some(line_indents.map(move |(buffer_row, indent)| { + Some(line_indents.map(move |(buffer_row, indent)| { let row = region.range.start.row + (buffer_row - region.buffer_range.start.row); (MultiBufferRow(row), indent, ®ion.excerpt.buffer) - })); + })) }) .flatten() } @@ -6545,7 +6546,7 @@ where && self .excerpts .item() - .map_or(false, |excerpt| excerpt.id != hunk_info.excerpt_id) + .is_some_and(|excerpt| excerpt.id != hunk_info.excerpt_id) { self.excerpts.next(); } @@ -6592,7 +6593,7 @@ where let prev_transform = self.diff_transforms.item(); self.diff_transforms.next(); - prev_transform.map_or(true, |next_transform| { + prev_transform.is_none_or(|next_transform| { matches!(next_transform, DiffTransform::BufferContent { .. }) }) } @@ -6607,12 +6608,12 @@ where } let next_transform = self.diff_transforms.next_item(); - next_transform.map_or(true, |next_transform| match next_transform { + next_transform.is_none_or(|next_transform| match next_transform { DiffTransform::BufferContent { .. } => true, DiffTransform::DeletedHunk { hunk_info, .. } => self .excerpts .item() - .map_or(false, |excerpt| excerpt.id != hunk_info.excerpt_id), + .is_some_and(|excerpt| excerpt.id != hunk_info.excerpt_id), }) } @@ -6645,7 +6646,7 @@ where buffer_end.add_assign(&buffer_range_len); let start = self.diff_transforms.start().output_dimension.0; let end = self.diff_transforms.end().output_dimension.0; - return Some(MultiBufferRegion { + Some(MultiBufferRegion { buffer, excerpt, has_trailing_newline: *has_trailing_newline, @@ -6655,7 +6656,7 @@ where )), buffer_range: buffer_start..buffer_end, range: start..end, - }); + }) } DiffTransform::BufferContent { inserted_hunk_info, .. @@ -7493,61 +7494,59 @@ impl Iterator for MultiBufferRows<'_> { self.cursor.next(); if let Some(next_region) = self.cursor.region() { region = next_region; - } else { - if self.point == self.cursor.diff_transforms.end().output_dimension.0 { - let multibuffer_row = MultiBufferRow(self.point.row); - let last_excerpt = self - .cursor - .excerpts - .item() - .or(self.cursor.excerpts.prev_item())?; - let last_row = last_excerpt - .range - .context - .end - .to_point(&last_excerpt.buffer) - .row; + } else if self.point == self.cursor.diff_transforms.end().output_dimension.0 { + let multibuffer_row = MultiBufferRow(self.point.row); + let last_excerpt = self + .cursor + .excerpts + .item() + .or(self.cursor.excerpts.prev_item())?; + let last_row = last_excerpt + .range + .context + .end + .to_point(&last_excerpt.buffer) + .row; - let first_row = last_excerpt - .range - .context - .start - .to_point(&last_excerpt.buffer) - .row; + let first_row = last_excerpt + .range + .context + .start + .to_point(&last_excerpt.buffer) + .row; - let expand_info = if self.is_singleton { - None - } else { - let needs_expand_up = first_row == last_row - && last_row > 0 - && !region.diff_hunk_status.is_some_and(|d| d.is_deleted()); - let needs_expand_down = last_row < last_excerpt.buffer.max_point().row; - - if needs_expand_up && needs_expand_down { - Some(ExpandExcerptDirection::UpAndDown) - } else if needs_expand_up { - Some(ExpandExcerptDirection::Up) - } else if needs_expand_down { - Some(ExpandExcerptDirection::Down) - } else { - None - } - .map(|direction| ExpandInfo { - direction, - excerpt_id: last_excerpt.id, - }) - }; - self.point += Point::new(1, 0); - return Some(RowInfo { - buffer_id: Some(last_excerpt.buffer_id), - buffer_row: Some(last_row), - multibuffer_row: Some(multibuffer_row), - diff_status: None, - expand_info, - }); + let expand_info = if self.is_singleton { + None } else { - return None; - } + let needs_expand_up = first_row == last_row + && last_row > 0 + && !region.diff_hunk_status.is_some_and(|d| d.is_deleted()); + let needs_expand_down = last_row < last_excerpt.buffer.max_point().row; + + if needs_expand_up && needs_expand_down { + Some(ExpandExcerptDirection::UpAndDown) + } else if needs_expand_up { + Some(ExpandExcerptDirection::Up) + } else if needs_expand_down { + Some(ExpandExcerptDirection::Down) + } else { + None + } + .map(|direction| ExpandInfo { + direction, + excerpt_id: last_excerpt.id, + }) + }; + self.point += Point::new(1, 0); + return Some(RowInfo { + buffer_id: Some(last_excerpt.buffer_id), + buffer_row: Some(last_row), + multibuffer_row: Some(multibuffer_row), + diff_status: None, + expand_info, + }); + } else { + return None; }; } diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index f92c122e71a00f08bcba1a4e16c510b00898cb56..871c72ea0b1d96dacf416100c969289382bc0030 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -197,7 +197,7 @@ impl NodeRuntime { state.instance = Some(instance.boxed_clone()); state.last_options = Some(options); - return instance; + instance } pub async fn binary_path(&self) -> Result<PathBuf> { diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index 9f299eb6ea0a994097bac282b60f08decb7ed838..6a072b00e94c9b6a995d18c06a6dd19759f192b0 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -210,12 +210,12 @@ impl ThemePreviewTile { } fn render_borderless(seed: f32, theme: Arc<Theme>) -> impl IntoElement { - return Self::render_editor( + Self::render_editor( seed, theme, Self::SIDEBAR_WIDTH_DEFAULT, Self::SKELETON_HEIGHT_DEFAULT, - ); + ) } fn render_border(seed: f32, theme: Arc<Theme>) -> impl IntoElement { @@ -246,7 +246,7 @@ impl ThemePreviewTile { ) -> impl IntoElement { let sidebar_width = relative(0.20); - return div() + div() .size_full() .p(Self::ROOT_PADDING) .rounded(Self::ROOT_RADIUS) @@ -278,7 +278,7 @@ impl ThemePreviewTile { )), ), ) - .into_any_element(); + .into_any_element() } } diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 1fb9a1342cc81a0198c7e3c4258d45521dae32e4..acf6ec434a013c711e0c77be3658913f9c6db7cb 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -9,7 +9,7 @@ use strum::EnumIter; pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1"; fn is_none_or_empty<T: AsRef<[U]>, U>(opt: &Option<T>) -> bool { - opt.as_ref().map_or(true, |v| v.as_ref().is_empty()) + opt.as_ref().is_none_or(|v| v.as_ref().is_empty()) } #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] @@ -241,7 +241,7 @@ impl Model { /// /// If the model does not support the parameter, do not pass it up. pub fn supports_prompt_cache_key(&self) -> bool { - return true; + true } } diff --git a/crates/open_router/src/open_router.rs b/crates/open_router/src/open_router.rs index 7c304bad642e22849fb42f34b69c5b80f6f261ad..b7e6d69d8fbe7c342e833cd13ad069399fb44a26 100644 --- a/crates/open_router/src/open_router.rs +++ b/crates/open_router/src/open_router.rs @@ -8,7 +8,7 @@ use std::convert::TryFrom; pub const OPEN_ROUTER_API_URL: &str = "https://openrouter.ai/api/v1"; fn is_none_or_empty<T: AsRef<[U]>, U>(opt: &Option<T>) -> bool { - opt.as_ref().map_or(true, |v| v.as_ref().is_empty()) + opt.as_ref().is_none_or(|v| v.as_ref().is_empty()) } #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 9b7ec473fdce9e53d41421580b60084741112edc..78f512f7f351189396a2de97f33d24ef387808ac 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -503,16 +503,16 @@ impl SearchData { && multi_buffer_snapshot .chars_at(extended_context_left_border) .last() - .map_or(false, |c| !c.is_whitespace()); + .is_some_and(|c| !c.is_whitespace()); let truncated_right = entire_context_text .chars() .last() - .map_or(true, |c| !c.is_whitespace()) + .is_none_or(|c| !c.is_whitespace()) && extended_context_right_border > context_right_border && multi_buffer_snapshot .chars_at(extended_context_right_border) .next() - .map_or(false, |c| !c.is_whitespace()); + .is_some_and(|c| !c.is_whitespace()); search_match_indices.iter_mut().for_each(|range| { range.start = multi_buffer_snapshot.clip_offset( range.start.saturating_sub(left_whitespaces_offset), @@ -1259,7 +1259,7 @@ impl OutlinePanel { dirs_worktree_id == worktree_id && dirs .last() - .map_or(false, |dir| dir.path.as_ref() == parent_path) + .is_some_and(|dir| dir.path.as_ref() == parent_path) } _ => false, }) @@ -1453,9 +1453,7 @@ impl OutlinePanel { if self .unfolded_dirs .get(&directory_worktree) - .map_or(true, |unfolded_dirs| { - !unfolded_dirs.contains(&directory_entry.id) - }) + .is_none_or(|unfolded_dirs| !unfolded_dirs.contains(&directory_entry.id)) { return false; } @@ -2156,7 +2154,7 @@ impl OutlinePanel { ExcerptOutlines::Invalidated(outlines) => Some(outlines), ExcerptOutlines::NotFetched => None, }) - .map_or(false, |outlines| !outlines.is_empty()); + .is_some_and(|outlines| !outlines.is_empty()); let is_expanded = !self .collapsed_entries .contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)); @@ -2953,7 +2951,7 @@ impl OutlinePanel { .map(|(parent_dir_id, _)| { new_unfolded_dirs .get(&directory.worktree_id) - .map_or(true, |unfolded_dirs| { + .is_none_or(|unfolded_dirs| { unfolded_dirs .contains(parent_dir_id) }) @@ -3444,9 +3442,8 @@ impl OutlinePanel { } fn is_singleton_active(&self, cx: &App) -> bool { - self.active_editor().map_or(false, |active_editor| { - active_editor.read(cx).buffer().read(cx).is_singleton() - }) + self.active_editor() + .is_some_and(|active_editor| active_editor.read(cx).buffer().read(cx).is_singleton()) } fn invalidate_outlines(&mut self, ids: &[ExcerptId]) { @@ -3664,7 +3661,7 @@ impl OutlinePanel { let is_root = project .read(cx) .worktree_for_id(directory_entry.worktree_id, cx) - .map_or(false, |worktree| { + .is_some_and(|worktree| { worktree.read(cx).root_entry() == Some(&directory_entry.entry) }); let folded = auto_fold_dirs @@ -3672,7 +3669,7 @@ impl OutlinePanel { && outline_panel .unfolded_dirs .get(&directory_entry.worktree_id) - .map_or(true, |unfolded_dirs| { + .is_none_or(|unfolded_dirs| { !unfolded_dirs.contains(&directory_entry.entry.id) }); let fs_depth = outline_panel @@ -3752,7 +3749,7 @@ impl OutlinePanel { .iter() .rev() .nth(folded_dirs.entries.len() + 1) - .map_or(true, |parent| parent.expanded); + .is_none_or(|parent| parent.expanded); if start_of_collapsed_dir_sequence || parent_expanded || query.is_some() @@ -3812,7 +3809,7 @@ impl OutlinePanel { .iter() .all(|entry| entry.path != parent.path) }) - .map_or(true, |parent| parent.expanded); + .is_none_or(|parent| parent.expanded); if !is_singleton && (parent_expanded || query.is_some()) { outline_panel.push_entry( &mut generation_state, @@ -3837,7 +3834,7 @@ impl OutlinePanel { .iter() .all(|entry| entry.path != parent.path) }) - .map_or(true, |parent| parent.expanded); + .is_none_or(|parent| parent.expanded); if !is_singleton && (parent_expanded || query.is_some()) { outline_panel.push_entry( &mut generation_state, @@ -3958,7 +3955,7 @@ impl OutlinePanel { .iter() .all(|entry| entry.path != parent.path) }) - .map_or(true, |parent| parent.expanded); + .is_none_or(|parent| parent.expanded); if parent_expanded || query.is_some() { outline_panel.push_entry( &mut generation_state, @@ -4438,7 +4435,7 @@ impl OutlinePanel { } fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool { - self.active_item().map_or(true, |active_item| { + self.active_item().is_none_or(|active_item| { !self.pinned && active_item.item_id() != new_active_item.item_id() }) } diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 8e1485dc9ac2bc6088692ec7a257ecfb333e3dfa..32e39d466f1a236da72b746fb4bf2a24b7300385 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -119,7 +119,7 @@ impl Prettier { None } }).any(|workspace_definition| { - workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().map_or(false, |path_matcher| path_matcher.is_match(subproject_path)) + workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().is_some_and(|path_matcher| path_matcher.is_match(subproject_path)) }) { anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Path {path_to_check:?} is the workspace root for project in {closest_package_json_path:?}, but it has no prettier installed"); log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {closest_package_json_path:?}"); @@ -217,7 +217,7 @@ impl Prettier { workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]) .ok() - .map_or(false, |path_matcher| { + .is_some_and(|path_matcher| { path_matcher.is_match(subproject_path) }) }) diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 96e87b1fe0ac30112543fd79498128179a9bb55e..296749c14ec505150ea11de683599c849300ebfc 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -234,7 +234,7 @@ impl RemoteBufferStore { } } } - return Ok(None); + Ok(None) } pub fn incomplete_buffer_ids(&self) -> Vec<BufferId> { @@ -1313,10 +1313,7 @@ impl BufferStore { let new_path = file.path.clone(); buffer.file_updated(Arc::new(file), cx); - if old_file - .as_ref() - .map_or(true, |old| *old.path() != new_path) - { + if old_file.as_ref().is_none_or(|old| *old.path() != new_path) { Some(old_file) } else { None diff --git a/crates/project/src/color_extractor.rs b/crates/project/src/color_extractor.rs index 5473da88af5bee6e66b005956366a289478f7ee4..dbbd3d7b996767d70ce00e01b73074bbb9523b3d 100644 --- a/crates/project/src/color_extractor.rs +++ b/crates/project/src/color_extractor.rs @@ -102,7 +102,7 @@ fn parse(str: &str, mode: ParseMode) -> Option<Hsla> { }; } - return None; + None } fn parse_component(value: &str, max: f32) -> Option<f32> { diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index 9a36584e717d9d9d7fcb8b013d5a15a9826d35a8..3e28fac8af22930876d096d5b7773a0825becf4f 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -146,7 +146,7 @@ impl DapLocator for CargoLocator { let is_test = build_config .args .first() - .map_or(false, |arg| arg == "test" || arg == "t"); + .is_some_and(|arg| arg == "test" || arg == "t"); let executables = output .lines() diff --git a/crates/project/src/debugger/locators/python.rs b/crates/project/src/debugger/locators/python.rs index 3de1281aed36c6a96970d08e0e4f5cb0ef3bd67f..71efbb75b91c819fcfdb857769877452f9e5a730 100644 --- a/crates/project/src/debugger/locators/python.rs +++ b/crates/project/src/debugger/locators/python.rs @@ -28,9 +28,7 @@ impl DapLocator for PythonLocator { let valid_program = build_config.command.starts_with("$ZED_") || Path::new(&build_config.command) .file_name() - .map_or(false, |name| { - name.to_str().is_some_and(|path| path.starts_with("python")) - }); + .is_some_and(|name| name.to_str().is_some_and(|path| path.starts_with("python"))); if !valid_program || build_config.args.iter().any(|arg| arg == "-c") { // We cannot debug selections. return None; diff --git a/crates/project/src/debugger/memory.rs b/crates/project/src/debugger/memory.rs index 092435fda7bbaf0f33a67a78801b77687e099b0e..a8729a8ff45cee346409d1b1ee09791a20243544 100644 --- a/crates/project/src/debugger/memory.rs +++ b/crates/project/src/debugger/memory.rs @@ -329,7 +329,7 @@ impl Iterator for MemoryIterator { } if !self.fetch_next_page() { self.start += 1; - return Some(MemoryCell(None)); + Some(MemoryCell(None)) } else { self.next() } diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index b5ae7148410884c75fb03bbf9ba68bef90eadad7..ee5baf1d3b0e9c58cf27bd2b7f5c90fd1997d4d5 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -431,7 +431,7 @@ impl RunningMode { let should_send_exception_breakpoints = capabilities .exception_breakpoint_filters .as_ref() - .map_or(false, |filters| !filters.is_empty()) + .is_some_and(|filters| !filters.is_empty()) || !configuration_done_supported; let supports_exception_filters = capabilities .supports_exception_filter_options @@ -710,9 +710,7 @@ where T: LocalDapCommand + PartialEq + Eq + Hash, { fn dyn_eq(&self, rhs: &dyn CacheableCommand) -> bool { - (rhs as &dyn Any) - .downcast_ref::<Self>() - .map_or(false, |rhs| self == rhs) + (rhs as &dyn Any).downcast_ref::<Self>() == Some(self) } fn dyn_hash(&self, mut hasher: &mut dyn Hasher) { @@ -1085,7 +1083,7 @@ impl Session { }) .detach(); - return tx; + tx } pub fn is_started(&self) -> bool { diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index ebc29a0a4b7f1329206e092d2fe67e2eb91d27bd..edc6b00a7be995c08cfea7b4d4cf0e7f46ef74b1 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -781,9 +781,7 @@ impl GitStore { let is_unmerged = self .repository_and_path_for_buffer_id(buffer_id, cx) - .map_or(false, |(repo, path)| { - repo.read(cx).snapshot.has_conflict(&path) - }); + .is_some_and(|(repo, path)| repo.read(cx).snapshot.has_conflict(&path)); let git_store = cx.weak_entity(); let buffer_git_state = self .diffs @@ -2501,14 +2499,14 @@ impl BufferGitState { pub fn wait_for_recalculation(&mut self) -> Option<impl Future<Output = ()> + use<>> { if *self.recalculating_tx.borrow() { let mut rx = self.recalculating_tx.subscribe(); - return Some(async move { + Some(async move { loop { let is_recalculating = rx.recv().await; if is_recalculating != Some(true) { break; } } - }); + }) } else { None } @@ -2879,7 +2877,7 @@ impl RepositorySnapshot { self.merge.conflicted_paths.contains(repo_path); let has_conflict_currently = self .status_for_path(repo_path) - .map_or(false, |entry| entry.status.is_conflicted()); + .is_some_and(|entry| entry.status.is_conflicted()); had_conflict_on_last_merge_head_change || has_conflict_currently } @@ -3531,7 +3529,7 @@ impl Repository { && buffer .read(cx) .file() - .map_or(false, |file| file.disk_state().exists()) + .is_some_and(|file| file.disk_state().exists()) { save_futures.push(buffer_store.save_buffer(buffer, cx)); } @@ -3597,7 +3595,7 @@ impl Repository { && buffer .read(cx) .file() - .map_or(false, |file| file.disk_state().exists()) + .is_some_and(|file| file.disk_state().exists()) { save_futures.push(buffer_store.save_buffer(buffer, cx)); } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 64414c654534d41fd9e420eb220319588fbcdeb4..217e00ee964d37b6d6f9f84f36664cfcc38b3b28 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -3447,9 +3447,7 @@ impl LspCommand for GetCodeLens { .server_capabilities .code_lens_provider .as_ref() - .map_or(false, |code_lens_options| { - code_lens_options.resolve_provider.unwrap_or(false) - }) + .is_some_and(|code_lens_options| code_lens_options.resolve_provider.unwrap_or(false)) } fn to_lsp( diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index e93e859dcf917a5b5fa75c3cec804118e0e71fac..e6ea01ff9ae90a4390fa2af4099f469202059611 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1038,7 +1038,7 @@ impl LocalLspStore { if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&state.id) { - return Some(server); + Some(server) } else { None } @@ -1879,7 +1879,7 @@ impl LocalLspStore { ) -> Result<Vec<(Range<Anchor>, Arc<str>)>> { let capabilities = &language_server.capabilities(); let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref(); - if range_formatting_provider.map_or(false, |provider| provider == &OneOf::Left(false)) { + if range_formatting_provider == Some(&OneOf::Left(false)) { anyhow::bail!( "{} language server does not support range formatting", language_server.name() @@ -2642,7 +2642,7 @@ impl LocalLspStore { this.request_lsp(buffer.clone(), server, request, cx) })? .await?; - return Ok(actions); + Ok(actions) } pub async fn execute_code_actions_on_server( @@ -2718,7 +2718,7 @@ impl LocalLspStore { } } } - return Ok(()); + Ok(()) } pub async fn deserialize_text_edits( @@ -2957,11 +2957,11 @@ impl LocalLspStore { .update(cx, |this, cx| { let path = buffer_to_edit.read(cx).project_path(cx); let active_entry = this.active_entry; - let is_active_entry = path.clone().map_or(false, |project_path| { + let is_active_entry = path.clone().is_some_and(|project_path| { this.worktree_store .read(cx) .entry_for_path(&project_path, cx) - .map_or(false, |entry| Some(entry.id) == active_entry) + .is_some_and(|entry| Some(entry.id) == active_entry) }); let local = this.as_local_mut().unwrap(); @@ -4038,7 +4038,7 @@ impl LspStore { servers.push((json_adapter, json_server, json_delegate)); } - return Some(servers); + Some(servers) }) .ok() .flatten(); @@ -4050,7 +4050,7 @@ impl LspStore { let Ok(Some((fs, _))) = this.read_with(cx, |this, _| { let local = this.as_local()?; let toolchain_store = local.toolchain_store().clone(); - return Some((local.fs.clone(), toolchain_store)); + Some((local.fs.clone(), toolchain_store)) }) else { return; }; @@ -4312,9 +4312,10 @@ impl LspStore { local_store.unregister_buffer_from_language_servers(buffer_entity, &file_url, cx); } buffer_entity.update(cx, |buffer, cx| { - if buffer.language().map_or(true, |old_language| { - !Arc::ptr_eq(old_language, &new_language) - }) { + if buffer + .language() + .is_none_or(|old_language| !Arc::ptr_eq(old_language, &new_language)) + { buffer.set_language(Some(new_language.clone()), cx); } }); @@ -4514,7 +4515,7 @@ impl LspStore { if !request.check_capabilities(language_server.adapter_server_capabilities()) { return Task::ready(Ok(Default::default())); } - return cx.spawn(async move |this, cx| { + cx.spawn(async move |this, cx| { let lsp_request = language_server.request::<R::LspRequest>(lsp_params); let id = lsp_request.id(); @@ -4573,7 +4574,7 @@ impl LspStore { ) .await; response - }); + }) } fn on_settings_changed(&mut self, cx: &mut Context<Self>) { @@ -7297,7 +7298,7 @@ impl LspStore { include_ignored || worktree .entry_for_path(path.as_ref()) - .map_or(false, |entry| !entry.is_ignored) + .is_some_and(|entry| !entry.is_ignored) }) .flat_map(move |(path, summaries)| { summaries.iter().map(move |(server_id, summary)| { @@ -9341,9 +9342,7 @@ impl LspStore { let is_disk_based_diagnostics_progress = disk_based_diagnostics_progress_token .as_ref() - .map_or(false, |disk_based_token| { - token.starts_with(disk_based_token) - }); + .is_some_and(|disk_based_token| token.starts_with(disk_based_token)); match progress { lsp::WorkDoneProgress::Begin(report) => { @@ -10676,7 +10675,7 @@ impl LspStore { let is_supporting = diagnostic .related_information .as_ref() - .map_or(false, |infos| { + .is_some_and(|infos| { infos.iter().any(|info| { primary_diagnostic_group_ids.contains_key(&( source, @@ -10689,11 +10688,11 @@ impl LspStore { let is_unnecessary = diagnostic .tags .as_ref() - .map_or(false, |tags| tags.contains(&DiagnosticTag::UNNECESSARY)); + .is_some_and(|tags| tags.contains(&DiagnosticTag::UNNECESSARY)); let underline = self .language_server_adapter_for_id(server_id) - .map_or(true, |adapter| adapter.underline_diagnostic(diagnostic)); + .is_none_or(|adapter| adapter.underline_diagnostic(diagnostic)); if is_supporting { supporting_diagnostics.insert( @@ -10703,7 +10702,7 @@ impl LspStore { } else { let group_id = post_inc(&mut self.as_local_mut().unwrap().next_diagnostic_group_id); let is_disk_based = - source.map_or(false, |source| disk_based_sources.contains(source)); + source.is_some_and(|source| disk_based_sources.contains(source)); sources_by_group_id.insert(group_id, source); primary_diagnostic_group_ids @@ -12409,7 +12408,7 @@ impl TryFrom<&FileOperationFilter> for RenameActionPredicate { ops.pattern .options .as_ref() - .map_or(false, |ops| ops.ignore_case.unwrap_or(false)), + .is_some_and(|ops| ops.ignore_case.unwrap_or(false)), ) .build()? .compile_matcher(), @@ -12424,7 +12423,7 @@ struct RenameActionPredicate { impl RenameActionPredicate { // Returns true if language server should be notified fn eval(&self, path: &str, is_dir: bool) -> bool { - self.kind.as_ref().map_or(true, |kind| { + self.kind.as_ref().is_none_or(|kind| { let expected_kind = if is_dir { FileOperationPatternKind::Folder } else { diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index f68905d14c21b831ba579a0f7d8a80646156c46e..750815c477c34416b05a339bc6e9efc591336cd2 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -218,10 +218,8 @@ impl ManifestQueryDelegate { impl ManifestDelegate for ManifestQueryDelegate { fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool { - self.worktree.entry_for_path(path).map_or(false, |entry| { - is_dir.map_or(true, |is_required_to_be_dir| { - is_required_to_be_dir == entry.is_dir() - }) + self.worktree.entry_for_path(path).is_some_and(|entry| { + is_dir.is_none_or(|is_required_to_be_dir| is_required_to_be_dir == entry.is_dir()) }) } diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index 7da43feeeff91a84a23e1c44daba5328ae4ca435..f5fd481324316d54e07de269ce676dc412f6c0fa 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -314,7 +314,7 @@ impl LanguageServerTree { pub(crate) fn remove_nodes(&mut self, ids: &BTreeSet<LanguageServerId>) { for (_, servers) in &mut self.instances { for (_, nodes) in &mut servers.roots { - nodes.retain(|_, (node, _)| node.id.get().map_or(true, |id| !ids.contains(id))); + nodes.retain(|_, (node, _)| node.id.get().is_none_or(|id| !ids.contains(id))); } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f825cd8c47ddf71e3e894ab84cf9f1d1891bc714..f9ad7b96d361432c64bf218f0b028caa6613ba38 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1897,7 +1897,7 @@ impl Project { return true; } - return false; + false } pub fn ssh_connection_string(&self, cx: &App) -> Option<SharedString> { @@ -1905,7 +1905,7 @@ impl Project { return Some(ssh_state.read(cx).connection_string().into()); } - return None; + None } pub fn ssh_connection_state(&self, cx: &App) -> Option<remote::ConnectionState> { @@ -4134,7 +4134,7 @@ impl Project { } }) } else { - return Task::ready(None); + Task::ready(None) } } @@ -5187,7 +5187,7 @@ impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet { } fn prefix(&self) -> Arc<str> { - if self.snapshot.root_entry().map_or(false, |e| e.is_file()) { + if self.snapshot.root_entry().is_some_and(|e| e.is_file()) { self.snapshot.root_name().into() } else if self.include_root_name { format!("{}{}", self.snapshot.root_name(), std::path::MAIN_SEPARATOR).into() @@ -5397,7 +5397,7 @@ impl Completion { self.source // `lsp::CompletionListItemDefaults` has `insert_text_format` field .lsp_completion(true) - .map_or(false, |lsp_completion| { + .is_some_and(|lsp_completion| { lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET) }) } @@ -5453,9 +5453,10 @@ fn provide_inline_values( .collect::<String>(); let point = snapshot.offset_to_point(capture_range.end); - while scopes.last().map_or(false, |scope: &Range<_>| { - !scope.contains(&capture_range.start) - }) { + while scopes + .last() + .is_some_and(|scope: &Range<_>| !scope.contains(&capture_range.start)) + { scopes.pop(); } @@ -5465,7 +5466,7 @@ fn provide_inline_values( let scope = if scopes .last() - .map_or(true, |scope| !scope.contains(&active_debug_line_offset)) + .is_none_or(|scope| !scope.contains(&active_debug_line_offset)) { VariableScope::Global } else { diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 050ca60e7a43d68ed926c1c94f7e7f270476186c..a6fea4059c2f20b3ef1a06fe47b17cb7275886e0 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -188,9 +188,9 @@ pub struct DiagnosticsSettings { impl DiagnosticsSettings { pub fn fetch_cargo_diagnostics(&self) -> bool { - self.cargo.as_ref().map_or(false, |cargo_diagnostics| { - cargo_diagnostics.fetch_cargo_diagnostics - }) + self.cargo + .as_ref() + .is_some_and(|cargo_diagnostics| cargo_diagnostics.fetch_cargo_diagnostics) } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 5b3827b42baa366272fe7b9a5d58f18459ca29c0..5137d64fabb9f1c48766eb85204e4e3018f89694 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -2947,9 +2947,10 @@ fn chunks_with_diagnostics<T: ToOffset + ToPoint>( ) -> Vec<(String, Option<DiagnosticSeverity>)> { let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new(); for chunk in buffer.snapshot().chunks(range, true) { - if chunks.last().map_or(false, |prev_chunk| { - prev_chunk.1 == chunk.diagnostic_severity - }) { + if chunks + .last() + .is_some_and(|prev_chunk| prev_chunk.1 == chunk.diagnostic_severity) + { chunks.last_mut().unwrap().0.push_str(chunk.text); } else { chunks.push((chunk.text.to_string(), chunk.diagnostic_severity)); diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 5f98a10c75d5e66dff428e25a3c368cc8b2ac519..212d2dd2d9a32e525edfe4827c4c0e7810b7c1dc 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -99,7 +99,7 @@ impl Project { } } - return None; + None } pub fn create_terminal( @@ -518,7 +518,7 @@ impl Project { smol::block_on(fs.metadata(&bin_path)) .ok() .flatten() - .map_or(false, |meta| meta.is_dir) + .is_some_and(|meta| meta.is_dir) }) } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index dd6b081e98f0fc3b5b88e905c21e85512d0a7eea..9a87874ed87e57716fcc4b8221ae4a86589feb6a 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -563,7 +563,7 @@ impl ProjectPanel { if project_panel .edit_state .as_ref() - .map_or(false, |state| state.processing_filename.is_none()) + .is_some_and(|state| state.processing_filename.is_none()) { project_panel.edit_state = None; project_panel.update_visible_entries(None, cx); @@ -3091,7 +3091,7 @@ impl ProjectPanel { entry.id == new_entry_id || { self.ancestors .get(&entry.id) - .map_or(false, |entries| entries.ancestors.contains(&new_entry_id)) + .is_some_and(|entries| entries.ancestors.contains(&new_entry_id)) } } else { false @@ -3974,7 +3974,7 @@ impl ProjectPanel { let is_marked = self.marked_entries.contains(&selection); let is_active = self .selection - .map_or(false, |selection| selection.entry_id == entry_id); + .is_some_and(|selection| selection.entry_id == entry_id); let file_name = details.filename.clone(); @@ -4181,7 +4181,7 @@ impl ProjectPanel { || this .expanded_dir_ids .get(&details.worktree_id) - .map_or(false, |ids| ids.binary_search(&entry_id).is_ok()) + .is_some_and(|ids| ids.binary_search(&entry_id).is_ok()) { return; } @@ -4401,19 +4401,17 @@ impl ProjectPanel { } else { h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted)) } + } else if let Some((icon_name, color)) = + entry_diagnostic_aware_icon_name_and_color(diagnostic_severity) + { + h_flex() + .size(IconSize::default().rems()) + .child(Icon::new(icon_name).color(color).size(IconSize::Small)) } else { - if let Some((icon_name, color)) = - entry_diagnostic_aware_icon_name_and_color(diagnostic_severity) - { - h_flex() - .size(IconSize::default().rems()) - .child(Icon::new(icon_name).color(color).size(IconSize::Small)) - } else { - h_flex() - .size(IconSize::default().rems()) - .invisible() - .flex_none() - } + h_flex() + .size(IconSize::default().rems()) + .invisible() + .flex_none() }) .child( if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { @@ -4465,7 +4463,7 @@ impl ProjectPanel { ); } else { let is_current_target = this.folded_directory_drag_target - .map_or(false, |target| + .is_some_and(|target| target.entry_id == entry_id && target.index == delimiter_target_index && target.is_delimiter_target @@ -4509,7 +4507,7 @@ impl ProjectPanel { } else { let is_current_target = this.folded_directory_drag_target .as_ref() - .map_or(false, |target| + .is_some_and(|target| target.entry_id == entry_id && target.index == index && !target.is_delimiter_target @@ -4528,7 +4526,7 @@ impl ProjectPanel { this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx); } })) - .when(folded_directory_drag_target.map_or(false, |target| + .when(folded_directory_drag_target.is_some_and(|target| target.entry_id == entry_id && target.index == index ), |this| { @@ -4694,7 +4692,7 @@ impl ProjectPanel { let is_cut = self .clipboard .as_ref() - .map_or(false, |e| e.is_cut() && e.items().contains(&selection)); + .is_some_and(|e| e.is_cut() && e.items().contains(&selection)); EntryDetails { filename, @@ -4892,7 +4890,7 @@ impl ProjectPanel { if skip_ignored && worktree .entry_for_id(entry_id) - .map_or(true, |entry| entry.is_ignored && !entry.is_always_included) + .is_none_or(|entry| entry.is_ignored && !entry.is_always_included) { anyhow::bail!("can't reveal an ignored entry in the project panel"); } @@ -5687,7 +5685,7 @@ impl Panel for ProjectPanel { project.visible_worktrees(cx).any(|tree| { tree.read(cx) .root_entry() - .map_or(false, |entry| entry.is_dir()) + .is_some_and(|entry| entry.is_dir()) }) } diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 9fffbde5f770d83fcbd569a8b27fa38e9f3c45ad..9d0f54bc01c3d4f3e4d19416a20c6903a9f8879b 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -196,7 +196,7 @@ impl PickerDelegate for ProjectSymbolsDelegate { .partition(|candidate| { project .entry_for_path(&symbols[candidate.id].path, cx) - .map_or(false, |e| !e.is_ignored) + .is_some_and(|e| !e.is_ignored) }); delegate.visible_match_candidates = visible_match_candidates; diff --git a/crates/prompt_store/src/prompt_store.rs b/crates/prompt_store/src/prompt_store.rs index 06a65b97cd66c122f7491d96a89862a722d21ae2..fb087ce34d6d67fe4ea11a33f554307ed558c18a 100644 --- a/crates/prompt_store/src/prompt_store.rs +++ b/crates/prompt_store/src/prompt_store.rs @@ -247,9 +247,7 @@ impl PromptStore { if metadata_db .get(&txn, &prompt_id_v2)? - .map_or(true, |metadata_v2| { - metadata_v1.saved_at > metadata_v2.saved_at - }) + .is_none_or(|metadata_v2| metadata_v1.saved_at > metadata_v2.saved_at) { metadata_db.put( &mut txn, diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs index 526d2c6a3428982429e9090ac70f3cb7f0021d74..cd34bafb20a73b0e23da326c5e117e670ed0bd97 100644 --- a/crates/prompt_store/src/prompts.rs +++ b/crates/prompt_store/src/prompts.rs @@ -286,7 +286,7 @@ impl PromptBuilder { break; } for event in changed_paths { - if event.path.starts_with(&templates_dir) && event.path.extension().map_or(false, |ext| ext == "hbs") { + if event.path.starts_with(&templates_dir) && event.path.extension().is_some_and(|ext| ext == "hbs") { log::info!("Reloading prompt template override: {}", event.path.display()); if let Some(content) = params.fs.load(&event.path).await.log_err() { let file_name = event.path.file_stem().unwrap().to_string_lossy(); diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index a6cd26355c7741679b546a74afa67320457d5273..9b79d3ce9c7fb36532b4cc9d2619c332e6272989 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -37,7 +37,7 @@ impl ModalView for DisconnectedOverlay { _window: &mut Window, _: &mut Context<Self>, ) -> workspace::DismissDecision { - return workspace::DismissDecision::Dismiss(self.finished); + workspace::DismissDecision::Dismiss(self.finished) } fn fade_out_background(&self) -> bool { true diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 0fd6d5af8c7396fa7bcc8d6fc5e9f7ba9f5f8b1a..0f43d83d860990008a5c1e685f2f9a4fbbabc387 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1410,7 +1410,7 @@ impl RemoteServerProjects { if ssh_settings .ssh_connections .as_ref() - .map_or(false, |connections| { + .is_some_and(|connections| { state .servers .iter() diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index 7b58792178f5ecc705857bedec5e6707a97db52b..670fcb4800281f0fe825228703bab9be4418d167 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -436,7 +436,7 @@ impl ModalView for SshConnectionModal { _window: &mut Window, _: &mut Context<Self>, ) -> workspace::DismissDecision { - return workspace::DismissDecision::Dismiss(self.finished); + workspace::DismissDecision::Dismiss(self.finished) } fn fade_out_background(&self) -> bool { diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 2180fbb5ee68fa8f9960345d874c3f244019bd21..abde2d7568f339134103253094e6e410c9f1632b 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -1119,7 +1119,7 @@ impl SshRemoteClient { } fn state_is(&self, check: impl FnOnce(&State) -> bool) -> bool { - self.state.lock().as_ref().map_or(false, check) + self.state.lock().as_ref().is_some_and(check) } fn try_set_state(&self, cx: &mut Context<Self>, map: impl FnOnce(&State) -> Option<State>) { @@ -1870,7 +1870,7 @@ impl SshRemoteConnection { .await?; self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) .await?; - return Ok(dst_path); + Ok(dst_path) } async fn download_binary_on_server( diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs index cd73783b4c6b7008c5b7bc35ddb7d639892cefde..b8fd2e57f2feb6b9745055fb19cad4d7786460e3 100644 --- a/crates/repl/src/components/kernel_options.rs +++ b/crates/repl/src/components/kernel_options.rs @@ -126,7 +126,7 @@ impl PickerDelegate for KernelPickerDelegate { .collect() }; - return Task::ready(()); + Task::ready(()) } fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) { diff --git a/crates/repl/src/repl_editor.rs b/crates/repl/src/repl_editor.rs index f5dd6595979f4c2fbe805d3a8d00d2dc876c31ea..e97223ceb9e4f440f2a57e190d7273834812c2f8 100644 --- a/crates/repl/src/repl_editor.rs +++ b/crates/repl/src/repl_editor.rs @@ -434,7 +434,7 @@ fn runnable_ranges( if start_language .zip(end_language) - .map_or(false, |(start, end)| start == end) + .is_some_and(|(start, end)| start == end) { (vec![snippet_range], None) } else { diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 78ce6f78a22b786bec4f8880282e25fbb2c9fc93..0d3f5abbdefd3850d02e66a1efbef5b58a5f2835 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -35,7 +35,7 @@ impl Rope { && (self .chunks .last() - .map_or(false, |c| c.text.len() < chunk::MIN_BASE) + .is_some_and(|c| c.text.len() < chunk::MIN_BASE) || chunk.text.len() < chunk::MIN_BASE) { self.push_chunk(chunk.as_slice()); @@ -816,7 +816,7 @@ impl<'a> Chunks<'a> { } } - return true; + true } } diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index ec83993e5f91f970b3336a5c2ef008a4c436c3b0..355deb5d2059c0535ad834ce5e4cb7cabc38fec2 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -703,9 +703,7 @@ impl RulesLibrary { .delegate .matches .get(picker.delegate.selected_index()) - .map_or(true, |old_selected_prompt| { - old_selected_prompt.id != prompt_id - }) + .is_none_or(|old_selected_prompt| old_selected_prompt.id != prompt_id) && let Some(ix) = picker .delegate .matches diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index e95617512deffbe63d5094186e9b503900d516dc..ae3f42853ac11af0c1e4510c7ac51bd7379cb657 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -653,7 +653,7 @@ impl KeymapFile { let is_only_binding = keymap.0[index] .bindings .as_ref() - .map_or(true, |bindings| bindings.len() == 1); + .is_none_or(|bindings| bindings.len() == 1); let key_path: &[&str] = if is_only_binding { &[] } else { @@ -703,7 +703,7 @@ impl KeymapFile { } else if keymap.0[index] .bindings .as_ref() - .map_or(true, |bindings| bindings.len() == 1) + .is_none_or(|bindings| bindings.len() == 1) { // if we are replacing the only binding in the section, // just update the section in place, updating the context @@ -1056,10 +1056,10 @@ mod tests { #[track_caller] fn parse_keystrokes(keystrokes: &str) -> Vec<Keystroke> { - return keystrokes + keystrokes .split(' ') .map(|s| Keystroke::parse(s).expect("Keystrokes valid")) - .collect(); + .collect() } #[test] diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index 8e7e11dc827c676a425860fa8b8b8633156b1d0d..c102b303c1c803b235c87a1953d3b83ea086166b 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -72,7 +72,7 @@ pub fn update_value_in_json_text<'a>( } } else if key_path .last() - .map_or(false, |key| preserved_keys.contains(key)) + .is_some_and(|key| preserved_keys.contains(key)) || old_value != new_value { let mut new_value = new_value.clone(); @@ -384,7 +384,7 @@ pub fn replace_top_level_array_value_in_json_text( remove_range.start = cursor.node().range().start_byte; } } - return Ok((remove_range, String::new())); + Ok((remove_range, String::new())) } else { let (mut replace_range, mut replace_value) = replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key); @@ -405,7 +405,7 @@ pub fn replace_top_level_array_value_in_json_text( } } - return Ok((replace_range, replace_value)); + Ok((replace_range, replace_value)) } } @@ -527,7 +527,7 @@ pub fn append_top_level_array_value_in_json_text( let descendant_index = cursor.descendant_index(); let res = cursor.goto_first_child() && cursor.node().kind() == kind; cursor.goto_descendant(descendant_index); - return res; + res } } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 23f495d850d9d06148fa449bd39995347cf895df..211db46c6c78d2fee8b87a0a91ec1c106cb28426 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1233,8 +1233,7 @@ impl SettingsStore { // If a local settings file changed, then avoid recomputing local // settings for any path outside of that directory. - if changed_local_path.map_or( - false, + if changed_local_path.is_some_and( |(changed_root_id, changed_local_path)| { *root_id != changed_root_id || !directory_path.starts_with(changed_local_path) diff --git a/crates/settings_profile_selector/src/settings_profile_selector.rs b/crates/settings_profile_selector/src/settings_profile_selector.rs index 8a34c120512aa82d71c023a0c16308e6a4e1c271..25be67bfd720dbcbd5e8a4e0b29431321806db0a 100644 --- a/crates/settings_profile_selector/src/settings_profile_selector.rs +++ b/crates/settings_profile_selector/src/settings_profile_selector.rs @@ -126,7 +126,7 @@ impl SettingsProfileSelectorDelegate { ) -> Option<String> { let mat = self.matches.get(self.selected_index)?; let profile_name = self.profile_names.get(mat.candidate_id)?; - return Self::update_active_profile_name_global(profile_name.clone(), cx); + Self::update_active_profile_name_global(profile_name.clone(), cx) } fn update_active_profile_name_global( diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index b8c52602a627b63bec4964ae0b0cf4fc1da1b86e..457d58e5a73943d884083433a0757ba2f1d13a1c 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -553,7 +553,7 @@ impl KeymapEditor { if exact_match { keystrokes_match_exactly(&keystroke_query, keystrokes) } else if keystroke_query.len() > keystrokes.len() { - return false; + false } else { for keystroke_offset in 0..keystrokes.len() { let mut found_count = 0; @@ -568,12 +568,9 @@ impl KeymapEditor { query.modifiers.is_subset_of(&keystroke.modifiers) && ((query.key.is_empty() || query.key == keystroke.key) - && query - .key_char - .as_ref() - .map_or(true, |q_kc| { - q_kc == &keystroke.key - })); + && query.key_char.as_ref().is_none_or( + |q_kc| q_kc == &keystroke.key, + )); if matches { found_count += 1; query_cursor += 1; @@ -585,7 +582,7 @@ impl KeymapEditor { return true; } } - return false; + false } }) }); @@ -2715,7 +2712,7 @@ impl ActionArgumentsEditor { }) .ok(); } - return result; + result }) .detach_and_log_err(cx); Self { @@ -2818,7 +2815,7 @@ impl Render for ActionArgumentsEditor { self.editor .update(cx, |editor, _| editor.set_text_style_refinement(text_style)); - return v_flex().w_full().child( + v_flex().w_full().child( h_flex() .min_h_8() .min_w_48() @@ -2831,7 +2828,7 @@ impl Render for ActionArgumentsEditor { .border_color(border_color) .track_focus(&self.focus_handle) .child(self.editor.clone()), - ); + ) } } @@ -2889,9 +2886,9 @@ impl CompletionProvider for KeyContextCompletionProvider { _menu_is_open: bool, _cx: &mut Context<Editor>, ) -> bool { - text.chars().last().map_or(false, |last_char| { - last_char.is_ascii_alphanumeric() || last_char == '_' - }) + text.chars() + .last() + .is_some_and(|last_char| last_char.is_ascii_alphanumeric() || last_char == '_') } } @@ -2910,7 +2907,7 @@ async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) Some(task) => task.await.context("Failed to load JSON language").log_err(), None => None, }; - return json_language.unwrap_or_else(|| { + json_language.unwrap_or_else(|| { Arc::new(Language::new( LanguageConfig { name: "JSON".into(), @@ -2918,7 +2915,7 @@ async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) }, Some(tree_sitter_json::LANGUAGE.into()), )) - }); + }) } async fn load_keybind_context_language( @@ -2942,7 +2939,7 @@ async fn load_keybind_context_language( .log_err(), None => None, }; - return language.unwrap_or_else(|| { + language.unwrap_or_else(|| { Arc::new(Language::new( LanguageConfig { name: "Zed Keybind Context".into(), @@ -2950,7 +2947,7 @@ async fn load_keybind_context_language( }, Some(tree_sitter_rust::LANGUAGE.into()), )) - }); + }) } async fn save_keybinding_update( @@ -3130,7 +3127,7 @@ fn collect_contexts_from_assets() -> Vec<SharedString> { let mut contexts = contexts.into_iter().collect::<Vec<_>>(); contexts.sort(); - return contexts; + contexts } impl SerializableItem for KeymapEditor { diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index de133d406b04d87508433dbd199ced01f38ef069..66593524a393def8f6050f4e9162c1520df75d15 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -116,19 +116,19 @@ impl KeystrokeInput { && self .keystrokes .last() - .map_or(false, |last| last.key.is_empty()) + .is_some_and(|last| last.key.is_empty()) { return &self.keystrokes[..self.keystrokes.len() - 1]; } - return &self.keystrokes; + &self.keystrokes } fn dummy(modifiers: Modifiers) -> Keystroke { - return Keystroke { + Keystroke { modifiers, key: "".to_string(), key_char: None, - }; + } } fn keystrokes_changed(&self, cx: &mut Context<Self>) { @@ -182,7 +182,7 @@ impl KeystrokeInput { fn end_close_keystrokes_capture(&mut self) -> Option<usize> { self.close_keystrokes.take(); self.clear_close_keystrokes_timer.take(); - return self.close_keystrokes_start.take(); + self.close_keystrokes_start.take() } fn handle_possible_close_keystroke( @@ -233,7 +233,7 @@ impl KeystrokeInput { return CloseKeystrokeResult::Partial; } self.end_close_keystrokes_capture(); - return CloseKeystrokeResult::None; + CloseKeystrokeResult::None } fn on_modifiers_changed( @@ -437,7 +437,7 @@ impl KeystrokeInput { // is a much more reliable check, as the intercept keystroke handlers are installed // on focus of the inner focus handle, thereby ensuring our recording state does // not get de-synced - return self.inner_focus_handle.is_focused(window); + self.inner_focus_handle.is_focused(window) } } @@ -934,7 +934,7 @@ mod tests { let change_tracker = KeystrokeUpdateTracker::new(self.input.clone(), &mut self.cx); let result = self.input.update_in(&mut self.cx, cb); KeystrokeUpdateTracker::finish(change_tracker, &self.cx); - return result; + result } } diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 66dd636d21eb2b3f1372fe869e8bb5d15ce31627..a91d497572522b130a463e0a534db45f5938c58c 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -731,7 +731,7 @@ impl<const COLS: usize> ColumnWidths<COLS> { } widths[col_idx] = widths[col_idx] + (diff - diff_remaining); - return diff_remaining; + diff_remaining } } diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs index 6a673fe08b43e7580cb333d063aac93cf0ea6857..4be4281d9ac0bb98388c93ba5781b197258ee859 100644 --- a/crates/snippet/src/snippet.rs +++ b/crates/snippet/src/snippet.rs @@ -33,7 +33,7 @@ impl Snippet { choices: None, }; - if !tabstops.last().map_or(false, |t| *t == end_tabstop) { + if !tabstops.last().is_some_and(|t| *t == end_tabstop) { tabstops.push(end_tabstop); } } diff --git a/crates/snippet_provider/src/lib.rs b/crates/snippet_provider/src/lib.rs index c8d2555df2798c3f3e33dbef9257e6ef3411033e..eac06924a7906aba08d90c0d1c3d1f1743531954 100644 --- a/crates/snippet_provider/src/lib.rs +++ b/crates/snippet_provider/src/lib.rs @@ -71,16 +71,16 @@ async fn process_updates( ) -> Result<()> { let fs = this.read_with(&cx, |this, _| this.fs.clone())?; for entry_path in entries { - if !entry_path + if entry_path .extension() - .map_or(false, |extension| extension == "json") + .is_none_or(|extension| extension != "json") { continue; } let entry_metadata = fs.metadata(&entry_path).await; // Entry could have been removed, in which case we should no longer show completions for it. let entry_exists = entry_metadata.is_ok(); - if entry_metadata.map_or(false, |entry| entry.map_or(false, |e| e.is_dir)) { + if entry_metadata.is_ok_and(|entry| entry.is_some_and(|e| e.is_dir)) { // Don't process dirs. continue; } diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index f551bb32e6fd92bfb388166602e27b14329faa29..710fdd4fbf12ccc2b60998207d964bd31550b345 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -94,9 +94,7 @@ impl<'a, S: Summary, D: Dimension<'a, S> + Ord> SeekTarget<'a, S, D> for D { } impl<'a, T: Summary> Dimension<'a, T> for () { - fn zero(_: &T::Context) -> Self { - () - } + fn zero(_: &T::Context) -> Self {} fn add_summary(&mut self, _: &'a T, _: &T::Context) {} } @@ -728,7 +726,7 @@ impl<T: KeyedItem> SumTree<T> { if old_item .as_ref() - .map_or(false, |old_item| old_item.key() < new_key) + .is_some_and(|old_item| old_item.key() < new_key) { new_tree.extend(buffered_items.drain(..), cx); let slice = cursor.slice(&new_key, Bias::Left); diff --git a/crates/supermaven/src/supermaven.rs b/crates/supermaven/src/supermaven.rs index a31b96d8825334a3aed5fceed0efb86db4fac9f5..743c0d4c7d82726751822fb12f3ef0df9dfd40a9 100644 --- a/crates/supermaven/src/supermaven.rs +++ b/crates/supermaven/src/supermaven.rs @@ -243,7 +243,7 @@ fn find_relevant_completion<'a>( None => continue 'completions, }; - if best_completion.map_or(false, |best| best.len() > trimmed_completion.len()) { + if best_completion.is_some_and(|best| best.len() > trimmed_completion.len()) { continue; } diff --git a/crates/supermaven_api/src/supermaven_api.rs b/crates/supermaven_api/src/supermaven_api.rs index 61d14d5dc7f0f4eff563881d3150accc783119bd..c4b1409d646b0d402684d3c883a0ea633d12bbb5 100644 --- a/crates/supermaven_api/src/supermaven_api.rs +++ b/crates/supermaven_api/src/supermaven_api.rs @@ -221,9 +221,7 @@ pub fn version_path(version: u64) -> PathBuf { } pub async fn has_version(version_path: &Path) -> bool { - fs::metadata(version_path) - .await - .map_or(false, |m| m.is_file()) + fs::metadata(version_path).await.is_ok_and(|m| m.is_file()) } pub async fn get_supermaven_agent_path(client: Arc<dyn HttpClient>) -> Result<PathBuf> { diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 90e6ea88788495b4ee933acf8c752cc4d44c51cb..dae366a9797ae81754949c6fc5512843a10cf803 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -283,7 +283,7 @@ pub fn task_contexts( .project() .read(cx) .worktree_for_id(*worktree_id, cx) - .map_or(false, |worktree| is_visible_directory(&worktree, cx)) + .is_some_and(|worktree| is_visible_directory(&worktree, cx)) }) .or_else(|| { workspace @@ -372,7 +372,7 @@ pub fn task_contexts( fn is_visible_directory(worktree: &Entity<Worktree>, cx: &App) -> bool { let worktree = worktree.read(cx); - worktree.is_visible() && worktree.root_entry().map_or(false, |entry| entry.is_dir()) + worktree.is_visible() && worktree.root_entry().is_some_and(|entry| entry.is_dir()) } fn worktree_context(worktree_abs_path: &Path) -> TaskContext { diff --git a/crates/telemetry/src/telemetry.rs b/crates/telemetry/src/telemetry.rs index f8f7d5851e66a94452cf3461487fbc1718735e44..ac43457c3368dba79131dc51966e9ec7ad1d870b 100644 --- a/crates/telemetry/src/telemetry.rs +++ b/crates/telemetry/src/telemetry.rs @@ -55,7 +55,6 @@ macro_rules! serialize_property { pub fn send_event(event: Event) { if let Some(queue) = TELEMETRY_QUEUE.get() { queue.unbounded_send(event).ok(); - return; } } diff --git a/crates/terminal/src/pty_info.rs b/crates/terminal/src/pty_info.rs index 802470493cc6c883008640af2b8f374254e3299d..a1a559051abdd20a1f0e8386fa0c71f683f1f40c 100644 --- a/crates/terminal/src/pty_info.rs +++ b/crates/terminal/src/pty_info.rs @@ -122,7 +122,7 @@ impl PtyProcessInfo { } pub(crate) fn kill_current_process(&mut self) -> bool { - self.refresh().map_or(false, |process| process.kill()) + self.refresh().is_some_and(|process| process.kill()) } fn load(&mut self) -> Option<ProcessInfo> { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 42b3694789cfa3fa463f2390cce274b4d3a84fb6..16c1efabbabeb3741282e029cad75bda9a0fb5aa 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1299,23 +1299,19 @@ impl Terminal { let selection = Selection::new(selection_type, point, side); self.events .push_back(InternalEvent::SetSelection(Some((selection, point)))); - return; } "escape" => { self.events.push_back(InternalEvent::SetSelection(None)); - return; } "y" => { self.copy(Some(false)); - return; } "i" => { self.scroll_to_bottom(); self.toggle_vi_mode(); - return; } _ => {} } @@ -1891,11 +1887,11 @@ impl Terminal { let e: Option<ExitStatus> = error_code.map(|code| { #[cfg(unix)] { - return std::os::unix::process::ExitStatusExt::from_raw(code); + std::os::unix::process::ExitStatusExt::from_raw(code) } #[cfg(windows)] { - return std::os::windows::process::ExitStatusExt::from_raw(code as u32); + std::os::windows::process::ExitStatusExt::from_raw(code as u32) } }); diff --git a/crates/terminal/src/terminal_hyperlinks.rs b/crates/terminal/src/terminal_hyperlinks.rs index e318ae21bdcb755646e069c93ec8786f8197ad6a..9f565bd306a49ad49e0df58315950838d6446eac 100644 --- a/crates/terminal/src/terminal_hyperlinks.rs +++ b/crates/terminal/src/terminal_hyperlinks.rs @@ -124,12 +124,12 @@ pub(super) fn find_from_grid_point<T: EventListener>( && file_path .chars() .nth(last_index - 1) - .map_or(false, |c| c.is_ascii_digit()); + .is_some_and(|c| c.is_ascii_digit()); let next_is_digit = last_index < file_path.len() - 1 && file_path .chars() .nth(last_index + 1) - .map_or(true, |c| c.is_ascii_digit()); + .is_none_or(|c| c.is_ascii_digit()); if prev_is_digit && !next_is_digit { let stripped_len = file_path.len() - last_index; word_match = Match::new( diff --git a/crates/terminal_view/src/color_contrast.rs b/crates/terminal_view/src/color_contrast.rs index fe4a881cea2f2dcc0ad05198bfd0bdd5f0aa9f2a..522dca3e91341adf9056b3d03e3b5536cfcdc695 100644 --- a/crates/terminal_view/src/color_contrast.rs +++ b/crates/terminal_view/src/color_contrast.rs @@ -235,12 +235,10 @@ fn adjust_lightness_for_contrast( } else { high = mid; } + } else if should_go_darker { + high = mid; } else { - if should_go_darker { - high = mid; - } else { - low = mid; - } + low = mid; } // If we're close enough to the target, stop diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 7575706db005987dc0647ddda9f0b01d3ab8a539..1c38dbc877c9da50f92d8edcf4616a6bc32a21ad 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1478,7 +1478,7 @@ pub fn is_blank(cell: &IndexedCell) -> bool { return false; } - return true; + true } fn to_highlighted_range_lines( diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index b161a8ea8918a913ba4a83dde970ee325dd25dd7..c50e2bd3a79ad1e5ea01484fca6ad417ab970b98 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -350,12 +350,10 @@ impl TerminalPanel { pane.set_zoomed(false, cx); }); cx.emit(PanelEvent::Close); - } else { - if let Some(focus_on_pane) = - focus_on_pane.as_ref().or_else(|| self.center.panes().pop()) - { - focus_on_pane.focus_handle(cx).focus(window); - } + } else if let Some(focus_on_pane) = + focus_on_pane.as_ref().or_else(|| self.center.panes().pop()) + { + focus_on_pane.focus_handle(cx).focus(window); } } pane::Event::ZoomIn => { @@ -896,9 +894,9 @@ impl TerminalPanel { } fn is_enabled(&self, cx: &App) -> bool { - self.workspace.upgrade().map_or(false, |workspace| { - is_enabled_in_workspace(workspace.read(cx), cx) - }) + self.workspace + .upgrade() + .is_some_and(|workspace| is_enabled_in_workspace(workspace.read(cx), cx)) } fn activate_pane_in_direction( @@ -1242,20 +1240,18 @@ impl Render for TerminalPanel { let panes = terminal_panel.center.panes(); if let Some(&pane) = panes.get(action.0) { window.focus(&pane.read(cx).focus_handle(cx)); - } else { - if let Some(new_pane) = - terminal_panel.new_pane_with_cloned_active_terminal(window, cx) - { - terminal_panel - .center - .split( - &terminal_panel.active_pane, - &new_pane, - SplitDirection::Right, - ) - .log_err(); - window.focus(&new_pane.focus_handle(cx)); - } + } else if let Some(new_pane) = + terminal_panel.new_pane_with_cloned_active_terminal(window, cx) + { + terminal_panel + .center + .split( + &terminal_panel.active_pane, + &new_pane, + SplitDirection::Right, + ) + .log_err(); + window.focus(&new_pane.focus_handle(cx)); } }), ) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 14b642bc123f3209778bc5cc806e669f1d1f1034..f434e4615929e8a76ed29e9a4acc182d77ecf488 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -385,9 +385,7 @@ impl TerminalView { .workspace .upgrade() .and_then(|workspace| workspace.read(cx).panel::<TerminalPanel>(cx)) - .map_or(false, |terminal_panel| { - terminal_panel.read(cx).assistant_enabled() - }); + .is_some_and(|terminal_panel| terminal_panel.read(cx).assistant_enabled()); let context_menu = ContextMenu::build(window, cx, |menu, _, _| { menu.context(self.focus_handle.clone()) .action("New Terminal", Box::new(NewTerminal)) diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index c4778216e05ee811e46fabea7eed22171c2498ff..becc5d9c0a113acf64c4b2d331432bea6d00a5c9 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -108,7 +108,7 @@ impl Anchor { fragment_cursor.seek(&Some(fragment_id), Bias::Left); fragment_cursor .item() - .map_or(false, |fragment| fragment.visible) + .is_some_and(|fragment| fragment.visible) } } } diff --git a/crates/text/src/patch.rs b/crates/text/src/patch.rs index 96fed17571514323ea4e22c807c6848c00ad329a..dcb35e9a921538134b94e2870011eb3b341f01de 100644 --- a/crates/text/src/patch.rs +++ b/crates/text/src/patch.rs @@ -57,7 +57,7 @@ where // Push the old edit if its new end is before the new edit's old start. if let Some(old_edit) = old_edit.as_ref() { let new_edit = new_edit.as_ref(); - if new_edit.map_or(true, |new_edit| old_edit.new.end < new_edit.old.start) { + if new_edit.is_none_or(|new_edit| old_edit.new.end < new_edit.old.start) { let catchup = old_edit.old.start - old_start; old_start += catchup; new_start += catchup; @@ -78,7 +78,7 @@ where // Push the new edit if its old end is before the old edit's new start. if let Some(new_edit) = new_edit.as_ref() { let old_edit = old_edit.as_ref(); - if old_edit.map_or(true, |old_edit| new_edit.old.end < old_edit.new.start) { + if old_edit.is_none_or(|old_edit| new_edit.old.end < old_edit.new.start) { let catchup = new_edit.new.start - new_start; old_start += catchup; new_start += catchup; diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 8e37567738f90ff702b2ac565b3c3985c5b3c72e..705d3f1788288eb67a0b3b756ba545dc99b031d3 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1149,7 +1149,7 @@ impl Buffer { // Insert the new text before any existing fragments within the range. if !new_text.is_empty() { let mut old_start = old_fragments.start().1; - if old_fragments.item().map_or(false, |f| f.visible) { + if old_fragments.item().is_some_and(|f| f.visible) { old_start += fragment_start.0 - old_fragments.start().0.full_offset().0; } let new_start = new_fragments.summary().text.visible; @@ -1834,7 +1834,7 @@ impl Buffer { let mut edits: Vec<(Range<usize>, Arc<str>)> = Vec::new(); let mut last_end = None; for _ in 0..edit_count { - if last_end.map_or(false, |last_end| last_end >= self.len()) { + if last_end.is_some_and(|last_end| last_end >= self.len()) { break; } let new_start = last_end.map_or(0, |last_end| last_end + 1); @@ -2671,7 +2671,7 @@ impl<D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator for Ed if pending_edit .as_ref() - .map_or(false, |(change, _)| change.new.end < self.new_end) + .is_some_and(|(change, _)| change.new.end < self.new_end) { break; } diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index c2171d3899285aebc5c90db7e53dbaec42db0637..275f47912a6b0c791ba27ba9768f4a22d6bfd50d 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -189,7 +189,7 @@ impl TitleBar { .as_ref()? .read(cx) .is_being_followed(collaborator.peer_id); - let is_present = project_id.map_or(false, |project_id| { + let is_present = project_id.is_some_and(|project_id| { collaborator.location == ParticipantLocation::SharedProject { project_id } }); diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs index e7cf0cd2d9326b68973935a7815ef281a01b03c3..ed43c5277a51d660738f2b0b3efee77ccbafd381 100644 --- a/crates/title_bar/src/onboarding_banner.rs +++ b/crates/title_bar/src/onboarding_banner.rs @@ -73,7 +73,7 @@ fn get_dismissed(source: &str) -> bool { db::kvp::KEY_VALUE_STORE .read_kvp(&dismissed_at) .log_err() - .map_or(false, |dismissed| dismissed.is_some()) + .is_some_and(|dismissed| dismissed.is_some()) } fn persist_dismissed(source: &str, cx: &mut App) { diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index f77eea4bdc7df950d0bce17d906cbbfda9233b6d..439b53f0388114aa37adcf5277e87744e6f4f9e4 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -93,16 +93,16 @@ impl<M: ManagedView> PopoverMenuHandle<M> { self.0 .borrow() .as_ref() - .map_or(false, |state| state.menu.borrow().as_ref().is_some()) + .is_some_and(|state| state.menu.borrow().as_ref().is_some()) } pub fn is_focused(&self, window: &Window, cx: &App) -> bool { - self.0.borrow().as_ref().map_or(false, |state| { + self.0.borrow().as_ref().is_some_and(|state| { state .menu .borrow() .as_ref() - .map_or(false, |model| model.focus_handle(cx).is_focused(window)) + .is_some_and(|model| model.focus_handle(cx).is_focused(window)) }) } diff --git a/crates/ui/src/components/sticky_items.rs b/crates/ui/src/components/sticky_items.rs index ca8b336a5aa97101f29d394399f36bda2fcc44b9..c3e0886404c08ca555185be811083b6a5b2952f5 100644 --- a/crates/ui/src/components/sticky_items.rs +++ b/crates/ui/src/components/sticky_items.rs @@ -105,7 +105,6 @@ impl Element for StickyItemsElement { _window: &mut Window, _cx: &mut App, ) -> Self::PrepaintState { - () } fn paint( diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 211831125d07196ebbc37e96bb5ad54734c816fa..292ec4874c96f48f417a5f15b4c9cdd146c71afc 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -1215,11 +1215,11 @@ mod tests { // Verify iterators advanced correctly assert!( - !a_iter.next().map_or(false, |c| c.is_ascii_digit()), + !a_iter.next().is_some_and(|c| c.is_ascii_digit()), "Iterator a should have consumed all digits" ); assert!( - !b_iter.next().map_or(false, |c| c.is_ascii_digit()), + !b_iter.next().is_some_and(|c| c.is_ascii_digit()), "Iterator b should have consumed all digits" ); diff --git a/crates/util/src/size.rs b/crates/util/src/size.rs index 084a0e5a5670e39e905dd0b3752f6641ca29db85..c6ecebd548505de5ce60b433099b1e532a3308f9 100644 --- a/crates/util/src/size.rs +++ b/crates/util/src/size.rs @@ -7,14 +7,12 @@ pub fn format_file_size(size: u64, use_decimal: bool) -> String { } else { format!("{:.1}MB", size as f64 / (1000.0 * 1000.0)) } + } else if size < 1024 { + format!("{size}B") + } else if size < 1024 * 1024 { + format!("{:.1}KiB", size as f64 / 1024.0) } else { - if size < 1024 { - format!("{size}B") - } else if size < 1024 * 1024 { - format!("{:.1}KiB", size as f64 / 1024.0) - } else { - format!("{:.1}MiB", size as f64 / (1024.0 * 1024.0)) - } + format!("{:.1}MiB", size as f64 / (1024.0 * 1024.0)) } } diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 187678f8af524d0f938bdcb5cc8b1b356ae5cde7..69a2c88706bff8b459ec7b678976a84ae4f943bf 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -301,7 +301,7 @@ pub fn get_shell_safe_zed_path() -> anyhow::Result<String> { let zed_path_escaped = shlex::try_quote(&zed_path).context("Failed to shell-escape Zed executable path.")?; - return Ok(zed_path_escaped.to_string()); + Ok(zed_path_escaped.to_string()) } #[cfg(unix)] @@ -825,7 +825,7 @@ mod rng { pub fn new(rng: T) -> Self { Self { rng, - simple_text: std::env::var("SIMPLE_TEXT").map_or(false, |v| !v.is_empty()), + simple_text: std::env::var("SIMPLE_TEXT").is_ok_and(|v| !v.is_empty()), } } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 00d3bde750a7fc22eeafd5c7d1771ee47368b2e8..7269fc8bec434cfcba434d8e36d21e237254bfa1 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -566,7 +566,6 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) { workspace.update(cx, |workspace, cx| { e.notify_err(workspace, cx); }); - return; } }); @@ -1444,7 +1443,7 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Vec<CommandInterceptRes }]; } } - return Vec::default(); + Vec::default() } fn generate_positions(string: &str, query: &str) -> Vec<usize> { diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs index beb3bd54bae2b6ef26126bd314db60af84f2dc94..248047bb550cacf61a4c934867069e241368f046 100644 --- a/crates/vim/src/digraph.rs +++ b/crates/vim/src/digraph.rs @@ -103,7 +103,6 @@ impl Vim { window.dispatch_keystroke(keystroke, cx); }); } - return; } pub fn handle_literal_input( diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 3cc9772d42c9efe5ef937995aee84fbdd60bb09d..e2ce54b9940da102a53edcf8f82d039718df2aff 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -47,7 +47,6 @@ impl Vim { } self.stop_recording_immediately(action.boxed_clone(), cx); self.switch_mode(Mode::HelixNormal, false, window, cx); - return; } pub fn helix_normal_motion( diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index e703b181172c01aa889626b13bbe8dc0acde0d67..92e3c972650ecf550a3f2313cbf95dad43eb7526 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -2375,7 +2375,7 @@ fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoin } } - return None; + None } fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { @@ -2517,7 +2517,7 @@ fn unmatched_forward( } display_point = new_point; } - return display_point; + display_point } fn unmatched_backward( diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs index 80d94def0544fffad6d9eaa39a05d8ee932b610c..619769d41adc690014a2872eff9a18d6f0250ae6 100644 --- a/crates/vim/src/normal/mark.rs +++ b/crates/vim/src/normal/mark.rs @@ -120,7 +120,6 @@ impl Vim { }); }) }); - return; } fn open_path_mark( diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 4054c552aeb9eba4f18d708769fc7373201f45ff..4fbeec72365c1613acd4d1d740c518a4676a48a5 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -224,7 +224,7 @@ impl Vim { .search .prior_selections .last() - .map_or(true, |range| range.start != new_head); + .is_none_or(|range| range.start != new_head); if is_different_head { count = count.saturating_sub(1) diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index db19562f027973f978578aa3c5a1ba6f82e970f2..81efcef17a116f2d6592d5dcb8f98f7d21cea6ac 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -606,11 +606,11 @@ impl MarksState { match target? { MarkLocation::Buffer(entity_id) => { let anchors = self.multibuffer_marks.get(entity_id)?; - return Some(Mark::Buffer(*entity_id, anchors.get(name)?.clone())); + Some(Mark::Buffer(*entity_id, anchors.get(name)?.clone())) } MarkLocation::Path(path) => { let points = self.serialized_marks.get(path)?; - return Some(Mark::Path(path.clone(), points.get(name)?.clone())); + Some(Mark::Path(path.clone(), points.get(name)?.clone())) } } } diff --git a/crates/web_search_providers/src/web_search_providers.rs b/crates/web_search_providers/src/web_search_providers.rs index 2248cb7eb36fc3b2c2307b8c89a76abeed683b11..7f8a5f3fa4b2634e50e4de78f9aa09fca0ab0413 100644 --- a/crates/web_search_providers/src/web_search_providers.rs +++ b/crates/web_search_providers/src/web_search_providers.rs @@ -46,7 +46,7 @@ fn register_zed_web_search_provider( let using_zed_provider = language_model_registry .read(cx) .default_model() - .map_or(false, |default| default.is_provided_by_zed()); + .is_some_and(|default| default.is_provided_by_zed()); if using_zed_provider { registry.register_provider(cloud::CloudWebSearchProvider::new(client, cx), cx) } else { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 079f66ae9de39e122b762268c7db664c9ffe498f..1d9170684e0261249c3f97567d2b14a8bb9cd487 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -460,7 +460,7 @@ impl Dock { }; let was_visible = this.is_open() - && this.visible_panel().map_or(false, |active_panel| { + && this.visible_panel().is_some_and(|active_panel| { active_panel.panel_id() == Entity::entity_id(&panel) }); @@ -523,7 +523,7 @@ impl Dock { PanelEvent::Close => { if this .visible_panel() - .map_or(false, |p| p.panel_id() == Entity::entity_id(panel)) + .is_some_and(|p| p.panel_id() == Entity::entity_id(panel)) { this.set_open(false, window, cx); } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 014af7b0bc7a7080289617fcb64dddcec731da2e..5a497398f9189cbba1be734d3ba383e59b9fcc71 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -489,7 +489,7 @@ where fn should_serialize(&self, event: &dyn Any, cx: &App) -> bool { event .downcast_ref::<T::Event>() - .map_or(false, |event| self.read(cx).should_serialize(event)) + .is_some_and(|event| self.read(cx).should_serialize(event)) } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a1affc5362a2c6f2ed8603bd7e956845b712038c..d42b59f08e98618fb5070ed61b33f7f0a9db32d2 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -552,9 +552,9 @@ impl Pane { // to the item, and `focus_handle.contains_focus` returns false because the `active_item` // is not hooked up to us in the dispatch tree. self.focus_handle.contains_focused(window, cx) - || self.active_item().map_or(false, |item| { - item.item_focus_handle(cx).contains_focused(window, cx) - }) + || self + .active_item() + .is_some_and(|item| item.item_focus_handle(cx).contains_focused(window, cx)) } fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) { @@ -1021,7 +1021,7 @@ impl Pane { existing_item .project_entry_ids(cx) .first() - .map_or(false, |existing_entry_id| { + .is_some_and(|existing_entry_id| { Some(existing_entry_id) == project_entry_id.as_ref() }) } else { @@ -1558,7 +1558,7 @@ impl Pane { let other_project_item_ids = open_item.project_item_model_ids(cx); dirty_project_item_ids.retain(|id| !other_project_item_ids.contains(id)); } - return dirty_project_item_ids.is_empty(); + dirty_project_item_ids.is_empty() } pub(super) fn file_names_for_prompt( @@ -2745,7 +2745,7 @@ impl Pane { worktree .read(cx) .root_entry() - .map_or(false, |entry| entry.is_dir()) + .is_some_and(|entry| entry.is_dir()) }); let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx); @@ -3210,8 +3210,7 @@ impl Pane { return; }; - if target.map_or(false, |target| this.is_tab_pinned(target)) - { + if target.is_some_and(|target| this.is_tab_pinned(target)) { this.pin_tab_at(index, window, cx); } }) @@ -3615,7 +3614,6 @@ impl Render for Pane { ) .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| { if cx.stop_active_drag(window) { - return; } else { cx.propagate(); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 4a22107c425016c764842bb97112e39463d3360f..9dac340b5c3a664846c8cb3fa735b4446f43fc48 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1804,7 +1804,7 @@ impl Workspace { .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id)) }); - latest_project_path_opened.map_or(true, |path| path == history_path) + latest_project_path_opened.is_none_or(|path| path == history_path) }) } @@ -2284,7 +2284,7 @@ impl Workspace { // the current session. if close_intent != CloseIntent::Quit && !save_last_workspace - && save_result.as_ref().map_or(false, |&res| res) + && save_result.as_ref().is_ok_and(|&res| res) { this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))? .await; @@ -5133,13 +5133,11 @@ impl Workspace { self.panes.retain(|p| p != pane); if let Some(focus_on) = focus_on { focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); - } else { - if self.active_pane() == pane { - self.panes - .last() - .unwrap() - .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); - } + } else if self.active_pane() == pane { + self.panes + .last() + .unwrap() + .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); } if self.last_active_center_pane == Some(pane.downgrade()) { self.last_active_center_pane = None; @@ -5893,7 +5891,6 @@ impl Workspace { pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) { if cx.stop_active_drag(window) { - return; } else if let Some((notification_id, _)) = self.notifications.pop() { dismiss_app_notification(¬ification_id, cx); } else { @@ -6100,7 +6097,7 @@ fn open_items( // here is a directory, it was already opened further above // with a `find_or_create_worktree`. if let Ok(task) = abs_path_task - && task.await.map_or(true, |p| p.is_file()) + && task.await.is_none_or(|p| p.is_file()) { return Some(( ix, @@ -6970,7 +6967,7 @@ async fn join_channel_internal( && project.visible_worktrees(cx).any(|tree| { tree.read(cx) .root_entry() - .map_or(false, |entry| entry.is_dir()) + .is_some_and(|entry| entry.is_dir()) }) { Some(workspace.project.clone()) @@ -7900,7 +7897,6 @@ fn join_pane_into_active( cx: &mut App, ) { if pane == active_pane { - return; } else if pane.read(cx).items_len() == 0 { pane.update(cx, |_, cx| { cx.emit(pane::Event::Remove { @@ -9149,11 +9145,11 @@ mod tests { workspace.update_in(cx, |workspace, window, cx| { workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx); }); - return item; + item } fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> { - return workspace.update_in(cx, |workspace, window, cx| { + workspace.update_in(cx, |workspace, window, cx| { let new_pane = workspace.split_pane( workspace.active_pane().clone(), SplitDirection::Right, @@ -9161,7 +9157,7 @@ mod tests { cx, ); new_pane - }); + }) } #[gpui::test] diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 9e1832721fcdf1577537de5e90dccace661db5ee..d38f3cac3d3a77d4b9c92cb97e8839907d403f2e 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3393,12 +3393,10 @@ impl File { let disk_state = if proto.is_deleted { DiskState::Deleted + } else if let Some(mtime) = proto.mtime.map(&Into::into) { + DiskState::Present { mtime } } else { - if let Some(mtime) = proto.mtime.map(&Into::into) { - DiskState::Present { mtime } - } else { - DiskState::New - } + DiskState::New }; Ok(Self { @@ -4074,10 +4072,10 @@ impl BackgroundScanner { } } - let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { + let parent_dir_is_loaded = relative_path.parent().is_none_or(|parent| { snapshot .entry_for_path(parent) - .map_or(false, |entry| entry.kind == EntryKind::Dir) + .is_some_and(|entry| entry.kind == EntryKind::Dir) }); if !parent_dir_is_loaded { log::debug!("ignoring event {relative_path:?} within unloaded directory"); @@ -4630,7 +4628,7 @@ impl BackgroundScanner { while let Some(parent_abs_path) = ignores_to_update.next() { while ignores_to_update .peek() - .map_or(false, |p| p.starts_with(&parent_abs_path)) + .is_some_and(|p| p.starts_with(&parent_abs_path)) { ignores_to_update.next().unwrap(); } @@ -4797,9 +4795,7 @@ impl BackgroundScanner { for (&work_directory_id, entry) in snapshot.git_repositories.iter() { let exists_in_snapshot = snapshot .entry_for_id(work_directory_id) - .map_or(false, |entry| { - snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some() - }); + .is_some_and(|entry| snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()); if exists_in_snapshot || matches!( @@ -4924,10 +4920,10 @@ fn build_diff( new_paths.next(); for path in event_paths { let path = PathKey(path.clone()); - if old_paths.item().map_or(false, |e| e.path < path.0) { + if old_paths.item().is_some_and(|e| e.path < path.0) { old_paths.seek_forward(&path, Bias::Left); } - if new_paths.item().map_or(false, |e| e.path < path.0) { + if new_paths.item().is_some_and(|e| e.path < path.0) { new_paths.seek_forward(&path, Bias::Left); } loop { @@ -4977,7 +4973,7 @@ fn build_diff( let is_newly_loaded = phase == InitialScan || last_newly_loaded_dir_path .as_ref() - .map_or(false, |dir| new_entry.path.starts_with(dir)); + .is_some_and(|dir| new_entry.path.starts_with(dir)); changes.push(( new_entry.path.clone(), new_entry.id, @@ -4995,7 +4991,7 @@ fn build_diff( let is_newly_loaded = phase == InitialScan || last_newly_loaded_dir_path .as_ref() - .map_or(false, |dir| new_entry.path.starts_with(dir)); + .is_some_and(|dir| new_entry.path.starts_with(dir)); changes.push(( new_entry.path.clone(), new_entry.id, diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index 26cf16e8f6c695d70c20ea7ce99b65928f73f171..b18d3509beb408c37beaf246a747248d2f17438a 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -82,7 +82,7 @@ impl Settings for WorktreeSettings { .ancestors() .map(|a| a.to_string_lossy().into()) }) - .filter(|p| p != "") + .filter(|p: &String| !p.is_empty()) .collect(); file_scan_exclusions.sort(); private_files.sort(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 93a62afc6f4468212709e612d635a899d100f203..d3a503f172bae0e7fbb3668f9f178243f7d0207f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1625,7 +1625,7 @@ fn open_local_file( .await .ok() .flatten() - .map_or(false, |metadata| !metadata.is_dir && !metadata.is_fifo); + .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo); file_exists }; diff --git a/crates/zed/src/zed/migrate.rs b/crates/zed/src/zed/migrate.rs index 48bffb4114011d119e86ff28180bb2e4b898b3d1..2452f17d04007364861e9a262b492155daec0c55 100644 --- a/crates/zed/src/zed/migrate.rs +++ b/crates/zed/src/zed/migrate.rs @@ -177,7 +177,7 @@ impl ToolbarItemView for MigrationBanner { })); } - return ToolbarItemLocation::Hidden; + ToolbarItemLocation::Hidden } } diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index d65053c05f66c7520d92a7dccb05e44e1d019941..10d60fcd9d6e6ea4d2bfe133a18321cd6a960ab8 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -175,9 +175,9 @@ impl Render for QuickActionBar { let code_action_menu = menu_ref .as_ref() .filter(|menu| matches!(menu, CodeContextMenu::CodeActions(..))); - code_action_menu.as_ref().map_or(false, |menu| { - matches!(menu.origin(), ContextMenuOrigin::QuickActionBar) - }) + code_action_menu + .as_ref() + .is_some_and(|menu| matches!(menu.origin(), ContextMenuOrigin::QuickActionBar)) }; let code_action_element = if is_deployed { editor.update(cx, |editor, cx| { diff --git a/crates/zeta/src/init.rs b/crates/zeta/src/init.rs index a01e3a89a2bc0e365dd58a80e2377a215e303c64..6e5b31f99a76cb0e066348150e962396cf1ad9c6 100644 --- a/crates/zeta/src/init.rs +++ b/crates/zeta/src/init.rs @@ -85,12 +85,10 @@ fn feature_gate_predict_edits_actions(cx: &mut App) { CommandPaletteFilter::update_global(cx, |filter, _cx| { if is_ai_disabled { filter.hide_action_types(&zeta_all_action_types); + } else if has_feature_flag { + filter.show_action_types(rate_completion_action_types.iter()); } else { - if has_feature_flag { - filter.show_action_types(rate_completion_action_types.iter()); - } else { - filter.hide_action_types(&rate_completion_action_types); - } + filter.hide_action_types(&rate_completion_action_types); } }); }) diff --git a/crates/zeta/src/onboarding_modal.rs b/crates/zeta/src/onboarding_modal.rs index c2886f2864502618eb3b51208d0749e9bbc3b48b..3a58c8c7b812b193724eaf911cb15db264204964 100644 --- a/crates/zeta/src/onboarding_modal.rs +++ b/crates/zeta/src/onboarding_modal.rs @@ -46,7 +46,7 @@ impl ZedPredictModal { user_store.clone(), client.clone(), copilot::Copilot::global(cx) - .map_or(false, |copilot| copilot.read(cx).status().is_configured()), + .is_some_and(|copilot| copilot.read(cx).status().is_configured()), Arc::new({ let this = weak_entity.clone(); move |_window, cx| { diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index 313e4c377984d1b4cf3e3ab72004e29bba9c705b..0cd814388ae92adb381ecd6dde7099a24991bcf9 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/crates/zeta/src/rate_completion_modal.rs @@ -607,7 +607,7 @@ impl Render for RateCompletionModal { .children(self.zeta.read(cx).shown_completions().cloned().enumerate().map( |(index, completion)| { let selected = - self.active_completion.as_ref().map_or(false, |selected| { + self.active_completion.as_ref().is_some_and(|selected| { selected.completion.id == completion.id }); let rated = diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 2a121c407c5456f3cdfb5cc8eb6206f8cd985f41..640f408dd3acc9a3bbf4f773d72188bcf57dac6a 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -106,7 +106,7 @@ impl Dismissable for ZedPredictUpsell { if KEY_VALUE_STORE .read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE) .log_err() - .map_or(false, |s| s.is_some()) + .is_some_and(|s| s.is_some()) { return true; } @@ -114,7 +114,7 @@ impl Dismissable for ZedPredictUpsell { KEY_VALUE_STORE .read_kvp(Self::KEY) .log_err() - .map_or(false, |s| s.is_some()) + .is_some_and(|s| s.is_some()) } } diff --git a/crates/zlog/src/filter.rs b/crates/zlog/src/filter.rs index cf1604bd9f5d39f3602276deaf3aee86fc714154..27a5314e28b4ae580692292d5e0a64f1e7dd34bb 100644 --- a/crates/zlog/src/filter.rs +++ b/crates/zlog/src/filter.rs @@ -55,7 +55,7 @@ pub fn init_env_filter(filter: env_config::EnvFilter) { } pub fn is_possibly_enabled_level(level: log::Level) -> bool { - return level as u8 <= LEVEL_ENABLED_MAX_CONFIG.load(Ordering::Relaxed); + level as u8 <= LEVEL_ENABLED_MAX_CONFIG.load(Ordering::Relaxed) } pub fn is_scope_enabled(scope: &Scope, module_path: Option<&str>, level: log::Level) -> bool { @@ -70,7 +70,7 @@ pub fn is_scope_enabled(scope: &Scope, module_path: Option<&str>, level: log::Le let is_enabled_by_default = level <= unsafe { LEVEL_ENABLED_MAX_STATIC }; let global_scope_map = SCOPE_MAP.read().unwrap_or_else(|err| { SCOPE_MAP.clear_poison(); - return err.into_inner(); + err.into_inner() }); let Some(map) = global_scope_map.as_ref() else { @@ -83,11 +83,11 @@ pub fn is_scope_enabled(scope: &Scope, module_path: Option<&str>, level: log::Le return is_enabled_by_default; } let enabled_status = map.is_enabled(scope, module_path, level); - return match enabled_status { + match enabled_status { EnabledStatus::NotConfigured => is_enabled_by_default, EnabledStatus::Enabled => true, EnabledStatus::Disabled => false, - }; + } } pub fn refresh_from_settings(settings: &HashMap<String, String>) { @@ -132,7 +132,7 @@ fn level_filter_from_str(level_str: &str) -> Option<log::LevelFilter> { return None; } }; - return Some(level); + Some(level) } fn scope_alloc_from_scope_str(scope_str: &str) -> Option<ScopeAlloc> { @@ -143,7 +143,7 @@ fn scope_alloc_from_scope_str(scope_str: &str) -> Option<ScopeAlloc> { let Some(scope) = scope_iter.next() else { break; }; - if scope == "" { + if scope.is_empty() { continue; } scope_buf[index] = scope; @@ -159,7 +159,7 @@ fn scope_alloc_from_scope_str(scope_str: &str) -> Option<ScopeAlloc> { return None; } let scope = scope_buf.map(|s| s.to_string()); - return Some(scope); + Some(scope) } #[derive(Debug, PartialEq, Eq)] @@ -280,7 +280,7 @@ impl ScopeMap { cursor += 1; } let sub_items_end = cursor; - if scope_name == "" { + if scope_name.is_empty() { assert_eq!(sub_items_start + 1, sub_items_end); assert_ne!(depth, 0); assert_ne!(parent_index, usize::MAX); @@ -288,7 +288,7 @@ impl ScopeMap { this.entries[parent_index].enabled = Some(items[sub_items_start].1); continue; } - let is_valid_scope = scope_name != ""; + let is_valid_scope = !scope_name.is_empty(); let is_last = depth + 1 == SCOPE_DEPTH_MAX || !is_valid_scope; let mut enabled = None; if is_last { @@ -321,7 +321,7 @@ impl ScopeMap { } } - return this; + this } pub fn is_empty(&self) -> bool { @@ -358,7 +358,7 @@ impl ScopeMap { } break 'search; } - return enabled; + enabled } let mut enabled = search(self, scope); @@ -394,7 +394,7 @@ impl ScopeMap { } return EnabledStatus::Disabled; } - return EnabledStatus::NotConfigured; + EnabledStatus::NotConfigured } } @@ -456,7 +456,7 @@ mod tests { let Some(scope) = scope_iter.next() else { break; }; - if scope == "" { + if scope.is_empty() { continue; } scope_buf[index] = scope; @@ -464,7 +464,7 @@ mod tests { } assert_ne!(index, 0); assert!(scope_iter.next().is_none()); - return scope_buf; + scope_buf } #[test] diff --git a/crates/zlog/src/zlog.rs b/crates/zlog/src/zlog.rs index df3a2102317775288f11650320bdc26d7a34d1af..d1c6cd474728026f58a89590bc76705232f12773 100644 --- a/crates/zlog/src/zlog.rs +++ b/crates/zlog/src/zlog.rs @@ -240,7 +240,7 @@ pub mod private { let Some((crate_name, _)) = module_path.split_at_checked(index) else { return module_path; }; - return crate_name; + crate_name } pub const fn scope_new(scopes: &[&'static str]) -> Scope { @@ -262,7 +262,7 @@ pub mod private { } pub fn scope_to_alloc(scope: &Scope) -> ScopeAlloc { - return scope.map(|s| s.to_string()); + scope.map(|s| s.to_string()) } } @@ -319,18 +319,18 @@ impl Drop for Timer { impl Timer { #[must_use = "Timer will stop when dropped, the result of this function should be saved in a variable prefixed with `_` if it should stop when dropped"] pub fn new(logger: Logger, name: &'static str) -> Self { - return Self { + Self { logger, name, start_time: std::time::Instant::now(), warn_if_longer_than: None, done: false, - }; + } } pub fn warn_if_gt(mut self, warn_limit: std::time::Duration) -> Self { self.warn_if_longer_than = Some(warn_limit); - return self; + self } pub fn end(mut self) { diff --git a/extensions/glsl/src/glsl.rs b/extensions/glsl/src/glsl.rs index ba506d2b11fca935fe327279421259d284dd8e3d..695fd7a05354991cc47740827ce86fdd1b612269 100644 --- a/extensions/glsl/src/glsl.rs +++ b/extensions/glsl/src/glsl.rs @@ -17,7 +17,7 @@ impl GlslExtension { } if let Some(path) = &self.cached_binary_path - && fs::metadata(path).map_or(false, |stat| stat.is_file()) + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) { return Ok(path.clone()); } @@ -60,7 +60,7 @@ impl GlslExtension { .map_err(|err| format!("failed to create directory '{version_dir}': {err}"))?; let binary_path = format!("{version_dir}/bin/glsl_analyzer"); - if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { zed::set_language_server_installation_status( language_server_id, &zed::LanguageServerInstallationStatus::Downloading, diff --git a/extensions/html/src/html.rs b/extensions/html/src/html.rs index 44ec4fe4b9e8681f7de0aba56e6313d1fecb2c60..07d4642ff404f2450edfcfcf5c52a6f2b373b897 100644 --- a/extensions/html/src/html.rs +++ b/extensions/html/src/html.rs @@ -13,7 +13,7 @@ struct HtmlExtension { impl HtmlExtension { fn server_exists(&self) -> bool { - fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file()) + fs::metadata(SERVER_PATH).is_ok_and(|stat| stat.is_file()) } fn server_script_path(&mut self, language_server_id: &LanguageServerId) -> Result<String> { diff --git a/extensions/ruff/src/ruff.rs b/extensions/ruff/src/ruff.rs index 7b811db21202cd0bb7c3f6b54248144ae8a2f6e6..b918c52686c63b0bcc73be9a7cc508c91a76c3b0 100644 --- a/extensions/ruff/src/ruff.rs +++ b/extensions/ruff/src/ruff.rs @@ -39,7 +39,7 @@ impl RuffExtension { } if let Some(path) = &self.cached_binary_path - && fs::metadata(path).map_or(false, |stat| stat.is_file()) + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) { return Ok(RuffBinary { path: path.clone(), @@ -94,7 +94,7 @@ impl RuffExtension { _ => format!("{version_dir}/{asset_stem}/ruff"), }; - if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { zed::set_language_server_installation_status( language_server_id, &zed::LanguageServerInstallationStatus::Downloading, diff --git a/extensions/snippets/src/snippets.rs b/extensions/snippets/src/snippets.rs index 682709a28a4857e854cda66787025a20569a5cca..b2d68b6e1a98a39c73ba746af5c452597ac59b82 100644 --- a/extensions/snippets/src/snippets.rs +++ b/extensions/snippets/src/snippets.rs @@ -18,7 +18,7 @@ impl SnippetExtension { } if let Some(path) = &self.cached_binary_path - && fs::metadata(path).map_or(false, |stat| stat.is_file()) + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) { return Ok(path.clone()); } @@ -59,7 +59,7 @@ impl SnippetExtension { let version_dir = format!("simple-completion-language-server-{}", release.version); let binary_path = format!("{version_dir}/simple-completion-language-server"); - if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { zed::set_language_server_installation_status( language_server_id, &zed::LanguageServerInstallationStatus::Downloading, diff --git a/extensions/test-extension/src/test_extension.rs b/extensions/test-extension/src/test_extension.rs index 0ef522bd51277f45897d3455a274d132683dd32c..ee0b1b36a1b14b899363b86c8d24db120efaf09b 100644 --- a/extensions/test-extension/src/test_extension.rs +++ b/extensions/test-extension/src/test_extension.rs @@ -19,7 +19,7 @@ impl TestExtension { println!("{}", String::from_utf8_lossy(&echo_output.stdout)); if let Some(path) = &self.cached_binary_path - && fs::metadata(path).map_or(false, |stat| stat.is_file()) + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) { return Ok(path.clone()); } @@ -61,7 +61,7 @@ impl TestExtension { let version_dir = format!("gleam-{}", release.version); let binary_path = format!("{version_dir}/gleam"); - if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { zed::set_language_server_installation_status( language_server_id, &zed::LanguageServerInstallationStatus::Downloading, diff --git a/extensions/toml/src/toml.rs b/extensions/toml/src/toml.rs index 30a2cd6ce3b9c83fcc25ed2c38adc1623a5fb353..c9b96aecacd17d192fad9b6801973c2f2389cf98 100644 --- a/extensions/toml/src/toml.rs +++ b/extensions/toml/src/toml.rs @@ -40,7 +40,7 @@ impl TomlExtension { } if let Some(path) = &self.cached_binary_path - && fs::metadata(path).map_or(false, |stat| stat.is_file()) + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) { return Ok(TaploBinary { path: path.clone(), @@ -93,7 +93,7 @@ impl TomlExtension { } ); - if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { zed::set_language_server_installation_status( language_server_id, &zed::LanguageServerInstallationStatus::Downloading, diff --git a/tooling/xtask/src/tasks/package_conformity.rs b/tooling/xtask/src/tasks/package_conformity.rs index c82b9cdf845b594fa0571a45839bc3fb5bed3582..c8bed4bb35185430d7942d4e2a6b704b6fcddff3 100644 --- a/tooling/xtask/src/tasks/package_conformity.rs +++ b/tooling/xtask/src/tasks/package_conformity.rs @@ -21,13 +21,11 @@ pub fn run_package_conformity(_args: PackageConformityArgs) -> Result<()> { .manifest_path .parent() .and_then(|parent| parent.parent()) - .map_or(false, |grandparent_dir| { - grandparent_dir.ends_with("extensions") - }); + .is_some_and(|grandparent_dir| grandparent_dir.ends_with("extensions")); let cargo_toml = read_cargo_toml(&package.manifest_path)?; - let is_using_workspace_lints = cargo_toml.lints.map_or(false, |lints| lints.workspace); + let is_using_workspace_lints = cargo_toml.lints.is_some_and(|lints| lints.workspace); if !is_using_workspace_lints { eprintln!( "{package:?} is not using workspace lints", From 69b1c6d6f56e8ebc4c6b0ce6aaed06986521a47d Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Tue, 19 Aug 2025 15:26:40 -0400 Subject: [PATCH 153/744] Fix `workspace::SendKeystrokes` example in docs (#36515) Closes: https://github.com/zed-industries/zed/issues/25683 Remove two bad examples from the key binding docs. `cmd-shift-p` (command palette) and `cmd-p` (file finder) are async operations and thus do not work properly with `workspace::SendKeystrokes`. Originally reported in https://github.com/zed-industries/zed/issues/25683#issuecomment-3145830534 Release Notes: - N/A --- docs/src/key-bindings.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index 9fc94840b7790673240246a6bee1aaf8c37119e0..838dceaa8625d520fd8a7011883dcd39bd7d5dc8 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -225,12 +225,14 @@ A common request is to be able to map from a single keystroke to a sequence. You [ { "bindings": { + // Move down four times "alt-down": ["workspace::SendKeystrokes", "down down down down"], + // Expand the selection (editor::SelectLargerSyntaxNode); + // copy to the clipboard; and then undo the selection expansion. "cmd-alt-c": [ "workspace::SendKeystrokes", - "cmd-shift-p copy relative path enter" - ], - "cmd-alt-r": ["workspace::SendKeystrokes", "cmd-p README enter"] + "ctrl-shift-right ctrl-shift-right ctrl-shift-right cmd-c ctrl-shift-left ctrl-shift-left ctrl-shift-left" + ] } }, { From 68257155037fa0f5c2093964eddb9a4c288741d9 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 19 Aug 2025 22:33:44 +0200 Subject: [PATCH 154/744] Another batch of lint fixes (#36521) - **Enable a bunch of extra lints** - **First batch of fixes** - **More fixes** Release Notes: - N/A --- Cargo.toml | 10 ++ crates/action_log/src/action_log.rs | 5 +- .../src/activity_indicator.rs | 29 ++-- crates/agent/src/thread.rs | 6 +- crates/agent/src/thread_store.rs | 2 +- crates/agent2/src/agent.rs | 2 +- crates/agent2/src/tools/edit_file_tool.rs | 3 +- crates/agent_servers/src/acp/v0.rs | 4 +- crates/agent_servers/src/claude/mcp_server.rs | 6 +- crates/agent_servers/src/e2e_tests.rs | 7 +- crates/agent_ui/src/acp/message_editor.rs | 5 +- crates/agent_ui/src/acp/thread_view.rs | 4 +- crates/agent_ui/src/active_thread.rs | 12 +- crates/agent_ui/src/agent_configuration.rs | 2 +- .../configure_context_server_modal.rs | 2 +- crates/agent_ui/src/agent_diff.rs | 24 +-- crates/agent_ui/src/agent_panel.rs | 55 +++--- crates/agent_ui/src/context_picker.rs | 9 +- crates/agent_ui/src/message_editor.rs | 17 +- crates/agent_ui/src/slash_command_picker.rs | 4 +- crates/agent_ui/src/text_thread_editor.rs | 15 +- crates/agent_ui/src/tool_compatibility.rs | 12 +- .../src/assistant_context.rs | 20 +-- .../src/file_command.rs | 6 +- crates/assistant_tool/src/tool_working_set.rs | 4 +- crates/assistant_tools/src/assistant_tools.rs | 5 +- crates/assistant_tools/src/edit_file_tool.rs | 3 +- crates/assistant_tools/src/terminal_tool.rs | 6 +- crates/cli/src/main.rs | 8 +- crates/collab/src/tests/integration_tests.rs | 2 +- .../src/chat_panel/message_editor.rs | 5 +- crates/context_server/src/listener.rs | 2 +- crates/dap_adapters/src/python.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/debugger_ui/src/session.rs | 6 +- crates/debugger_ui/src/session/running.rs | 6 +- .../src/session/running/breakpoint_list.rs | 36 ++-- .../src/session/running/variable_list.rs | 2 +- crates/debugger_ui/src/tests/variable_list.rs | 7 +- crates/docs_preprocessor/src/main.rs | 19 +- crates/editor/src/display_map/invisibles.rs | 10 +- crates/editor/src/editor.rs | 162 ++++++++---------- crates/editor/src/editor_tests.rs | 4 +- crates/editor/src/element.rs | 29 ++-- crates/editor/src/git/blame.rs | 7 +- crates/editor/src/hover_popover.rs | 21 +-- crates/editor/src/items.rs | 16 +- crates/editor/src/jsx_tag_auto_close.rs | 5 +- crates/editor/src/proposed_changes_editor.rs | 25 +-- crates/editor/src/selections_collection.rs | 22 +-- crates/eval/src/instance.rs | 10 +- crates/extension/src/extension_builder.rs | 12 +- crates/extension_host/src/extension_host.rs | 5 +- crates/extension_host/src/wasm_host.rs | 2 +- crates/extensions_ui/src/extensions_ui.rs | 6 +- crates/file_finder/src/open_path_prompt.rs | 2 +- crates/fs/src/fs.rs | 5 +- crates/git/src/repository.rs | 2 +- crates/git_ui/src/file_diff_view.rs | 2 +- crates/git_ui/src/git_panel.rs | 11 +- crates/git_ui/src/project_diff.rs | 31 ++-- crates/git_ui/src/text_diff_view.rs | 2 +- crates/gpui/src/app.rs | 4 +- crates/gpui/src/elements/text.rs | 6 +- .../gpui/src/platform/linux/wayland/client.rs | 49 +++--- .../gpui/src/platform/linux/wayland/window.rs | 141 ++++++++------- crates/gpui/src/platform/linux/x11/client.rs | 44 ++--- crates/gpui/src/platform/mac/events.rs | 6 +- crates/gpui/src/platform/mac/window.rs | 4 +- .../gpui/src/platform/scap_screen_capture.rs | 2 +- crates/gpui/src/platform/windows/events.rs | 29 ++-- crates/gpui/src/taffy.rs | 18 +- crates/gpui/src/text_system/line_wrapper.rs | 2 +- crates/gpui/src/util.rs | 8 +- crates/gpui_macros/src/test.rs | 2 +- crates/http_client/src/http_client.rs | 3 +- crates/jj/src/jj_repository.rs | 7 +- crates/journal/src/journal.rs | 6 +- crates/language/src/buffer.rs | 13 +- crates/language/src/language.rs | 5 +- crates/language_model/src/language_model.rs | 2 +- .../language_models/src/provider/open_ai.rs | 2 +- crates/languages/src/json.rs | 2 +- crates/languages/src/python.rs | 2 +- crates/multi_buffer/src/multi_buffer.rs | 8 +- crates/node_runtime/src/node_runtime.rs | 5 +- crates/onboarding/src/ai_setup_page.rs | 4 +- crates/onboarding/src/basics_page.rs | 10 +- crates/onboarding/src/editing_page.rs | 2 +- crates/outline_panel/src/outline_panel.rs | 20 +-- crates/project/src/buffer_store.rs | 18 +- crates/project/src/color_extractor.rs | 6 +- crates/project/src/context_server_store.rs | 16 +- crates/project/src/debugger/dap_store.rs | 3 +- crates/project/src/debugger/locators/go.rs | 6 +- crates/project/src/debugger/session.rs | 13 +- crates/project/src/image_store.rs | 26 ++- crates/project/src/lsp_command.rs | 4 +- crates/project/src/lsp_store.rs | 46 ++--- crates/project/src/manifest_tree.rs | 16 +- crates/project/src/project.rs | 26 ++- crates/project/src/project_tests.rs | 6 +- crates/project/src/task_inventory.rs | 2 +- crates/project/src/terminals.rs | 6 +- crates/project_panel/src/project_panel.rs | 6 +- .../src/disconnected_overlay.rs | 7 +- crates/remote/src/protocol.rs | 4 +- crates/remote_server/src/headless_project.rs | 37 ++-- crates/remote_server/src/unix.rs | 8 +- crates/repl/src/components/kernel_options.rs | 5 +- crates/repl/src/notebook/cell.rs | 2 +- crates/repl/src/notebook/notebook_ui.rs | 4 +- crates/repl/src/outputs/plain.rs | 6 +- crates/rope/src/chunk.rs | 2 +- crates/rules_library/src/rules_library.rs | 2 +- crates/search/src/project_search.rs | 4 +- crates/settings/src/key_equivalents.rs | 2 +- crates/settings/src/settings_json.rs | 6 +- crates/settings_ui/src/keybindings.rs | 26 ++- .../src/ui_components/keystroke_input.rs | 2 +- crates/settings_ui/src/ui_components/table.rs | 4 +- crates/storybook/src/story_selector.rs | 6 +- crates/svg_preview/src/svg_preview_view.rs | 43 ++--- crates/task/src/shell_builder.rs | 12 +- crates/tasks_ui/src/modal.rs | 5 +- crates/terminal_view/src/terminal_panel.rs | 53 +++--- crates/terminal_view/src/terminal_view.rs | 7 +- crates/theme/src/icon_theme.rs | 2 +- crates/title_bar/src/collab.rs | 6 +- crates/ui/src/components/facepile.rs | 2 +- crates/ui/src/components/toggle.rs | 2 +- crates/ui/src/components/tooltip.rs | 2 +- crates/vim/src/helix.rs | 20 +-- crates/vim/src/normal/change.rs | 7 +- crates/vim/src/state.rs | 56 +++--- crates/vim/src/test/neovim_connection.rs | 2 +- crates/web_search_providers/src/cloud.rs | 2 +- .../src/web_search_providers.rs | 5 +- crates/workspace/src/shared_screen.rs | 11 +- crates/workspace/src/workspace.rs | 13 +- crates/zed/src/main.rs | 9 +- crates/zed/src/zed.rs | 7 +- .../zed/src/zed/edit_prediction_registry.rs | 5 +- crates/zeta/src/zeta.rs | 12 +- crates/zeta_cli/src/main.rs | 15 +- crates/zlog/src/filter.rs | 13 +- crates/zlog/src/zlog.rs | 11 +- 147 files changed, 784 insertions(+), 1038 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 46c5646c90daf90c4cee00cf7fc66b5239e54f4d..ad45def2d4f51d27e99a5f4afe0910524fe1ab5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -822,14 +822,20 @@ style = { level = "allow", priority = -1 } # Temporary list of style lints that we've fixed so far. comparison_to_empty = "warn" +into_iter_on_ref = "warn" iter_cloned_collect = "warn" iter_next_slice = "warn" iter_nth = "warn" iter_nth_zero = "warn" iter_skip_next = "warn" +let_and_return = "warn" module_inception = { level = "deny" } question_mark = { level = "deny" } +single_match = "warn" redundant_closure = { level = "deny" } +redundant_static_lifetimes = { level = "warn" } +redundant_pattern_matching = "warn" +redundant_field_names = "warn" declare_interior_mutable_const = { level = "deny" } collapsible_if = { level = "warn"} collapsible_else_if = { level = "warn" } @@ -857,6 +863,10 @@ too_many_arguments = "allow" # We often have large enum variants yet we rarely actually bother with splitting them up. large_enum_variant = "allow" +# `enum_variant_names` fires for all enums, even when they derive serde traits. +# Adhering to this lint would be a breaking change. +enum_variant_names = "allow" + [workspace.metadata.cargo-machete] ignored = [ "bindgen", diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index 602357ed2b5bfb8e52f906dfcd3de911a4068907..1c3cad386d652648fc448d8df3b1bbd6ca173597 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -264,15 +264,14 @@ impl ActionLog { if let Some((git_diff, (buffer_repo, _))) = git_diff.as_ref().zip(buffer_repo) { cx.update(|cx| { let mut old_head = buffer_repo.read(cx).head_commit.clone(); - Some(cx.subscribe(git_diff, move |_, event, cx| match event { - buffer_diff::BufferDiffEvent::DiffChanged { .. } => { + Some(cx.subscribe(git_diff, move |_, event, cx| { + if let buffer_diff::BufferDiffEvent::DiffChanged { .. } = event { let new_head = buffer_repo.read(cx).head_commit.clone(); if new_head != old_head { old_head = new_head; git_diff_updates_tx.send(()).ok(); } } - _ => {} })) })? } else { diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 8faf74736af6229a408cba96a13e27a0b4fab241..324480f5b49748eb1f107a520d44a916ac72659f 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -103,26 +103,21 @@ impl ActivityIndicator { cx.subscribe_in( &workspace_handle, window, - |activity_indicator, _, event, window, cx| match event { - workspace::Event::ClearActivityIndicator { .. } => { - if activity_indicator.statuses.pop().is_some() { - activity_indicator.dismiss_error_message( - &DismissErrorMessage, - window, - cx, - ); - cx.notify(); - } + |activity_indicator, _, event, window, cx| { + if let workspace::Event::ClearActivityIndicator { .. } = event + && activity_indicator.statuses.pop().is_some() + { + activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx); + cx.notify(); } - _ => {} }, ) .detach(); cx.subscribe( &project.read(cx).lsp_store(), - |activity_indicator, _, event, cx| match event { - LspStoreEvent::LanguageServerUpdate { name, message, .. } => { + |activity_indicator, _, event, cx| { + if let LspStoreEvent::LanguageServerUpdate { name, message, .. } = event { if let proto::update_language_server::Variant::StatusUpdate(status_update) = message { @@ -191,7 +186,6 @@ impl ActivityIndicator { } cx.notify() } - _ => {} }, ) .detach(); @@ -206,9 +200,10 @@ impl ActivityIndicator { cx.subscribe( &project.read(cx).git_store().clone(), - |_, _, event: &GitStoreEvent, cx| match event { - project::git_store::GitStoreEvent::JobsUpdated => cx.notify(), - _ => {} + |_, _, event: &GitStoreEvent, cx| { + if let project::git_store::GitStoreEvent::JobsUpdated = event { + cx.notify() + } }, ) .detach(); diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 5c4b2b8ebfae97aa0919fae68a5132fefc7b342f..80ed277f10cb8db19df5488b0cbe903feb7c2198 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1645,15 +1645,13 @@ impl Thread { self.tool_use .request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx); - let pending_tool_use = self.tool_use.insert_tool_output( + self.tool_use.insert_tool_output( tool_use_id.clone(), tool_name, tool_output, self.configured_model.as_ref(), self.completion_mode, - ); - - pending_tool_use + ) } pub fn stream_completion( diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 63d0f72e00b272bbe91859ffe52bce317ef1963f..45e551dbdf01425f8ecd454e2808d1ea1cf94c51 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -74,7 +74,7 @@ impl Column for DataType { } } -const RULES_FILE_NAMES: [&'static str; 9] = [ +const RULES_FILE_NAMES: [&str; 9] = [ ".rules", ".cursorrules", ".windsurfrules", diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index bc46ad1657870d0445d9cb23f84898a1f3f08999..48f46a52fcc7324140bddaff1640307d985aaa04 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -28,7 +28,7 @@ use std::rc::Rc; use std::sync::Arc; use util::ResultExt; -const RULES_FILE_NAMES: [&'static str; 9] = [ +const RULES_FILE_NAMES: [&str; 9] = [ ".rules", ".cursorrules", ".windsurfrules", diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 21eb282110100b89abcdce4047a0ace6797408ca..a87699bd1202b5d8c561559743e1990dcae288f7 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -655,8 +655,7 @@ mod tests { mode: mode.clone(), }; - let result = cx.update(|cx| resolve_path(&input, project, cx)); - result + cx.update(|cx| resolve_path(&input, project, cx)) } fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) { diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index aa80f01c15a4c2da96973356ab1dbf838a8eb28a..30643dd00516a9a10391c70b20d984fa47738f95 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -149,7 +149,7 @@ impl acp_old::Client for OldAcpClientDelegate { Ok(acp_old::RequestToolCallConfirmationResponse { id: acp_old::ToolCallId(old_acp_id), - outcome: outcome, + outcome, }) } @@ -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, + id, title: request.label, kind: acp_kind_from_old_icon(request.icon), status: acp::ToolCallStatus::InProgress, diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index 38587574db63a91ea468da9f1154725290f79f1e..30867528502d5e94fe0d1af2bb36f864cc4e2390 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -175,9 +175,9 @@ impl McpServerTool for PermissionTool { let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone()); let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into()); - const ALWAYS_ALLOW: &'static str = "always_allow"; - const ALLOW: &'static str = "allow"; - const REJECT: &'static str = "reject"; + const ALWAYS_ALLOW: &str = "always_allow"; + const ALLOW: &str = "allow"; + const REJECT: &str = "reject"; let chosen_option = thread .update(cx, |thread, cx| { diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index fef80b4d42b6d49385acc5e0fae01997969da005..8b2703575d9ea68ff7ba2bf1b71877f02e01c36a 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -428,12 +428,9 @@ pub async fn new_test_thread( .await .unwrap(); - let thread = cx - .update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx)) + cx.update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx)) .await - .unwrap(); - - thread + .unwrap() } pub async fn run_until_first_tool_call( diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index e7f0d4f88fd85fc6e6bd643d2e5d9ad050d019dd..311fe258de9e035be95697bee9a5e9b4d6851ffa 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -134,8 +134,8 @@ impl MessageEditor { if prevent_slash_commands { subscriptions.push(cx.subscribe_in(&editor, window, { let semantics_provider = semantics_provider.clone(); - move |this, editor, event, window, cx| match event { - EditorEvent::Edited { .. } => { + move |this, editor, event, window, cx| { + if let EditorEvent::Edited { .. } = event { this.highlight_slash_command( semantics_provider.clone(), editor.clone(), @@ -143,7 +143,6 @@ impl MessageEditor { cx, ); } - _ => {} } })); } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 7b38ba93015fa8616b62780f91af60f4c5eeedd7..9f1e8d857fb772b5dd720986c8dc85323943f94f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2124,7 +2124,7 @@ impl AcpThreadView { .map(|view| div().px_4().w_full().max_w_128().child(view)), ) .child(h_flex().mt_1p5().justify_center().children( - connection.auth_methods().into_iter().map(|method| { + connection.auth_methods().iter().map(|method| { Button::new(SharedString::from(method.id.0.clone()), method.name.clone()) .on_click({ let method_id = method.id.clone(); @@ -2574,7 +2574,7 @@ impl AcpThreadView { ) -> Div { let editor_bg_color = cx.theme().colors().editor_background; - v_flex().children(changed_buffers.into_iter().enumerate().flat_map( + v_flex().children(changed_buffers.iter().enumerate().flat_map( |(index, (buffer, _diff))| { let file = buffer.read(cx).file()?; let path = file.path(); diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index a1e51f883a4359734c8c6de8c98ec33051de9e83..e595b94ebba9844390a06f4cc54bb8dbaf9e09c5 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1373,12 +1373,12 @@ impl ActiveThread { editor.focus_handle(cx).focus(window); editor.move_to_end(&editor::actions::MoveToEnd, window, cx); }); - let buffer_edited_subscription = cx.subscribe(&editor, |this, _, event, cx| match event { - EditorEvent::BufferEdited => { - this.update_editing_message_token_count(true, cx); - } - _ => {} - }); + let buffer_edited_subscription = + cx.subscribe(&editor, |this, _, event: &EditorEvent, cx| { + if event == &EditorEvent::BufferEdited { + this.update_editing_message_token_count(true, cx); + } + }); let context_picker_menu_handle = PopoverMenuHandle::default(); let context_strip = cx.new(|cx| { diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index b032201d8c72443697b154cb0338e96a496d0340..ecb0bca4a1571d94558a87260b638d351b93b9b6 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -958,7 +958,7 @@ impl AgentConfiguration { } parent.child(v_flex().py_1p5().px_1().gap_1().children( - tools.into_iter().enumerate().map(|(ix, tool)| { + tools.iter().enumerate().map(|(ix, tool)| { h_flex() .id(("tool-item", ix)) .px_1() diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index 311f75af3ba3f85e2db2193d8a739c08b3e37c89..6159b9be8096f79f4e463249682b2f55c7af5fec 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -487,7 +487,7 @@ impl ConfigureContextServerModal { } fn render_modal_description(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement { - const MODAL_DESCRIPTION: &'static str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables."; + const MODAL_DESCRIPTION: &str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables."; if let ConfigurationSource::Extension { installation_instructions: Some(installation_instructions), diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index e80cd2084693447e5d6b7bc84820f42aab8f3590..9d2ee0bf89ba3236f563a2374c37ee3b3e354b4a 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -322,16 +322,14 @@ impl AgentDiffPane { } fn handle_native_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) { - match event { - ThreadEvent::SummaryGenerated => self.update_title(cx), - _ => {} + if let ThreadEvent::SummaryGenerated = event { + self.update_title(cx) } } fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context<Self>) { - match event { - AcpThreadEvent::TitleUpdated => self.update_title(cx), - _ => {} + if let AcpThreadEvent::TitleUpdated = event { + self.update_title(cx) } } @@ -1541,15 +1539,11 @@ impl AgentDiff { window: &mut Window, cx: &mut Context<Self>, ) { - match event { - workspace::Event::ItemAdded { item } => { - if let Some(editor) = item.downcast::<Editor>() - && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) - { - self.register_editor(workspace.downgrade(), buffer.clone(), editor, window, cx); - } - } - _ => {} + if let workspace::Event::ItemAdded { item } = event + && let Some(editor) = item.downcast::<Editor>() + && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) + { + self.register_editor(workspace.downgrade(), buffer.clone(), editor, window, cx); } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index c79349e3a9e9467780226d956e89310efd01ae57..c5cab340302b16a06f0eb63fe70c5cb8a4dafa82 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -354,7 +354,7 @@ impl ActiveView { Self::Thread { change_title_editor: editor, thread: active_thread, - message_editor: message_editor, + message_editor, _subscriptions: subscriptions, } } @@ -756,25 +756,25 @@ impl AgentPanel { .ok(); }); - let _default_model_subscription = cx.subscribe( - &LanguageModelRegistry::global(cx), - |this, _, event: &language_model::Event, cx| match event { - language_model::Event::DefaultModelChanged => match &this.active_view { - ActiveView::Thread { thread, .. } => { - thread - .read(cx) - .thread() - .clone() - .update(cx, |thread, cx| thread.get_or_init_configured_model(cx)); + let _default_model_subscription = + cx.subscribe( + &LanguageModelRegistry::global(cx), + |this, _, event: &language_model::Event, cx| { + if let language_model::Event::DefaultModelChanged = event { + match &this.active_view { + ActiveView::Thread { thread, .. } => { + thread.read(cx).thread().clone().update(cx, |thread, cx| { + thread.get_or_init_configured_model(cx) + }); + } + ActiveView::ExternalAgentThread { .. } + | ActiveView::TextThread { .. } + | ActiveView::History + | ActiveView::Configuration => {} + } } - ActiveView::ExternalAgentThread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => {} }, - _ => {} - }, - ); + ); let onboarding = cx.new(|cx| { AgentPanelOnboarding::new( @@ -1589,17 +1589,14 @@ impl AgentPanel { let current_is_special = current_is_history || current_is_config; let new_is_special = new_is_history || new_is_config; - match &self.active_view { - ActiveView::Thread { thread, .. } => { - let thread = thread.read(cx); - if thread.is_empty() { - let id = thread.thread().read(cx).id().clone(); - self.history_store.update(cx, |store, cx| { - store.remove_recently_opened_thread(id, cx); - }); - } + if let ActiveView::Thread { thread, .. } = &self.active_view { + let thread = thread.read(cx); + if thread.is_empty() { + let id = thread.thread().read(cx).id().clone(); + self.history_store.update(cx, |store, cx| { + store.remove_recently_opened_thread(id, cx); + }); } - _ => {} } match &new_view { @@ -3465,7 +3462,7 @@ impl AgentPanel { .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| { let tasks = paths .paths() - .into_iter() + .iter() .map(|path| { Workspace::project_path_for_path(this.project.clone(), path, false, cx) }) diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 697f704991a3099e6f135c58635144edbc8794ad..0b4568dc87ddb33d5640c0313402c2a599be22d1 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -385,12 +385,11 @@ impl ContextPicker { } pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) { - match &self.mode { - ContextPickerState::Default(entity) => entity.update(cx, |entity, cx| { + // Other variants already select their first entry on open automatically + if let ContextPickerState::Default(entity) = &self.mode { + entity.update(cx, |entity, cx| { entity.select_first(&Default::default(), window, cx) - }), - // Other variants already select their first entry on open automatically - _ => {} + }) } } diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 6e4d2638c1fa4c3592d55c9be250c26730ee46e5..f70d10c1ae63f1ea4a15b6685baa15bb23cf40ae 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -117,7 +117,7 @@ pub(crate) fn create_editor( let mut editor = Editor::new( editor::EditorMode::AutoHeight { min_lines, - max_lines: max_lines, + max_lines, }, buffer, None, @@ -215,9 +215,10 @@ impl MessageEditor { let subscriptions = vec![ cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event), - cx.subscribe(&editor, |this, _, event, cx| match event { - EditorEvent::BufferEdited => this.handle_message_changed(cx), - _ => {} + cx.subscribe(&editor, |this, _, event: &EditorEvent, cx| { + if event == &EditorEvent::BufferEdited { + this.handle_message_changed(cx) + } }), cx.observe(&context_store, |this, _, cx| { // When context changes, reload it for token counting. @@ -1132,7 +1133,7 @@ impl MessageEditor { ) .when(is_edit_changes_expanded, |parent| { parent.child( - v_flex().children(changed_buffers.into_iter().enumerate().flat_map( + v_flex().children(changed_buffers.iter().enumerate().flat_map( |(index, (buffer, _diff))| { let file = buffer.read(cx).file()?; let path = file.path(); @@ -1605,7 +1606,8 @@ pub fn extract_message_creases( .collect::<HashMap<_, _>>(); // Filter the addon's list of creases based on what the editor reports, // since the addon might have removed creases in it. - let creases = editor.display_map.update(cx, |display_map, cx| { + + editor.display_map.update(cx, |display_map, cx| { display_map .snapshot(cx) .crease_snapshot @@ -1629,8 +1631,7 @@ pub fn extract_message_creases( } }) .collect() - }); - creases + }) } impl EventEmitter<MessageEditorEvent> for MessageEditor {} diff --git a/crates/agent_ui/src/slash_command_picker.rs b/crates/agent_ui/src/slash_command_picker.rs index 03f2c97887375ad349b603ac30d876514ee5a4a1..a6bb61510cbeb557e22018c73082bba17d177d7e 100644 --- a/crates/agent_ui/src/slash_command_picker.rs +++ b/crates/agent_ui/src/slash_command_picker.rs @@ -327,9 +327,7 @@ where }; let picker_view = cx.new(|cx| { - let picker = - Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into())); - picker + Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into())) }); let handle = self diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index b7e5d83d6d842b8463b3a856ed7e557bdb2ac001..b3f55ffc43e9cbaf11cce6ebc1b38077098c9fc3 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -540,7 +540,7 @@ impl TextThreadEditor { let context = self.context.read(cx); let sections = context .slash_command_output_sections() - .into_iter() + .iter() .filter(|section| section.is_valid(context.buffer().read(cx))) .cloned() .collect::<Vec<_>>(); @@ -1237,7 +1237,7 @@ impl TextThreadEditor { let mut new_blocks = vec![]; let mut block_index_to_message = vec![]; for message in self.context.read(cx).messages(cx) { - if let Some(_) = blocks_to_remove.remove(&message.id) { + if blocks_to_remove.remove(&message.id).is_some() { // This is an old message that we might modify. let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else { debug_assert!( @@ -1275,7 +1275,7 @@ impl TextThreadEditor { context_editor_view: &Entity<TextThreadEditor>, cx: &mut Context<Workspace>, ) -> Option<(String, bool)> { - const CODE_FENCE_DELIMITER: &'static str = "```"; + const CODE_FENCE_DELIMITER: &str = "```"; let context_editor = context_editor_view.read(cx).editor.clone(); context_editor.update(cx, |context_editor, cx| { @@ -2161,8 +2161,8 @@ impl TextThreadEditor { /// Returns the contents of the *outermost* fenced code block that contains the given offset. fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> { - const CODE_BLOCK_NODE: &'static str = "fenced_code_block"; - const CODE_BLOCK_CONTENT: &'static str = "code_fence_content"; + const CODE_BLOCK_NODE: &str = "fenced_code_block"; + const CODE_BLOCK_CONTENT: &str = "code_fence_content"; let layer = snapshot.syntax_layers().next()?; @@ -3129,7 +3129,7 @@ mod tests { let context_editor = window .update(&mut cx, |_, window, cx| { cx.new(|cx| { - let editor = TextThreadEditor::for_context( + TextThreadEditor::for_context( context.clone(), fs, workspace.downgrade(), @@ -3137,8 +3137,7 @@ mod tests { None, window, cx, - ); - editor + ) }) }) .unwrap(); diff --git a/crates/agent_ui/src/tool_compatibility.rs b/crates/agent_ui/src/tool_compatibility.rs index d4e1da5bb0a532c8307364582349378d98c51a26..046c0a4abc5e3ac0130b2af5406cfbb3f977b00c 100644 --- a/crates/agent_ui/src/tool_compatibility.rs +++ b/crates/agent_ui/src/tool_compatibility.rs @@ -14,13 +14,11 @@ pub struct IncompatibleToolsState { impl IncompatibleToolsState { pub fn new(thread: Entity<Thread>, cx: &mut Context<Self>) -> Self { - let _tool_working_set_subscription = - cx.subscribe(&thread, |this, _, event, _| match event { - ThreadEvent::ProfileChanged => { - this.cache.clear(); - } - _ => {} - }); + let _tool_working_set_subscription = cx.subscribe(&thread, |this, _, event, _| { + if let ThreadEvent::ProfileChanged = event { + this.cache.clear(); + } + }); Self { cache: HashMap::default(), diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 2d71a1c08a7cff3de8db2608d762248d2b96c6b4..4d0bfae44443098abcac610529e5fcfe1cf778a8 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -590,7 +590,7 @@ impl From<&Message> for MessageMetadata { impl MessageMetadata { pub fn is_cache_valid(&self, buffer: &BufferSnapshot, range: &Range<usize>) -> bool { - let result = match &self.cache { + match &self.cache { Some(MessageCacheMetadata { cached_at, .. }) => !buffer.has_edits_since_in_range( cached_at, Range { @@ -599,8 +599,7 @@ impl MessageMetadata { }, ), _ => false, - }; - result + } } } @@ -2081,15 +2080,12 @@ impl AssistantContext { match event { LanguageModelCompletionEvent::StatusUpdate(status_update) => { - match status_update { - CompletionRequestStatus::UsageUpdated { amount, limit } => { - this.update_model_request_usage( - amount as u32, - limit, - cx, - ); - } - _ => {} + if let CompletionRequestStatus::UsageUpdated { amount, limit } = status_update { + this.update_model_request_usage( + amount as u32, + limit, + cx, + ); } } LanguageModelCompletionEvent::StartMessage { .. } => {} diff --git a/crates/assistant_slash_commands/src/file_command.rs b/crates/assistant_slash_commands/src/file_command.rs index 68751899272afa2ee2ef03541d99c5debf5a94d2..894aa94a272f63c72b346b3bc537a5663ee02e24 100644 --- a/crates/assistant_slash_commands/src/file_command.rs +++ b/crates/assistant_slash_commands/src/file_command.rs @@ -223,7 +223,7 @@ fn collect_files( cx: &mut App, ) -> impl Stream<Item = Result<SlashCommandEvent>> + use<> { let Ok(matchers) = glob_inputs - .into_iter() + .iter() .map(|glob_input| { custom_path_matcher::PathMatcher::new(&[glob_input.to_owned()]) .with_context(|| format!("invalid path {glob_input}")) @@ -379,7 +379,7 @@ fn collect_files( } } - while let Some(_) = directory_stack.pop() { + while directory_stack.pop().is_some() { events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?; } } @@ -491,7 +491,7 @@ mod custom_path_matcher { impl PathMatcher { pub fn new(globs: &[String]) -> Result<Self, globset::Error> { let globs = globs - .into_iter() + .iter() .map(|glob| Glob::new(&SanitizedPath::from(glob).to_glob_string())) .collect::<Result<Vec<_>, _>>()?; let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect(); diff --git a/crates/assistant_tool/src/tool_working_set.rs b/crates/assistant_tool/src/tool_working_set.rs index c0a358917b499908d85fbc157212cf6db5b5e0eb..61f57affc76aad9e4d2185665b539f9092e3491c 100644 --- a/crates/assistant_tool/src/tool_working_set.rs +++ b/crates/assistant_tool/src/tool_working_set.rs @@ -156,13 +156,13 @@ fn resolve_context_server_tool_name_conflicts( if duplicated_tool_names.is_empty() { return context_server_tools - .into_iter() + .iter() .map(|tool| (resolve_tool_name(tool).into(), tool.clone())) .collect(); } context_server_tools - .into_iter() + .iter() .filter_map(|tool| { let mut tool_name = resolve_tool_name(tool); if !duplicated_tool_names.contains(&tool_name) { diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index f381103c278b83c87aeeb59c06939797f97c7067..ce3b639cb2c46d3f736490c0b2153260f970963c 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -72,11 +72,10 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) { register_web_search_tool(&LanguageModelRegistry::global(cx), cx); cx.subscribe( &LanguageModelRegistry::global(cx), - move |registry, event, cx| match event { - language_model::Event::DefaultModelChanged => { + move |registry, event, cx| { + if let language_model::Event::DefaultModelChanged = event { register_web_search_tool(®istry, cx); } - _ => {} }, ) .detach(); diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 2d6b5ce92450ccd2e59c69538bcc65d3f3644bc9..33d08b4f88ba523254ccaf593019ea8692d19bdf 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -1356,8 +1356,7 @@ mod tests { mode: mode.clone(), }; - let result = cx.update(|cx| resolve_path(&input, project, cx)); - result + cx.update(|cx| resolve_path(&input, project, cx)) } fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) { diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index dd0a0c8e4c34b8e7f5f7a7dc9768c188dc66ed8a..14bbcef8b4c8f1f6992db82ed04e5e9a6b1e8212 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -216,7 +216,8 @@ impl Tool for TerminalTool { async move |cx| { let program = program.await; let env = env.await; - let terminal = project + + project .update(cx, |project, cx| { project.create_terminal( TerminalKind::Task(task::SpawnInTerminal { @@ -229,8 +230,7 @@ impl Tool for TerminalTool { cx, ) })? - .await; - terminal + .await } }); diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 57890628f2893e3a8e47105cc23aea14079ce307..925d5ddefbb1c892686fb36e5f34a10c967b13f2 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -494,11 +494,11 @@ mod linux { Ok(Fork::Parent(_)) => Ok(()), Ok(Fork::Child) => { unsafe { std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "") }; - if let Err(_) = fork::setsid() { + if fork::setsid().is_err() { eprintln!("failed to setsid: {}", std::io::Error::last_os_error()); process::exit(1); } - if let Err(_) = fork::close_fd() { + if fork::close_fd().is_err() { eprintln!("failed to close_fd: {}", std::io::Error::last_os_error()); } let error = @@ -534,8 +534,8 @@ mod flatpak { use std::process::Command; use std::{env, process}; - const EXTRA_LIB_ENV_NAME: &'static str = "ZED_FLATPAK_LIB_PATH"; - const NO_ESCAPE_ENV_NAME: &'static str = "ZED_FLATPAK_NO_ESCAPE"; + const EXTRA_LIB_ENV_NAME: &str = "ZED_FLATPAK_LIB_PATH"; + const NO_ESCAPE_ENV_NAME: &str = "ZED_FLATPAK_NO_ESCAPE"; /// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak pub fn ld_extra_libs() { diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 5a2c40b890cfe32510347c33a1257af4cbea0768..930e635dd806475d3488732fe0fc1db19debcee0 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4970,7 +4970,7 @@ async fn test_references( "Rust", FakeLspAdapter { name: "my-fake-lsp-adapter", - capabilities: capabilities, + capabilities, ..FakeLspAdapter::default() }, ); diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 57f63412971e844d5e98f2c58a5f5ad03d6eda30..5fead5bcf10cc62dc6f60414978366cb2eac313b 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -397,11 +397,10 @@ impl MessageEditor { ) -> Option<(Anchor, String, &'static [StringMatchCandidate])> { static EMOJI_FUZZY_MATCH_CANDIDATES: LazyLock<Vec<StringMatchCandidate>> = LazyLock::new(|| { - let emojis = emojis::iter() + emojis::iter() .flat_map(|s| s.shortcodes()) .map(|emoji| StringMatchCandidate::new(0, emoji)) - .collect::<Vec<_>>(); - emojis + .collect::<Vec<_>>() }); let end_offset = end_anchor.to_offset(buffer.read(cx)); diff --git a/crates/context_server/src/listener.rs b/crates/context_server/src/listener.rs index f3c199a14e3ce42511c24c8dadfd93b2b271b87e..6f4b5c13695a3dc90325c4b09f769d98b667d8af 100644 --- a/crates/context_server/src/listener.rs +++ b/crates/context_server/src/listener.rs @@ -77,7 +77,7 @@ impl McpServer { socket_path, _server_task: server_task, tools, - handlers: handlers, + handlers, }) }) } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 6e80ec484c20bde6618bfde0199aa170b44f6514..614cd0e05d1821539c74eb4e78321fb1e0c29445 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -238,7 +238,7 @@ impl PythonDebugAdapter { return Err("Failed to create base virtual environment".into()); } - const DIR: &'static str = if cfg!(target_os = "windows") { + const DIR: &str = if cfg!(target_os = "windows") { "Scripts" } else { "bin" diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 4e1b0d19e250638a84589ab8bfc210677c386406..6c70a935e023dd0b3c7918f4edd3c3926f31f464 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -257,7 +257,7 @@ impl DebugPanel { .as_ref() .map(|entity| entity.downgrade()), task_context: task_context.clone(), - worktree_id: worktree_id, + worktree_id, }); }; running.resolve_scenario( diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 73cfef78cc6410196441ff974f09b5abe3d86916..0fc003a14dd9ac51c2608df86644a628a44b3e8e 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -87,7 +87,7 @@ impl DebugSession { self.stack_trace_view.get_or_init(|| { let stackframe_list = running_state.read(cx).stack_frame_list().clone(); - let stack_frame_view = cx.new(|cx| { + cx.new(|cx| { StackTraceView::new( workspace.clone(), project.clone(), @@ -95,9 +95,7 @@ impl DebugSession { window, cx, ) - }); - - stack_frame_view + }) }) } diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 449deb4ddbd74ada3fba7e07302e6a744f7bd6f8..e3682ac991cbb46ed41a7c51e30d48df12bfad8e 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -358,7 +358,7 @@ pub(crate) fn new_debugger_pane( } }; - let ret = cx.new(move |cx| { + cx.new(move |cx| { let mut pane = Pane::new( workspace.clone(), project.clone(), @@ -562,9 +562,7 @@ pub(crate) fn new_debugger_pane( } }); pane - }); - - ret + }) } pub struct DebugTerminal { diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 26a26c7bef565224eb2a6de1b2e628ab8fbcec69..c17fffc42cfde97d2657eb481c28396c71c65c9f 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -329,8 +329,8 @@ impl BreakpointList { let text = self.input.read(cx).text(cx); match mode { - ActiveBreakpointStripMode::Log => match &entry.kind { - BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { + ActiveBreakpointStripMode::Log => { + if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind { Self::edit_line_breakpoint_inner( &self.breakpoint_store, line_breakpoint.breakpoint.path.clone(), @@ -339,10 +339,9 @@ impl BreakpointList { cx, ); } - _ => {} - }, - ActiveBreakpointStripMode::Condition => match &entry.kind { - BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { + } + ActiveBreakpointStripMode::Condition => { + if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind { Self::edit_line_breakpoint_inner( &self.breakpoint_store, line_breakpoint.breakpoint.path.clone(), @@ -351,10 +350,9 @@ impl BreakpointList { cx, ); } - _ => {} - }, - ActiveBreakpointStripMode::HitCondition => match &entry.kind { - BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { + } + ActiveBreakpointStripMode::HitCondition => { + if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind { Self::edit_line_breakpoint_inner( &self.breakpoint_store, line_breakpoint.breakpoint.path.clone(), @@ -363,8 +361,7 @@ impl BreakpointList { cx, ); } - _ => {} - }, + } } self.focus_handle.focus(window); } else { @@ -426,13 +423,10 @@ impl BreakpointList { return; }; - match &mut entry.kind { - BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { - let path = line_breakpoint.breakpoint.path.clone(); - let row = line_breakpoint.breakpoint.row; - self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx); - } - _ => {} + if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &mut entry.kind { + let path = line_breakpoint.breakpoint.path.clone(); + let row = line_breakpoint.breakpoint.row; + self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx); } cx.notify(); } @@ -967,7 +961,7 @@ impl LineBreakpoint { props, breakpoint: BreakpointEntry { kind: BreakpointEntryKind::LineBreakpoint(self.clone()), - weak: weak, + weak, }, is_selected, focus_handle, @@ -1179,7 +1173,7 @@ impl ExceptionBreakpoint { props, breakpoint: BreakpointEntry { kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()), - weak: weak, + weak, }, is_selected, focus_handle, diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index 3cc5fbc272bd3c59ba1bb6d35d37bfc3964e4d9e..7461bffdf94625bea545ab4141906ac3d3a8b8c7 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -947,7 +947,7 @@ impl VariableList { #[track_caller] #[cfg(test)] pub(crate) fn assert_visual_entries(&self, expected: Vec<&str>) { - const INDENT: &'static str = " "; + const INDENT: &str = " "; let entries = &self.entries; let mut visual_entries = Vec::with_capacity(entries.len()); diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index fbbd52964105659c2cae645cec494824069f5529..4cfdae093f6a1464b178c053e629a6ebe6d76d02 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -1445,11 +1445,8 @@ async fn test_variable_list_only_sends_requests_when_rendering( cx.run_until_parked(); - let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| { - let state = item.running_state().clone(); - - state - }); + let running_state = active_debug_session_panel(workspace, cx) + .update_in(cx, |item, _, _| item.running_state().clone()); client .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 6ac0f49fada427f84b55aae11bc56cb08f39a62b..99e588ada9a70d59b3ba41c92323854b4814e112 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -21,7 +21,7 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| { static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions); -const FRONT_MATTER_COMMENT: &'static str = "<!-- ZED_META {} -->"; +const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->"; fn main() -> Result<()> { zlog::init(); @@ -105,8 +105,8 @@ fn handle_preprocessing() -> Result<()> { template_and_validate_actions(&mut book, &mut errors); if !errors.is_empty() { - const ANSI_RED: &'static str = "\x1b[31m"; - const ANSI_RESET: &'static str = "\x1b[0m"; + const ANSI_RED: &str = "\x1b[31m"; + const ANSI_RESET: &str = "\x1b[0m"; for error in &errors { eprintln!("{ANSI_RED}ERROR{ANSI_RESET}: {}", error); } @@ -143,11 +143,8 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>) &serde_json::to_string(&metadata).expect("Failed to serialize metadata"), ) }); - match new_content { - Cow::Owned(content) => { - chapter.content = content; - } - Cow::Borrowed(_) => {} + if let Cow::Owned(content) = new_content { + chapter.content = content; } }); } @@ -409,13 +406,13 @@ fn handle_postprocessing() -> Result<()> { .captures(contents) .with_context(|| format!("Failed to find title in {:?}", pretty_path)) .expect("Page has <title> element")[1]; - let title = title_tag_contents + + title_tag_contents .trim() .strip_suffix("- Zed") .unwrap_or(title_tag_contents) .trim() - .to_string(); - title + .to_string() } } diff --git a/crates/editor/src/display_map/invisibles.rs b/crates/editor/src/display_map/invisibles.rs index 19e4c2b42aa32448234fa2219c8cc656431e781e..0712ddf9e2e53c22081c6fa63ebb4baeced37f78 100644 --- a/crates/editor/src/display_map/invisibles.rs +++ b/crates/editor/src/display_map/invisibles.rs @@ -61,14 +61,14 @@ pub fn replacement(c: char) -> Option<&'static str> { // but could if we tracked state in the classifier. const IDEOGRAPHIC_SPACE: char = '\u{3000}'; -const C0_SYMBOLS: &'static [&'static str] = &[ +const C0_SYMBOLS: &[&str] = &[ "␀", "␁", "␂", "␃", "␄", "␅", "␆", "␇", "␈", "␉", "␊", "␋", "␌", "␍", "␎", "␏", "␐", "␑", "␒", "␓", "␔", "␕", "␖", "␗", "␘", "␙", "␚", "␛", "␜", "␝", "␞", "␟", ]; -const DEL: &'static str = "␡"; +const DEL: &str = "␡"; // generated using ucd-generate: ucd-generate general-category --include Format --chars ucd-16.0.0 -pub const FORMAT: &'static [(char, char)] = &[ +pub const FORMAT: &[(char, char)] = &[ ('\u{ad}', '\u{ad}'), ('\u{600}', '\u{605}'), ('\u{61c}', '\u{61c}'), @@ -93,7 +93,7 @@ pub const FORMAT: &'static [(char, char)] = &[ ]; // hand-made base on https://invisible-characters.com (Excluding Cf) -pub const OTHER: &'static [(char, char)] = &[ +pub const OTHER: &[(char, char)] = &[ ('\u{034f}', '\u{034f}'), ('\u{115F}', '\u{1160}'), ('\u{17b4}', '\u{17b5}'), @@ -107,7 +107,7 @@ pub const OTHER: &'static [(char, char)] = &[ ]; // a subset of FORMAT/OTHER that may appear within glyphs -const PRESERVE: &'static [(char, char)] = &[ +const PRESERVE: &[(char, char)] = &[ ('\u{034f}', '\u{034f}'), ('\u{200d}', '\u{200d}'), ('\u{17b4}', '\u{17b5}'), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7c36a410468da4a07d6a02d4038b2c9d048ae7f7..38059042439cb7c2610e4a73828796d3f8c6d434 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1943,26 +1943,24 @@ impl Editor { let git_store = project.read(cx).git_store().clone(); let project = project.clone(); project_subscriptions.push(cx.subscribe(&git_store, move |this, _, event, cx| { - match event { - GitStoreEvent::RepositoryUpdated( - _, - RepositoryEvent::Updated { - new_instance: true, .. - }, - _, - ) => { - this.load_diff_task = Some( - update_uncommitted_diff_for_buffer( - cx.entity(), - &project, - this.buffer.read(cx).all_buffers(), - this.buffer.clone(), - cx, - ) - .shared(), - ); - } - _ => {} + if let GitStoreEvent::RepositoryUpdated( + _, + RepositoryEvent::Updated { + new_instance: true, .. + }, + _, + ) = event + { + this.load_diff_task = Some( + update_uncommitted_diff_for_buffer( + cx.entity(), + &project, + this.buffer.read(cx).all_buffers(), + this.buffer.clone(), + cx, + ) + .shared(), + ); } })); } @@ -3221,35 +3219,31 @@ impl Editor { selections.select_anchors(other_selections); }); - let other_subscription = - cx.subscribe(&other, |this, other, other_evt, cx| match other_evt { - EditorEvent::SelectionsChanged { local: true } => { - let other_selections = other.read(cx).selections.disjoint.to_vec(); - if other_selections.is_empty() { - return; - } - this.selections.change_with(cx, |selections| { - selections.select_anchors(other_selections); - }); + let other_subscription = cx.subscribe(&other, |this, other, other_evt, cx| { + if let EditorEvent::SelectionsChanged { local: true } = other_evt { + let other_selections = other.read(cx).selections.disjoint.to_vec(); + if other_selections.is_empty() { + return; } - _ => {} - }); + this.selections.change_with(cx, |selections| { + selections.select_anchors(other_selections); + }); + } + }); - let this_subscription = - cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| match this_evt { - EditorEvent::SelectionsChanged { local: true } => { - let these_selections = this.selections.disjoint.to_vec(); - if these_selections.is_empty() { - return; - } - other.update(cx, |other_editor, cx| { - other_editor.selections.change_with(cx, |selections| { - selections.select_anchors(these_selections); - }) - }); + let this_subscription = cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| { + if let EditorEvent::SelectionsChanged { local: true } = this_evt { + let these_selections = this.selections.disjoint.to_vec(); + if these_selections.is_empty() { + return; } - _ => {} - }); + other.update(cx, |other_editor, cx| { + other_editor.selections.change_with(cx, |selections| { + selections.select_anchors(these_selections); + }) + }); + } + }); Subscription::join(other_subscription, this_subscription) } @@ -5661,34 +5655,31 @@ impl Editor { let Ok(()) = editor.update_in(cx, |editor, window, cx| { // Newer menu already set, so exit. - match editor.context_menu.borrow().as_ref() { - Some(CodeContextMenu::Completions(prev_menu)) => { - if prev_menu.id > id { - return; - } - } - _ => {} + if let Some(CodeContextMenu::Completions(prev_menu)) = + editor.context_menu.borrow().as_ref() + && prev_menu.id > id + { + return; }; // Only valid to take prev_menu because it the new menu is immediately set // below, or the menu is hidden. - match editor.context_menu.borrow_mut().take() { - Some(CodeContextMenu::Completions(prev_menu)) => { - let position_matches = - if prev_menu.initial_position == menu.initial_position { - true - } else { - let snapshot = editor.buffer.read(cx).read(cx); - prev_menu.initial_position.to_offset(&snapshot) - == menu.initial_position.to_offset(&snapshot) - }; - if position_matches { - // Preserve markdown cache before `set_filter_results` because it will - // try to populate the documentation cache. - menu.preserve_markdown_cache(prev_menu); - } + if let Some(CodeContextMenu::Completions(prev_menu)) = + editor.context_menu.borrow_mut().take() + { + let position_matches = + if prev_menu.initial_position == menu.initial_position { + true + } else { + let snapshot = editor.buffer.read(cx).read(cx); + prev_menu.initial_position.to_offset(&snapshot) + == menu.initial_position.to_offset(&snapshot) + }; + if position_matches { + // Preserve markdown cache before `set_filter_results` because it will + // try to populate the documentation cache. + menu.preserve_markdown_cache(prev_menu); } - _ => {} }; menu.set_filter_results(matches, provider, window, cx); @@ -6179,12 +6170,11 @@ impl Editor { } }); Some(cx.background_spawn(async move { - let scenarios = futures::future::join_all(scenarios) + futures::future::join_all(scenarios) .await .into_iter() .flatten() - .collect::<Vec<_>>(); - scenarios + .collect::<Vec<_>>() })) }) .unwrap_or_else(|| Task::ready(vec![])) @@ -7740,12 +7730,9 @@ impl Editor { self.edit_prediction_settings = self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx); - match self.edit_prediction_settings { - EditPredictionSettings::Disabled => { - self.discard_edit_prediction(false, cx); - return None; - } - _ => {} + if let EditPredictionSettings::Disabled = self.edit_prediction_settings { + self.discard_edit_prediction(false, cx); + return None; }; self.edit_prediction_indent_conflict = multibuffer.is_line_whitespace_upto(cursor); @@ -10638,8 +10625,7 @@ impl Editor { .buffer_snapshot .anchor_after(Point::new(row, line_len)); - let bp = self - .breakpoint_store + self.breakpoint_store .as_ref()? .read_with(cx, |breakpoint_store, cx| { breakpoint_store @@ -10664,8 +10650,7 @@ impl Editor { None } }) - }); - bp + }) } pub fn edit_log_breakpoint( @@ -10701,7 +10686,7 @@ impl Editor { let cursors = self .selections .disjoint_anchors() - .into_iter() + .iter() .map(|selection| { let cursor_position: Point = selection.head().to_point(&snapshot.buffer_snapshot); @@ -14878,7 +14863,7 @@ impl Editor { let start = parent.start - offset; offset += parent.len() - text.len(); selections.push(Selection { - id: id, + id, start, end: start + text.len(), reversed: false, @@ -19202,7 +19187,7 @@ impl Editor { let locations = self .selections .all_anchors(cx) - .into_iter() + .iter() .map(|selection| Location { buffer: buffer.clone(), range: selection.start.text_anchor..selection.end.text_anchor, @@ -19914,11 +19899,8 @@ impl Editor { event: &SessionEvent, cx: &mut Context<Self>, ) { - match event { - SessionEvent::InvalidateInlineValue => { - self.refresh_inline_values(cx); - } - _ => {} + if let SessionEvent::InvalidateInlineValue = event { + self.refresh_inline_values(cx); } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 685cc47cdbc6f75ae6dbd6ca621b5cebf616912e..1f1239ba0a07891114489fe103b3b0e5020d562a 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -21037,7 +21037,7 @@ fn assert_breakpoint( let mut breakpoint = breakpoints .get(path) .unwrap() - .into_iter() + .iter() .map(|breakpoint| { ( breakpoint.row, @@ -23622,7 +23622,7 @@ pub fn handle_completion_request( complete_from_position ); Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList { - is_incomplete: is_incomplete, + is_incomplete, item_defaults: None, items: completions .iter() diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c14e49fc1d2c52c8285f56538de6dd7b3d368a3f..f1ebd2c3dff99c4b555e4cf57abb37d568153b71 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -724,7 +724,7 @@ impl EditorElement { ColumnarMode::FromMouse => true, ColumnarMode::FromSelection => false, }, - mode: mode, + mode, goal_column: point_for_position.exact_unclipped.column(), }, window, @@ -2437,14 +2437,13 @@ impl EditorElement { .unwrap_or_default() .padding as f32; - 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, - _ => {} - } + if let Some(edit_prediction) = editor.active_edit_prediction.as_ref() + && let EditPrediction::Edit { + display_mode: EditDisplayMode::TabAccept, + .. + } = &edit_prediction.completion + { + padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS } padding * em_width @@ -2978,8 +2977,8 @@ impl EditorElement { .ilog10() + 1; - let elements = buffer_rows - .into_iter() + buffer_rows + .iter() .enumerate() .map(|(ix, row_info)| { let ExpandInfo { @@ -3034,9 +3033,7 @@ impl EditorElement { Some((toggle, origin)) }) - .collect(); - - elements + .collect() } fn calculate_relative_line_numbers( @@ -3136,7 +3133,7 @@ impl EditorElement { let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to); let mut line_number = String::new(); let line_numbers = buffer_rows - .into_iter() + .iter() .enumerate() .flat_map(|(ix, row_info)| { let display_row = DisplayRow(rows.start.0 + ix as u32); @@ -3213,7 +3210,7 @@ impl EditorElement { && self.editor.read(cx).is_singleton(cx); if include_fold_statuses { row_infos - .into_iter() + .iter() .enumerate() .map(|(ix, info)| { if info.expand_info.is_some() { diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 2f6106c86cda709f58c49daae713a56a6cbb0f68..b11617ccec22f08737ba40ab257e19a81eddfd89 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -213,8 +213,8 @@ impl GitBlame { let project_subscription = cx.subscribe(&project, { let buffer = buffer.clone(); - move |this, _, event, cx| match event { - project::Event::WorktreeUpdatedEntries(_, updated) => { + move |this, _, event, cx| { + if let project::Event::WorktreeUpdatedEntries(_, updated) = event { let project_entry_id = buffer.read(cx).entry_id(cx); if updated .iter() @@ -224,7 +224,6 @@ impl GitBlame { this.generate(cx); } } - _ => {} } }); @@ -292,7 +291,7 @@ impl GitBlame { let buffer_id = self.buffer_snapshot.remote_id(); let mut cursor = self.entries.cursor::<u32>(&()); - rows.into_iter().map(move |info| { + rows.iter().map(move |info| { let row = info .buffer_row .filter(|_| info.buffer_id == Some(buffer_id))?; diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index a8cdfa99df4136f1c79821abf31c6ba7932cc891..bb3fd2830da21791444effa954ca28d0eb819dd1 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -603,18 +603,15 @@ async fn parse_blocks( }) .join("\n\n"); - let rendered_block = cx - .new_window_entity(|_window, cx| { - Markdown::new( - combined_text.into(), - language_registry.cloned(), - language.map(|language| language.name()), - cx, - ) - }) - .ok(); - - rendered_block + cx.new_window_entity(|_window, cx| { + Markdown::new( + combined_text.into(), + language_registry.cloned(), + language.map(|language| language.name()), + cx, + ) + }) + .ok() } pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index e3d2f92c553a2f446cf837615da39a6d21d0b876..8957e0e99c4f39406e8c5baabcaa2d23019e9b0c 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1009,16 +1009,12 @@ impl Item for Editor { ) { self.workspace = Some((workspace.weak_handle(), workspace.database_id())); if let Some(workspace) = &workspace.weak_handle().upgrade() { - cx.subscribe( - workspace, - |editor, _, event: &workspace::Event, _cx| match event { - workspace::Event::ModalOpened => { - editor.mouse_context_menu.take(); - editor.inline_blame_popover.take(); - } - _ => {} - }, - ) + cx.subscribe(workspace, |editor, _, event: &workspace::Event, _cx| { + if let workspace::Event::ModalOpened = event { + editor.mouse_context_menu.take(); + editor.inline_blame_popover.take(); + } + }) .detach(); } } diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index 13e5d0a8c75d4b329aa466405b6a3d2167e7c955..a3fc41228f2ad9928da2602b97106bb6bd4be6da 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -808,10 +808,7 @@ mod jsx_tag_autoclose_tests { ); buf }); - let buffer_c = cx.new(|cx| { - let buf = language::Buffer::local("<span", cx); - buf - }); + let buffer_c = cx.new(|cx| language::Buffer::local("<span", cx)); let buffer = cx.new(|cx| { let mut buf = MultiBuffer::new(language::Capability::ReadWrite); buf.push_excerpts( diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index e549f64758b99199c67123d433447d45ec07bf00..c79feccb4b1fb0ef7ad686408358e77319ce446c 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -241,24 +241,13 @@ impl ProposedChangesEditor { event: &BufferEvent, _cx: &mut Context<Self>, ) { - match event { - BufferEvent::Operation { .. } => { - self.recalculate_diffs_tx - .unbounded_send(RecalculateDiff { - buffer, - debounce: true, - }) - .ok(); - } - // BufferEvent::DiffBaseChanged => { - // self.recalculate_diffs_tx - // .unbounded_send(RecalculateDiff { - // buffer, - // debounce: false, - // }) - // .ok(); - // } - _ => (), + if let BufferEvent::Operation { .. } = event { + self.recalculate_diffs_tx + .unbounded_send(RecalculateDiff { + buffer, + debounce: true, + }) + .ok(); } } } diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 73c5f1c076e510b2aeb7d648b7ce066b65f9094c..0a02390b641e1020aff8d9cf0167b44485baf489 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -119,8 +119,8 @@ impl SelectionsCollection { cx: &mut App, ) -> Option<Selection<D>> { let map = self.display_map(cx); - let selection = resolve_selections(self.pending_anchor().as_ref(), &map).next(); - selection + + resolve_selections(self.pending_anchor().as_ref(), &map).next() } pub(crate) fn pending_mode(&self) -> Option<SelectMode> { @@ -276,18 +276,18 @@ impl SelectionsCollection { cx: &mut App, ) -> Selection<D> { let map = self.display_map(cx); - let selection = resolve_selections([self.newest_anchor()], &map) + + resolve_selections([self.newest_anchor()], &map) .next() - .unwrap(); - selection + .unwrap() } pub fn newest_display(&self, cx: &mut App) -> Selection<DisplayPoint> { let map = self.display_map(cx); - let selection = resolve_selections_display([self.newest_anchor()], &map) + + resolve_selections_display([self.newest_anchor()], &map) .next() - .unwrap(); - selection + .unwrap() } pub fn oldest_anchor(&self) -> &Selection<Anchor> { @@ -303,10 +303,10 @@ impl SelectionsCollection { cx: &mut App, ) -> Selection<D> { let map = self.display_map(cx); - let selection = resolve_selections([self.oldest_anchor()], &map) + + resolve_selections([self.oldest_anchor()], &map) .next() - .unwrap(); - selection + .unwrap() } pub fn first_anchor(&self) -> Selection<Anchor> { diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index dd9b4f8bba6c466b9f750e97dfc7cb261d2c8226..bbbe54b43fe87e952c22b1a0bd9fba29aa56bb1e 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -678,8 +678,8 @@ pub fn wait_for_lang_server( [ cx.subscribe(&lsp_store, { let log_prefix = log_prefix.clone(); - move |_, event, _| match event { - project::LspStoreEvent::LanguageServerUpdate { + move |_, event, _| { + if let project::LspStoreEvent::LanguageServerUpdate { message: client::proto::update_language_server::Variant::WorkProgress( LspWorkProgress { @@ -688,8 +688,10 @@ pub fn wait_for_lang_server( }, ), .. - } => println!("{}⟲ {message}", log_prefix), - _ => {} + } = event + { + println!("{}⟲ {message}", log_prefix) + } } }), cx.subscribe(project, { diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index 432adaf4bc90e8b3c60b8baec14426dc4ec392b0..3a3026f19c1961a6f4ac4c7fe5ac217ef6855cea 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -484,14 +484,10 @@ impl ExtensionBuilder { _ => {} } - match &payload { - CustomSection(c) => { - if strip_custom_section(c.name()) { - continue; - } - } - - _ => {} + if let CustomSection(c) = &payload + && strip_custom_section(c.name()) + { + continue; } if let Some((id, range)) = payload.as_section() { RawSection { diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 1a05dbc570e7b45963028b70cf643d93c94dd59c..4c3ab8d242763e0a18bc05734c223516789620ad 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1675,9 +1675,8 @@ impl ExtensionStore { let schema_path = &extension::build_debug_adapter_schema_path(adapter_name, meta); if fs.is_file(&src_dir.join(schema_path)).await { - match schema_path.parent() { - Some(parent) => fs.create_dir(&tmp_dir.join(parent)).await?, - None => {} + if let Some(parent) = schema_path.parent() { + fs.create_dir(&tmp_dir.join(parent)).await? } fs.copy_file( &src_dir.join(schema_path), diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 4fe27aedc9f197719c5be2add091214c056066d8..c5bc21fc1c44659b845b7616aa1714a0872f90f3 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -532,7 +532,7 @@ fn wasm_engine(executor: &BackgroundExecutor) -> wasmtime::Engine { // `Future::poll`. const EPOCH_INTERVAL: Duration = Duration::from_millis(100); let mut timer = Timer::interval(EPOCH_INTERVAL); - while let Some(_) = timer.next().await { + while (timer.next().await).is_some() { // Exit the loop and thread once the engine is dropped. let Some(engine) = engine_ref.upgrade() else { break; diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 7f0e8171f6e5f5d32e87f08e9202491b7fc15b8d..a6ee84eb60319fd55f2d054d16e8fade1d905548 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -863,7 +863,7 @@ impl ExtensionsPage { window: &mut Window, cx: &mut App, ) -> Entity<ContextMenu> { - let context_menu = ContextMenu::build(window, cx, |context_menu, window, _| { + ContextMenu::build(window, cx, |context_menu, window, _| { context_menu .entry( "Install Another Version...", @@ -887,9 +887,7 @@ impl ExtensionsPage { cx.write_to_clipboard(ClipboardItem::new_string(authors.join(", "))); } }) - }); - - context_menu + }) } fn show_extension_version_list( diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 77acdf8097bdd86bd26f8a0f2d8a221ea7938f03..ffe3d42a278c63f7da58250c4307c38780057d9a 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -112,7 +112,7 @@ impl OpenPathDelegate { entries, .. } => user_input - .into_iter() + .iter() .filter(|user_input| !user_input.exists || !user_input.is_dir) .map(|user_input| user_input.file.string.clone()) .chain(self.string_matches.iter().filter_map(|string_match| { diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index d17cbdcf51d30fea6e402419ed21c8ee70b63a2e..11177512c3ea34690d4d6bea123322dc92011b34 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -2419,12 +2419,11 @@ impl Fs for FakeFs { let watcher = watcher.clone(); move |events| { let result = events.iter().any(|evt_path| { - let result = watcher + watcher .prefixes .lock() .iter() - .any(|prefix| evt_path.path.starts_with(prefix)); - result + .any(|prefix| evt_path.path.starts_with(prefix)) }); let executor = executor.clone(); async move { diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index edcad514bb2b232cac56d4ca87c04a8121ba1713..9c125d2c4726fb369274d782137d384c8dfb4c0c 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -2028,7 +2028,7 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> { branches.push(Branch { is_head: is_current_branch, - ref_name: ref_name, + ref_name, most_recent_commit: Some(CommitSummary { sha: head_sha, subject, diff --git a/crates/git_ui/src/file_diff_view.rs b/crates/git_ui/src/file_diff_view.rs index f7d29cdfa70d5b8691a319312b52869e84c815e8..a320888b3b409a79866818887b898ea853eb0609 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -123,7 +123,7 @@ impl FileDiffView { old_buffer, new_buffer, _recalculate_diff_task: cx.spawn(async move |this, cx| { - while let Ok(_) = buffer_changes_rx.recv().await { + while buffer_changes_rx.recv().await.is_ok() { loop { let mut timer = cx .background_executor() diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index ace3a8eb15a001208fdd4977bb7b40ce0a35d8e6..3eae1acb0451fb67bdceb7d452cb6fd40475123c 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -426,7 +426,7 @@ impl GitPanel { let git_store = project.read(cx).git_store().clone(); let active_repository = project.read(cx).active_repository(cx); - let git_panel = cx.new(|cx| { + cx.new(|cx| { let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, window, Self::focus_in).detach(); cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { @@ -563,9 +563,7 @@ impl GitPanel { this.schedule_update(false, window, cx); this - }); - - git_panel + }) } fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) { @@ -1198,14 +1196,13 @@ impl GitPanel { window, cx, ); - cx.spawn(async move |this, cx| match prompt.await { - Ok(RestoreCancel::RestoreTrackedFiles) => { + cx.spawn(async move |this, cx| { + if let Ok(RestoreCancel::RestoreTrackedFiles) = prompt.await { this.update(cx, |this, cx| { this.perform_checkout(entries, cx); }) .ok(); } - _ => {} }) .detach(); } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index c12ef58ce26b8a1bf524605870975021259d9024..cc1535b7c30c00d7ca80d2bf796d06892ae328d2 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -346,22 +346,19 @@ impl ProjectDiff { window: &mut Window, cx: &mut Context<Self>, ) { - match event { - EditorEvent::SelectionsChanged { local: true } => { - let Some(project_path) = self.active_path(cx) else { - return; - }; - self.workspace - .update(cx, |workspace, cx| { - if let Some(git_panel) = workspace.panel::<GitPanel>(cx) { - git_panel.update(cx, |git_panel, cx| { - git_panel.select_entry_by_path(project_path, window, cx) - }) - } - }) - .ok(); - } - _ => {} + if let EditorEvent::SelectionsChanged { local: true } = event { + let Some(project_path) = self.active_path(cx) else { + return; + }; + self.workspace + .update(cx, |workspace, cx| { + if let Some(git_panel) = workspace.panel::<GitPanel>(cx) { + git_panel.update(cx, |git_panel, cx| { + git_panel.select_entry_by_path(project_path, window, cx) + }) + } + }) + .ok(); } if editor.focus_handle(cx).contains_focused(window, cx) && self.multibuffer.read(cx).is_empty() @@ -513,7 +510,7 @@ impl ProjectDiff { mut recv: postage::watch::Receiver<()>, cx: &mut AsyncWindowContext, ) -> Result<()> { - while let Some(_) = recv.next().await { + while (recv.next().await).is_some() { let buffers_to_load = this.update(cx, |this, cx| this.load_buffers(cx))?; for buffer_to_load in buffers_to_load { if let Some(buffer) = buffer_to_load.await.log_err() { diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index d07868c3e1eee4d5ccfb0b0e3c15cdc869795411..e38e3698d54d1d6869002ba10e83f3a8401af219 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -207,7 +207,7 @@ impl TextDiffView { path: Some(format!("Clipboard ↔ {selection_location_path}").into()), buffer_changes_tx, _recalculate_diff_task: cx.spawn(async move |_, cx| { - while let Ok(_) = buffer_changes_rx.recv().await { + while buffer_changes_rx.recv().await.is_ok() { loop { let mut timer = cx .background_executor() diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2be1a34e4993c50d1d42da1a5d86db9a354a272e..bbd59fa7bc1276bedac8a17e9fe947a7211172eb 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1707,8 +1707,8 @@ impl App { .unwrap_or_else(|| { is_first = true; let future = A::load(source.clone(), self); - let task = self.background_executor().spawn(future).shared(); - task + + self.background_executor().spawn(future).shared() }); self.loading_assets.insert(asset_id, Box::new(task.clone())); diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index c58f72267c281bd66d844c184f1c56756141371b..b5e071279623611685ea744e38b072284e764e2a 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -326,7 +326,7 @@ impl TextLayout { vec![text_style.to_run(text.len())] }; - let layout_id = window.request_measured_layout(Default::default(), { + window.request_measured_layout(Default::default(), { let element_state = self.clone(); move |known_dimensions, available_space, window, cx| { @@ -416,9 +416,7 @@ impl TextLayout { size } - }); - - layout_id + }) } fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) { diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 3278dfbe385e85e40fb6b7c4e9bafdf32174a68f..4d314280940eb066c66b6520d0b8ca6ec815f024 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -949,11 +949,8 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr { }; drop(state); - match event { - wl_callback::Event::Done { .. } => { - window.frame(); - } - _ => {} + if let wl_callback::Event::Done { .. } = event { + window.frame(); } } } @@ -2014,25 +2011,22 @@ impl Dispatch<wl_data_offer::WlDataOffer, ()> for WaylandClientStatePtr { let client = this.get_client(); let mut state = client.borrow_mut(); - match event { - wl_data_offer::Event::Offer { mime_type } => { - // Drag and drop - if mime_type == FILE_LIST_MIME_TYPE { - let serial = state.serial_tracker.get(SerialKind::DataDevice); - let mime_type = mime_type.clone(); - data_offer.accept(serial, Some(mime_type)); - } + if let wl_data_offer::Event::Offer { mime_type } = event { + // Drag and drop + if mime_type == FILE_LIST_MIME_TYPE { + let serial = state.serial_tracker.get(SerialKind::DataDevice); + let mime_type = mime_type.clone(); + data_offer.accept(serial, Some(mime_type)); + } - // Clipboard - if let Some(offer) = state - .data_offers - .iter_mut() - .find(|wrapper| wrapper.inner.id() == data_offer.id()) - { - offer.add_mime_type(mime_type); - } + // Clipboard + if let Some(offer) = state + .data_offers + .iter_mut() + .find(|wrapper| wrapper.inner.id() == data_offer.id()) + { + offer.add_mime_type(mime_type); } - _ => {} } } } @@ -2113,13 +2107,10 @@ impl Dispatch<zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ()> let client = this.get_client(); let mut state = client.borrow_mut(); - match event { - zwp_primary_selection_offer_v1::Event::Offer { mime_type } => { - if let Some(offer) = state.primary_data_offer.as_mut() { - offer.add_mime_type(mime_type); - } - } - _ => {} + if let zwp_primary_selection_offer_v1::Event::Offer { mime_type } = event + && let Some(offer) = state.primary_data_offer.as_mut() + { + offer.add_mime_type(mime_type); } } } diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 1d1166a56c89eaa877625099e13664b6f93be790..ce1468335d5bec7f1fefe4ae1faa13318b32feda 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -355,85 +355,82 @@ impl WaylandWindowStatePtr { } pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) { - match event { - xdg_surface::Event::Configure { serial } => { - { - let mut state = self.state.borrow_mut(); - if let Some(window_controls) = state.in_progress_window_controls.take() { - state.window_controls = window_controls; - - drop(state); - let mut callbacks = self.callbacks.borrow_mut(); - if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { - appearance_changed(); - } + if let xdg_surface::Event::Configure { serial } = event { + { + let mut state = self.state.borrow_mut(); + if let Some(window_controls) = state.in_progress_window_controls.take() { + state.window_controls = window_controls; + + drop(state); + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { + appearance_changed(); } } - { - let mut state = self.state.borrow_mut(); - - if let Some(mut configure) = state.in_progress_configure.take() { - let got_unmaximized = state.maximized && !configure.maximized; - state.fullscreen = configure.fullscreen; - state.maximized = configure.maximized; - state.tiling = configure.tiling; - // Limit interactive resizes to once per vblank - if configure.resizing && state.resize_throttle { - return; - } else if configure.resizing { - state.resize_throttle = true; - } - if !configure.fullscreen && !configure.maximized { - configure.size = if got_unmaximized { - Some(state.window_bounds.size) - } else { - compute_outer_size(state.inset(), configure.size, state.tiling) - }; - if let Some(size) = configure.size { - state.window_bounds = Bounds { - origin: Point::default(), - size, - }; - } - } - drop(state); + } + { + let mut state = self.state.borrow_mut(); + + if let Some(mut configure) = state.in_progress_configure.take() { + let got_unmaximized = state.maximized && !configure.maximized; + state.fullscreen = configure.fullscreen; + state.maximized = configure.maximized; + state.tiling = configure.tiling; + // Limit interactive resizes to once per vblank + if configure.resizing && state.resize_throttle { + return; + } else if configure.resizing { + state.resize_throttle = true; + } + if !configure.fullscreen && !configure.maximized { + configure.size = if got_unmaximized { + Some(state.window_bounds.size) + } else { + compute_outer_size(state.inset(), configure.size, state.tiling) + }; if let Some(size) = configure.size { - self.resize(size); + state.window_bounds = Bounds { + origin: Point::default(), + size, + }; } } - } - let mut state = self.state.borrow_mut(); - state.xdg_surface.ack_configure(serial); - - let window_geometry = inset_by_tiling( - state.bounds.map_origin(|_| px(0.0)), - state.inset(), - state.tiling, - ) - .map(|v| v.0 as i32) - .map_size(|v| if v <= 0 { 1 } else { v }); - - state.xdg_surface.set_window_geometry( - window_geometry.origin.x, - window_geometry.origin.y, - window_geometry.size.width, - window_geometry.size.height, - ); - - let request_frame_callback = !state.acknowledged_first_configure; - if request_frame_callback { - state.acknowledged_first_configure = true; drop(state); - self.frame(); + if let Some(size) = configure.size { + self.resize(size); + } } } - _ => {} + let mut state = self.state.borrow_mut(); + state.xdg_surface.ack_configure(serial); + + let window_geometry = inset_by_tiling( + state.bounds.map_origin(|_| px(0.0)), + state.inset(), + state.tiling, + ) + .map(|v| v.0 as i32) + .map_size(|v| if v <= 0 { 1 } else { v }); + + state.xdg_surface.set_window_geometry( + window_geometry.origin.x, + window_geometry.origin.y, + window_geometry.size.width, + window_geometry.size.height, + ); + + let request_frame_callback = !state.acknowledged_first_configure; + if request_frame_callback { + state.acknowledged_first_configure = true; + drop(state); + self.frame(); + } } } pub fn handle_toplevel_decoration_event(&self, event: zxdg_toplevel_decoration_v1::Event) { - match event { - zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode { + if let zxdg_toplevel_decoration_v1::Event::Configure { mode } = event { + match mode { WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => { self.state.borrow_mut().decorations = WindowDecorations::Server; if let Some(mut appearance_changed) = @@ -457,17 +454,13 @@ impl WaylandWindowStatePtr { WEnum::Unknown(v) => { log::warn!("Unknown decoration mode: {}", v); } - }, - _ => {} + } } } pub fn handle_fractional_scale_event(&self, event: wp_fractional_scale_v1::Event) { - match event { - wp_fractional_scale_v1::Event::PreferredScale { scale } => { - self.rescale(scale as f32 / 120.0); - } - _ => {} + if let wp_fractional_scale_v1::Event::PreferredScale { scale } = event { + self.rescale(scale as f32 / 120.0); } } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index e422af961fd6ed2b731fbec0f433987986cc015d..346ba8718b3b6ab1fe99b0dec8a11bbd78fa8b54 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -232,15 +232,12 @@ impl X11ClientStatePtr { }; let mut state = client.0.borrow_mut(); - if let Some(window_ref) = state.windows.remove(&x_window) { - match window_ref.refresh_state { - Some(RefreshState::PeriodicRefresh { - event_loop_token, .. - }) => { - state.loop_handle.remove(event_loop_token); - } - _ => {} - } + if let Some(window_ref) = state.windows.remove(&x_window) + && let Some(RefreshState::PeriodicRefresh { + event_loop_token, .. + }) = window_ref.refresh_state + { + state.loop_handle.remove(event_loop_token); } if state.mouse_focused_window == Some(x_window) { state.mouse_focused_window = None; @@ -876,22 +873,19 @@ impl X11Client { let Some(reply) = reply else { return Some(()); }; - match str::from_utf8(&reply.value) { - Ok(file_list) => { - let paths: SmallVec<[_; 2]> = file_list - .lines() - .filter_map(|path| Url::parse(path).log_err()) - .filter_map(|url| url.to_file_path().log_err()) - .collect(); - let input = PlatformInput::FileDrop(FileDropEvent::Entered { - position: state.xdnd_state.position, - paths: crate::ExternalPaths(paths), - }); - drop(state); - window.handle_input(input); - self.0.borrow_mut().xdnd_state.retrieved = true; - } - Err(_) => {} + if let Ok(file_list) = str::from_utf8(&reply.value) { + let paths: SmallVec<[_; 2]> = file_list + .lines() + .filter_map(|path| Url::parse(path).log_err()) + .filter_map(|url| url.to_file_path().log_err()) + .collect(); + let input = PlatformInput::FileDrop(FileDropEvent::Entered { + position: state.xdnd_state.position, + paths: crate::ExternalPaths(paths), + }); + drop(state); + window.handle_input(input); + self.0.borrow_mut().xdnd_state.retrieved = true; } } Event::ConfigureNotify(event) => { diff --git a/crates/gpui/src/platform/mac/events.rs b/crates/gpui/src/platform/mac/events.rs index 50a516cb388a404443d2fc635f589cdc4d8c64ee..938db4b76205ee43eb979995c240b8d96e2aa57a 100644 --- a/crates/gpui/src/platform/mac/events.rs +++ b/crates/gpui/src/platform/mac/events.rs @@ -426,7 +426,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { key_char = Some(chars_for_modified_key(native_event.keyCode(), mods)); } - let mut key = if shift + if shift && chars_ignoring_modifiers .chars() .all(|c| c.is_ascii_lowercase()) @@ -437,9 +437,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { chars_with_shift } else { chars_ignoring_modifiers - }; - - key + } } }; diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index bc60e13a59355abc8520213517a89cbc56934400..cd923a18596c249d5435e662974ad8ee4097b8e7 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -2063,8 +2063,8 @@ fn screen_point_to_gpui_point(this: &Object, position: NSPoint) -> Point<Pixels> let frame = get_frame(this); let window_x = position.x - frame.origin.x; let window_y = frame.size.height - (position.y - frame.origin.y); - let position = point(px(window_x as f32), px(window_y as f32)); - position + + point(px(window_x as f32), px(window_y as f32)) } extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation { diff --git a/crates/gpui/src/platform/scap_screen_capture.rs b/crates/gpui/src/platform/scap_screen_capture.rs index 32041b655fdc20b046717291c623dcb5c4d5146c..d6d19cd8102d58ceaa9bc87bff348eaeda9adfef 100644 --- a/crates/gpui/src/platform/scap_screen_capture.rs +++ b/crates/gpui/src/platform/scap_screen_capture.rs @@ -228,7 +228,7 @@ fn run_capture( display, size, })); - if let Err(_) = stream_send_result { + if stream_send_result.is_err() { return; } while !cancel_stream.load(std::sync::atomic::Ordering::SeqCst) { diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index c3bb8bb22babe6d07e1c01cdbe07706deb7bcf03..607163b577fbb81c1193f6af16be5edb7b3c17fe 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -1128,22 +1128,19 @@ impl WindowsWindowInner { && 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); - } + if 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) @@ -1469,7 +1466,7 @@ pub(crate) fn current_modifiers() -> Modifiers { #[inline] pub(crate) fn current_capslock() -> Capslock { let on = unsafe { GetKeyState(VK_CAPITAL.0 as i32) & 1 } > 0; - Capslock { on: on } + Capslock { on } } fn get_client_area_insets( diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index f78d6b30c7648b278d9076daf0cca2e4c65acb01..f198bb771849a0c617d3f4b4a1cf0e5ceda475f5 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -58,23 +58,21 @@ impl TaffyLayoutEngine { children: &[LayoutId], ) -> LayoutId { let taffy_style = style.to_taffy(rem_size); - let layout_id = if children.is_empty() { + + if children.is_empty() { self.taffy .new_leaf(taffy_style) .expect(EXPECT_MESSAGE) .into() } else { - let parent_id = self - .taffy + self.taffy // This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId. .new_with_children(taffy_style, unsafe { std::mem::transmute::<&[LayoutId], &[taffy::NodeId]>(children) }) .expect(EXPECT_MESSAGE) - .into(); - parent_id - }; - layout_id + .into() + } } pub fn request_measured_layout( @@ -91,8 +89,7 @@ impl TaffyLayoutEngine { ) -> LayoutId { let taffy_style = style.to_taffy(rem_size); - let layout_id = self - .taffy + self.taffy .new_leaf_with_context( taffy_style, NodeContext { @@ -100,8 +97,7 @@ impl TaffyLayoutEngine { }, ) .expect(EXPECT_MESSAGE) - .into(); - layout_id + .into() } // Used to understand performance diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index 648d714c89765d09623f154ef55ddd44d9716028..93ec6c854c31d3f312006f61d6994a4eee4b88ef 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -44,7 +44,7 @@ impl LineWrapper { let mut prev_c = '\0'; let mut index = 0; let mut candidates = fragments - .into_iter() + .iter() .flat_map(move |fragment| fragment.wrap_boundary_candidates()) .peekable(); iter::from_fn(move || { diff --git a/crates/gpui/src/util.rs b/crates/gpui/src/util.rs index f357034fbf52eda780447ebd7c0ff32432eaac4a..3d7fa06e6ca013ae38b1c63d1bfd624d46cdf4f1 100644 --- a/crates/gpui/src/util.rs +++ b/crates/gpui/src/util.rs @@ -58,13 +58,7 @@ pub trait FluentBuilder { where Self: Sized, { - self.map(|this| { - if let Some(_) = option { - this - } else { - then(this) - } - }) + self.map(|this| if option.is_some() { this } else { then(this) }) } } diff --git a/crates/gpui_macros/src/test.rs b/crates/gpui_macros/src/test.rs index 0153c5889adf53f8a95b5876726d70230aad587d..648d3499edb0a8f13031092e37d761368363af08 100644 --- a/crates/gpui_macros/src/test.rs +++ b/crates/gpui_macros/src/test.rs @@ -86,7 +86,7 @@ impl Parse for Args { Ok(Args { seeds, max_retries, - max_iterations: max_iterations, + max_iterations, on_failure_fn_name, }) } diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index a7f75b0962561ac713e57f9ad26cb64ed82f8003..62468573ed29687c0436e98a0174baa515b0ee3d 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -435,8 +435,7 @@ impl HttpClient for FakeHttpClient { &self, req: Request<AsyncBody>, ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> { - let future = (self.handler.lock().as_ref().unwrap())(req); - future + ((self.handler.lock().as_ref().unwrap())(req)) as _ } fn user_agent(&self) -> Option<&HeaderValue> { diff --git a/crates/jj/src/jj_repository.rs b/crates/jj/src/jj_repository.rs index 93ae79eb90992a8fc71804788325683eae800cb4..afbe54c99dcb40a039e8f7cc87c14dc393ebac3a 100644 --- a/crates/jj/src/jj_repository.rs +++ b/crates/jj/src/jj_repository.rs @@ -50,16 +50,13 @@ impl RealJujutsuRepository { impl JujutsuRepository for RealJujutsuRepository { fn list_bookmarks(&self) -> Vec<Bookmark> { - let bookmarks = self - .repository + self.repository .view() .bookmarks() .map(|(ref_name, _target)| Bookmark { ref_name: ref_name.as_str().to_string().into(), }) - .collect(); - - bookmarks + .collect() } } diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 53887eb7366c844796da5505923ba74fdfb0e4c7..81dc36093b1eebeabb58f443dc25bc4f93d1607e 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -195,11 +195,9 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap } fn journal_dir(path: &str) -> Option<PathBuf> { - let expanded_journal_dir = shellexpand::full(path) //TODO handle this better + shellexpand::full(path) //TODO handle this better .ok() - .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal")); - - expanded_journal_dir + .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal")) } fn heading_entry(now: NaiveTime, hour_format: &Option<HourFormat>) -> String { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 972a90ddab0386eb0d347d239f1169927e80934a..cc96022e63b86879ada909b05adf30fe45d9d356 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1128,7 +1128,7 @@ impl Buffer { } else { ranges.as_slice() } - .into_iter() + .iter() .peekable(); let mut edits = Vec::new(); @@ -1395,7 +1395,8 @@ impl Buffer { is_first = false; return true; } - let any_sub_ranges_contain_range = layer + + layer .included_sub_ranges .map(|sub_ranges| { sub_ranges.iter().any(|sub_range| { @@ -1404,9 +1405,7 @@ impl Buffer { !is_before_start && !is_after_end }) }) - .unwrap_or(true); - let result = any_sub_ranges_contain_range; - result + .unwrap_or(true) }) .last() .map(|info| info.language.clone()) @@ -2616,7 +2615,7 @@ impl Buffer { self.completion_triggers = self .completion_triggers_per_language_server .values() - .flat_map(|triggers| triggers.into_iter().cloned()) + .flat_map(|triggers| triggers.iter().cloned()) .collect(); } else { self.completion_triggers_per_language_server @@ -2776,7 +2775,7 @@ impl Buffer { self.completion_triggers = self .completion_triggers_per_language_server .values() - .flat_map(|triggers| triggers.into_iter().cloned()) + .flat_map(|triggers| triggers.iter().cloned()) .collect(); } else { self.completion_triggers_per_language_server diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 68addc804eddd009b9b0bc523b8df25daabdd6a0..b70e4662465e267f427367c3053a53eea5869159 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1513,9 +1513,8 @@ impl Language { .map(|ix| { let mut config = BracketsPatternConfig::default(); for setting in query.property_settings(ix) { - match setting.key.as_ref() { - "newline.only" => config.newline_only = true, - _ => {} + if setting.key.as_ref() == "newline.only" { + config.newline_only = true } } config diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 70e42cb02d066e7f2224a7133131d286e65b4b0c..b10529c3d93c51154531661a4e02e0cdd3061aab 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -300,7 +300,7 @@ impl From<AnthropicError> for LanguageModelCompletionError { }, AnthropicError::ServerOverloaded { retry_after } => Self::ServerOverloaded { provider, - retry_after: retry_after, + retry_after, }, AnthropicError::ApiError(api_error) => api_error.into(), } diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 1a5c09cdc449bbe66f2412abd7c90de5d19feeb3..4348fd42110b2554de801b812a7b001dc49ad06e 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -404,7 +404,7 @@ pub fn into_open_ai( match content { MessageContent::Text(text) | MessageContent::Thinking { text, .. } => { add_message_content_part( - open_ai::MessagePart::Text { text: text }, + open_ai::MessagePart::Text { text }, message.role, &mut messages, ) diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 2c490b45cfce50cd2dc1a43162da84e1d8586094..ac653d5b2eaea4af497e81707383099384de146a 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -234,7 +234,7 @@ impl JsonLspAdapter { schemas .as_array_mut() .unwrap() - .extend(cx.all_action_names().into_iter().map(|&name| { + .extend(cx.all_action_names().iter().map(|&name| { project::lsp_store::json_language_server_ext::url_schema_for_action(name) })); diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 906e45bb3a5bc1b149efb433d907624eb9a796af..6c92d78525f9382770df851bb0a9622183460e32 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -711,7 +711,7 @@ impl Default for PythonToolchainProvider { } } -static ENV_PRIORITY_LIST: &'static [PythonEnvironmentKind] = &[ +static ENV_PRIORITY_LIST: &[PythonEnvironmentKind] = &[ // Prioritize non-Conda environments. PythonEnvironmentKind::Poetry, PythonEnvironmentKind::Pipenv, diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 7f65ccf5ea4ed7a96ce5421cba96f782dbaf2093..162e3bea78352c9eb98d6e7b6057bbe922c8b03c 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -5205,13 +5205,9 @@ impl MultiBufferSnapshot { if offset == diff_transforms.start().0 && bias == Bias::Left && let Some(prev_item) = diff_transforms.prev_item() + && let DiffTransform::DeletedHunk { .. } = prev_item { - match prev_item { - DiffTransform::DeletedHunk { .. } => { - diff_transforms.prev(); - } - _ => {} - } + diff_transforms.prev(); } let offset_in_transform = offset - diff_transforms.start().0; let mut excerpt_offset = diff_transforms.start().1; diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 871c72ea0b1d96dacf416100c969289382bc0030..9d41eb1562943683aae3e785b1daac8bc3bfeb1a 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -76,9 +76,8 @@ impl NodeRuntime { let mut state = self.0.lock().await; let options = loop { - match state.options.borrow().as_ref() { - Some(options) => break options.clone(), - None => {} + if let Some(options) = state.options.borrow().as_ref() { + break options.clone(); } match state.options.changed().await { Ok(()) => {} diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index d700fa08bd75ce658fdca606bbd204132da170cd..672bcf1cd992ee128f885825d60d805bbb011c40 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -19,7 +19,7 @@ use util::ResultExt; use workspace::{ModalView, Workspace}; use zed_actions::agent::OpenSettings; -const FEATURED_PROVIDERS: [&'static str; 4] = ["anthropic", "google", "openai", "ollama"]; +const FEATURED_PROVIDERS: [&str; 4] = ["anthropic", "google", "openai", "ollama"]; fn render_llm_provider_section( tab_index: &mut isize, @@ -410,7 +410,7 @@ impl AiPrivacyTooltip { impl Render for AiPrivacyTooltip { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { - const DESCRIPTION: &'static str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. "; + const DESCRIPTION: &str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. "; tooltip_container(window, cx, move |this, _, _| { this.child( diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 8d89c6662ef97b6b426c6216b0afa6faf1f7a094..77a70dfc8dbbee90cac1d25b3d8336d6a8588ccb 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -16,8 +16,8 @@ use vim_mode_setting::VimModeSetting; use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile}; -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 LIGHT_THEMES: [&str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; +const DARK_THEMES: [&str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; const FAMILY_NAMES: [SharedString; 3] = [ SharedString::new_static("One"), SharedString::new_static("Ayu"), @@ -114,7 +114,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement let themes = theme_names.map(|theme| theme_registry.get(theme).unwrap()); - let theme_previews = [0, 1, 2].map(|index| { + [0, 1, 2].map(|index| { let theme = &themes[index]; let is_selected = theme.name == current_theme_name; let name = theme.name.clone(); @@ -176,9 +176,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement .color(Color::Muted) .size(LabelSize::Small), ) - }); - - theme_previews + }) } fn write_mode_change(mode: ThemeMode, cx: &mut App) { diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index d941a0315afd726d493ecb121d2862d0067e6dd3..60a9856abe3c5f0ef7ee397af7b64a716b78fb17 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -605,7 +605,7 @@ fn render_popular_settings_section( window: &mut Window, cx: &mut App, ) -> impl IntoElement { - const LIGATURE_TOOLTIP: &'static str = + const LIGATURE_TOOLTIP: &str = "Font ligatures combine two characters into one. For example, turning =/= into ≠."; v_flex() diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 78f512f7f351189396a2de97f33d24ef387808ac..891ae1595d3a95ab1f81d4bbd8840e94bd06cd20 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -733,7 +733,8 @@ impl OutlinePanel { ) -> Entity<Self> { let project = workspace.project().clone(); let workspace_handle = cx.entity().downgrade(); - let outline_panel = cx.new(|cx| { + + cx.new(|cx| { let filter_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Filter...", cx); @@ -912,9 +913,7 @@ impl OutlinePanel { outline_panel.replace_active_editor(item, editor, window, cx); } outline_panel - }); - - outline_panel + }) } fn serialization_key(workspace: &Workspace) -> Option<String> { @@ -2624,7 +2623,7 @@ impl OutlinePanel { } fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &App) -> String { - let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) { + match self.project.read(cx).worktree_for_id(*worktree_id, cx) { Some(worktree) => { let worktree = worktree.read(cx); match worktree.snapshot().root_entry() { @@ -2645,8 +2644,7 @@ impl OutlinePanel { } } None => file_name(entry.path.as_ref()), - }; - name + } } fn update_fs_entries( @@ -2681,7 +2679,8 @@ impl OutlinePanel { new_collapsed_entries = outline_panel.collapsed_entries.clone(); new_unfolded_dirs = outline_panel.unfolded_dirs.clone(); let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx); - let buffer_excerpts = multi_buffer_snapshot.excerpts().fold( + + multi_buffer_snapshot.excerpts().fold( HashMap::default(), |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| { let buffer_id = buffer_snapshot.remote_id(); @@ -2728,8 +2727,7 @@ impl OutlinePanel { ); buffer_excerpts }, - ); - buffer_excerpts + ) }) else { return; }; @@ -4807,7 +4805,7 @@ impl OutlinePanel { .with_compute_indents_fn(cx.entity(), |outline_panel, range, _, _| { let entries = outline_panel.cached_entries.get(range); if let Some(entries) = entries { - entries.into_iter().map(|item| item.depth).collect() + entries.iter().map(|item| item.depth).collect() } else { smallvec::SmallVec::new() } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 296749c14ec505150ea11de683599c849300ebfc..d36508937768919902b3a29b321d1650dcd87c61 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -413,13 +413,10 @@ impl LocalBufferStore { cx: &mut Context<BufferStore>, ) { cx.subscribe(worktree, |this, worktree, event, cx| { - if worktree.read(cx).is_local() { - match event { - worktree::Event::UpdatedEntries(changes) => { - Self::local_worktree_entries_changed(this, &worktree, changes, cx); - } - _ => {} - } + if worktree.read(cx).is_local() + && let worktree::Event::UpdatedEntries(changes) = event + { + Self::local_worktree_entries_changed(this, &worktree, changes, cx); } }) .detach(); @@ -947,10 +944,9 @@ impl BufferStore { } pub fn get_by_path(&self, path: &ProjectPath) -> Option<Entity<Buffer>> { - self.path_to_buffer_id.get(path).and_then(|buffer_id| { - let buffer = self.get(*buffer_id); - buffer - }) + self.path_to_buffer_id + .get(path) + .and_then(|buffer_id| self.get(*buffer_id)) } pub fn get(&self, buffer_id: BufferId) -> Option<Entity<Buffer>> { diff --git a/crates/project/src/color_extractor.rs b/crates/project/src/color_extractor.rs index dbbd3d7b996767d70ce00e01b73074bbb9523b3d..6e9907e30b7393a3074f4af579536d74140418f9 100644 --- a/crates/project/src/color_extractor.rs +++ b/crates/project/src/color_extractor.rs @@ -4,8 +4,8 @@ use gpui::{Hsla, Rgba}; use lsp::{CompletionItem, Documentation}; use regex::{Regex, RegexBuilder}; -const HEX: &'static str = r#"(#(?:[\da-fA-F]{3}){1,2})"#; -const RGB_OR_HSL: &'static str = r#"(rgba?|hsla?)\(\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*(?:,\s*(1|0?\.\d+))?\s*\)"#; +const HEX: &str = r#"(#(?:[\da-fA-F]{3}){1,2})"#; +const RGB_OR_HSL: &str = r#"(rgba?|hsla?)\(\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*(?:,\s*(1|0?\.\d+))?\s*\)"#; static RELAXED_HEX_REGEX: LazyLock<Regex> = LazyLock::new(|| { RegexBuilder::new(HEX) @@ -141,7 +141,7 @@ mod tests { use gpui::rgba; use lsp::{CompletionItem, CompletionItemKind}; - pub const COLOR_TABLE: &[(&'static str, Option<u32>)] = &[ + pub const COLOR_TABLE: &[(&str, Option<u32>)] = &[ // -- Invalid -- // Invalid hex ("f0f", None), diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index f80f24bb717cda4e9d061f6dbc0cc98186036f08..16625caeb4bbee62cce06c230c40f7dbb5bd33d3 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -642,8 +642,8 @@ mod tests { #[gpui::test] async fn test_context_server_status(cx: &mut TestAppContext) { - const SERVER_1_ID: &'static str = "mcp-1"; - const SERVER_2_ID: &'static str = "mcp-2"; + const SERVER_1_ID: &str = "mcp-1"; + const SERVER_2_ID: &str = "mcp-2"; let (_fs, project) = setup_context_server_test( cx, @@ -722,8 +722,8 @@ mod tests { #[gpui::test] async fn test_context_server_status_events(cx: &mut TestAppContext) { - const SERVER_1_ID: &'static str = "mcp-1"; - const SERVER_2_ID: &'static str = "mcp-2"; + const SERVER_1_ID: &str = "mcp-1"; + const SERVER_2_ID: &str = "mcp-2"; let (_fs, project) = setup_context_server_test( cx, @@ -784,7 +784,7 @@ mod tests { #[gpui::test(iterations = 25)] async fn test_context_server_concurrent_starts(cx: &mut TestAppContext) { - const SERVER_1_ID: &'static str = "mcp-1"; + const SERVER_1_ID: &str = "mcp-1"; let (_fs, project) = setup_context_server_test( cx, @@ -845,8 +845,8 @@ mod tests { #[gpui::test] async fn test_context_server_maintain_servers_loop(cx: &mut TestAppContext) { - const SERVER_1_ID: &'static str = "mcp-1"; - const SERVER_2_ID: &'static str = "mcp-2"; + const SERVER_1_ID: &str = "mcp-1"; + const SERVER_2_ID: &str = "mcp-2"; let server_1_id = ContextServerId(SERVER_1_ID.into()); let server_2_id = ContextServerId(SERVER_2_ID.into()); @@ -1084,7 +1084,7 @@ mod tests { #[gpui::test] async fn test_context_server_enabled_disabled(cx: &mut TestAppContext) { - const SERVER_1_ID: &'static str = "mcp-1"; + const SERVER_1_ID: &str = "mcp-1"; let server_1_id = ContextServerId(SERVER_1_ID.into()); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index ccda64fba8ed3ada5e97d12048337d5d90ce65ac..382e83587a661e467113111791b57b627721bbf3 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -470,9 +470,8 @@ impl DapStore { session_id: impl Borrow<SessionId>, ) -> Option<Entity<session::Session>> { let session_id = session_id.borrow(); - let client = self.sessions.get(session_id).cloned(); - client + self.sessions.get(session_id).cloned() } pub fn sessions(&self) -> impl Iterator<Item = &Entity<Session>> { self.sessions.values() diff --git a/crates/project/src/debugger/locators/go.rs b/crates/project/src/debugger/locators/go.rs index 61436fce8f3659d4b12c3010b82e0d845654c4e9..eec06084ec78548e1a627080663d2afccc8a0aca 100644 --- a/crates/project/src/debugger/locators/go.rs +++ b/crates/project/src/debugger/locators/go.rs @@ -174,7 +174,7 @@ impl DapLocator for GoLocator { request: "launch".to_string(), mode: "test".to_string(), program, - args: args, + args, build_flags, cwd: build_config.cwd.clone(), env: build_config.env.clone(), @@ -185,7 +185,7 @@ impl DapLocator for GoLocator { label: resolved_label.to_string().into(), adapter: adapter.0.clone(), build: None, - config: config, + config, tcp_connection: None, }) } @@ -220,7 +220,7 @@ impl DapLocator for GoLocator { request: "launch".to_string(), mode: "debug".to_string(), program, - args: args, + args, build_flags, }) .unwrap(); diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index ee5baf1d3b0e9c58cf27bd2b7f5c90fd1997d4d5..cd792877b66e5897b1ea2f8a88615f544814e815 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -226,7 +226,7 @@ impl RunningMode { fn unset_breakpoints_from_paths(&self, paths: &Vec<Arc<Path>>, cx: &mut App) -> Task<()> { let tasks: Vec<_> = paths - .into_iter() + .iter() .map(|path| { self.request(dap_command::SetBreakpoints { source: client_source(path), @@ -508,13 +508,12 @@ impl RunningMode { .ok(); } - let ret = if configuration_done_supported { + if configuration_done_supported { this.request(ConfigurationDone {}) } else { Task::ready(Ok(())) } - .await; - ret + .await } }); @@ -839,7 +838,7 @@ impl Session { }) .detach(); - let this = Self { + Self { mode: SessionState::Booting(None), id: session_id, child_session_ids: HashSet::default(), @@ -868,9 +867,7 @@ impl Session { task_context, memory: memory::Memory::new(), quirks, - }; - - this + } }) } diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index 54d87d230cb856fb62d84fb34dc77907b2e6df19..c5a198954e1d2d30655c140d7d46cf84ccbf0c69 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -446,15 +446,12 @@ impl ImageStore { event: &ImageItemEvent, cx: &mut Context<Self>, ) { - match event { - ImageItemEvent::FileHandleChanged => { - if let Some(local) = self.state.as_local() { - local.update(cx, |local, cx| { - local.image_changed_file(image, cx); - }) - } - } - _ => {} + if let ImageItemEvent::FileHandleChanged = event + && let Some(local) = self.state.as_local() + { + local.update(cx, |local, cx| { + local.image_changed_file(image, cx); + }) } } } @@ -531,13 +528,10 @@ impl ImageStoreImpl for Entity<LocalImageStore> { impl LocalImageStore { fn subscribe_to_worktree(&mut self, worktree: &Entity<Worktree>, cx: &mut Context<Self>) { cx.subscribe(worktree, |this, worktree, event, cx| { - if worktree.read(cx).is_local() { - match event { - worktree::Event::UpdatedEntries(changes) => { - this.local_worktree_entries_changed(&worktree, changes, cx); - } - _ => {} - } + if worktree.read(cx).is_local() + && let worktree::Event::UpdatedEntries(changes) = event + { + this.local_worktree_entries_changed(&worktree, changes, cx); } }) .detach(); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 217e00ee964d37b6d6f9f84f36664cfcc38b3b28..de6848701f3cd878afcf9c78aaabaebf721bb901 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2501,8 +2501,8 @@ pub(crate) fn parse_completion_text_edit( }; Some(ParsedCompletionEdit { - insert_range: insert_range, - replace_range: replace_range, + insert_range, + replace_range, new_text: new_text.clone(), }) } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index e6ea01ff9ae90a4390fa2af4099f469202059611..a8c6ffd87851b95a240641b888bf80fb2801403d 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -550,7 +550,7 @@ impl LocalLspStore { if let Some(settings) = settings.binary.as_ref() { if let Some(arguments) = &settings.arguments { - binary.arguments = arguments.into_iter().map(Into::into).collect(); + binary.arguments = arguments.iter().map(Into::into).collect(); } if let Some(env) = &settings.env { shell_env.extend(env.iter().map(|(k, v)| (k.clone(), v.clone()))); @@ -1060,8 +1060,8 @@ impl LocalLspStore { }; let delegate: Arc<dyn ManifestDelegate> = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); - let root = self - .lsp_tree + + self.lsp_tree .get( project_path, language.name(), @@ -1069,9 +1069,7 @@ impl LocalLspStore { &delegate, cx, ) - .collect::<Vec<_>>(); - - root + .collect::<Vec<_>>() } fn language_server_ids_for_buffer( @@ -2397,7 +2395,8 @@ impl LocalLspStore { let server_id = server_node.server_id_or_init(|disposition| { let path = &disposition.path; - let server_id = { + + { let uri = Url::from_file_path(worktree.read(cx).abs_path().join(&path.path)); @@ -2415,9 +2414,7 @@ impl LocalLspStore { state.add_workspace_folder(uri); }; server_id - }; - - server_id + } })?; let server_state = self.language_servers.get(&server_id)?; if let LanguageServerState::Running { @@ -3047,16 +3044,14 @@ impl LocalLspStore { buffer.edit([(range, text)], None, cx); } - let transaction = buffer.end_transaction(cx).and_then(|transaction_id| { + buffer.end_transaction(cx).and_then(|transaction_id| { if push_to_history { buffer.finalize_last_transaction(); buffer.get_transaction(transaction_id).cloned() } else { buffer.forget_transaction(transaction_id) } - }); - - transaction + }) })?; if let Some(transaction) = transaction { project_transaction.0.insert(buffer_to_edit, transaction); @@ -4370,13 +4365,11 @@ impl LspStore { if let Some((client, downstream_project_id)) = self.downstream_client.clone() && let Some(diangostic_summaries) = self.diagnostic_summaries.get(&worktree.id()) { - let mut summaries = diangostic_summaries - .into_iter() - .flat_map(|(path, summaries)| { - summaries - .into_iter() - .map(|(server_id, summary)| summary.to_proto(*server_id, path)) - }); + let mut summaries = diangostic_summaries.iter().flat_map(|(path, summaries)| { + summaries + .iter() + .map(|(server_id, summary)| summary.to_proto(*server_id, path)) + }); if let Some(summary) = summaries.next() { client .send(proto::UpdateDiagnosticSummary { @@ -4564,7 +4557,7 @@ impl LspStore { anyhow::anyhow!(message) })?; - let response = request + request .response_from_lsp( response, this.upgrade().context("no app context")?, @@ -4572,8 +4565,7 @@ impl LspStore { language_server.server_id(), cx.clone(), ) - .await; - response + .await }) } @@ -4853,7 +4845,7 @@ impl LspStore { push_to_history: bool, cx: &mut Context<Self>, ) -> Task<anyhow::Result<ProjectTransaction>> { - if let Some(_) = self.as_local() { + if self.as_local().is_some() { cx.spawn(async move |lsp_store, cx| { let buffers = buffers.into_iter().collect::<Vec<_>>(); let result = LocalLspStore::execute_code_action_kind_locally( @@ -7804,7 +7796,7 @@ impl LspStore { } None => { diagnostics_summary = Some(proto::UpdateDiagnosticSummary { - project_id: project_id, + project_id, worktree_id: worktree_id.to_proto(), summary: Some(proto::DiagnosticSummary { path: project_path.path.as_ref().to_proto(), @@ -10054,7 +10046,7 @@ impl LspStore { cx: &mut Context<Self>, ) -> Task<anyhow::Result<ProjectTransaction>> { let logger = zlog::scoped!("format"); - if let Some(_) = self.as_local() { + if self.as_local().is_some() { zlog::trace!(logger => "Formatting locally"); let logger = zlog::scoped!(logger => "local"); let buffers = buffers diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index 750815c477c34416b05a339bc6e9efc591336cd2..ced9b34d93c836a714f57aa01afaef6a4458a16b 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -43,12 +43,9 @@ impl WorktreeRoots { match event { WorktreeEvent::UpdatedEntries(changes) => { for (path, _, kind) in changes.iter() { - match kind { - worktree::PathChange::Removed => { - let path = TriePath::from(path.as_ref()); - this.roots.remove(&path); - } - _ => {} + if kind == &worktree::PathChange::Removed { + let path = TriePath::from(path.as_ref()); + this.roots.remove(&path); } } } @@ -197,11 +194,8 @@ impl ManifestTree { evt: &WorktreeStoreEvent, _: &mut Context<Self>, ) { - match evt { - WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => { - self.root_points.remove(worktree_id); - } - _ => {} + if let WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) = evt { + self.root_points.remove(worktree_id); } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f9ad7b96d361432c64bf218f0b028caa6613ba38..6712b3fab0d81330dbd201d2981ed20345dc808b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2885,14 +2885,11 @@ impl Project { event: &DapStoreEvent, cx: &mut Context<Self>, ) { - match event { - DapStoreEvent::Notification(message) => { - cx.emit(Event::Toast { - notification_id: "dap".into(), - message: message.clone(), - }); - } - _ => {} + if let DapStoreEvent::Notification(message) = event { + cx.emit(Event::Toast { + notification_id: "dap".into(), + message: message.clone(), + }); } } @@ -3179,14 +3176,11 @@ impl Project { event: &ImageItemEvent, cx: &mut Context<Self>, ) -> Option<()> { - match event { - ImageItemEvent::ReloadNeeded => { - if !self.is_via_collab() { - self.reload_images([image.clone()].into_iter().collect(), cx) - .detach_and_log_err(cx); - } - } - _ => {} + if let ImageItemEvent::ReloadNeeded = event + && !self.is_via_collab() + { + self.reload_images([image.clone()].into_iter().collect(), cx) + .detach_and_log_err(cx); } None diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 5137d64fabb9f1c48766eb85204e4e3018f89694..eb1e3828e902b1c75f7197be0773a67f5c5056d6 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -695,7 +695,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { assert_eq!( buffer .completion_triggers() - .into_iter() + .iter() .cloned() .collect::<Vec<_>>(), &[".".to_string(), "::".to_string()] @@ -747,7 +747,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { assert_eq!( buffer .completion_triggers() - .into_iter() + .iter() .cloned() .collect::<Vec<_>>(), &[":".to_string()] @@ -766,7 +766,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { assert_eq!( buffer .completion_triggers() - .into_iter() + .iter() .cloned() .collect::<Vec<_>>(), &[".".to_string(), "::".to_string()] diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 8d8a1bd008d01f0e7b4c9ec8605dad6cbd715eff..e51f8e0b3b5ecd4513f1c86ede35d95f6633df4a 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -110,7 +110,7 @@ impl<T: InventoryContents> InventoryFor<T> { fn global_scenarios(&self) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> { self.global.iter().flat_map(|(file_path, templates)| { - templates.into_iter().map(|template| { + templates.iter().map(|template| { ( TaskSourceKind::AbsPath { id_base: Cow::Owned(format!("global {}", T::GLOBAL_SOURCE_FILE)), diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 212d2dd2d9a32e525edfe4827c4c0e7810b7c1dc..b2556d7584d07db0c22a246489d9a78328b91fea 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -67,13 +67,11 @@ pub struct SshDetails { impl Project { pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> { - let worktree = self - .active_entry() + self.active_entry() .and_then(|entry_id| self.worktree_for_entry(entry_id, cx)) .into_iter() .chain(self.worktrees(cx)) - .find_map(|tree| tree.read(cx).root_dir()); - worktree + .find_map(|tree| tree.read(cx).root_dir()) } pub fn first_project_directory(&self, cx: &App) -> Option<PathBuf> { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 9a87874ed87e57716fcc4b8221ae4a86589feb6a..dc92ee8c70c30b75737322be628ea1bbb7662dfe 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -3589,7 +3589,7 @@ impl ProjectPanel { previous_components.next(); } - if let Some(_) = suffix_components { + if suffix_components.is_some() { new_path.push(previous_components); } if let Some(str) = new_path.to_str() { @@ -4422,9 +4422,7 @@ impl ProjectPanel { let components = Path::new(&file_name) .components() .map(|comp| { - let comp_str = - comp.as_os_str().to_string_lossy().into_owned(); - comp_str + comp.as_os_str().to_string_lossy().into_owned() }) .collect::<Vec<_>>(); diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index 9b79d3ce9c7fb36532b4cc9d2619c332e6272989..dd4d788cfdb4f8ee60efdc7568ba16697822a58c 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -88,11 +88,8 @@ impl DisconnectedOverlay { self.finished = true; cx.emit(DismissEvent); - match &self.host { - Host::SshRemoteProject(ssh_connection_options) => { - self.reconnect_to_ssh_remote(ssh_connection_options.clone(), window, cx); - } - _ => {} + if let Host::SshRemoteProject(ssh_connection_options) = &self.host { + self.reconnect_to_ssh_remote(ssh_connection_options.clone(), window, cx); } } diff --git a/crates/remote/src/protocol.rs b/crates/remote/src/protocol.rs index 787094781d831b98bf19d91cc77f00a6b00e5015..e5a9c5b7a55bf7a49d720ba3d761c04cc597e4fd 100644 --- a/crates/remote/src/protocol.rs +++ b/crates/remote/src/protocol.rs @@ -31,8 +31,8 @@ pub async fn read_message<S: AsyncRead + Unpin>( stream.read_exact(buffer).await?; let len = message_len_from_buffer(buffer); - let result = read_message_with_len(stream, buffer, len).await; - result + + read_message_with_len(stream, buffer, len).await } pub async fn write_message<S: AsyncWrite + Unpin>( diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 85150f629ee8ab775b812fa7c0f53671ce37c7ec..6fc327ac1c6af3449c72803f72a22b82343dfb28 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -194,15 +194,11 @@ impl HeadlessProject { languages.clone(), ); - cx.subscribe( - &buffer_store, - |_this, _buffer_store, event, cx| match event { - BufferStoreEvent::BufferAdded(buffer) => { - cx.subscribe(buffer, Self::on_buffer_event).detach(); - } - _ => {} - }, - ) + cx.subscribe(&buffer_store, |_this, _buffer_store, event, cx| { + if let BufferStoreEvent::BufferAdded(buffer) = event { + cx.subscribe(buffer, Self::on_buffer_event).detach(); + } + }) .detach(); let extensions = HeadlessExtensionStore::new( @@ -285,18 +281,17 @@ impl HeadlessProject { event: &BufferEvent, cx: &mut Context<Self>, ) { - match event { - BufferEvent::Operation { - operation, - is_local: true, - } => cx - .background_spawn(self.session.request(proto::UpdateBuffer { - project_id: SSH_PROJECT_ID, - buffer_id: buffer.read(cx).remote_id().to_proto(), - operations: vec![serialize_operation(operation)], - })) - .detach(), - _ => {} + if let BufferEvent::Operation { + operation, + is_local: true, + } = event + { + cx.background_spawn(self.session.request(proto::UpdateBuffer { + project_id: SSH_PROJECT_ID, + buffer_id: buffer.read(cx).remote_id().to_proto(), + operations: vec![serialize_operation(operation)], + })) + .detach() } } diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 9315536e6b4b942d1bf5d572c930b0ea4e736931..15a465a8806d96554742d95c5d8e1931c1672595 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -334,7 +334,7 @@ fn start_server( let (mut stdin_msg_tx, mut stdin_msg_rx) = mpsc::unbounded::<Envelope>(); cx.background_spawn(async move { while let Ok(msg) = read_message(&mut stdin_stream, &mut input_buffer).await { - if let Err(_) = stdin_msg_tx.send(msg).await { + if (stdin_msg_tx.send(msg).await).is_err() { break; } } @@ -891,7 +891,8 @@ pub fn handle_settings_file_changes( fn read_proxy_settings(cx: &mut Context<HeadlessProject>) -> Option<Url> { let proxy_str = ProxySettings::get_global(cx).proxy.to_owned(); - let proxy_url = proxy_str + + proxy_str .as_ref() .and_then(|input: &String| { input @@ -899,8 +900,7 @@ fn read_proxy_settings(cx: &mut Context<HeadlessProject>) -> Option<Url> { .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e)) .ok() }) - .or_else(read_proxy_from_env); - proxy_url + .or_else(read_proxy_from_env) } fn daemonize() -> Result<ControlFlow<()>> { diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs index b8fd2e57f2feb6b9745055fb19cad4d7786460e3..714cb3aed33259023b9ff99e3e3819f4a7419e1a 100644 --- a/crates/repl/src/components/kernel_options.rs +++ b/crates/repl/src/components/kernel_options.rs @@ -269,10 +269,9 @@ where }; let picker_view = cx.new(|cx| { - let picker = Picker::uniform_list(delegate, window, cx) + Picker::uniform_list(delegate, window, cx) .width(rems(30.)) - .max_height(Some(rems(20.).into())); - picker + .max_height(Some(rems(20.).into())) }); PopoverMenu::new("kernel-switcher") diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index 15179a632c04c5feb82724e1bc9ab2193332120b..87b8e1d55ae85e09c0398848a989b7764e0d3b04 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/crates/repl/src/notebook/cell.rs @@ -91,7 +91,7 @@ fn convert_outputs( cx: &mut App, ) -> Vec<Output> { outputs - .into_iter() + .iter() .map(|output| match output { nbformat::v4::Output::Stream { text, .. } => Output::Stream { content: cx.new(|cx| TerminalOutput::from(&text.0, window, cx)), diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index a84f147dd27a178f25bd01ee8695de26ecde811f..325d262d9eddc164093f088d0e4790d0fa581167 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -584,8 +584,8 @@ impl project::ProjectItem for NotebookItem { Ok(nbformat::Notebook::Legacy(legacy_notebook)) => { // TODO: Decide if we want to mutate the notebook by including Cell IDs // and any other conversions - let notebook = nbformat::upgrade_legacy_notebook(legacy_notebook)?; - notebook + + nbformat::upgrade_legacy_notebook(legacy_notebook)? } // Bad notebooks and notebooks v4.0 and below are not supported Err(e) => { diff --git a/crates/repl/src/outputs/plain.rs b/crates/repl/src/outputs/plain.rs index 74c7bfa3c3a33da450f07f879b5681e9bdc1fdca..ae3c728c8aad01e2aa7968423c2d3c3fdc549b19 100644 --- a/crates/repl/src/outputs/plain.rs +++ b/crates/repl/src/outputs/plain.rs @@ -68,7 +68,7 @@ pub fn text_style(window: &mut Window, cx: &mut App) -> TextStyle { let theme = cx.theme(); - let text_style = TextStyle { + TextStyle { font_family, font_features, font_weight, @@ -81,9 +81,7 @@ pub fn text_style(window: &mut Window, cx: &mut App) -> TextStyle { // These are going to be overridden per-cell color: theme.colors().terminal_foreground, ..Default::default() - }; - - text_style + } } /// Returns the default terminal size for the terminal output. diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index 96f7d1db1104cccb962c9d2ca740ccc679951bd4..e3c7d6f750be176eab06e37aefb78b682beefdcc 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -543,7 +543,7 @@ impl Iterator for Tabs { // Since tabs are 1 byte the tab offset is the same as the byte offset let position = TabPosition { byte_offset: tab_offset, - char_offset: char_offset, + char_offset, }; // Remove the tab we've just seen self.tabs ^= 1 << tab_offset; diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 355deb5d2059c0535ad834ce5e4cb7cabc38fec2..bebe4315e444aa100fa70b636e5d23015ed5fbe2 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -49,7 +49,7 @@ actions!( ] ); -const BUILT_IN_TOOLTIP_TEXT: &'static str = concat!( +const BUILT_IN_TOOLTIP_TEXT: &str = concat!( "This rule supports special functionality.\n", "It's read-only, but you can remove it from your default rules." ); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b6836324db10305454763b142cb51d150290fb7a..0886654d62302a5e6322ecff117f5b6c9a4f8fe0 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1113,8 +1113,8 @@ impl ProjectSearchView { .await .log_err(); } - let should_search = result != 2; - should_search + + result != 2 } else { true }; diff --git a/crates/settings/src/key_equivalents.rs b/crates/settings/src/key_equivalents.rs index bf08de97ae1e5bb47fa0d5e8ea0c592319b58529..65801375356289f41fa1688cbb32dff4249333d9 100644 --- a/crates/settings/src/key_equivalents.rs +++ b/crates/settings/src/key_equivalents.rs @@ -1415,7 +1415,7 @@ pub fn get_key_equivalents(layout: &str) -> Option<HashMap<char, char>> { _ => return None, }; - Some(HashMap::from_iter(mappings.into_iter().cloned())) + Some(HashMap::from_iter(mappings.iter().cloned())) } #[cfg(not(target_os = "macos"))] diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index c102b303c1c803b235c87a1953d3b83ea086166b..a472c50e6c6c73613b224524466ce90f241a54eb 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -295,9 +295,9 @@ fn replace_value_in_json_text( } } -const TS_DOCUMENT_KIND: &'static str = "document"; -const TS_ARRAY_KIND: &'static str = "array"; -const TS_COMMENT_KIND: &'static str = "comment"; +const TS_DOCUMENT_KIND: &str = "document"; +const TS_ARRAY_KIND: &str = "array"; +const TS_COMMENT_KIND: &str = "comment"; pub fn replace_top_level_array_value_in_json_text( text: &str, diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 457d58e5a73943d884083433a0757ba2f1d13a1c..12e3c0c2742aaa0dc1cc31fc7617873f5b541418 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -621,8 +621,7 @@ impl KeymapEditor { let key_bindings_ptr = cx.key_bindings(); let lock = key_bindings_ptr.borrow(); let key_bindings = lock.bindings(); - let mut unmapped_action_names = - HashSet::from_iter(cx.all_action_names().into_iter().copied()); + let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names().iter().copied()); let action_documentation = cx.action_documentation(); let mut generator = KeymapFile::action_schema_generator(); let actions_with_schemas = HashSet::from_iter( @@ -1289,7 +1288,7 @@ struct HumanizedActionNameCache { impl HumanizedActionNameCache { fn new(cx: &App) -> Self { - let cache = HashMap::from_iter(cx.all_action_names().into_iter().map(|&action_name| { + let cache = HashMap::from_iter(cx.all_action_names().iter().map(|&action_name| { ( action_name, command_palette::humanize_action_name(action_name).into(), @@ -1857,18 +1856,15 @@ impl Render for KeymapEditor { mouse_down_event: &gpui::MouseDownEvent, window, cx| { - match mouse_down_event.button { - MouseButton::Right => { - this.select_index( - row_index, None, window, cx, - ); - this.create_context_menu( - mouse_down_event.position, - window, - cx, - ); - } - _ => {} + if mouse_down_event.button == MouseButton::Right { + this.select_index( + row_index, None, window, cx, + ); + this.create_context_menu( + mouse_down_event.position, + window, + cx, + ); } }, )) diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index 66593524a393def8f6050f4e9162c1520df75d15..1b8010853ecabc7f4198172a4364f7a1f88fbe67 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -19,7 +19,7 @@ actions!( ] ); -const KEY_CONTEXT_VALUE: &'static str = "KeystrokeInput"; +const KEY_CONTEXT_VALUE: &str = "KeystrokeInput"; const CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(300); diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index a91d497572522b130a463e0a534db45f5938c58c..9d7bb0736061181eda93d072640f87b5946a2675 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -213,7 +213,7 @@ impl TableInteractionState { let mut column_ix = 0; let resizable_columns_slice = *resizable_columns; - let mut resizable_columns = resizable_columns.into_iter(); + let mut resizable_columns = resizable_columns.iter(); let dividers = intersperse_with(spacers, || { window.with_id(column_ix, |window| { @@ -801,7 +801,7 @@ impl<const COLS: usize> Table<COLS> { ) -> Self { self.rows = TableContents::UniformList(UniformListData { element_id: id.into(), - row_count: row_count, + row_count, render_item_fn: Box::new(render_item_fn), }); self diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index fd0be97ff6f8e5ef04126a4de60f41d4f31e2bef..aad3875410e9ac303c4c03469be44c1d11bfb56f 100644 --- a/crates/storybook/src/story_selector.rs +++ b/crates/storybook/src/story_selector.rs @@ -109,15 +109,13 @@ static ALL_STORY_SELECTORS: OnceLock<Vec<StorySelector>> = OnceLock::new(); impl ValueEnum for StorySelector { fn value_variants<'a>() -> &'a [Self] { - let stories = ALL_STORY_SELECTORS.get_or_init(|| { + (ALL_STORY_SELECTORS.get_or_init(|| { let component_stories = ComponentStory::iter().map(StorySelector::Component); component_stories .chain(std::iter::once(StorySelector::KitchenSink)) .collect::<Vec<_>>() - }); - - stories + })) as _ } fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> { diff --git a/crates/svg_preview/src/svg_preview_view.rs b/crates/svg_preview/src/svg_preview_view.rs index 4e4c83c8de58cba07dde3c75513c98139a4e31ad..12dd97f0c8f3fbec1bbbcaabadc4118c3a4e0229 100644 --- a/crates/svg_preview/src/svg_preview_view.rs +++ b/crates/svg_preview/src/svg_preview_view.rs @@ -157,18 +157,15 @@ impl SvgPreviewView { &active_editor, window, |this: &mut SvgPreviewView, _editor, event: &EditorEvent, window, cx| { - match event { - EditorEvent::Saved => { - // Remove cached image to force reload - if let Some(svg_path) = &this.svg_path { - let resource = Resource::Path(svg_path.clone().into()); - this.image_cache.update(cx, |cache, cx| { - cache.remove(&resource, window, cx); - }); - } - cx.notify(); + if event == &EditorEvent::Saved { + // Remove cached image to force reload + if let Some(svg_path) = &this.svg_path { + let resource = Resource::Path(svg_path.clone().into()); + this.image_cache.update(cx, |cache, cx| { + cache.remove(&resource, window, cx); + }); } - _ => {} + cx.notify(); } }, ); @@ -184,22 +181,18 @@ impl SvgPreviewView { event: &workspace::Event, _window, cx| { - match event { - workspace::Event::ActiveItemChanged => { - let workspace_read = workspace.read(cx); - if let Some(active_item) = workspace_read.active_item(cx) - && let Some(editor_entity) = - active_item.downcast::<Editor>() - && Self::is_svg_file(&editor_entity, cx) - { - let new_path = Self::get_svg_path(&editor_entity, cx); - if this.svg_path != new_path { - this.svg_path = new_path; - cx.notify(); - } + if let workspace::Event::ActiveItemChanged = event { + let workspace_read = workspace.read(cx); + if let Some(active_item) = workspace_read.active_item(cx) + && let Some(editor_entity) = active_item.downcast::<Editor>() + && Self::is_svg_file(&editor_entity, cx) + { + let new_path = Self::get_svg_path(&editor_entity, cx); + if this.svg_path != new_path { + this.svg_path = new_path; + cx.notify(); } } - _ => {} } }, ) diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index b8c49d4230f384982b74e7a3055b8504a8671711..5ed29fd733d6e72d221ee7a00cc7c2d823f17b45 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -237,13 +237,11 @@ impl ShellBuilder { task_args: &Vec<String>, ) -> (String, Vec<String>) { if let Some(task_command) = task_command { - let combined_command = task_args - .into_iter() - .fold(task_command, |mut command, arg| { - command.push(' '); - command.push_str(&self.kind.to_shell_variable(arg)); - command - }); + let combined_command = task_args.iter().fold(task_command, |mut command, arg| { + command.push(' '); + command.push_str(&self.kind.to_shell_variable(arg)); + command + }); self.args .extend(self.kind.args_for_shell(self.interactive, combined_command)); diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index c4b0931c353a5651906dbc26c2eba77f55a080b2..9fbdc152f385c6f603b20ba372a15f9b8ed5eccf 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -550,7 +550,7 @@ impl PickerDelegate for TasksModalDelegate { list_item.tooltip(move |_, _| item_label.clone()) }) .map(|item| { - let item = if matches!(source_kind, TaskSourceKind::UserInput) + if matches!(source_kind, TaskSourceKind::UserInput) || Some(ix) <= self.divider_index { let task_index = hit.candidate_id; @@ -579,8 +579,7 @@ impl PickerDelegate for TasksModalDelegate { item.end_hover_slot(delete_button) } else { item - }; - item + } }) .toggle_state(selected) .child(highlighted_location.render(window, cx)), diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index c50e2bd3a79ad1e5ea01484fca6ad417ab970b98..1d76f701520fceaaa2aa1ed88924be31c7a33e40 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -236,7 +236,7 @@ impl TerminalPanel { ) -> Result<Entity<Self>> { let mut terminal_panel = None; - match workspace + if let Some((database_id, serialization_key)) = workspace .read_with(&cx, |workspace, _| { workspace .database_id() @@ -244,34 +244,29 @@ impl TerminalPanel { }) .ok() .flatten() + && let Some(serialized_panel) = cx + .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) }) + .await + .log_err() + .flatten() + .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel)) + .transpose() + .log_err() + .flatten() + && let Ok(serialized) = workspace + .update_in(&mut cx, |workspace, window, cx| { + deserialize_terminal_panel( + workspace.weak_handle(), + workspace.project().clone(), + database_id, + serialized_panel, + window, + cx, + ) + })? + .await { - Some((database_id, serialization_key)) => { - if let Some(serialized_panel) = cx - .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) }) - .await - .log_err() - .flatten() - .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel)) - .transpose() - .log_err() - .flatten() - && let Ok(serialized) = workspace - .update_in(&mut cx, |workspace, window, cx| { - deserialize_terminal_panel( - workspace.weak_handle(), - workspace.project().clone(), - database_id, - serialized_panel, - window, - cx, - ) - })? - .await - { - terminal_panel = Some(serialized); - } - } - _ => {} + terminal_panel = Some(serialized); } let terminal_panel = if let Some(panel) = terminal_panel { @@ -629,7 +624,7 @@ impl TerminalPanel { workspace .read(cx) .panes() - .into_iter() + .iter() .cloned() .flat_map(pane_terminal_views), ) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index f434e4615929e8a76ed29e9a4acc182d77ecf488..956bcebfd0061c9693fed09d66a9f8389709cf84 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1937,7 +1937,8 @@ impl SearchableItem for TerminalView { // Selection head might have a value if there's a selection that isn't // associated with a match. Therefore, if there are no matches, we should // report None, no matter the state of the terminal - let res = if !matches.is_empty() { + + if !matches.is_empty() { if let Some(selection_head) = self.terminal().read(cx).selection_head { // If selection head is contained in a match. Return that match match direction { @@ -1977,9 +1978,7 @@ impl SearchableItem for TerminalView { } } else { None - }; - - res + } } fn replace( &mut self, diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs index 5bd69c173340fa0cb31aa334072084f33a7c7281..c21709559a62f712fa190021803a479cf189f061 100644 --- a/crates/theme/src/icon_theme.rs +++ b/crates/theme/src/icon_theme.rs @@ -398,7 +398,7 @@ static DEFAULT_ICON_THEME: LazyLock<Arc<IconTheme>> = LazyLock::new(|| { }, file_stems: icon_keys_by_association(FILE_STEMS_BY_ICON_KEY), file_suffixes: icon_keys_by_association(FILE_SUFFIXES_BY_ICON_KEY), - file_icons: HashMap::from_iter(FILE_ICONS.into_iter().map(|(ty, path)| { + file_icons: HashMap::from_iter(FILE_ICONS.iter().map(|(ty, path)| { ( ty.to_string(), IconDefinition { diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index 275f47912a6b0c791ba27ba9768f4a22d6bfd50d..5be68afeb4c09b4d6536626c255b4918ecc66c01 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -41,7 +41,8 @@ fn toggle_screen_sharing( let Some(room) = call.room().cloned() else { return; }; - let toggle_screen_sharing = room.update(cx, |room, cx| { + + room.update(cx, |room, cx| { let clicked_on_currently_shared_screen = room.shared_screen_id().is_some_and(|screen_id| { Some(screen_id) @@ -78,8 +79,7 @@ fn toggle_screen_sharing( } else { Task::ready(Ok(())) } - }); - toggle_screen_sharing + }) } Err(e) => Task::ready(Err(e)), }; diff --git a/crates/ui/src/components/facepile.rs b/crates/ui/src/components/facepile.rs index 879bfce0417a5f9567a662b1de58c46324736e2d..83e99df7c24dab20b088bf15aeab10501e81d6ef 100644 --- a/crates/ui/src/components/facepile.rs +++ b/crates/ui/src/components/facepile.rs @@ -78,7 +78,7 @@ impl RenderOnce for Facepile { } } -pub const EXAMPLE_FACES: [&'static str; 6] = [ +pub const EXAMPLE_FACES: [&str; 6] = [ "https://avatars.githubusercontent.com/u/326587?s=60&v=4", "https://avatars.githubusercontent.com/u/2280405?s=60&v=4", "https://avatars.githubusercontent.com/u/1789?s=60&v=4", diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index e5f28e3b255b92744c3286243bc6cbadbd7375a5..2ca635c05bbd442fe26bcee2fbda4b168ad0afeb 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -616,7 +616,7 @@ impl SwitchField { Self { id: id.into(), label: label.into(), - description: description, + description, toggle_state: toggle_state.into(), on_click: Arc::new(on_click), disabled: false, diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index ed0fdd0114137256273f420acd647228bf605218..65ed2f2b6848a2b0c74c6102499027badce935a9 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -175,7 +175,7 @@ impl Tooltip { move |_, cx| { let title = title.clone(); cx.new(|_| Self { - title: title, + title, meta: None, key_binding: None, }) diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index e2ce54b9940da102a53edcf8f82d039718df2aff..2bc531268d4f909c11c29d6b001d8a34e887c927 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -201,10 +201,7 @@ impl Vim { let right_kind = classifier.kind_with(right, ignore_punctuation); let at_newline = (left == '\n') ^ (right == '\n'); - let found = (left_kind != right_kind && right_kind != CharKind::Whitespace) - || at_newline; - - found + (left_kind != right_kind && right_kind != CharKind::Whitespace) || at_newline }) } Motion::NextWordEnd { ignore_punctuation } => { @@ -213,10 +210,7 @@ impl Vim { let right_kind = classifier.kind_with(right, ignore_punctuation); let at_newline = (left == '\n') ^ (right == '\n'); - let found = (left_kind != right_kind && left_kind != CharKind::Whitespace) - || at_newline; - - found + (left_kind != right_kind && left_kind != CharKind::Whitespace) || at_newline }) } Motion::PreviousWordStart { ignore_punctuation } => { @@ -225,10 +219,7 @@ impl Vim { let right_kind = classifier.kind_with(right, ignore_punctuation); let at_newline = (left == '\n') ^ (right == '\n'); - let found = (left_kind != right_kind && left_kind != CharKind::Whitespace) - || at_newline; - - found + (left_kind != right_kind && left_kind != CharKind::Whitespace) || at_newline }) } Motion::PreviousWordEnd { ignore_punctuation } => { @@ -237,10 +228,7 @@ impl Vim { let right_kind = classifier.kind_with(right, ignore_punctuation); let at_newline = (left == '\n') ^ (right == '\n'); - let found = (left_kind != right_kind && right_kind != CharKind::Whitespace) - || at_newline; - - found + (left_kind != right_kind && right_kind != CharKind::Whitespace) || at_newline }) } Motion::FindForward { diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index fcd36dd7ee5556b889cea0688b7a53720d922c75..2af22bf050b805a75ad6bfff1e68fef9480e94c3 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -155,12 +155,11 @@ fn expand_changed_word_selection( let classifier = map .buffer_snapshot .char_classifier_at(selection.start.to_point(map)); - let in_word = map - .buffer_chars_at(selection.head().to_offset(map, Bias::Left)) + + map.buffer_chars_at(selection.head().to_offset(map, Bias::Left)) .next() .map(|(c, _)| !classifier.is_whitespace(c)) - .unwrap_or_default(); - in_word + .unwrap_or_default() }; if (times.is_none() || times.unwrap() == 1) && is_in_word() { let next_char = map diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 81efcef17a116f2d6592d5dcb8f98f7d21cea6ac..23efd3913907d3d15806911390b51b6705c7e3b3 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -255,16 +255,11 @@ impl MarksState { pub fn new(workspace: &Workspace, cx: &mut App) -> Entity<MarksState> { cx.new(|cx| { let buffer_store = workspace.project().read(cx).buffer_store().clone(); - let subscription = - cx.subscribe( - &buffer_store, - move |this: &mut Self, _, event, cx| match event { - project::buffer_store::BufferStoreEvent::BufferAdded(buffer) => { - this.on_buffer_loaded(buffer, cx); - } - _ => {} - }, - ); + let subscription = cx.subscribe(&buffer_store, move |this: &mut Self, _, event, cx| { + if let project::buffer_store::BufferStoreEvent::BufferAdded(buffer) = event { + this.on_buffer_loaded(buffer, cx); + } + }); let mut this = Self { workspace: workspace.weak_handle(), @@ -596,7 +591,7 @@ impl MarksState { if let Some(anchors) = self.buffer_marks.get(&buffer_id) { let text_anchors = anchors.get(name)?; let anchors = text_anchors - .into_iter() + .iter() .map(|anchor| Anchor::in_buffer(excerpt_id, buffer_id, *anchor)) .collect(); return Some(Mark::Local(anchors)); @@ -1710,26 +1705,25 @@ impl VimDb { marks: HashMap<String, Vec<Point>>, ) -> Result<()> { log::debug!("Setting path {path:?} for {} marks", marks.len()); - let result = self - .write(move |conn| { - let mut query = conn.exec_bound(sql!( - INSERT OR REPLACE INTO vim_marks - (workspace_id, mark_name, path, value) - VALUES - (?, ?, ?, ?) - ))?; - for (mark_name, value) in marks { - let pairs: Vec<(u32, u32)> = value - .into_iter() - .map(|point| (point.row, point.column)) - .collect(); - let serialized = serde_json::to_string(&pairs)?; - query((workspace_id, mark_name, path.clone(), serialized))?; - } - Ok(()) - }) - .await; - result + + self.write(move |conn| { + let mut query = conn.exec_bound(sql!( + INSERT OR REPLACE INTO vim_marks + (workspace_id, mark_name, path, value) + VALUES + (?, ?, ?, ?) + ))?; + for (mark_name, value) in marks { + let pairs: Vec<(u32, u32)> = value + .into_iter() + .map(|point| (point.row, point.column)) + .collect(); + let serialized = serde_json::to_string(&pairs)?; + query((workspace_id, mark_name, path.clone(), serialized))?; + } + Ok(()) + }) + .await } fn get_marks(&self, workspace_id: WorkspaceId) -> Result<Vec<SerializedMark>> { diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 98dabb83163569c5b1cf9ecce49a7e793b5209a6..c2f7414f44e0bcdb35ff14ddfa2d75e548830b81 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -590,7 +590,7 @@ fn parse_state(marked_text: &str) -> (String, Vec<Range<Point>>) { #[cfg(feature = "neovim")] fn encode_ranges(text: &str, point_ranges: &Vec<Range<Point>>) -> String { let byte_ranges = point_ranges - .into_iter() + .iter() .map(|range| { let mut byte_range = 0..0; let mut ix = 0; diff --git a/crates/web_search_providers/src/cloud.rs b/crates/web_search_providers/src/cloud.rs index 52ee0da0d46287c78164d4ff6cc3eb31e46167b1..75ffb1da63109c802207e80da167cdb0cc3c9a0a 100644 --- a/crates/web_search_providers/src/cloud.rs +++ b/crates/web_search_providers/src/cloud.rs @@ -50,7 +50,7 @@ impl State { } } -pub const ZED_WEB_SEARCH_PROVIDER_ID: &'static str = "zed.dev"; +pub const ZED_WEB_SEARCH_PROVIDER_ID: &str = "zed.dev"; impl WebSearchProvider for CloudWebSearchProvider { fn id(&self) -> WebSearchProviderId { diff --git a/crates/web_search_providers/src/web_search_providers.rs b/crates/web_search_providers/src/web_search_providers.rs index 7f8a5f3fa4b2634e50e4de78f9aa09fca0ab0413..8ab0aee47a414c4cc669ab05e727a827d17c2844 100644 --- a/crates/web_search_providers/src/web_search_providers.rs +++ b/crates/web_search_providers/src/web_search_providers.rs @@ -27,11 +27,10 @@ fn register_web_search_providers( cx.subscribe( &LanguageModelRegistry::global(cx), - move |this, registry, event, cx| match event { - language_model::Event::DefaultModelChanged => { + move |this, registry, event, cx| { + if let language_model::Event::DefaultModelChanged = event { register_zed_web_search_provider(this, client.clone(), ®istry, cx) } - _ => {} }, ) .detach(); diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index febb83d6838bb4733b056d9775fe1922e343fa6c..d77be8ed7632dd113e10a03552da79735d82fa6c 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -33,13 +33,12 @@ impl SharedScreen { cx: &mut Context<Self>, ) -> Self { let my_sid = track.sid(); - cx.subscribe(&room, move |_, _, ev, cx| match ev { - call::room::Event::RemoteVideoTrackUnsubscribed { sid } => { - if sid == &my_sid { - cx.emit(Event::Close) - } + cx.subscribe(&room, move |_, _, ev, cx| { + if let call::room::Event::RemoteVideoTrackUnsubscribed { sid } = ev + && sid == &my_sid + { + cx.emit(Event::Close) } - _ => {} }) .detach(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9dac340b5c3a664846c8cb3fa735b4446f43fc48..8c1be61abfc78870b6d7c4a047063f8de67c6511 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3283,7 +3283,8 @@ impl Workspace { let task = self.load_path(project_path.clone(), window, cx); window.spawn(cx, async move |cx| { let (project_entry_id, build_item) = task.await?; - let result = pane.update_in(cx, |pane, window, cx| { + + pane.update_in(cx, |pane, window, cx| { pane.open_item( project_entry_id, project_path, @@ -3295,8 +3296,7 @@ impl Workspace { cx, build_item, ) - }); - result + }) }) } @@ -9150,13 +9150,12 @@ mod tests { fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> { workspace.update_in(cx, |workspace, window, cx| { - let new_pane = workspace.split_pane( + workspace.split_pane( workspace.active_pane().clone(), SplitDirection::Right, window, cx, - ); - new_pane + ) }) } @@ -9413,7 +9412,7 @@ mod tests { let workspace = workspace.clone(); move |cx: &mut VisualTestContext| { workspace.update_in(cx, |workspace, window, cx| { - if let Some(_) = workspace.active_modal::<TestModal>(cx) { + if workspace.active_modal::<TestModal>(cx).is_some() { workspace.toggle_modal(window, cx, TestModal::new); workspace.toggle_modal(window, cx, TestModal::new); } else { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index df30d4dd7beba769d5ce9f44446fdf6f82efd881..851c4e79f116b55d1f7c220e973add761a2eddd5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -80,12 +80,9 @@ fn files_not_created_on_launch(errors: HashMap<io::ErrorKind, Vec<&Path>>) { #[cfg(unix)] { - match kind { - io::ErrorKind::PermissionDenied => { - error_kind_details.push_str("\n\nConsider using chown and chmod tools for altering the directories permissions if your user has corresponding rights.\ - \nFor example, `sudo chown $(whoami):staff ~/.config` and `chmod +uwrx ~/.config`"); - } - _ => {} + if kind == io::ErrorKind::PermissionDenied { + error_kind_details.push_str("\n\nConsider using chown and chmod tools for altering the directories permissions if your user has corresponding rights.\ + \nFor example, `sudo chown $(whoami):staff ~/.config` and `chmod +uwrx ~/.config`"); } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d3a503f172bae0e7fbb3668f9f178243f7d0207f..232dfc42a3bab5cce78802d16f0440a2d03f2733 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1620,13 +1620,12 @@ fn open_local_file( .read_with(cx, |tree, _| tree.abs_path().join(settings_relative_path))?; let fs = project.read_with(cx, |project, _| project.fs().clone())?; - let file_exists = fs - .metadata(&full_path) + + fs.metadata(&full_path) .await .ok() .flatten() - .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo); - file_exists + .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo) }; if !file_exists { diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index 8d12a5bfad746492286ff4198818e8b84fc21111..1123e53ddd1f1a0affbd5de807886d4bbf4d81ea 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -60,8 +60,8 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) { cx.subscribe(&user_store, { let editors = editors.clone(); let client = client.clone(); - move |user_store, event, cx| match event { - client::user::Event::PrivateUserInfoUpdated => { + move |user_store, event, cx| { + if let client::user::Event::PrivateUserInfoUpdated = event { assign_edit_prediction_providers( &editors, provider, @@ -70,7 +70,6 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) { cx, ); } - _ => {} } }) .detach(); diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 640f408dd3acc9a3bbf4f773d72188bcf57dac6a..916699d29b73f4b158c2179360fd2319eb711de7 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -55,10 +55,10 @@ use workspace::Workspace; use workspace::notifications::{ErrorMessagePrompt, NotificationId}; use worktree::Worktree; -const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>"; -const START_OF_FILE_MARKER: &'static str = "<|start_of_file|>"; -const EDITABLE_REGION_START_MARKER: &'static str = "<|editable_region_start|>"; -const EDITABLE_REGION_END_MARKER: &'static str = "<|editable_region_end|>"; +const CURSOR_MARKER: &str = "<|user_cursor_is_here|>"; +const START_OF_FILE_MARKER: &str = "<|start_of_file|>"; +const EDITABLE_REGION_START_MARKER: &str = "<|editable_region_start|>"; +const EDITABLE_REGION_END_MARKER: &str = "<|editable_region_end|>"; const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1); const ZED_PREDICT_DATA_COLLECTION_CHOICE: &str = "zed_predict_data_collection_choice"; @@ -166,7 +166,7 @@ fn interpolate( ) -> Option<Vec<(Range<Anchor>, String)>> { let mut edits = Vec::new(); - let mut model_edits = current_edits.into_iter().peekable(); + let mut model_edits = current_edits.iter().peekable(); for user_edit in new_snapshot.edits_since::<usize>(&old_snapshot.version) { while let Some((model_old_range, _)) = model_edits.peek() { let model_old_range = model_old_range.to_offset(old_snapshot); @@ -2123,7 +2123,7 @@ mod tests { let completion = completion_task.await.unwrap().unwrap(); completion .edits - .into_iter() + .iter() .map(|(old_range, new_text)| (old_range.to_point(&snapshot), new_text.clone())) .collect::<Vec<_>>() } diff --git a/crates/zeta_cli/src/main.rs b/crates/zeta_cli/src/main.rs index ba854b87323bbda3629e1d2ef4823db4e3c4b6f3..5b2d4cf615be67d9493d617ae7de38fdc8fa4b2f 100644 --- a/crates/zeta_cli/src/main.rs +++ b/crates/zeta_cli/src/main.rs @@ -190,9 +190,8 @@ async fn get_context( .await; // Disable data collection for these requests, as this is currently just used for evals - match gather_context_output.as_mut() { - Ok(gather_context_output) => gather_context_output.body.can_collect_data = false, - Err(_) => {} + if let Ok(gather_context_output) = gather_context_output.as_mut() { + gather_context_output.body.can_collect_data = false } gather_context_output @@ -277,8 +276,8 @@ pub fn wait_for_lang_server( let subscriptions = [ cx.subscribe(&lsp_store, { let log_prefix = log_prefix.clone(); - move |_, event, _| match event { - project::LspStoreEvent::LanguageServerUpdate { + move |_, event, _| { + if let project::LspStoreEvent::LanguageServerUpdate { message: client::proto::update_language_server::Variant::WorkProgress( client::proto::LspWorkProgress { @@ -287,8 +286,10 @@ pub fn wait_for_lang_server( }, ), .. - } => println!("{}⟲ {message}", log_prefix), - _ => {} + } = event + { + println!("{}⟲ {message}", log_prefix) + } } }), cx.subscribe(project, { diff --git a/crates/zlog/src/filter.rs b/crates/zlog/src/filter.rs index 27a5314e28b4ae580692292d5e0a64f1e7dd34bb..36a77e37bda799efe5f2bff64d95ff92f84c3468 100644 --- a/crates/zlog/src/filter.rs +++ b/crates/zlog/src/filter.rs @@ -4,7 +4,6 @@ use std::{ OnceLock, RwLock, atomic::{AtomicU8, Ordering}, }, - usize, }; use crate::{SCOPE_DEPTH_MAX, SCOPE_STRING_SEP_STR, Scope, ScopeAlloc, env_config, private}; @@ -152,7 +151,7 @@ fn scope_alloc_from_scope_str(scope_str: &str) -> Option<ScopeAlloc> { if index == 0 { return None; } - if let Some(_) = scope_iter.next() { + if scope_iter.next().is_some() { crate::warn!( "Invalid scope key, too many nested scopes: '{scope_str}'. Max depth is {SCOPE_DEPTH_MAX}", ); @@ -204,12 +203,10 @@ impl ScopeMap { .map(|(scope_str, level_filter)| (scope_str.as_str(), *level_filter)) }); - let new_filters = items_input_map - .into_iter() - .filter_map(|(scope_str, level_str)| { - let level_filter = level_filter_from_str(level_str)?; - Some((scope_str.as_str(), level_filter)) - }); + let new_filters = items_input_map.iter().filter_map(|(scope_str, level_str)| { + let level_filter = level_filter_from_str(level_str)?; + Some((scope_str.as_str(), level_filter)) + }); let all_filters = default_filters .iter() diff --git a/crates/zlog/src/zlog.rs b/crates/zlog/src/zlog.rs index d1c6cd474728026f58a89590bc76705232f12773..d0e8958df5720f60abecd296576f22e684002c35 100644 --- a/crates/zlog/src/zlog.rs +++ b/crates/zlog/src/zlog.rs @@ -10,12 +10,9 @@ pub use sink::{flush, init_output_file, init_output_stderr, init_output_stdout}; pub const SCOPE_DEPTH_MAX: usize = 4; pub fn init() { - match try_init() { - Err(err) => { - log::error!("{err}"); - eprintln!("{err}"); - } - Ok(()) => {} + if let Err(err) = try_init() { + log::error!("{err}"); + eprintln!("{err}"); } } @@ -268,7 +265,7 @@ pub mod private { pub type Scope = [&'static str; SCOPE_DEPTH_MAX]; pub type ScopeAlloc = [String; SCOPE_DEPTH_MAX]; -const SCOPE_STRING_SEP_STR: &'static str = "."; +const SCOPE_STRING_SEP_STR: &str = "."; const SCOPE_STRING_SEP_CHAR: char = '.'; #[derive(Clone, Copy, Debug, PartialEq, Eq)] From 5fb68cb8bef6a18c48a21ca7357dc7b049d3021f Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Tue, 19 Aug 2025 22:40:31 +0200 Subject: [PATCH 155/744] agent2: Token count (#36496) Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <agus@zed.dev> --- crates/acp_thread/src/acp_thread.rs | 19 ++++ crates/acp_thread/src/connection.rs | 2 +- crates/agent2/Cargo.toml | 1 + crates/agent2/src/agent.rs | 44 +++++--- crates/agent2/src/db.rs | 21 +++- crates/agent2/src/tests/mod.rs | 144 ++++++++++++++++++++++++- crates/agent2/src/thread.rs | 74 +++++++++++-- crates/agent_ui/src/acp/thread_view.rs | 41 ++++++- crates/agent_ui/src/agent_diff.rs | 1 + 9 files changed, 321 insertions(+), 26 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index d4d73e1eddd6ae0015049e82d825d6ecb508a985..793ef35be2b2f8f460ad0e503c500a3b347086bc 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -6,6 +6,7 @@ mod terminal; pub use connection::*; pub use diff::*; pub use mention::*; +use serde::{Deserialize, Serialize}; pub use terminal::*; use action_log::ActionLog; @@ -664,6 +665,12 @@ impl PlanEntry { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TokenUsage { + pub max_tokens: u64, + pub used_tokens: u64, +} + #[derive(Debug, Clone)] pub struct RetryStatus { pub last_error: SharedString, @@ -683,12 +690,14 @@ pub struct AcpThread { send_task: Option<Task<()>>, connection: Rc<dyn AgentConnection>, session_id: acp::SessionId, + token_usage: Option<TokenUsage>, } #[derive(Debug)] pub enum AcpThreadEvent { NewEntry, TitleUpdated, + TokenUsageUpdated, EntryUpdated(usize), EntriesRemoved(Range<usize>), ToolAuthorizationRequired, @@ -748,6 +757,7 @@ impl AcpThread { send_task: None, connection, session_id, + token_usage: None, } } @@ -787,6 +797,10 @@ impl AcpThread { } } + pub fn token_usage(&self) -> Option<&TokenUsage> { + self.token_usage.as_ref() + } + pub fn has_pending_edit_tool_calls(&self) -> bool { for entry in self.entries.iter().rev() { match entry { @@ -937,6 +951,11 @@ impl AcpThread { Ok(()) } + pub fn update_token_usage(&mut self, usage: Option<TokenUsage>, cx: &mut Context<Self>) { + self.token_usage = usage; + cx.emit(AcpThreadEvent::TokenUsageUpdated); + } + pub fn update_retry_status(&mut self, status: RetryStatus, cx: &mut Context<Self>) { cx.emit(AcpThreadEvent::Retry(status)); } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index b09f383029d0d2e635076a729204b5eddadd5ad5..8cae975ce553b185f2a9c7d4d69d568e9c28673a 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -10,7 +10,7 @@ use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc}; use ui::{App, IconName}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub struct UserMessageId(Arc<str>); impl UserMessageId { diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 890f7e774b417a68bbe3d9acbeff1f90fd40782a..d18773ff7bee92488c5e6ecee8cd6a03b0a6c564 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -66,6 +66,7 @@ zstd.workspace = true [dev-dependencies] agent = { workspace = true, "features" = ["test-support"] } +assistant_context = { workspace = true, "features" = ["test-support"] } ctor.workspace = true client = { workspace = true, "features" = ["test-support"] } clock = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 48f46a52fcc7324140bddaff1640307d985aaa04..6303144d96e740230006fb4f3fb8c09bd854dc3d 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,8 +1,8 @@ +use crate::HistoryStore; use crate::{ - ContextServerRegistry, Thread, ThreadEvent, ToolCallAuthorization, UserMessageContent, - templates::Templates, + ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization, + UserMessageContent, templates::Templates, }; -use crate::{HistoryStore, ThreadsDatabase}; use acp_thread::{AcpThread, AgentModelSelector}; use action_log::ActionLog; use agent_client_protocol as acp; @@ -673,6 +673,11 @@ impl NativeAgentConnection { thread.update_tool_call(update, cx) })??; } + ThreadEvent::TokenUsageUpdate(usage) => { + acp_thread.update(cx, |thread, cx| { + thread.update_token_usage(Some(usage), cx) + })?; + } ThreadEvent::TitleUpdate(title) => { acp_thread .update(cx, |thread, cx| thread.update_title(title, cx))??; @@ -895,10 +900,12 @@ impl acp_thread::AgentConnection for NativeAgentConnection { cx: &mut App, ) -> Option<Rc<dyn acp_thread::AgentSessionEditor>> { self.0.update(cx, |agent, _cx| { - agent - .sessions - .get(session_id) - .map(|session| Rc::new(NativeAgentSessionEditor(session.thread.clone())) as _) + agent.sessions.get(session_id).map(|session| { + Rc::new(NativeAgentSessionEditor { + thread: session.thread.clone(), + acp_thread: session.acp_thread.clone(), + }) as _ + }) }) } @@ -907,14 +914,27 @@ impl acp_thread::AgentConnection for NativeAgentConnection { } } -struct NativeAgentSessionEditor(Entity<Thread>); +struct NativeAgentSessionEditor { + thread: Entity<Thread>, + acp_thread: WeakEntity<AcpThread>, +} impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor { fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> { - Task::ready( - self.0 - .update(cx, |thread, cx| thread.truncate(message_id, cx)), - ) + match self.thread.update(cx, |thread, cx| { + thread.truncate(message_id.clone(), cx)?; + Ok(thread.latest_token_usage()) + }) { + Ok(usage) => { + self.acp_thread + .update(cx, |thread, cx| { + thread.update_token_usage(usage, cx); + }) + .ok(); + Task::ready(Ok(())) + } + Err(error) => Task::ready(Err(error)), + } } } diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index 27a109c573d956f592ac4fd8540aab42ae414a75..610a2575c4dadeee3e19e8bee2020fd5ead7d5f9 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent2/src/db.rs @@ -1,4 +1,5 @@ use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent}; +use acp_thread::UserMessageId; use agent::thread_store; use agent_client_protocol as acp; use agent_settings::{AgentProfileId, CompletionMode}; @@ -42,7 +43,7 @@ pub struct DbThread { #[serde(default)] pub cumulative_token_usage: language_model::TokenUsage, #[serde(default)] - pub request_token_usage: Vec<language_model::TokenUsage>, + pub request_token_usage: HashMap<acp_thread::UserMessageId, language_model::TokenUsage>, #[serde(default)] pub model: Option<DbLanguageModel>, #[serde(default)] @@ -67,7 +68,10 @@ impl DbThread { fn upgrade_from_agent_1(thread: agent::SerializedThread) -> Result<Self> { let mut messages = Vec::new(); - for msg in thread.messages { + let mut request_token_usage = HashMap::default(); + + let mut last_user_message_id = None; + for (ix, msg) in thread.messages.into_iter().enumerate() { let message = match msg.role { language_model::Role::User => { let mut content = Vec::new(); @@ -93,9 +97,12 @@ impl DbThread { content.push(UserMessageContent::Text(msg.context)); } + let id = UserMessageId::new(); + last_user_message_id = Some(id.clone()); + crate::Message::User(UserMessage { // MessageId from old format can't be meaningfully converted, so generate a new one - id: acp_thread::UserMessageId::new(), + id, content, }) } @@ -154,6 +161,12 @@ impl DbThread { ); } + if let Some(last_user_message_id) = &last_user_message_id + && let Some(token_usage) = thread.request_token_usage.get(ix).copied() + { + request_token_usage.insert(last_user_message_id.clone(), token_usage); + } + crate::Message::Agent(AgentMessage { content, tool_results, @@ -175,7 +188,7 @@ impl DbThread { summary: thread.detailed_summary_state, initial_project_snapshot: thread.initial_project_snapshot, cumulative_token_usage: thread.cumulative_token_usage, - request_token_usage: thread.request_token_usage, + request_token_usage, model: thread.model, completion_mode: thread.completion_mode, profile: thread.profile, diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 7fa12e57117c55d78f3a910188c74bb1057330bf..d07ca42d3be6455c0917177506f3b33d3e1c5c94 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1117,7 +1117,7 @@ async fn test_refusal(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_truncate(cx: &mut TestAppContext) { +async fn test_truncate_first_message(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); @@ -1137,9 +1137,18 @@ async fn test_truncate(cx: &mut TestAppContext) { Hello "} ); + assert_eq!(thread.latest_token_usage(), None); }); fake_model.send_last_completion_stream_text_chunk("Hey!"); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( + language_model::TokenUsage { + input_tokens: 32_000, + output_tokens: 16_000, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + )); cx.run_until_parked(); thread.read_with(cx, |thread, _| { assert_eq!( @@ -1154,6 +1163,13 @@ async fn test_truncate(cx: &mut TestAppContext) { Hey! "} ); + assert_eq!( + thread.latest_token_usage(), + Some(acp_thread::TokenUsage { + used_tokens: 32_000 + 16_000, + max_tokens: 1_000_000, + }) + ); }); thread @@ -1162,6 +1178,7 @@ async fn test_truncate(cx: &mut TestAppContext) { cx.run_until_parked(); thread.read_with(cx, |thread, _| { assert_eq!(thread.to_markdown(), ""); + assert_eq!(thread.latest_token_usage(), None); }); // Ensure we can still send a new message after truncation. @@ -1182,6 +1199,14 @@ async fn test_truncate(cx: &mut TestAppContext) { }); cx.run_until_parked(); fake_model.send_last_completion_stream_text_chunk("Ahoy!"); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( + language_model::TokenUsage { + input_tokens: 40_000, + output_tokens: 20_000, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + )); cx.run_until_parked(); thread.read_with(cx, |thread, _| { assert_eq!( @@ -1196,7 +1221,124 @@ async fn test_truncate(cx: &mut TestAppContext) { Ahoy! "} ); + + assert_eq!( + thread.latest_token_usage(), + Some(acp_thread::TokenUsage { + used_tokens: 40_000 + 20_000, + max_tokens: 1_000_000, + }) + ); + }); +} + +#[gpui::test] +async fn test_truncate_second_message(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Message 1"], cx) + }) + .unwrap(); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Message 1 response"); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( + language_model::TokenUsage { + input_tokens: 32_000, + output_tokens: 16_000, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + let assert_first_message_state = |cx: &mut TestAppContext| { + thread.clone().read_with(cx, |thread, _| { + assert_eq!( + thread.to_markdown(), + indoc! {" + ## User + + Message 1 + + ## Assistant + + Message 1 response + "} + ); + + assert_eq!( + thread.latest_token_usage(), + Some(acp_thread::TokenUsage { + used_tokens: 32_000 + 16_000, + max_tokens: 1_000_000, + }) + ); + }); + }; + + assert_first_message_state(cx); + + let second_message_id = UserMessageId::new(); + thread + .update(cx, |thread, cx| { + thread.send(second_message_id.clone(), ["Message 2"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + fake_model.send_last_completion_stream_text_chunk("Message 2 response"); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( + language_model::TokenUsage { + input_tokens: 40_000, + output_tokens: 20_000, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + thread.read_with(cx, |thread, _| { + assert_eq!( + thread.to_markdown(), + indoc! {" + ## User + + Message 1 + + ## Assistant + + Message 1 response + + ## User + + Message 2 + + ## Assistant + + Message 2 response + "} + ); + + assert_eq!( + thread.latest_token_usage(), + Some(acp_thread::TokenUsage { + used_tokens: 40_000 + 20_000, + max_tokens: 1_000_000, + }) + ); }); + + thread + .update(cx, |thread, cx| thread.truncate(second_message_id, cx)) + .unwrap(); + cx.run_until_parked(); + + assert_first_message_state(cx); } #[gpui::test] diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index ba5cd1f47725c1d1a5edd7855295b3cc05b4ecad..4bc45f1544acc7e1d5e947a866b4754ee24aadf0 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -13,7 +13,7 @@ use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; use chrono::{DateTime, Utc}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus}; -use collections::IndexMap; +use collections::{HashMap, IndexMap}; use fs::Fs; use futures::{ FutureExt, @@ -24,8 +24,8 @@ use futures::{ use git::repository::DiffType; use gpui::{App, AppContext, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; use language_model::{ - LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, - LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest, + LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt, + LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, @@ -481,6 +481,7 @@ pub enum ThreadEvent { ToolCall(acp::ToolCall), ToolCallUpdate(acp_thread::ToolCallUpdate), ToolCallAuthorization(ToolCallAuthorization), + TokenUsageUpdate(acp_thread::TokenUsage), TitleUpdate(SharedString), Retry(acp_thread::RetryStatus), Stop(acp::StopReason), @@ -509,8 +510,7 @@ pub struct Thread { pending_message: Option<AgentMessage>, tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>, tool_use_limit_reached: bool, - #[allow(unused)] - request_token_usage: Vec<TokenUsage>, + request_token_usage: HashMap<UserMessageId, language_model::TokenUsage>, #[allow(unused)] cumulative_token_usage: TokenUsage, #[allow(unused)] @@ -548,7 +548,7 @@ impl Thread { pending_message: None, tools: BTreeMap::default(), tool_use_limit_reached: false, - request_token_usage: Vec::new(), + request_token_usage: HashMap::default(), cumulative_token_usage: TokenUsage::default(), initial_project_snapshot: { let project_snapshot = Self::project_snapshot(project.clone(), cx); @@ -951,6 +951,15 @@ impl Thread { self.flush_pending_message(cx); } + pub fn update_token_usage(&mut self, update: language_model::TokenUsage) { + let Some(last_user_message) = self.last_user_message() else { + return; + }; + + self.request_token_usage + .insert(last_user_message.id.clone(), update); + } + pub fn truncate(&mut self, message_id: UserMessageId, cx: &mut Context<Self>) -> Result<()> { self.cancel(cx); let Some(position) = self.messages.iter().position( @@ -958,11 +967,31 @@ impl Thread { ) else { return Err(anyhow!("Message not found")); }; - self.messages.truncate(position); + + for message in self.messages.drain(position..) { + match message { + Message::User(message) => { + self.request_token_usage.remove(&message.id); + } + Message::Agent(_) | Message::Resume => {} + } + } + cx.notify(); Ok(()) } + pub fn latest_token_usage(&self) -> Option<acp_thread::TokenUsage> { + let last_user_message = self.last_user_message()?; + let tokens = self.request_token_usage.get(&last_user_message.id)?; + let model = self.model.clone()?; + + Some(acp_thread::TokenUsage { + max_tokens: model.max_token_count_for_mode(self.completion_mode.into()), + used_tokens: tokens.total_tokens(), + }) + } + pub fn resume( &mut self, cx: &mut Context<Self>, @@ -1148,6 +1177,21 @@ impl Thread { )) => { *tool_use_limit_reached = true; } + Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => { + let usage = acp_thread::TokenUsage { + max_tokens: model.max_token_count_for_mode( + request + .mode + .unwrap_or(cloud_llm_client::CompletionMode::Normal), + ), + used_tokens: token_usage.total_tokens(), + }; + + this.update(cx, |this, _cx| this.update_token_usage(token_usage)) + .ok(); + + event_stream.send_token_usage_update(usage); + } Ok(LanguageModelCompletionEvent::Stop(StopReason::Refusal)) => { *refusal = true; return Ok(FuturesUnordered::default()); @@ -1532,6 +1576,16 @@ impl Thread { }) })) } + fn last_user_message(&self) -> Option<&UserMessage> { + self.messages + .iter() + .rev() + .find_map(|message| match message { + Message::User(user_message) => Some(user_message), + Message::Agent(_) => None, + Message::Resume => None, + }) + } fn pending_message(&mut self) -> &mut AgentMessage { self.pending_message.get_or_insert_default() @@ -2051,6 +2105,12 @@ impl ThreadEventStream { .ok(); } + fn send_token_usage_update(&self, usage: acp_thread::TokenUsage) { + self.0 + .unbounded_send(Ok(ThreadEvent::TokenUsageUpdate(usage))) + .ok(); + } + fn send_retry(&self, status: acp_thread::RetryStatus) { self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 9f1e8d857fb772b5dd720986c8dc85323943f94f..878891c6f1d9baf11135eac464aeb5d9843cfb44 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -816,7 +816,7 @@ impl AcpThreadView { self.thread_retry_status.take(); self.thread_state = ThreadState::ServerExited { status: *status }; } - AcpThreadEvent::TitleUpdated => {} + AcpThreadEvent::TitleUpdated | AcpThreadEvent::TokenUsageUpdated => {} } cx.notify(); } @@ -2794,6 +2794,7 @@ impl AcpThreadView { .child( h_flex() .gap_1() + .children(self.render_token_usage(cx)) .children(self.profile_selector.clone()) .children(self.model_selector.clone()) .child(self.render_send_button(cx)), @@ -2816,6 +2817,44 @@ impl AcpThreadView { .thread(acp_thread.session_id(), cx) } + fn render_token_usage(&self, cx: &mut Context<Self>) -> Option<Div> { + let thread = self.thread()?.read(cx); + let usage = thread.token_usage()?; + let is_generating = thread.status() != ThreadStatus::Idle; + + let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens); + let max = crate::text_thread_editor::humanize_token_count(usage.max_tokens); + + Some( + h_flex() + .flex_shrink_0() + .gap_0p5() + .mr_1() + .child( + Label::new(used) + .size(LabelSize::Small) + .color(Color::Muted) + .map(|label| { + if is_generating { + label + .with_animation( + "used-tokens-label", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.6, 1.)), + |label, delta| label.alpha(delta), + ) + .into_any() + } else { + label.into_any_element() + } + }), + ) + .child(Label::new("/").size(LabelSize::Small).color(Color::Muted)) + .child(Label::new(max).size(LabelSize::Small).color(Color::Muted)), + ) + } + fn toggle_burn_mode( &mut self, _: &ToggleBurnMode, diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 9d2ee0bf89ba3236f563a2374c37ee3b3e354b4a..a695136562c850ce55de57578eddf4c40aa28702 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1526,6 +1526,7 @@ impl AgentDiff { self.update_reviewing_editors(workspace, window, cx); } AcpThreadEvent::TitleUpdated + | AcpThreadEvent::TokenUsageUpdated | AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::ToolAuthorizationRequired | AcpThreadEvent::Retry(_) => {} From 88c4a5ca49799637b7cc790771de65bd9b4b5253 Mon Sep 17 00:00:00 2001 From: Julia Ryan <juliaryan3.14@gmail.com> Date: Tue, 19 Aug 2025 16:31:13 -0500 Subject: [PATCH 156/744] Suspend macOS threads during crashes (#36520) This should improve our detection of which thread crashed since they wont be able to resume while the minidump is being generated. Release Notes: - N/A --- Cargo.lock | 20 +++++++++++++++----- Cargo.toml | 1 + crates/crashes/Cargo.toml | 3 +++ crates/crashes/src/crashes.rs | 20 ++++++++++++++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a5dec4734e8bbb0ad82bc9f0e5d027c1bcf19ea..d1f4b22e9d12a1573a24be05e63bec2bee1bfc06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3872,7 +3872,7 @@ dependencies = [ "jni", "js-sys", "libc", - "mach2", + "mach2 0.4.2", "ndk", "ndk-context", "num-derive", @@ -4022,7 +4022,7 @@ checksum = "031ed29858d90cfdf27fe49fae28028a1f20466db97962fa2f4ea34809aeebf3" dependencies = [ "cfg-if", "libc", - "mach2", + "mach2 0.4.2", ] [[package]] @@ -4034,7 +4034,7 @@ dependencies = [ "cfg-if", "crash-context", "libc", - "mach2", + "mach2 0.4.2", "parking_lot", ] @@ -4044,6 +4044,7 @@ version = "0.1.0" dependencies = [ "crash-handler", "log", + "mach2 0.5.0", "minidumper", "paths", "release_channel", @@ -9866,6 +9867,15 @@ dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -10202,7 +10212,7 @@ dependencies = [ "goblin", "libc", "log", - "mach2", + "mach2 0.4.2", "memmap2", "memoffset", "minidump-common", @@ -18292,7 +18302,7 @@ dependencies = [ "indexmap", "libc", "log", - "mach2", + "mach2 0.4.2", "memfd", "object", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index ad45def2d4f51d27e99a5f4afe0910524fe1ab5c..dc14c8ebd92c43afcc9622daa129d07b1375a516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -515,6 +515,7 @@ libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" } +mach2 = "0.5" markup5ever_rcdom = "0.3.0" metal = "0.29" minidumper = "0.8" diff --git a/crates/crashes/Cargo.toml b/crates/crashes/Cargo.toml index 2420b499f8fecb3d66f2cabbce57bbd39fd19a7c..f12913d1cbda34219430d8aba8e56431a056da59 100644 --- a/crates/crashes/Cargo.toml +++ b/crates/crashes/Cargo.toml @@ -16,6 +16,9 @@ serde.workspace = true serde_json.workspace = true workspace-hack.workspace = true +[target.'cfg(target_os = "macos")'.dependencies] +mach2.workspace = true + [lints] workspace = true diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index ddf6468be817638d40cea3bfdd2a00e8a83e998f..12997f51a33464a66eb9dd5db8a78b606115bf3b 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -74,6 +74,9 @@ pub async fn init(crash_init: InitCrashHandler) { .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) .is_ok() { + #[cfg(target_os = "macos")] + suspend_all_other_threads(); + client.ping().unwrap(); client.request_dump(crash_context).is_ok() } else { @@ -98,6 +101,23 @@ pub async fn init(crash_init: InitCrashHandler) { } } +#[cfg(target_os = "macos")] +unsafe fn suspend_all_other_threads() { + let task = unsafe { mach2::traps::current_task() }; + let mut threads: mach2::mach_types::thread_act_array_t = std::ptr::null_mut(); + let mut count = 0; + unsafe { + mach2::task::task_threads(task, &raw mut threads, &raw mut count); + } + let current = unsafe { mach2::mach_init::mach_thread_self() }; + for i in 0..count { + let t = unsafe { *threads.add(i as usize) }; + if t != current { + unsafe { mach2::thread_act::thread_suspend(t) }; + } + } +} + pub struct CrashServer { initialization_params: OnceLock<InitCrashHandler>, panic_info: OnceLock<CrashPanic>, From 88754a70f7f2a566daf26980ed177d8c0e3b3240 Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Tue, 19 Aug 2025 16:26:30 -0600 Subject: [PATCH 157/744] Rebuild recently opened threads for ACP (#36531) Closes #ISSUE Release Notes: - N/A --- Cargo.lock | 1 + crates/agent2/Cargo.toml | 1 + crates/agent2/src/agent.rs | 6 +- crates/agent2/src/history_store.rs | 102 +++++++++++++------------ crates/agent2/src/tests/mod.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 39 ++++++++-- crates/agent_ui/src/agent_panel.rs | 21 +++-- 7 files changed, 109 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1f4b22e9d12a1573a24be05e63bec2bee1bfc06..34a8ceac4937beab70da4360450c794b82e23934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,6 +206,7 @@ dependencies = [ "collections", "context_server", "ctor", + "db", "editor", "env_logger 0.11.8", "fs", diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index d18773ff7bee92488c5e6ecee8cd6a03b0a6c564..849ea041e93c206d12898b2264e026a676c0cfce 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -26,6 +26,7 @@ chrono.workspace = true cloud_llm_client.workspace = true collections.workspace = true context_server.workspace = true +db.workspace = true fs.workspace = true futures.workspace = true git.workspace = true diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 6303144d96e740230006fb4f3fb8c09bd854dc3d..212460d6909ffa7620f712f0230a66b42e377fb2 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -974,7 +974,7 @@ mod tests { .await; let project = Project::test(fs.clone(), [], cx).await; let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); let agent = NativeAgent::new( project.clone(), history_store, @@ -1032,7 +1032,7 @@ mod tests { fs.insert_tree("/", json!({ "a": {} })).await; let project = Project::test(fs.clone(), [], cx).await; let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); let connection = NativeAgentConnection( NativeAgent::new( project.clone(), @@ -1088,7 +1088,7 @@ mod tests { let project = Project::test(fs.clone(), [], cx).await; let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); // Create the agent and connection let agent = NativeAgent::new( diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 34a5e7b4efe313291dcd04b6c8c2c8f4e1e22e53..4ce304ae5ff8acd96d53d4f77e2c4c5f2e970aee 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -3,6 +3,7 @@ use agent_client_protocol as acp; use anyhow::{Context as _, Result, anyhow}; use assistant_context::SavedContextMetadata; use chrono::{DateTime, Utc}; +use db::kvp::KEY_VALUE_STORE; use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*}; use itertools::Itertools; use paths::contexts_dir; @@ -11,7 +12,7 @@ use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration}; use util::ResultExt as _; const MAX_RECENTLY_OPENED_ENTRIES: usize = 6; -const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json"; +const RECENTLY_OPENED_THREADS_KEY: &str = "recent-agent-threads"; const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50); const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread"); @@ -53,12 +54,10 @@ pub enum HistoryEntryId { TextThread(Arc<Path>), } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] enum SerializedRecentOpen { - Thread(String), - ContextName(String), - /// Old format which stores the full path - Context(String), + AcpThread(String), + TextThread(String), } pub struct HistoryStore { @@ -72,29 +71,26 @@ pub struct HistoryStore { impl HistoryStore { pub fn new( context_store: Entity<assistant_context::ContextStore>, - initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>, cx: &mut Context<Self>, ) -> Self { let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())]; cx.spawn(async move |this, cx| { - let entries = Self::load_recently_opened_entries(cx).await.log_err()?; - this.update(cx, |this, _| { - this.recently_opened_entries - .extend( - entries.into_iter().take( - MAX_RECENTLY_OPENED_ENTRIES - .saturating_sub(this.recently_opened_entries.len()), - ), - ); + let entries = Self::load_recently_opened_entries(cx).await; + this.update(cx, |this, cx| { + if let Some(entries) = entries.log_err() { + this.recently_opened_entries = entries; + } + + this.reload(cx); }) - .ok() + .ok(); }) .detach(); Self { context_store, - recently_opened_entries: initial_recent_entries.into_iter().collect(), + recently_opened_entries: VecDeque::default(), threads: Vec::default(), _subscriptions: subscriptions, _save_recently_opened_entries_task: Task::ready(()), @@ -134,6 +130,18 @@ impl HistoryStore { .await?; this.update(cx, |this, cx| { + if this.recently_opened_entries.len() < MAX_RECENTLY_OPENED_ENTRIES { + for thread in threads + .iter() + .take(MAX_RECENTLY_OPENED_ENTRIES - this.recently_opened_entries.len()) + .rev() + { + this.push_recently_opened_entry( + HistoryEntryId::AcpThread(thread.id.clone()), + cx, + ) + } + } this.threads = threads; cx.notify(); }) @@ -162,6 +170,16 @@ impl HistoryStore { history_entries } + pub fn is_empty(&self, cx: &App) -> bool { + self.threads.is_empty() + && self + .context_store + .read(cx) + .unordered_contexts() + .next() + .is_none() + } + pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> { self.entries(cx).into_iter().take(limit).collect() } @@ -215,58 +233,44 @@ impl HistoryStore { .iter() .filter_map(|entry| match entry { HistoryEntryId::TextThread(path) => path.file_name().map(|file| { - SerializedRecentOpen::ContextName(file.to_string_lossy().to_string()) + SerializedRecentOpen::TextThread(file.to_string_lossy().to_string()) }), - HistoryEntryId::AcpThread(id) => Some(SerializedRecentOpen::Thread(id.to_string())), + HistoryEntryId::AcpThread(id) => { + Some(SerializedRecentOpen::AcpThread(id.to_string())) + } }) .collect::<Vec<_>>(); self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| { + let content = serde_json::to_string(&serialized_entries).unwrap(); cx.background_executor() .timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE) .await; - cx.background_spawn(async move { - let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH); - let content = serde_json::to_string(&serialized_entries)?; - std::fs::write(path, content)?; - anyhow::Ok(()) - }) - .await - .log_err(); + KEY_VALUE_STORE + .write_kvp(RECENTLY_OPENED_THREADS_KEY.to_owned(), content) + .await + .log_err(); }); } - fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<Vec<HistoryEntryId>>> { + fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> { cx.background_spawn(async move { - let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH); - let contents = match smol::fs::read_to_string(path).await { - Ok(it) => it, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - return Ok(Vec::new()); - } - Err(e) => { - return Err(e) - .context("deserializing persisted agent panel navigation history"); - } - }; - let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&contents) + let json = KEY_VALUE_STORE + .read_kvp(RECENTLY_OPENED_THREADS_KEY)? + .unwrap_or("[]".to_string()); + let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&json) .context("deserializing persisted agent panel navigation history")? .into_iter() .take(MAX_RECENTLY_OPENED_ENTRIES) .flat_map(|entry| match entry { - SerializedRecentOpen::Thread(id) => Some(HistoryEntryId::AcpThread( + SerializedRecentOpen::AcpThread(id) => Some(HistoryEntryId::AcpThread( acp::SessionId(id.as_str().into()), )), - SerializedRecentOpen::ContextName(file_name) => Some( + SerializedRecentOpen::TextThread(file_name) => Some( HistoryEntryId::TextThread(contexts_dir().join(file_name).into()), ), - SerializedRecentOpen::Context(path) => { - Path::new(&path).file_name().map(|file_name| { - HistoryEntryId::TextThread(contexts_dir().join(file_name).into()) - }) - } }) - .collect::<Vec<_>>(); + .collect(); Ok(entries) }) } diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index d07ca42d3be6455c0917177506f3b33d3e1c5c94..55bfa6f0b5319bd60e30ae6f8e00ba20b7597e5d 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1414,7 +1414,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) { let project = Project::test(fake_fs.clone(), [Path::new("/test")], cx).await; let cwd = Path::new("/test"); let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); // Create agent and connection let agent = NativeAgent::new( diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 878891c6f1d9baf11135eac464aeb5d9843cfb44..5e5d4bb83c8c3461568fdfce46b5a0d0e0bb1420 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -9,7 +9,7 @@ use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::{self as acp}; use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; -use agent2::DbThreadMetadata; +use agent2::{DbThreadMetadata, HistoryEntryId, HistoryStore}; use anyhow::bail; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; @@ -111,6 +111,7 @@ pub struct AcpThreadView { workspace: WeakEntity<Workspace>, project: Entity<Project>, thread_state: ThreadState, + history_store: Entity<HistoryStore>, entry_view_state: Entity<EntryViewState>, message_editor: Entity<MessageEditor>, model_selector: Option<Entity<AcpModelSelectorPopover>>, @@ -159,6 +160,7 @@ impl AcpThreadView { resume_thread: Option<DbThreadMetadata>, workspace: WeakEntity<Workspace>, project: Entity<Project>, + history_store: Entity<HistoryStore>, thread_store: Entity<ThreadStore>, text_thread_store: Entity<TextThreadStore>, window: &mut Window, @@ -223,6 +225,7 @@ impl AcpThreadView { plan_expanded: false, editor_expanded: false, terminal_expanded: true, + history_store, _subscriptions: subscriptions, _cancel_task: None, } @@ -260,7 +263,7 @@ impl AcpThreadView { let result = if let Some(native_agent) = connection .clone() .downcast::<agent2::NativeAgentConnection>() - && let Some(resume) = resume_thread + && let Some(resume) = resume_thread.clone() { cx.update(|_, cx| { native_agent @@ -313,6 +316,15 @@ impl AcpThreadView { } }); + if let Some(resume) = resume_thread { + this.history_store.update(cx, |history, cx| { + history.push_recently_opened_entry( + HistoryEntryId::AcpThread(resume.id), + cx, + ); + }); + } + AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx); this.model_selector = @@ -555,9 +567,15 @@ impl AcpThreadView { } fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) { - if let Some(thread) = self.thread() - && thread.read(cx).status() != ThreadStatus::Idle - { + let Some(thread) = self.thread() else { return }; + self.history_store.update(cx, |history, cx| { + history.push_recently_opened_entry( + HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()), + cx, + ); + }); + + if thread.read(cx).status() != ThreadStatus::Idle { self.stop_current_and_send_new_message(window, cx); return; } @@ -3942,6 +3960,7 @@ pub(crate) mod tests { use acp_thread::StubAgentConnection; use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::SessionId; + use assistant_context::ContextStore; use editor::EditorSettings; use fs::FakeFs; use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext}; @@ -4079,6 +4098,10 @@ pub(crate) mod tests { cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx))); let text_thread_store = cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); + let context_store = + cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx))); + let history_store = + cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(context_store, cx))); let thread_view = cx.update(|window, cx| { cx.new(|cx| { @@ -4087,6 +4110,7 @@ pub(crate) mod tests { None, workspace.downgrade(), project, + history_store, thread_store.clone(), text_thread_store.clone(), window, @@ -4283,6 +4307,10 @@ pub(crate) mod tests { cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx))); let text_thread_store = cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); + let context_store = + cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx))); + let history_store = + cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(context_store, cx))); let connection = Rc::new(StubAgentConnection::new()); let thread_view = cx.update(|window, cx| { @@ -4292,6 +4320,7 @@ pub(crate) mod tests { None, workspace.downgrade(), project.clone(), + history_store.clone(), thread_store.clone(), text_thread_store.clone(), window, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index c5cab340302b16a06f0eb63fe70c5cb8a4dafa82..0310ae7c8062fd26d9a16e2e377af20e782ad8ae 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -648,8 +648,7 @@ impl AgentPanel { ) }); - let acp_history_store = - cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), [], cx)); + let acp_history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx)); let acp_history = cx.new(|cx| AcpThreadHistory::new(acp_history_store.clone(), window, cx)); cx.subscribe_in( &acp_history, @@ -1073,6 +1072,7 @@ impl AgentPanel { resume_thread, workspace.clone(), project, + this.acp_history_store.clone(), thread_store.clone(), text_thread_store.clone(), window, @@ -1609,6 +1609,14 @@ impl AgentPanel { if let Some(path) = context_editor.read(cx).context().read(cx).path() { store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx) } + }); + self.acp_history_store.update(cx, |store, cx| { + if let Some(path) = context_editor.read(cx).context().read(cx).path() { + store.push_recently_opened_entry( + agent2::HistoryEntryId::TextThread(path.clone()), + cx, + ) + } }) } ActiveView::ExternalAgentThread { .. } => {} @@ -2763,9 +2771,12 @@ impl AgentPanel { false } _ => { - let history_is_empty = self - .history_store - .update(cx, |store, cx| store.recent_entries(1, cx).is_empty()); + let history_is_empty = if cx.has_flag::<AcpFeatureFlag>() { + self.acp_history_store.read(cx).is_empty(cx) + } else { + self.history_store + .update(cx, |store, cx| store.recent_entries(1, cx).is_empty()) + }; let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx) .providers() From ecee6746ecada543ae89d37ff3882a38dd555cae Mon Sep 17 00:00:00 2001 From: Julia Ryan <juliaryan3.14@gmail.com> Date: Tue, 19 Aug 2025 17:37:39 -0500 Subject: [PATCH 158/744] Attach minidump errors to uploaded crash events (#36527) We see a bunch of crash events with truncated minidumps where they have a valid header but no events. We think this is due to an issue generating them, so we're attaching the relevant result to the uploaded tags. Release Notes: - N/A Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com> --- crates/crashes/src/crashes.rs | 12 ++++++------ crates/zed/src/reliability.rs | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index 12997f51a33464a66eb9dd5db8a78b606115bf3b..4e4b69f639f72edae2d1bbfd9c09191f1835345b 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -128,6 +128,7 @@ pub struct CrashServer { pub struct CrashInfo { pub init: InitCrashHandler, pub panic: Option<CrashPanic>, + pub minidump_error: Option<String>, } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -162,16 +163,14 @@ impl minidumper::ServerHandler for CrashServer { } fn on_minidump_created(&self, result: Result<MinidumpBinary, minidumper::Error>) -> LoopAction { - match result { + let minidump_error = match result { Ok(mut md_bin) => { use io::Write; let _ = md_bin.file.flush(); - info!("wrote minidump to disk {:?}", md_bin.path); + None } - Err(e) => { - info!("failed to write minidump: {:#}", e); - } - } + Err(e) => Some(format!("{e:?}")), + }; let crash_info = CrashInfo { init: self @@ -180,6 +179,7 @@ impl minidumper::ServerHandler for CrashServer { .expect("not initialized") .clone(), panic: self.panic_info.get().cloned(), + minidump_error, }; let crash_data_path = paths::logs_dir() diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index cbd31c2e26471292cf1ccbac45720082f672f7d9..f55468280ce3d17aa789780c4bfb2a6d6acfb514 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -607,6 +607,9 @@ async fn upload_minidump( // TODO: add gpu-context, feature-flag-context, and more of device-context like gpu // name, screen resolution, available ram, device model, etc } + if let Some(minidump_error) = metadata.minidump_error.clone() { + form = form.text("minidump_error", minidump_error); + } let mut response_text = String::new(); let mut response = http.send_multipart_form(endpoint, form).await?; From 757b37fd41b459988c0741f2d51b1e77d02b9d3f Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Tue, 19 Aug 2025 16:42:52 -0600 Subject: [PATCH 159/744] Hide old Agent UI when ACP flag set (#36533) - **Use key value store instead of JSON** - **Default NewThread to the native agent when flagged** Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/agent_ui/src/agent_panel.rs | 34 ++++++------------------------ 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 0310ae7c8062fd26d9a16e2e377af20e782ad8ae..93e9f619af31b4618de862290e3bc8c20d6f74d9 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -881,6 +881,9 @@ impl AgentPanel { } fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) { + if cx.has_flag::<AcpFeatureFlag>() { + return self.new_agent_thread(AgentType::NativeAgent, window, cx); + } // Preserve chat box text when using creating new thread let preserved_text = self .active_message_editor() @@ -2386,9 +2389,9 @@ impl AgentPanel { }) .item( ContextMenuEntry::new("New Thread") - .icon(IconName::Thread) - .icon_color(Color::Muted) .action(NewThread::default().boxed_clone()) + .icon(IconName::ZedAssistant) + .icon_color(Color::Muted) .handler({ let workspace = workspace.clone(); move |window, cx| { @@ -2399,7 +2402,7 @@ impl AgentPanel { { panel.update(cx, |panel, cx| { panel.set_selected_agent( - AgentType::Zed, + AgentType::NativeAgent, window, cx, ); @@ -2436,31 +2439,6 @@ impl AgentPanel { } }), ) - .item( - ContextMenuEntry::new("New Native Agent Thread") - .icon(IconName::ZedAssistant) - .icon_color(Color::Muted) - .handler({ - let workspace = workspace.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::<AgentPanel>(cx) - { - panel.update(cx, |panel, cx| { - panel.set_selected_agent( - AgentType::NativeAgent, - window, - cx, - ); - }); - } - }); - } - } - }), - ) .separator() .header("External Agents") .when(cx.has_flag::<AcpFeatureFlag>(), |menu| { From 82ac8a8aaaac089a9e2d1333108686cf2f11636f Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Tue, 19 Aug 2025 20:25:07 -0400 Subject: [PATCH 160/744] collab: Make `stripe_subscription_id` and `stripe_subscription_status` nullable on `billing_subscriptions` (#36536) This PR makes the `stripe_subscription_id` and `stripe_subscription_status` columns nullable on the `billing_subscriptions` table. Release Notes: - N/A --- ...916_make_stripe_fields_optional_on_billing_subscription.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 crates/collab/migrations/20250819225916_make_stripe_fields_optional_on_billing_subscription.sql diff --git a/crates/collab/migrations/20250819225916_make_stripe_fields_optional_on_billing_subscription.sql b/crates/collab/migrations/20250819225916_make_stripe_fields_optional_on_billing_subscription.sql new file mode 100644 index 0000000000000000000000000000000000000000..cf3b79da60be98da8dd78a2bcb01f7532be7fc59 --- /dev/null +++ b/crates/collab/migrations/20250819225916_make_stripe_fields_optional_on_billing_subscription.sql @@ -0,0 +1,3 @@ +alter table billing_subscriptions + alter column stripe_subscription_id drop not null, + alter column stripe_subscription_status drop not null; From ce216432be5a967feb0d30ee9878d0cf4fb07cb7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld <maxbrunsfeld@gmail.com> Date: Tue, 19 Aug 2025 17:33:56 -0700 Subject: [PATCH 161/744] Refactor ssh remoting - make ChannelClient type private (#36514) This PR is one step in a series of refactors to prepare for having "remote" projects that do not use SSH. The main use cases for this are WSL and dev containers. Release Notes: - N/A --- crates/editor/src/editor.rs | 5 +- crates/project/src/project.rs | 23 +-- crates/remote/src/ssh_session.rs | 146 +++++++++---------- crates/remote_server/src/headless_project.rs | 67 ++++----- crates/remote_server/src/unix.rs | 13 +- crates/rpc/src/proto_client.rs | 19 +++ crates/tasks_ui/src/tasks_ui.rs | 6 +- 7 files changed, 133 insertions(+), 146 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 38059042439cb7c2610e4a73828796d3f8c6d434..f943e64923bb91525e2c189bfb9c896b28d9bcf3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -14895,10 +14895,7 @@ impl Editor { }; let hide_runnables = project - .update(cx, |project, cx| { - // Do not display any test indicators in non-dev server remote projects. - project.is_via_collab() && project.ssh_connection_string(cx).is_none() - }) + .update(cx, |project, _| project.is_via_collab()) .unwrap_or(true); if hide_runnables { return; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6712b3fab0d81330dbd201d2981ed20345dc808b..f07ee13866511ad643029935405c1b050b846106 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1346,14 +1346,13 @@ impl Project { }; // ssh -> local machine handlers - let ssh = ssh.read(cx); - ssh.subscribe_to_entity(SSH_PROJECT_ID, &cx.entity()); - ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store); - ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store); - ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store); - ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.dap_store); - ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer); - ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.git_store); + ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &cx.entity()); + ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store); + ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store); + ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store); + ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.dap_store); + ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer); + ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.git_store); ssh_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer); ssh_proto.add_entity_message_handler(Self::handle_update_worktree); @@ -1900,14 +1899,6 @@ impl Project { false } - pub fn ssh_connection_string(&self, cx: &App) -> Option<SharedString> { - if let Some(ssh_state) = &self.ssh_client { - return Some(ssh_state.read(cx).connection_string().into()); - } - - None - } - pub fn ssh_connection_state(&self, cx: &App) -> Option<remote::ConnectionState> { self.ssh_client .as_ref() diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index abde2d7568f339134103253094e6e410c9f1632b..ffd0cac310529e7dfc9034f936287eb98145df4f 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -26,8 +26,7 @@ use parking_lot::Mutex; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use rpc::{ - AnyProtoClient, EntityMessageSubscriber, ErrorExt, ProtoClient, ProtoMessageHandlerSet, - RpcError, + AnyProtoClient, ErrorExt, ProtoClient, ProtoMessageHandlerSet, RpcError, proto::{self, Envelope, EnvelopedMessage, PeerId, RequestMessage, build_typed_envelope}, }; use schemars::JsonSchema; @@ -37,7 +36,6 @@ use smol::{ process::{self, Child, Stdio}, }; use std::{ - any::TypeId, collections::VecDeque, fmt, iter, ops::ControlFlow, @@ -664,6 +662,7 @@ impl ConnectionIdentifier { pub fn setup() -> Self { Self::Setup(NEXT_ID.fetch_add(1, SeqCst)) } + // This string gets used in a socket name, and so must be relatively short. // The total length of: // /home/{username}/.local/share/zed/server_state/{name}/stdout.sock @@ -760,6 +759,15 @@ impl SshRemoteClient { }) } + pub fn proto_client_from_channels( + incoming_rx: mpsc::UnboundedReceiver<Envelope>, + outgoing_tx: mpsc::UnboundedSender<Envelope>, + cx: &App, + name: &'static str, + ) -> AnyProtoClient { + ChannelClient::new(incoming_rx, outgoing_tx, cx, name).into() + } + pub fn shutdown_processes<T: RequestMessage>( &self, shutdown_request: Option<T>, @@ -990,64 +998,63 @@ impl SshRemoteClient { }; cx.spawn(async move |cx| { - let mut missed_heartbeats = 0; - - let keepalive_timer = cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse(); - futures::pin_mut!(keepalive_timer); + let mut missed_heartbeats = 0; - loop { - select_biased! { - result = connection_activity_rx.next().fuse() => { - if result.is_none() { - log::warn!("ssh heartbeat: connection activity channel has been dropped. stopping."); - return Ok(()); - } + let keepalive_timer = cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse(); + futures::pin_mut!(keepalive_timer); - if missed_heartbeats != 0 { - missed_heartbeats = 0; - let _ =this.update(cx, |this, cx| { - this.handle_heartbeat_result(missed_heartbeats, cx) - })?; - } + loop { + select_biased! { + result = connection_activity_rx.next().fuse() => { + if result.is_none() { + log::warn!("ssh heartbeat: connection activity channel has been dropped. stopping."); + return Ok(()); } - _ = keepalive_timer => { - log::debug!("Sending heartbeat to server..."); - - let result = select_biased! { - _ = connection_activity_rx.next().fuse() => { - Ok(()) - } - ping_result = client.ping(HEARTBEAT_TIMEOUT).fuse() => { - ping_result - } - }; - - if result.is_err() { - missed_heartbeats += 1; - log::warn!( - "No heartbeat from server after {:?}. Missed heartbeat {} out of {}.", - HEARTBEAT_TIMEOUT, - missed_heartbeats, - MAX_MISSED_HEARTBEATS - ); - } else if missed_heartbeats != 0 { - missed_heartbeats = 0; - } else { - continue; - } - let result = this.update(cx, |this, cx| { + if missed_heartbeats != 0 { + missed_heartbeats = 0; + let _ =this.update(cx, |this, cx| { this.handle_heartbeat_result(missed_heartbeats, cx) })?; - if result.is_break() { - return Ok(()); - } } } + _ = keepalive_timer => { + log::debug!("Sending heartbeat to server..."); + + let result = select_biased! { + _ = connection_activity_rx.next().fuse() => { + Ok(()) + } + ping_result = client.ping(HEARTBEAT_TIMEOUT).fuse() => { + ping_result + } + }; + + if result.is_err() { + missed_heartbeats += 1; + log::warn!( + "No heartbeat from server after {:?}. Missed heartbeat {} out of {}.", + HEARTBEAT_TIMEOUT, + missed_heartbeats, + MAX_MISSED_HEARTBEATS + ); + } else if missed_heartbeats != 0 { + missed_heartbeats = 0; + } else { + continue; + } - keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse()); + let result = this.update(cx, |this, cx| { + this.handle_heartbeat_result(missed_heartbeats, cx) + })?; + if result.is_break() { + return Ok(()); + } + } } + keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse()); + } }) } @@ -1145,10 +1152,6 @@ impl SshRemoteClient { cx.notify(); } - pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Entity<E>) { - self.client.subscribe_to_entity(remote_id, entity); - } - pub fn ssh_info(&self) -> Option<(SshArgs, PathStyle)> { self.state .lock() @@ -1222,7 +1225,7 @@ impl SshRemoteClient { pub fn fake_server( client_cx: &mut gpui::TestAppContext, server_cx: &mut gpui::TestAppContext, - ) -> (SshConnectionOptions, Arc<ChannelClient>) { + ) -> (SshConnectionOptions, AnyProtoClient) { let port = client_cx .update(|cx| cx.default_global::<ConnectionPool>().connections.len() as u16 + 1); let opts = SshConnectionOptions { @@ -1255,7 +1258,7 @@ impl SshRemoteClient { }) }); - (opts, server_client) + (opts, server_client.into()) } #[cfg(any(test, feature = "test-support"))] @@ -2269,7 +2272,7 @@ impl SshRemoteConnection { type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, oneshot::Sender<()>)>>>; -pub struct ChannelClient { +struct ChannelClient { next_message_id: AtomicU32, outgoing_tx: Mutex<mpsc::UnboundedSender<Envelope>>, buffer: Mutex<VecDeque<Envelope>>, @@ -2281,7 +2284,7 @@ pub struct ChannelClient { } impl ChannelClient { - pub fn new( + fn new( incoming_rx: mpsc::UnboundedReceiver<Envelope>, outgoing_tx: mpsc::UnboundedSender<Envelope>, cx: &App, @@ -2402,7 +2405,7 @@ impl ChannelClient { }) } - pub fn reconnect( + fn reconnect( self: &Arc<Self>, incoming_rx: UnboundedReceiver<Envelope>, outgoing_tx: UnboundedSender<Envelope>, @@ -2412,26 +2415,7 @@ impl ChannelClient { *self.task.lock() = Self::start_handling_messages(Arc::downgrade(self), incoming_rx, cx); } - pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Entity<E>) { - let id = (TypeId::of::<E>(), remote_id); - - let mut message_handlers = self.message_handlers.lock(); - if message_handlers - .entities_by_type_and_remote_id - .contains_key(&id) - { - panic!("already subscribed to entity"); - } - - message_handlers.entities_by_type_and_remote_id.insert( - id, - EntityMessageSubscriber::Entity { - handle: entity.downgrade().into(), - }, - ); - } - - pub fn request<T: RequestMessage>( + fn request<T: RequestMessage>( &self, payload: T, ) -> impl 'static + Future<Output = Result<T::Response>> { @@ -2453,7 +2437,7 @@ impl ChannelClient { } } - pub async fn resync(&self, timeout: Duration) -> Result<()> { + async fn resync(&self, timeout: Duration) -> Result<()> { smol::future::or( async { self.request_internal(proto::FlushBufferedMessages {}, false) @@ -2475,7 +2459,7 @@ impl ChannelClient { .await } - pub async fn ping(&self, timeout: Duration) -> Result<()> { + async fn ping(&self, timeout: Duration) -> Result<()> { smol::future::or( async { self.request(proto::Ping {}).await?; diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 6fc327ac1c6af3449c72803f72a22b82343dfb28..3bcdcbd73c6cdc2a7ab6e4d8c947ace48fa98134 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -19,7 +19,6 @@ use project::{ task_store::TaskStore, worktree_store::WorktreeStore, }; -use remote::ssh_session::ChannelClient; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self, SSH_PEER_ID, SSH_PROJECT_ID}, @@ -50,7 +49,7 @@ pub struct HeadlessProject { } pub struct HeadlessAppState { - pub session: Arc<ChannelClient>, + pub session: AnyProtoClient, pub fs: Arc<dyn Fs>, pub http_client: Arc<dyn HttpClient>, pub node_runtime: NodeRuntime, @@ -81,7 +80,7 @@ impl HeadlessProject { let worktree_store = cx.new(|cx| { let mut store = WorktreeStore::local(true, fs.clone()); - store.shared(SSH_PROJECT_ID, session.clone().into(), cx); + store.shared(SSH_PROJECT_ID, session.clone(), cx); store }); @@ -99,7 +98,7 @@ impl HeadlessProject { let buffer_store = cx.new(|cx| { let mut buffer_store = BufferStore::local(worktree_store.clone(), cx); - buffer_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); + buffer_store.shared(SSH_PROJECT_ID, session.clone(), cx); buffer_store }); @@ -117,7 +116,7 @@ impl HeadlessProject { breakpoint_store.clone(), cx, ); - dap_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); + dap_store.shared(SSH_PROJECT_ID, session.clone(), cx); dap_store }); @@ -129,7 +128,7 @@ impl HeadlessProject { fs.clone(), cx, ); - store.shared(SSH_PROJECT_ID, session.clone().into(), cx); + store.shared(SSH_PROJECT_ID, session.clone(), cx); store }); @@ -152,7 +151,7 @@ impl HeadlessProject { environment.clone(), cx, ); - task_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); + task_store.shared(SSH_PROJECT_ID, session.clone(), cx); task_store }); let settings_observer = cx.new(|cx| { @@ -162,7 +161,7 @@ impl HeadlessProject { task_store.clone(), cx, ); - observer.shared(SSH_PROJECT_ID, session.clone().into(), cx); + observer.shared(SSH_PROJECT_ID, session.clone(), cx); observer }); @@ -183,7 +182,7 @@ impl HeadlessProject { fs.clone(), cx, ); - lsp_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); + lsp_store.shared(SSH_PROJECT_ID, session.clone(), cx); lsp_store }); @@ -210,8 +209,6 @@ impl HeadlessProject { cx, ); - let client: AnyProtoClient = session.clone().into(); - // local_machine -> ssh handlers session.subscribe_to_entity(SSH_PROJECT_ID, &worktree_store); session.subscribe_to_entity(SSH_PROJECT_ID, &buffer_store); @@ -223,44 +220,45 @@ impl HeadlessProject { session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer); session.subscribe_to_entity(SSH_PROJECT_ID, &git_store); - client.add_request_handler(cx.weak_entity(), Self::handle_list_remote_directory); - client.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata); - client.add_request_handler(cx.weak_entity(), Self::handle_shutdown_remote_server); - client.add_request_handler(cx.weak_entity(), Self::handle_ping); + session.add_request_handler(cx.weak_entity(), Self::handle_list_remote_directory); + session.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata); + session.add_request_handler(cx.weak_entity(), Self::handle_shutdown_remote_server); + session.add_request_handler(cx.weak_entity(), Self::handle_ping); - client.add_entity_request_handler(Self::handle_add_worktree); - client.add_request_handler(cx.weak_entity(), Self::handle_remove_worktree); + session.add_entity_request_handler(Self::handle_add_worktree); + session.add_request_handler(cx.weak_entity(), Self::handle_remove_worktree); - client.add_entity_request_handler(Self::handle_open_buffer_by_path); - client.add_entity_request_handler(Self::handle_open_new_buffer); - client.add_entity_request_handler(Self::handle_find_search_candidates); - client.add_entity_request_handler(Self::handle_open_server_settings); + session.add_entity_request_handler(Self::handle_open_buffer_by_path); + session.add_entity_request_handler(Self::handle_open_new_buffer); + session.add_entity_request_handler(Self::handle_find_search_candidates); + session.add_entity_request_handler(Self::handle_open_server_settings); - client.add_entity_request_handler(BufferStore::handle_update_buffer); - client.add_entity_message_handler(BufferStore::handle_close_buffer); + session.add_entity_request_handler(BufferStore::handle_update_buffer); + session.add_entity_message_handler(BufferStore::handle_close_buffer); - client.add_request_handler( + session.add_request_handler( extensions.clone().downgrade(), HeadlessExtensionStore::handle_sync_extensions, ); - client.add_request_handler( + session.add_request_handler( extensions.clone().downgrade(), HeadlessExtensionStore::handle_install_extension, ); - BufferStore::init(&client); - WorktreeStore::init(&client); - SettingsObserver::init(&client); - LspStore::init(&client); - TaskStore::init(Some(&client)); - ToolchainStore::init(&client); - DapStore::init(&client, cx); + BufferStore::init(&session); + WorktreeStore::init(&session); + SettingsObserver::init(&session); + LspStore::init(&session); + TaskStore::init(Some(&session)); + ToolchainStore::init(&session); + DapStore::init(&session, cx); // todo(debugger): Re init breakpoint store when we set it up for collab // BreakpointStore::init(&client); - GitStore::init(&client); + GitStore::init(&session); HeadlessProject { - session: client, + next_entry_id: Default::default(), + session, settings_observer, fs, worktree_store, @@ -268,7 +266,6 @@ impl HeadlessProject { lsp_store, task_store, dap_store, - next_entry_id: Default::default(), languages, extensions, git_store, diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 15a465a8806d96554742d95c5d8e1931c1672595..3352b317cbff3e332ee1ab7e0439acd59f71f48a 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -19,11 +19,11 @@ use project::project_settings::ProjectSettings; use proto::CrashReport; use release_channel::{AppVersion, RELEASE_CHANNEL, ReleaseChannel}; -use remote::proxy::ProxyLaunchError; -use remote::ssh_session::ChannelClient; +use remote::SshRemoteClient; use remote::{ json_log::LogRecord, protocol::{read_message, write_message}, + proxy::ProxyLaunchError, }; use reqwest_client::ReqwestClient; use rpc::proto::{self, Envelope, SSH_PROJECT_ID}; @@ -199,8 +199,7 @@ fn init_panic_hook(session_id: String) { })); } -fn handle_crash_files_requests(project: &Entity<HeadlessProject>, client: &Arc<ChannelClient>) { - let client: AnyProtoClient = client.clone().into(); +fn handle_crash_files_requests(project: &Entity<HeadlessProject>, client: &AnyProtoClient) { client.add_request_handler( project.downgrade(), |_, _: TypedEnvelope<proto::GetCrashFiles>, _cx| async move { @@ -276,7 +275,7 @@ fn start_server( listeners: ServerListeners, log_rx: Receiver<Vec<u8>>, cx: &mut App, -) -> Arc<ChannelClient> { +) -> AnyProtoClient { // This is the server idle timeout. If no connection comes in this timeout, the server will shut down. const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10 * 60); @@ -395,7 +394,7 @@ fn start_server( }) .detach(); - ChannelClient::new(incoming_rx, outgoing_tx, cx, "server") + SshRemoteClient::proto_client_from_channels(incoming_rx, outgoing_tx, cx, "server") } fn init_paths() -> anyhow::Result<()> { @@ -792,7 +791,7 @@ async fn write_size_prefixed_buffer<S: AsyncWrite + Unpin>( } fn initialize_settings( - session: Arc<ChannelClient>, + session: AnyProtoClient, fs: Arc<dyn Fs>, cx: &mut App, ) -> watch::Receiver<Option<NodeBinaryOptions>> { diff --git a/crates/rpc/src/proto_client.rs b/crates/rpc/src/proto_client.rs index eb570b96a372c083d1902c9edce11160cca3dbf2..05b6bd1439c96a3c49dbabe69453e491f23b02da 100644 --- a/crates/rpc/src/proto_client.rs +++ b/crates/rpc/src/proto_client.rs @@ -315,4 +315,23 @@ impl AnyProtoClient { }), ); } + + pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Entity<E>) { + let id = (TypeId::of::<E>(), remote_id); + + let mut message_handlers = self.0.message_handler_set().lock(); + if message_handlers + .entities_by_type_and_remote_id + .contains_key(&id) + { + panic!("already subscribed to entity"); + } + + message_handlers.entities_by_type_and_remote_id.insert( + id, + EntityMessageSubscriber::Entity { + handle: entity.downgrade().into(), + }, + ); + } } diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index dae366a9797ae81754949c6fc5512843a10cf803..a4fdc24e177d9bebba1106c7df865f3f621b6c10 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -148,9 +148,9 @@ pub fn toggle_modal( ) -> Task<()> { let task_store = workspace.project().read(cx).task_store().clone(); let workspace_handle = workspace.weak_handle(); - let can_open_modal = workspace.project().update(cx, |project, cx| { - project.is_local() || project.ssh_connection_string(cx).is_some() || project.is_via_ssh() - }); + let can_open_modal = workspace + .project() + .read_with(cx, |project, _| !project.is_via_collab()); if can_open_modal { let task_contexts = task_contexts(workspace, window, cx); cx.spawn_in(window, async move |workspace, cx| { From 714c36fa7b196c398c03c536a973811a8cb5851d Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Tue, 19 Aug 2025 22:30:26 -0300 Subject: [PATCH 162/744] claude: Include all mentions and images in user message (#36539) User messages sent to Claude Code will now include the content of all mentions, and any images included. Release Notes: - N/A --- crates/agent_servers/src/claude.rs | 242 ++++++++++++++++++++++++++--- 1 file changed, 218 insertions(+), 24 deletions(-) diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index f27c973ad62c6205d9d4b01103ecf41aa9b2157e..e214ee875ce23ca6c666852018f580a4b5957942 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -32,7 +32,7 @@ use util::{ResultExt, debug_panic}; use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig}; use crate::claude::tools::ClaudeTool; use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings}; -use acp_thread::{AcpThread, AgentConnection, AuthRequired}; +use acp_thread::{AcpThread, AgentConnection, AuthRequired, MentionUri}; #[derive(Clone)] pub struct ClaudeCode; @@ -267,27 +267,12 @@ impl AgentConnection for ClaudeAgentConnection { let (end_tx, end_rx) = oneshot::channel(); session.turn_state.replace(TurnState::InProgress { end_tx }); - let mut content = String::new(); - for chunk in params.prompt { - match chunk { - acp::ContentBlock::Text(text_content) => { - content.push_str(&text_content.text); - } - acp::ContentBlock::ResourceLink(resource_link) => { - content.push_str(&format!("@{}", resource_link.uri)); - } - acp::ContentBlock::Audio(_) - | acp::ContentBlock::Image(_) - | acp::ContentBlock::Resource(_) => { - // TODO - } - } - } + let content = acp_content_to_claude(params.prompt); if let Err(err) = session.outgoing_tx.unbounded_send(SdkMessage::User { message: Message { role: Role::User, - content: Content::UntaggedText(content), + content: Content::Chunks(content), id: None, model: None, stop_reason: None, @@ -513,10 +498,17 @@ impl ClaudeAgentSession { chunk ); } + ContentChunk::Image { source } => { + if !turn_state.borrow().is_canceled() { + thread + .update(cx, |thread, cx| { + thread.push_user_content_block(None, source.into(), cx) + }) + .log_err(); + } + } - ContentChunk::Image - | ContentChunk::Document - | ContentChunk::WebSearchToolResult => { + ContentChunk::Document | ContentChunk::WebSearchToolResult => { thread .update(cx, |thread, cx| { thread.push_assistant_content_block( @@ -602,7 +594,14 @@ impl ClaudeAgentSession { "Should not get tool results with role: assistant. should we handle this?" ); } - ContentChunk::Image | ContentChunk::Document => { + ContentChunk::Image { source } => { + thread + .update(cx, |thread, cx| { + thread.push_assistant_content_block(source.into(), false, cx) + }) + .log_err(); + } + ContentChunk::Document => { thread .update(cx, |thread, cx| { thread.push_assistant_content_block( @@ -768,14 +767,44 @@ enum ContentChunk { thinking: String, }, RedactedThinking, + Image { + source: ImageSource, + }, // TODO - Image, Document, WebSearchToolResult, #[serde(untagged)] UntaggedText(String), } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +enum ImageSource { + Base64 { data: String, media_type: String }, + Url { url: String }, +} + +impl Into<acp::ContentBlock> for ImageSource { + fn into(self) -> acp::ContentBlock { + match self { + ImageSource::Base64 { data, media_type } => { + acp::ContentBlock::Image(acp::ImageContent { + annotations: None, + data, + mime_type: media_type, + uri: None, + }) + } + ImageSource::Url { url } => acp::ContentBlock::Image(acp::ImageContent { + annotations: None, + data: "".to_string(), + mime_type: "".to_string(), + uri: Some(url), + }), + } + } +} + impl Display for ContentChunk { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -784,7 +813,7 @@ impl Display for ContentChunk { ContentChunk::RedactedThinking => write!(f, "Thinking: [REDACTED]"), ContentChunk::UntaggedText(text) => write!(f, "{}", text), ContentChunk::ToolResult { content, .. } => write!(f, "{}", content), - ContentChunk::Image + ContentChunk::Image { .. } | ContentChunk::Document | ContentChunk::ToolUse { .. } | ContentChunk::WebSearchToolResult => { @@ -896,6 +925,75 @@ impl Display for ResultErrorType { } } +fn acp_content_to_claude(prompt: Vec<acp::ContentBlock>) -> Vec<ContentChunk> { + let mut content = Vec::with_capacity(prompt.len()); + let mut context = Vec::with_capacity(prompt.len()); + + for chunk in prompt { + match chunk { + acp::ContentBlock::Text(text_content) => { + content.push(ContentChunk::Text { + text: text_content.text, + }); + } + acp::ContentBlock::ResourceLink(resource_link) => { + match MentionUri::parse(&resource_link.uri) { + Ok(uri) => { + content.push(ContentChunk::Text { + text: format!("{}", uri.as_link()), + }); + } + Err(_) => { + content.push(ContentChunk::Text { + text: resource_link.uri, + }); + } + } + } + acp::ContentBlock::Resource(resource) => match resource.resource { + acp::EmbeddedResourceResource::TextResourceContents(resource) => { + match MentionUri::parse(&resource.uri) { + Ok(uri) => { + content.push(ContentChunk::Text { + text: format!("{}", uri.as_link()), + }); + } + Err(_) => { + content.push(ContentChunk::Text { + text: resource.uri.clone(), + }); + } + } + + context.push(ContentChunk::Text { + text: format!( + "\n<context ref=\"{}\">\n{}\n</context>", + resource.uri, resource.text + ), + }); + } + acp::EmbeddedResourceResource::BlobResourceContents(_) => { + // Unsupported by SDK + } + }, + acp::ContentBlock::Image(acp::ImageContent { + data, mime_type, .. + }) => content.push(ContentChunk::Image { + source: ImageSource::Base64 { + data, + media_type: mime_type, + }, + }), + acp::ContentBlock::Audio(_) => { + // Unsupported by SDK + } + } + } + + content.extend(context); + content +} + fn new_request_id() -> String { use rand::Rng; // In the Claude Code TS SDK they just generate a random 12 character string, @@ -1112,4 +1210,100 @@ pub(crate) mod tests { _ => panic!("Expected ToolResult variant"), } } + + #[test] + fn test_acp_content_to_claude() { + let acp_content = vec![ + acp::ContentBlock::Text(acp::TextContent { + text: "Hello world".to_string(), + annotations: None, + }), + acp::ContentBlock::Image(acp::ImageContent { + data: "base64data".to_string(), + mime_type: "image/png".to_string(), + annotations: None, + uri: None, + }), + acp::ContentBlock::ResourceLink(acp::ResourceLink { + uri: "file:///path/to/example.rs".to_string(), + name: "example.rs".to_string(), + annotations: None, + description: None, + mime_type: None, + size: None, + title: None, + }), + acp::ContentBlock::Resource(acp::EmbeddedResource { + annotations: None, + resource: acp::EmbeddedResourceResource::TextResourceContents( + acp::TextResourceContents { + mime_type: None, + text: "fn main() { println!(\"Hello!\"); }".to_string(), + uri: "file:///path/to/code.rs".to_string(), + }, + ), + }), + acp::ContentBlock::ResourceLink(acp::ResourceLink { + uri: "invalid_uri_format".to_string(), + name: "invalid.txt".to_string(), + annotations: None, + description: None, + mime_type: None, + size: None, + title: None, + }), + ]; + + let claude_content = acp_content_to_claude(acp_content); + + assert_eq!(claude_content.len(), 6); + + match &claude_content[0] { + ContentChunk::Text { text } => assert_eq!(text, "Hello world"), + _ => panic!("Expected Text chunk"), + } + + match &claude_content[1] { + ContentChunk::Image { source } => match source { + ImageSource::Base64 { data, media_type } => { + assert_eq!(data, "base64data"); + assert_eq!(media_type, "image/png"); + } + _ => panic!("Expected Base64 image source"), + }, + _ => panic!("Expected Image chunk"), + } + + match &claude_content[2] { + ContentChunk::Text { text } => { + assert!(text.contains("example.rs")); + assert!(text.contains("file:///path/to/example.rs")); + } + _ => panic!("Expected Text chunk for ResourceLink"), + } + + match &claude_content[3] { + ContentChunk::Text { text } => { + assert!(text.contains("code.rs")); + assert!(text.contains("file:///path/to/code.rs")); + } + _ => panic!("Expected Text chunk for Resource"), + } + + match &claude_content[4] { + ContentChunk::Text { text } => { + assert_eq!(text, "invalid_uri_format"); + } + _ => panic!("Expected Text chunk for invalid URI"), + } + + match &claude_content[5] { + ContentChunk::Text { text } => { + assert!(text.contains("<context ref=\"file:///path/to/code.rs\">")); + assert!(text.contains("fn main() { println!(\"Hello!\"); }")); + assert!(text.contains("</context>")); + } + _ => panic!("Expected Text chunk for context"), + } + } } From 7c7043947b1551470a55063ad13e0ea3b6745171 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Tue, 19 Aug 2025 22:42:11 -0300 Subject: [PATCH 163/744] Improve claude tools (#36538) - Return unified diff from `Edit` tool so model can see the final state - Format on save if enabled - Provide `Write` tool - Disable `MultiEdit` tool - Better prompting Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 76 ++++- crates/agent_servers/Cargo.toml | 1 + crates/agent_servers/src/claude.rs | 25 +- crates/agent_servers/src/claude/edit_tool.rs | 178 +++++++++++ crates/agent_servers/src/claude/mcp_server.rs | 279 +----------------- .../src/claude/permission_tool.rs | 158 ++++++++++ crates/agent_servers/src/claude/read_tool.rs | 59 ++++ crates/agent_servers/src/claude/tools.rs | 39 ++- crates/agent_servers/src/claude/write_tool.rs | 59 ++++ crates/context_server/src/listener.rs | 24 +- crates/context_server/src/types.rs | 10 + 11 files changed, 606 insertions(+), 302 deletions(-) create mode 100644 crates/agent_servers/src/claude/edit_tool.rs create mode 100644 crates/agent_servers/src/claude/permission_tool.rs create mode 100644 crates/agent_servers/src/claude/read_tool.rs create mode 100644 crates/agent_servers/src/claude/write_tool.rs diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 793ef35be2b2f8f460ad0e503c500a3b347086bc..2be7ea7a12d88a24ed866e906b52542de978840c 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -3,9 +3,12 @@ mod diff; mod mention; mod terminal; +use collections::HashSet; pub use connection::*; pub use diff::*; +use language::language_settings::FormatOnSave; pub use mention::*; +use project::lsp_store::{FormatTrigger, LspFormatTarget}; use serde::{Deserialize, Serialize}; pub use terminal::*; @@ -1051,6 +1054,22 @@ impl AcpThread { }) } + pub fn tool_call(&mut self, id: &acp::ToolCallId) -> Option<(usize, &ToolCall)> { + self.entries + .iter() + .enumerate() + .rev() + .find_map(|(index, tool_call)| { + if let AgentThreadEntry::ToolCall(tool_call) = tool_call + && &tool_call.id == id + { + Some((index, tool_call)) + } else { + None + } + }) + } + pub fn resolve_locations(&mut self, id: acp::ToolCallId, cx: &mut Context<Self>) { let project = self.project.clone(); let Some((_, tool_call)) = self.tool_call_mut(&id) else { @@ -1601,30 +1620,59 @@ impl AcpThread { .collect::<Vec<_>>() }) .await; - cx.update(|cx| { - project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: edits - .last() - .map(|(range, _)| range.end) - .unwrap_or(Anchor::MIN), - }), - cx, - ); - }); + project.update(cx, |project, cx| { + project.set_agent_location( + Some(AgentLocation { + buffer: buffer.downgrade(), + position: edits + .last() + .map(|(range, _)| range.end) + .unwrap_or(Anchor::MIN), + }), + cx, + ); + })?; + + let format_on_save = cx.update(|cx| { action_log.update(cx, |action_log, cx| { action_log.buffer_read(buffer.clone(), cx); }); - buffer.update(cx, |buffer, cx| { + + let format_on_save = buffer.update(cx, |buffer, cx| { buffer.edit(edits, None, cx); + + let settings = language::language_settings::language_settings( + buffer.language().map(|l| l.name()), + buffer.file(), + cx, + ); + + settings.format_on_save != FormatOnSave::Off }); action_log.update(cx, |action_log, cx| { action_log.buffer_edited(buffer.clone(), cx); }); + format_on_save })?; + + if format_on_save { + let format_task = project.update(cx, |project, cx| { + project.format( + HashSet::from_iter([buffer.clone()]), + LspFormatTarget::Buffers, + false, + FormatTrigger::Save, + cx, + ) + })?; + format_task.await.log_err(); + + action_log.update(cx, |action_log, cx| { + action_log.buffer_edited(buffer.clone(), cx); + })?; + } + project .update(cx, |project, cx| project.save_buffer(buffer, cx))? .await diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index cbc874057a088124475da7507b5e090cfa3a5509..8cd6980ae142ef82b646f07302a995bed4b408e4 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -29,6 +29,7 @@ futures.workspace = true gpui.workspace = true indoc.workspace = true itertools.workspace = true +language.workspace = true language_model.workspace = true language_models.workspace = true log.workspace = true diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index e214ee875ce23ca6c666852018f580a4b5957942..a53c81d4c416626cb0ba55f2f1fa3c9090e5fc3f 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1,5 +1,9 @@ +mod edit_tool; mod mcp_server; +mod permission_tool; +mod read_tool; pub mod tools; +mod write_tool; use action_log::ActionLog; use collections::HashMap; @@ -351,18 +355,16 @@ fn spawn_claude( &format!( "mcp__{}__{}", mcp_server::SERVER_NAME, - mcp_server::PermissionTool::NAME, + permission_tool::PermissionTool::NAME, ), "--allowedTools", &format!( - "mcp__{}__{},mcp__{}__{}", - mcp_server::SERVER_NAME, - mcp_server::EditTool::NAME, + "mcp__{}__{}", mcp_server::SERVER_NAME, - mcp_server::ReadTool::NAME + read_tool::ReadTool::NAME ), "--disallowedTools", - "Read,Edit", + "Read,Write,Edit,MultiEdit", ]) .args(match mode { ClaudeSessionMode::Start => ["--session-id".to_string(), session_id.to_string()], @@ -470,9 +472,16 @@ impl ClaudeAgentSession { let content = content.to_string(); thread .update(cx, |thread, cx| { + let id = acp::ToolCallId(tool_use_id.into()); + let set_new_content = !content.is_empty() + && thread.tool_call(&id).is_none_or(|(_, tool_call)| { + // preserve rich diff if we have one + tool_call.diffs().next().is_none() + }); + thread.update_tool_call( acp::ToolCallUpdate { - id: acp::ToolCallId(tool_use_id.into()), + id, fields: acp::ToolCallUpdateFields { status: if turn_state.borrow().is_canceled() { // Do not set to completed if turn was canceled @@ -480,7 +489,7 @@ impl ClaudeAgentSession { } else { Some(acp::ToolCallStatus::Completed) }, - content: (!content.is_empty()) + content: set_new_content .then(|| vec![content.into()]), ..Default::default() }, diff --git a/crates/agent_servers/src/claude/edit_tool.rs b/crates/agent_servers/src/claude/edit_tool.rs new file mode 100644 index 0000000000000000000000000000000000000000..a8d93c3f3d5579173709b3ace6194059745885a7 --- /dev/null +++ b/crates/agent_servers/src/claude/edit_tool.rs @@ -0,0 +1,178 @@ +use acp_thread::AcpThread; +use anyhow::Result; +use context_server::{ + listener::{McpServerTool, ToolResponse}, + types::{ToolAnnotations, ToolResponseContent}, +}; +use gpui::{AsyncApp, WeakEntity}; +use language::unified_diff; +use util::markdown::MarkdownCodeBlock; + +use crate::tools::EditToolParams; + +#[derive(Clone)] +pub struct EditTool { + thread_rx: watch::Receiver<WeakEntity<AcpThread>>, +} + +impl EditTool { + pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self { + Self { thread_rx } + } +} + +impl McpServerTool for EditTool { + type Input = EditToolParams; + type Output = (); + + const NAME: &'static str = "Edit"; + + fn annotations(&self) -> ToolAnnotations { + ToolAnnotations { + title: Some("Edit file".to_string()), + read_only_hint: Some(false), + destructive_hint: Some(false), + open_world_hint: Some(false), + idempotent_hint: Some(false), + } + } + + async fn run( + &self, + input: Self::Input, + cx: &mut AsyncApp, + ) -> Result<ToolResponse<Self::Output>> { + 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.abs_path.clone(), None, None, true, cx) + })? + .await?; + + let (new_content, diff) = cx + .background_executor() + .spawn(async move { + let new_content = content.replace(&input.old_text, &input.new_text); + if new_content == content { + return Err(anyhow::anyhow!("Failed to find `old_text`",)); + } + let diff = unified_diff(&content, &new_content); + + Ok((new_content, diff)) + }) + .await?; + + thread + .update(cx, |thread, cx| { + thread.write_text_file(input.abs_path, new_content, cx) + })? + .await?; + + Ok(ToolResponse { + content: vec![ToolResponseContent::Text { + text: MarkdownCodeBlock { + tag: "diff", + text: diff.as_str().trim_end_matches('\n'), + } + .to_string(), + }], + structured_content: (), + }) + } +} + +#[cfg(test)] +mod tests { + use std::rc::Rc; + + use acp_thread::{AgentConnection, StubAgentConnection}; + use gpui::{Entity, TestAppContext}; + use indoc::indoc; + use project::{FakeFs, Project}; + use serde_json::json; + use settings::SettingsStore; + use util::path; + + use super::*; + + #[gpui::test] + async fn old_text_not_found(cx: &mut TestAppContext) { + let (_thread, tool) = init_test(cx).await; + + let result = tool + .run( + EditToolParams { + abs_path: path!("/root/file.txt").into(), + old_text: "hi".into(), + new_text: "bye".into(), + }, + &mut cx.to_async(), + ) + .await; + + assert_eq!(result.unwrap_err().to_string(), "Failed to find `old_text`"); + } + + #[gpui::test] + async fn found_and_replaced(cx: &mut TestAppContext) { + let (_thread, tool) = init_test(cx).await; + + let result = tool + .run( + EditToolParams { + abs_path: path!("/root/file.txt").into(), + old_text: "hello".into(), + new_text: "hi".into(), + }, + &mut cx.to_async(), + ) + .await; + + assert_eq!( + result.unwrap().content[0].text().unwrap(), + indoc! { + r" + ```diff + @@ -1,1 +1,1 @@ + -hello + +hi + ``` + " + } + ); + } + + async fn init_test(cx: &mut TestAppContext) -> (Entity<AcpThread>, EditTool) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + }); + + let connection = Rc::new(StubAgentConnection::new()); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/root"), + json!({ + "file.txt": "hello" + }), + ) + .await; + let project = Project::test(fs, [path!("/root").as_ref()], cx).await; + let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid()); + + let thread = cx + .update(|cx| connection.new_thread(project, path!("/test").as_ref(), cx)) + .await + .unwrap(); + + thread_tx.send(thread.downgrade()).unwrap(); + + (thread, EditTool::new(thread_rx)) + } +} diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index 30867528502d5e94fe0d1af2bb36f864cc4e2390..6442c784b59a655def92bcae108c27c503daa630 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -1,23 +1,22 @@ use std::path::PathBuf; use std::sync::Arc; -use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams}; +use crate::claude::edit_tool::EditTool; +use crate::claude::permission_tool::PermissionTool; +use crate::claude::read_tool::ReadTool; +use crate::claude::write_tool::WriteTool; use acp_thread::AcpThread; -use agent_client_protocol as acp; -use agent_settings::AgentSettings; -use anyhow::{Context, Result}; +#[cfg(not(test))] +use anyhow::Context as _; +use anyhow::Result; use collections::HashMap; -use context_server::listener::{McpServerTool, ToolResponse}; use context_server::types::{ Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities, - ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests, + ToolsCapabilities, requests, }; use gpui::{App, AsyncApp, Task, WeakEntity}; use project::Fs; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings as _, update_settings_file}; -use util::debug_panic; +use serde::Serialize; pub struct ClaudeZedMcpServer { server: context_server::listener::McpServer, @@ -34,16 +33,10 @@ impl ClaudeZedMcpServer { let mut mcp_server = context_server::listener::McpServer::new(cx).await?; mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize); - mcp_server.add_tool(PermissionTool { - thread_rx: thread_rx.clone(), - fs: fs.clone(), - }); - mcp_server.add_tool(ReadTool { - thread_rx: thread_rx.clone(), - }); - mcp_server.add_tool(EditTool { - thread_rx: thread_rx.clone(), - }); + mcp_server.add_tool(PermissionTool::new(fs.clone(), thread_rx.clone())); + mcp_server.add_tool(ReadTool::new(thread_rx.clone())); + mcp_server.add_tool(EditTool::new(thread_rx.clone())); + mcp_server.add_tool(WriteTool::new(thread_rx.clone())); Ok(Self { server: mcp_server }) } @@ -104,249 +97,3 @@ pub struct McpServerConfig { #[serde(skip_serializing_if = "Option::is_none")] pub env: Option<HashMap<String, String>>, } - -// Tools - -#[derive(Clone)] -pub struct PermissionTool { - fs: Arc<dyn Fs>, - thread_rx: watch::Receiver<WeakEntity<AcpThread>>, -} - -#[derive(Deserialize, JsonSchema, Debug)] -pub struct PermissionToolParams { - tool_name: String, - input: serde_json::Value, - tool_use_id: Option<String>, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct PermissionToolResponse { - behavior: PermissionToolBehavior, - updated_input: serde_json::Value, -} - -#[derive(Serialize)] -#[serde(rename_all = "snake_case")] -enum PermissionToolBehavior { - Allow, - Deny, -} - -impl McpServerTool for PermissionTool { - type Input = PermissionToolParams; - type Output = (); - - const NAME: &'static str = "Confirmation"; - - fn description(&self) -> &'static str { - "Request permission for tool calls" - } - - async fn run( - &self, - input: Self::Input, - cx: &mut AsyncApp, - ) -> Result<ToolResponse<Self::Output>> { - if agent_settings::AgentSettings::try_read_global(cx, |settings| { - settings.always_allow_tool_actions - }) - .unwrap_or(false) - { - let response = PermissionToolResponse { - behavior: PermissionToolBehavior::Allow, - updated_input: input.input, - }; - - return Ok(ToolResponse { - content: vec![ToolResponseContent::Text { - text: serde_json::to_string(&response)?, - }], - structured_content: (), - }); - } - - let mut thread_rx = self.thread_rx.clone(); - let Some(thread) = thread_rx.recv().await?.upgrade() else { - anyhow::bail!("Thread closed"); - }; - - let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone()); - let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into()); - - const ALWAYS_ALLOW: &str = "always_allow"; - const ALLOW: &str = "allow"; - const REJECT: &str = "reject"; - - let chosen_option = thread - .update(cx, |thread, cx| { - thread.request_tool_call_authorization( - claude_tool.as_acp(tool_call_id).into(), - vec![ - acp::PermissionOption { - id: acp::PermissionOptionId(ALWAYS_ALLOW.into()), - name: "Always Allow".into(), - kind: acp::PermissionOptionKind::AllowAlways, - }, - acp::PermissionOption { - id: acp::PermissionOptionId(ALLOW.into()), - name: "Allow".into(), - kind: acp::PermissionOptionKind::AllowOnce, - }, - acp::PermissionOption { - id: acp::PermissionOptionId(REJECT.into()), - name: "Reject".into(), - kind: acp::PermissionOptionKind::RejectOnce, - }, - ], - cx, - ) - })?? - .await?; - - let response = match chosen_option.0.as_ref() { - ALWAYS_ALLOW => { - cx.update(|cx| { - update_settings_file::<AgentSettings>(self.fs.clone(), cx, |settings, _| { - settings.set_always_allow_tool_actions(true); - }); - })?; - - PermissionToolResponse { - behavior: PermissionToolBehavior::Allow, - updated_input: input.input, - } - } - ALLOW => PermissionToolResponse { - behavior: PermissionToolBehavior::Allow, - updated_input: input.input, - }, - REJECT => PermissionToolResponse { - behavior: PermissionToolBehavior::Deny, - updated_input: input.input, - }, - opt => { - debug_panic!("Unexpected option: {}", opt); - PermissionToolResponse { - behavior: PermissionToolBehavior::Deny, - updated_input: input.input, - } - } - }; - - Ok(ToolResponse { - content: vec![ToolResponseContent::Text { - text: serde_json::to_string(&response)?, - }], - structured_content: (), - }) - } -} - -#[derive(Clone)] -pub struct ReadTool { - thread_rx: watch::Receiver<WeakEntity<AcpThread>>, -} - -impl McpServerTool for ReadTool { - type Input = ReadToolParams; - type Output = (); - - const NAME: &'static str = "Read"; - - fn description(&self) -> &'static str { - "Read the contents of a file. In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents." - } - - fn annotations(&self) -> ToolAnnotations { - ToolAnnotations { - title: Some("Read file".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - open_world_hint: Some(false), - idempotent_hint: None, - } - } - - async fn run( - &self, - input: Self::Input, - cx: &mut AsyncApp, - ) -> Result<ToolResponse<Self::Output>> { - 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.abs_path, input.offset, input.limit, false, cx) - })? - .await?; - - Ok(ToolResponse { - content: vec![ToolResponseContent::Text { text: content }], - structured_content: (), - }) - } -} - -#[derive(Clone)] -pub struct EditTool { - thread_rx: watch::Receiver<WeakEntity<AcpThread>>, -} - -impl McpServerTool for EditTool { - type Input = EditToolParams; - type Output = (); - - const NAME: &'static str = "Edit"; - - fn description(&self) -> &'static str { - "Edits a file. In sessions with mcp__zed__Edit always use it instead of Edit as it will show the diff to the user better." - } - - fn annotations(&self) -> ToolAnnotations { - ToolAnnotations { - title: Some("Edit file".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(false), - open_world_hint: Some(false), - idempotent_hint: Some(false), - } - } - - async fn run( - &self, - input: Self::Input, - cx: &mut AsyncApp, - ) -> Result<ToolResponse<Self::Output>> { - 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.abs_path.clone(), None, None, true, cx) - })? - .await?; - - let new_content = content.replace(&input.old_text, &input.new_text); - if new_content == content { - return Err(anyhow::anyhow!("The old_text was not found in the content")); - } - - thread - .update(cx, |thread, cx| { - thread.write_text_file(input.abs_path, new_content, cx) - })? - .await?; - - Ok(ToolResponse { - content: vec![], - structured_content: (), - }) - } -} diff --git a/crates/agent_servers/src/claude/permission_tool.rs b/crates/agent_servers/src/claude/permission_tool.rs new file mode 100644 index 0000000000000000000000000000000000000000..96a24105e87bd99b46ec16e39dc32df57557f882 --- /dev/null +++ b/crates/agent_servers/src/claude/permission_tool.rs @@ -0,0 +1,158 @@ +use std::sync::Arc; + +use acp_thread::AcpThread; +use agent_client_protocol as acp; +use agent_settings::AgentSettings; +use anyhow::{Context as _, Result}; +use context_server::{ + listener::{McpServerTool, ToolResponse}, + types::ToolResponseContent, +}; +use gpui::{AsyncApp, WeakEntity}; +use project::Fs; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{Settings as _, update_settings_file}; +use util::debug_panic; + +use crate::tools::ClaudeTool; + +#[derive(Clone)] +pub struct PermissionTool { + fs: Arc<dyn Fs>, + thread_rx: watch::Receiver<WeakEntity<AcpThread>>, +} + +/// Request permission for tool calls +#[derive(Deserialize, JsonSchema, Debug)] +pub struct PermissionToolParams { + tool_name: String, + input: serde_json::Value, + tool_use_id: Option<String>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PermissionToolResponse { + behavior: PermissionToolBehavior, + updated_input: serde_json::Value, +} + +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +enum PermissionToolBehavior { + Allow, + Deny, +} + +impl PermissionTool { + pub fn new(fs: Arc<dyn Fs>, thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self { + Self { fs, thread_rx } + } +} + +impl McpServerTool for PermissionTool { + type Input = PermissionToolParams; + type Output = (); + + const NAME: &'static str = "Confirmation"; + + async fn run( + &self, + input: Self::Input, + cx: &mut AsyncApp, + ) -> Result<ToolResponse<Self::Output>> { + if agent_settings::AgentSettings::try_read_global(cx, |settings| { + settings.always_allow_tool_actions + }) + .unwrap_or(false) + { + let response = PermissionToolResponse { + behavior: PermissionToolBehavior::Allow, + updated_input: input.input, + }; + + return Ok(ToolResponse { + content: vec![ToolResponseContent::Text { + text: serde_json::to_string(&response)?, + }], + structured_content: (), + }); + } + + let mut thread_rx = self.thread_rx.clone(); + let Some(thread) = thread_rx.recv().await?.upgrade() else { + anyhow::bail!("Thread closed"); + }; + + let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone()); + let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into()); + + const ALWAYS_ALLOW: &str = "always_allow"; + const ALLOW: &str = "allow"; + const REJECT: &str = "reject"; + + let chosen_option = thread + .update(cx, |thread, cx| { + thread.request_tool_call_authorization( + claude_tool.as_acp(tool_call_id).into(), + vec![ + acp::PermissionOption { + id: acp::PermissionOptionId(ALWAYS_ALLOW.into()), + name: "Always Allow".into(), + kind: acp::PermissionOptionKind::AllowAlways, + }, + acp::PermissionOption { + id: acp::PermissionOptionId(ALLOW.into()), + name: "Allow".into(), + kind: acp::PermissionOptionKind::AllowOnce, + }, + acp::PermissionOption { + id: acp::PermissionOptionId(REJECT.into()), + name: "Reject".into(), + kind: acp::PermissionOptionKind::RejectOnce, + }, + ], + cx, + ) + })?? + .await?; + + let response = match chosen_option.0.as_ref() { + ALWAYS_ALLOW => { + cx.update(|cx| { + update_settings_file::<AgentSettings>(self.fs.clone(), cx, |settings, _| { + settings.set_always_allow_tool_actions(true); + }); + })?; + + PermissionToolResponse { + behavior: PermissionToolBehavior::Allow, + updated_input: input.input, + } + } + ALLOW => PermissionToolResponse { + behavior: PermissionToolBehavior::Allow, + updated_input: input.input, + }, + REJECT => PermissionToolResponse { + behavior: PermissionToolBehavior::Deny, + updated_input: input.input, + }, + opt => { + debug_panic!("Unexpected option: {}", opt); + PermissionToolResponse { + behavior: PermissionToolBehavior::Deny, + updated_input: input.input, + } + } + }; + + Ok(ToolResponse { + content: vec![ToolResponseContent::Text { + text: serde_json::to_string(&response)?, + }], + structured_content: (), + }) + } +} diff --git a/crates/agent_servers/src/claude/read_tool.rs b/crates/agent_servers/src/claude/read_tool.rs new file mode 100644 index 0000000000000000000000000000000000000000..cbe25876b3deabc32d1d30f7d8ad4de90fb21494 --- /dev/null +++ b/crates/agent_servers/src/claude/read_tool.rs @@ -0,0 +1,59 @@ +use acp_thread::AcpThread; +use anyhow::Result; +use context_server::{ + listener::{McpServerTool, ToolResponse}, + types::{ToolAnnotations, ToolResponseContent}, +}; +use gpui::{AsyncApp, WeakEntity}; + +use crate::tools::ReadToolParams; + +#[derive(Clone)] +pub struct ReadTool { + thread_rx: watch::Receiver<WeakEntity<AcpThread>>, +} + +impl ReadTool { + pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self { + Self { thread_rx } + } +} + +impl McpServerTool for ReadTool { + type Input = ReadToolParams; + type Output = (); + + const NAME: &'static str = "Read"; + + fn annotations(&self) -> ToolAnnotations { + ToolAnnotations { + title: Some("Read file".to_string()), + read_only_hint: Some(true), + destructive_hint: Some(false), + open_world_hint: Some(false), + idempotent_hint: None, + } + } + + async fn run( + &self, + input: Self::Input, + cx: &mut AsyncApp, + ) -> Result<ToolResponse<Self::Output>> { + 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.abs_path, input.offset, input.limit, false, cx) + })? + .await?; + + Ok(ToolResponse { + content: vec![ToolResponseContent::Text { text: content }], + structured_content: (), + }) + } +} diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index 7ca150c0bd0b30b958a4791db9d01684d16460d6..3be10ed94c05e7562bb4956c12f72da730a6589b 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -34,6 +34,7 @@ impl ClaudeTool { // Known tools "mcp__zed__Read" => Self::ReadFile(serde_json::from_value(input).log_err()), "mcp__zed__Edit" => Self::Edit(serde_json::from_value(input).log_err()), + "mcp__zed__Write" => Self::Write(serde_json::from_value(input).log_err()), "MultiEdit" => Self::MultiEdit(serde_json::from_value(input).log_err()), "Write" => Self::Write(serde_json::from_value(input).log_err()), "LS" => Self::Ls(serde_json::from_value(input).log_err()), @@ -93,7 +94,7 @@ impl ClaudeTool { } Self::MultiEdit(None) => "Multi Edit".into(), Self::Write(Some(params)) => { - format!("Write {}", params.file_path.display()) + format!("Write {}", params.abs_path.display()) } Self::Write(None) => "Write".into(), Self::Glob(Some(params)) => { @@ -153,7 +154,7 @@ impl ClaudeTool { }], Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff { diff: acp::Diff { - path: params.file_path.clone(), + path: params.abs_path.clone(), old_text: None, new_text: params.content.clone(), }, @@ -229,7 +230,10 @@ impl ClaudeTool { line: None, }] } - Self::Write(Some(WriteToolParams { file_path, .. })) => { + Self::Write(Some(WriteToolParams { + abs_path: file_path, + .. + })) => { vec![acp::ToolCallLocation { path: file_path.clone(), line: None, @@ -302,6 +306,20 @@ impl ClaudeTool { } } +/// Edit a file. +/// +/// In sessions with mcp__zed__Edit always use it instead of Edit as it will +/// allow the user to conveniently review changes. +/// +/// File editing instructions: +/// - The `old_text` param must match existing file content, including indentation. +/// - The `old_text` param must come from the actual file, not an outline. +/// - The `old_text` section must not be empty. +/// - Be minimal with replacements: +/// - For unique lines, include only those lines. +/// - For non-unique lines, include enough context to identify them. +/// - Do not escape quotes, newlines, or other characters. +/// - Only edit the specified file. #[derive(Deserialize, JsonSchema, Debug)] pub struct EditToolParams { /// The absolute path to the file to read. @@ -312,6 +330,11 @@ pub struct EditToolParams { pub new_text: String, } +/// Reads the content of the given file in the project. +/// +/// Never attempt to read a path that hasn't been previously mentioned. +/// +/// In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents. #[derive(Deserialize, JsonSchema, Debug)] pub struct ReadToolParams { /// The absolute path to the file to read. @@ -324,11 +347,15 @@ pub struct ReadToolParams { pub limit: Option<u32>, } +/// Writes content to the specified file in the project. +/// +/// In sessions with mcp__zed__Write always use it instead of Write as it will +/// allow the user to conveniently review changes. #[derive(Deserialize, JsonSchema, Debug)] pub struct WriteToolParams { - /// Absolute path for new file - pub file_path: PathBuf, - /// File content + /// The absolute path of the file to write. + pub abs_path: PathBuf, + /// The full content to write. pub content: String, } diff --git a/crates/agent_servers/src/claude/write_tool.rs b/crates/agent_servers/src/claude/write_tool.rs new file mode 100644 index 0000000000000000000000000000000000000000..39479a9c38ba616b3d0f2e4197c112bcbea68261 --- /dev/null +++ b/crates/agent_servers/src/claude/write_tool.rs @@ -0,0 +1,59 @@ +use acp_thread::AcpThread; +use anyhow::Result; +use context_server::{ + listener::{McpServerTool, ToolResponse}, + types::ToolAnnotations, +}; +use gpui::{AsyncApp, WeakEntity}; + +use crate::tools::WriteToolParams; + +#[derive(Clone)] +pub struct WriteTool { + thread_rx: watch::Receiver<WeakEntity<AcpThread>>, +} + +impl WriteTool { + pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self { + Self { thread_rx } + } +} + +impl McpServerTool for WriteTool { + type Input = WriteToolParams; + type Output = (); + + const NAME: &'static str = "Write"; + + fn annotations(&self) -> ToolAnnotations { + ToolAnnotations { + title: Some("Write file".to_string()), + read_only_hint: Some(false), + destructive_hint: Some(false), + open_world_hint: Some(false), + idempotent_hint: Some(false), + } + } + + async fn run( + &self, + input: Self::Input, + cx: &mut AsyncApp, + ) -> Result<ToolResponse<Self::Output>> { + 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.abs_path, input.content, cx) + })? + .await?; + + Ok(ToolResponse { + content: vec![], + structured_content: (), + }) + } +} diff --git a/crates/context_server/src/listener.rs b/crates/context_server/src/listener.rs index 6f4b5c13695a3dc90325c4b09f769d98b667d8af..1b44cefbd294ab7aea8da0426d6bf0aa717d402b 100644 --- a/crates/context_server/src/listener.rs +++ b/crates/context_server/src/listener.rs @@ -14,6 +14,7 @@ use serde::de::DeserializeOwned; use serde_json::{json, value::RawValue}; use smol::stream::StreamExt; use std::{ + any::TypeId, cell::RefCell, path::{Path, PathBuf}, rc::Rc, @@ -87,18 +88,26 @@ impl McpServer { settings.inline_subschemas = true; let mut generator = settings.into_generator(); - let output_schema = generator.root_schema_for::<T::Output>(); - let unit_schema = generator.root_schema_for::<T::Output>(); + let input_schema = generator.root_schema_for::<T::Input>(); + + let description = input_schema + .get("description") + .and_then(|desc| desc.as_str()) + .map(|desc| desc.to_string()); + debug_assert!( + description.is_some(), + "Input schema struct must include a doc comment for the tool description" + ); let registered_tool = RegisteredTool { tool: Tool { name: T::NAME.into(), - description: Some(tool.description().into()), - input_schema: generator.root_schema_for::<T::Input>().into(), - output_schema: if output_schema == unit_schema { + description, + input_schema: input_schema.into(), + output_schema: if TypeId::of::<T::Output>() == TypeId::of::<()>() { None } else { - Some(output_schema.into()) + Some(generator.root_schema_for::<T::Output>().into()) }, annotations: Some(tool.annotations()), }, @@ -399,8 +408,6 @@ pub trait McpServerTool { const NAME: &'static str; - fn description(&self) -> &'static str; - fn annotations(&self) -> ToolAnnotations { ToolAnnotations { title: None, @@ -418,6 +425,7 @@ pub trait McpServerTool { ) -> impl Future<Output = Result<ToolResponse<Self::Output>>>; } +#[derive(Debug)] pub struct ToolResponse<T> { pub content: Vec<ToolResponseContent>, pub structured_content: T, diff --git a/crates/context_server/src/types.rs b/crates/context_server/src/types.rs index e92a18c763fd6fb674014c505a9c1b52ff80a43b..03aca4f3caf7995091bbc8e049494b324674a9d3 100644 --- a/crates/context_server/src/types.rs +++ b/crates/context_server/src/types.rs @@ -711,6 +711,16 @@ pub enum ToolResponseContent { Resource { resource: ResourceContents }, } +impl ToolResponseContent { + pub fn text(&self) -> Option<&str> { + if let ToolResponseContent::Text { text } = self { + Some(text) + } else { + None + } + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListToolsResponse { From 3996587c0b05ec30c54491f6911edb24c01996a5 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Tue, 19 Aug 2025 21:59:14 -0400 Subject: [PATCH 164/744] Add version detection for CC (#36502) - Render a helpful message when the installed CC version is too old - Show the full path for agent binaries when the version is not recent enough (helps in cases where multiple binaries are installed in different places) - Add UI for the case where a server binary is not installed at all - Refresh thread view after installing/updating server binary Release Notes: - N/A --- Cargo.lock | 1 + crates/acp_thread/src/acp_thread.rs | 22 ++- crates/agent_servers/Cargo.toml | 1 + crates/agent_servers/src/acp/v1.rs | 6 +- crates/agent_servers/src/claude.rs | 57 +++++++- crates/agent_servers/src/gemini.rs | 11 +- crates/agent_ui/src/acp/thread_view.rs | 180 +++++++++++++++---------- crates/agent_ui/src/agent_diff.rs | 2 +- 8 files changed, 195 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34a8ceac4937beab70da4360450c794b82e23934..5dced73fb98977a210b5d3cfed2eae5cde3d829a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,6 +285,7 @@ dependencies = [ "project", "rand 0.8.5", "schemars", + "semver", "serde", "serde_json", "settings", diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 2be7ea7a12d88a24ed866e906b52542de978840c..5d3b35d0185f5d4b8b961d97dcd077ac63c6e879 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -707,7 +707,7 @@ pub enum AcpThreadEvent { Retry(RetryStatus), Stopped, Error, - ServerExited(ExitStatus), + LoadError(LoadError), } impl EventEmitter<AcpThreadEvent> for AcpThread {} @@ -721,20 +721,30 @@ pub enum ThreadStatus { #[derive(Debug, Clone)] pub enum LoadError { + NotInstalled { + error_message: SharedString, + install_message: SharedString, + install_command: String, + }, Unsupported { error_message: SharedString, upgrade_message: SharedString, upgrade_command: String, }, - Exited(i32), + Exited { + status: ExitStatus, + }, Other(SharedString), } impl Display for LoadError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - LoadError::Unsupported { error_message, .. } => write!(f, "{}", error_message), - LoadError::Exited(status) => write!(f, "Server exited with status {}", status), + LoadError::NotInstalled { error_message, .. } + | LoadError::Unsupported { error_message, .. } => { + write!(f, "{error_message}") + } + LoadError::Exited { status } => write!(f, "Server exited with status {status}"), LoadError::Other(msg) => write!(f, "{}", msg), } } @@ -1683,8 +1693,8 @@ impl AcpThread { self.entries.iter().map(|e| e.to_markdown(cx)).collect() } - pub fn emit_server_exited(&mut self, status: ExitStatus, cx: &mut Context<Self>) { - cx.emit(AcpThreadEvent::ServerExited(status)); + pub fn emit_load_error(&mut self, error: LoadError, cx: &mut Context<Self>) { + cx.emit(AcpThreadEvent::LoadError(error)); } } diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 8cd6980ae142ef82b646f07302a995bed4b408e4..b654486cb60f46cf15fefbafa5d64e4b2310e1f3 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -37,6 +37,7 @@ paths.workspace = true project.workspace = true rand.workspace = true schemars.workspace = true +semver.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index d749537c4c3ff60affe05b745a12a0baac69f722..e0e92f29ba0c55ac5e7c256f9cfc29f96d68d16b 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -14,7 +14,7 @@ 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}; +use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError}; pub struct AcpConnection { server_name: &'static str, @@ -87,7 +87,9 @@ impl AcpConnection { for session in sessions.borrow().values() { session .thread - .update(cx, |thread, cx| thread.emit_server_exited(status, cx)) + .update(cx, |thread, cx| { + thread.emit_load_error(LoadError::Exited { status }, cx) + }) .ok(); } diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index a53c81d4c416626cb0ba55f2f1fa3c9090e5fc3f..3008edebeb4267faa250f8c1272e062a0e26d369 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -15,8 +15,9 @@ use smol::process::Child; use std::any::Any; use std::cell::RefCell; use std::fmt::Display; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::rc::Rc; +use util::command::new_smol_command; use uuid::Uuid; use agent_client_protocol as acp; @@ -36,7 +37,7 @@ use util::{ResultExt, debug_panic}; use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig}; use crate::claude::tools::ClaudeTool; use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings}; -use acp_thread::{AcpThread, AgentConnection, AuthRequired, MentionUri}; +use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri}; #[derive(Clone)] pub struct ClaudeCode; @@ -103,7 +104,11 @@ impl AgentConnection for ClaudeAgentConnection { ) .await else { - anyhow::bail!("Failed to find claude binary"); + return Err(LoadError::NotInstalled { + error_message: "Failed to find Claude Code binary".into(), + install_message: "Install Claude Code".into(), + install_command: "npm install -g @anthropic-ai/claude-code@latest".into(), + }.into()); }; let api_key = @@ -211,9 +216,32 @@ impl AgentConnection for ClaudeAgentConnection { if let Some(status) = child.status().await.log_err() && let Some(thread) = thread_rx.recv().await.ok() { + let version = claude_version(command.path.clone(), cx).await.log_err(); + let help = claude_help(command.path.clone(), cx).await.log_err(); thread .update(cx, |thread, cx| { - thread.emit_server_exited(status, cx); + let error = if let Some(version) = version + && let Some(help) = help + && (!help.contains("--input-format") + || !help.contains("--session-id")) + { + LoadError::Unsupported { + error_message: format!( + "Your installed version of Claude Code ({}, version {}) does not have required features for use with Zed.", + command.path.to_string_lossy(), + version, + ) + .into(), + upgrade_message: "Upgrade Claude Code to latest".into(), + upgrade_command: format!( + "{} update", + command.path.to_string_lossy() + ), + } + } else { + LoadError::Exited { status } + }; + thread.emit_load_error(error, cx); }) .ok(); } @@ -383,6 +411,27 @@ fn spawn_claude( Ok(child) } +fn claude_version(path: PathBuf, cx: &mut AsyncApp) -> Task<Result<semver::Version>> { + cx.background_spawn(async move { + let output = new_smol_command(path).arg("--version").output().await?; + let output = String::from_utf8(output.stdout)?; + let version = output + .trim() + .strip_suffix(" (Claude Code)") + .context("parsing Claude version")?; + let version = semver::Version::parse(version)?; + anyhow::Ok(version) + }) +} + +fn claude_help(path: PathBuf, cx: &mut AsyncApp) -> Task<Result<String>> { + cx.background_spawn(async move { + let output = new_smol_command(path).arg("--help").output().await?; + let output = String::from_utf8(output.stdout)?; + anyhow::Ok(output) + }) +} + struct ClaudeAgentSession { outgoing_tx: UnboundedSender<SdkMessage>, turn_state: Rc<RefCell<TurnState>>, diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 167e632d79847027e1e6822964cf6a2beedb5155..e1ecaf0bb572697b443db0ec15d3e4050ac24e0d 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -50,7 +50,11 @@ impl AgentServer for Gemini { let Some(command) = AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await else { - anyhow::bail!("Failed to find gemini binary"); + return Err(LoadError::NotInstalled { + error_message: "Failed to find Gemini CLI binary".into(), + install_message: "Install Gemini CLI".into(), + install_command: "npm install -g @google/gemini-cli@latest".into() + }.into()); }; let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await; @@ -75,10 +79,11 @@ impl AgentServer for Gemini { if !supported { return Err(LoadError::Unsupported { error_message: format!( - "Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).", + "Your installed version of Gemini CLI ({}, version {}) doesn't support the Agentic Coding Protocol (ACP).", + command.path.to_string_lossy(), current_version ).into(), - upgrade_message: "Upgrade Gemini to Latest".into(), + upgrade_message: "Upgrade Gemini CLI to latest".into(), upgrade_command: "npm install -g @google/gemini-cli@latest".into(), }.into()) } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 5e5d4bb83c8c3461568fdfce46b5a0d0e0bb1420..4a9001b9f49f409ecc64487f6eb4a2b1b6af1cd2 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -37,7 +37,7 @@ use rope::Point; use settings::{Settings as _, SettingsStore}; use std::sync::Arc; use std::time::Instant; -use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration}; +use std::{collections::BTreeMap, rc::Rc, time::Duration}; use text::Anchor; use theme::ThemeSettings; use ui::{ @@ -149,9 +149,6 @@ enum ThreadState { configuration_view: Option<AnyView>, _subscription: Option<Subscription>, }, - ServerExited { - status: ExitStatus, - }, } impl AcpThreadView { @@ -451,8 +448,7 @@ impl AcpThreadView { ThreadState::Ready { thread, .. } => Some(thread), ThreadState::Unauthenticated { .. } | ThreadState::Loading { .. } - | ThreadState::LoadError(..) - | ThreadState::ServerExited { .. } => None, + | ThreadState::LoadError { .. } => None, } } @@ -462,7 +458,6 @@ impl AcpThreadView { ThreadState::Loading { .. } => "Loading…".into(), ThreadState::LoadError(_) => "Failed to load".into(), ThreadState::Unauthenticated { .. } => "Authentication Required".into(), - ThreadState::ServerExited { .. } => "Server exited unexpectedly".into(), } } @@ -830,9 +825,9 @@ impl AcpThreadView { cx, ); } - AcpThreadEvent::ServerExited(status) => { + AcpThreadEvent::LoadError(error) => { self.thread_retry_status.take(); - self.thread_state = ThreadState::ServerExited { status: *status }; + self.thread_state = ThreadState::LoadError(error.clone()); } AcpThreadEvent::TitleUpdated | AcpThreadEvent::TokenUsageUpdated => {} } @@ -2154,28 +2149,6 @@ impl AcpThreadView { )) } - fn render_server_exited(&self, status: ExitStatus, _cx: &Context<Self>) -> AnyElement { - v_flex() - .items_center() - .justify_center() - .child(self.render_error_agent_logo()) - .child( - v_flex() - .mt_4() - .mb_2() - .gap_0p5() - .text_center() - .items_center() - .child(Headline::new("Server exited unexpectedly").size(HeadlineSize::Medium)) - .child( - Label::new(format!("Exit status: {}", status.code().unwrap_or(-127))) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .into_any_element() - } - fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement { let mut container = v_flex() .items_center() @@ -2204,39 +2177,102 @@ impl AcpThreadView { { let upgrade_message = upgrade_message.clone(); let upgrade_command = upgrade_command.clone(); - container = container.child(Button::new("upgrade", upgrade_message).on_click( - cx.listener(move |this, _, window, cx| { - this.workspace - .update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - let cwd = project.first_project_directory(cx); - let shell = project.terminal_settings(&cwd, cx).shell.clone(); - let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId("install".to_string()), - full_label: upgrade_command.clone(), - label: upgrade_command.clone(), - command: Some(upgrade_command.clone()), - args: Vec::new(), - command_label: upgrade_command.clone(), - cwd, - env: Default::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: Default::default(), - reveal_target: Default::default(), - hide: Default::default(), - shell, - show_summary: true, - show_command: true, - show_rerun: false, - }; - workspace - .spawn_in_terminal(spawn_in_terminal, window, cx) - .detach(); + container = container.child( + Button::new("upgrade", upgrade_message) + .tooltip(Tooltip::text(upgrade_command.clone())) + .on_click(cx.listener(move |this, _, window, cx| { + let task = this + .workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + let cwd = project.first_project_directory(cx); + let shell = project.terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId("upgrade".to_string()), + full_label: upgrade_command.clone(), + label: upgrade_command.clone(), + command: Some(upgrade_command.clone()), + args: Vec::new(), + command_label: upgrade_command.clone(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + workspace.spawn_in_terminal(spawn_in_terminal, window, cx) + }) + .ok(); + let Some(task) = task else { return }; + cx.spawn_in(window, async move |this, cx| { + if let Some(Ok(_)) = task.await { + this.update_in(cx, |this, window, cx| { + this.reset(window, cx); + }) + .ok(); + } }) - .ok(); - }), - )); + .detach() + })), + ); + } else if let LoadError::NotInstalled { + install_message, + install_command, + .. + } = e + { + let install_message = install_message.clone(); + let install_command = install_command.clone(); + container = container.child( + Button::new("install", install_message) + .tooltip(Tooltip::text(install_command.clone())) + .on_click(cx.listener(move |this, _, window, cx| { + let task = this + .workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + let cwd = project.first_project_directory(cx); + let shell = project.terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId("install".to_string()), + full_label: install_command.clone(), + label: install_command.clone(), + command: Some(install_command.clone()), + args: Vec::new(), + command_label: install_command.clone(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + workspace.spawn_in_terminal(spawn_in_terminal, window, cx) + }) + .ok(); + let Some(task) = task else { return }; + cx.spawn_in(window, async move |this, cx| { + if let Some(Ok(_)) = task.await { + this.update_in(cx, |this, window, cx| { + this.reset(window, cx); + }) + .ok(); + } + }) + .detach() + })), + ); } container.into_any() @@ -3705,6 +3741,18 @@ impl AcpThreadView { } })) } + + fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) { + self.thread_state = Self::initial_state( + self.agent.clone(), + None, + self.workspace.clone(), + self.project.clone(), + window, + cx, + ); + cx.notify(); + } } impl Focusable for AcpThreadView { @@ -3743,12 +3791,6 @@ impl Render for AcpThreadView { .items_center() .justify_center() .child(self.render_load_error(e, cx)), - ThreadState::ServerExited { status } => v_flex() - .p_2() - .flex_1() - .items_center() - .justify_center() - .child(self.render_server_exited(*status, cx)), ThreadState::Ready { thread, .. } => { let thread_clone = thread.clone(); diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index a695136562c850ce55de57578eddf4c40aa28702..b20b126d9b934cbeea2923214d131d2b0b18ebff 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1522,7 +1522,7 @@ impl AgentDiff { self.update_reviewing_editors(workspace, window, cx); } } - AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::ServerExited(_) => { + AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) => { self.update_reviewing_editors(workspace, window, cx); } AcpThreadEvent::TitleUpdated From b12d862236d9872a31868746c4ed7423535137d6 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Tue, 19 Aug 2025 23:11:17 -0300 Subject: [PATCH 165/744] Rename acp flag (#36541) Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 18 +++++++++--------- crates/feature_flags/src/feature_flags.rs | 13 +++++++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 93e9f619af31b4618de862290e3bc8c20d6f74d9..297bb5f3e842e9aed8f998f1b51bc04d313208a2 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -45,7 +45,7 @@ use assistant_tool::ToolWorkingSet; use client::{UserStore, zed_urls}; use cloud_llm_client::{CompletionIntent, Plan, UsageLimit}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; -use feature_flags::{self, AcpFeatureFlag, ClaudeCodeFeatureFlag, FeatureFlagAppExt}; +use feature_flags::{self, ClaudeCodeFeatureFlag, FeatureFlagAppExt, GeminiAndNativeFeatureFlag}; use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem, @@ -725,7 +725,7 @@ impl AgentPanel { let assistant_navigation_menu = ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| { if let Some(panel) = panel.upgrade() { - if cx.has_flag::<AcpFeatureFlag>() { + if cx.has_flag::<GeminiAndNativeFeatureFlag>() { menu = Self::populate_recently_opened_menu_section_new(menu, panel, cx); } else { menu = Self::populate_recently_opened_menu_section_old(menu, panel, cx); @@ -881,7 +881,7 @@ impl AgentPanel { } fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) { - if cx.has_flag::<AcpFeatureFlag>() { + if cx.has_flag::<GeminiAndNativeFeatureFlag>() { return self.new_agent_thread(AgentType::NativeAgent, window, cx); } // Preserve chat box text when using creating new thread @@ -1058,7 +1058,7 @@ impl AgentPanel { this.update_in(cx, |this, window, cx| { match ext_agent { crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => { - if !cx.has_flag::<AcpFeatureFlag>() { + if !cx.has_flag::<GeminiAndNativeFeatureFlag>() { return; } } @@ -1825,7 +1825,7 @@ impl Focusable for AgentPanel { ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx), ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx), ActiveView::History => { - if cx.has_flag::<feature_flags::AcpFeatureFlag>() { + if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>() { self.acp_history.focus_handle(cx) } else { self.history.focus_handle(cx) @@ -2441,7 +2441,7 @@ impl AgentPanel { ) .separator() .header("External Agents") - .when(cx.has_flag::<AcpFeatureFlag>(), |menu| { + .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| { menu.item( ContextMenuEntry::new("New Gemini Thread") .icon(IconName::AiGemini) @@ -2564,7 +2564,7 @@ impl AgentPanel { } fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { - if cx.has_flag::<feature_flags::AcpFeatureFlag>() + if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>() || cx.has_flag::<feature_flags::ClaudeCodeFeatureFlag>() { self.render_toolbar_new(window, cx).into_any_element() @@ -2749,7 +2749,7 @@ impl AgentPanel { false } _ => { - let history_is_empty = if cx.has_flag::<AcpFeatureFlag>() { + let history_is_empty = if cx.has_flag::<GeminiAndNativeFeatureFlag>() { self.acp_history_store.read(cx).is_empty(cx) } else { self.history_store @@ -3641,7 +3641,7 @@ impl Render for AgentPanel { .child(thread_view.clone()) .child(self.render_drag_target(cx)), ActiveView::History => { - if cx.has_flag::<feature_flags::AcpFeatureFlag>() { + if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>() { parent.child(self.acp_history.clone()) } else { parent.child(self.history.clone()) diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 49ccfcc85c09194017aeb96a3a590fb60a4a71ca..422979c4297cc72fdf2bf1d14cfba433d155c80e 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -89,10 +89,15 @@ impl FeatureFlag for JjUiFeatureFlag { const NAME: &'static str = "jj-ui"; } -pub struct AcpFeatureFlag; - -impl FeatureFlag for AcpFeatureFlag { - const NAME: &'static str = "acp"; +pub struct GeminiAndNativeFeatureFlag; + +impl FeatureFlag for GeminiAndNativeFeatureFlag { + // This was previously called "acp". + // + // We renamed it because existing builds used it to enable the Claude Code + // integration too, and we'd like to turn Gemini/Native on in new builds + // without enabling Claude Code in old builds. + const NAME: &'static str = "gemini-and-native"; } pub struct ClaudeCodeFeatureFlag; From cac80e2ebde41fb66de4e47cc9f8e9a8398cb7a6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Tue, 19 Aug 2025 20:26:56 -0600 Subject: [PATCH 166/744] Silence a bucketload of logs (#36534) Closes #ISSUE Release Notes: - Silenced a bunch of logs that were on by default --- crates/agent2/src/tools/terminal_tool.rs | 5 +---- crates/assistant_context/src/context_store.rs | 2 +- crates/assistant_tools/src/terminal_tool.rs | 5 +---- crates/context_server/src/client.rs | 4 ++-- crates/context_server/src/context_server.rs | 2 +- crates/db/src/db.rs | 2 +- crates/editor/src/editor.rs | 5 +---- crates/gpui/src/arena.rs | 2 +- crates/language/src/language.rs | 6 +++--- crates/project/src/context_server_store.rs | 1 - crates/project/src/context_server_store/extension.rs | 2 +- crates/project/src/debugger/breakpoint_store.rs | 1 - crates/project/src/lsp_store.rs | 4 ++-- crates/prompt_store/src/prompts.rs | 8 ++++---- crates/rpc/src/peer.rs | 2 -- crates/workspace/src/workspace.rs | 2 -- crates/zeta/src/license_detection.rs | 8 ++++---- 17 files changed, 23 insertions(+), 38 deletions(-) diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index d8f0282f4bd038c0c73b2d394c33be32c5f7b6ce..17e671fba3dffa551a033afaeef0efbe7d0448a5 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -47,12 +47,9 @@ impl TerminalTool { } if which::which("bash").is_ok() { - log::info!("agent selected bash for terminal tool"); "bash".into() } else { - let shell = get_system_shell(); - log::info!("agent selected {shell} for terminal tool"); - shell + get_system_shell() } }); Self { diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index 6d13531a57de2b8b654ba4ce0c734fc575c659cb..c5b5e99a527c58046622b2e8ee062c9d0ece68a7 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -905,7 +905,7 @@ impl ContextStore { .into_iter() .filter(assistant_slash_commands::acceptable_prompt) .map(|prompt| { - log::info!("registering context server command: {:?}", prompt.name); + log::debug!("registering context server command: {:?}", prompt.name); slash_command_working_set.insert(Arc::new( assistant_slash_commands::ContextServerSlashCommand::new( context_server_store.clone(), diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 14bbcef8b4c8f1f6992db82ed04e5e9a6b1e8212..358d62ee1a580ba0b011ba76035f55c413b3f1bc 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -59,12 +59,9 @@ impl TerminalTool { } if which::which("bash").is_ok() { - log::info!("agent selected bash for terminal tool"); "bash".into() } else { - let shell = get_system_shell(); - log::info!("agent selected {shell} for terminal tool"); - shell + get_system_shell() } }); Self { diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index 609d2c43e36f3cb51a18abc9fe4a1cb61e4c6508..ccf7622d82fb22b0792d5e33e18992037a493d54 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -161,7 +161,7 @@ impl Client { working_directory: &Option<PathBuf>, cx: AsyncApp, ) -> Result<Self> { - log::info!( + log::debug!( "starting context server (executable={:?}, args={:?})", binary.executable, &binary.args @@ -295,7 +295,7 @@ impl Client { /// Continuously reads and logs any error messages from the server. async fn handle_err(transport: Arc<dyn Transport>) -> anyhow::Result<()> { while let Some(err) = transport.receive_err().next().await { - log::warn!("context server stderr: {}", err.trim()); + log::debug!("context server stderr: {}", err.trim()); } Ok(()) diff --git a/crates/context_server/src/context_server.rs b/crates/context_server/src/context_server.rs index 34fa29678d5d68f864de7d9df3bef82d4c667f05..9ca78138dbc229a6aedd5c53c460d9205502df94 100644 --- a/crates/context_server/src/context_server.rs +++ b/crates/context_server/src/context_server.rs @@ -137,7 +137,7 @@ impl ContextServer { } async fn initialize(&self, client: Client) -> Result<()> { - log::info!("starting context server {}", self.id); + log::debug!("starting context server {}", self.id); let protocol = crate::protocol::ModelContextProtocol::new(client); let client_info = types::Implementation { name: "Zed".to_string(), diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 37e347282d0181ecfd8f58af7f65c8d6e6ecef40..8b790cbec8498c1c3f83d55b25c14042b04b9424 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -74,7 +74,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, scope: &str) -> Threa } async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnection> { - log::info!("Opening database {}", db_path.display()); + log::trace!("Opening database {}", db_path.display()); ThreadSafeConnection::builder::<M>(db_path.to_string_lossy().as_ref(), true) .with_db_initialization_query(DB_INITIALIZE_QUERY) .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f943e64923bb91525e2c189bfb9c896b28d9bcf3..575631b51767d35eb18e66553eca91759fbd889c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -16606,10 +16606,7 @@ impl Editor { .transaction(transaction_id_prev) .map(|t| t.0.clone()) }) - .unwrap_or_else(|| { - log::info!("Failed to determine selections from before format. Falling back to selections when format was initiated"); - self.selections.disjoint_anchors() - }); + .unwrap_or_else(|| self.selections.disjoint_anchors()); let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse(); let format = project.update(cx, |project, cx| { diff --git a/crates/gpui/src/arena.rs b/crates/gpui/src/arena.rs index ee72d0e96425816220094f4cbff86315153afb74..0983bd23454c9a3a921ed721ecd32561387f9049 100644 --- a/crates/gpui/src/arena.rs +++ b/crates/gpui/src/arena.rs @@ -142,7 +142,7 @@ impl Arena { if self.current_chunk_index >= self.chunks.len() { self.chunks.push(Chunk::new(self.chunk_size)); assert_eq!(self.current_chunk_index, self.chunks.len() - 1); - log::info!( + log::trace!( "increased element arena capacity to {}kb", self.capacity() / 1024, ); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index b70e4662465e267f427367c3053a53eea5869159..87fc846a537000313ee96e893b4fa6aa1a1e43f1 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -331,7 +331,7 @@ pub trait LspAdapter: 'static + Send + Sync { // for each worktree we might have open. if binary_options.allow_path_lookup && let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await { - log::info!( + log::debug!( "found user-installed language server for {}. path: {:?}, arguments: {:?}", self.name().0, binary.path, @@ -601,7 +601,7 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized> } let name = adapter.name(); - log::info!("fetching latest version of language server {:?}", name.0); + log::debug!("fetching latest version of language server {:?}", name.0); delegate.update_status(name.clone(), BinaryStatus::CheckingForUpdate); let latest_version = adapter @@ -612,7 +612,7 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized> .check_if_version_installed(latest_version.as_ref(), &container_dir, delegate.as_ref()) .await { - log::info!("language server {:?} is already installed", name.0); + log::debug!("language server {:?} is already installed", name.0); delegate.update_status(name.clone(), BinaryStatus::None); Ok(binary) } else { diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index 16625caeb4bbee62cce06c230c40f7dbb5bd33d3..e826f44b7b029ec22ca5e1484f7ff44d487ba196 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -399,7 +399,6 @@ impl ContextServerStore { async move |this, cx| { match server.clone().start(cx).await { Ok(_) => { - log::info!("Started {} context server", id); debug_assert!(server.client().is_some()); this.update(cx, |this, cx| { diff --git a/crates/project/src/context_server_store/extension.rs b/crates/project/src/context_server_store/extension.rs index 1eb0fe7da129ba9dbd3ee640cb6e02474a3990b6..2a3a0c2e4b99e56c66993d0db1fbec5b3fb9ef29 100644 --- a/crates/project/src/context_server_store/extension.rs +++ b/crates/project/src/context_server_store/extension.rs @@ -63,7 +63,7 @@ impl registry::ContextServerDescriptor for ContextServerDescriptor { .await?; command.command = extension.path_from_extension(&command.command); - log::info!("loaded command for context server {id}: {command:?}"); + log::debug!("loaded command for context server {id}: {command:?}"); Ok(ContextServerCommand { path: command.command, diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 38d8b4cfc65300931dd7045cfce53fc240eb8de2..00fcc7e69fc33a47134c3bc884e2e520ffc55023 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -831,7 +831,6 @@ impl BreakpointStore { new_breakpoints.insert(path, breakpoints_for_file); } this.update(cx, |this, cx| { - log::info!("Finish deserializing breakpoints & initializing breakpoint store"); for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| { (path.to_string_lossy(), bp_in_file.breakpoints.len()) }) { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index a8c6ffd87851b95a240641b888bf80fb2801403d..d2fb12ee3721ae33fc3aa0d9b3e87737f2e504ed 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -296,7 +296,7 @@ impl LocalLspStore { let stderr_capture = Arc::new(Mutex::new(Some(String::new()))); let server_id = self.languages.next_language_server_id(); - log::info!( + log::trace!( "attempting to start language server {:?}, path: {root_path:?}, id: {server_id}", adapter.name.0 ); @@ -7529,7 +7529,7 @@ impl LspStore { .ok() .flatten()?; - log::info!("Refreshing workspace configurations for servers {refreshed_servers:?}"); + log::debug!("Refreshing workspace configurations for servers {refreshed_servers:?}"); // TODO this asynchronous job runs concurrently with extension (de)registration and may take enough time for a certain extension // to stop and unregister its language server wrapper. // This is racy : an extension might have already removed all `local.language_servers` state, but here we `.clone()` and hold onto it anyway. diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs index cd34bafb20a73b0e23da326c5e117e670ed0bd97..4ab867ab64415cf546ffd9d66c1b7aad67df0aae 100644 --- a/crates/prompt_store/src/prompts.rs +++ b/crates/prompt_store/src/prompts.rs @@ -229,12 +229,12 @@ impl PromptBuilder { log_message.push_str(" -> "); log_message.push_str(&target.display().to_string()); } - log::info!("{}.", log_message); + log::trace!("{}.", log_message); } else { if !found_dir_once { - log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display()); + log::trace!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display()); if let Some(target) = symlink_status { - log::info!("Symlink found pointing to {}, but target is invalid.", target.display()); + log::trace!("Symlink found pointing to {}, but target is invalid.", target.display()); } } @@ -247,7 +247,7 @@ impl PromptBuilder { log_message.push_str(" -> "); log_message.push_str(&target.display().to_string()); } - log::info!("{}.", log_message); + log::trace!("{}.", log_message); break; } } diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 8b77788d22677eea4691f8bfbf9af64412b095ec..98f5fa40e9636ae6ba2b5d448859b42a8214ef56 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -26,7 +26,6 @@ use std::{ time::Duration, time::Instant, }; -use tracing::instrument; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize)] pub struct ConnectionId { @@ -109,7 +108,6 @@ impl Peer { self.epoch.load(SeqCst) } - #[instrument(skip_all)] pub fn add_connection<F, Fut, Out>( self: &Arc<Self>, connection: Connection, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8c1be61abfc78870b6d7c4a047063f8de67c6511..d64a4472a072739e2bfb7d0a4159ca443f9edbce 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2503,8 +2503,6 @@ impl Workspace { window: &mut Window, cx: &mut Context<Self>, ) -> Task<Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>> { - log::info!("open paths {abs_paths:?}"); - let fs = self.app_state.fs.clone(); // Sort the paths to ensure we add worktrees for parents before their children. diff --git a/crates/zeta/src/license_detection.rs b/crates/zeta/src/license_detection.rs index 3dd025c1e156c95cb3a8bdf5f5a2e49060033304..022b2d19de433e9087454fec0874c0d1b31ae6c3 100644 --- a/crates/zeta/src/license_detection.rs +++ b/crates/zeta/src/license_detection.rs @@ -143,10 +143,10 @@ impl LicenseDetectionWatcher { } async fn is_path_eligible(fs: &Arc<dyn Fs>, abs_path: PathBuf) -> Option<bool> { - log::info!("checking if `{abs_path:?}` is an open source license"); + log::debug!("checking if `{abs_path:?}` is an open source license"); // Resolve symlinks so that the file size from metadata is correct. let Some(abs_path) = fs.canonicalize(&abs_path).await.ok() else { - log::info!( + log::debug!( "`{abs_path:?}` license file probably deleted (error canonicalizing the path)" ); return None; @@ -159,11 +159,11 @@ impl LicenseDetectionWatcher { let text = fs.load(&abs_path).await.log_err()?; let is_eligible = is_license_eligible_for_data_collection(&text); if is_eligible { - log::info!( + log::debug!( "`{abs_path:?}` matches a license that is eligible for data collection (if enabled)" ); } else { - log::info!( + log::debug!( "`{abs_path:?}` does not match a license that is eligible for data collection" ); } From ceec258bf32cf86ef6a3948d60385aeb8a639390 Mon Sep 17 00:00:00 2001 From: Ben Brandt <benjamin.j.brandt@gmail.com> Date: Wed, 20 Aug 2025 05:40:39 +0200 Subject: [PATCH 167/744] Some clippy fixes (#36544) These showed up today, so just applied the simplifications, which were mostly switching matches to if let Release Notes: - N/A --- crates/inspector_ui/src/div_inspector.rs | 42 +++-- crates/remote/src/ssh_session.rs | 189 +++++++++++------------ crates/vim/src/test/neovim_connection.rs | 8 +- 3 files changed, 115 insertions(+), 124 deletions(-) diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs index e9460cc9cca7420b67368184dcd36d8ecb079f4d..0c2b16b9f49019bff1f7860ae5cd658ae20f12b5 100644 --- a/crates/inspector_ui/src/div_inspector.rs +++ b/crates/inspector_ui/src/div_inspector.rs @@ -93,8 +93,8 @@ impl DivInspector { Ok((json_style_buffer, rust_style_buffer)) => { this.update_in(cx, |this, window, cx| { this.state = State::BuffersLoaded { - json_style_buffer: json_style_buffer, - rust_style_buffer: rust_style_buffer, + json_style_buffer, + rust_style_buffer, }; // Initialize editors immediately instead of waiting for @@ -200,8 +200,8 @@ impl DivInspector { cx.subscribe_in(&json_style_editor, window, { let id = id.clone(); let rust_style_buffer = rust_style_buffer.clone(); - move |this, editor, event: &EditorEvent, window, cx| match event { - EditorEvent::BufferEdited => { + move |this, editor, event: &EditorEvent, window, cx| { + if event == &EditorEvent::BufferEdited { let style_json = editor.read(cx).text(cx); match serde_json_lenient::from_str_lenient::<StyleRefinement>(&style_json) { Ok(new_style) => { @@ -243,7 +243,6 @@ impl DivInspector { Err(err) => this.json_style_error = Some(err.to_string().into()), } } - _ => {} } }) .detach(); @@ -251,11 +250,10 @@ impl DivInspector { cx.subscribe(&rust_style_editor, { let json_style_buffer = json_style_buffer.clone(); let rust_style_buffer = rust_style_buffer.clone(); - move |this, _editor, event: &EditorEvent, cx| match event { - EditorEvent::BufferEdited => { + move |this, _editor, event: &EditorEvent, cx| { + if let EditorEvent::BufferEdited = event { this.update_json_style_from_rust(&json_style_buffer, &rust_style_buffer, cx); } - _ => {} } }) .detach(); @@ -271,23 +269,19 @@ impl DivInspector { } fn reset_style(&mut self, cx: &mut App) { - match &self.state { - State::Ready { - rust_style_buffer, - json_style_buffer, - .. - } => { - if let Err(err) = self.reset_style_editors( - &rust_style_buffer.clone(), - &json_style_buffer.clone(), - cx, - ) { - self.json_style_error = Some(format!("{err}").into()); - } else { - self.json_style_error = None; - } + if let State::Ready { + rust_style_buffer, + json_style_buffer, + .. + } = &self.state + { + if let Err(err) = + self.reset_style_editors(&rust_style_buffer.clone(), &json_style_buffer.clone(), cx) + { + self.json_style_error = Some(format!("{err}").into()); + } else { + self.json_style_error = None; } - _ => {} } } diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index ffd0cac310529e7dfc9034f936287eb98145df4f..7173bc9b3b18e656323e7908ab7008769cc8e9e8 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -2125,109 +2125,106 @@ impl SshRemoteConnection { .env("RUSTFLAGS", &rust_flags), ) .await?; - } else { - if build_remote_server.contains("cross") { - #[cfg(target_os = "windows")] - use util::paths::SanitizedPath; - - delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx); - log::info!("installing cross"); - run_cmd(Command::new("cargo").args([ - "install", - "cross", - "--git", - "https://github.com/cross-rs/cross", - ])) - .await?; + } else if build_remote_server.contains("cross") { + #[cfg(target_os = "windows")] + use util::paths::SanitizedPath; + + delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx); + log::info!("installing cross"); + run_cmd(Command::new("cargo").args([ + "install", + "cross", + "--git", + "https://github.com/cross-rs/cross", + ])) + .await?; - delegate.set_status( - Some(&format!( - "Building remote server binary from source for {} with Docker", - &triple - )), - cx, - ); - log::info!("building remote server binary from source for {}", &triple); + delegate.set_status( + Some(&format!( + "Building remote server binary from source for {} with Docker", + &triple + )), + cx, + ); + log::info!("building remote server binary from source for {}", &triple); - // On Windows, the binding needs to be set to the canonical path - #[cfg(target_os = "windows")] - let src = - SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string(); - #[cfg(not(target_os = "windows"))] - let src = "./target"; - run_cmd( - Command::new("cross") - .args([ - "build", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env( - "CROSS_CONTAINER_OPTS", - format!("--mount type=bind,src={src},dst=/app/target"), - ) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - } else { - let which = cx - .background_spawn(async move { which::which("zig") }) - .await; + // On Windows, the binding needs to be set to the canonical path + #[cfg(target_os = "windows")] + let src = + SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string(); + #[cfg(not(target_os = "windows"))] + let src = "./target"; + run_cmd( + Command::new("cross") + .args([ + "build", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env( + "CROSS_CONTAINER_OPTS", + format!("--mount type=bind,src={src},dst=/app/target"), + ) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; + } else { + let which = cx + .background_spawn(async move { which::which("zig") }) + .await; - if which.is_err() { - #[cfg(not(target_os = "windows"))] - { - anyhow::bail!( - "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" - ) - } - #[cfg(target_os = "windows")] - { - anyhow::bail!( - "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" - ) - } + if which.is_err() { + #[cfg(not(target_os = "windows"))] + { + anyhow::bail!( + "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" + ) } + #[cfg(target_os = "windows")] + { + anyhow::bail!( + "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" + ) + } + } - delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); - log::info!("adding rustup target"); - run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?; + delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); + log::info!("adding rustup target"); + run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?; - delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx); - log::info!("installing cargo-zigbuild"); - run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])) - .await?; + delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx); + log::info!("installing cargo-zigbuild"); + run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?; - delegate.set_status( - Some(&format!( - "Building remote binary from source for {triple} with Zig" - )), - cx, - ); - log::info!("building remote binary from source for {triple} with Zig"); - run_cmd( - Command::new("cargo") - .args([ - "zigbuild", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - } + delegate.set_status( + Some(&format!( + "Building remote binary from source for {triple} with Zig" + )), + cx, + ); + log::info!("building remote binary from source for {triple} with Zig"); + run_cmd( + Command::new("cargo") + .args([ + "zigbuild", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; }; let bin_path = Path::new("target") .join("remote_server") diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index c2f7414f44e0bcdb35ff14ddfa2d75e548830b81..f87ccc283fb9173ff4f283f21e409de25fe6ad11 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -299,10 +299,10 @@ impl NeovimConnection { if let Some(NeovimData::Get { .. }) = self.data.front() { self.data.pop_front(); }; - if let Some(NeovimData::ReadRegister { name, value }) = self.data.pop_front() { - if name == register { - return value; - } + if let Some(NeovimData::ReadRegister { name, value }) = self.data.pop_front() + && name == register + { + return value; } panic!("operation does not match recorded script. re-record with --features=neovim") From d273aca1c1f3abc5159457b8af977fd40fbedb7c Mon Sep 17 00:00:00 2001 From: Ben Brandt <benjamin.j.brandt@gmail.com> Date: Wed, 20 Aug 2025 06:06:24 +0200 Subject: [PATCH 168/744] agent_ui: Add check to prevent sending empty messages in MessageEditor (#36545) Release Notes: - N/A --- crates/agent_ui/src/acp/message_editor.rs | 5 ++++- crates/agent_ui/src/acp/thread_view.rs | 26 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 311fe258de9e035be95697bee9a5e9b4d6851ffa..cb20740f3c7f5949cf954f0d613af347f9534e1c 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -66,7 +66,7 @@ pub struct MessageEditor { _parse_slash_command_task: Task<()>, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum MessageEditorEvent { Send, Cancel, @@ -728,6 +728,9 @@ impl MessageEditor { } fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) { + if self.is_empty(cx) { + return; + } cx.emit(MessageEditorEvent::Send) } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 4a9001b9f49f409ecc64487f6eb4a2b1b6af1cd2..05f626d48e8aaf883dd881b172c7210d150a11ed 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4564,6 +4564,32 @@ pub(crate) mod tests { }); } + #[gpui::test] + async fn test_message_doesnt_send_if_empty(cx: &mut TestAppContext) { + init_test(cx); + + let connection = StubAgentConnection::new(); + + let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; + add_to_workspace(thread_view.clone(), cx); + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + let mut events = cx.events(&message_editor); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("", window, cx); + }); + + message_editor.update_in(cx, |_editor, window, cx| { + window.dispatch_action(Box::new(Chat), cx); + }); + cx.run_until_parked(); + // We shouldn't have received any messages + assert!(matches!( + events.try_next(), + Err(futures::channel::mpsc::TryRecvError { .. }) + )); + } + #[gpui::test] async fn test_message_editing_regenerate(cx: &mut TestAppContext) { init_test(cx); From fbba6addfd1f1539408af582e65b356a308ba2f7 Mon Sep 17 00:00:00 2001 From: zumbalogy <zumbalogy@users.noreply.github.com> Date: Wed, 20 Aug 2025 06:39:51 +0200 Subject: [PATCH 169/744] docs: Document `global_lsp_settings.button` and remove duplicate docs for `lsp_highlight_debounce` (#36547) Follow up to this discussion: https://github.com/zed-industries/zed/pull/36337 Release Notes: - N/A This will (gracefully) break links to https://zed.dev/docs/configuring-zed#lsp-highlight-debounce-1 I don't see anything show up for that on google or github search and I don't think its load bearing. --------- Co-authored-by: zumbalogy <3770982+zumbalogy@users.noreply.github.com> --- docs/src/configuring-zed.md | 18 ++++++++++++------ docs/src/visual-customization.md | 6 ++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 9d561302566d855e093cfb1993ab466d0c3dfe72..39d172ea5f22c5f88606bfc5ccaef35b09d47831 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -539,12 +539,6 @@ List of `string` values - Setting: `selection_highlight` - Default: `true` -## LSP Highlight Debounce - -- Description: The debounce delay before querying highlights from the language server based on the current cursor location. -- Setting: `lsp_highlight_debounce` -- Default: `75` - ## Cursor Blink - Description: Whether or not the cursor blinks. @@ -1339,6 +1333,18 @@ While other options may be changed at a runtime and should be placed under `sett - Setting: `lsp_highlight_debounce` - Default: `75` +## Global LSP Settings + +- Description: Common language server settings. +- Setting: `global_lsp_settings` +- Default: + +```json +"global_lsp_settings": { + "button": true +} +``` + **Options** `integer` values representing milliseconds diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 6e598f44361556d9e2325086c992f11c3d202a28..3ad1e381d9517dd613e58e8a52f664aabbd2cab0 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -321,6 +321,12 @@ TBD: Centered layout related settings // Defaults to true. "cursor_position_button": true, }, + "global_lsp_settings": { + // Show/hide the LSP button in the status bar. + // Activity from the LSP is still shown. + // Button is not shown if "enable_language_server" if false. + "button": true + }, ``` ### Multibuffer From 60960409f7a22ff0f66db53d74b0b45f574881d6 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 20 Aug 2025 01:47:28 -0300 Subject: [PATCH 170/744] thread view: Refine the UI a bit (#36504) Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <agus@zed.dev> --- assets/icons/menu_alt.svg | 4 +- assets/icons/menu_alt_temp.svg | 3 + assets/icons/x_circle_filled.svg | 3 + assets/icons/zed_agent.svg | 27 ++ crates/agent2/src/native_agent_server.rs | 7 +- crates/agent_servers/src/gemini.rs | 2 +- crates/agent_ui/src/acp/entry_view_state.rs | 1 + crates/agent_ui/src/acp/thread_view.rs | 259 +++++++++++++------- crates/agent_ui/src/agent_panel.rs | 88 +++---- crates/icons/src/icons.rs | 3 + crates/markdown/src/markdown.rs | 8 +- 11 files changed, 261 insertions(+), 144 deletions(-) create mode 100644 assets/icons/menu_alt_temp.svg create mode 100644 assets/icons/x_circle_filled.svg create mode 100644 assets/icons/zed_agent.svg diff --git a/assets/icons/menu_alt.svg b/assets/icons/menu_alt.svg index f73102e286c51e5c52fcec40cb976a3bd6a981cf..87add13216d9eb8c4c3d8f345ff1695e98be2d5d 100644 --- a/assets/icons/menu_alt.svg +++ b/assets/icons/menu_alt.svg @@ -1 +1,3 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.667 8h8M2.667 4h10.666M2.667 12H8"/></svg> +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M13.333 10H8M13.333 6H2.66701" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/icons/menu_alt_temp.svg b/assets/icons/menu_alt_temp.svg new file mode 100644 index 0000000000000000000000000000000000000000..87add13216d9eb8c4c3d8f345ff1695e98be2d5d --- /dev/null +++ b/assets/icons/menu_alt_temp.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M13.333 10H8M13.333 6H2.66701" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/icons/x_circle_filled.svg b/assets/icons/x_circle_filled.svg new file mode 100644 index 0000000000000000000000000000000000000000..52215acda8a6b7fc57820fa90f6ed405e6af637c --- /dev/null +++ b/assets/icons/x_circle_filled.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2ZM10.4238 5.57617C10.1895 5.34187 9.81049 5.3419 9.57617 5.57617L8 7.15234L6.42383 5.57617C6.18953 5.34187 5.81049 5.3419 5.57617 5.57617C5.34186 5.81049 5.34186 6.18951 5.57617 6.42383L7.15234 8L5.57617 9.57617C5.34186 9.81049 5.34186 10.1895 5.57617 10.4238C5.81049 10.6581 6.18954 10.6581 6.42383 10.4238L8 8.84766L9.57617 10.4238C9.81049 10.6581 10.1895 10.6581 10.4238 10.4238C10.6581 10.1895 10.658 9.81048 10.4238 9.57617L8.84766 8L10.4238 6.42383C10.6581 6.18954 10.658 5.81048 10.4238 5.57617Z" fill="black"/> +</svg> diff --git a/assets/icons/zed_agent.svg b/assets/icons/zed_agent.svg new file mode 100644 index 0000000000000000000000000000000000000000..b6e120a0b6c3ca1d7eaf1049c51f2db7e9ff5b97 --- /dev/null +++ b/assets/icons/zed_agent.svg @@ -0,0 +1,27 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M11 8.75V10.5C8.93097 10.5 8.06903 10.5 6 10.5V10L11 6V5.5H6V7.25" stroke="black" stroke-width="1.2"/> +<path d="M2 8.5C2.27614 8.5 2.5 8.27614 2.5 8C2.5 7.72386 2.27614 7.5 2 7.5C1.72386 7.5 1.5 7.72386 1.5 8C1.5 8.27614 1.72386 8.5 2 8.5Z" fill="black"/> +<path d="M2.99976 6.33002C3.2759 6.33002 3.49976 6.10616 3.49976 5.83002C3.49976 5.55387 3.2759 5.33002 2.99976 5.33002C2.72361 5.33002 2.49976 5.55387 2.49976 5.83002C2.49976 6.10616 2.72361 6.33002 2.99976 6.33002Z" fill="black"/> +<path d="M2.99976 10.66C3.2759 10.66 3.49976 10.4361 3.49976 10.16C3.49976 9.88383 3.2759 9.65997 2.99976 9.65997C2.72361 9.65997 2.49976 9.88383 2.49976 10.16C2.49976 10.4361 2.72361 10.66 2.99976 10.66Z" fill="black"/> +<path d="M15 8.5C15.2761 8.5 15.5 8.27614 15.5 8C15.5 7.72386 15.2761 7.5 15 7.5C14.7239 7.5 14.5 7.72386 14.5 8C14.5 8.27614 14.7239 8.5 15 8.5Z" fill="black"/> +<path d="M14 6.33002C14.2761 6.33002 14.5 6.10616 14.5 5.83002C14.5 5.55387 14.2761 5.33002 14 5.33002C13.7239 5.33002 13.5 5.55387 13.5 5.83002C13.5 6.10616 13.7239 6.33002 14 6.33002Z" fill="black"/> +<path d="M14 10.66C14.2761 10.66 14.5 10.4361 14.5 10.16C14.5 9.88383 14.2761 9.65997 14 9.65997C13.7239 9.65997 13.5 9.88383 13.5 10.16C13.5 10.4361 13.7239 10.66 14 10.66Z" fill="black"/> +<path d="M8.49219 2C8.76833 2 8.99219 1.77614 8.99219 1.5C8.99219 1.22386 8.76833 1 8.49219 1C8.21605 1 7.99219 1.22386 7.99219 1.5C7.99219 1.77614 8.21605 2 8.49219 2Z" fill="black"/> +<path d="M6 3C6.27614 3 6.5 2.77614 6.5 2.5C6.5 2.22386 6.27614 2 6 2C5.72386 2 5.5 2.22386 5.5 2.5C5.5 2.77614 5.72386 3 6 3Z" fill="black"/> +<path d="M4 4C4.27614 4 4.5 3.77614 4.5 3.5C4.5 3.22386 4.27614 3 4 3C3.72386 3 3.5 3.22386 3.5 3.5C3.5 3.77614 3.72386 4 4 4Z" fill="black"/> +<path d="M3.99976 13C4.2759 13 4.49976 12.7761 4.49976 12.5C4.49976 12.2239 4.2759 12 3.99976 12C3.72361 12 3.49976 12.2239 3.49976 12.5C3.49976 12.7761 3.72361 13 3.99976 13Z" fill="black"/> +<path d="M2 12.5C2.27614 12.5 2.5 12.2761 2.5 12C2.5 11.7239 2.27614 11.5 2 11.5C1.72386 11.5 1.5 11.7239 1.5 12C1.5 12.2761 1.72386 12.5 2 12.5Z" fill="black"/> +<path d="M2 4.5C2.27614 4.5 2.5 4.27614 2.5 4C2.5 3.72386 2.27614 3.5 2 3.5C1.72386 3.5 1.5 3.72386 1.5 4C1.5 4.27614 1.72386 4.5 2 4.5Z" fill="black"/> +<path d="M15 12.5C15.2761 12.5 15.5 12.2761 15.5 12C15.5 11.7239 15.2761 11.5 15 11.5C14.7239 11.5 14.5 11.7239 14.5 12C14.5 12.2761 14.7239 12.5 15 12.5Z" fill="black"/> +<path d="M15 4.5C15.2761 4.5 15.5 4.27614 15.5 4C15.5 3.72386 15.2761 3.5 15 3.5C14.7239 3.5 14.5 3.72386 14.5 4C14.5 4.27614 14.7239 4.5 15 4.5Z" fill="black"/> +<path d="M3.99976 15C4.2759 15 4.49976 14.7761 4.49976 14.5C4.49976 14.2239 4.2759 14 3.99976 14C3.72361 14 3.49976 14.2239 3.49976 14.5C3.49976 14.7761 3.72361 15 3.99976 15Z" fill="black"/> +<path d="M4 2C4.27614 2 4.5 1.77614 4.5 1.5C4.5 1.22386 4.27614 1 4 1C3.72386 1 3.5 1.22386 3.5 1.5C3.5 1.77614 3.72386 2 4 2Z" fill="black"/> +<path d="M13 15C13.2761 15 13.5 14.7761 13.5 14.5C13.5 14.2239 13.2761 14 13 14C12.7239 14 12.5 14.2239 12.5 14.5C12.5 14.7761 12.7239 15 13 15Z" fill="black"/> +<path d="M13 2C13.2761 2 13.5 1.77614 13.5 1.5C13.5 1.22386 13.2761 1 13 1C12.7239 1 12.5 1.22386 12.5 1.5C12.5 1.77614 12.7239 2 13 2Z" fill="black"/> +<path d="M13 4C13.2761 4 13.5 3.77614 13.5 3.5C13.5 3.22386 13.2761 3 13 3C12.7239 3 12.5 3.22386 12.5 3.5C12.5 3.77614 12.7239 4 13 4Z" fill="black"/> +<path d="M13 13C13.2761 13 13.5 12.7761 13.5 12.5C13.5 12.2239 13.2761 12 13 12C12.7239 12 12.5 12.2239 12.5 12.5C12.5 12.7761 12.7239 13 13 13Z" fill="black"/> +<path d="M11 3C11.2761 3 11.5 2.77614 11.5 2.5C11.5 2.22386 11.2761 2 11 2C10.7239 2 10.5 2.22386 10.5 2.5C10.5 2.77614 10.7239 3 11 3Z" fill="black"/> +<path d="M8.5 15C8.77614 15 9 14.7761 9 14.5C9 14.2239 8.77614 14 8.5 14C8.22386 14 8 14.2239 8 14.5C8 14.7761 8.22386 15 8.5 15Z" fill="black"/> +<path d="M6 14C6.27614 14 6.5 13.7761 6.5 13.5C6.5 13.2239 6.27614 13 6 13C5.72386 13 5.5 13.2239 5.5 13.5C5.5 13.7761 5.72386 14 6 14Z" fill="black"/> +<path d="M11 14C11.2761 14 11.5 13.7761 11.5 13.5C11.5 13.2239 11.2761 13 11 13C10.7239 13 10.5 13.2239 10.5 13.5C10.5 13.7761 10.7239 14 11 14Z" fill="black"/> +</svg> diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index f8cf3dd602c2a27a0062bdb1ad71901e920f9387..74d24efb13703dedd2e9109e8d19175cac606631 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -27,16 +27,15 @@ impl AgentServer for NativeAgentServer { } fn empty_state_headline(&self) -> &'static str { - "Native Agent" + "" } fn empty_state_message(&self) -> &'static str { - "How can I help you today?" + "" } fn logo(&self) -> ui::IconName { - // Using the ZedAssistant icon as it's the native built-in agent - ui::IconName::ZedAssistant + ui::IconName::ZedAgent } fn connect( diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index e1ecaf0bb572697b443db0ec15d3e4050ac24e0d..813f8b1fe0eb0036b74bd441d1a1c827b882b4d0 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -26,7 +26,7 @@ impl AgentServer for Gemini { } fn empty_state_message(&self) -> &'static str { - "Ask questions, edit files, run commands.\nBe specific for the best results." + "Ask questions, edit files, run commands" } fn logo(&self) -> ui::IconName { diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 0b0b8471a7debb9388e26659b347137b4e062c05..98af9bf8388f66b25a6cf959f6030ab745a2bc4c 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -189,6 +189,7 @@ pub enum ViewEvent { MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent), } +#[derive(Debug)] pub enum Entry { UserMessage(Entity<MessageEditor>), Content(HashMap<EntityId, AnyEntity>), diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 05f626d48e8aaf883dd881b172c7210d150a11ed..4862bb0aa6d465397d2da08295709ebab78a2d67 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -21,11 +21,11 @@ use file_icons::FileIcons; use fs::Fs; use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, - EdgesRefinement, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, - MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, - TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, - WindowHandle, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*, - pulsating_between, + EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, + ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, + Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, + WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point, + prelude::*, pulsating_between, }; use language::Buffer; @@ -170,7 +170,7 @@ impl AcpThreadView { project.clone(), thread_store.clone(), text_thread_store.clone(), - "Message the agent - @ to include context", + "Message the agent — @ to include context", prevent_slash_commands, editor::EditorMode::AutoHeight { min_lines: MIN_EDITOR_LINES, @@ -928,29 +928,41 @@ impl AcpThreadView { None }; - div() + v_flex() .id(("user_message", entry_ix)) - .py_4() + .pt_2() + .pb_4() .px_2() + .gap_1p5() + .w_full() + .children(rules_item) .children(message.id.clone().and_then(|message_id| { message.checkpoint.as_ref()?.show.then(|| { - Button::new("restore-checkpoint", "Restore Checkpoint") - .icon(IconName::Undo) - .icon_size(IconSize::XSmall) - .icon_position(IconPosition::Start) - .label_size(LabelSize::XSmall) - .on_click(cx.listener(move |this, _, _window, cx| { - this.rewind(&message_id, cx); - })) + h_flex() + .gap_2() + .child(Divider::horizontal()) + .child( + Button::new("restore-checkpoint", "Restore Checkpoint") + .icon(IconName::Undo) + .icon_size(IconSize::XSmall) + .icon_position(IconPosition::Start) + .label_size(LabelSize::XSmall) + .icon_color(Color::Muted) + .color(Color::Muted) + .on_click(cx.listener(move |this, _, _window, cx| { + this.rewind(&message_id, cx); + })) + ) + .child(Divider::horizontal()) }) })) - .children(rules_item) .child( div() .relative() .child( div() - .p_3() + .py_3() + .px_2() .rounded_lg() .shadow_md() .bg(cx.theme().colors().editor_background) @@ -1080,12 +1092,20 @@ impl AcpThreadView { if let Some(editing_index) = self.editing_message.as_ref() && *editing_index < entry_ix { + let backdrop = div() + .id(("backdrop", entry_ix)) + .size_full() + .absolute() + .inset_0() + .bg(cx.theme().colors().panel_background) + .opacity(0.8) + .block_mouse_except_scroll() + .on_click(cx.listener(Self::cancel_editing)); + div() + .relative() .child(primary) - .opacity(0.2) - .block_mouse_except_scroll() - .id("overlay") - .on_click(cx.listener(Self::cancel_editing)) + .child(backdrop) .into_any_element() } else { primary @@ -1100,7 +1120,7 @@ impl AcpThreadView { } fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla { - cx.theme().colors().border.opacity(0.6) + cx.theme().colors().border.opacity(0.8) } fn tool_name_font_size(&self) -> Rems { @@ -1299,23 +1319,14 @@ impl AcpThreadView { tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } ); - let is_edit = matches!(tool_call.kind, acp::ToolKind::Edit); - let has_diff = tool_call - .content - .iter() - .any(|content| matches!(content, ToolCallContent::Diff { .. })); - let has_nonempty_diff = tool_call.content.iter().any(|content| match content { - ToolCallContent::Diff(diff) => diff.read(cx).has_revealed_range(cx), - _ => false, - }); - let use_card_layout = needs_confirmation || is_edit || has_diff; + let is_edit = + matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some(); + let use_card_layout = needs_confirmation || is_edit; let is_collapsible = !tool_call.content.is_empty() && !use_card_layout; - let is_open = tool_call.content.is_empty() - || needs_confirmation - || has_nonempty_diff - || self.expanded_tool_calls.contains(&tool_call.id); + let is_open = + needs_confirmation || is_edit || self.expanded_tool_calls.contains(&tool_call.id); let gradient_overlay = |color: Hsla| { div() @@ -1336,41 +1347,49 @@ impl AcpThreadView { cx.theme().colors().panel_background }; - let tool_output_display = match &tool_call.status { - ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex() - .w_full() - .children(tool_call.content.iter().map(|content| { - div() - .child( - self.render_tool_call_content(entry_ix, content, tool_call, window, cx), - ) - .into_any_element() - })) - .child(self.render_permission_buttons( - options, - entry_ix, - tool_call.id.clone(), - tool_call.content.is_empty(), - cx, - )), - ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed - | ToolCallStatus::Failed - | ToolCallStatus::Canceled => { - v_flex() + let tool_output_display = if is_open { + match &tool_call.status { + ToolCallStatus::WaitingForConfirmation { options, .. } => { + v_flex() + .w_full() + .children(tool_call.content.iter().map(|content| { + div() + .child(self.render_tool_call_content( + entry_ix, content, tool_call, window, cx, + )) + .into_any_element() + })) + .child(self.render_permission_buttons( + options, + entry_ix, + tool_call.id.clone(), + tool_call.content.is_empty(), + cx, + )) + .into_any() + } + ToolCallStatus::Pending | ToolCallStatus::InProgress + if is_edit && tool_call.content.is_empty() => + { + self.render_diff_loading(cx).into_any() + } + ToolCallStatus::Pending + | ToolCallStatus::InProgress + | ToolCallStatus::Completed + | ToolCallStatus::Failed + | ToolCallStatus::Canceled => v_flex() .w_full() .children(tool_call.content.iter().map(|content| { - div() - .child( - self.render_tool_call_content( - entry_ix, content, tool_call, window, cx, - ), - ) - .into_any_element() + div().child( + self.render_tool_call_content(entry_ix, content, tool_call, window, cx), + ) })) + .into_any(), + ToolCallStatus::Rejected => Empty.into_any(), } - ToolCallStatus::Rejected => v_flex().size_0(), + .into() + } else { + None }; v_flex() @@ -1390,9 +1409,13 @@ impl AcpThreadView { .map(|this| { if use_card_layout { this.pl_2() - .pr_1() + .pr_1p5() .py_1() .rounded_t_md() + .when(is_open, |this| { + this.border_b_1() + .border_color(self.tool_card_border_color(cx)) + }) .bg(self.tool_card_header_bg(cx)) } else { this.opacity(0.8).hover(|style| style.opacity(1.)) @@ -1403,6 +1426,7 @@ impl AcpThreadView { .group(&card_header_id) .relative() .w_full() + .min_h_6() .text_size(self.tool_name_font_size()) .child(self.render_tool_call_icon( card_header_id, @@ -1456,11 +1480,7 @@ impl AcpThreadView { .overflow_x_scroll() .child(self.render_markdown( tool_call.label.clone(), - default_markdown_style( - needs_confirmation || is_edit || has_diff, - window, - cx, - ), + default_markdown_style(false, window, cx), )), ) .child(gradient_overlay(gradient_color)) @@ -1480,7 +1500,7 @@ impl AcpThreadView { ) .children(status_icon), ) - .when(is_open, |this| this.child(tool_output_display)) + .children(tool_output_display) } fn render_tool_call_content( @@ -1501,7 +1521,7 @@ impl AcpThreadView { Empty.into_any_element() } } - ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, diff, cx), + ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, diff, tool_call, cx), ToolCallContent::Terminal(terminal) => { self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx) } @@ -1645,21 +1665,69 @@ impl AcpThreadView { }))) } + fn render_diff_loading(&self, cx: &Context<Self>) -> AnyElement { + let bar = |n: u64, width_class: &str| { + let bg_color = cx.theme().colors().element_active; + let base = h_flex().h_1().rounded_full(); + + let modified = match width_class { + "w_4_5" => base.w_3_4(), + "w_1_4" => base.w_1_4(), + "w_2_4" => base.w_2_4(), + "w_3_5" => base.w_3_5(), + "w_2_5" => base.w_2_5(), + _ => base.w_1_2(), + }; + + modified.with_animation( + ElementId::Integer(n), + Animation::new(Duration::from_secs(2)).repeat(), + move |tab, delta| { + let delta = (delta - 0.15 * n as f32) / 0.7; + let delta = 1.0 - (0.5 - delta).abs() * 2.; + let delta = ease_in_out(delta.clamp(0., 1.)); + let delta = 0.1 + 0.9 * delta; + + tab.bg(bg_color.opacity(delta)) + }, + ) + }; + + v_flex() + .p_3() + .gap_1() + .rounded_b_md() + .bg(cx.theme().colors().editor_background) + .child(bar(0, "w_4_5")) + .child(bar(1, "w_1_4")) + .child(bar(2, "w_2_4")) + .child(bar(3, "w_3_5")) + .child(bar(4, "w_2_5")) + .into_any_element() + } + fn render_diff_editor( &self, entry_ix: usize, diff: &Entity<acp_thread::Diff>, + tool_call: &ToolCall, cx: &Context<Self>, ) -> AnyElement { + let tool_progress = matches!( + &tool_call.status, + ToolCallStatus::InProgress | ToolCallStatus::Pending + ); + v_flex() .h_full() - .border_t_1() - .border_color(self.tool_card_border_color(cx)) .child( if let Some(entry) = self.entry_view_state.read(cx).entry(entry_ix) && let Some(editor) = entry.editor_for_diff(diff) + && diff.read(cx).has_revealed_range(cx) { editor.clone().into_any_element() + } else if tool_progress { + self.render_diff_loading(cx) } else { Empty.into_any() }, @@ -1924,11 +1992,11 @@ impl AcpThreadView { .justify_center() .child(div().opacity(0.3).child(logo)) .child( - h_flex().absolute().right_1().bottom_0().child( - Icon::new(IconName::XCircle) - .color(Color::Error) - .size(IconSize::Small), - ), + h_flex() + .absolute() + .right_1() + .bottom_0() + .child(Icon::new(IconName::XCircleFilled).color(Color::Error)), ) .into_any_element() } @@ -1982,12 +2050,12 @@ impl AcpThreadView { Some( v_flex() - .pt_2() .px_2p5() .gap_1() .when_some(user_rules_text, |parent, user_rules_text| { parent.child( h_flex() + .group("user-rules") .w_full() .child( Icon::new(IconName::Reader) @@ -2008,6 +2076,7 @@ impl AcpThreadView { .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) + .visible_on_hover("user-rules") // TODO: Figure out a way to pass focus handle here so we can display the `OpenRulesLibrary` keybinding .tooltip(Tooltip::text("View User Rules")) .on_click(move |_event, window, cx| { @@ -2024,6 +2093,7 @@ impl AcpThreadView { .when_some(rules_file_text, |parent, rules_file_text| { parent.child( h_flex() + .group("project-rules") .w_full() .child( Icon::new(IconName::File) @@ -2044,7 +2114,8 @@ impl AcpThreadView { .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) .on_click(cx.listener(Self::handle_open_rules)) - .tooltip(Tooltip::text("View Rules")), + .visible_on_hover("project-rules") + .tooltip(Tooltip::text("View Project Rules")), ), ) }) @@ -2119,11 +2190,9 @@ impl AcpThreadView { .items_center() .justify_center() .child(self.render_error_agent_logo()) - .child( - h_flex().mt_4().mb_1().justify_center().child( - Headline::new("Authentication Required").size(HeadlineSize::Medium), - ), - ) + .child(h_flex().mt_4().mb_1().justify_center().child( + Headline::new(self.agent.empty_state_headline()).size(HeadlineSize::Medium), + )) .into_any(), ) .children(description.map(|desc| { @@ -2838,10 +2907,10 @@ impl AcpThreadView { .child( h_flex() .flex_none() + .flex_wrap() .justify_between() .child( h_flex() - .gap_1() .child(self.render_follow_toggle(cx)) .children(self.render_burn_mode_toggle(cx)), ) @@ -2883,7 +2952,7 @@ impl AcpThreadView { h_flex() .flex_shrink_0() .gap_0p5() - .mr_1() + .mr_1p5() .child( Label::new(used) .size(LabelSize::Small) @@ -2904,7 +2973,11 @@ impl AcpThreadView { } }), ) - .child(Label::new("/").size(LabelSize::Small).color(Color::Muted)) + .child( + Label::new("/") + .size(LabelSize::Small) + .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.5))), + ) .child(Label::new(max).size(LabelSize::Small).color(Color::Muted)), ) } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 297bb5f3e842e9aed8f998f1b51bc04d313208a2..c89dc567958f61b22d772e6a8774ae65eae7b652 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -65,8 +65,8 @@ use theme::ThemeSettings; use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ - Banner, Callout, ContextMenu, ContextMenuEntry, Divider, ElevationIndex, KeyBinding, - PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, + Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu, + PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, }; use util::ResultExt as _; use workspace::{ @@ -245,17 +245,16 @@ impl AgentType { match self { Self::Zed | Self::TextThread => "Zed Agent", Self::NativeAgent => "Agent 2", - Self::Gemini => "Google Gemini", + Self::Gemini => "Gemini CLI", Self::ClaudeCode => "Claude Code", } } - fn icon(self) -> IconName { + fn icon(self) -> Option<IconName> { match self { - Self::Zed | Self::TextThread => IconName::AiZed, - Self::NativeAgent => IconName::ZedAssistant, - Self::Gemini => IconName::AiGemini, - Self::ClaudeCode => IconName::AiClaude, + Self::Zed | Self::NativeAgent | Self::TextThread => None, + Self::Gemini => Some(IconName::AiGemini), + Self::ClaudeCode => Some(IconName::AiClaude), } } } @@ -2158,12 +2157,17 @@ impl AgentPanel { }) } - fn render_recent_entries_menu(&self, cx: &mut Context<Self>) -> impl IntoElement { + fn render_recent_entries_menu( + &self, + icon: IconName, + corner: Corner, + cx: &mut Context<Self>, + ) -> impl IntoElement { let focus_handle = self.focus_handle(cx); PopoverMenu::new("agent-nav-menu") .trigger_with_tooltip( - IconButton::new("agent-nav-menu", IconName::MenuAlt).icon_size(IconSize::Small), + IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small), { let focus_handle = focus_handle.clone(); move |window, cx| { @@ -2177,7 +2181,7 @@ impl AgentPanel { } }, ) - .anchor(Corner::TopLeft) + .anchor(corner) .with_handle(self.assistant_navigation_menu_handle.clone()) .menu({ let menu = self.assistant_navigation_menu.clone(); @@ -2304,7 +2308,9 @@ impl AgentPanel { .pl(DynamicSpacing::Base04.rems(cx)) .child(self.render_toolbar_back_button(cx)) .into_any_element(), - _ => self.render_recent_entries_menu(cx).into_any_element(), + _ => self + .render_recent_entries_menu(IconName::MenuAlt, Corner::TopLeft, cx) + .into_any_element(), }) .child(self.render_title_view(window, cx)), ) @@ -2390,7 +2396,7 @@ impl AgentPanel { .item( ContextMenuEntry::new("New Thread") .action(NewThread::default().boxed_clone()) - .icon(IconName::ZedAssistant) + .icon(IconName::Thread) .icon_color(Color::Muted) .handler({ let workspace = workspace.clone(); @@ -2443,7 +2449,7 @@ impl AgentPanel { .header("External Agents") .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| { menu.item( - ContextMenuEntry::new("New Gemini Thread") + ContextMenuEntry::new("New Gemini CLI Thread") .icon(IconName::AiGemini) .icon_color(Color::Muted) .handler({ @@ -2503,16 +2509,18 @@ impl AgentPanel { let selected_agent_label = self.selected_agent.label().into(); let selected_agent = div() .id("selected_agent_icon") - .px(DynamicSpacing::Base02.rems(cx)) - .child(Icon::new(self.selected_agent.icon()).color(Color::Muted)) - .tooltip(move |window, cx| { - Tooltip::with_meta( - selected_agent_label.clone(), - None, - "Selected Agent", - window, - cx, - ) + .when_some(self.selected_agent.icon(), |this, icon| { + this.px(DynamicSpacing::Base02.rems(cx)) + .child(Icon::new(icon).color(Color::Muted)) + .tooltip(move |window, cx| { + Tooltip::with_meta( + selected_agent_label.clone(), + None, + "Selected Agent", + window, + cx, + ) + }) }) .into_any_element(); @@ -2535,31 +2543,23 @@ impl AgentPanel { ActiveView::History | ActiveView::Configuration => { self.render_toolbar_back_button(cx).into_any_element() } - _ => h_flex() - .gap_1() - .child(self.render_recent_entries_menu(cx)) - .child(Divider::vertical()) - .child(selected_agent) - .into_any_element(), + _ => selected_agent.into_any_element(), }) .child(self.render_title_view(window, cx)), ) .child( h_flex() - .h_full() - .gap_2() - .children(self.render_token_count(cx)) - .child( - h_flex() - .h_full() - .gap(DynamicSpacing::Base02.rems(cx)) - .pl(DynamicSpacing::Base04.rems(cx)) - .pr(DynamicSpacing::Base06.rems(cx)) - .border_l_1() - .border_color(cx.theme().colors().border) - .child(new_thread_menu) - .child(self.render_panel_options_menu(window, cx)), - ), + .flex_none() + .gap(DynamicSpacing::Base02.rems(cx)) + .pl(DynamicSpacing::Base04.rems(cx)) + .pr(DynamicSpacing::Base06.rems(cx)) + .child(new_thread_menu) + .child(self.render_recent_entries_menu( + IconName::MenuAltTemp, + Corner::TopRight, + cx, + )) + .child(self.render_panel_options_menu(window, cx)), ) } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 8bd76cbecf59a8c515118bfe473386e2b05efac4..38f02c2206b3a876b68585d8961f0a7e679a8f32 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -155,6 +155,7 @@ pub enum IconName { Maximize, Menu, MenuAlt, + MenuAltTemp, Mic, MicMute, Minimize, @@ -245,6 +246,8 @@ pub enum IconName { Warning, WholeWord, XCircle, + XCircleFilled, + ZedAgent, ZedAssistant, ZedBurnMode, ZedBurnModeOn, diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 7939e97e48117ce7b23834697007e27b5f79fcc6..a161ddd074401db53ff428b68347d56f0c3fd856 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1084,7 +1084,13 @@ impl Element for MarkdownElement { cx, ); el.child( - div().absolute().top_1().right_0p5().w_5().child(codeblock), + h_flex() + .w_5() + .absolute() + .top_1() + .right_1() + .justify_center() + .child(codeblock), ) }); } From 1e1110ee8c8616cf2a35bbf021b127f6f08392ae Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 20 Aug 2025 02:20:58 -0300 Subject: [PATCH 171/744] thread_view: Increase click area of the user rules links (#36549) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 4862bb0aa6d465397d2da08295709ebab78a2d67..ee033bf1f61947b90c320334109834c86cf7228b 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2056,6 +2056,7 @@ impl AcpThreadView { parent.child( h_flex() .group("user-rules") + .id("user-rules") .w_full() .child( Icon::new(IconName::Reader) @@ -2078,25 +2079,26 @@ impl AcpThreadView { .icon_color(Color::Ignored) .visible_on_hover("user-rules") // TODO: Figure out a way to pass focus handle here so we can display the `OpenRulesLibrary` keybinding - .tooltip(Tooltip::text("View User Rules")) - .on_click(move |_event, window, cx| { - window.dispatch_action( - Box::new(OpenRulesLibrary { - prompt_to_select: first_user_rules_id, - }), - cx, - ) + .tooltip(Tooltip::text("View User Rules")), + ) + .on_click(move |_event, window, cx| { + window.dispatch_action( + Box::new(OpenRulesLibrary { + prompt_to_select: first_user_rules_id, }), - ), + cx, + ) + }), ) }) .when_some(rules_file_text, |parent, rules_file_text| { parent.child( h_flex() .group("project-rules") + .id("project-rules") .w_full() .child( - Icon::new(IconName::File) + Icon::new(IconName::Reader) .size(IconSize::XSmall) .color(Color::Disabled), ) @@ -2113,10 +2115,10 @@ impl AcpThreadView { .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) - .on_click(cx.listener(Self::handle_open_rules)) .visible_on_hover("project-rules") .tooltip(Tooltip::text("View Project Rules")), - ), + ) + .on_click(cx.listener(Self::handle_open_rules)), ) }) .into_any(), From 159b5e9fb5a74840fda1f2810af4c423522bcc96 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 20 Aug 2025 02:30:43 -0300 Subject: [PATCH 172/744] agent2: Port `user_modifier_to_send` setting (#36550) Release Notes: - N/A --- assets/keymaps/default-linux.json | 12 ++++++++- assets/keymaps/default-macos.json | 12 ++++++++- crates/agent_ui/src/acp/message_editor.rs | 32 ++++++++++++++++++++--- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 01c0b4e9696f3ee31d599f171acd27f4c00fdf3c..b4efa70572bd51650713509b02e4ac4ad2df33b4 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -327,7 +327,7 @@ } }, { - "context": "AcpThread > Editor", + "context": "AcpThread > Editor && !use_modifier_to_send", "use_key_equivalents": true, "bindings": { "enter": "agent::Chat", @@ -336,6 +336,16 @@ "ctrl-shift-n": "agent::RejectAll" } }, + { + "context": "AcpThread > Editor && use_modifier_to_send", + "use_key_equivalents": true, + "bindings": { + "ctrl-enter": "agent::Chat", + "shift-ctrl-r": "agent::OpenAgentDiff", + "ctrl-shift-y": "agent::KeepAll", + "ctrl-shift-n": "agent::RejectAll" + } + }, { "context": "ThreadHistory", "bindings": { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index e5b7fff9e1ce269f4f1c2f630f6bd41d790ffd21..ad2ab2ba8999e0b2bf40a29af5ca308ca1d604f4 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -379,7 +379,7 @@ } }, { - "context": "AcpThread > Editor", + "context": "AcpThread > Editor && !use_modifier_to_send", "use_key_equivalents": true, "bindings": { "enter": "agent::Chat", @@ -388,6 +388,16 @@ "cmd-shift-n": "agent::RejectAll" } }, + { + "context": "AcpThread > Editor && use_modifier_to_send", + "use_key_equivalents": true, + "bindings": { + "cmd-enter": "agent::Chat", + "shift-ctrl-r": "agent::OpenAgentDiff", + "cmd-shift-y": "agent::KeepAll", + "cmd-shift-n": "agent::RejectAll" + } + }, { "context": "ThreadHistory", "bindings": { diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index cb20740f3c7f5949cf954f0d613af347f9534e1c..01a81c8ccef58617161e78b2032dc48ef270bb45 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -9,7 +9,7 @@ use anyhow::{Context as _, Result, anyhow}; use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ - Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, + Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, SemanticsProvider, ToOffset, actions::Paste, @@ -21,8 +21,8 @@ use futures::{ }; use gpui::{ AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, - HighlightStyle, Image, ImageFormat, Img, Subscription, Task, TextStyle, UnderlineStyle, - WeakEntity, + HighlightStyle, Image, ImageFormat, Img, KeyContext, Subscription, Task, TextStyle, + UnderlineStyle, WeakEntity, }; use language::{Buffer, Language}; use language_model::LanguageModelImage; @@ -122,6 +122,7 @@ impl MessageEditor { if prevent_slash_commands { editor.set_semantics_provider(Some(semantics_provider.clone())); } + editor.register_addon(MessageEditorAddon::new()); editor }); @@ -1648,6 +1649,31 @@ fn parse_slash_command(text: &str) -> Option<(usize, usize)> { None } +pub struct MessageEditorAddon {} + +impl MessageEditorAddon { + pub fn new() -> Self { + Self {} + } +} + +impl Addon for MessageEditorAddon { + fn to_any(&self) -> &dyn std::any::Any { + self + } + + fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { + Some(self) + } + + fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) { + let settings = agent_settings::AgentSettings::get_global(cx); + if settings.use_modifier_to_send { + key_context.add("use_modifier_to_send"); + } + } +} + #[cfg(test)] mod tests { use std::{ops::Range, path::Path, sync::Arc}; From 5d2bb2466e4dc6d98063737a012b638c9deb2284 Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Wed, 20 Aug 2025 00:25:07 -0600 Subject: [PATCH 173/744] ACP history mentions (#36551) - **TEMP** - **Update @-mentions to use new history** Closes #ISSUE Release Notes: - N/A --- Cargo.lock | 1 - crates/acp_thread/Cargo.toml | 1 - crates/acp_thread/src/mention.rs | 8 +- crates/agent/src/thread.rs | 11 +- crates/agent2/Cargo.toml | 4 + crates/agent2/src/agent.rs | 22 + crates/agent2/src/db.rs | 13 +- crates/agent2/src/history_store.rs | 41 +- crates/agent2/src/thread.rs | 77 ++- crates/agent_settings/src/agent_settings.rs | 2 + crates/agent_ui/Cargo.toml | 2 + .../agent_ui/src/acp/completion_provider.rs | 559 ++++++++++-------- crates/agent_ui/src/acp/entry_view_state.rs | 30 +- crates/agent_ui/src/acp/message_editor.rs | 133 ++--- crates/agent_ui/src/acp/thread_view.rs | 44 +- crates/agent_ui/src/agent_panel.rs | 23 +- crates/assistant_context/src/context_store.rs | 2 +- 17 files changed, 581 insertions(+), 392 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5dced73fb98977a210b5d3cfed2eae5cde3d829a..fdc858ef50fd8f4a58c495698fdaadea25c706b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,7 +7,6 @@ name = "acp_thread" version = "0.1.0" dependencies = [ "action_log", - "agent", "agent-client-protocol", "anyhow", "buffer_diff", diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 173f4c42083bb0fb9a43b7174dad69fb0c6acbe2..eab756db51885b8b2e2797bbf0303937f19fefb9 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -18,7 +18,6 @@ test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"] [dependencies] action_log.workspace = true agent-client-protocol.workspace = true -agent.workspace = true anyhow.workspace = true buffer_diff.workspace = true collections.workspace = true diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 4615e9a5515ef29b1dc4b9c79ff49fc018cbfd2c..a1e713cffa051a0eef58a45fceafc8876cab3311 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -1,4 +1,4 @@ -use agent::ThreadId; +use agent_client_protocol as acp; use anyhow::{Context as _, Result, bail}; use file_icons::FileIcons; use prompt_store::{PromptId, UserPromptId}; @@ -12,7 +12,7 @@ use std::{ use ui::{App, IconName, SharedString}; use url::Url; -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] pub enum MentionUri { File { abs_path: PathBuf, @@ -26,7 +26,7 @@ pub enum MentionUri { line_range: Range<u32>, }, Thread { - id: ThreadId, + id: acp::SessionId, name: String, }, TextThread { @@ -89,7 +89,7 @@ impl MentionUri { if let Some(thread_id) = path.strip_prefix("/agent/thread/") { let name = single_query_param(&url, "name")?.context("Missing thread name")?; Ok(Self::Thread { - id: thread_id.into(), + id: acp::SessionId(thread_id.into()), name, }) } else if let Some(path) = path.strip_prefix("/agent/text-thread/") { diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 80ed277f10cb8db19df5488b0cbe903feb7c2198..fc91e1bb62b1154ac2b5bb5651f3b8352625635f 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -9,7 +9,10 @@ use crate::{ tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState}, }; use action_log::ActionLog; -use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT}; +use agent_settings::{ + AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT, + SUMMARIZE_THREAD_PROMPT, +}; use anyhow::{Result, anyhow}; use assistant_tool::{AnyToolCard, Tool, ToolWorkingSet}; use chrono::{DateTime, Utc}; @@ -107,7 +110,7 @@ impl std::fmt::Display for PromptId { } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] -pub struct MessageId(pub(crate) usize); +pub struct MessageId(pub usize); impl MessageId { fn post_inc(&mut self) -> Self { @@ -2425,12 +2428,10 @@ impl Thread { return; } - let added_user_message = include_str!("./prompts/summarize_thread_detailed_prompt.txt"); - let request = self.to_summarize_request( &model, CompletionIntent::ThreadContextSummarization, - added_user_message.into(), + SUMMARIZE_THREAD_DETAILED_PROMPT.into(), cx, ); diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 849ea041e93c206d12898b2264e026a676c0cfce..2a39440af897b9d15f00592efe7c62b61846b9c3 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -8,6 +8,9 @@ license = "GPL-3.0-or-later" [lib] path = "src/agent2.rs" +[features] +test-support = ["db/test-support"] + [lints] workspace = true @@ -72,6 +75,7 @@ ctor.workspace = true client = { workspace = true, "features" = ["test-support"] } clock = { workspace = true, "features" = ["test-support"] } context_server = { workspace = true, "features" = ["test-support"] } +db = { workspace = true, "features" = ["test-support"] } editor = { workspace = true, "features" = ["test-support"] } env_logger.workspace = true fs = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 212460d6909ffa7620f712f0230a66b42e377fb2..3c605de80383c13a2eb9f416806910cfcf31ecc5 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -536,6 +536,28 @@ impl NativeAgent { }) } + pub fn thread_summary( + &mut self, + id: acp::SessionId, + cx: &mut Context<Self>, + ) -> Task<Result<SharedString>> { + let thread = self.open_thread(id.clone(), cx); + cx.spawn(async move |this, cx| { + let acp_thread = thread.await?; + let result = this + .update(cx, |this, cx| { + this.sessions + .get(&id) + .unwrap() + .thread + .update(cx, |thread, cx| thread.summary(cx)) + })? + .await?; + drop(acp_thread); + Ok(result) + }) + } + fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) { let database_future = ThreadsDatabase::connect(cx); let (id, db_thread) = diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index 610a2575c4dadeee3e19e8bee2020fd5ead7d5f9..c6a6c382019867a1c4580b31bf955ee701768c0c 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent2/src/db.rs @@ -1,6 +1,6 @@ use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent}; use acp_thread::UserMessageId; -use agent::thread_store; +use agent::{thread::DetailedSummaryState, thread_store}; use agent_client_protocol as acp; use agent_settings::{AgentProfileId, CompletionMode}; use anyhow::{Result, anyhow}; @@ -20,7 +20,7 @@ use std::sync::Arc; use ui::{App, SharedString}; pub type DbMessage = crate::Message; -pub type DbSummary = agent::thread::DetailedSummaryState; +pub type DbSummary = DetailedSummaryState; pub type DbLanguageModel = thread_store::SerializedLanguageModel; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -37,7 +37,7 @@ pub struct DbThread { pub messages: Vec<DbMessage>, pub updated_at: DateTime<Utc>, #[serde(default)] - pub summary: DbSummary, + pub detailed_summary: Option<SharedString>, #[serde(default)] pub initial_project_snapshot: Option<Arc<agent::thread::ProjectSnapshot>>, #[serde(default)] @@ -185,7 +185,12 @@ impl DbThread { title: thread.summary, messages, updated_at: thread.updated_at, - summary: thread.detailed_summary_state, + detailed_summary: match thread.detailed_summary_state { + DetailedSummaryState::NotGenerated | DetailedSummaryState::Generating { .. } => { + None + } + DetailedSummaryState::Generated { text, .. } => Some(text), + }, initial_project_snapshot: thread.initial_project_snapshot, cumulative_token_usage: thread.cumulative_token_usage, request_token_usage, diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 4ce304ae5ff8acd96d53d4f77e2c4c5f2e970aee..7eb7da94ba1a0b6690b7d16422a02b2a22ba6b92 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -1,7 +1,8 @@ use crate::{DbThreadMetadata, ThreadsDatabase}; +use acp_thread::MentionUri; use agent_client_protocol as acp; use anyhow::{Context as _, Result, anyhow}; -use assistant_context::SavedContextMetadata; +use assistant_context::{AssistantContext, SavedContextMetadata}; use chrono::{DateTime, Utc}; use db::kvp::KEY_VALUE_STORE; use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*}; @@ -38,6 +39,19 @@ impl HistoryEntry { } } + pub fn mention_uri(&self) -> MentionUri { + match self { + HistoryEntry::AcpThread(thread) => MentionUri::Thread { + id: thread.id.clone(), + name: thread.title.to_string(), + }, + HistoryEntry::TextThread(context) => MentionUri::TextThread { + path: context.path.as_ref().to_owned(), + name: context.title.to_string(), + }, + } + } + pub fn title(&self) -> &SharedString { match self { HistoryEntry::AcpThread(thread) if thread.title.is_empty() => DEFAULT_TITLE, @@ -48,7 +62,7 @@ impl HistoryEntry { } /// Generic identifier for a history entry. -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, Hash)] pub enum HistoryEntryId { AcpThread(acp::SessionId), TextThread(Arc<Path>), @@ -120,6 +134,16 @@ impl HistoryStore { }) } + pub fn load_text_thread( + &self, + path: Arc<Path>, + cx: &mut Context<Self>, + ) -> Task<Result<Entity<AssistantContext>>> { + self.context_store.update(cx, |context_store, cx| { + context_store.open_local_context(path, cx) + }) + } + pub fn reload(&self, cx: &mut Context<Self>) { let database_future = ThreadsDatabase::connect(cx); cx.spawn(async move |this, cx| { @@ -149,7 +173,7 @@ impl HistoryStore { .detach_and_log_err(cx); } - pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> { + pub fn entries(&self, cx: &App) -> Vec<HistoryEntry> { let mut history_entries = Vec::new(); #[cfg(debug_assertions)] @@ -180,10 +204,6 @@ impl HistoryStore { .is_none() } - pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> { - self.entries(cx).into_iter().take(limit).collect() - } - pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { @@ -246,6 +266,10 @@ impl HistoryStore { cx.background_executor() .timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE) .await; + + if cfg!(any(feature = "test-support", test)) { + return; + } KEY_VALUE_STORE .write_kvp(RECENTLY_OPENED_THREADS_KEY.to_owned(), content) .await @@ -255,6 +279,9 @@ impl HistoryStore { fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> { cx.background_spawn(async move { + if cfg!(any(feature = "test-support", test)) { + anyhow::bail!("history store does not persist in tests"); + } let json = KEY_VALUE_STORE .read_kvp(RECENTLY_OPENED_THREADS_KEY)? .unwrap_or("[]".to_string()); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 4bc45f1544acc7e1d5e947a866b4754ee24aadf0..c1778bf38baf3ef388684a4f0c83869d455d1b72 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -6,9 +6,12 @@ use crate::{ }; use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; -use agent::thread::{DetailedSummaryState, GitState, ProjectSnapshot, WorktreeSnapshot}; +use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot}; use agent_client_protocol as acp; -use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT}; +use agent_settings::{ + AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT, + SUMMARIZE_THREAD_PROMPT, +}; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; use chrono::{DateTime, Utc}; @@ -499,8 +502,7 @@ pub struct Thread { prompt_id: PromptId, updated_at: DateTime<Utc>, title: Option<SharedString>, - #[allow(unused)] - summary: DetailedSummaryState, + summary: Option<SharedString>, messages: Vec<Message>, completion_mode: CompletionMode, /// Holds the task that handles agent interaction until the end of the turn. @@ -541,7 +543,7 @@ impl Thread { prompt_id: PromptId::new(), updated_at: Utc::now(), title: None, - summary: DetailedSummaryState::default(), + summary: None, messages: Vec::new(), completion_mode: AgentSettings::get_global(cx).preferred_completion_mode, running_turn: None, @@ -691,7 +693,7 @@ impl Thread { } else { Some(db_thread.title.clone()) }, - summary: db_thread.summary, + summary: db_thread.detailed_summary, messages: db_thread.messages, completion_mode: db_thread.completion_mode.unwrap_or_default(), running_turn: None, @@ -719,7 +721,7 @@ impl Thread { title: self.title.clone().unwrap_or_default(), messages: self.messages.clone(), updated_at: self.updated_at, - summary: self.summary.clone(), + detailed_summary: self.summary.clone(), initial_project_snapshot: None, cumulative_token_usage: self.cumulative_token_usage, request_token_usage: self.request_token_usage.clone(), @@ -976,7 +978,7 @@ impl Thread { Message::Agent(_) | Message::Resume => {} } } - + self.summary = None; cx.notify(); Ok(()) } @@ -1047,6 +1049,7 @@ impl Thread { let event_stream = ThreadEventStream(events_tx); let message_ix = self.messages.len().saturating_sub(1); self.tool_use_limit_reached = false; + self.summary = None; self.running_turn = Some(RunningTurn { event_stream: event_stream.clone(), _task: cx.spawn(async move |this, cx| { @@ -1507,6 +1510,63 @@ impl Thread { self.title.clone().unwrap_or("New Thread".into()) } + pub fn summary(&mut self, cx: &mut Context<Self>) -> Task<Result<SharedString>> { + if let Some(summary) = self.summary.as_ref() { + return Task::ready(Ok(summary.clone())); + } + let Some(model) = self.summarization_model.clone() else { + return Task::ready(Err(anyhow!("No summarization model available"))); + }; + let mut request = LanguageModelRequest { + intent: Some(CompletionIntent::ThreadSummarization), + temperature: AgentSettings::temperature_for_model(&model, cx), + ..Default::default() + }; + + for message in &self.messages { + request.messages.extend(message.to_request()); + } + + request.messages.push(LanguageModelRequestMessage { + role: Role::User, + content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()], + cache: false, + }); + cx.spawn(async move |this, cx| { + let mut summary = String::new(); + let mut messages = model.stream_completion(request, cx).await?; + while let Some(event) = messages.next().await { + let event = event?; + let text = match event { + LanguageModelCompletionEvent::Text(text) => text, + LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::UsageUpdated { .. }, + ) => { + // this.update(cx, |thread, cx| { + // thread.update_model_request_usage(amount as u32, limit, cx); + // })?; + // TODO: handle usage update + continue; + } + _ => continue, + }; + + let mut lines = text.lines(); + summary.extend(lines.next()); + } + + log::info!("Setting summary: {}", summary); + let summary = SharedString::from(summary); + + this.update(cx, |this, cx| { + this.summary = Some(summary.clone()); + cx.notify() + })?; + + Ok(summary) + }) + } + fn update_title( &mut self, event_stream: &ThreadEventStream, @@ -1617,6 +1677,7 @@ impl Thread { self.messages.push(Message::Agent(message)); self.updated_at = Utc::now(); + self.summary = None; cx.notify() } diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index afc834cdd8f9c2f5d053002049e9ae439fe166c0..1fe41d002ca4767206b1e77028ffe19bf1d4f690 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -15,6 +15,8 @@ pub use crate::agent_profile::*; pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("../../agent/src/prompts/summarize_thread_prompt.txt"); +pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str = + include_str!("../../agent/src/prompts/summarize_thread_detailed_prompt.txt"); pub fn init(cx: &mut App) { AgentSettings::register(cx); diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index fbf8590e681c1d355e7904f171abec8cafff97da..43e3b251245af5d014c952afe2fec1f30abafe53 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -104,9 +104,11 @@ zed_actions.workspace = true [dev-dependencies] acp_thread = { workspace = true, features = ["test-support"] } agent = { workspace = true, features = ["test-support"] } +agent2 = { workspace = true, features = ["test-support"] } assistant_context = { workspace = true, features = ["test-support"] } assistant_tools.workspace = true buffer_diff = { workspace = true, features = ["test-support"] } +db = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, "features" = ["test-support"] } indoc.workspace = true diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 1a5e9c7d81c1a8db46ecc23c97c349e5ba63dd7a..999e469d30dab1e4e429ba71bfe06867237e8670 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use std::sync::atomic::AtomicBool; use acp_thread::MentionUri; +use agent2::{HistoryEntry, HistoryStore}; use anyhow::Result; use editor::{CompletionProvider, Editor, ExcerptId}; use fuzzy::{StringMatch, StringMatchCandidate}; @@ -18,25 +19,21 @@ use text::{Anchor, ToPoint as _}; use ui::prelude::*; use workspace::Workspace; -use agent::thread_store::{TextThreadStore, ThreadStore}; - +use crate::AgentPanel; use crate::acp::message_editor::MessageEditor; use crate::context_picker::file_context_picker::{FileMatch, search_files}; use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules}; use crate::context_picker::symbol_context_picker::SymbolMatch; use crate::context_picker::symbol_context_picker::search_symbols; -use crate::context_picker::thread_context_picker::{ - ThreadContextEntry, ThreadMatch, search_threads, -}; use crate::context_picker::{ - ContextPickerAction, ContextPickerEntry, ContextPickerMode, RecentEntry, - available_context_picker_entries, recent_context_picker_entries, selection_ranges, + ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges, }; pub(crate) enum Match { File(FileMatch), Symbol(SymbolMatch), - Thread(ThreadMatch), + Thread(HistoryEntry), + RecentThread(HistoryEntry), Fetch(SharedString), Rules(RulesContextEntry), Entry(EntryMatch), @@ -53,6 +50,7 @@ impl Match { Match::File(file) => file.mat.score, Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.), Match::Thread(_) => 1., + Match::RecentThread(_) => 1., Match::Symbol(_) => 1., Match::Rules(_) => 1., Match::Fetch(_) => 1., @@ -60,209 +58,25 @@ impl Match { } } -fn search( - mode: Option<ContextPickerMode>, - query: String, - cancellation_flag: Arc<AtomicBool>, - recent_entries: Vec<RecentEntry>, - prompt_store: Option<Entity<PromptStore>>, - thread_store: WeakEntity<ThreadStore>, - text_thread_context_store: WeakEntity<assistant_context::ContextStore>, - workspace: Entity<Workspace>, - cx: &mut App, -) -> Task<Vec<Match>> { - match mode { - Some(ContextPickerMode::File) => { - let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); - cx.background_spawn(async move { - search_files_task - .await - .into_iter() - .map(Match::File) - .collect() - }) - } - - Some(ContextPickerMode::Symbol) => { - let search_symbols_task = - search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx); - cx.background_spawn(async move { - search_symbols_task - .await - .into_iter() - .map(Match::Symbol) - .collect() - }) - } - - Some(ContextPickerMode::Thread) => { - if let Some((thread_store, context_store)) = thread_store - .upgrade() - .zip(text_thread_context_store.upgrade()) - { - let search_threads_task = search_threads( - query.clone(), - cancellation_flag.clone(), - thread_store, - context_store, - cx, - ); - cx.background_spawn(async move { - search_threads_task - .await - .into_iter() - .map(Match::Thread) - .collect() - }) - } else { - Task::ready(Vec::new()) - } - } - - Some(ContextPickerMode::Fetch) => { - if !query.is_empty() { - Task::ready(vec![Match::Fetch(query.into())]) - } else { - Task::ready(Vec::new()) - } - } - - Some(ContextPickerMode::Rules) => { - if let Some(prompt_store) = prompt_store.as_ref() { - let search_rules_task = - search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx); - cx.background_spawn(async move { - search_rules_task - .await - .into_iter() - .map(Match::Rules) - .collect::<Vec<_>>() - }) - } else { - Task::ready(Vec::new()) - } - } - - None => { - if query.is_empty() { - let mut matches = recent_entries - .into_iter() - .map(|entry| match entry { - RecentEntry::File { - project_path, - path_prefix, - } => Match::File(FileMatch { - mat: fuzzy::PathMatch { - score: 1., - positions: Vec::new(), - worktree_id: project_path.worktree_id.to_usize(), - path: project_path.path, - path_prefix, - is_dir: false, - distance_to_relative_ancestor: 0, - }, - is_recent: true, - }), - RecentEntry::Thread(thread_context_entry) => Match::Thread(ThreadMatch { - thread: thread_context_entry, - is_recent: true, - }), - }) - .collect::<Vec<_>>(); - - matches.extend( - available_context_picker_entries( - &prompt_store, - &Some(thread_store.clone()), - &workspace, - cx, - ) - .into_iter() - .map(|mode| { - Match::Entry(EntryMatch { - entry: mode, - mat: None, - }) - }), - ); - - Task::ready(matches) - } else { - let executor = cx.background_executor().clone(); - - let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); - - let entries = available_context_picker_entries( - &prompt_store, - &Some(thread_store.clone()), - &workspace, - cx, - ); - let entry_candidates = entries - .iter() - .enumerate() - .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword())) - .collect::<Vec<_>>(); - - cx.background_spawn(async move { - let mut matches = search_files_task - .await - .into_iter() - .map(Match::File) - .collect::<Vec<_>>(); - - let entry_matches = fuzzy::match_strings( - &entry_candidates, - &query, - false, - true, - 100, - &Arc::new(AtomicBool::default()), - executor, - ) - .await; - - matches.extend(entry_matches.into_iter().map(|mat| { - Match::Entry(EntryMatch { - entry: entries[mat.candidate_id], - mat: Some(mat), - }) - })); - - matches.sort_by(|a, b| { - b.score() - .partial_cmp(&a.score()) - .unwrap_or(std::cmp::Ordering::Equal) - }); - - matches - }) - } - } - } -} - pub struct ContextPickerCompletionProvider { - workspace: WeakEntity<Workspace>, - thread_store: WeakEntity<ThreadStore>, - text_thread_store: WeakEntity<TextThreadStore>, message_editor: WeakEntity<MessageEditor>, + workspace: WeakEntity<Workspace>, + history_store: Entity<HistoryStore>, + prompt_store: Option<Entity<PromptStore>>, } impl ContextPickerCompletionProvider { pub fn new( - workspace: WeakEntity<Workspace>, - thread_store: WeakEntity<ThreadStore>, - text_thread_store: WeakEntity<TextThreadStore>, message_editor: WeakEntity<MessageEditor>, + workspace: WeakEntity<Workspace>, + history_store: Entity<HistoryStore>, + prompt_store: Option<Entity<PromptStore>>, ) -> Self { Self { - workspace, - thread_store, - text_thread_store, message_editor, + workspace, + history_store, + prompt_store, } } @@ -349,22 +163,13 @@ impl ContextPickerCompletionProvider { } fn completion_for_thread( - thread_entry: ThreadContextEntry, + thread_entry: HistoryEntry, source_range: Range<Anchor>, recent: bool, editor: WeakEntity<MessageEditor>, cx: &mut App, ) -> Completion { - let uri = match &thread_entry { - ThreadContextEntry::Thread { id, title } => MentionUri::Thread { - id: id.clone(), - name: title.to_string(), - }, - ThreadContextEntry::Context { path, title } => MentionUri::TextThread { - path: path.to_path_buf(), - name: title.to_string(), - }, - }; + let uri = thread_entry.mention_uri(); let icon_for_completion = if recent { IconName::HistoryRerun.path().into() @@ -547,6 +352,251 @@ impl ContextPickerCompletionProvider { )), }) } + + fn search( + &self, + mode: Option<ContextPickerMode>, + query: String, + cancellation_flag: Arc<AtomicBool>, + cx: &mut App, + ) -> Task<Vec<Match>> { + let Some(workspace) = self.workspace.upgrade() else { + return Task::ready(Vec::default()); + }; + match mode { + Some(ContextPickerMode::File) => { + let search_files_task = + search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + cx.background_spawn(async move { + search_files_task + .await + .into_iter() + .map(Match::File) + .collect() + }) + } + + Some(ContextPickerMode::Symbol) => { + let search_symbols_task = + search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx); + cx.background_spawn(async move { + search_symbols_task + .await + .into_iter() + .map(Match::Symbol) + .collect() + }) + } + + Some(ContextPickerMode::Thread) => { + let search_threads_task = search_threads( + query.clone(), + cancellation_flag.clone(), + &self.history_store, + cx, + ); + cx.background_spawn(async move { + search_threads_task + .await + .into_iter() + .map(Match::Thread) + .collect() + }) + } + + Some(ContextPickerMode::Fetch) => { + if !query.is_empty() { + Task::ready(vec![Match::Fetch(query.into())]) + } else { + Task::ready(Vec::new()) + } + } + + Some(ContextPickerMode::Rules) => { + if let Some(prompt_store) = self.prompt_store.as_ref() { + let search_rules_task = + search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx); + cx.background_spawn(async move { + search_rules_task + .await + .into_iter() + .map(Match::Rules) + .collect::<Vec<_>>() + }) + } else { + Task::ready(Vec::new()) + } + } + + None if query.is_empty() => { + let mut matches = self.recent_context_picker_entries(&workspace, cx); + + matches.extend( + self.available_context_picker_entries(&workspace, cx) + .into_iter() + .map(|mode| { + Match::Entry(EntryMatch { + entry: mode, + mat: None, + }) + }), + ); + + Task::ready(matches) + } + None => { + let executor = cx.background_executor().clone(); + + let search_files_task = + search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + + let entries = self.available_context_picker_entries(&workspace, cx); + let entry_candidates = entries + .iter() + .enumerate() + .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword())) + .collect::<Vec<_>>(); + + cx.background_spawn(async move { + let mut matches = search_files_task + .await + .into_iter() + .map(Match::File) + .collect::<Vec<_>>(); + + let entry_matches = fuzzy::match_strings( + &entry_candidates, + &query, + false, + true, + 100, + &Arc::new(AtomicBool::default()), + executor, + ) + .await; + + matches.extend(entry_matches.into_iter().map(|mat| { + Match::Entry(EntryMatch { + entry: entries[mat.candidate_id], + mat: Some(mat), + }) + })); + + matches.sort_by(|a, b| { + b.score() + .partial_cmp(&a.score()) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + matches + }) + } + } + } + + fn recent_context_picker_entries( + &self, + workspace: &Entity<Workspace>, + cx: &mut App, + ) -> Vec<Match> { + let mut recent = Vec::with_capacity(6); + + let mut mentions = self + .message_editor + .read_with(cx, |message_editor, _cx| message_editor.mentions()) + .unwrap_or_default(); + let workspace = workspace.read(cx); + let project = workspace.project().read(cx); + + if let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) + && let Some(thread) = agent_panel.read(cx).active_agent_thread(cx) + { + let thread = thread.read(cx); + mentions.insert(MentionUri::Thread { + id: thread.session_id().clone(), + name: thread.title().into(), + }); + } + + recent.extend( + workspace + .recent_navigation_history_iter(cx) + .filter(|(_, abs_path)| { + abs_path.as_ref().is_none_or(|path| { + !mentions.contains(&MentionUri::File { + abs_path: path.clone(), + }) + }) + }) + .take(4) + .filter_map(|(project_path, _)| { + project + .worktree_for_id(project_path.worktree_id, cx) + .map(|worktree| { + let path_prefix = worktree.read(cx).root_name().into(); + Match::File(FileMatch { + mat: fuzzy::PathMatch { + score: 1., + positions: Vec::new(), + worktree_id: project_path.worktree_id.to_usize(), + path: project_path.path, + path_prefix, + is_dir: false, + distance_to_relative_ancestor: 0, + }, + is_recent: true, + }) + }) + }), + ); + + const RECENT_COUNT: usize = 2; + let threads = self + .history_store + .read(cx) + .recently_opened_entries(cx) + .into_iter() + .filter(|thread| !mentions.contains(&thread.mention_uri())) + .take(RECENT_COUNT) + .collect::<Vec<_>>(); + + recent.extend(threads.into_iter().map(Match::RecentThread)); + + recent + } + + fn available_context_picker_entries( + &self, + workspace: &Entity<Workspace>, + cx: &mut App, + ) -> Vec<ContextPickerEntry> { + let mut entries = vec![ + ContextPickerEntry::Mode(ContextPickerMode::File), + ContextPickerEntry::Mode(ContextPickerMode::Symbol), + ContextPickerEntry::Mode(ContextPickerMode::Thread), + ]; + + let has_selection = workspace + .read(cx) + .active_item(cx) + .and_then(|item| item.downcast::<Editor>()) + .is_some_and(|editor| { + editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx)) + }); + if has_selection { + entries.push(ContextPickerEntry::Action( + ContextPickerAction::AddSelections, + )); + } + + if self.prompt_store.is_some() { + entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules)); + } + + entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch)); + + entries + } } fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel { @@ -596,45 +646,12 @@ impl CompletionProvider for ContextPickerCompletionProvider { let source_range = snapshot.anchor_before(state.source_range.start) ..snapshot.anchor_after(state.source_range.end); - let thread_store = self.thread_store.clone(); - let text_thread_store = self.text_thread_store.clone(); let editor = self.message_editor.clone(); - let Ok((exclude_paths, exclude_threads)) = - self.message_editor.update(cx, |message_editor, _cx| { - message_editor.mentioned_path_and_threads() - }) - else { - return Task::ready(Ok(Vec::new())); - }; let MentionCompletion { mode, argument, .. } = state; let query = argument.unwrap_or_else(|| "".to_string()); - let recent_entries = recent_context_picker_entries( - Some(thread_store.clone()), - Some(text_thread_store.clone()), - workspace.clone(), - &exclude_paths, - &exclude_threads, - cx, - ); - - let prompt_store = thread_store - .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone()) - .ok() - .flatten(); - - let search_task = search( - mode, - query, - Arc::<AtomicBool>::default(), - recent_entries, - prompt_store, - thread_store.clone(), - text_thread_store.clone(), - workspace.clone(), - cx, - ); + let search_task = self.search(mode, query, Arc::<AtomicBool>::default(), cx); cx.spawn(async move |_, cx| { let matches = search_task.await; @@ -669,12 +686,18 @@ impl CompletionProvider for ContextPickerCompletionProvider { cx, ), - Match::Thread(ThreadMatch { - thread, is_recent, .. - }) => Some(Self::completion_for_thread( + Match::Thread(thread) => Some(Self::completion_for_thread( + thread, + source_range.clone(), + false, + editor.clone(), + cx, + )), + + Match::RecentThread(thread) => Some(Self::completion_for_thread( thread, source_range.clone(), - is_recent, + true, editor.clone(), cx, )), @@ -748,6 +771,42 @@ impl CompletionProvider for ContextPickerCompletionProvider { } } +pub(crate) fn search_threads( + query: String, + cancellation_flag: Arc<AtomicBool>, + history_store: &Entity<HistoryStore>, + cx: &mut App, +) -> Task<Vec<HistoryEntry>> { + let threads = history_store.read(cx).entries(cx); + if query.is_empty() { + return Task::ready(threads); + } + + let executor = cx.background_executor().clone(); + cx.background_spawn(async move { + let candidates = threads + .iter() + .enumerate() + .map(|(id, thread)| StringMatchCandidate::new(id, thread.title())) + .collect::<Vec<_>>(); + let matches = fuzzy::match_strings( + &candidates, + &query, + false, + true, + 100, + &cancellation_flag, + executor, + ) + .await; + + matches + .into_iter() + .map(|mat| threads[mat.candidate_id].clone()) + .collect() + }) +} + fn confirm_completion_callback( crease_text: SharedString, start: Anchor, diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 98af9bf8388f66b25a6cf959f6030ab745a2bc4c..67acbb8b5b8354ecc143216f2ee5bc4303afec1c 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -1,7 +1,7 @@ use std::ops::Range; use acp_thread::{AcpThread, AgentThreadEntry}; -use agent::{TextThreadStore, ThreadStore}; +use agent2::HistoryStore; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; use gpui::{ @@ -10,6 +10,7 @@ use gpui::{ }; use language::language_settings::SoftWrap; use project::Project; +use prompt_store::PromptStore; use settings::Settings as _; use terminal_view::TerminalView; use theme::ThemeSettings; @@ -21,8 +22,8 @@ use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; pub struct EntryViewState { workspace: WeakEntity<Workspace>, project: Entity<Project>, - thread_store: Entity<ThreadStore>, - text_thread_store: Entity<TextThreadStore>, + history_store: Entity<HistoryStore>, + prompt_store: Option<Entity<PromptStore>>, entries: Vec<Entry>, prevent_slash_commands: bool, } @@ -31,15 +32,15 @@ impl EntryViewState { pub fn new( workspace: WeakEntity<Workspace>, project: Entity<Project>, - thread_store: Entity<ThreadStore>, - text_thread_store: Entity<TextThreadStore>, + history_store: Entity<HistoryStore>, + prompt_store: Option<Entity<PromptStore>>, prevent_slash_commands: bool, ) -> Self { Self { workspace, project, - thread_store, - text_thread_store, + history_store, + prompt_store, entries: Vec::new(), prevent_slash_commands, } @@ -77,8 +78,8 @@ impl EntryViewState { let mut editor = MessageEditor::new( self.workspace.clone(), self.project.clone(), - self.thread_store.clone(), - self.text_thread_store.clone(), + self.history_store.clone(), + self.prompt_store.clone(), "Edit message - @ to include context", self.prevent_slash_commands, editor::EditorMode::AutoHeight { @@ -313,9 +314,10 @@ mod tests { use std::{path::Path, rc::Rc}; use acp_thread::{AgentConnection, StubAgentConnection}; - use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol as acp; use agent_settings::AgentSettings; + use agent2::HistoryStore; + use assistant_context::ContextStore; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use editor::{EditorSettings, RowInfo}; use fs::FakeFs; @@ -378,15 +380,15 @@ mod tests { connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx) }); - let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); let view_state = cx.new(|_cx| { EntryViewState::new( workspace.downgrade(), project.clone(), - thread_store, - text_thread_store, + history_store, + None, false, ) }); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 01a81c8ccef58617161e78b2032dc48ef270bb45..c87c824015b729f5da26ad2e0fa0877f17db33bd 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -3,8 +3,9 @@ use crate::{ context_picker::fetch_context_picker::fetch_url_content, }; use acp_thread::{MentionUri, selection_name}; -use agent::{TextThreadStore, ThreadId, ThreadStore}; use agent_client_protocol as acp; +use agent_servers::AgentServer; +use agent2::HistoryStore; use anyhow::{Context as _, Result, anyhow}; use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; @@ -27,6 +28,7 @@ use gpui::{ use language::{Buffer, Language}; use language_model::LanguageModelImage; use project::{Project, ProjectPath, Worktree}; +use prompt_store::PromptStore; use rope::Point; use settings::Settings; use std::{ @@ -59,8 +61,8 @@ pub struct MessageEditor { editor: Entity<Editor>, project: Entity<Project>, workspace: WeakEntity<Workspace>, - thread_store: Entity<ThreadStore>, - text_thread_store: Entity<TextThreadStore>, + history_store: Entity<HistoryStore>, + prompt_store: Option<Entity<PromptStore>>, prevent_slash_commands: bool, _subscriptions: Vec<Subscription>, _parse_slash_command_task: Task<()>, @@ -79,8 +81,8 @@ impl MessageEditor { pub fn new( workspace: WeakEntity<Workspace>, project: Entity<Project>, - thread_store: Entity<ThreadStore>, - text_thread_store: Entity<TextThreadStore>, + history_store: Entity<HistoryStore>, + prompt_store: Option<Entity<PromptStore>>, placeholder: impl Into<Arc<str>>, prevent_slash_commands: bool, mode: EditorMode, @@ -95,10 +97,10 @@ impl MessageEditor { None, ); let completion_provider = ContextPickerCompletionProvider::new( - workspace.clone(), - thread_store.downgrade(), - text_thread_store.downgrade(), cx.weak_entity(), + workspace.clone(), + history_store.clone(), + prompt_store.clone(), ); let semantics_provider = Rc::new(SlashCommandSemanticsProvider { range: Cell::new(None), @@ -152,9 +154,9 @@ impl MessageEditor { editor, project, mention_set, - thread_store, - text_thread_store, workspace, + history_store, + prompt_store, prevent_slash_commands, _subscriptions: subscriptions, _parse_slash_command_task: Task::ready(()), @@ -175,23 +177,12 @@ impl MessageEditor { self.editor.read(cx).is_empty(cx) } - pub fn mentioned_path_and_threads(&self) -> (HashSet<PathBuf>, HashSet<ThreadId>) { - let mut excluded_paths = HashSet::default(); - let mut excluded_threads = HashSet::default(); - - for uri in self.mention_set.uri_by_crease_id.values() { - match uri { - MentionUri::File { abs_path, .. } => { - excluded_paths.insert(abs_path.clone()); - } - MentionUri::Thread { id, .. } => { - excluded_threads.insert(id.clone()); - } - _ => {} - } - } - - (excluded_paths, excluded_threads) + pub fn mentions(&self) -> HashSet<MentionUri> { + self.mention_set + .uri_by_crease_id + .values() + .cloned() + .collect() } pub fn confirm_completion( @@ -529,7 +520,7 @@ impl MessageEditor { &mut self, crease_id: CreaseId, anchor: Anchor, - id: ThreadId, + id: acp::SessionId, name: String, window: &mut Window, cx: &mut Context<Self>, @@ -538,17 +529,25 @@ impl MessageEditor { id: id.clone(), name, }; - let open_task = self.thread_store.update(cx, |thread_store, cx| { - thread_store.open_thread(&id, window, cx) + let server = Rc::new(agent2::NativeAgentServer::new( + self.project.read(cx).fs().clone(), + self.history_store.clone(), + )); + let connection = server.connect(Path::new(""), &self.project, cx); + let load_summary = cx.spawn({ + let id = id.clone(); + async move |_, cx| { + let agent = connection.await?; + let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap(); + let summary = agent + .0 + .update(cx, |agent, cx| agent.thread_summary(id, cx))? + .await?; + anyhow::Ok(summary) + } }); let task = cx - .spawn(async move |_, cx| { - let thread = open_task.await.map_err(|e| e.to_string())?; - let content = thread - .read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text()) - .map_err(|e| e.to_string())?; - Ok(content) - }) + .spawn(async move |_, _| load_summary.await.map_err(|e| format!("{e}"))) .shared(); self.mention_set.insert_thread(id.clone(), task.clone()); @@ -590,8 +589,8 @@ impl MessageEditor { path: path.clone(), name, }; - let context = self.text_thread_store.update(cx, |text_thread_store, cx| { - text_thread_store.open_local_context(path.as_path().into(), cx) + let context = self.history_store.update(cx, |text_thread_store, cx| { + text_thread_store.load_text_thread(path.as_path().into(), cx) }); let task = cx .spawn(async move |_, cx| { @@ -637,7 +636,7 @@ impl MessageEditor { ) -> Task<Result<Vec<acp::ContentBlock>>> { let contents = self.mention_set - .contents(self.project.clone(), self.thread_store.clone(), window, cx); + .contents(&self.project, self.prompt_store.as_ref(), window, cx); let editor = self.editor.clone(); let prevent_slash_commands = self.prevent_slash_commands; @@ -1316,7 +1315,7 @@ pub struct MentionSet { uri_by_crease_id: HashMap<CreaseId, MentionUri>, fetch_results: HashMap<Url, Shared<Task<Result<String, String>>>>, images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>, - thread_summaries: HashMap<ThreadId, Shared<Task<Result<SharedString, String>>>>, + thread_summaries: HashMap<acp::SessionId, Shared<Task<Result<SharedString, String>>>>, text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>, directories: HashMap<PathBuf, Shared<Task<Result<String, String>>>>, } @@ -1338,7 +1337,11 @@ impl MentionSet { self.images.insert(crease_id, task); } - fn insert_thread(&mut self, id: ThreadId, task: Shared<Task<Result<SharedString, String>>>) { + fn insert_thread( + &mut self, + id: acp::SessionId, + task: Shared<Task<Result<SharedString, String>>>, + ) { self.thread_summaries.insert(id, task); } @@ -1358,8 +1361,8 @@ impl MentionSet { pub fn contents( &self, - project: Entity<Project>, - thread_store: Entity<ThreadStore>, + project: &Entity<Project>, + prompt_store: Option<&Entity<PromptStore>>, _window: &mut Window, cx: &mut App, ) -> Task<Result<HashMap<CreaseId, Mention>>> { @@ -1484,8 +1487,7 @@ impl MentionSet { }) } MentionUri::Rule { id: prompt_id, .. } => { - let Some(prompt_store) = thread_store.read(cx).prompt_store().clone() - else { + let Some(prompt_store) = prompt_store else { return Task::ready(Err(anyhow!("missing prompt store"))); }; let text_task = prompt_store.read(cx).load(*prompt_id, cx); @@ -1678,8 +1680,9 @@ impl Addon for MessageEditorAddon { mod tests { use std::{ops::Range, path::Path, sync::Arc}; - use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol as acp; + use agent2::HistoryStore; + use assistant_context::ContextStore; use editor::{AnchorRangeExt as _, Editor, EditorMode}; use fs::FakeFs; use futures::StreamExt as _; @@ -1710,16 +1713,16 @@ mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); let message_editor = cx.update(|window, cx| { cx.new(|cx| { MessageEditor::new( workspace.downgrade(), project.clone(), - thread_store.clone(), - text_thread_store.clone(), + history_store.clone(), + None, "Test", false, EditorMode::AutoHeight { @@ -1908,8 +1911,8 @@ mod tests { opened_editors.push(buffer); } - let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { let workspace_handle = cx.weak_entity(); @@ -1917,8 +1920,8 @@ mod tests { MessageEditor::new( workspace_handle, project.clone(), - thread_store.clone(), - text_thread_store.clone(), + history_store.clone(), + None, "Test", false, EditorMode::AutoHeight { @@ -2011,12 +2014,9 @@ mod tests { let contents = message_editor .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - project.clone(), - thread_store.clone(), - window, - cx, - ) + message_editor + .mention_set() + .contents(&project, None, window, cx) }) .await .unwrap() @@ -2066,12 +2066,9 @@ mod tests { let contents = message_editor .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - project.clone(), - thread_store.clone(), - window, - cx, - ) + message_editor + .mention_set() + .contents(&project, None, window, cx) }) .await .unwrap() @@ -2181,7 +2178,7 @@ mod tests { .update_in(&mut cx, |message_editor, window, cx| { message_editor .mention_set() - .contents(project.clone(), thread_store, window, cx) + .contents(&project, None, window, cx) }) .await .unwrap() diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index ee033bf1f61947b90c320334109834c86cf7228b..3be88ee3c3a0d8b59f5b5cfae7754f390cfe3885 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -5,7 +5,6 @@ use acp_thread::{ }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; -use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::{self as acp}; use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; @@ -32,7 +31,7 @@ use language::Buffer; use language_model::LanguageModelRegistry; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use project::{Project, ProjectEntryId}; -use prompt_store::PromptId; +use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::{Settings as _, SettingsStore}; use std::sync::Arc; @@ -158,8 +157,7 @@ impl AcpThreadView { workspace: WeakEntity<Workspace>, project: Entity<Project>, history_store: Entity<HistoryStore>, - thread_store: Entity<ThreadStore>, - text_thread_store: Entity<TextThreadStore>, + prompt_store: Option<Entity<PromptStore>>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { @@ -168,8 +166,8 @@ impl AcpThreadView { MessageEditor::new( workspace.clone(), project.clone(), - thread_store.clone(), - text_thread_store.clone(), + history_store.clone(), + prompt_store.clone(), "Message the agent — @ to include context", prevent_slash_commands, editor::EditorMode::AutoHeight { @@ -187,8 +185,8 @@ impl AcpThreadView { EntryViewState::new( workspace.clone(), project.clone(), - thread_store.clone(), - text_thread_store.clone(), + history_store.clone(), + prompt_store.clone(), prevent_slash_commands, ) }); @@ -3201,12 +3199,18 @@ impl AcpThreadView { }) .detach_and_log_err(cx); } - MentionUri::Thread { id, .. } => { + MentionUri::Thread { id, name } => { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { panel.update(cx, |panel, cx| { - panel - .open_thread_by_id(&id, window, cx) - .detach_and_log_err(cx) + panel.load_agent_thread( + DbThreadMetadata { + id, + title: name.into(), + updated_at: Default::default(), + }, + window, + cx, + ) }); } } @@ -4075,7 +4079,6 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { #[cfg(test)] pub(crate) mod tests { use acp_thread::StubAgentConnection; - use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::SessionId; use assistant_context::ContextStore; use editor::EditorSettings; @@ -4211,10 +4214,6 @@ pub(crate) mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let thread_store = - cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx))); - let text_thread_store = - cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); let context_store = cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx))); let history_store = @@ -4228,8 +4227,7 @@ pub(crate) mod tests { workspace.downgrade(), project, history_store, - thread_store.clone(), - text_thread_store.clone(), + None, window, cx, ) @@ -4400,6 +4398,7 @@ pub(crate) mod tests { ThemeSettings::register(cx); release_channel::init(SemanticVersion::default(), cx); EditorSettings::register(cx); + prompt_store::init(cx) }); } @@ -4420,10 +4419,6 @@ pub(crate) mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let thread_store = - cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx))); - let text_thread_store = - cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); let context_store = cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx))); let history_store = @@ -4438,8 +4433,7 @@ pub(crate) mod tests { workspace.downgrade(), project.clone(), history_store.clone(), - thread_store.clone(), - text_thread_store.clone(), + None, window, cx, ) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index c89dc567958f61b22d772e6a8774ae65eae7b652..b857052d695bacf96de1926b3ddd64cdc7ea0d03 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use std::sync::Arc; use std::time::Duration; +use acp_thread::AcpThread; use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; @@ -1016,8 +1017,6 @@ impl AgentPanel { agent: crate::ExternalAgent, } - let thread_store = self.thread_store.clone(); - let text_thread_store = self.context_store.clone(); let history = self.acp_history_store.clone(); cx.spawn_in(window, async move |this, cx| { @@ -1075,8 +1074,7 @@ impl AgentPanel { workspace.clone(), project, this.acp_history_store.clone(), - thread_store.clone(), - text_thread_store.clone(), + this.prompt_store.clone(), window, cx, ) @@ -1499,6 +1497,14 @@ impl AgentPanel { _ => None, } } + pub(crate) fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> { + match &self.active_view { + ActiveView::ExternalAgentThread { thread_view, .. } => { + thread_view.read(cx).thread().cloned() + } + _ => None, + } + } pub(crate) fn delete_thread( &mut self, @@ -1816,6 +1822,15 @@ impl AgentPanel { } } } + + pub fn load_agent_thread( + &mut self, + thread: DbThreadMetadata, + window: &mut Window, + cx: &mut Context<Self>, + ) { + self.external_thread(Some(ExternalAgent::NativeAgent), Some(thread), window, cx); + } } impl Focusable for AgentPanel { diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index c5b5e99a527c58046622b2e8ee062c9d0ece68a7..6d13531a57de2b8b654ba4ce0c734fc575c659cb 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -905,7 +905,7 @@ impl ContextStore { .into_iter() .filter(assistant_slash_commands::acceptable_prompt) .map(|prompt| { - log::debug!("registering context server command: {:?}", prompt.name); + log::info!("registering context server command: {:?}", prompt.name); slash_command_working_set.insert(Arc::new( assistant_slash_commands::ContextServerSlashCommand::new( context_server_store.clone(), From 4c85a0dc71c8f48ebd8acc090d0c8025b465cc14 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Wed, 20 Aug 2025 12:20:09 +0530 Subject: [PATCH 174/744] project: Register dynamic capabilities even when registerOptions doesn't exist (#36554) Closes #36482 Looks like we accidentally referenced [common/formatting.ts#L67-L70](https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/formatting.ts#L67-L70) instead of [common/client.ts#L2133](https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133). Release Notes: - Fixed code not formatting on save in language servers like Biome. (Preview Only) --- crates/project/src/lsp_store.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index d2fb12ee3721ae33fc3aa0d9b3e87737f2e504ed..12505a6a03de788c320e65f258e488bec6faa960 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -12032,16 +12032,15 @@ impl LspStore { } } -// Registration with empty capabilities should be ignored. -// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/formatting.ts#L67-L70 +// Registration with registerOptions as null, should fallback to true. +// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133 fn parse_register_capabilities<T: serde::de::DeserializeOwned>( reg: lsp::Registration, ) -> anyhow::Result<Option<OneOf<bool, T>>> { - Ok(reg - .register_options - .map(|options| serde_json::from_value::<T>(options)) - .transpose()? - .map(OneOf::Right)) + Ok(match reg.register_options { + Some(options) => Some(OneOf::Right(serde_json::from_value::<T>(options)?)), + None => Some(OneOf::Left(true)), + }) } fn subscribe_to_binary_statuses( From d4d049d7b91b3e8c846a13a35eedaa070e73a303 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Wed, 20 Aug 2025 10:45:03 +0200 Subject: [PATCH 175/744] agent2: Port more Zed AI features (#36559) Release Notes: - N/A --------- Co-authored-by: Danilo Leal <daniloleal09@gmail.com> --- crates/acp_thread/src/acp_thread.rs | 31 ++++++++ crates/agent_ui/src/acp/thread_view.rs | 101 +++++++++++++++++++++++++ crates/ui/src/components/callout.rs | 3 +- 3 files changed, 134 insertions(+), 1 deletion(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 5d3b35d0185f5d4b8b961d97dcd077ac63c6e879..e58f0a291f2bf1a0393f87050ba78f3730adf5be 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -674,6 +674,37 @@ pub struct TokenUsage { pub used_tokens: u64, } +impl TokenUsage { + pub fn ratio(&self) -> TokenUsageRatio { + #[cfg(debug_assertions)] + let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD") + .unwrap_or("0.8".to_string()) + .parse() + .unwrap(); + #[cfg(not(debug_assertions))] + let warning_threshold: f32 = 0.8; + + // When the maximum is unknown because there is no selected model, + // avoid showing the token limit warning. + if self.max_tokens == 0 { + TokenUsageRatio::Normal + } else if self.used_tokens >= self.max_tokens { + TokenUsageRatio::Exceeded + } else if self.used_tokens as f32 / self.max_tokens as f32 >= warning_threshold { + TokenUsageRatio::Warning + } else { + TokenUsageRatio::Normal + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TokenUsageRatio { + Normal, + Warning, + Exceeded, +} + #[derive(Debug, Clone)] pub struct RetryStatus { pub last_error: SharedString, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 3be88ee3c3a0d8b59f5b5cfae7754f390cfe3885..b93df3a5db441269109f63078227bc22685ac222 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -54,6 +54,7 @@ use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent}; use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; use crate::profile_selector::{ProfileProvider, ProfileSelector}; +use crate::ui::preview::UsageCallout; use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip}; use crate::{ AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow, @@ -2940,6 +2941,12 @@ impl AcpThreadView { .thread(acp_thread.session_id(), cx) } + fn is_using_zed_ai_models(&self, cx: &App) -> bool { + self.as_native_thread(cx) + .and_then(|thread| thread.read(cx).model()) + .is_some_and(|model| model.provider_id() == language_model::ZED_CLOUD_PROVIDER_ID) + } + fn render_token_usage(&self, cx: &mut Context<Self>) -> Option<Div> { let thread = self.thread()?.read(cx); let usage = thread.token_usage()?; @@ -3587,6 +3594,88 @@ impl AcpThreadView { .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) } + fn render_token_limit_callout( + &self, + line_height: Pixels, + cx: &mut Context<Self>, + ) -> Option<Callout> { + let token_usage = self.thread()?.read(cx).token_usage()?; + let ratio = token_usage.ratio(); + + let (severity, title) = match ratio { + acp_thread::TokenUsageRatio::Normal => return None, + acp_thread::TokenUsageRatio::Warning => { + (Severity::Warning, "Thread reaching the token limit soon") + } + acp_thread::TokenUsageRatio::Exceeded => { + (Severity::Error, "Thread reached the token limit") + } + }; + + let burn_mode_available = self.as_native_thread(cx).is_some_and(|thread| { + thread.read(cx).completion_mode() == CompletionMode::Normal + && thread + .read(cx) + .model() + .is_some_and(|model| model.supports_burn_mode()) + }); + + let description = if burn_mode_available { + "To continue, start a new thread from a summary or turn Burn Mode on." + } else { + "To continue, start a new thread from a summary." + }; + + Some( + Callout::new() + .severity(severity) + .line_height(line_height) + .title(title) + .description(description) + .actions_slot( + h_flex() + .gap_0p5() + .child( + Button::new("start-new-thread", "Start New Thread") + .label_size(LabelSize::Small) + .on_click(cx.listener(|_this, _, _window, _cx| { + // todo: Once thread summarization is implemented, start a new thread from a summary. + })), + ) + .when(burn_mode_available, |this| { + this.child( + IconButton::new("burn-mode-callout", IconName::ZedBurnMode) + .icon_size(IconSize::XSmall) + .on_click(cx.listener(|this, _event, window, cx| { + this.toggle_burn_mode(&ToggleBurnMode, window, cx); + })), + ) + }), + ), + ) + } + + fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> { + if !self.is_using_zed_ai_models(cx) { + return None; + } + + let user_store = self.project.read(cx).user_store().read(cx); + if user_store.is_usage_based_billing_enabled() { + return None; + } + + let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree); + + let usage = user_store.model_request_usage()?; + + Some( + div() + .child(UsageCallout::new(plan, usage)) + .line_height(line_height), + ) + } + fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) { self.entry_view_state.update(cx, |entry_view_state, cx| { entry_view_state.settings_changed(cx); @@ -3843,6 +3932,7 @@ impl Focusable for AcpThreadView { impl Render for AcpThreadView { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let has_messages = self.list_state.item_count() > 0; + let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5; v_flex() .size_full() @@ -3921,6 +4011,17 @@ impl Render for AcpThreadView { }) .children(self.render_thread_retry_status_callout(window, cx)) .children(self.render_thread_error(window, cx)) + .children( + if let Some(usage_callout) = self.render_usage_callout(line_height, cx) { + Some(usage_callout.into_any_element()) + } else if let Some(token_limit_callout) = + self.render_token_limit_callout(line_height, cx) + { + Some(token_limit_callout.into_any_element()) + } else { + None + }, + ) .child(self.render_message_editor(window, cx)) } } diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs index 22ba0468cde7fa22a197395379621c8a7c876517..7ffeda881c9ed5c0e9c14c100909db4a91693dec 100644 --- a/crates/ui/src/components/callout.rs +++ b/crates/ui/src/components/callout.rs @@ -81,7 +81,8 @@ impl Callout { self } - /// Sets an optional tertiary call-to-action button. + /// Sets an optional dismiss button, which is usually an icon button with a close icon. + /// This button is always rendered as the last one to the far right. pub fn dismiss_action(mut self, action: impl IntoElement) -> Self { self.dismiss_action = Some(action.into_any_element()); self From 44941b5dfe5a6ce4fbb45fb3aaba8dcecee481b6 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Wed, 20 Aug 2025 12:22:19 +0300 Subject: [PATCH 176/744] Fix `clippy::for_kv_map` lint violations (#36493) Release Notes: - N/A --- Cargo.toml | 1 + crates/agent_ui/src/agent_diff.rs | 2 +- crates/channel/src/channel_buffer.rs | 2 +- crates/channel/src/channel_store.rs | 2 +- crates/extension_host/src/extension_host.rs | 10 +++++----- crates/gpui/src/platform/linux/wayland/client.rs | 2 +- crates/gpui/src/platform/linux/x11/client.rs | 2 +- crates/outline_panel/src/outline_panel.rs | 2 +- crates/project/src/lsp_store.rs | 2 +- crates/project/src/manifest_tree.rs | 2 +- crates/project/src/manifest_tree/server_tree.rs | 4 ++-- 11 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dc14c8ebd92c43afcc9622daa129d07b1375a516..1ed8edf836cbcf746cada045d6590f893e232483 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -823,6 +823,7 @@ style = { level = "allow", priority = -1 } # Temporary list of style lints that we've fixed so far. comparison_to_empty = "warn" +for_kv_map = "warn" into_iter_on_ref = "warn" iter_cloned_collect = "warn" iter_next_slice = "warn" diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index b20b126d9b934cbeea2923214d131d2b0b18ebff..61a3ddd9063a51f07c637396abd97fabe24fe419 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1643,7 +1643,7 @@ impl AgentDiff { continue; }; - for (weak_editor, _) in buffer_editors { + for weak_editor in buffer_editors.keys() { let Some(editor) = weak_editor.upgrade() else { continue; }; diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 943e819ad6ddf9b8da3b7a224b13c33024bd86ba..828248b330b6ef6cfe0e13eab426de2900d364b2 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -135,7 +135,7 @@ impl ChannelBuffer { } } - for (_, old_collaborator) in &self.collaborators { + for old_collaborator in self.collaborators.values() { if !new_collaborators.contains_key(&old_collaborator.peer_id) { self.buffer.update(cx, |buffer, cx| { buffer.remove_peer(old_collaborator.replica_id, cx) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 850a4946135305d372e9b08b0b8ecc9ad7daf407..daa8a91c7c8952804c854b170d0bc2e1aa817631 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1073,7 +1073,7 @@ impl ChannelStore { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { - for (_, buffer) in &this.opened_buffers { + for buffer in this.opened_buffers.values() { if let OpenEntityHandle::Open(buffer) = &buffer && let Some(buffer) = buffer.upgrade() { diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 4c3ab8d242763e0a18bc05734c223516789620ad..fde0aeac9405d114f9cee89ca054d4503a35d482 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1175,16 +1175,16 @@ impl ExtensionStore { } } - for (server_id, _) in &extension.manifest.context_servers { + for server_id in extension.manifest.context_servers.keys() { self.proxy.unregister_context_server(server_id.clone(), cx); } - for (adapter, _) in &extension.manifest.debug_adapters { + for adapter in extension.manifest.debug_adapters.keys() { self.proxy.unregister_debug_adapter(adapter.clone()); } - for (locator, _) in &extension.manifest.debug_locators { + for locator in extension.manifest.debug_locators.keys() { self.proxy.unregister_debug_locator(locator.clone()); } - for (command_name, _) in &extension.manifest.slash_commands { + for command_name in extension.manifest.slash_commands.keys() { self.proxy.unregister_slash_command(command_name.clone()); } } @@ -1386,7 +1386,7 @@ impl ExtensionStore { ); } - for (id, _context_server_entry) in &manifest.context_servers { + for id in manifest.context_servers.keys() { this.proxy .register_context_server(extension.clone(), id.clone(), cx); } diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 4d314280940eb066c66b6520d0b8ca6ec815f024..2fe1da067bb3bdd8bde4ee394768f83c0aad6801 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -528,7 +528,7 @@ impl WaylandClient { client.common.appearance = appearance; - for (_, window) in &mut client.windows { + for window in client.windows.values_mut() { window.set_appearance(appearance); } } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 346ba8718b3b6ab1fe99b0dec8a11bbd78fa8b54..68198a285f9ebafe495d4f55798c17d6f5c4ee73 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -456,7 +456,7 @@ impl X11Client { move |event, _, client| match event { XDPEvent::WindowAppearance(appearance) => { client.with_common(|common| common.appearance = appearance); - for (_, window) in &mut client.0.borrow_mut().windows { + for window in client.0.borrow_mut().windows.values_mut() { window.window.set_appearance(appearance); } } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 891ae1595d3a95ab1f81d4bbd8840e94bd06cd20..832b7f09d1d0860671c90f31a36d760bfe718506 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -5338,7 +5338,7 @@ fn subscribe_for_editor_events( } EditorEvent::Reparsed(buffer_id) => { if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) { - for (_, excerpt) in excerpts { + for excerpt in excerpts.values_mut() { excerpt.invalidate_outlines(); } } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 12505a6a03de788c320e65f258e488bec6faa960..04b14ae06e20a8c3f9f2cc0b9a62e6e495836b72 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -10883,7 +10883,7 @@ impl LspStore { // Find all worktrees that have this server in their language server tree for (worktree_id, servers) in &local.lsp_tree.instances { if *worktree_id != key.worktree_id { - for (_, server_map) in &servers.roots { + for server_map in servers.roots.values() { if server_map.contains_key(&key.name) { worktrees_using_server.push(*worktree_id); } diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index ced9b34d93c836a714f57aa01afaef6a4458a16b..5a3c7bd40fb11ee5bebe340ddc57ec71a112270b 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -77,7 +77,7 @@ impl ManifestTree { _subscriptions: [ cx.subscribe(&worktree_store, Self::on_worktree_store_event), cx.observe_global::<SettingsStore>(|this, cx| { - for (_, roots) in &mut this.root_points { + for roots in this.root_points.values_mut() { roots.update(cx, |worktree_roots, _| { worktree_roots.roots = RootPathTrie::new(); }) diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index f5fd481324316d54e07de269ce676dc412f6c0fa..5e5f4bab49c118078c9dc3ba6af808ec91187c7c 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -312,8 +312,8 @@ impl LanguageServerTree { /// Remove nodes with a given ID from the tree. pub(crate) fn remove_nodes(&mut self, ids: &BTreeSet<LanguageServerId>) { - for (_, servers) in &mut self.instances { - for (_, nodes) in &mut servers.roots { + for servers in self.instances.values_mut() { + for nodes in &mut servers.roots.values_mut() { nodes.retain(|_, (node, _)| node.id.get().is_none_or(|id| !ids.contains(id))); } } From 4290f043cdb33c1f9ae5e296f95bd0509bb88b5b Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Wed, 20 Aug 2025 11:29:05 +0200 Subject: [PATCH 177/744] agent2: Fix token count not updating when changing model/toggling burn mode (#36562) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> --- crates/agent2/Cargo.toml | 1 + crates/agent2/src/agent.rs | 25 +++++++++--- crates/agent2/src/thread.rs | 76 ++++++++++++++++++++++++------------- 3 files changed, 69 insertions(+), 33 deletions(-) diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 2a39440af897b9d15f00592efe7c62b61846b9c3..bc32a79622249e81d3c94238c60f158db8714929 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -26,6 +26,7 @@ assistant_context.workspace = true assistant_tool.workspace = true assistant_tools.workspace = true chrono.workspace = true +client.workspace = true cloud_llm_client.workspace = true collections.workspace = true context_server.workspace = true diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 3c605de80383c13a2eb9f416806910cfcf31ecc5..ab5716d8ad7f34bf960fa575afe4e1909cfeb39a 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,8 +1,8 @@ -use crate::HistoryStore; use crate::{ ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization, UserMessageContent, templates::Templates, }; +use crate::{HistoryStore, TokenUsageUpdated}; use acp_thread::{AcpThread, AgentModelSelector}; use action_log::ActionLog; use agent_client_protocol as acp; @@ -253,6 +253,7 @@ impl NativeAgent { cx.observe_release(&acp_thread, |this, acp_thread, _cx| { this.sessions.remove(acp_thread.session_id()); }), + cx.subscribe(&thread_handle, Self::handle_thread_token_usage_updated), cx.observe(&thread_handle, move |this, thread, cx| { this.save_thread(thread.clone(), cx) }), @@ -440,6 +441,23 @@ impl NativeAgent { }) } + fn handle_thread_token_usage_updated( + &mut self, + thread: Entity<Thread>, + usage: &TokenUsageUpdated, + cx: &mut Context<Self>, + ) { + let Some(session) = self.sessions.get(thread.read(cx).id()) else { + return; + }; + session + .acp_thread + .update(cx, |acp_thread, cx| { + acp_thread.update_token_usage(usage.0.clone(), cx); + }) + .ok(); + } + fn handle_project_event( &mut self, _project: Entity<Project>, @@ -695,11 +713,6 @@ impl NativeAgentConnection { thread.update_tool_call(update, cx) })??; } - ThreadEvent::TokenUsageUpdate(usage) => { - acp_thread.update(cx, |thread, cx| { - thread.update_token_usage(Some(usage), cx) - })?; - } ThreadEvent::TitleUpdate(title) => { acp_thread .update(cx, |thread, cx| thread.update_title(title, cx))??; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index c1778bf38baf3ef388684a4f0c83869d455d1b72..b6405dbcbdf437a5dcec763ad78467edc3de6d79 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -15,7 +15,8 @@ use agent_settings::{ use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; use chrono::{DateTime, Utc}; -use cloud_llm_client::{CompletionIntent, CompletionRequestStatus}; +use client::{ModelRequestUsage, RequestUsage}; +use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; use collections::{HashMap, IndexMap}; use fs::Fs; use futures::{ @@ -25,7 +26,9 @@ use futures::{ stream::FuturesUnordered, }; use git::repository::DiffType; -use gpui::{App, AppContext, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; +use gpui::{ + App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity, +}; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt, LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest, @@ -484,7 +487,6 @@ pub enum ThreadEvent { ToolCall(acp::ToolCall), ToolCallUpdate(acp_thread::ToolCallUpdate), ToolCallAuthorization(ToolCallAuthorization), - TokenUsageUpdate(acp_thread::TokenUsage), TitleUpdate(SharedString), Retry(acp_thread::RetryStatus), Stop(acp::StopReason), @@ -873,7 +875,12 @@ impl Thread { } pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) { + let old_usage = self.latest_token_usage(); self.model = Some(model); + let new_usage = self.latest_token_usage(); + if old_usage != new_usage { + cx.emit(TokenUsageUpdated(new_usage)); + } cx.notify() } @@ -891,7 +898,12 @@ impl Thread { } pub fn set_completion_mode(&mut self, mode: CompletionMode, cx: &mut Context<Self>) { + let old_usage = self.latest_token_usage(); self.completion_mode = mode; + let new_usage = self.latest_token_usage(); + if old_usage != new_usage { + cx.emit(TokenUsageUpdated(new_usage)); + } cx.notify() } @@ -953,13 +965,15 @@ impl Thread { self.flush_pending_message(cx); } - pub fn update_token_usage(&mut self, update: language_model::TokenUsage) { + fn update_token_usage(&mut self, update: language_model::TokenUsage, cx: &mut Context<Self>) { let Some(last_user_message) = self.last_user_message() else { return; }; self.request_token_usage .insert(last_user_message.id.clone(), update); + cx.emit(TokenUsageUpdated(self.latest_token_usage())); + cx.notify(); } pub fn truncate(&mut self, message_id: UserMessageId, cx: &mut Context<Self>) -> Result<()> { @@ -1180,20 +1194,15 @@ impl Thread { )) => { *tool_use_limit_reached = true; } + Ok(LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::UsageUpdated { amount, limit }, + )) => { + this.update(cx, |this, cx| { + this.update_model_request_usage(amount, limit, cx) + })?; + } Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => { - let usage = acp_thread::TokenUsage { - max_tokens: model.max_token_count_for_mode( - request - .mode - .unwrap_or(cloud_llm_client::CompletionMode::Normal), - ), - used_tokens: token_usage.total_tokens(), - }; - - this.update(cx, |this, _cx| this.update_token_usage(token_usage)) - .ok(); - - event_stream.send_token_usage_update(usage); + this.update(cx, |this, cx| this.update_token_usage(token_usage, cx))?; } Ok(LanguageModelCompletionEvent::Stop(StopReason::Refusal)) => { *refusal = true; @@ -1214,8 +1223,7 @@ impl Thread { event_stream, cx, )); - }) - .ok(); + })?; } Err(error) => { let completion_mode = @@ -1325,8 +1333,8 @@ impl Thread { json_parse_error, ))); } - UsageUpdate(_) | StatusUpdate(_) => {} - Stop(_) => unreachable!(), + StatusUpdate(_) => {} + UsageUpdate(_) | Stop(_) => unreachable!(), } None @@ -1506,6 +1514,21 @@ impl Thread { } } + fn update_model_request_usage(&self, amount: usize, limit: UsageLimit, cx: &mut Context<Self>) { + self.project + .read(cx) + .user_store() + .update(cx, |user_store, cx| { + user_store.update_model_request_usage( + ModelRequestUsage(RequestUsage { + amount: amount as i32, + limit, + }), + cx, + ) + }); + } + pub fn title(&self) -> SharedString { self.title.clone().unwrap_or("New Thread".into()) } @@ -1636,6 +1659,7 @@ impl Thread { }) })) } + fn last_user_message(&self) -> Option<&UserMessage> { self.messages .iter() @@ -1934,6 +1958,10 @@ impl RunningTurn { } } +pub struct TokenUsageUpdated(pub Option<acp_thread::TokenUsage>); + +impl EventEmitter<TokenUsageUpdated> for Thread {} + pub trait AgentTool where Self: 'static + Sized, @@ -2166,12 +2194,6 @@ impl ThreadEventStream { .ok(); } - fn send_token_usage_update(&self, usage: acp_thread::TokenUsage) { - self.0 - .unbounded_send(Ok(ThreadEvent::TokenUsageUpdate(usage))) - .ok(); - } - fn send_retry(&self, status: acp_thread::RetryStatus) { self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); } From 83d361ba694fac74a131fde835ecae26b043100f Mon Sep 17 00:00:00 2001 From: Finn Evers <finn@zed.dev> Date: Wed, 20 Aug 2025 11:29:53 +0200 Subject: [PATCH 178/744] Add more string and comment overrides (#36566) Follow-up to #36469 Part of the issue was that we hadn't defined comment and string overrides for some languages. Hence, even after the fix edit predictions would show up in comments for me in e.g. JSONC files. This PR adds some more overrides where possible for this repo to ensure this happens less frequently. Release Notes: - N/A --- crates/languages/src/bash/overrides.scm | 2 ++ crates/languages/src/jsonc/overrides.scm | 1 + crates/languages/src/yaml/overrides.scm | 5 +++++ 3 files changed, 8 insertions(+) create mode 100644 crates/languages/src/bash/overrides.scm create mode 100644 crates/languages/src/yaml/overrides.scm diff --git a/crates/languages/src/bash/overrides.scm b/crates/languages/src/bash/overrides.scm new file mode 100644 index 0000000000000000000000000000000000000000..81fec9a5f57b28fc67b4781ec37df43559e21dc9 --- /dev/null +++ b/crates/languages/src/bash/overrides.scm @@ -0,0 +1,2 @@ +(comment) @comment.inclusive +(string) @string diff --git a/crates/languages/src/jsonc/overrides.scm b/crates/languages/src/jsonc/overrides.scm index cc966ad4c13e0cc7f7fc27a1152b461f24e3c6b0..81fec9a5f57b28fc67b4781ec37df43559e21dc9 100644 --- a/crates/languages/src/jsonc/overrides.scm +++ b/crates/languages/src/jsonc/overrides.scm @@ -1 +1,2 @@ +(comment) @comment.inclusive (string) @string diff --git a/crates/languages/src/yaml/overrides.scm b/crates/languages/src/yaml/overrides.scm new file mode 100644 index 0000000000000000000000000000000000000000..9503051a62080eb2fdfca3416ef9e5286464dd17 --- /dev/null +++ b/crates/languages/src/yaml/overrides.scm @@ -0,0 +1,5 @@ +(comment) @comment.inclusive +[ + (single_quote_scalar) + (double_quote_scalar) +] @string From 0a80209c5e4f268f9ccdda0460ede2cd874f3c7b Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Wed, 20 Aug 2025 11:54:26 +0200 Subject: [PATCH 179/744] agent2: Fix remaining update_model_request_usage todos (#36570) Release Notes: - N/A --- crates/agent2/src/thread.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index b6405dbcbdf437a5dcec763ad78467edc3de6d79..73a86d53eabfb2b53478c123896defec8f6ad2e9 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1563,12 +1563,11 @@ impl Thread { let text = match event { LanguageModelCompletionEvent::Text(text) => text, LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { .. }, + CompletionRequestStatus::UsageUpdated { amount, limit }, ) => { - // this.update(cx, |thread, cx| { - // thread.update_model_request_usage(amount as u32, limit, cx); - // })?; - // TODO: handle usage update + this.update(cx, |thread, cx| { + thread.update_model_request_usage(amount, limit, cx); + })?; continue; } _ => continue, @@ -1629,12 +1628,11 @@ impl Thread { let text = match event { LanguageModelCompletionEvent::Text(text) => text, LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { .. }, + CompletionRequestStatus::UsageUpdated { amount, limit }, ) => { - // this.update(cx, |thread, cx| { - // thread.update_model_request_usage(amount as u32, limit, cx); - // })?; - // TODO: handle usage update + this.update(cx, |thread, cx| { + thread.update_model_request_usage(amount, limit, cx); + })?; continue; } _ => continue, From a32a264508cf1142c8cb943c68615771474c7183 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Wed, 20 Aug 2025 12:03:35 +0200 Subject: [PATCH 180/744] agent2: Use correct completion intent when generating summary (#36573) Release Notes: - N/A --- crates/agent2/src/thread.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 73a86d53eabfb2b53478c123896defec8f6ad2e9..0e1287a920dbffe10cf2e2bd7c93067242e00ba9 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1541,7 +1541,7 @@ impl Thread { return Task::ready(Err(anyhow!("No summarization model available"))); }; let mut request = LanguageModelRequest { - intent: Some(CompletionIntent::ThreadSummarization), + intent: Some(CompletionIntent::ThreadContextSummarization), temperature: AgentSettings::temperature_for_model(&model, cx), ..Default::default() }; From cf7c64d77f1806cdd34b3812bbf27681fb3cb905 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 20 Aug 2025 12:05:58 +0200 Subject: [PATCH 181/744] lints: A bunch of extra style lint fixes (#36568) - **lints: Fix 'doc_lazy_continuation'** - **lints: Fix 'doc_overindented_list_items'** - **inherent_to_string and io_other_error** - **Some more lint fixes** - **lints: enable bool_assert_comparison, match_like_matches_macro and wrong_self_convention** Release Notes: - N/A --- Cargo.toml | 7 ++++ crates/agent/src/history_store.rs | 7 ++-- crates/agent/src/thread.rs | 4 +- crates/agent2/src/history_store.rs | 7 ++-- crates/agent_settings/src/agent_settings.rs | 5 +-- crates/agent_ui/src/active_thread.rs | 2 +- .../add_llm_provider_modal.rs | 24 ++++++------ .../src/assistant_context_tests.rs | 2 +- .../src/assistant_slash_command.rs | 14 +++---- .../src/extension_slash_command.rs | 2 +- .../src/cargo_workspace_command.rs | 2 +- .../src/context_server_command.rs | 2 +- .../src/default_command.rs | 2 +- .../src/delta_command.rs | 2 +- .../src/diagnostics_command.rs | 2 +- .../src/fetch_command.rs | 2 +- .../src/file_command.rs | 2 +- .../src/now_command.rs | 2 +- .../src/prompt_command.rs | 2 +- .../src/symbols_command.rs | 2 +- .../src/tab_command.rs | 2 +- .../src/auto_update_helper.rs | 19 ++++----- crates/auto_update_helper/src/updater.rs | 12 +----- crates/client/src/user.rs | 2 +- crates/collab/src/db.rs | 2 +- crates/collab/src/tests/integration_tests.rs | 12 +++--- crates/collab/src/tests/test_server.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 8 ++-- crates/context_server/src/client.rs | 6 +-- crates/dap/src/client.rs | 2 +- crates/editor/src/display_map/block_map.rs | 7 +--- crates/editor/src/editor.rs | 5 +-- crates/editor/src/items.rs | 2 +- crates/editor/src/scroll/scroll_amount.rs | 5 +-- .../src/test/editor_lsp_test_context.rs | 2 + crates/eval/src/example.rs | 2 +- crates/eval/src/instance.rs | 4 +- crates/extension_api/src/extension_api.rs | 6 +-- crates/git/src/status.rs | 39 ++++++------------- crates/git_ui/src/project_diff.rs | 7 +--- crates/gpui/src/action.rs | 12 +++--- crates/gpui/src/app/test_context.rs | 7 ++-- crates/gpui/src/color.rs | 8 ++-- crates/gpui/src/geometry.rs | 28 ++++++------- crates/gpui/src/gpui.rs | 4 ++ crates/gpui/src/keymap.rs | 28 ++++++------- crates/gpui/src/keymap/binding.rs | 7 +--- crates/gpui/src/platform.rs | 2 +- crates/gpui/src/platform/linux/platform.rs | 31 +++++++-------- crates/gpui/src/platform/linux/wayland.rs | 2 +- .../gpui/src/platform/linux/wayland/window.rs | 4 +- crates/gpui/src/platform/linux/x11/window.rs | 2 +- crates/gpui/src/platform/mac/window.rs | 2 +- crates/gpui/src/tab_stop.rs | 21 +++------- crates/gpui_macros/src/gpui_macros.rs | 2 +- crates/language/src/language_registry.rs | 2 +- crates/language_model/src/role.rs | 2 +- crates/migrator/src/migrator.rs | 2 +- crates/multi_buffer/src/multi_buffer.rs | 2 +- crates/paths/src/paths.rs | 2 +- crates/project/src/debugger.rs | 4 +- .../project/src/debugger/breakpoint_store.rs | 2 +- crates/project/src/debugger/memory.rs | 3 +- crates/project/src/git_store/conflict_set.rs | 2 +- crates/project/src/git_store/git_traversal.rs | 2 +- crates/project/src/lsp_store.rs | 4 +- crates/project/src/manifest_tree/path_trie.rs | 6 +-- crates/project/src/project.rs | 30 ++++++-------- crates/project/src/project_tests.rs | 4 +- crates/project_panel/src/project_panel.rs | 4 +- crates/remote/src/ssh_session.rs | 10 ++--- .../remote_server/src/remote_editing_tests.rs | 2 +- crates/remote_server/src/unix.rs | 2 +- crates/repl/src/kernels/mod.rs | 5 +-- crates/reqwest_client/src/reqwest_client.rs | 2 +- crates/rope/src/chunk.rs | 2 +- crates/rpc/src/conn.rs | 4 +- crates/search/src/search.rs | 4 +- crates/settings/src/settings.rs | 4 +- crates/task/src/shell_builder.rs | 2 +- .../src/terminal_slash_command.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 2 +- .../ui/src/components/button/button_like.rs | 10 ++--- crates/ui/src/utils/format_distance.rs | 4 +- crates/util/src/archive.rs | 2 +- crates/util/src/markdown.rs | 2 +- crates/util/src/paths.rs | 25 +++++++----- crates/vim/src/motion.rs | 39 +++++++++---------- crates/vim/src/vim.rs | 5 +-- crates/worktree/src/worktree.rs | 6 +-- crates/worktree/src/worktree_tests.rs | 12 +++--- crates/x_ai/src/x_ai.rs | 5 +-- 92 files changed, 277 insertions(+), 345 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1ed8edf836cbcf746cada045d6590f893e232483..3610808984bdaf85d45c45ba62b5db11f91c3f30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -822,15 +822,21 @@ single_range_in_vec_init = "allow" style = { level = "allow", priority = -1 } # Temporary list of style lints that we've fixed so far. +bool_assert_comparison = "warn" comparison_to_empty = "warn" +doc_lazy_continuation = "warn" +doc_overindented_list_items = "warn" +inherent_to_string = "warn" for_kv_map = "warn" into_iter_on_ref = "warn" +io_other_error = "warn" iter_cloned_collect = "warn" iter_next_slice = "warn" iter_nth = "warn" iter_nth_zero = "warn" iter_skip_next = "warn" let_and_return = "warn" +match_like_matches_macro = "warn" module_inception = { level = "deny" } question_mark = { level = "deny" } single_match = "warn" @@ -846,6 +852,7 @@ needless_return = { level = "warn" } unnecessary_mut_passed = {level = "warn"} unnecessary_map_or = { level = "warn" } unused_unit = "warn" +wrong_self_convention = "warn" # Individual rules that have violations in the codebase: type_complexity = "allow" diff --git a/crates/agent/src/history_store.rs b/crates/agent/src/history_store.rs index eb39c3e454c25fdc87baeffd550ea5cb29155aab..8f4c1a1e2e6533b4760956c60bfc1a26123df92a 100644 --- a/crates/agent/src/history_store.rs +++ b/crates/agent/src/history_store.rs @@ -254,10 +254,9 @@ impl HistoryStore { } pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) { - self.recently_opened_entries.retain(|entry| match entry { - HistoryEntryId::Thread(thread_id) if thread_id == &id => false, - _ => true, - }); + self.recently_opened_entries.retain( + |entry| !matches!(entry, HistoryEntryId::Thread(thread_id) if thread_id == &id), + ); self.save_recently_opened_entries(cx); } diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index fc91e1bb62b1154ac2b5bb5651f3b8352625635f..88f82701a499b505a6fd805b8cf49c8708d6ce09 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -181,7 +181,7 @@ impl Message { } } - pub fn to_string(&self) -> String { + pub fn to_message_content(&self) -> String { let mut result = String::new(); if !self.loaded_context.text.is_empty() { @@ -2823,7 +2823,7 @@ impl Thread { let message_content = self .message(message_id) - .map(|msg| msg.to_string()) + .map(|msg| msg.to_message_content()) .unwrap_or_default(); cx.background_spawn(async move { diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 7eb7da94ba1a0b6690b7d16422a02b2a22ba6b92..3df4eddde4cb7716ca8e6948c2f67fffc6f6ec7b 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -312,10 +312,9 @@ impl HistoryStore { } pub fn remove_recently_opened_thread(&mut self, id: acp::SessionId, cx: &mut Context<Self>) { - self.recently_opened_entries.retain(|entry| match entry { - HistoryEntryId::AcpThread(thread_id) if thread_id == &id => false, - _ => true, - }); + self.recently_opened_entries.retain( + |entry| !matches!(entry, HistoryEntryId::AcpThread(thread_id) if thread_id == &id), + ); self.save_recently_opened_entries(cx); } diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 1fe41d002ca4767206b1e77028ffe19bf1d4f690..ed1ed2b89879c18eceaab22843390a766e4f6c77 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -505,9 +505,8 @@ impl Settings for AgentSettings { } } - debug_assert_eq!( - sources.default.always_allow_tool_actions.unwrap_or(false), - false, + debug_assert!( + !sources.default.always_allow_tool_actions.unwrap_or(false), "For security, agent.always_allow_tool_actions should always be false in default.json. If it's true, that is a bug that should be fixed!" ); diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index e595b94ebba9844390a06f4cc54bb8dbaf9e09c5..92588cf213c9ef5fd6bd932a358cc566977a5eb2 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1765,7 +1765,7 @@ impl ActiveThread { .thread .read(cx) .message(message_id) - .map(|msg| msg.to_string()) + .map(|msg| msg.to_message_content()) .unwrap_or_default(); telemetry::event!( 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 998641bf01c950d3717fa651a5400e95cbedb618..182831f488870997d175cce0ad7e1c94e392f1ea 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 @@ -668,10 +668,10 @@ mod tests { ); let parsed_model = model_input.parse(cx).unwrap(); - assert_eq!(parsed_model.capabilities.tools, true); - assert_eq!(parsed_model.capabilities.images, false); - assert_eq!(parsed_model.capabilities.parallel_tool_calls, false); - assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + assert!(parsed_model.capabilities.tools); + assert!(!parsed_model.capabilities.images); + assert!(!parsed_model.capabilities.parallel_tool_calls); + assert!(!parsed_model.capabilities.prompt_cache_key); }); } @@ -693,10 +693,10 @@ mod tests { model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected; let parsed_model = model_input.parse(cx).unwrap(); - assert_eq!(parsed_model.capabilities.tools, false); - assert_eq!(parsed_model.capabilities.images, false); - assert_eq!(parsed_model.capabilities.parallel_tool_calls, false); - assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + assert!(!parsed_model.capabilities.tools); + assert!(!parsed_model.capabilities.images); + assert!(!parsed_model.capabilities.parallel_tool_calls); + assert!(!parsed_model.capabilities.prompt_cache_key); }); } @@ -719,10 +719,10 @@ mod tests { let parsed_model = model_input.parse(cx).unwrap(); assert_eq!(parsed_model.name, "somemodel"); - assert_eq!(parsed_model.capabilities.tools, true); - assert_eq!(parsed_model.capabilities.images, false); - assert_eq!(parsed_model.capabilities.parallel_tool_calls, true); - assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + assert!(parsed_model.capabilities.tools); + assert!(!parsed_model.capabilities.images); + assert!(parsed_model.capabilities.parallel_tool_calls); + assert!(!parsed_model.capabilities.prompt_cache_key); }); } diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_context/src/assistant_context_tests.rs index 28cc8ef8f058e9627e6ff73b8e40a1198f218856..3db4a33b192d019a893609096a17cdffe504a37e 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_context/src/assistant_context_tests.rs @@ -1436,6 +1436,6 @@ impl SlashCommand for FakeSlashCommand { sections: vec![], run_commands_in_text: false, } - .to_event_stream())) + .into_event_stream())) } } diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 828f115bf5ed8cfedf14c67243b4a8048d07ebd0..4b85fa2edf2afd6b3ea7df154b5e14ab492a8013 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -161,7 +161,7 @@ impl SlashCommandOutput { } /// Returns this [`SlashCommandOutput`] as a stream of [`SlashCommandEvent`]s. - pub fn to_event_stream(mut self) -> BoxStream<'static, Result<SlashCommandEvent>> { + pub fn into_event_stream(mut self) -> BoxStream<'static, Result<SlashCommandEvent>> { self.ensure_valid_section_ranges(); let mut events = Vec::new(); @@ -363,7 +363,7 @@ mod tests { run_commands_in_text: false, }; - let events = output.clone().to_event_stream().collect::<Vec<_>>().await; + let events = output.clone().into_event_stream().collect::<Vec<_>>().await; let events = events .into_iter() .filter_map(|event| event.ok()) @@ -386,7 +386,7 @@ mod tests { ); let new_output = - SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) + SlashCommandOutput::from_event_stream(output.clone().into_event_stream()) .await .unwrap(); @@ -415,7 +415,7 @@ mod tests { run_commands_in_text: false, }; - let events = output.clone().to_event_stream().collect::<Vec<_>>().await; + let events = output.clone().into_event_stream().collect::<Vec<_>>().await; let events = events .into_iter() .filter_map(|event| event.ok()) @@ -452,7 +452,7 @@ mod tests { ); let new_output = - SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) + SlashCommandOutput::from_event_stream(output.clone().into_event_stream()) .await .unwrap(); @@ -493,7 +493,7 @@ mod tests { run_commands_in_text: false, }; - let events = output.clone().to_event_stream().collect::<Vec<_>>().await; + let events = output.clone().into_event_stream().collect::<Vec<_>>().await; let events = events .into_iter() .filter_map(|event| event.ok()) @@ -562,7 +562,7 @@ mod tests { ); let new_output = - SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) + SlashCommandOutput::from_event_stream(output.clone().into_event_stream()) .await .unwrap(); diff --git a/crates/assistant_slash_command/src/extension_slash_command.rs b/crates/assistant_slash_command/src/extension_slash_command.rs index 74c46ffb5ffefb2ccbefdba8edec4e9e778489b5..e47ae52c98740af17c90fe657386bb0120773d9b 100644 --- a/crates/assistant_slash_command/src/extension_slash_command.rs +++ b/crates/assistant_slash_command/src/extension_slash_command.rs @@ -166,7 +166,7 @@ impl SlashCommand for ExtensionSlashCommand { .collect(), run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/cargo_workspace_command.rs b/crates/assistant_slash_commands/src/cargo_workspace_command.rs index 8b088ea012de5f1ef6f7c787924c3cb2c6ec44c8..d58b2edc4c3dffd799dd9eb1c104686dc6488687 100644 --- a/crates/assistant_slash_commands/src/cargo_workspace_command.rs +++ b/crates/assistant_slash_commands/src/cargo_workspace_command.rs @@ -150,7 +150,7 @@ impl SlashCommand for CargoWorkspaceSlashCommand { }], run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) }); output.unwrap_or_else(|error| Task::ready(Err(error))) diff --git a/crates/assistant_slash_commands/src/context_server_command.rs b/crates/assistant_slash_commands/src/context_server_command.rs index 6caa1beb3bd82fdbc70fd516cdbef9db63978a76..ee0cbf54c23a595f6503162c91dd1df3be019dd5 100644 --- a/crates/assistant_slash_commands/src/context_server_command.rs +++ b/crates/assistant_slash_commands/src/context_server_command.rs @@ -191,7 +191,7 @@ impl SlashCommand for ContextServerSlashCommand { text: prompt, run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) } else { Task::ready(Err(anyhow!("Context server not found"))) diff --git a/crates/assistant_slash_commands/src/default_command.rs b/crates/assistant_slash_commands/src/default_command.rs index 6fce7f07a46d3d248c1c1292a67f1ad577c43645..01eff881cff0f07db9bf34e25853432e413ed79f 100644 --- a/crates/assistant_slash_commands/src/default_command.rs +++ b/crates/assistant_slash_commands/src/default_command.rs @@ -85,7 +85,7 @@ impl SlashCommand for DefaultSlashCommand { text, run_commands_in_text: true, } - .to_event_stream()) + .into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/delta_command.rs b/crates/assistant_slash_commands/src/delta_command.rs index 2cc4591386633ef85ae180c5fa0a802887485e7e..ea05fca588d0a496eeb3a2d2128b3861ba8a1e30 100644 --- a/crates/assistant_slash_commands/src/delta_command.rs +++ b/crates/assistant_slash_commands/src/delta_command.rs @@ -118,7 +118,7 @@ impl SlashCommand for DeltaSlashCommand { } anyhow::ensure!(changes_detected, "no new changes detected"); - Ok(output.to_event_stream()) + Ok(output.into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/diagnostics_command.rs b/crates/assistant_slash_commands/src/diagnostics_command.rs index 536fe9f0efc0d5145663d1a88295bf849e0587ab..10f950c86662524d97f0dcb454ef1e82dddfa7d6 100644 --- a/crates/assistant_slash_commands/src/diagnostics_command.rs +++ b/crates/assistant_slash_commands/src/diagnostics_command.rs @@ -189,7 +189,7 @@ impl SlashCommand for DiagnosticsSlashCommand { window.spawn(cx, async move |_| { task.await? - .map(|output| output.to_event_stream()) + .map(|output| output.into_event_stream()) .context("No diagnostics found") }) } diff --git a/crates/assistant_slash_commands/src/fetch_command.rs b/crates/assistant_slash_commands/src/fetch_command.rs index 4e0bb3d05a7f3c2828206a6c4deeaee8c505ed7e..6d3f66c9a23c896c765ba6c0a43b7a99dbc7ee73 100644 --- a/crates/assistant_slash_commands/src/fetch_command.rs +++ b/crates/assistant_slash_commands/src/fetch_command.rs @@ -177,7 +177,7 @@ impl SlashCommand for FetchSlashCommand { }], run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/file_command.rs b/crates/assistant_slash_commands/src/file_command.rs index 894aa94a272f63c72b346b3bc537a5663ee02e24..a973d653e4527be808618f76d60af59e4a891947 100644 --- a/crates/assistant_slash_commands/src/file_command.rs +++ b/crates/assistant_slash_commands/src/file_command.rs @@ -371,7 +371,7 @@ fn collect_files( &mut output, ) .log_err(); - let mut buffer_events = output.to_event_stream(); + let mut buffer_events = output.into_event_stream(); while let Some(event) = buffer_events.next().await { events_tx.unbounded_send(event)?; } diff --git a/crates/assistant_slash_commands/src/now_command.rs b/crates/assistant_slash_commands/src/now_command.rs index e4abef2a7c80fbdc96df28cbd1072d180fd864f3..aec21e7173bafd4cb07e7c37135fa0ad6fa88812 100644 --- a/crates/assistant_slash_commands/src/now_command.rs +++ b/crates/assistant_slash_commands/src/now_command.rs @@ -66,6 +66,6 @@ impl SlashCommand for NowSlashCommand { }], run_commands_in_text: false, } - .to_event_stream())) + .into_event_stream())) } } diff --git a/crates/assistant_slash_commands/src/prompt_command.rs b/crates/assistant_slash_commands/src/prompt_command.rs index c177f9f3599525924aa18700ea09d5fe977a5698..27029ac1567fa3833cb7c13d80f10ba60e2e3f2d 100644 --- a/crates/assistant_slash_commands/src/prompt_command.rs +++ b/crates/assistant_slash_commands/src/prompt_command.rs @@ -117,7 +117,7 @@ impl SlashCommand for PromptSlashCommand { }], run_commands_in_text: true, } - .to_event_stream()) + .into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/symbols_command.rs b/crates/assistant_slash_commands/src/symbols_command.rs index ef9314643116689d36e99b2a9bcb7d69982a776f..30287091444db391056a77664890229df95a1d46 100644 --- a/crates/assistant_slash_commands/src/symbols_command.rs +++ b/crates/assistant_slash_commands/src/symbols_command.rs @@ -92,7 +92,7 @@ impl SlashCommand for OutlineSlashCommand { text: outline_text, run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) }); diff --git a/crates/assistant_slash_commands/src/tab_command.rs b/crates/assistant_slash_commands/src/tab_command.rs index e4ae391a9c9482b3961b6cc8ffd98e2ae0627fd2..a124beed6302d6c67085ccb70f4c3aa58834d3f2 100644 --- a/crates/assistant_slash_commands/src/tab_command.rs +++ b/crates/assistant_slash_commands/src/tab_command.rs @@ -157,7 +157,7 @@ impl SlashCommand for TabSlashCommand { for (full_path, buffer, _) in tab_items_search.await? { append_buffer_to_output(&buffer, full_path.as_deref(), &mut output).log_err(); } - Ok(output.to_event_stream()) + Ok(output.into_event_stream()) }) } } diff --git a/crates/auto_update_helper/src/auto_update_helper.rs b/crates/auto_update_helper/src/auto_update_helper.rs index 3aa57094d38f07400d6077e42203746d0cbb5bff..21ead701b2629960a9f2b5bc639f5d6dcdbc96c5 100644 --- a/crates/auto_update_helper/src/auto_update_helper.rs +++ b/crates/auto_update_helper/src/auto_update_helper.rs @@ -128,23 +128,20 @@ mod windows_impl { #[test] fn test_parse_args() { // launch can be specified via two separate arguments - assert_eq!(parse_args(["--launch".into(), "true".into()]).launch, true); - assert_eq!( - parse_args(["--launch".into(), "false".into()]).launch, - false - ); + assert!(parse_args(["--launch".into(), "true".into()]).launch); + assert!(!parse_args(["--launch".into(), "false".into()]).launch); // launch can be specified via one single argument - assert_eq!(parse_args(["--launch=true".into()]).launch, true); - assert_eq!(parse_args(["--launch=false".into()]).launch, false); + assert!(parse_args(["--launch=true".into()]).launch); + assert!(!parse_args(["--launch=false".into()]).launch); // launch defaults to true on no arguments - assert_eq!(parse_args([]).launch, true); + assert!(parse_args([]).launch); // launch defaults to true on invalid arguments - assert_eq!(parse_args(["--launch".into()]).launch, true); - assert_eq!(parse_args(["--launch=".into()]).launch, true); - assert_eq!(parse_args(["--launch=invalid".into()]).launch, true); + assert!(parse_args(["--launch".into()]).launch); + assert!(parse_args(["--launch=".into()]).launch); + assert!(parse_args(["--launch=invalid".into()]).launch); } } } diff --git a/crates/auto_update_helper/src/updater.rs b/crates/auto_update_helper/src/updater.rs index 920f8d5fcf3224f8842ff888249c2281412c478a..762771617609e63996685d3d96fae69135355249 100644 --- a/crates/auto_update_helper/src/updater.rs +++ b/crates/auto_update_helper/src/updater.rs @@ -90,11 +90,7 @@ pub(crate) const JOBS: [Job; 2] = [ std::thread::sleep(Duration::from_millis(1000)); if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") { match config.as_str() { - "err" => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Simulated error", - )) - .context("Anyhow!"), + "err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"), _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config), } } else { @@ -105,11 +101,7 @@ pub(crate) const JOBS: [Job; 2] = [ std::thread::sleep(Duration::from_millis(1000)); if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") { match config.as_str() { - "err" => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Simulated error", - )) - .context("Anyhow!"), + "err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"), _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config), } } else { diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 2599be9b1690727ca3c91b16c9d3f03923617c13..20f99e394460f2f594c078dfde6cd568dfc7906b 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -41,7 +41,7 @@ impl std::fmt::Display for ChannelId { pub struct ProjectId(pub u64); impl ProjectId { - pub fn to_proto(&self) -> u64 { + pub fn to_proto(self) -> u64 { self.0 } } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 774eec5d2c553ae0014921a1df76bbf74cfabead..95a485305ca31bde351f4962d1678e30801d8a01 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -685,7 +685,7 @@ impl LocalSettingsKind { } } - pub fn to_proto(&self) -> proto::LocalSettingsKind { + pub fn to_proto(self) -> proto::LocalSettingsKind { match self { Self::Settings => proto::LocalSettingsKind::Settings, Self::Tasks => proto::LocalSettingsKind::Tasks, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 930e635dd806475d3488732fe0fc1db19debcee0..e01736f0ef9daced0d35742d08503ea8b188f733 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3208,7 +3208,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -3237,7 +3237,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -3266,7 +3266,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -3295,7 +3295,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); project_b @@ -3304,7 +3304,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); project_b @@ -3313,7 +3313,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 07ea1efc9dada31b7ce6aa82dbcdcfd9a31e1c0f..f1c0b2d182a874ea985afb42311cce1edd1e1fa0 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -897,7 +897,7 @@ impl TestClient { let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap()); let entity = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(*window.deref(), cx).as_mut(); + let cx = VisualTestContext::from_window(*window.deref(), cx).into_mut(); // it might be nice to try and cleanup these at the end of each test. (entity, cx) } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 0f785c1f90d71f6a9826254c6918a88fcdf67b1d..b756984a09ba4ffecf3440ac298df4c40bfacab3 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1821,10 +1821,10 @@ impl CollabPanel { } fn select_channel_editor(&mut self) { - self.selection = self.entries.iter().position(|entry| match entry { - ListEntry::ChannelEditor { .. } => true, - _ => false, - }); + self.selection = self + .entries + .iter() + .position(|entry| matches!(entry, ListEntry::ChannelEditor { .. })); } fn new_subchannel( diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index ccf7622d82fb22b0792d5e33e18992037a493d54..03cf047ac5273071354756119864ab6914e524c6 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -67,11 +67,7 @@ pub(crate) struct Client { pub(crate) struct ContextServerId(pub Arc<str>); fn is_null_value<T: Serialize>(value: &T) -> bool { - if let Ok(Value::Null) = serde_json::to_value(value) { - true - } else { - false - } + matches!(serde_json::to_value(value), Ok(Value::Null)) } #[derive(Serialize, Deserialize)] diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 7b791450ecba3b09b6571ac84fbebdf92fff57b8..2590bf5c8b0db8e70a7897b8de4bc878187e4daa 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -23,7 +23,7 @@ impl SessionId { Self(client_id as u32) } - pub fn to_proto(&self) -> u64 { + pub fn to_proto(self) -> u64 { self.0 as u64 } } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 5d5c9500ebcd1c1088318a9f7a2a3f4db5ddb0b1..0d31398a54deffee61176550f6f8da804b450297 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -2868,12 +2868,7 @@ mod tests { 1, blocks .iter() - .filter(|(_, block)| { - match block { - Block::FoldedBuffer { .. } => true, - _ => false, - } - }) + .filter(|(_, block)| { matches!(block, Block::FoldedBuffer { .. }) }) .count(), "Should have one folded block, producing a header of the second buffer" ); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 575631b51767d35eb18e66553eca91759fbd889c..2f3ced65dc1a6c9cf481d613cd715c39bc9e5aab 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -782,10 +782,7 @@ impl MinimapVisibility { } fn disabled(&self) -> bool { - match *self { - Self::Disabled => true, - _ => false, - } + matches!(*self, Self::Disabled) } fn settings_visibility(&self) -> bool { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 8957e0e99c4f39406e8c5baabcaa2d23019e9b0c..62889c638fdcd1db43e83f3d7b55a80c8e99d996 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -293,7 +293,7 @@ impl FollowableItem for Editor { EditorEvent::ExcerptsRemoved { ids, .. } => { update .deleted_excerpts - .extend(ids.iter().map(ExcerptId::to_proto)); + .extend(ids.iter().copied().map(ExcerptId::to_proto)); true } EditorEvent::ScrollPositionChanged { autoscroll, .. } if !autoscroll => { diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs index b2af4f8e4fbce899c6aee317402ee1365cee8600..5992c9023c1f9d6eb7e7eb201099c6eef17a33d8 100644 --- a/crates/editor/src/scroll/scroll_amount.rs +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -67,10 +67,7 @@ impl ScrollAmount { } pub fn is_full_page(&self) -> bool { - match self { - ScrollAmount::Page(count) if count.abs() == 1.0 => true, - _ => false, - } + matches!(self, ScrollAmount::Page(count) if count.abs() == 1.0) } pub fn direction(&self) -> ScrollDirection { diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index c59786b1eb387835a21e2c155efaf6acefd4ff4a..3f78fa2f3e9bcd592ba5d2a9f29c42967a27c126 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -300,6 +300,7 @@ impl EditorLspTestContext { self.to_lsp_range(ranges[0].clone()) } + #[expect(clippy::wrong_self_convention, reason = "This is test code")] pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range { let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx)); let start_point = range.start.to_point(&snapshot.buffer_snapshot); @@ -326,6 +327,7 @@ impl EditorLspTestContext { }) } + #[expect(clippy::wrong_self_convention, reason = "This is test code")] pub fn to_lsp(&mut self, offset: usize) -> lsp::Position { let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx)); let point = offset.to_point(&snapshot.buffer_snapshot); diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index 82e95728a1cdb6e23e3defe692f0e1833277c80f..457b62e98ca4cabf83fb379cbaa70f07957ac6b7 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -335,7 +335,7 @@ impl ExampleContext { for message in thread.messages().skip(message_count_before) { messages.push(Message { _role: message.role, - text: message.to_string(), + text: message.to_message_content(), tool_use: thread .tool_uses_for_message(message.id, cx) .into_iter() diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index bbbe54b43fe87e952c22b1a0bd9fba29aa56bb1e..53ce6088c02fbdd05d0025e651fabfbcc77b38bb 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -1192,7 +1192,7 @@ mod test { output.analysis, Some("The model did a good job but there were still compilations errors.".into()) ); - assert_eq!(output.passed, true); + assert!(output.passed); let response = r#" Text around ignored @@ -1212,6 +1212,6 @@ mod test { output.analysis, Some("Failed to compile:\n- Error 1\n- Error 2".into()) ); - assert_eq!(output.passed, false); + assert!(!output.passed); } } diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index aacc5d8795202e8d84c043a881933eabefae36bd..72327179ee08550994854d8b95a190ac94d84cea 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -232,10 +232,10 @@ pub trait Extension: Send + Sync { /// /// To work through a real-world example, take a `cargo run` task and a hypothetical `cargo` locator: /// 1. We may need to modify the task; in this case, it is problematic that `cargo run` spawns a binary. We should turn `cargo run` into a debug scenario with - /// `cargo build` task. This is the decision we make at `dap_locator_create_scenario` scope. + /// `cargo build` task. This is the decision we make at `dap_locator_create_scenario` scope. /// 2. Then, after the build task finishes, we will run `run_dap_locator` of the locator that produced the build task to find the program to be debugged. This function - /// should give us a debugger-agnostic configuration for launching a debug target (that we end up resolving with [`Extension::dap_config_to_scenario`]). It's almost as if the user - /// found the artifact path by themselves. + /// should give us a debugger-agnostic configuration for launching a debug target (that we end up resolving with [`Extension::dap_config_to_scenario`]). It's almost as if the user + /// found the artifact path by themselves. /// /// Note that you're not obliged to use build tasks with locators. Specifically, it is sufficient to provide a debug configuration directly in the return value of /// `dap_locator_create_scenario` if you're able to do that. Make sure to not fill out `build` field in that case, as that will prevent Zed from running second phase of resolution in such case. diff --git a/crates/git/src/status.rs b/crates/git/src/status.rs index 92836042f2b9382728bf153b964a3f4585d9a502..71ca14c5b2c4b82ae7dc21e832a2a07c55de8fc3 100644 --- a/crates/git/src/status.rs +++ b/crates/git/src/status.rs @@ -153,17 +153,11 @@ impl FileStatus { } pub fn is_conflicted(self) -> bool { - match self { - FileStatus::Unmerged { .. } => true, - _ => false, - } + matches!(self, FileStatus::Unmerged { .. }) } pub fn is_ignored(self) -> bool { - match self { - FileStatus::Ignored => true, - _ => false, - } + matches!(self, FileStatus::Ignored) } pub fn has_changes(&self) -> bool { @@ -176,40 +170,31 @@ impl FileStatus { pub fn is_modified(self) -> bool { match self { - FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) { - (StatusCode::Modified, _) | (_, StatusCode::Modified) => true, - _ => false, - }, + FileStatus::Tracked(tracked) => matches!( + (tracked.index_status, tracked.worktree_status), + (StatusCode::Modified, _) | (_, StatusCode::Modified) + ), _ => false, } } pub fn is_created(self) -> bool { match self { - FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) { - (StatusCode::Added, _) | (_, StatusCode::Added) => true, - _ => false, - }, + FileStatus::Tracked(tracked) => matches!( + (tracked.index_status, tracked.worktree_status), + (StatusCode::Added, _) | (_, StatusCode::Added) + ), FileStatus::Untracked => true, _ => false, } } pub fn is_deleted(self) -> bool { - match self { - FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) { - (StatusCode::Deleted, _) | (_, StatusCode::Deleted) => true, - _ => false, - }, - _ => false, - } + matches!(self, FileStatus::Tracked(tracked) if matches!((tracked.index_status, tracked.worktree_status), (StatusCode::Deleted, _) | (_, StatusCode::Deleted))) } pub fn is_untracked(self) -> bool { - match self { - FileStatus::Untracked => true, - _ => false, - } + matches!(self, FileStatus::Untracked) } pub fn summary(self) -> GitSummary { diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index cc1535b7c30c00d7ca80d2bf796d06892ae328d2..c1521004a25f3a62141209bc1e75ae18fb7c0e38 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -1070,8 +1070,7 @@ pub struct ProjectDiffEmptyState { impl RenderOnce for ProjectDiffEmptyState { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let status_against_remote = |ahead_by: usize, behind_by: usize| -> bool { - match self.current_branch { - Some(Branch { + matches!(self.current_branch, Some(Branch { upstream: Some(Upstream { tracking: @@ -1081,9 +1080,7 @@ impl RenderOnce for ProjectDiffEmptyState { .. }), .. - }) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0) => true, - _ => false, - } + }) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0)) }; let change_count = |current_branch: &Branch| -> (usize, usize) { diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index b179076cd5f0da826ca0d5da5e2a5a41cbb5e806..0b824fec34aee7abcb2dbba285265c79b6851d16 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -73,18 +73,18 @@ macro_rules! actions { /// - `name = "ActionName"` overrides the action's name. This must not contain `::`. /// /// - `no_json` causes the `build` method to always error and `action_json_schema` to return `None`, -/// and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`. +/// and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`. /// /// - `no_register` skips registering the action. This is useful for implementing the `Action` trait -/// while not supporting invocation by name or JSON deserialization. +/// while not supporting invocation by name or JSON deserialization. /// /// - `deprecated_aliases = ["editor::SomeAction"]` specifies deprecated old names for the action. -/// These action names should *not* correspond to any actions that are registered. These old names -/// can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will -/// accept these old names and provide warnings. +/// These action names should *not* correspond to any actions that are registered. These old names +/// can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will +/// accept these old names and provide warnings. /// /// - `deprecated = "Message about why this action is deprecation"` specifies a deprecation message. -/// In Zed, the keymap JSON schema will cause this to be displayed as a warning. +/// In Zed, the keymap JSON schema will cause this to be displayed as a warning. /// /// # Manual Implementation /// diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 43adacf7ddb7aa3a08d3a11bc8d0fade3b34a073..a69d9d1e26935d387f3bdfb46c4a1bd616a3b2e6 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -192,6 +192,7 @@ impl TestAppContext { &self.foreground_executor } + #[expect(clippy::wrong_self_convention)] fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> { let mut cx = self.app.borrow_mut(); cx.new(build_entity) @@ -244,7 +245,7 @@ impl TestAppContext { ) .unwrap(); drop(cx); - let cx = VisualTestContext::from_window(*window.deref(), self).as_mut(); + let cx = VisualTestContext::from_window(*window.deref(), self).into_mut(); cx.run_until_parked(); cx } @@ -273,7 +274,7 @@ impl TestAppContext { .unwrap(); drop(cx); let view = window.root(self).unwrap(); - let cx = VisualTestContext::from_window(*window.deref(), self).as_mut(); + let cx = VisualTestContext::from_window(*window.deref(), self).into_mut(); cx.run_until_parked(); // it might be nice to try and cleanup these at the end of each test. @@ -882,7 +883,7 @@ impl VisualTestContext { /// Get an &mut VisualTestContext (which is mostly what you need to pass to other methods). /// This method internally retains the VisualTestContext until the end of the test. - pub fn as_mut(self) -> &'static mut Self { + pub fn into_mut(self) -> &'static mut Self { let ptr = Box::into_raw(Box::new(self)); // safety: on_quit will be called after the test has finished. // the executor will ensure that all tasks related to the test have stopped. diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 639c84c10144310b14a94c2a22b84957b8b09524..cb7329c03fbb2064da0ef5873eef92c2c33d4953 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -905,9 +905,9 @@ mod tests { assert_eq!(background.solid, color); assert_eq!(background.opacity(0.5).solid, color.opacity(0.5)); - assert_eq!(background.is_transparent(), false); + assert!(!background.is_transparent()); background.solid = hsla(0.0, 0.0, 0.0, 0.0); - assert_eq!(background.is_transparent(), true); + assert!(background.is_transparent()); } #[test] @@ -921,7 +921,7 @@ mod tests { assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5)); assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5)); - assert_eq!(background.is_transparent(), false); - assert_eq!(background.opacity(0.0).is_transparent(), true); + assert!(!background.is_transparent()); + assert!(background.opacity(0.0).is_transparent()); } } diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 2de3e23ff716d179bb4e2b55c80650d2b010c38e..ef446a073e1854552350456208fd773b7fa7eae0 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -1641,7 +1641,7 @@ impl Bounds<Pixels> { } /// Convert the bounds from logical pixels to physical pixels - pub fn to_device_pixels(&self, factor: f32) -> Bounds<DevicePixels> { + pub fn to_device_pixels(self, factor: f32) -> Bounds<DevicePixels> { Bounds { origin: point( DevicePixels((self.origin.x.0 * factor).round() as i32), @@ -1957,7 +1957,7 @@ impl Edges<DefiniteLength> { /// assert_eq!(edges_in_pixels.bottom, px(32.0)); // 2 rems /// assert_eq!(edges_in_pixels.left, px(50.0)); // 25% of parent width /// ``` - pub fn to_pixels(&self, parent_size: Size<AbsoluteLength>, rem_size: Pixels) -> Edges<Pixels> { + pub fn to_pixels(self, parent_size: Size<AbsoluteLength>, rem_size: Pixels) -> Edges<Pixels> { Edges { top: self.top.to_pixels(parent_size.height, rem_size), right: self.right.to_pixels(parent_size.width, rem_size), @@ -2027,7 +2027,7 @@ impl Edges<AbsoluteLength> { /// assert_eq!(edges_in_pixels.bottom, px(20.0)); // Already in pixels /// assert_eq!(edges_in_pixels.left, px(32.0)); // 2 rems converted to pixels /// ``` - pub fn to_pixels(&self, rem_size: Pixels) -> Edges<Pixels> { + pub fn to_pixels(self, rem_size: Pixels) -> Edges<Pixels> { Edges { top: self.top.to_pixels(rem_size), right: self.right.to_pixels(rem_size), @@ -2272,7 +2272,7 @@ impl Corners<AbsoluteLength> { /// assert_eq!(corners_in_pixels.bottom_right, Pixels(30.0)); /// assert_eq!(corners_in_pixels.bottom_left, Pixels(32.0)); // 2 rems converted to pixels /// ``` - pub fn to_pixels(&self, rem_size: Pixels) -> Corners<Pixels> { + pub fn to_pixels(self, rem_size: Pixels) -> Corners<Pixels> { Corners { top_left: self.top_left.to_pixels(rem_size), top_right: self.top_right.to_pixels(rem_size), @@ -2858,7 +2858,7 @@ impl DevicePixels { /// let total_bytes = pixels.to_bytes(bytes_per_pixel); /// assert_eq!(total_bytes, 40); // 10 pixels * 4 bytes/pixel = 40 bytes /// ``` - pub fn to_bytes(&self, bytes_per_pixel: u8) -> u32 { + pub fn to_bytes(self, bytes_per_pixel: u8) -> u32 { self.0 as u32 * bytes_per_pixel as u32 } } @@ -3073,8 +3073,8 @@ pub struct Rems(pub f32); impl Rems { /// Convert this Rem value to pixels. - pub fn to_pixels(&self, rem_size: Pixels) -> Pixels { - *self * rem_size + pub fn to_pixels(self, rem_size: Pixels) -> Pixels { + self * rem_size } } @@ -3168,9 +3168,9 @@ impl AbsoluteLength { /// assert_eq!(length_in_pixels.to_pixels(rem_size), Pixels(42.0)); /// assert_eq!(length_in_rems.to_pixels(rem_size), Pixels(32.0)); /// ``` - pub fn to_pixels(&self, rem_size: Pixels) -> Pixels { + pub fn to_pixels(self, rem_size: Pixels) -> Pixels { match self { - AbsoluteLength::Pixels(pixels) => *pixels, + AbsoluteLength::Pixels(pixels) => pixels, AbsoluteLength::Rems(rems) => rems.to_pixels(rem_size), } } @@ -3184,10 +3184,10 @@ impl AbsoluteLength { /// # Returns /// /// Returns the `AbsoluteLength` as `Pixels`. - pub fn to_rems(&self, rem_size: Pixels) -> Rems { + pub fn to_rems(self, rem_size: Pixels) -> Rems { match self { AbsoluteLength::Pixels(pixels) => Rems(pixels.0 / rem_size.0), - AbsoluteLength::Rems(rems) => *rems, + AbsoluteLength::Rems(rems) => rems, } } } @@ -3315,12 +3315,12 @@ impl DefiniteLength { /// assert_eq!(length_in_rems.to_pixels(base_size, rem_size), Pixels(32.0)); /// assert_eq!(length_as_fraction.to_pixels(base_size, rem_size), Pixels(50.0)); /// ``` - pub fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels { + pub fn to_pixels(self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels { match self { DefiniteLength::Absolute(size) => size.to_pixels(rem_size), DefiniteLength::Fraction(fraction) => match base_size { - AbsoluteLength::Pixels(px) => px * *fraction, - AbsoluteLength::Rems(rems) => rems * rem_size * *fraction, + AbsoluteLength::Pixels(px) => px * fraction, + AbsoluteLength::Rems(rems) => rems * rem_size * fraction, }, } } diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index f0ce04a915bba30fff6988ae42b7973bb286b49e..5e4b5fe6e9d221a2915e1f8234eb01e14c177a46 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -172,6 +172,10 @@ pub trait AppContext { type Result<T>; /// Create a new entity in the app context. + #[expect( + clippy::wrong_self_convention, + reason = "`App::new` is an ubiquitous function for creating entities" + )] fn new<T: 'static>( &mut self, build_entity: impl FnOnce(&mut Context<T>) -> T, diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 66f191ca5db6d82e8e38f2628f73ef7380790244..d0078765907daa60600576927e871e01c9fd81f0 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -364,29 +364,29 @@ mod tests { // Ensure `space` results in pending input on the workspace, but not editor let space_workspace = keymap.bindings_for_input(&[space()], &workspace_context()); assert!(space_workspace.0.is_empty()); - assert_eq!(space_workspace.1, true); + assert!(space_workspace.1); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert_eq!(space_editor.1, false); + assert!(!space_editor.1); // Ensure `space w` results in pending input on the workspace, but not editor let space_w_workspace = keymap.bindings_for_input(&space_w, &workspace_context()); assert!(space_w_workspace.0.is_empty()); - assert_eq!(space_w_workspace.1, true); + assert!(space_w_workspace.1); let space_w_editor = keymap.bindings_for_input(&space_w, &editor_workspace_context()); assert!(space_w_editor.0.is_empty()); - assert_eq!(space_w_editor.1, false); + assert!(!space_w_editor.1); // Ensure `space w w` results in the binding in the workspace, but not in the editor let space_w_w_workspace = keymap.bindings_for_input(&space_w_w, &workspace_context()); assert!(!space_w_w_workspace.0.is_empty()); - assert_eq!(space_w_w_workspace.1, false); + assert!(!space_w_w_workspace.1); let space_w_w_editor = keymap.bindings_for_input(&space_w_w, &editor_workspace_context()); assert!(space_w_w_editor.0.is_empty()); - assert_eq!(space_w_w_editor.1, false); + assert!(!space_w_w_editor.1); // Now test what happens if we have another binding defined AFTER the NoAction // that should result in pending @@ -400,7 +400,7 @@ mod tests { let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert_eq!(space_editor.1, true); + assert!(space_editor.1); // Now test what happens if we have another binding defined BEFORE the NoAction // that should result in pending @@ -414,7 +414,7 @@ mod tests { let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert_eq!(space_editor.1, true); + assert!(space_editor.1); // Now test what happens if we have another binding defined at a higher context // that should result in pending @@ -428,7 +428,7 @@ mod tests { let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert_eq!(space_editor.1, true); + assert!(space_editor.1); } #[test] @@ -447,7 +447,7 @@ mod tests { &[KeyContext::parse("editor").unwrap()], ); assert!(result.is_empty()); - assert_eq!(pending, true); + assert!(pending); let bindings = [ KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")), @@ -463,7 +463,7 @@ mod tests { &[KeyContext::parse("editor").unwrap()], ); assert_eq!(result.len(), 1); - assert_eq!(pending, false); + assert!(!pending); } #[test] @@ -482,7 +482,7 @@ mod tests { &[KeyContext::parse("editor").unwrap()], ); assert!(result.is_empty()); - assert_eq!(pending, false); + assert!(!pending); } #[test] @@ -505,7 +505,7 @@ mod tests { ], ); assert_eq!(result.len(), 1); - assert_eq!(pending, false); + assert!(!pending); } #[test] @@ -527,7 +527,7 @@ mod tests { ], ); assert_eq!(result.len(), 0); - assert_eq!(pending, false); + assert!(!pending); } #[test] diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index 6d36cbb4e0951c49caa095f6f2e8a9c6a147da56..729498d153b62b3e250421c82b4bdc05e6c0030f 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -30,11 +30,8 @@ impl Clone for KeyBinding { impl KeyBinding { /// Construct a new keybinding from the given data. Panics on parse error. pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self { - let context_predicate = if let Some(context) = context { - Some(KeyBindingContextPredicate::parse(context).unwrap().into()) - } else { - None - }; + let context_predicate = + context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap() } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 1df8a608f457da356d8f723f743ebbcc58955733..4d2feeaf1d041245b110bf25674fd18145a9a7ee 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -673,7 +673,7 @@ impl PlatformTextSystem for NoopTextSystem { } } let mut runs = Vec::default(); - if glyphs.len() > 0 { + if !glyphs.is_empty() { runs.push(ShapedRun { font_id: FontId(0), glyphs, diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index ed824744a98b97e7410699f8335480ace4f94a7c..399411843be7f3e96d9a59eab4fd79a32b76f8a2 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -667,7 +667,7 @@ pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr"; impl CursorStyle { #[cfg(any(feature = "wayland", feature = "x11"))] - pub(super) fn to_icon_names(&self) -> &'static [&'static str] { + pub(super) fn to_icon_names(self) -> &'static [&'static str] { // Based on cursor names from chromium: // https://github.com/chromium/chromium/blob/d3069cf9c973dc3627fa75f64085c6a86c8f41bf/ui/base/cursor/cursor_factory.cc#L113 match self { @@ -990,21 +990,18 @@ mod tests { #[test] fn test_is_within_click_distance() { let zero = Point::new(px(0.0), px(0.0)); - assert_eq!( - is_within_click_distance(zero, Point::new(px(5.0), px(5.0))), - true - ); - assert_eq!( - is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))), - true - ); - assert_eq!( - is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))), - true - ); - assert_eq!( - is_within_click_distance(zero, Point::new(px(5.0), px(5.1))), - false - ); + assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0)))); + assert!(is_within_click_distance( + zero, + Point::new(px(-4.9), px(5.0)) + )); + assert!(is_within_click_distance( + Point::new(px(3.0), px(2.0)), + Point::new(px(-2.0), px(-2.0)) + )); + assert!(!is_within_click_distance( + zero, + Point::new(px(5.0), px(5.1)) + ),); } } diff --git a/crates/gpui/src/platform/linux/wayland.rs b/crates/gpui/src/platform/linux/wayland.rs index cf73832b11fb1baad08bf5ee3142e461876fbe92..487bc9f38c927609100a238ac4726c2aab3b87b0 100644 --- a/crates/gpui/src/platform/linux/wayland.rs +++ b/crates/gpui/src/platform/linux/wayland.rs @@ -12,7 +12,7 @@ use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1:: use crate::CursorStyle; impl CursorStyle { - pub(super) fn to_shape(&self) -> Shape { + pub(super) fn to_shape(self) -> Shape { match self { CursorStyle::Arrow => Shape::Default, CursorStyle::IBeam => Shape::Text, diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index ce1468335d5bec7f1fefe4ae1faa13318b32feda..7570c58c09e8d5c63091174fa51bc30c54c005e1 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -1139,7 +1139,7 @@ fn update_window(mut state: RefMut<WaylandWindowState>) { } impl WindowDecorations { - fn to_xdg(&self) -> zxdg_toplevel_decoration_v1::Mode { + fn to_xdg(self) -> zxdg_toplevel_decoration_v1::Mode { match self { WindowDecorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide, WindowDecorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide, @@ -1148,7 +1148,7 @@ impl WindowDecorations { } impl ResizeEdge { - fn to_xdg(&self) -> xdg_toplevel::ResizeEdge { + fn to_xdg(self) -> xdg_toplevel::ResizeEdge { match self { ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top, ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight, diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index c33d6fa4621a924d3a808a54cce1c0abe44e3ef4..6af943b31761dc26b2cde4090cad4ce6574dd5c9 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -95,7 +95,7 @@ fn query_render_extent( } impl ResizeEdge { - fn to_moveresize(&self) -> u32 { + fn to_moveresize(self) -> u32 { match self { ResizeEdge::TopLeft => 0, ResizeEdge::Top => 1, diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index cd923a18596c249d5435e662974ad8ee4097b8e7..4425d4fe24c91a0bcf840b59de139ecf4f8187b0 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1090,7 +1090,7 @@ impl PlatformWindow for MacWindow { NSView::removeFromSuperview(blur_view); this.blurred_view = None; } - } else if this.blurred_view == None { + } else if this.blurred_view.is_none() { let content_view = this.native_window.contentView(); let frame = NSView::bounds(content_view); let mut blur_view: id = msg_send![BLURRED_VIEW_CLASS, alloc]; diff --git a/crates/gpui/src/tab_stop.rs b/crates/gpui/src/tab_stop.rs index 30d24e85e79744721e4fa8d07ad7cbca9ccb57ad..c4d2fda6e9a9c3e0adfb2d02cf5c372869d42751 100644 --- a/crates/gpui/src/tab_stop.rs +++ b/crates/gpui/src/tab_stop.rs @@ -45,27 +45,18 @@ impl TabHandles { }) .unwrap_or_default(); - if let Some(next_handle) = self.handles.get(next_ix) { - Some(next_handle.clone()) - } else { - None - } + self.handles.get(next_ix).cloned() } pub(crate) fn prev(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> { let ix = self.current_index(focused_id).unwrap_or_default(); - let prev_ix; - if ix == 0 { - prev_ix = self.handles.len().saturating_sub(1); + let prev_ix = if ix == 0 { + self.handles.len().saturating_sub(1) } else { - prev_ix = ix.saturating_sub(1); - } + ix.saturating_sub(1) + }; - if let Some(prev_handle) = self.handles.get(prev_ix) { - Some(prev_handle.clone()) - } else { - None - } + self.handles.get(prev_ix).cloned() } } diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index 3a58af67052d06f108b4b9c87d52fc358405466e..0f1365be77ec221d9061f588f84ff6acab3c32ab 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -172,7 +172,7 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { /// - `#[gpui::test(iterations = 5)]` runs five times, providing as seed the values in the range `0..5`. /// - `#[gpui::test(retries = 3)]` runs up to four times if it fails to try and make it pass. /// - `#[gpui::test(on_failure = "crate::test::report_failure")]` will call the specified function after the -/// tests fail so that you can write out more detail about the failure. +/// tests fail so that you can write out more detail about the failure. /// /// You can combine `iterations = ...` with `seeds(...)`: /// - `#[gpui::test(iterations = 5, seed = 10)]` is equivalent to `#[gpui::test(seeds(0, 1, 2, 3, 4, 10))]`. diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 589fc68e99cd05e0a11c776895c2e8f06b610199..be68dc1e9f22f8ae73fb72262e26a136cbd73399 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -49,7 +49,7 @@ impl LanguageName { pub fn from_proto(s: String) -> Self { Self(SharedString::from(s)) } - pub fn to_proto(self) -> String { + pub fn to_proto(&self) -> String { self.0.to_string() } pub fn lsp_id(&self) -> String { diff --git a/crates/language_model/src/role.rs b/crates/language_model/src/role.rs index 953dfa6fdff91c61a3a444076fd768f260b882c5..4b47ef36dd564e5950ce7d42a7e4f9263f3998b7 100644 --- a/crates/language_model/src/role.rs +++ b/crates/language_model/src/role.rs @@ -19,7 +19,7 @@ impl Role { } } - pub fn to_proto(&self) -> proto::LanguageModelRole { + pub fn to_proto(self) -> proto::LanguageModelRole { match self { Role::User => proto::LanguageModelRole::LanguageModelUser, Role::Assistant => proto::LanguageModelRole::LanguageModelAssistant, diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index 88e3e12f024805f4e2834e169acefdeac324818e..2180a049d03daf5fcd2a60e1f1f7ddd0013c7d1f 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -28,7 +28,7 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Opt let mut parser = tree_sitter::Parser::new(); parser.set_language(&tree_sitter_json::LANGUAGE.into())?; let syntax_tree = parser - .parse(&text, None) + .parse(text, None) .context("failed to parse settings")?; let mut cursor = tree_sitter::QueryCursor::new(); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 162e3bea78352c9eb98d6e7b6057bbe922c8b03c..0cc2f654eac6dab790731cd1ae4f628797729984 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -7149,7 +7149,7 @@ impl ExcerptId { Self(usize::MAX) } - pub fn to_proto(&self) -> u64 { + pub fn to_proto(self) -> u64 { self.0 as _ } diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index 47a0f12c0634dbde48d015e4f577519babc67b34..aab0354c9696f8bcdde5fd4bb00bd3651ac4b888 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -41,7 +41,7 @@ pub fn remote_server_dir_relative() -> &'static Path { /// # Arguments /// /// * `dir` - The path to use as the custom data directory. This will be used as the base -/// directory for all user data, including databases, extensions, and logs. +/// directory for all user data, including databases, extensions, and logs. /// /// # Returns /// diff --git a/crates/project/src/debugger.rs b/crates/project/src/debugger.rs index 6c22468040097768688d93cde0720320a9e45be9..0bf6a0d61b792bd747992a821adc82150d93c8bf 100644 --- a/crates/project/src/debugger.rs +++ b/crates/project/src/debugger.rs @@ -6,9 +6,9 @@ //! //! There are few reasons for this divide: //! - Breakpoints persist across debug sessions and they're not really specific to any particular session. Sure, we have to send protocol messages for them -//! (so they're a "thing" in the protocol), but we also want to set them before any session starts up. +//! (so they're a "thing" in the protocol), but we also want to set them before any session starts up. //! - Debug clients are doing the heavy lifting, and this is where UI grabs all of it's data from. They also rely on breakpoint store during initialization to obtain -//! current set of breakpoints. +//! current set of breakpoints. //! - Since DAP store knows about all of the available debug sessions, it is responsible for routing RPC requests to sessions. It also knows how to find adapters for particular kind of session. pub mod breakpoint_store; diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 00fcc7e69fc33a47134c3bc884e2e520ffc55023..343ee83ccbe1581b551045abd769169cb410c3f3 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -904,7 +904,7 @@ impl BreakpointState { } #[inline] - pub fn to_int(&self) -> i32 { + pub fn to_int(self) -> i32 { match self { BreakpointState::Enabled => 0, BreakpointState::Disabled => 1, diff --git a/crates/project/src/debugger/memory.rs b/crates/project/src/debugger/memory.rs index a8729a8ff45cee346409d1b1ee09791a20243544..42ad64e6880ba653c6c95cb13f0e6bcc23c9bdae 100644 --- a/crates/project/src/debugger/memory.rs +++ b/crates/project/src/debugger/memory.rs @@ -3,6 +3,7 @@ //! Each byte in memory can either be mapped or unmapped. We try to mimic that twofold: //! - We assume that the memory is divided into pages of a fixed size. //! - We assume that each page can be either mapped or unmapped. +//! //! These two assumptions drive the shape of the memory representation. //! In particular, we want the unmapped pages to be represented without allocating any memory, as *most* //! of the memory in a program space is usually unmapped. @@ -165,8 +166,8 @@ impl Memory { /// - If it succeeds/fails wholesale, cool; we have no unknown memory regions in this page. /// - If it succeeds partially, we know # of mapped bytes. /// We might also know the # of unmapped bytes. -/// However, we're still unsure about what's *after* the unreadable region. /// +/// However, we're still unsure about what's *after* the unreadable region. /// This is where this builder comes in. It lets us track the state of figuring out contents of a single page. pub(super) struct MemoryPageBuilder { chunks: MappedPageContents, diff --git a/crates/project/src/git_store/conflict_set.rs b/crates/project/src/git_store/conflict_set.rs index 27b191f65f896e6488a4d9c52f37e9426cac1c46..9d7bd26a92960774b827ca6c12848050b5afbb8b 100644 --- a/crates/project/src/git_store/conflict_set.rs +++ b/crates/project/src/git_store/conflict_set.rs @@ -653,7 +653,7 @@ mod tests { cx.run_until_parked(); conflict_set.update(cx, |conflict_set, _| { - assert_eq!(conflict_set.has_conflict, false); + assert!(!conflict_set.has_conflict); assert_eq!(conflict_set.snapshot.conflicts.len(), 0); }); diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index 4594e8d14061a651fd69c4f20557216445e0db4e..9eadaeac824756bd3b128ef3e0118ceef1c05680 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -199,7 +199,7 @@ pub struct GitEntryRef<'a> { } impl GitEntryRef<'_> { - pub fn to_owned(&self) -> GitEntry { + pub fn to_owned(self) -> GitEntry { GitEntry { entry: self.entry.clone(), git_summary: self.git_summary, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 04b14ae06e20a8c3f9f2cc0b9a62e6e495836b72..aa2398e29b0312c0f13f418e3a279cc745bdbff3 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -5,7 +5,7 @@ //! This module is split up into three distinct parts: //! - [`LocalLspStore`], which is ran on the host machine (either project host or SSH host), that manages the lifecycle of language servers. //! - [`RemoteLspStore`], which is ran on the remote machine (project guests) which is mostly about passing through the requests via RPC. -//! The remote stores don't really care about which language server they're running against - they don't usually get to decide which language server is going to responsible for handling their request. +//! The remote stores don't really care about which language server they're running against - they don't usually get to decide which language server is going to responsible for handling their request. //! - [`LspStore`], which unifies the two under one consistent interface for interacting with language servers. //! //! Most of the interesting work happens at the local layer, as bulk of the complexity is with managing the lifecycle of language servers. The actual implementation of the LSP protocol is handled by [`lsp`] crate. @@ -12691,7 +12691,7 @@ impl DiagnosticSummary { } pub fn to_proto( - &self, + self, language_server_id: LanguageServerId, path: &Path, ) -> proto::DiagnosticSummary { diff --git a/crates/project/src/manifest_tree/path_trie.rs b/crates/project/src/manifest_tree/path_trie.rs index 16110463ac096ae7f29ab1e46a51cfde2629774f..9cebfda25c69fa35b06cefe9ec744b5e6152a820 100644 --- a/crates/project/src/manifest_tree/path_trie.rs +++ b/crates/project/src/manifest_tree/path_trie.rs @@ -22,9 +22,9 @@ pub(super) struct RootPathTrie<Label> { /// Label presence is a marker that allows to optimize searches within [RootPathTrie]; node label can be: /// - Present; we know there's definitely a project root at this node. /// - Known Absent - we know there's definitely no project root at this node and none of it's ancestors are Present (descendants can be present though!). -/// The distinction is there to optimize searching; when we encounter a node with unknown status, we don't need to look at it's full path -/// to the root of the worktree; it's sufficient to explore only the path between last node with a KnownAbsent state and the directory of a path, since we run searches -/// from the leaf up to the root of the worktree. +/// The distinction is there to optimize searching; when we encounter a node with unknown status, we don't need to look at it's full path +/// to the root of the worktree; it's sufficient to explore only the path between last node with a KnownAbsent state and the directory of a path, since we run searches +/// from the leaf up to the root of the worktree. /// /// In practical terms, it means that by storing label presence we don't need to do a project discovery on a given folder more than once /// (unless the node is invalidated, which can happen when FS entries are renamed/removed). diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f07ee13866511ad643029935405c1b050b846106..9cd83647acd257c5c1d5498d64d32c198b79491d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4329,7 +4329,7 @@ impl Project { /// # Arguments /// /// * `path` - A full path that starts with a worktree root name, or alternatively a - /// relative path within a visible worktree. + /// relative path within a visible worktree. /// * `cx` - A reference to the `AppContext`. /// /// # Returns @@ -5508,7 +5508,7 @@ mod disable_ai_settings_tests { project: &[], }; let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert_eq!(settings.disable_ai, false, "Default should allow AI"); + assert!(!settings.disable_ai, "Default should allow AI"); // Test 2: Global true, local false -> still disabled (local cannot re-enable) let global_true = Some(true); @@ -5525,8 +5525,8 @@ mod disable_ai_settings_tests { project: &[&local_false], }; let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert_eq!( - settings.disable_ai, true, + assert!( + settings.disable_ai, "Local false cannot override global true" ); @@ -5545,10 +5545,7 @@ mod disable_ai_settings_tests { project: &[&local_true], }; let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert_eq!( - settings.disable_ai, true, - "Local true can override global false" - ); + assert!(settings.disable_ai, "Local true can override global false"); // Test 4: Server can only make more restrictive (set to true) let user_false = Some(false); @@ -5565,8 +5562,8 @@ mod disable_ai_settings_tests { project: &[], }; let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert_eq!( - settings.disable_ai, true, + assert!( + settings.disable_ai, "Server can set to true even if user is false" ); @@ -5585,8 +5582,8 @@ mod disable_ai_settings_tests { project: &[], }; let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert_eq!( - settings.disable_ai, true, + assert!( + settings.disable_ai, "Server false cannot override user true" ); @@ -5607,10 +5604,7 @@ mod disable_ai_settings_tests { project: &[&local_false3, &local_true2, &local_false4], }; let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert_eq!( - settings.disable_ai, true, - "Any local true should disable AI" - ); + assert!(settings.disable_ai, "Any local true should disable AI"); // Test 7: All three sources can independently disable AI let user_false2 = Some(false); @@ -5628,8 +5622,8 @@ mod disable_ai_settings_tests { project: &[&local_true3], }; let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert_eq!( - settings.disable_ai, true, + assert!( + settings.disable_ai, "Local can disable even if user and server are false" ); }); diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index eb1e3828e902b1c75f7197be0773a67f5c5056d6..70eb6d34f8d07506dabee7f2f41fcf1ba89565dd 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -4123,7 +4123,7 @@ async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) { }) .unwrap() .await - .to_included() + .into_included() .unwrap(); cx.executor().run_until_parked(); @@ -5918,7 +5918,7 @@ async fn test_create_entry(cx: &mut gpui::TestAppContext) { }) .await .unwrap() - .to_included() + .into_included() .unwrap(); // Can't create paths outside the project diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index dc92ee8c70c30b75737322be628ea1bbb7662dfe..bb612ac47594867653c1c80eeb14c81daf51058b 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -2515,7 +2515,7 @@ impl ProjectPanel { if clip_is_cut { // Convert the clipboard cut entry to a copy entry after the first paste. - self.clipboard = self.clipboard.take().map(ClipboardEntry::to_copy_entry); + self.clipboard = self.clipboard.take().map(ClipboardEntry::into_copy_entry); } self.expand_entry(worktree_id, entry.id, cx); @@ -5709,7 +5709,7 @@ impl ClipboardEntry { } } - fn to_copy_entry(self) -> Self { + fn into_copy_entry(self) -> Self { match self { ClipboardEntry::Copied(_) => self, ClipboardEntry::Cut(entries) => ClipboardEntry::Copied(entries), diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 7173bc9b3b18e656323e7908ab7008769cc8e9e8..fddf47660dc49f96d1507c8f45716b559cce1026 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -1452,7 +1452,7 @@ impl RemoteConnection for SshRemoteConnection { .arg(format!( "{}:{}", self.socket.connection_options.scp_url(), - dest_path.to_string() + dest_path )) .output(); @@ -1836,11 +1836,7 @@ impl SshRemoteConnection { })??; let tmp_path_gz = RemotePathBuf::new( - PathBuf::from(format!( - "{}-download-{}.gz", - dst_path.to_string(), - std::process::id() - )), + PathBuf::from(format!("{}-download-{}.gz", dst_path, std::process::id())), self.ssh_path_style, ); if !self.socket.connection_options.upload_binary_over_ssh @@ -2036,7 +2032,7 @@ impl SshRemoteConnection { .arg(format!( "{}:{}", self.socket.connection_options.scp_url(), - dest_path.to_string() + dest_path )) .output() .await?; diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 514e5ce4c0e083bc8f34a2db067ca2ee9a60f267..69fae7f399e506b26f005d30fdda6b7240df3e0b 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -1207,7 +1207,7 @@ async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestA }) .await .unwrap() - .to_included() + .into_included() .unwrap(); cx.run_until_parked(); diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 3352b317cbff3e332ee1ab7e0439acd59f71f48a..4ce133cbb102ab9c6137417db9f30146f31211da 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -84,7 +84,7 @@ fn init_logging_server(log_file_path: PathBuf) -> Result<Receiver<Vec<u8>>> { fn flush(&mut self) -> std::io::Result<()> { self.channel .send_blocking(self.buffer.clone()) - .map_err(|error| std::io::Error::new(std::io::ErrorKind::Other, error))?; + .map_err(std::io::Error::other)?; self.buffer.clear(); self.file.flush() } diff --git a/crates/repl/src/kernels/mod.rs b/crates/repl/src/kernels/mod.rs index 3c3b766612c869bffb7a18608671f3daafb75df6..52188a39c48f5fc07a1f4a64949a82d205f75f9f 100644 --- a/crates/repl/src/kernels/mod.rs +++ b/crates/repl/src/kernels/mod.rs @@ -169,10 +169,7 @@ pub enum KernelStatus { impl KernelStatus { pub fn is_connected(&self) -> bool { - match self { - KernelStatus::Idle | KernelStatus::Busy => true, - _ => false, - } + matches!(self, KernelStatus::Idle | KernelStatus::Busy) } } diff --git a/crates/reqwest_client/src/reqwest_client.rs b/crates/reqwest_client/src/reqwest_client.rs index 9053f4e452790dfe06fd4f3559b32be72740d56b..d0d25bdf258e12a28bef1f29e608532075bbed7b 100644 --- a/crates/reqwest_client/src/reqwest_client.rs +++ b/crates/reqwest_client/src/reqwest_client.rs @@ -264,7 +264,7 @@ impl http_client::HttpClient for ReqwestClient { let bytes = response .bytes_stream() - .map_err(|e| futures::io::Error::new(futures::io::ErrorKind::Other, e)) + .map_err(futures::io::Error::other) .into_async_read(); let body = http_client::AsyncBody::from_reader(bytes); diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index e3c7d6f750be176eab06e37aefb78b682beefdcc..379daa4224d5cc930f1411c415aa471088bb53ae 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -92,7 +92,7 @@ impl Into<Chunk> for ChunkSlice<'_> { impl<'a> ChunkSlice<'a> { #[inline(always)] - pub fn is_empty(self) -> bool { + pub fn is_empty(&self) -> bool { self.text.is_empty() } diff --git a/crates/rpc/src/conn.rs b/crates/rpc/src/conn.rs index 0a41570fcc81f7f099ca76c8fea70fceeaf328fd..78db80e3983cecdcb1e39a1632dcca8e2b73bf9b 100644 --- a/crates/rpc/src/conn.rs +++ b/crates/rpc/src/conn.rs @@ -56,7 +56,7 @@ impl Connection { ) { use anyhow::anyhow; use futures::channel::mpsc; - use std::io::{Error, ErrorKind}; + use std::io::Error; let (tx, rx) = mpsc::unbounded::<WebSocketMessage>(); @@ -71,7 +71,7 @@ impl Connection { // Writes to a half-open TCP connection will error. if killed.load(SeqCst) { - std::io::Result::Err(Error::new(ErrorKind::Other, "connection lost"))?; + std::io::Result::Err(Error::other("connection lost"))?; } Ok(msg) diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 1afbc2c23b6ff74379a4fea6a6a375890584c6e4..65e59fd5de6a10ebc0c477f8933d96128048f6b7 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -116,8 +116,8 @@ impl SearchOption { } } - pub fn to_toggle_action(&self) -> &'static dyn Action { - match *self { + pub fn to_toggle_action(self) -> &'static dyn Action { + match self { SearchOption::WholeWord => &ToggleWholeWord, SearchOption::CaseSensitive => &ToggleCaseSensitive, SearchOption::IncludeIgnored => &ToggleIncludeIgnored, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index afd4ea08907654c9a4fc5edfbfb90bd2f87a3285..b73ab9ae95ae75b3ac9b7a58b663a79235261b5b 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -50,11 +50,11 @@ impl WorktreeId { Self(id as usize) } - pub fn to_proto(&self) -> u64 { + pub fn to_proto(self) -> u64 { self.0 as u64 } - pub fn to_usize(&self) -> usize { + pub fn to_usize(self) -> usize { self.0 } } diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index 5ed29fd733d6e72d221ee7a00cc7c2d823f17b45..770312bafc3a9249c83f3bc4eca130afd59da95f 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -28,7 +28,7 @@ impl ShellKind { } } - fn to_shell_variable(&self, input: &str) -> String { + fn to_shell_variable(self, input: &str) -> String { match self { Self::Powershell => Self::to_powershell_variable(input), Self::Cmd => Self::to_cmd_variable(input), diff --git a/crates/terminal_view/src/terminal_slash_command.rs b/crates/terminal_view/src/terminal_slash_command.rs index ac86eef2bc4e17f4fc9475a44cfb3ad718200882..13c2cef48c3596d77c1bc7f00587f17dfc1c75e5 100644 --- a/crates/terminal_view/src/terminal_slash_command.rs +++ b/crates/terminal_view/src/terminal_slash_command.rs @@ -104,7 +104,7 @@ impl SlashCommand for TerminalSlashCommand { }], run_commands_in_text: false, } - .to_event_stream())) + .into_event_stream())) } } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 956bcebfd0061c9693fed09d66a9f8389709cf84..0c16e3fb9d6b39dad0ca9bd083724c36161db742 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -2192,7 +2192,7 @@ mod tests { }) .await .unwrap() - .to_included() + .into_included() .unwrap(); (wt, entry) diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 31bf76e84341dd38a6c4c0d7e804034b46774abf..477fc57b22f9178edc2123a76fcaf68701f8fb4d 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -582,13 +582,9 @@ impl RenderOnce for ButtonLike { .when_some(self.width, |this, width| { this.w(width).justify_center().text_center() }) - .when( - match self.style { - ButtonStyle::Outlined => true, - _ => false, - }, - |this| this.border_1(), - ) + .when(matches!(self.style, ButtonStyle::Outlined), |this| { + this.border_1() + }) .when_some(self.rounding, |this, rounding| match rounding { ButtonLikeRounding::All => this.rounded_sm(), ButtonLikeRounding::Left => this.rounded_l_sm(), diff --git a/crates/ui/src/utils/format_distance.rs b/crates/ui/src/utils/format_distance.rs index 213d9c8b4c2ef25594a10d778ef132fecac607bb..a8f27f01dad22ab59481ef75c82ee07d736237d9 100644 --- a/crates/ui/src/utils/format_distance.rs +++ b/crates/ui/src/utils/format_distance.rs @@ -13,9 +13,9 @@ impl DateTimeType { /// /// If the [`DateTimeType`] is already a [`NaiveDateTime`], it will be returned as is. /// If the [`DateTimeType`] is a [`DateTime<Local>`], it will be converted to a [`NaiveDateTime`]. - pub fn to_naive(&self) -> NaiveDateTime { + pub fn to_naive(self) -> NaiveDateTime { match self { - DateTimeType::Naive(naive) => *naive, + DateTimeType::Naive(naive) => naive, DateTimeType::Local(local) => local.naive_local(), } } diff --git a/crates/util/src/archive.rs b/crates/util/src/archive.rs index 3e4d281c29902b682d14886431bc9387baf9cee3..9b58b16bedb2114503a3d87756ae4b2c4d460190 100644 --- a/crates/util/src/archive.rs +++ b/crates/util/src/archive.rs @@ -154,7 +154,7 @@ mod tests { let mut builder = ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate); use std::os::unix::fs::PermissionsExt; - let metadata = std::fs::metadata(&path)?; + let metadata = std::fs::metadata(path)?; let perms = metadata.permissions().mode() as u16; builder = builder.unix_permissions(perms); writer.write_entry_whole(builder, &data).await?; diff --git a/crates/util/src/markdown.rs b/crates/util/src/markdown.rs index 7e66ed7bae3806629f3d21669ef27398ad21bc17..303dbe0cf59d868209c4f350fa88a0b156f66464 100644 --- a/crates/util/src/markdown.rs +++ b/crates/util/src/markdown.rs @@ -23,7 +23,7 @@ impl Display for MarkdownString { /// the other characters involved are escaped: /// /// * `!`, `]`, `(`, and `)` are used in link syntax, but `[` is escaped so these are parsed as -/// plaintext. +/// plaintext. /// /// * `;` is used in HTML entity syntax, but `&` is escaped, so they are parsed as plaintext. /// diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 292ec4874c96f48f417a5f15b4c9cdd146c71afc..b4301203146c876db3f9832b059d15d4321600e0 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -2,6 +2,7 @@ use globset::{Glob, GlobSet, GlobSetBuilder}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; +use std::fmt::{Display, Formatter}; use std::path::StripPrefixError; use std::sync::{Arc, OnceLock}; use std::{ @@ -113,10 +114,6 @@ impl SanitizedPath { &self.0 } - pub fn to_string(&self) -> String { - self.0.to_string_lossy().to_string() - } - pub fn to_glob_string(&self) -> String { #[cfg(target_os = "windows")] { @@ -137,6 +134,12 @@ impl SanitizedPath { } } +impl Display for SanitizedPath { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.display()) + } +} + impl From<SanitizedPath> for Arc<Path> { fn from(sanitized_path: SanitizedPath) -> Self { sanitized_path.0 @@ -220,12 +223,8 @@ impl RemotePathBuf { Self::new(path_buf, style) } - pub fn to_string(&self) -> String { - self.string.clone() - } - #[cfg(target_os = "windows")] - pub fn to_proto(self) -> String { + pub fn to_proto(&self) -> String { match self.path_style() { PathStyle::Posix => self.to_string(), PathStyle::Windows => self.inner.to_string_lossy().replace('\\', "/"), @@ -233,7 +232,7 @@ impl RemotePathBuf { } #[cfg(not(target_os = "windows"))] - pub fn to_proto(self) -> String { + pub fn to_proto(&self) -> String { match self.path_style() { PathStyle::Posix => self.inner.to_string_lossy().to_string(), PathStyle::Windows => self.to_string(), @@ -255,6 +254,12 @@ impl RemotePathBuf { } } +impl Display for RemotePathBuf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.string) + } +} + /// A delimiter to use in `path_query:row_number:column_number` strings parsing. pub const FILE_ROW_COLUMN_DELIMITER: char = ':'; diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 92e3c972650ecf550a3f2313cbf95dad43eb7526..350ffd666b2486cadc72d6fbedf198dbde61cf95 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -816,10 +816,7 @@ impl Motion { } fn skip_exclusive_special_case(&self) -> bool { - match self { - Motion::WrappingLeft | Motion::WrappingRight => true, - _ => false, - } + matches!(self, Motion::WrappingLeft | Motion::WrappingRight) } pub(crate) fn push_to_jump_list(&self) -> bool { @@ -4099,7 +4096,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇhe quick brown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick bˇrown fox @@ -4109,7 +4106,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick brown foˇx @@ -4119,7 +4116,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇ jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); } #[gpui::test] @@ -4134,7 +4131,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇbrown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick bˇrown fox @@ -4144,7 +4141,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" the quickˇown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick brown foˇx @@ -4154,7 +4151,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" the quicˇk jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" ˇthe quick brown fox @@ -4164,7 +4161,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇ fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" ˇthe quick brown fox @@ -4174,7 +4171,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇuick brown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); } #[gpui::test] @@ -4189,7 +4186,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" the quick brown foˇx jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" ˇthe quick brown fox @@ -4199,7 +4196,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇx jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); } #[gpui::test] @@ -4215,7 +4212,7 @@ mod test { the quick brown fox ˇthe quick brown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick bˇrown fox @@ -4226,7 +4223,7 @@ mod test { the quick brˇrown fox jumped overown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick brown foˇx @@ -4237,7 +4234,7 @@ mod test { the quick brown foxˇx jumped over the la jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick brown fox @@ -4248,7 +4245,7 @@ mod test { thˇhe quick brown fox je quick brown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); } #[gpui::test] @@ -4263,7 +4260,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇe quick brown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick bˇrown fox @@ -4273,7 +4270,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" the quick bˇn fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick brown foˇx @@ -4282,6 +4279,6 @@ mod test { cx.simulate_shared_keystrokes("d v e").await; cx.shared_state().await.assert_eq(indoc! {" the quick brown foˇd over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 81c1a6b0b388227c236449de36f43f6b0287e48b..11d6d89bac6722fb577310af856de9f327705dd2 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1734,10 +1734,7 @@ impl Vim { editor.set_autoindent(vim.should_autoindent()); editor.selections.line_mode = matches!(vim.mode, Mode::VisualLine); - let hide_edit_predictions = match vim.mode { - Mode::Insert | Mode::Replace => false, - _ => true, - }; + let hide_edit_predictions = !matches!(vim.mode, Mode::Insert | Mode::Replace); editor.set_edit_predictions_hidden_for_vim_mode(hide_edit_predictions, window, cx); }); cx.notify() diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index d38f3cac3d3a77d4b9c92cb97e8839907d403f2e..b12fd137677d886f8bcf58c2e32c4dc3c46db7b5 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -5509,7 +5509,7 @@ impl ProjectEntryId { Self(id as usize) } - pub fn to_proto(&self) -> u64 { + pub fn to_proto(self) -> u64 { self.0 as u64 } @@ -5517,14 +5517,14 @@ impl ProjectEntryId { ProjectEntryId(id) } - pub fn to_usize(&self) -> usize { + pub fn to_usize(self) -> usize { self.0 } } #[cfg(any(test, feature = "test-support"))] impl CreatedEntry { - pub fn to_included(self) -> Option<Entry> { + pub fn into_included(self) -> Option<Entry> { match self { CreatedEntry::Included(entry) => Some(entry), CreatedEntry::Excluded { .. } => None, diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index d4c309e5bc849696898da4812d5145ac2d9a70bf..ca9debb64784b34fc1b5c49befc69d061f5b2c9f 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -1274,7 +1274,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { }) .await .unwrap() - .to_included() + .into_included() .unwrap(); assert!(entry.is_dir()); @@ -1323,7 +1323,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { }) .await .unwrap() - .to_included() + .into_included() .unwrap(); assert!(entry.is_file()); @@ -1357,7 +1357,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { }) .await .unwrap() - .to_included() + .into_included() .unwrap(); assert!(entry.is_file()); @@ -1377,7 +1377,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { }) .await .unwrap() - .to_included() + .into_included() .unwrap(); assert!(entry.is_file()); @@ -1395,7 +1395,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { }) .await .unwrap() - .to_included() + .into_included() .unwrap(); assert!(entry.is_file()); @@ -1726,7 +1726,7 @@ fn randomly_mutate_worktree( ); let task = worktree.rename_entry(entry.id, new_path, cx); cx.background_spawn(async move { - task.await?.to_included().unwrap(); + task.await?.into_included().unwrap(); Ok(()) }) } diff --git a/crates/x_ai/src/x_ai.rs b/crates/x_ai/src/x_ai.rs index 23cd5b9320fafe290fe7d4f509f2aef7ef93e6ba..569503784c68d2676de24d369cb36774ee48f054 100644 --- a/crates/x_ai/src/x_ai.rs +++ b/crates/x_ai/src/x_ai.rs @@ -122,9 +122,6 @@ impl Model { } pub fn supports_images(&self) -> bool { - match self { - Self::Grok2Vision => true, - _ => false, - } + matches!(self, Self::Grok2Vision) } } From 7bdc99abc15327db75baab28957cba7e7e9fa122 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Wed, 20 Aug 2025 13:20:13 +0300 Subject: [PATCH 182/744] Fix `clippy::redundant_clone` lint violations (#36558) This removes around 900 unnecessary clones, ranging from cloning a few ints all the way to large data structures and images. A lot of these were fixed using `cargo clippy --fix --workspace --all-targets`, however it often breaks other lints and needs to be run again. This was then followed up with some manual fixing. I understand this is a large diff, but all the changes are pretty trivial. Rust is doing some heavy lifting here for us. Once I get it up to speed with main, I'd appreciate this getting merged rather sooner than later. Release Notes: - N/A --- Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 4 +- crates/acp_thread/src/diff.rs | 3 +- crates/action_log/src/action_log.rs | 10 +-- crates/agent/src/agent_profile.rs | 6 +- crates/agent/src/context_server_tool.rs | 6 +- crates/agent/src/thread.rs | 16 ++-- crates/agent2/src/agent.rs | 6 +- crates/agent2/src/db.rs | 6 +- crates/agent2/src/tests/mod.rs | 4 +- crates/agent2/src/thread.rs | 2 +- .../src/tools/context_server_registry.rs | 6 +- crates/agent2/src/tools/edit_file_tool.rs | 2 +- crates/agent2/src/tools/grep_tool.rs | 8 +- crates/agent2/src/tools/terminal_tool.rs | 2 +- crates/agent_servers/src/claude.rs | 2 +- crates/agent_servers/src/claude/tools.rs | 2 +- .../agent_ui/src/acp/completion_provider.rs | 38 ++++---- crates/agent_ui/src/acp/message_editor.rs | 19 ++-- crates/agent_ui/src/acp/thread_view.rs | 12 ++- crates/agent_ui/src/active_thread.rs | 12 ++- crates/agent_ui/src/agent_configuration.rs | 10 +-- .../configure_context_server_modal.rs | 1 - .../manage_profiles_modal.rs | 2 +- crates/agent_ui/src/agent_diff.rs | 10 +-- crates/agent_ui/src/agent_model_selector.rs | 6 +- crates/agent_ui/src/agent_panel.rs | 24 ++--- crates/agent_ui/src/agent_ui.rs | 8 +- crates/agent_ui/src/buffer_codegen.rs | 12 +-- crates/agent_ui/src/context_picker.rs | 9 +- .../src/context_picker/completion_provider.rs | 41 ++++----- crates/agent_ui/src/inline_assistant.rs | 2 +- .../agent_ui/src/language_model_selector.rs | 4 +- crates/agent_ui/src/message_editor.rs | 5 +- crates/agent_ui/src/profile_selector.rs | 4 +- crates/agent_ui/src/slash_command.rs | 4 +- .../agent_ui/src/terminal_inline_assistant.rs | 2 +- crates/agent_ui/src/text_thread_editor.rs | 8 +- crates/agent_ui/src/ui/context_pill.rs | 4 +- .../src/agent_api_keys_onboarding.rs | 2 +- .../src/agent_panel_onboarding_content.rs | 2 +- .../src/assistant_context.rs | 2 +- .../src/assistant_context_tests.rs | 4 +- crates/assistant_context/src/context_store.rs | 2 +- .../src/diagnostics_command.rs | 2 +- .../src/prompt_command.rs | 2 +- .../assistant_tools/src/edit_agent/evals.rs | 7 +- .../src/edit_agent/streaming_fuzzy_matcher.rs | 16 ++-- crates/assistant_tools/src/edit_file_tool.rs | 6 +- crates/assistant_tools/src/grep_tool.rs | 6 +- crates/assistant_tools/src/terminal_tool.rs | 2 +- .../src/ui/tool_call_card_header.rs | 9 +- crates/assistant_tools/src/web_search_tool.rs | 5 +- crates/auto_update_ui/src/auto_update_ui.rs | 2 +- crates/buffer_diff/src/buffer_diff.rs | 12 +-- crates/channel/src/channel_store_tests.rs | 2 +- crates/cli/src/main.rs | 2 +- crates/client/src/client.rs | 9 +- crates/client/src/telemetry.rs | 8 +- crates/collab/src/api/events.rs | 2 +- crates/collab/src/auth.rs | 2 +- crates/collab/src/db/tests/embedding_tests.rs | 4 +- crates/collab/src/rpc.rs | 10 +-- crates/collab/src/tests/editor_tests.rs | 18 +--- .../src/tests/random_channel_buffer_tests.rs | 2 +- .../random_project_collaboration_tests.rs | 2 +- crates/collab/src/tests/test_server.rs | 4 +- crates/collab_ui/src/channel_view.rs | 2 +- crates/collab_ui/src/chat_panel.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 6 +- crates/collab_ui/src/notification_panel.rs | 2 +- crates/command_palette/src/command_palette.rs | 2 +- crates/component/src/component_layout.rs | 2 +- crates/context_server/src/listener.rs | 1 - crates/copilot/src/copilot.rs | 5 +- .../src/copilot_completion_provider.rs | 2 +- crates/debugger_tools/src/dap_log.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 10 +-- crates/debugger_ui/src/debugger_ui.rs | 6 +- crates/debugger_ui/src/dropdown_menus.rs | 3 +- crates/debugger_ui/src/new_process_modal.rs | 2 +- crates/debugger_ui/src/persistence.rs | 2 +- crates/debugger_ui/src/session/running.rs | 8 +- .../src/session/running/breakpoint_list.rs | 2 - .../src/session/running/console.rs | 2 +- .../src/session/running/loaded_source_list.rs | 2 +- .../src/session/running/memory_view.rs | 2 +- .../src/session/running/module_list.rs | 2 +- .../src/session/running/stack_frame_list.rs | 6 +- .../src/session/running/variable_list.rs | 5 +- .../debugger_ui/src/tests/debugger_panel.rs | 5 +- .../src/tests/new_process_modal.rs | 4 +- crates/docs_preprocessor/src/main.rs | 6 +- .../src/edit_prediction_button.rs | 2 +- crates/editor/src/code_context_menus.rs | 2 +- crates/editor/src/display_map/block_map.rs | 22 +++-- crates/editor/src/display_map/fold_map.rs | 12 +-- crates/editor/src/display_map/tab_map.rs | 14 +-- crates/editor/src/editor.rs | 29 +++--- crates/editor/src/editor_settings_controls.rs | 2 +- crates/editor/src/editor_tests.rs | 81 +++++++---------- crates/editor/src/element.rs | 6 +- crates/editor/src/hover_popover.rs | 4 +- crates/editor/src/jsx_tag_auto_close.rs | 2 +- crates/editor/src/test/editor_test_context.rs | 8 +- crates/eval/src/eval.rs | 10 +-- crates/eval/src/instance.rs | 9 +- .../extension_host/src/capability_granter.rs | 2 +- .../src/components/feature_upsell.rs | 1 - crates/extensions_ui/src/extensions_ui.rs | 4 +- crates/feedback/src/system_specs.rs | 2 +- crates/file_finder/src/file_finder.rs | 2 +- crates/file_finder/src/open_path_prompt.rs | 6 +- crates/fs/src/fake_git_repo.rs | 2 +- crates/fs/src/fs.rs | 6 +- crates/fs/src/fs_watcher.rs | 2 +- crates/git_ui/src/blame_ui.rs | 6 +- crates/git_ui/src/branch_picker.rs | 4 +- crates/git_ui/src/commit_modal.rs | 8 +- crates/git_ui/src/commit_tooltip.rs | 2 +- crates/git_ui/src/conflict_view.rs | 5 +- crates/git_ui/src/git_panel.rs | 36 ++++---- crates/git_ui/src/git_ui.rs | 17 ++-- crates/git_ui/src/project_diff.rs | 8 +- crates/git_ui/src/text_diff_view.rs | 4 +- crates/go_to_line/src/go_to_line.rs | 2 +- crates/gpui/examples/input.rs | 4 +- crates/gpui/examples/text.rs | 2 +- crates/gpui/src/app/async_context.rs | 2 +- crates/gpui/src/app/entity_map.rs | 3 +- crates/gpui/src/app/test_context.rs | 8 +- crates/gpui/src/elements/img.rs | 2 +- crates/gpui/src/geometry.rs | 14 +-- crates/gpui/src/keymap.rs | 32 +++---- crates/gpui/src/keymap/context.rs | 10 +-- crates/gpui/src/platform/linux/platform.rs | 4 +- .../gpui/src/platform/linux/wayland/client.rs | 1 - .../gpui/src/platform/linux/wayland/cursor.rs | 2 +- crates/gpui/src/platform/linux/x11/client.rs | 2 +- .../gpui/src/platform/mac/metal_renderer.rs | 2 +- crates/gpui/src/platform/test/platform.rs | 4 +- crates/gpui/src/platform/windows/events.rs | 2 +- crates/gpui/src/shared_string.rs | 2 +- crates/gpui/src/window.rs | 10 +-- .../tests/derive_inspector_reflection.rs | 4 +- crates/http_client/src/async_body.rs | 2 +- crates/journal/src/journal.rs | 2 +- crates/language/src/buffer.rs | 17 ++-- crates/language/src/buffer_tests.rs | 10 +-- crates/language/src/language.rs | 2 +- crates/language/src/language_registry.rs | 7 +- crates/language/src/language_settings.rs | 4 +- crates/language/src/syntax_map.rs | 2 +- .../src/syntax_map/syntax_map_tests.rs | 15 ++-- crates/language/src/text_diff.rs | 10 +-- crates/language_model/src/language_model.rs | 4 +- .../language_model/src/model/cloud_model.rs | 2 +- crates/language_models/src/language_models.rs | 2 +- .../language_models/src/provider/bedrock.rs | 15 +--- crates/language_models/src/provider/cloud.rs | 6 +- crates/language_models/src/provider/google.rs | 2 +- .../language_models/src/provider/lmstudio.rs | 2 +- crates/language_models/src/provider/ollama.rs | 2 +- .../src/ui/instruction_list_item.rs | 2 +- crates/language_tools/src/lsp_log.rs | 11 +-- crates/language_tools/src/syntax_tree_view.rs | 2 +- crates/languages/src/c.rs | 10 +-- crates/languages/src/go.rs | 8 +- crates/languages/src/json.rs | 2 +- crates/languages/src/lib.rs | 24 ++--- crates/languages/src/python.rs | 6 +- crates/languages/src/rust.rs | 8 +- crates/languages/src/tailwind.rs | 2 +- crates/languages/src/typescript.rs | 4 +- crates/languages/src/vtsls.rs | 2 +- crates/languages/src/yaml.rs | 2 +- crates/livekit_client/examples/test_app.rs | 2 +- .../src/livekit_client/playback.rs | 5 +- crates/markdown/examples/markdown_as_child.rs | 2 +- crates/markdown/src/markdown.rs | 3 +- .../markdown_preview/src/markdown_parser.rs | 5 +- .../src/markdown_preview_view.rs | 3 +- .../src/migrations/m_2025_01_02/settings.rs | 4 +- .../src/migrations/m_2025_01_29/keymap.rs | 2 +- .../src/migrations/m_2025_01_29/settings.rs | 2 +- .../src/migrations/m_2025_01_30/settings.rs | 6 +- .../src/migrations/m_2025_03_29/settings.rs | 2 +- .../src/migrations/m_2025_05_29/settings.rs | 4 +- crates/multi_buffer/src/multi_buffer.rs | 6 +- crates/onboarding/src/basics_page.rs | 4 +- crates/onboarding/src/editing_page.rs | 8 +- crates/onboarding/src/theme_preview.rs | 12 +-- crates/outline_panel/src/outline_panel.rs | 2 +- crates/panel/src/panel.rs | 2 +- crates/picker/src/popover_menu.rs | 2 +- crates/project/src/buffer_store.rs | 8 +- crates/project/src/context_server_store.rs | 2 +- .../project/src/debugger/breakpoint_store.rs | 16 ++-- crates/project/src/debugger/dap_command.rs | 2 +- crates/project/src/debugger/dap_store.rs | 2 +- crates/project/src/debugger/session.rs | 9 +- crates/project/src/git_store.rs | 19 ++-- crates/project/src/git_store/conflict_set.rs | 4 +- crates/project/src/image_store.rs | 3 +- crates/project/src/lsp_command.rs | 9 +- crates/project/src/lsp_store.rs | 13 ++- crates/project/src/lsp_store/clangd_ext.rs | 2 +- .../src/lsp_store/rust_analyzer_ext.rs | 1 - crates/project/src/project.rs | 6 +- crates/project/src/project_tests.rs | 10 +-- crates/project/src/task_inventory.rs | 4 +- crates/project/src/terminals.rs | 2 +- crates/project/src/worktree_store.rs | 2 +- crates/project_panel/src/project_panel.rs | 17 ++-- .../project_panel/src/project_panel_tests.rs | 90 +++++++++---------- crates/project_symbols/src/project_symbols.rs | 8 +- crates/prompt_store/src/prompts.rs | 2 +- crates/recent_projects/src/remote_servers.rs | 10 +-- crates/recent_projects/src/ssh_connections.rs | 2 +- crates/remote/src/ssh_session.rs | 6 +- crates/remote_server/src/headless_project.rs | 4 +- crates/remote_server/src/unix.rs | 3 +- crates/repl/src/components/kernel_options.rs | 2 +- crates/repl/src/kernels/remote_kernels.rs | 4 +- crates/repl/src/outputs.rs | 19 ++-- crates/repl/src/outputs/markdown.rs | 2 +- crates/repl/src/repl_editor.rs | 12 +-- crates/repl/src/repl_sessions_ui.rs | 1 - crates/repl/src/session.rs | 5 +- crates/rope/src/chunk.rs | 2 +- crates/rpc/src/conn.rs | 1 - crates/rpc/src/peer.rs | 1 - crates/rules_library/src/rules_library.rs | 4 +- crates/search/src/buffer_search.rs | 4 +- crates/search/src/buffer_search/registrar.rs | 1 - crates/search/src/project_search.rs | 3 +- crates/semantic_index/examples/index.rs | 4 +- crates/semantic_index/src/embedding_index.rs | 7 +- crates/semantic_index/src/semantic_index.rs | 2 +- crates/semantic_index/src/summary_index.rs | 2 +- crates/settings/src/settings_json.rs | 4 +- .../src/settings_profile_selector.rs | 2 +- .../src/appearance_settings_controls.rs | 4 +- crates/settings_ui/src/keybindings.rs | 12 +-- crates/story/src/story.rs | 2 +- crates/supermaven/src/supermaven.rs | 4 +- .../src/supermaven_completion_provider.rs | 4 +- crates/task/src/static_source.rs | 1 - crates/task/src/task.rs | 2 +- crates/task/src/task_template.rs | 12 ++- crates/tasks_ui/src/tasks_ui.rs | 2 +- crates/terminal/src/terminal.rs | 4 +- crates/terminal_view/src/terminal_element.rs | 5 +- crates/terminal_view/src/terminal_panel.rs | 1 - crates/theme/src/theme.rs | 6 +- crates/theme_importer/src/vscode/converter.rs | 12 ++- crates/title_bar/src/application_menu.rs | 2 +- crates/title_bar/src/collab.rs | 2 +- crates/title_bar/src/title_bar.rs | 7 +- .../src/toolchain_selector.rs | 1 - crates/ui/src/components/dropdown_menu.rs | 4 +- crates/ui/src/components/indent_guides.rs | 3 +- crates/ui/src/components/keybinding.rs | 2 +- crates/ui/src/components/keybinding_hint.rs | 2 +- .../components/notification/alert_modal.rs | 2 +- crates/ui/src/components/sticky_items.rs | 2 +- crates/ui/src/utils/format_distance.rs | 12 +-- crates/ui_input/src/ui_input.rs | 4 +- crates/vim/src/command.rs | 2 +- crates/vim/src/mode_indicator.rs | 6 +- crates/vim/src/motion.rs | 20 ++--- crates/vim/src/normal/paste.rs | 6 +- crates/vim/src/object.rs | 4 +- crates/vim/src/state.rs | 14 +-- .../src/test/neovim_backed_test_context.rs | 7 +- crates/vim/src/test/neovim_connection.rs | 2 +- crates/vim/src/test/vim_test_context.rs | 2 +- crates/vim/src/vim.rs | 2 +- crates/vim/src/visual.rs | 2 +- crates/watch/src/watch.rs | 2 +- crates/web_search/src/web_search.rs | 2 +- crates/workspace/src/dock.rs | 4 +- crates/workspace/src/notifications.rs | 1 - crates/workspace/src/pane.rs | 4 +- crates/workspace/src/pane_group.rs | 2 +- crates/workspace/src/persistence/model.rs | 2 +- crates/workspace/src/searchable.rs | 4 +- crates/workspace/src/status_bar.rs | 4 +- crates/workspace/src/theme_preview.rs | 1 - crates/workspace/src/workspace.rs | 6 +- crates/worktree/src/worktree.rs | 4 +- crates/worktree/src/worktree_tests.rs | 2 +- crates/zed/src/main.rs | 18 ++-- crates/zed/src/zed.rs | 8 +- crates/zed/src/zed/component_preview.rs | 18 ++-- .../zed/src/zed/edit_prediction_registry.rs | 11 +-- crates/zed/src/zed/open_listener.rs | 7 +- crates/zed/src/zed/quick_action_bar.rs | 4 +- .../zed/src/zed/quick_action_bar/repl_menu.rs | 5 +- crates/zeta/src/input_excerpt.rs | 2 +- crates/zeta_cli/src/headless.rs | 6 +- crates/zlog/src/filter.rs | 2 +- extensions/glsl/src/glsl.rs | 2 +- extensions/html/src/html.rs | 2 +- extensions/ruff/src/ruff.rs | 4 +- extensions/snippets/src/snippets.rs | 2 +- 306 files changed, 805 insertions(+), 1102 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3610808984bdaf85d45c45ba62b5db11f91c3f30..c3c70912792b123e89c0ee50fff6fd729fe93c71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -840,6 +840,7 @@ match_like_matches_macro = "warn" module_inception = { level = "deny" } question_mark = { level = "deny" } single_match = "warn" +redundant_clone = "warn" redundant_closure = { level = "deny" } redundant_static_lifetimes = { level = "warn" } redundant_pattern_matching = "warn" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index e58f0a291f2bf1a0393f87050ba78f3730adf5be..4f20dbd587440e4953187c5d306a3584a4cad20c 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -471,7 +471,7 @@ impl ContentBlock { fn block_string_contents(&self, block: acp::ContentBlock) -> String { match block { - acp::ContentBlock::Text(text_content) => text_content.text.clone(), + acp::ContentBlock::Text(text_content) => text_content.text, acp::ContentBlock::ResourceLink(resource_link) => { Self::resource_link_md(&resource_link.uri) } @@ -1020,7 +1020,7 @@ impl AcpThread { let location_updated = update.fields.locations.is_some(); current_call.update_fields(update.fields, languages, cx); if location_updated { - self.resolve_locations(update.id.clone(), cx); + self.resolve_locations(update.id, cx); } } ToolCallUpdate::UpdateDiff(update) => { diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index 4b779931c5d333089fa8055c0403e0ef2cf8dbaa..70367e340adbf5f787557f81963de85293273cc1 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -222,7 +222,7 @@ impl PendingDiff { fn finalize(&self, cx: &mut Context<Diff>) -> FinalizedDiff { let ranges = self.excerpt_ranges(cx); let base_text = self.base_text.clone(); - let language_registry = self.buffer.read(cx).language_registry().clone(); + let language_registry = self.buffer.read(cx).language_registry(); let path = self .buffer @@ -248,7 +248,6 @@ impl PendingDiff { let buffer_diff = cx.spawn({ let buffer = buffer.clone(); - let language_registry = language_registry.clone(); async move |_this, cx| { build_buffer_diff(base_text, &buffer, language_registry, cx).await } diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index 1c3cad386d652648fc448d8df3b1bbd6ca173597..a1f332fc7cb838e27f01417b383c3d36f78f9356 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -161,7 +161,7 @@ impl ActionLog { diff_base, last_seen_base, unreviewed_edits, - snapshot: text_snapshot.clone(), + snapshot: text_snapshot, status, version: buffer.read(cx).version(), diff, @@ -461,7 +461,7 @@ impl ActionLog { anyhow::Ok(( tracked_buffer.diff.clone(), buffer.read(cx).language().cloned(), - buffer.read(cx).language_registry().clone(), + buffer.read(cx).language_registry(), )) })??; let diff_snapshot = BufferDiff::update_diff( @@ -529,12 +529,12 @@ impl ActionLog { /// Mark a buffer as created by agent, so we can refresh it in the context pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { - self.track_buffer_internal(buffer.clone(), true, cx); + self.track_buffer_internal(buffer, true, cx); } /// Mark a buffer as edited by agent, so we can refresh it in the context pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { - let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); + let tracked_buffer = self.track_buffer_internal(buffer, false, cx); if let TrackedBufferStatus::Deleted = tracked_buffer.status { tracked_buffer.status = TrackedBufferStatus::Modified; } @@ -2425,7 +2425,7 @@ mod tests { assert_eq!( unreviewed_hunks(&action_log, cx), vec![( - buffer.clone(), + buffer, vec![ HunkStatus { range: Point::new(6, 0)..Point::new(7, 0), diff --git a/crates/agent/src/agent_profile.rs b/crates/agent/src/agent_profile.rs index 1636508df6b2012333ee90d9062c73918ddf0f35..c9e73372f60686cf330531926f4129e9c9b25db8 100644 --- a/crates/agent/src/agent_profile.rs +++ b/crates/agent/src/agent_profile.rs @@ -132,7 +132,7 @@ mod tests { }); let tool_set = default_tool_set(cx); - let profile = AgentProfile::new(id.clone(), tool_set); + let profile = AgentProfile::new(id, tool_set); let mut enabled_tools = cx .read(|cx| profile.enabled_tools(cx)) @@ -169,7 +169,7 @@ mod tests { }); let tool_set = default_tool_set(cx); - let profile = AgentProfile::new(id.clone(), tool_set); + let profile = AgentProfile::new(id, tool_set); let mut enabled_tools = cx .read(|cx| profile.enabled_tools(cx)) @@ -202,7 +202,7 @@ mod tests { }); let tool_set = default_tool_set(cx); - let profile = AgentProfile::new(id.clone(), tool_set); + let profile = AgentProfile::new(id, tool_set); let mut enabled_tools = cx .read(|cx| profile.enabled_tools(cx)) diff --git a/crates/agent/src/context_server_tool.rs b/crates/agent/src/context_server_tool.rs index 22d1a72bf5f833a6594f34fd8f5d7b9102740740..696c569356bca36adf54bc84ec52fa7295048b75 100644 --- a/crates/agent/src/context_server_tool.rs +++ b/crates/agent/src/context_server_tool.rs @@ -86,15 +86,13 @@ impl Tool for ContextServerTool { ) -> ToolResult { if let Some(server) = self.store.read(cx).get_running_server(&self.server_id) { let tool_name = self.tool.name.clone(); - let server_clone = server.clone(); - let input_clone = input.clone(); cx.spawn(async move |_cx| { - let Some(protocol) = server_clone.client() else { + let Some(protocol) = server.client() else { bail!("Context server not initialized"); }; - let arguments = if let serde_json::Value::Object(map) = input_clone { + let arguments = if let serde_json::Value::Object(map) = input { Some(map.into_iter().collect()) } else { None diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 88f82701a499b505a6fd805b8cf49c8708d6ce09..a584fba88169e8b385678643db66b26820428c30 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -494,7 +494,7 @@ impl Thread { last_received_chunk_at: None, request_callback: None, remaining_turns: u32::MAX, - configured_model: configured_model.clone(), + configured_model, profile: AgentProfile::new(profile_id, tools), } } @@ -532,7 +532,7 @@ impl Thread { .and_then(|model| { let model = SelectedModel { provider: model.provider.clone().into(), - model: model.model.clone().into(), + model: model.model.into(), }; registry.select_model(&model, cx) }) @@ -1646,10 +1646,10 @@ impl Thread { }; self.tool_use - .request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx); + .request_tool_use(tool_message_id, tool_use, tool_use_metadata, cx); self.tool_use.insert_tool_output( - tool_use_id.clone(), + tool_use_id, tool_name, tool_output, self.configured_model.as_ref(), @@ -3241,7 +3241,7 @@ impl Thread { self.configured_model.as_ref(), self.completion_mode, ); - self.tool_finished(tool_use_id.clone(), None, true, window, cx); + self.tool_finished(tool_use_id, None, true, window, cx); } } @@ -3873,7 +3873,7 @@ fn main() {{ AgentSettings { model_parameters: vec![LanguageModelParameters { provider: Some(model.provider_id().0.to_string().into()), - model: Some(model.id().0.clone()), + model: Some(model.id().0), temperature: Some(0.66), }], ..AgentSettings::get_global(cx).clone() @@ -3893,7 +3893,7 @@ fn main() {{ AgentSettings { model_parameters: vec![LanguageModelParameters { provider: None, - model: Some(model.id().0.clone()), + model: Some(model.id().0), temperature: Some(0.66), }], ..AgentSettings::get_global(cx).clone() @@ -3933,7 +3933,7 @@ fn main() {{ AgentSettings { model_parameters: vec![LanguageModelParameters { provider: Some("anthropic".into()), - model: Some(model.id().0.clone()), + model: Some(model.id().0), temperature: Some(0.66), }], ..AgentSettings::get_global(cx).clone() diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index ab5716d8ad7f34bf960fa575afe4e1909cfeb39a..5496ecea7b177b406af1745bf05de9a3eb110170 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -255,7 +255,7 @@ impl NativeAgent { }), cx.subscribe(&thread_handle, Self::handle_thread_token_usage_updated), cx.observe(&thread_handle, move |this, thread, cx| { - this.save_thread(thread.clone(), cx) + this.save_thread(thread, cx) }), ]; @@ -499,8 +499,8 @@ impl NativeAgent { self.models.refresh_list(cx); let registry = LanguageModelRegistry::read_global(cx); - let default_model = registry.default_model().map(|m| m.model.clone()); - let summarization_model = registry.thread_summary_model().map(|m| m.model.clone()); + let default_model = registry.default_model().map(|m| m.model); + let summarization_model = registry.thread_summary_model().map(|m| m.model); for session in self.sessions.values_mut() { session.thread.update(cx, |thread, cx| { diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index c6a6c382019867a1c4580b31bf955ee701768c0c..1b88955a241945996be911a337035a970c8b4026 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent2/src/db.rs @@ -287,7 +287,7 @@ impl ThreadsDatabase { .map_err(|e| anyhow!("Failed to create threads table: {}", e))?; let db = Self { - executor: executor.clone(), + executor, connection: Arc::new(Mutex::new(connection)), }; @@ -325,7 +325,7 @@ impl ThreadsDatabase { INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?) "})?; - insert((id.0.clone(), title, updated_at, data_type, data))?; + insert((id.0, title, updated_at, data_type, data))?; Ok(()) } @@ -434,7 +434,7 @@ mod tests { let client = Client::new(clock, http_client, cx); agent::init(cx); agent_settings::init(cx); - language_model::init(client.clone(), cx); + language_model::init(client, cx); }); } diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 55bfa6f0b5319bd60e30ae6f8e00ba20b7597e5d..478604b14a8934e6f736f3632cdac6853b7b42bb 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1401,7 +1401,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) { let client = Client::new(clock, http_client, cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), client.clone(), cx); + language_models::init(user_store, client.clone(), cx); Project::init_settings(cx); LanguageModelRegistry::test(cx); agent_settings::init(cx); @@ -1854,7 +1854,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { let client = Client::production(cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), client.clone(), cx); + language_models::init(user_store, client.clone(), cx); watch_settings(fs.clone(), cx); }); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 0e1287a920dbffe10cf2e2bd7c93067242e00ba9..cd97fa2060da70927bedb23d326db6dd2517c147 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -679,7 +679,7 @@ impl Thread { .and_then(|model| { let model = SelectedModel { provider: model.provider.clone().into(), - model: model.model.clone().into(), + model: model.model.into(), }; registry.select_model(&model, cx) }) diff --git a/crates/agent2/src/tools/context_server_registry.rs b/crates/agent2/src/tools/context_server_registry.rs index 69c4221a815eba4c9e5b04dfc97e04ef90bc62b7..c7963fa6e6e14ffa34d076dc2ca5dbdc23c78cab 100644 --- a/crates/agent2/src/tools/context_server_registry.rs +++ b/crates/agent2/src/tools/context_server_registry.rs @@ -176,15 +176,13 @@ impl AnyAgentTool for ContextServerTool { return Task::ready(Err(anyhow!("Context server not found"))); }; let tool_name = self.tool.name.clone(); - let server_clone = server.clone(); - let input_clone = input.clone(); cx.spawn(async move |_cx| { - let Some(protocol) = server_clone.client() else { + let Some(protocol) = server.client() else { bail!("Context server not initialized"); }; - let arguments = if let serde_json::Value::Object(map) = input_clone { + let arguments = if let serde_json::Value::Object(map) = input { Some(map.into_iter().collect()) } else { None diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index a87699bd1202b5d8c561559743e1990dcae288f7..24fedda4eb57d36db9e8769a107a947fed998a05 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -427,7 +427,7 @@ impl AgentTool for EditFileTool { Ok(EditFileToolOutput { input_path: input.path, - new_text: new_text.clone(), + new_text, old_text, diff: unified_diff, edit_agent_output, diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent2/src/tools/grep_tool.rs index 6d7c05d2115498b18d26a6963ea4330f1dadd4c2..265c26926d816a00a414755ea1193eb22d1c915f 100644 --- a/crates/agent2/src/tools/grep_tool.rs +++ b/crates/agent2/src/tools/grep_tool.rs @@ -318,7 +318,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), serde_json::json!({ @@ -403,7 +403,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), serde_json::json!({ @@ -478,7 +478,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); // Create test file with syntax structures fs.insert_tree( @@ -763,7 +763,7 @@ mod tests { if cfg!(windows) { result.replace("root\\", "root/") } else { - result.to_string() + result } } Err(e) => panic!("Failed to run grep tool: {}", e), diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index 17e671fba3dffa551a033afaeef0efbe7d0448a5..3d4faf2e031aca522af8c7787b05bf302e45969e 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -234,7 +234,7 @@ fn process_content( if is_empty { "Command executed successfully.".to_string() } else { - content.to_string() + content } } Some(exit_status) => { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 3008edebeb4267faa250f8c1272e062a0e26d369..df2a24e6982913aaee343ddf2f9bd88854425f00 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -787,7 +787,7 @@ impl Content { pub fn chunks(self) -> impl Iterator<Item = ContentChunk> { match self { Self::Chunks(chunks) => chunks.into_iter(), - Self::UntaggedText(text) => vec![ContentChunk::Text { text: text.clone() }].into_iter(), + Self::UntaggedText(text) => vec![ContentChunk::Text { text }].into_iter(), } } } diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index 3be10ed94c05e7562bb4956c12f72da730a6589b..323190300131c87a443b95e4bb18424cf034e667 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -58,7 +58,7 @@ impl ClaudeTool { Self::Terminal(None) } else { Self::Other { - name: tool_name.to_string(), + name: tool_name, input, } } diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 999e469d30dab1e4e429ba71bfe06867237e8670..d90520d26acf0a8ce14605dc43a12a72816fd133 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -89,7 +89,7 @@ impl ContextPickerCompletionProvider { ) -> Option<Completion> { match entry { ContextPickerEntry::Mode(mode) => Some(Completion { - replace_range: source_range.clone(), + replace_range: source_range, new_text: format!("@{} ", mode.keyword()), label: CodeLabel::plain(mode.label().to_string(), None), icon_path: Some(mode.icon().path().into()), @@ -146,7 +146,7 @@ impl ContextPickerCompletionProvider { }; Some(Completion { - replace_range: source_range.clone(), + replace_range: source_range, new_text, label: CodeLabel::plain(action.label().to_string(), None), icon_path: Some(action.icon().path().into()), @@ -187,7 +187,7 @@ impl ContextPickerCompletionProvider { documentation: None, insert_text_mode: None, source: project::CompletionSource::Custom, - icon_path: Some(icon_for_completion.clone()), + icon_path: Some(icon_for_completion), confirm: Some(confirm_completion_callback( thread_entry.title().clone(), source_range.start, @@ -218,9 +218,9 @@ impl ContextPickerCompletionProvider { documentation: None, insert_text_mode: None, source: project::CompletionSource::Custom, - icon_path: Some(icon_path.clone()), + icon_path: Some(icon_path), confirm: Some(confirm_completion_callback( - rule.title.clone(), + rule.title, source_range.start, new_text_len - 1, editor, @@ -260,7 +260,7 @@ impl ContextPickerCompletionProvider { let completion_icon_path = if is_recent { IconName::HistoryRerun.path().into() } else { - crease_icon_path.clone() + crease_icon_path }; let new_text = format!("{} ", uri.as_link()); @@ -309,10 +309,10 @@ impl ContextPickerCompletionProvider { label, documentation: None, source: project::CompletionSource::Custom, - icon_path: Some(icon_path.clone()), + icon_path: Some(icon_path), insert_text_mode: None, confirm: Some(confirm_completion_callback( - symbol.name.clone().into(), + symbol.name.into(), source_range.start, new_text_len - 1, message_editor, @@ -327,7 +327,7 @@ impl ContextPickerCompletionProvider { message_editor: WeakEntity<MessageEditor>, cx: &mut App, ) -> Option<Completion> { - let new_text = format!("@fetch {} ", url_to_fetch.clone()); + let new_text = format!("@fetch {} ", url_to_fetch); let url_to_fetch = url::Url::parse(url_to_fetch.as_ref()) .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) .ok()?; @@ -341,7 +341,7 @@ impl ContextPickerCompletionProvider { label: CodeLabel::plain(url_to_fetch.to_string(), None), documentation: None, source: project::CompletionSource::Custom, - icon_path: Some(icon_path.clone()), + icon_path: Some(icon_path), insert_text_mode: None, confirm: Some(confirm_completion_callback( url_to_fetch.to_string().into(), @@ -365,8 +365,7 @@ impl ContextPickerCompletionProvider { }; match mode { Some(ContextPickerMode::File) => { - let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + let search_files_task = search_files(query, cancellation_flag, &workspace, cx); cx.background_spawn(async move { search_files_task .await @@ -377,8 +376,7 @@ impl ContextPickerCompletionProvider { } Some(ContextPickerMode::Symbol) => { - let search_symbols_task = - search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx); + let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx); cx.background_spawn(async move { search_symbols_task .await @@ -389,12 +387,8 @@ impl ContextPickerCompletionProvider { } Some(ContextPickerMode::Thread) => { - let search_threads_task = search_threads( - query.clone(), - cancellation_flag.clone(), - &self.history_store, - cx, - ); + let search_threads_task = + search_threads(query, cancellation_flag, &self.history_store, cx); cx.background_spawn(async move { search_threads_task .await @@ -415,7 +409,7 @@ impl ContextPickerCompletionProvider { Some(ContextPickerMode::Rules) => { if let Some(prompt_store) = self.prompt_store.as_ref() { let search_rules_task = - search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx); + search_rules(query, cancellation_flag, prompt_store, cx); cx.background_spawn(async move { search_rules_task .await @@ -448,7 +442,7 @@ impl ContextPickerCompletionProvider { let executor = cx.background_executor().clone(); let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + search_files(query.clone(), cancellation_flag, &workspace, cx); let entries = self.available_context_picker_entries(&workspace, cx); let entry_candidates = entries diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index c87c824015b729f5da26ad2e0fa0877f17db33bd..b5282bf8911fc0cfc6c013d17dde1653d1456596 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -260,7 +260,7 @@ impl MessageEditor { *excerpt_id, start, content_len, - crease_text.clone(), + crease_text, mention_uri.icon_path(cx), self.editor.clone(), window, @@ -883,7 +883,7 @@ impl MessageEditor { .spawn_in(window, { let abs_path = abs_path.clone(); async move |_, cx| { - let image = image.await.map_err(|e| e.to_string())?; + let image = image.await?; let format = image.format; let image = cx .update(|_, cx| LanguageModelImage::from_image(image, cx)) @@ -1231,7 +1231,6 @@ fn render_image_fold_icon_button( editor: WeakEntity<Editor>, ) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> { Arc::new({ - let image_task = image_task.clone(); move |fold_id, fold_range, cx| { let is_in_text_selection = editor .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) @@ -1408,10 +1407,7 @@ impl MentionSet { crease_id, Mention::Text { uri, - content: content - .await - .map_err(|e| anyhow::anyhow!("{e}"))? - .to_string(), + content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, }, )) }) @@ -1478,10 +1474,7 @@ impl MentionSet { crease_id, Mention::Text { uri, - content: content - .await - .map_err(|e| anyhow::anyhow!("{e}"))? - .to_string(), + content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, }, )) }) @@ -1821,7 +1814,7 @@ mod tests { impl Focusable for MessageEditorItem { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx).clone() + self.0.read(cx).focus_handle(cx) } } @@ -2219,7 +2212,7 @@ mod tests { let completions = editor.current_completions().expect("Missing completions"); completions .into_iter() - .map(|completion| completion.label.text.to_string()) + .map(|completion| completion.label.text) .collect::<Vec<_>>() } } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index b93df3a5db441269109f63078227bc22685ac222..b5277758509d5dd4f25ca9f5ceede004c6b817d6 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1534,7 +1534,7 @@ impl AcpThreadView { window: &Window, cx: &Context<Self>, ) -> AnyElement { - let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id.clone())); + let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id)); v_flex() .mt_1p5() @@ -1555,9 +1555,8 @@ impl AcpThreadView { .icon_color(Color::Muted) .icon_position(IconPosition::Start) .on_click(cx.listener({ - let id = tool_call_id.clone(); move |this: &mut Self, _, _, cx: &mut Context<Self>| { - this.expanded_tool_calls.remove(&id); + this.expanded_tool_calls.remove(&tool_call_id); cx.notify(); } })), @@ -1578,7 +1577,7 @@ impl AcpThreadView { uri.clone() }; - let button_id = SharedString::from(format!("item-{}", uri.clone())); + let button_id = SharedString::from(format!("item-{}", uri)); div() .ml(px(7.)) @@ -1724,7 +1723,7 @@ impl AcpThreadView { && let Some(editor) = entry.editor_for_diff(diff) && diff.read(cx).has_revealed_range(cx) { - editor.clone().into_any_element() + editor.into_any_element() } else if tool_progress { self.render_diff_loading(cx) } else { @@ -2888,7 +2887,6 @@ impl AcpThreadView { .icon_size(IconSize::Small) .icon_color(Color::Muted) .tooltip({ - let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( expand_tooltip, @@ -4372,7 +4370,7 @@ pub(crate) mod tests { impl Focusable for ThreadViewItem { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx).clone() + self.0.read(cx).focus_handle(cx) } } diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 92588cf213c9ef5fd6bd932a358cc566977a5eb2..bb5b47f0d69b00d89b32be13c29ad71f23abd239 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -491,7 +491,7 @@ fn render_markdown_code_block( .on_click({ let active_thread = active_thread.clone(); let parsed_markdown = parsed_markdown.clone(); - let code_block_range = metadata.content_range.clone(); + let code_block_range = metadata.content_range; move |_event, _window, cx| { active_thread.update(cx, |this, cx| { this.copied_code_block_ids.insert((message_id, ix)); @@ -532,7 +532,6 @@ fn render_markdown_code_block( "Expand Code" })) .on_click({ - let active_thread = active_thread.clone(); move |_event, _window, cx| { active_thread.update(cx, |this, cx| { this.toggle_codeblock_expanded(message_id, ix); @@ -916,7 +915,7 @@ impl ActiveThread { ) { let rendered = self .rendered_tool_uses - .entry(tool_use_id.clone()) + .entry(tool_use_id) .or_insert_with(|| RenderedToolUse { label: cx.new(|cx| { Markdown::new("".into(), Some(self.language_registry.clone()), None, cx) @@ -1218,7 +1217,7 @@ impl ActiveThread { match AgentSettings::get_global(cx).notify_when_agent_waiting { NotifyWhenAgentWaiting::PrimaryScreen => { if let Some(primary) = cx.primary_display() { - self.pop_up(icon, caption.into(), title.clone(), window, primary, cx); + self.pop_up(icon, caption.into(), title, window, primary, cx); } } NotifyWhenAgentWaiting::AllScreens => { @@ -2112,7 +2111,7 @@ impl ActiveThread { .gap_1() .children(message_content) .when_some(editing_message_state, |this, state| { - let focus_handle = state.editor.focus_handle(cx).clone(); + let focus_handle = state.editor.focus_handle(cx); this.child( h_flex() @@ -2173,7 +2172,6 @@ impl ActiveThread { .icon_color(Color::Muted) .icon_size(IconSize::Small) .tooltip({ - let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( "Regenerate", @@ -2312,7 +2310,7 @@ impl ActiveThread { .into_any_element() } else if let Some(error) = error { restore_checkpoint_button - .tooltip(Tooltip::text(error.to_string())) + .tooltip(Tooltip::text(error)) .into_any_element() } else { restore_checkpoint_button.into_any_element() diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index ecb0bca4a1571d94558a87260b638d351b93b9b6..6da84758eee532aefbd720d986ae00dde13725e4 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -165,8 +165,8 @@ impl AgentConfiguration { provider: &Arc<dyn LanguageModelProvider>, cx: &mut Context<Self>, ) -> impl IntoElement + use<> { - let provider_id = provider.id().0.clone(); - let provider_name = provider.name().0.clone(); + let provider_id = provider.id().0; + let provider_name = provider.name().0; let provider_id_string = SharedString::from(format!("provider-disclosure-{provider_id}")); let configuration_view = self @@ -269,7 +269,7 @@ impl AgentConfiguration { .closed_icon(IconName::ChevronDown), ) .on_click(cx.listener({ - let provider_id = provider.id().clone(); + let provider_id = provider.id(); move |this, _event, _window, _cx| { let is_expanded = this .expanded_provider_configurations @@ -665,7 +665,7 @@ impl AgentConfiguration { .size(IconSize::XSmall) .color(Color::Accent) .with_animation( - SharedString::from(format!("{}-starting", context_server_id.0.clone(),)), + SharedString::from(format!("{}-starting", context_server_id.0,)), Animation::new(Duration::from_secs(3)).repeat(), |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), ) @@ -865,7 +865,6 @@ impl AgentConfiguration { .on_click({ let context_server_manager = self.context_server_store.clone(); - let context_server_id = context_server_id.clone(); let fs = self.fs.clone(); move |state, _window, cx| { @@ -1075,7 +1074,6 @@ fn show_unable_to_uninstall_extension_with_context_server( cx, move |this, _cx| { let workspace_handle = workspace_handle.clone(); - let context_server_id = context_server_id.clone(); this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning)) .dismiss_button(true) diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index 6159b9be8096f79f4e463249682b2f55c7af5fec..c898a5acb5b8d0a45780efb383ece19b4cfe289d 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -261,7 +261,6 @@ impl ConfigureContextServerModal { _cx: &mut Context<Workspace>, ) { workspace.register_action({ - let language_registry = language_registry.clone(); move |_workspace, _: &AddContextServer, window, cx| { let workspace_handle = cx.weak_entity(); let language_registry = language_registry.clone(); 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 09ad013d1ceb56d7c031cfc9eededb429aed2841..7fcf76d1cbec64ab99f3f97998600d6d9889207a 100644 --- a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs +++ b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs @@ -464,7 +464,7 @@ impl ManageProfilesModal { }, )) .child(ListSeparator) - .child(h_flex().p_2().child(mode.name_editor.clone())) + .child(h_flex().p_2().child(mode.name_editor)) } fn render_view_profile( diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 61a3ddd9063a51f07c637396abd97fabe24fe419..e07424987c48ef917b9470adbdfdbc6812957967 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -185,7 +185,7 @@ impl AgentDiffPane { let focus_handle = cx.focus_handle(); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - let project = thread.project(cx).clone(); + let project = thread.project(cx); let editor = cx.new(|cx| { let mut editor = Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); @@ -196,7 +196,7 @@ impl AgentDiffPane { editor }); - let action_log = thread.action_log(cx).clone(); + let action_log = thread.action_log(cx); let mut this = Self { _subscriptions: vec![ @@ -1312,7 +1312,7 @@ impl AgentDiff { let entity = cx.new(|_cx| Self::default()); let global = AgentDiffGlobal(entity.clone()); cx.set_global(global); - entity.clone() + entity }) } @@ -1334,7 +1334,7 @@ impl AgentDiff { window: &mut Window, cx: &mut Context<Self>, ) { - let action_log = thread.action_log(cx).clone(); + let action_log = thread.action_log(cx); let action_log_subscription = cx.observe_in(&action_log, window, { let workspace = workspace.clone(); @@ -1544,7 +1544,7 @@ impl AgentDiff { && let Some(editor) = item.downcast::<Editor>() && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) { - self.register_editor(workspace.downgrade(), buffer.clone(), editor, window, cx); + self.register_editor(workspace.downgrade(), buffer, editor, window, cx); } } diff --git a/crates/agent_ui/src/agent_model_selector.rs b/crates/agent_ui/src/agent_model_selector.rs index b989e7bf1e9147c7f6beb90b5054120cef7b818f..3de1027d91f6d613e9f3aa723b345e5d5f17ee6f 100644 --- a/crates/agent_ui/src/agent_model_selector.rs +++ b/crates/agent_ui/src/agent_model_selector.rs @@ -66,10 +66,8 @@ impl AgentModelSelector { fs.clone(), cx, move |settings, _cx| { - settings.set_inline_assistant_model( - provider.clone(), - model_id.clone(), - ); + settings + .set_inline_assistant_model(provider.clone(), model_id); }, ); } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index b857052d695bacf96de1926b3ddd64cdc7ea0d03..3c4c403a7703af78f1224fee773a9e912aee6bae 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -956,7 +956,7 @@ impl AgentPanel { message_editor.focus_handle(cx).focus(window); - let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx); + let thread_view = ActiveView::thread(active_thread, message_editor, window, cx); self.set_active_view(thread_view, window, cx); AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); @@ -1163,7 +1163,7 @@ impl AgentPanel { }); self.set_active_view( ActiveView::prompt_editor( - editor.clone(), + editor, self.history_store.clone(), self.acp_history_store.clone(), self.language_registry.clone(), @@ -1236,7 +1236,7 @@ impl AgentPanel { }); message_editor.focus_handle(cx).focus(window); - let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx); + let thread_view = ActiveView::thread(active_thread, message_editor, window, cx); self.set_active_view(thread_view, window, cx); AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); } @@ -1525,7 +1525,7 @@ impl AgentPanel { return; } - let model = thread_state.configured_model().map(|cm| cm.model.clone()); + let model = thread_state.configured_model().map(|cm| cm.model); if let Some(model) = model { thread.update(cx, |active_thread, cx| { active_thread.thread().update(cx, |thread, cx| { @@ -1680,7 +1680,7 @@ impl AgentPanel { .open_thread_by_id(&id, window, cx) .detach_and_log_err(cx), HistoryEntryId::Context(path) => this - .open_saved_prompt_editor(path.clone(), window, cx) + .open_saved_prompt_editor(path, window, cx) .detach_and_log_err(cx), }) .ok(); @@ -1966,7 +1966,7 @@ impl AgentPanel { }; match state { - ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone()) + ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT) .truncate() .into_any_element(), ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER) @@ -2106,7 +2106,6 @@ impl AgentPanel { .anchor(Corner::TopRight) .with_handle(self.agent_panel_menu_handle.clone()) .menu({ - let focus_handle = focus_handle.clone(); move |window, cx| { Some(ContextMenu::build(window, cx, |mut menu, _window, _| { menu = menu.context(focus_handle.clone()); @@ -2184,7 +2183,6 @@ impl AgentPanel { .trigger_with_tooltip( IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small), { - let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( "Toggle Recent Threads", @@ -2222,8 +2220,6 @@ impl AgentPanel { this.go_back(&workspace::GoBack, window, cx); })) .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx) } @@ -2249,7 +2245,6 @@ impl AgentPanel { .anchor(Corner::TopRight) .with_handle(self.new_thread_menu_handle.clone()) .menu({ - let focus_handle = focus_handle.clone(); move |window, cx| { let active_thread = active_thread.clone(); Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { @@ -2377,7 +2372,6 @@ impl AgentPanel { .anchor(Corner::TopLeft) .with_handle(self.new_thread_menu_handle.clone()) .menu({ - let focus_handle = focus_handle.clone(); let workspace = self.workspace.clone(); move |window, cx| { @@ -3015,7 +3009,7 @@ impl AgentPanel { // TODO: Add keyboard navigation. let is_hovered = self.hovered_recent_history_item == Some(index); - HistoryEntryElement::new(entry.clone(), cx.entity().downgrade()) + HistoryEntryElement::new(entry, cx.entity().downgrade()) .hovered(is_hovered) .on_hover(cx.listener( move |this, is_hovered, _window, cx| { @@ -3339,7 +3333,7 @@ impl AgentPanel { .severity(Severity::Error) .icon(IconName::XCircle) .title(header) - .description(message.clone()) + .description(message) .actions_slot( h_flex() .gap_0p5() @@ -3359,7 +3353,7 @@ impl AgentPanel { Callout::new() .severity(Severity::Error) .title("Error") - .description(message.clone()) + .description(message) .actions_slot( h_flex() .gap_0p5() diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index a1dbc77084d62ccef7848d6328b8daf2731bf01a..01a248994db1d615ebccc6b644deff7c06d73c1d 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -240,12 +240,7 @@ pub fn init( client.telemetry().clone(), cx, ); - terminal_inline_assistant::init( - fs.clone(), - prompt_builder.clone(), - client.telemetry().clone(), - cx, - ); + terminal_inline_assistant::init(fs.clone(), prompt_builder, client.telemetry().clone(), cx); cx.observe_new(move |workspace, window, cx| { ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx) }) @@ -391,7 +386,6 @@ fn register_slash_commands(cx: &mut App) { slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true); cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({ - let slash_command_registry = slash_command_registry.clone(); move |is_enabled, _cx| { if is_enabled { slash_command_registry.register_command( diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index ff5e9362dd3e6f734590e8d3a3efd4779c613316..04eb41793f2257a9dccfdd089594d2f90d0ce513 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -1129,7 +1129,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); let mut new_text = concat!( " let mut x = 0;\n", @@ -1196,7 +1196,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); cx.background_executor.run_until_parked(); @@ -1265,7 +1265,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); cx.background_executor.run_until_parked(); @@ -1334,7 +1334,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); let new_text = concat!( "func main() {\n", "\tx := 0\n", @@ -1391,7 +1391,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); chunks_tx .unbounded_send("let mut x = 0;\nx += 1;".to_string()) .unwrap(); @@ -1473,7 +1473,7 @@ mod tests { } fn simulate_response_stream( - codegen: Entity<CodegenAlternative>, + codegen: &Entity<CodegenAlternative>, cx: &mut TestAppContext, ) -> mpsc::UnboundedSender<String> { let (chunks_tx, chunks_rx) = mpsc::unbounded(); diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 0b4568dc87ddb33d5640c0313402c2a599be22d1..405b5ed90ba1606ef97b8b048b959bfc354bc5cd 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -818,13 +818,8 @@ pub fn crease_for_mention( let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any(); - Crease::inline( - range, - placeholder.clone(), - fold_toggle("mention"), - render_trailer, - ) - .with_metadata(CreaseMetadata { icon_path, label }) + Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer) + .with_metadata(CreaseMetadata { icon_path, label }) } fn render_fold_icon_button( diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index 747ec46e0a03d25df32b2b4bcb9dd2e915196f8c..020d799c799f4184494fc3d9ec6a0ef8119a9897 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -79,8 +79,7 @@ fn search( ) -> Task<Vec<Match>> { match mode { Some(ContextPickerMode::File) => { - let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + let search_files_task = search_files(query, cancellation_flag, &workspace, cx); cx.background_spawn(async move { search_files_task .await @@ -91,8 +90,7 @@ fn search( } Some(ContextPickerMode::Symbol) => { - let search_symbols_task = - search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx); + let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx); cx.background_spawn(async move { search_symbols_task .await @@ -108,13 +106,8 @@ fn search( .and_then(|t| t.upgrade()) .zip(text_thread_context_store.as_ref().and_then(|t| t.upgrade())) { - let search_threads_task = search_threads( - query.clone(), - cancellation_flag.clone(), - thread_store, - context_store, - cx, - ); + let search_threads_task = + search_threads(query, cancellation_flag, thread_store, context_store, cx); cx.background_spawn(async move { search_threads_task .await @@ -137,8 +130,7 @@ fn search( Some(ContextPickerMode::Rules) => { if let Some(prompt_store) = prompt_store.as_ref() { - let search_rules_task = - search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx); + let search_rules_task = search_rules(query, cancellation_flag, prompt_store, cx); cx.background_spawn(async move { search_rules_task .await @@ -196,7 +188,7 @@ fn search( let executor = cx.background_executor().clone(); let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + search_files(query.clone(), cancellation_flag, &workspace, cx); let entries = available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx); @@ -283,7 +275,7 @@ impl ContextPickerCompletionProvider { ) -> Option<Completion> { match entry { ContextPickerEntry::Mode(mode) => Some(Completion { - replace_range: source_range.clone(), + replace_range: source_range, new_text: format!("@{} ", mode.keyword()), label: CodeLabel::plain(mode.label().to_string(), None), icon_path: Some(mode.icon().path().into()), @@ -330,9 +322,6 @@ impl ContextPickerCompletionProvider { ); let callback = Arc::new({ - let context_store = context_store.clone(); - let selections = selections.clone(); - let selection_infos = selection_infos.clone(); move |_, window: &mut Window, cx: &mut App| { context_store.update(cx, |context_store, cx| { for (buffer, range) in &selections { @@ -441,7 +430,7 @@ impl ContextPickerCompletionProvider { excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), + editor, context_store.clone(), move |window, cx| match &thread_entry { ThreadContextEntry::Thread { id, .. } => { @@ -510,7 +499,7 @@ impl ContextPickerCompletionProvider { excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), + editor, context_store.clone(), move |_, cx| { let user_prompt_id = rules.prompt_id; @@ -547,7 +536,7 @@ impl ContextPickerCompletionProvider { excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), + editor, context_store.clone(), move |_, cx| { let context_store = context_store.clone(); @@ -704,16 +693,16 @@ impl ContextPickerCompletionProvider { excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), + editor, context_store.clone(), move |_, cx| { let symbol = symbol.clone(); let context_store = context_store.clone(); let workspace = workspace.clone(); let result = super::symbol_context_picker::add_symbol( - symbol.clone(), + symbol, false, - workspace.clone(), + workspace, context_store.downgrade(), cx, ); @@ -1162,7 +1151,7 @@ mod tests { impl Focusable for AtMentionEditor { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx).clone() + self.0.read(cx).focus_handle(cx) } } @@ -1480,7 +1469,7 @@ mod tests { let completions = editor.current_completions().expect("Missing completions"); completions .into_iter() - .map(|completion| completion.label.text.to_string()) + .map(|completion| completion.label.text) .collect::<Vec<_>>() } diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 90302236fb13eb9bed612affc4c40c80e2cacd3a..21115533400703e8ffa31f06054d19f46f226727 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -1693,7 +1693,7 @@ impl InlineAssist { }), range, codegen: codegen.clone(), - workspace: workspace.clone(), + workspace, _subscriptions: vec![ window.on_focus_in(&prompt_editor_focus_handle, cx, move |_, cx| { InlineAssistant::update_global(cx, |this, cx| { diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 845540979a9559e5eb0f573d0a039de3c2095917..3633e533da97b2b80e5c8d62c271da7121d3582b 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -93,7 +93,7 @@ impl LanguageModelPickerDelegate { let entries = models.entries(); Self { - on_model_changed: on_model_changed.clone(), + on_model_changed, all_models: Arc::new(models), selected_index: Self::get_active_model_index(&entries, get_active_model(cx)), filtered_entries: entries, @@ -514,7 +514,7 @@ impl PickerDelegate for LanguageModelPickerDelegate { .pl_0p5() .gap_1p5() .w(px(240.)) - .child(Label::new(model_info.model.name().0.clone()).truncate()), + .child(Label::new(model_info.model.name().0).truncate()), ) .end_slot(div().pr_3().when(is_selected, |this| { this.child( diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index f70d10c1ae63f1ea4a15b6685baa15bb23cf40ae..fdbce14415e4aba230ed1485197a318933fc168e 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -248,7 +248,7 @@ impl MessageEditor { editor: editor.clone(), project: thread.read(cx).project().clone(), thread, - incompatible_tools_state: incompatible_tools.clone(), + incompatible_tools_state: incompatible_tools, workspace, context_store, prompt_store, @@ -839,7 +839,6 @@ impl MessageEditor { .child(self.profile_selector.clone()) .child(self.model_selector.clone()) .map({ - let focus_handle = focus_handle.clone(); move |parent| { if is_generating { parent @@ -1801,7 +1800,7 @@ impl AgentPreview for MessageEditor { .bg(cx.theme().colors().panel_background) .border_1() .border_color(cx.theme().colors().border) - .child(default_message_editor.clone()) + .child(default_message_editor) .into_any_element(), )]) .into_any_element(), diff --git a/crates/agent_ui/src/profile_selector.rs b/crates/agent_ui/src/profile_selector.rs index ce25f531e254750fce36628f7d0138b728c9205a..f0f53b96b24c1d4f97fe94ecf155ebb7b73c6fa9 100644 --- a/crates/agent_ui/src/profile_selector.rs +++ b/crates/agent_ui/src/profile_selector.rs @@ -137,12 +137,11 @@ impl ProfileSelector { entry.handler({ let fs = self.fs.clone(); let provider = self.provider.clone(); - let profile_id = profile_id.clone(); move |_window, cx| { update_settings_file::<AgentSettings>(fs.clone(), cx, { let profile_id = profile_id.clone(); move |settings, _cx| { - settings.set_profile(profile_id.clone()); + settings.set_profile(profile_id); } }); @@ -175,7 +174,6 @@ impl Render for ProfileSelector { PopoverMenu::new("profile-selector") .trigger_with_tooltip(trigger_button, { - let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( "Toggle Profile Menu", diff --git a/crates/agent_ui/src/slash_command.rs b/crates/agent_ui/src/slash_command.rs index 6b37c5a2d7d6aaf2c9878efb90a22d11ddac2419..87e5d45fe85218c35316cb0a043caadf8837a2ea 100644 --- a/crates/agent_ui/src/slash_command.rs +++ b/crates/agent_ui/src/slash_command.rs @@ -88,8 +88,6 @@ impl SlashCommandCompletionProvider { .map(|(editor, workspace)| { let command_name = mat.string.clone(); let command_range = command_range.clone(); - let editor = editor.clone(); - let workspace = workspace.clone(); Arc::new( move |intent: CompletionIntent, window: &mut Window, @@ -158,7 +156,7 @@ impl SlashCommandCompletionProvider { if let Some(command) = self.slash_commands.command(command_name, cx) { let completions = command.complete_argument( arguments, - new_cancel_flag.clone(), + new_cancel_flag, self.workspace.clone(), window, cx, diff --git a/crates/agent_ui/src/terminal_inline_assistant.rs b/crates/agent_ui/src/terminal_inline_assistant.rs index 3859863ebed516de75962899234021137ea24996..e7070c0d7fc4878c1f73a6d5f874607422ae53d6 100644 --- a/crates/agent_ui/src/terminal_inline_assistant.rs +++ b/crates/agent_ui/src/terminal_inline_assistant.rs @@ -432,7 +432,7 @@ impl TerminalInlineAssist { terminal: terminal.downgrade(), prompt_editor: Some(prompt_editor.clone()), codegen: codegen.clone(), - workspace: workspace.clone(), + workspace, context_store, prompt_store, _subscriptions: vec![ diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index b3f55ffc43e9cbaf11cce6ebc1b38077098c9fc3..a928f7af545ff3fc8a5fcbfa4acc71eaaaa943ce 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1739,7 +1739,7 @@ impl TextThreadEditor { render_slash_command_output_toggle, |_, _, _, _| Empty.into_any(), ) - .with_metadata(metadata.crease.clone()) + .with_metadata(metadata.crease) }), cx, ); @@ -1810,7 +1810,7 @@ impl TextThreadEditor { .filter_map(|(anchor, render_image)| { const MAX_HEIGHT_IN_LINES: u32 = 8; let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap(); - let image = render_image.clone(); + let image = render_image; anchor.is_valid(&buffer).then(|| BlockProperties { placement: BlockPlacement::Above(anchor), height: Some(MAX_HEIGHT_IN_LINES), @@ -1873,7 +1873,7 @@ impl TextThreadEditor { } fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { - let focus_handle = self.focus_handle(cx).clone(); + let focus_handle = self.focus_handle(cx); let (style, tooltip) = match token_state(&self.context, cx) { Some(TokenState::NoTokensLeft { .. }) => ( @@ -2015,7 +2015,7 @@ impl TextThreadEditor { None => IconName::Ai, }; - let focus_handle = self.editor().focus_handle(cx).clone(); + let focus_handle = self.editor().focus_handle(cx); PickerPopoverMenu::new( self.language_model_selector.clone(), diff --git a/crates/agent_ui/src/ui/context_pill.rs b/crates/agent_ui/src/ui/context_pill.rs index 4e33e151cdc04686a5525ea9ed164237d86c90e9..7c7fbd27f0d4ebe3b5c42cc6c5a244ae6add5614 100644 --- a/crates/agent_ui/src/ui/context_pill.rs +++ b/crates/agent_ui/src/ui/context_pill.rs @@ -499,7 +499,7 @@ impl AddedContext { let thread = handle.thread.clone(); Some(Rc::new(move |_, cx| { let text = thread.read(cx).latest_detailed_summary_or_text(); - ContextPillHover::new_text(text.clone(), cx).into() + ContextPillHover::new_text(text, cx).into() })) }, handle: AgentContextHandle::Thread(handle), @@ -574,7 +574,7 @@ impl AddedContext { .unwrap_or_else(|| "Unnamed Rule".into()); Some(AddedContext { kind: ContextKind::Rules, - name: title.clone(), + name: title, parent: None, tooltip: None, icon_path: None, diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs index 0a34a290685746671a49a108ff770bde17fd08db..fadc4222ae44f3dbad862fd9479b89321dbd3016 100644 --- a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs +++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs @@ -33,7 +33,7 @@ impl ApiKeysWithProviders { .filter(|provider| { provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID }) - .map(|provider| (provider.icon(), provider.name().0.clone())) + .map(|provider| (provider.icon(), provider.name().0)) .collect() } } diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index 23810b74f3251629cfb3695ea651a7fc40f4c955..1a44fa3c17acca95ef970050c9eb512a0e3f2334 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -50,7 +50,7 @@ impl AgentPanelOnboarding { .filter(|provider| { provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID }) - .map(|provider| (provider.icon(), provider.name().0.clone())) + .map(|provider| (provider.icon(), provider.name().0)) .collect() } } diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 4d0bfae44443098abcac610529e5fcfe1cf778a8..12eda0954a2e1cca9ddc7df9816b8f5a37d0ce10 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -2282,7 +2282,7 @@ impl AssistantContext { let mut contents = self.contents(cx).peekable(); fn collect_text_content(buffer: &Buffer, range: Range<usize>) -> Option<String> { - let text: String = buffer.text_for_range(range.clone()).collect(); + let text: String = buffer.text_for_range(range).collect(); if text.trim().is_empty() { None } else { diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_context/src/assistant_context_tests.rs index 3db4a33b192d019a893609096a17cdffe504a37e..61d748cbddb0858dda2f181ea6c943426393e087 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_context/src/assistant_context_tests.rs @@ -1321,7 +1321,7 @@ fn test_summarize_error( fn setup_context_editor_with_fake_model( cx: &mut TestAppContext, ) -> (Entity<AssistantContext>, Arc<FakeLanguageModel>) { - let registry = Arc::new(LanguageRegistry::test(cx.executor().clone())); + let registry = Arc::new(LanguageRegistry::test(cx.executor())); let fake_provider = Arc::new(FakeLanguageModelProvider::default()); let fake_model = Arc::new(fake_provider.test_model()); @@ -1376,7 +1376,7 @@ fn messages_cache( context .read(cx) .messages(cx) - .map(|message| (message.id, message.cache.clone())) + .map(|message| (message.id, message.cache)) .collect() } diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index 6d13531a57de2b8b654ba4ce0c734fc575c659cb..6960d9db7948e8f09ea65ede91702d98e6bc99be 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -862,7 +862,7 @@ impl ContextStore { ContextServerStatus::Running => { self.load_context_server_slash_commands( server_id.clone(), - context_server_store.clone(), + context_server_store, cx, ); } diff --git a/crates/assistant_slash_commands/src/diagnostics_command.rs b/crates/assistant_slash_commands/src/diagnostics_command.rs index 10f950c86662524d97f0dcb454ef1e82dddfa7d6..8b1dbd515cabeb498d2a639387b426527dcda651 100644 --- a/crates/assistant_slash_commands/src/diagnostics_command.rs +++ b/crates/assistant_slash_commands/src/diagnostics_command.rs @@ -44,7 +44,7 @@ impl DiagnosticsSlashCommand { score: 0., positions: Vec::new(), worktree_id: entry.worktree_id.to_usize(), - path: entry.path.clone(), + path: entry.path, path_prefix: path_prefix.clone(), is_dir: false, // Diagnostics can't be produced for directories distance_to_relative_ancestor: 0, diff --git a/crates/assistant_slash_commands/src/prompt_command.rs b/crates/assistant_slash_commands/src/prompt_command.rs index 27029ac1567fa3833cb7c13d80f10ba60e2e3f2d..bbd6d3e3ad201c06940d6dc986616f61c8e15547 100644 --- a/crates/assistant_slash_commands/src/prompt_command.rs +++ b/crates/assistant_slash_commands/src/prompt_command.rs @@ -80,7 +80,7 @@ impl SlashCommand for PromptSlashCommand { }; let store = PromptStore::global(cx); - let title = SharedString::from(title.clone()); + let title = SharedString::from(title); let prompt = cx.spawn({ let title = title.clone(); async move |cx| { diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index ea2fa02663674c3aaae2dc8fe3198ec110d0f42e..4f182b31481c5d855b59f4398e104d0eea05bc74 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -1153,8 +1153,7 @@ impl EvalInput { .expect("Conversation must end with an edit_file tool use") .clone(); - let edit_file_input: EditFileToolInput = - serde_json::from_value(tool_use.input.clone()).unwrap(); + let edit_file_input: EditFileToolInput = serde_json::from_value(tool_use.input).unwrap(); EvalInput { conversation, @@ -1460,7 +1459,7 @@ impl EditAgentTest { async fn new(cx: &mut TestAppContext) -> Self { cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); cx.update(|cx| { settings::init(cx); gpui_tokio::init(cx); @@ -1475,7 +1474,7 @@ impl EditAgentTest { Project::init_settings(cx); language::init(cx); language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), client.clone(), cx); + language_models::init(user_store, client.clone(), cx); crate::init(client.http_client(), cx); }); diff --git a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs b/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs index 092bdce8b347ee5bcb5849703533710652b5b01c..2dba8a2b6d4dbf22868b7512b1a9675132a0edaf 100644 --- a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs +++ b/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs @@ -319,7 +319,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); assert_eq!(push(&mut finder, ""), None); assert_eq!(finish(finder), None); } @@ -333,7 +333,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); // Push partial query assert_eq!(push(&mut finder, "This"), None); @@ -365,7 +365,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); // Push a fuzzy query that should match the first function assert_eq!( @@ -391,7 +391,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); // No match initially assert_eq!(push(&mut finder, "Lin"), None); @@ -420,7 +420,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); // Push text in small chunks across line boundaries assert_eq!(push(&mut finder, "jumps "), None); // No newline yet @@ -458,7 +458,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); assert_eq!( push(&mut finder, "impl Debug for User {\n"), @@ -711,7 +711,7 @@ mod tests { "Expected to match `second_function` based on the line hint" ); - let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut matcher = StreamingFuzzyMatcher::new(snapshot); matcher.push(query, None); matcher.finish(); let best_match = matcher.select_best_match(); @@ -727,7 +727,7 @@ mod tests { let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.clone()); let snapshot = buffer.snapshot(); - let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut matcher = StreamingFuzzyMatcher::new(snapshot); // Split query into random chunks let chunks = to_random_chunks(rng, query); diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 33d08b4f88ba523254ccaf593019ea8692d19bdf..95b01c40eb96472caf85f239b2212f25e06fe9e2 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -376,7 +376,7 @@ impl Tool for EditFileTool { let output = EditFileToolOutput { original_path: project_path.path.to_path_buf(), - new_text: new_text.clone(), + new_text, old_text, raw_output: Some(agent_output), }; @@ -643,7 +643,7 @@ impl EditFileToolCard { diff }); - self.buffer = Some(buffer.clone()); + self.buffer = Some(buffer); self.base_text = Some(base_text.into()); self.buffer_diff = Some(buffer_diff.clone()); @@ -776,7 +776,6 @@ impl EditFileToolCard { let buffer_diff = cx.spawn({ let buffer = buffer.clone(); - let language_registry = language_registry.clone(); async move |_this, cx| { build_buffer_diff(base_text, &buffer, &language_registry, cx).await } @@ -863,7 +862,6 @@ impl ToolCard for EditFileToolCard { ) .on_click({ let path = self.path.clone(); - let workspace = workspace.clone(); move |_, window, cx| { workspace .update(cx, { diff --git a/crates/assistant_tools/src/grep_tool.rs b/crates/assistant_tools/src/grep_tool.rs index 1dd74b99e752b610a090f79ee1d894881f1fa5f8..41dde5bbfe49ad5bd82fdb5072c14f5728c518c5 100644 --- a/crates/assistant_tools/src/grep_tool.rs +++ b/crates/assistant_tools/src/grep_tool.rs @@ -327,7 +327,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), serde_json::json!({ @@ -415,7 +415,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), serde_json::json!({ @@ -494,7 +494,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); // Create test file with syntax structures fs.insert_tree( diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 358d62ee1a580ba0b011ba76035f55c413b3f1bc..b28e55e78aef40554a8ebe60108bd81da3f9d95a 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -350,7 +350,7 @@ fn process_content( if is_empty { "Command executed successfully.".to_string() } else { - content.to_string() + content } } Some(exit_status) => { diff --git a/crates/assistant_tools/src/ui/tool_call_card_header.rs b/crates/assistant_tools/src/ui/tool_call_card_header.rs index b71453373feb84d91168576a5bc7c22f8d883aa9..b41f19432f99685cf745f684228169b53939fffb 100644 --- a/crates/assistant_tools/src/ui/tool_call_card_header.rs +++ b/crates/assistant_tools/src/ui/tool_call_card_header.rs @@ -101,14 +101,11 @@ impl RenderOnce for ToolCallCardHeader { }) .when_some(secondary_text, |this, secondary_text| { this.child(bullet_divider()) - .child(div().text_size(font_size).child(secondary_text.clone())) + .child(div().text_size(font_size).child(secondary_text)) }) .when_some(code_path, |this, code_path| { - this.child(bullet_divider()).child( - Label::new(code_path.clone()) - .size(LabelSize::Small) - .inline_code(cx), - ) + this.child(bullet_divider()) + .child(Label::new(code_path).size(LabelSize::Small).inline_code(cx)) }) .with_animation( "loading-label", diff --git a/crates/assistant_tools/src/web_search_tool.rs b/crates/assistant_tools/src/web_search_tool.rs index 47a6958b7ad278f01fb654d23b68360d562d73e9..dbcca0a1f6f2d5f679fd240a5bfe64c6c9705256 100644 --- a/crates/assistant_tools/src/web_search_tool.rs +++ b/crates/assistant_tools/src/web_search_tool.rs @@ -193,10 +193,7 @@ impl ToolCard for WebSearchToolCard { ) } }) - .on_click({ - let url = url.clone(); - move |_, _, cx| cx.open_url(&url) - }) + .on_click(move |_, _, cx| cx.open_url(&url)) })) .into_any(), ), diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs index 63baef1f7d178045a2a2b5c976ede9ad75adb646..7063dffd6d84a13f8aa4d08ca91a6ce85826417d 100644 --- a/crates/auto_update_ui/src/auto_update_ui.rs +++ b/crates/auto_update_ui/src/auto_update_ui.rs @@ -114,7 +114,7 @@ fn view_release_notes_locally( cx, ); workspace.add_item_to_active_pane( - Box::new(markdown_preview.clone()), + Box::new(markdown_preview), None, true, window, diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 6b38fe557672726a690cca14b8e93085438726c0..6a9ca026e7a6d546adcd6b67263ab2c1c8947b9f 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -175,12 +175,8 @@ impl BufferDiffSnapshot { if let Some(text) = &base_text { let base_text_rope = Rope::from(text.as_str()); base_text_pair = Some((text.clone(), base_text_rope.clone())); - let snapshot = language::Buffer::build_snapshot( - base_text_rope, - language.clone(), - language_registry.clone(), - cx, - ); + let snapshot = + language::Buffer::build_snapshot(base_text_rope, language, language_registry, cx); base_text_snapshot = cx.background_spawn(snapshot); base_text_exists = true; } else { @@ -957,7 +953,7 @@ impl BufferDiff { .buffer_range .start; let end = self - .hunks_intersecting_range_rev(range.clone(), buffer) + .hunks_intersecting_range_rev(range, buffer) .next()? .buffer_range .end; @@ -1441,7 +1437,7 @@ mod tests { .unindent(); let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); - let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text.clone(), cx); + let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx); let mut uncommitted_diff = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx); uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff)); diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index c92226eeebd131170b0a5b04e4ed7f42c19a64fc..2a914330847053bc044da07e11642906b65a3159 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -438,7 +438,7 @@ fn init_test(cx: &mut App) -> Entity<ChannelStore> { let clock = Arc::new(FakeSystemClock::new()); let http = FakeHttpClient::with_404_response(); - let client = Client::new(clock, http.clone(), cx); + let client = Client::new(clock, http, cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); client::init(&client, cx); diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 925d5ddefbb1c892686fb36e5f34a10c967b13f2..b84e7a9f7a53a471bd854a15377c79f45003aaf4 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -926,7 +926,7 @@ mod mac_os { fn path(&self) -> PathBuf { match self { - Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed").clone(), + Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"), Bundle::LocalPath { executable, .. } => executable.clone(), } } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 058a12417a6f52a54f405eab655b2c9bb3b4fa5a..b6ce9d24e9a86bb5deb2e1f33879d6959f17b677 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -181,7 +181,7 @@ pub fn init(client: &Arc<Client>, cx: &mut App) { }); cx.on_action({ - let client = client.clone(); + let client = client; move |_: &Reconnect, cx| { if let Some(client) = client.upgrade() { cx.spawn(async move |cx| { @@ -791,7 +791,7 @@ impl Client { Arc::new(move |subscriber, envelope, client, cx| { let subscriber = subscriber.downcast::<E>().unwrap(); let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap(); - handler(subscriber, *envelope, client.clone(), cx).boxed_local() + handler(subscriber, *envelope, client, cx).boxed_local() }), ); if prev_handler.is_some() { @@ -2048,10 +2048,7 @@ mod tests { assert_eq!(*auth_count.lock(), 1); assert_eq!(*dropped_auth_count.lock(), 0); - let _authenticate = cx.spawn({ - let client = client.clone(); - |cx| async move { client.connect(false, &cx).await } - }); + let _authenticate = cx.spawn(|cx| async move { client.connect(false, &cx).await }); executor.run_until_parked(); assert_eq!(*auth_count.lock(), 2); assert_eq!(*dropped_auth_count.lock(), 1); diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 54b3d3f801ff45c7ef13ebadbd38b8a81f76d644..f3142a0af667d2022dfddd1c0f3f7b309e803d46 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -739,7 +739,7 @@ mod tests { ); // Third scan of worktree does not double report, as we already reported - test_project_discovery_helper(telemetry.clone(), vec!["package.json"], None, worktree_id); + test_project_discovery_helper(telemetry, vec!["package.json"], None, worktree_id); } #[gpui::test] @@ -751,7 +751,7 @@ mod tests { let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx)); test_project_discovery_helper( - telemetry.clone(), + telemetry, vec!["package.json", "pnpm-lock.yaml"], Some(vec!["node", "pnpm"]), 1, @@ -767,7 +767,7 @@ mod tests { let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx)); test_project_discovery_helper( - telemetry.clone(), + telemetry, vec!["package.json", "yarn.lock"], Some(vec!["node", "yarn"]), 1, @@ -786,7 +786,7 @@ mod tests { // project type for the same worktree multiple times test_project_discovery_helper( - telemetry.clone().clone(), + telemetry.clone(), vec!["global.json"], Some(vec!["dotnet"]), 1, diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index c500872fd787476d7b7197cd17c31de07575d5b6..da78a980693bec2243d872092a4f373698958b7a 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -280,7 +280,7 @@ pub async fn post_hang( service = "client", version = %report.app_version.unwrap_or_default().to_string(), os_name = %report.os_name, - os_version = report.os_version.unwrap_or_default().to_string(), + os_version = report.os_version.unwrap_or_default(), incident_id = %incident_id, installation_id = %report.installation_id.unwrap_or_default(), backtrace = %backtrace, diff --git a/crates/collab/src/auth.rs b/crates/collab/src/auth.rs index 5a2a1329bbf0cf50cbfbd47c80148ea93b70d37e..e484d6b510f444e764ac38210d6a5cfc42142807 100644 --- a/crates/collab/src/auth.rs +++ b/crates/collab/src/auth.rs @@ -236,7 +236,7 @@ mod test { #[gpui::test] async fn test_verify_access_token(cx: &mut gpui::TestAppContext) { - let test_db = crate::db::TestDb::sqlite(cx.executor().clone()); + let test_db = crate::db::TestDb::sqlite(cx.executor()); let db = test_db.db(); let user = db diff --git a/crates/collab/src/db/tests/embedding_tests.rs b/crates/collab/src/db/tests/embedding_tests.rs index 367e89f87bff827fe321b0935d52647a9034794a..5d8d69c0304d3a16b55e9d7b1477fe62cc22024a 100644 --- a/crates/collab/src/db/tests/embedding_tests.rs +++ b/crates/collab/src/db/tests/embedding_tests.rs @@ -8,7 +8,7 @@ use time::{Duration, OffsetDateTime, PrimitiveDateTime}; // SQLite does not support array arguments, so we only test this against a real postgres instance #[gpui::test] async fn test_get_embeddings_postgres(cx: &mut gpui::TestAppContext) { - let test_db = TestDb::postgres(cx.executor().clone()); + let test_db = TestDb::postgres(cx.executor()); let db = test_db.db(); let provider = "test_model"; @@ -38,7 +38,7 @@ async fn test_get_embeddings_postgres(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) { - let test_db = TestDb::postgres(cx.executor().clone()); + let test_db = TestDb::postgres(cx.executor()); let db = test_db.db(); let model = "test_model"; diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 01f553edf28dcaab516cb04bb8b29ffbf0232aac..06eb68610f0b0b97f42b54ae00c54754de646b0a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -310,7 +310,7 @@ impl Server { let mut server = Self { id: parking_lot::Mutex::new(id), peer: Peer::new(id.0 as u32), - app_state: app_state.clone(), + app_state, connection_pool: Default::default(), handlers: Default::default(), teardown: watch::channel(false).0, @@ -1386,9 +1386,7 @@ async fn create_room( let live_kit = live_kit?; let user_id = session.user_id().to_string(); - let token = live_kit - .room_token(&livekit_room, &user_id.to_string()) - .trace_err()?; + let token = live_kit.room_token(&livekit_room, &user_id).trace_err()?; Some(proto::LiveKitConnectionInfo { server_url: live_kit.url().into(), @@ -2015,9 +2013,9 @@ async fn join_project( .unzip(); response.send(proto::JoinProjectResponse { project_id: project.id.0 as u64, - worktrees: worktrees.clone(), + worktrees, replica_id: replica_id.0 as u32, - collaborators: collaborators.clone(), + collaborators, language_servers, language_server_capabilities, role: project.role.into(), diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 7b95fdd45803554492e2541a83e1ede526e11753..4e7996ce3b3d3b52f1a2fbf49565a0857f62b60c 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -3593,7 +3593,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let abs_path = project_a.read_with(cx_a, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -3647,20 +3647,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints_a.len()); @@ -3680,20 +3676,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints_a.len()); @@ -3713,20 +3705,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints_a.len()); @@ -3746,20 +3734,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(0, breakpoints_a.len()); diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index c283a9fcd1741ad62c15e4c514df0a81ffb42062..6fcd6d75cd0d827296f555bfa54c18dac518a3be 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -266,7 +266,7 @@ impl RandomizedTest for RandomChannelBufferTest { "client {user_id} has different text than client {prev_user_id} for channel {channel_name}", ); } else { - prev_text = Some((user_id, text.clone())); + prev_text = Some((user_id, text)); } // Assert that all clients and the server agree about who is present in the diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index cd4cf69f60663c90fd4525c8946aa34038f8b164..ac5c4c54ca570bf5545505419cb20a021ca97202 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -643,7 +643,7 @@ impl RandomizedTest for ProjectCollaborationTest { ); let project = project.await?; - client.dev_server_projects_mut().push(project.clone()); + client.dev_server_projects_mut().push(project); } ClientOperation::CreateWorktreeEntry { diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index f1c0b2d182a874ea985afb42311cce1edd1e1fa0..fd5e3eefc158034e8b15dc3fd7e401b1041fe08e 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -370,8 +370,8 @@ impl TestServer { let client = TestClient { app_state, username: name.to_string(), - channel_store: cx.read(ChannelStore::global).clone(), - notification_store: cx.read(NotificationStore::global).clone(), + channel_store: cx.read(ChannelStore::global), + notification_store: cx.read(NotificationStore::global), state: Default::default(), }; client.wait_for_current_user(cx).await; diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 9993c0841c0c88fb854764ce9044751f6955dfb9..61b3e05e48a9fe3da35957b05fcd7dbf7206f146 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -66,7 +66,7 @@ impl ChannelView { channel_id, link_position, pane.clone(), - workspace.clone(), + workspace, window, cx, ); diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 5ed3907f6cfdb3e5b5eba3fc4c930b6b2b0ffb18..8aaf6c0aa21f0b677ec091880fd8be674be1d6fe 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -1038,7 +1038,7 @@ impl Render for ChatPanel { .cloned(); el.when_some(reply_message, |el, reply_message| { - let user_being_replied_to = reply_message.sender.clone(); + let user_being_replied_to = reply_message.sender; el.child( h_flex() diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index b756984a09ba4ffecf3440ac298df4c40bfacab3..cd37549783118910048d1a9bc85adcd0e593209b 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2507,7 +2507,7 @@ impl CollabPanel { let button = match section { Section::ActiveCall => channel_link.map(|channel_link| { - let channel_link_copy = channel_link.clone(); + let channel_link_copy = channel_link; IconButton::new("channel-link", IconName::Copy) .icon_size(IconSize::Small) .size(ButtonSize::None) @@ -2691,7 +2691,7 @@ impl CollabPanel { h_flex() .w_full() .justify_between() - .child(Label::new(github_login.clone())) + .child(Label::new(github_login)) .child(h_flex().children(controls)), ) .start_slot(Avatar::new(user.avatar_uri.clone())) @@ -3125,7 +3125,7 @@ impl Panel for CollabPanel { impl Focusable for CollabPanel { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.filter_editor.focus_handle(cx).clone() + self.filter_editor.focus_handle(cx) } } diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index a900d585f8f0834c1d6be310f5a840ed939d0cf4..bf6fc3b224c54cd1512011987f73623c58d33c32 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -289,7 +289,7 @@ impl NotificationPanel { .gap_1() .size_full() .overflow_hidden() - .child(Label::new(text.clone())) + .child(Label::new(text)) .child( h_flex() .child( diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index b8800ff91284e6f105c029f7fffe9b4b83b6bcd1..227d246f04cecf8a5c58f2361d0b543ff678eac6 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -206,7 +206,7 @@ impl CommandPaletteDelegate { if parse_zed_link(&query, cx).is_some() { intercept_results = vec![CommandInterceptResult { action: OpenZedUrl { url: query.clone() }.boxed_clone(), - string: query.clone(), + string: query, positions: vec![], }] } diff --git a/crates/component/src/component_layout.rs b/crates/component/src/component_layout.rs index 58bf1d8f0c85533a4a06bd38c07f840c08cc6de3..a840d520a62b57516f20c190f2a5148505ccfed4 100644 --- a/crates/component/src/component_layout.rs +++ b/crates/component/src/component_layout.rs @@ -42,7 +42,7 @@ impl RenderOnce for ComponentExample { div() .text_size(rems(0.875)) .text_color(cx.theme().colors().text_muted) - .child(description.clone()), + .child(description), ) }), ) diff --git a/crates/context_server/src/listener.rs b/crates/context_server/src/listener.rs index 1b44cefbd294ab7aea8da0426d6bf0aa717d402b..4e5da2566ee25ee70e1687cf5f0806e19789a824 100644 --- a/crates/context_server/src/listener.rs +++ b/crates/context_server/src/listener.rs @@ -112,7 +112,6 @@ impl McpServer { annotations: Some(tool.annotations()), }, handler: Box::new({ - let tool = tool.clone(); move |input_value, cx| { let input = match input_value { Some(input) => serde_json::from_value(input), diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 1916853a692f5123fd0e09fbb14491368448622a..33455f5e52110e157a3d888a2e71b572e39f2fa1 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -81,10 +81,7 @@ pub fn init( }; copilot_chat::init(fs.clone(), http.clone(), configuration, cx); - let copilot = cx.new({ - let node_runtime = node_runtime.clone(); - move |cx| Copilot::start(new_server_id, fs, node_runtime, cx) - }); + let copilot = cx.new(move |cx| Copilot::start(new_server_id, fs, node_runtime, cx)); Copilot::set_global(copilot.clone(), cx); cx.observe(&copilot, |copilot, cx| { copilot.update(cx, |copilot, cx| copilot.update_action_visibilities(cx)); diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 2fd6df27b9e15d4247d85edca4d8836c35b23df1..9308500ed49f8f619f87aba585ee5e6b00a350ae 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -1083,7 +1083,7 @@ mod tests { let replace_range_marker: TextRangeMarker = ('<', '>').into(); let (_, mut marked_ranges) = marked_text_ranges_by( marked_string, - vec![complete_from_marker.clone(), replace_range_marker.clone()], + vec![complete_from_marker, replace_range_marker.clone()], ); let replace_range = diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 131272da6b031a04cd9c4e5421a7ff024b73cfc2..c4338c6d0017a215c721c772871647c89227775e 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -664,7 +664,7 @@ impl ToolbarItemView for DapLogToolbarItemView { if let Some(item) = active_pane_item && let Some(log_view) = item.downcast::<DapLogView>() { - self.log_view = Some(log_view.clone()); + self.log_view = Some(log_view); return workspace::ToolbarItemLocation::PrimaryLeft; } self.log_view = None; diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 6c70a935e023dd0b3c7918f4edd3c3926f31f464..f81c1fff89a965ae99205d405d1afa52bdde813a 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -386,10 +386,10 @@ impl DebugPanel { return; }; - let dap_store_handle = self.project.read(cx).dap_store().clone(); + let dap_store_handle = self.project.read(cx).dap_store(); let label = curr_session.read(cx).label(); let quirks = curr_session.read(cx).quirks(); - let adapter = curr_session.read(cx).adapter().clone(); + let adapter = curr_session.read(cx).adapter(); let binary = curr_session.read(cx).binary().cloned().unwrap(); let task_context = curr_session.read(cx).task_context().clone(); @@ -447,9 +447,9 @@ impl DebugPanel { return; }; - let dap_store_handle = self.project.read(cx).dap_store().clone(); + let dap_store_handle = self.project.read(cx).dap_store(); let label = self.label_for_child_session(&parent_session, request, cx); - let adapter = parent_session.read(cx).adapter().clone(); + let adapter = parent_session.read(cx).adapter(); let quirks = parent_session.read(cx).quirks(); let Some(mut binary) = parent_session.read(cx).binary().cloned() else { log::error!("Attempted to start a child-session without a binary"); @@ -932,7 +932,6 @@ impl DebugPanel { .cloned(), |this, running_state| { this.children({ - let running_state = running_state.clone(); let threads = running_state.update(cx, |running_state, cx| { let session = running_state.session(); @@ -1645,7 +1644,6 @@ impl Render for DebugPanel { } }) .on_action({ - let this = this.clone(); move |_: &ToggleSessionPicker, window, cx| { this.update(cx, |this, cx| { this.toggle_session_picker(window, cx); diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 5f5dfd1a1e6a543cdb7a4d87e1b8e9984c4ecba9..581cc16ff4c9a41e24036bcd3ff000ad47d3a076 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -272,7 +272,6 @@ pub fn init(cx: &mut App) { } }) .on_action({ - let active_item = active_item.clone(); move |_: &ToggleIgnoreBreakpoints, _, cx| { active_item .update(cx, |item, cx| item.toggle_ignore_breakpoints(cx)) @@ -293,9 +292,8 @@ pub fn init(cx: &mut App) { let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else { return; }; - let Some(active_session) = debug_panel - .clone() - .update(cx, |panel, _| panel.active_session()) + let Some(active_session) = + debug_panel.update(cx, |panel, _| panel.active_session()) else { return; }; diff --git a/crates/debugger_ui/src/dropdown_menus.rs b/crates/debugger_ui/src/dropdown_menus.rs index dca15eb0527cfc78bd137889a1910e6b32abf98c..c5399f6f69648dcfa775a6dd6da62bd637124f2c 100644 --- a/crates/debugger_ui/src/dropdown_menus.rs +++ b/crates/debugger_ui/src/dropdown_menus.rs @@ -272,10 +272,9 @@ impl DebugPanel { .child(session_entry.label_element(self_depth, cx)) .child( IconButton::new("close-debug-session", IconName::Close) - .visible_on_hover(id.clone()) + .visible_on_hover(id) .icon_size(IconSize::Small) .on_click({ - let weak = weak.clone(); move |_, window, cx| { weak.update(cx, |panel, cx| { panel.close_session(session_entity_id, window, cx); diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index eb0ad92dcc64b6ac6925001006df7df07bcc8d10..b30e3995ffdb994fdb9c936821b360ef7e6eff04 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -785,7 +785,7 @@ impl RenderOnce for AttachMode { v_flex() .w_full() .track_focus(&self.attach_picker.focus_handle(cx)) - .child(self.attach_picker.clone()) + .child(self.attach_picker) } } diff --git a/crates/debugger_ui/src/persistence.rs b/crates/debugger_ui/src/persistence.rs index f0d7fd6fddff66ed7846db6fa9e712e11436ea42..cff2ba83355208d702cf7936c46ea5b167c7c649 100644 --- a/crates/debugger_ui/src/persistence.rs +++ b/crates/debugger_ui/src/persistence.rs @@ -256,7 +256,7 @@ pub(crate) fn deserialize_pane_layout( Some(Member::Axis(PaneAxis::load( if should_invert { axis.invert() } else { axis }, members, - flexes.clone(), + flexes, ))) } SerializedPaneLayout::Pane(serialized_pane) => { diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index e3682ac991cbb46ed41a7c51e30d48df12bfad8e..4306104877539139fc7522fbadf6da699ec8c7b6 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -180,7 +180,7 @@ impl SubView { let weak_list = list.downgrade(); let focus_handle = list.focus_handle(cx); let this = Self::new( - focus_handle.clone(), + focus_handle, list.into(), DebuggerPaneItem::BreakpointList, cx, @@ -1167,9 +1167,9 @@ impl RunningState { id: task::TaskId("debug".to_string()), full_label: title.clone(), label: title.clone(), - command: command.clone(), + command, args, - command_label: title.clone(), + command_label: title, cwd, env: envs, use_new_terminal: true, @@ -1756,7 +1756,7 @@ impl RunningState { this.activate_item(0, false, false, window, cx); }); - let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx); + let rightmost_pane = new_debugger_pane(workspace.clone(), project, window, cx); rightmost_pane.update(cx, |this, cx| { this.add_item( Box::new(SubView::new( diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index c17fffc42cfde97d2657eb481c28396c71c65c9f..d04443e20131de5cfec86b77a7d7bb68b186da10 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -685,7 +685,6 @@ impl BreakpointList { selection_kind.map(|kind| kind.0) != Some(SelectedBreakpointKind::Source), ) .on_click({ - let focus_handle = focus_handle.clone(); move |_, window, cx| { focus_handle.focus(window); window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx) @@ -1139,7 +1138,6 @@ impl ExceptionBreakpoint { } }) .on_click({ - let list = list.clone(); move |_, _, cx| { list.update(cx, |this, cx| { this.toggle_exception_breakpoint(&id, cx); diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 05d2231da45ec81c0db05a6175dd69563eb8a024..a801cedd26924198d6a0639a3832ab0253b53adb 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -365,7 +365,7 @@ impl Console { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target.clone()) + el.context(keybinding_target) }) .action("Watch Expression", WatchExpression.boxed_clone()) })) diff --git a/crates/debugger_ui/src/session/running/loaded_source_list.rs b/crates/debugger_ui/src/session/running/loaded_source_list.rs index 6b376bb892e1ea5aae64a1d5873b91487e65f3c2..921ebd8b5f5bdfe8a3c8a8f7bb1625bd1ffad7fb 100644 --- a/crates/debugger_ui/src/session/running/loaded_source_list.rs +++ b/crates/debugger_ui/src/session/running/loaded_source_list.rs @@ -57,7 +57,7 @@ impl LoadedSourceList { h_flex() .text_ui_xs(cx) .text_color(cx.theme().colors().text_muted) - .when_some(source.path.clone(), |this, path| this.child(path)), + .when_some(source.path, |this, path| this.child(path)), ) .into_any() } diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index a09df6e728d061307b445e4b4048a04151d87cdf..e7b7963d3f0775c18489ceb306badce1725a6268 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -461,7 +461,7 @@ impl MemoryView { let data_breakpoint_info = this.data_breakpoint_info(context.clone(), None, cx); cx.spawn(async move |this, cx| { if let Some(info) = data_breakpoint_info.await { - let Some(data_id) = info.data_id.clone() else { + let Some(data_id) = info.data_id else { return; }; _ = this.update(cx, |this, cx| { diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index 74a9fb457a57cf2e70af694ed586af2227ee4a0a..1c1e0f3efc552abdcca8d8fce6e215b24fe0b2ac 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -157,7 +157,7 @@ impl ModuleList { h_flex() .text_ui_xs(cx) .text_color(cx.theme().colors().text_muted) - .when_some(module.path.clone(), |this, path| this.child(path)), + .when_some(module.path, |this, path| this.child(path)), ) .into_any() } diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index 8b44c231c37dfe9bc3deb9905699e2c80df8897f..f9b5ed5e3f9c3068ce23bdd9fa3a653d90356523 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -126,7 +126,7 @@ impl StackFrameList { self.stack_frames(cx) .unwrap_or_default() .into_iter() - .map(|stack_frame| stack_frame.dap.clone()) + .map(|stack_frame| stack_frame.dap) .collect() } @@ -224,7 +224,7 @@ impl StackFrameList { let collapsed_entries = std::mem::take(&mut collapsed_entries); if !collapsed_entries.is_empty() { - entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone())); + entries.push(StackFrameEntry::Collapsed(collapsed_entries)); } self.entries = entries; @@ -418,7 +418,7 @@ impl StackFrameList { let source = stack_frame.source.clone(); let is_selected_frame = Some(ix) == self.selected_ix; - let path = source.clone().and_then(|s| s.path.or(s.name)); + let path = source.and_then(|s| s.path.or(s.name)); let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,)); let formatted_path = formatted_path.map(|path| { Label::new(path) diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index 7461bffdf94625bea545ab4141906ac3d3a8b8c7..18f574389e0838daf14ea7381d4d3b6415e5f323 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -313,7 +313,7 @@ impl VariableList { watcher.variables_reference, watcher.variables_reference, EntryPath::for_watcher(watcher.expression.clone()), - DapEntry::Watcher(watcher.clone()), + DapEntry::Watcher(watcher), ) }) .collect::<Vec<_>>(), @@ -1301,8 +1301,6 @@ impl VariableList { IconName::Close, ) .on_click({ - let weak = weak.clone(); - let path = path.clone(); move |_, window, cx| { weak.update(cx, |variable_list, cx| { variable_list.selection = Some(path.clone()); @@ -1470,7 +1468,6 @@ impl VariableList { })) }) .on_secondary_mouse_down(cx.listener({ - let path = path.clone(); let entry = variable.clone(); move |this, event: &MouseDownEvent, window, cx| { this.selection = Some(path.clone()); diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 6180831ea9dccfb3c1ee861daac099e54b2242c3..ab6d5cb9605d5d774187f836130fdae66a8d3404 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -1330,7 +1330,6 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action( let called_set_breakpoints = Arc::new(AtomicBool::new(false)); client.on_request::<SetBreakpoints, _>({ - let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { assert!( args.breakpoints.is_none_or(|bps| bps.is_empty()), @@ -1445,7 +1444,6 @@ async fn test_we_send_arguments_from_user_config( let launch_handler_called = Arc::new(AtomicBool::new(false)); start_debug_session_with(&workspace, cx, debug_definition.clone(), { - let debug_definition = debug_definition.clone(); let launch_handler_called = launch_handler_called.clone(); move |client| { @@ -1783,9 +1781,8 @@ async fn test_debug_adapters_shutdown_on_app_quit( let disconnect_request_received = Arc::new(AtomicBool::new(false)); let disconnect_clone = disconnect_request_received.clone(); - let disconnect_clone_for_handler = disconnect_clone.clone(); client.on_request::<Disconnect, _>(move |_, _| { - disconnect_clone_for_handler.store(true, Ordering::SeqCst); + disconnect_clone.store(true, Ordering::SeqCst); Ok(()) }); diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index 5ac6af389d23eecba8180f9f7ddf7de279f2cc83..bfc445cf67329b7190f8e5b8d353415fb53fcd74 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -106,9 +106,7 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths( ); let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") { - input_path - .replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path")) - .to_owned() + input_path.replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path")) } else { input_path.to_string() }; diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 99e588ada9a70d59b3ba41c92323854b4814e112..33158577c4e313dab051976854467a1d3c9019bd 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -61,15 +61,13 @@ impl PreprocessorError { for alias in action.deprecated_aliases { if alias == &action_name { return PreprocessorError::DeprecatedActionUsed { - used: action_name.clone(), + used: action_name, should_be: action.name.to_string(), }; } } } - PreprocessorError::ActionNotFound { - action_name: action_name.to_string(), - } + PreprocessorError::ActionNotFound { action_name } } } diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 21c934fefaa480dd8c58e3927e21ef0a3c14a2b7..4f69af7ee414a664af623d1bf980520ade6a4a49 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -168,7 +168,7 @@ impl Render for EditPredictionButton { let account_status = agent.account_status.clone(); match account_status { AccountStatus::NeedsActivation { activate_url } => { - SupermavenButtonStatus::NeedsActivation(activate_url.clone()) + SupermavenButtonStatus::NeedsActivation(activate_url) } AccountStatus::Unknown => SupermavenButtonStatus::Initializing, AccountStatus::Ready => SupermavenButtonStatus::Ready, diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 4847bc25658d8ff12dc3ee6c3a74d2fae2048f36..96809d68777ca8d84623c308bb8b06eec493a5be 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -514,7 +514,7 @@ impl CompletionsMenu { // Expand the range to resolve more completions than are predicted to be visible, to reduce // jank on navigation. let entry_indices = util::expanded_and_wrapped_usize_range( - entry_range.clone(), + entry_range, RESOLVE_BEFORE_ITEMS, RESOLVE_AFTER_ITEMS, entries.len(), diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 0d31398a54deffee61176550f6f8da804b450297..1e0cdc34ac6824bfc3500fdfc2b3a0905ac21dbe 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -2156,7 +2156,7 @@ mod tests { } let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx); @@ -2275,7 +2275,7 @@ mod tests { new_heights.insert(block_ids[0], 3); block_map_writer.resize(new_heights); - let snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); + let snapshot = block_map.read(wraps_snapshot, Default::default()); // Same height as before, should remain the same assert_eq!(snapshot.text(), "aaa\n\n\n\n\n\nbbb\nccc\nddd\n\n\n"); } @@ -2360,16 +2360,14 @@ mod tests { buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx); buffer.snapshot(cx) }); - let (inlay_snapshot, inlay_edits) = inlay_map.sync( - buffer_snapshot.clone(), - buffer_subscription.consume().into_inner(), - ); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, buffer_subscription.consume().into_inner()); let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { wrap_map.sync(tab_snapshot, tab_edits, cx) }); - let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits); + let blocks_snapshot = block_map.read(wraps_snapshot, wrap_edits); assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { @@ -2454,7 +2452,7 @@ mod tests { // Removing the replace block shows all the hidden blocks again. let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); writer.remove(HashSet::from_iter([replace_block_id])); - let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); + let blocks_snapshot = block_map.read(wraps_snapshot, Default::default()); assert_eq!( blocks_snapshot.text(), "\nline1\n\nline2\n\n\nline 2.1\nline2.2\nline 2.3\nline 2.4\n\nline4\n\nline5" @@ -2793,7 +2791,7 @@ mod tests { buffer.read_with(cx, |buffer, cx| { writer.fold_buffers([buffer_id_3], buffer, cx); }); - let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default()); + let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default()); let blocks = blocks_snapshot .blocks_in_range(0..u32::MAX) .collect::<Vec<_>>(); @@ -2846,7 +2844,7 @@ mod tests { assert_eq!(buffer_ids.len(), 1); let buffer_id = buffer_ids[0]; - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (_, wrap_snapshot) = @@ -2860,7 +2858,7 @@ mod tests { buffer.read_with(cx, |buffer, cx| { writer.fold_buffers([buffer_id], buffer, cx); }); - let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default()); + let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default()); let blocks = blocks_snapshot .blocks_in_range(0..u32::MAX) .collect::<Vec<_>>(); @@ -3527,7 +3525,7 @@ mod tests { ..buffer_snapshot.anchor_after(Point::new(1, 0))], false, ); - let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); + let blocks_snapshot = block_map.read(wraps_snapshot, Default::default()); assert_eq!(blocks_snapshot.text(), "abc\n\ndef\nghi\njkl\nmno"); } diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 3dcd172c3c484a51456a86c4f242b959b7a732f3..42f46fb74969301007d19032f1b96377d141a724 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1557,7 +1557,7 @@ mod tests { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot, vec![]); @@ -1636,7 +1636,7 @@ mod tests { let buffer = MultiBuffer::build_simple("abcdefghijkl", cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); { let mut map = FoldMap::new(inlay_snapshot.clone()).0; @@ -1712,7 +1712,7 @@ mod tests { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); @@ -1720,7 +1720,7 @@ mod tests { (Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()), (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()), ]); - let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + let (snapshot, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { @@ -1747,7 +1747,7 @@ mod tests { (Point::new(1, 2)..Point::new(3, 2), FoldPlaceholder::test()), (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()), ]); - let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + let (snapshot, _) = map.read(inlay_snapshot, vec![]); let fold_ranges = snapshot .folds_in_range(Point::new(1, 0)..Point::new(1, 3)) .map(|fold| { @@ -1782,7 +1782,7 @@ mod tests { let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); let mut map = FoldMap::new(inlay_snapshot.clone()).0; - let (mut initial_snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + let (mut initial_snapshot, _) = map.read(inlay_snapshot, vec![]); let mut snapshot_edits = Vec::new(); let mut next_inlay_id = 0; diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index eb5d57d48472bdd4b2d4f150c40da05c7e422e19..6f5df9bb8e658b95260dde4feb2b00c177c98520 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -116,7 +116,7 @@ impl TabMap { state.new.end = edit.new.end; Some(None) // Skip this edit, it's merged } else { - let new_state = edit.clone(); + let new_state = edit; let result = Some(Some(state.clone())); // Yield the previous edit **state = new_state; result @@ -611,7 +611,7 @@ mod tests { fn test_expand_tabs(cx: &mut gpui::App) { let buffer = MultiBuffer::build_simple("", cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -628,7 +628,7 @@ mod tests { let buffer = MultiBuffer::build_simple(input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -675,7 +675,7 @@ mod tests { let buffer = MultiBuffer::build_simple(input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -689,7 +689,7 @@ mod tests { let buffer = MultiBuffer::build_simple(input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -749,7 +749,7 @@ mod tests { let buffer_snapshot = buffer.read(cx).snapshot(cx); log::info!("Buffer text: {:?}", buffer_snapshot.text()); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); log::info!("InlayMap text: {:?}", inlay_snapshot.text()); let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone()); fold_map.randomly_mutate(&mut rng); @@ -758,7 +758,7 @@ mod tests { let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng); log::info!("InlayMap text: {:?}", inlay_snapshot.text()); - let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); + let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size); let tabs_snapshot = tab_map.set_max_expansion_column(32); let text = text::Rope::from(tabs_snapshot.text().as_str()); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2f3ced65dc1a6c9cf481d613cd715c39bc9e5aab..5fc017dcfc55c754a466a23edd53873116b00c63 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4528,7 +4528,7 @@ impl Editor { let mut char_position = 0u32; let mut end_tag_offset = None; - 'outer: for chunk in snapshot.text_for_range(range.clone()) { + 'outer: for chunk in snapshot.text_for_range(range) { if let Some(byte_pos) = chunk.find(&**end_tag) { let chars_before_match = chunk[..byte_pos].chars().count() as u32; @@ -4881,7 +4881,7 @@ impl Editor { let multibuffer = self.buffer.read(cx); let Some(buffer) = position .buffer_id - .and_then(|buffer_id| multibuffer.buffer(buffer_id).clone()) + .and_then(|buffer_id| multibuffer.buffer(buffer_id)) else { return false; }; @@ -6269,7 +6269,7 @@ impl Editor { })) } CodeActionsItem::DebugScenario(scenario) => { - let context = actions_menu.actions.context.clone(); + let context = actions_menu.actions.context; workspace.update(cx, |workspace, cx| { dap::send_telemetry(&scenario, TelemetrySpawnLocation::Gutter, cx); @@ -6469,7 +6469,7 @@ impl Editor { fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<()> { let newest_selection = self.selections.newest_anchor().clone(); - let newest_selection_adjusted = self.selections.newest_adjusted(cx).clone(); + let newest_selection_adjusted = self.selections.newest_adjusted(cx); let buffer = self.buffer.read(cx); if newest_selection.head().diff_base_anchor.is_some() { return None; @@ -8188,8 +8188,6 @@ impl Editor { .icon_color(color) .style(ButtonStyle::Transparent) .on_click(cx.listener({ - let breakpoint = breakpoint.clone(); - move |editor, event: &ClickEvent, window, cx| { let edit_action = if event.modifiers().platform || breakpoint.is_disabled() { BreakpointEditAction::InvertState @@ -14837,7 +14835,7 @@ impl Editor { if parent == child { return None; } - let text = buffer.text_for_range(child.clone()).collect::<String>(); + let text = buffer.text_for_range(child).collect::<String>(); Some((selection.id, parent, text)) }) .collect::<Vec<_>>(); @@ -15940,7 +15938,7 @@ impl Editor { if !split && Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() { - editor.go_to_singleton_buffer_range(range.clone(), window, cx); + editor.go_to_singleton_buffer_range(range, window, cx); } else { window.defer(cx, move |window, cx| { let target_editor: Entity<Self> = @@ -16198,14 +16196,14 @@ impl Editor { let item_id = item.item_id(); if split { - workspace.split_item(SplitDirection::Right, item.clone(), window, cx); + workspace.split_item(SplitDirection::Right, item, window, cx); } else if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { let (preview_item_id, preview_item_idx) = workspace.active_pane().read_with(cx, |pane, _| { (pane.preview_item_id(), pane.preview_item_idx()) }); - workspace.add_item_to_active_pane(item.clone(), preview_item_idx, true, window, cx); + workspace.add_item_to_active_pane(item, preview_item_idx, true, window, cx); if let Some(preview_item_id) = preview_item_id { workspace.active_pane().update(cx, |pane, cx| { @@ -16213,7 +16211,7 @@ impl Editor { }); } } else { - workspace.add_item_to_active_pane(item.clone(), None, true, window, cx); + workspace.add_item_to_active_pane(item, None, true, window, cx); } workspace.active_pane().update(cx, |pane, cx| { pane.set_preview_item_id(Some(item_id), cx); @@ -19004,10 +19002,7 @@ impl Editor { let selection = text::ToPoint::to_point(&range.start, buffer).row ..text::ToPoint::to_point(&range.end, buffer).row; - Some(( - multi_buffer.buffer(buffer.remote_id()).unwrap().clone(), - selection, - )) + Some((multi_buffer.buffer(buffer.remote_id()).unwrap(), selection)) }); let Some((buffer, selection)) = buffer_and_selection else { @@ -19249,7 +19244,7 @@ impl Editor { row_highlights.insert( ix, RowHighlight { - range: range.clone(), + range, index, color, options, @@ -21676,7 +21671,7 @@ fn wrap_with_prefix( let subsequent_lines_prefix_len = char_len_with_expanded_tabs(0, &subsequent_lines_prefix, tab_size); let mut wrapped_text = String::new(); - let mut current_line = first_line_prefix.clone(); + let mut current_line = first_line_prefix; let mut is_first_line = true; let tokenizer = WordBreakingTokenizer::new(&unwrapped_text); diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index dc5557b05277da972ea36ba43ffdf08a565edda9..91022d94a8843a2e9b7e9c77137d4d2ba57bfa7f 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -88,7 +88,7 @@ impl RenderOnce for BufferFontFamilyControl { .child(Icon::new(IconName::Font)) .child(DropdownMenu::new( "buffer-font-family", - value.clone(), + value, ContextMenu::build(window, cx, |mut menu, _, cx| { let font_family_cache = FontFamilyCache::global(cx); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 1f1239ba0a07891114489fe103b3b0e5020d562a..955ade04cd1fc29d9ae7bfe0060271e698c6e4e2 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -708,7 +708,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { _ = workspace.update(cx, |_v, window, cx| { cx.new(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); - let mut editor = build_editor(buffer.clone(), window, cx); + let mut editor = build_editor(buffer, window, cx); let handle = cx.entity(); editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); @@ -898,7 +898,7 @@ fn test_fold_action(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -989,7 +989,7 @@ fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -1074,7 +1074,7 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -1173,7 +1173,7 @@ fn test_fold_at_level(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -1335,7 +1335,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); assert_eq!('🟥'.len_utf8(), 4); @@ -1452,7 +1452,7 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { @@ -2479,7 +2479,7 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("one two three four", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -2527,7 +2527,7 @@ fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); let del_to_prev_word_start = DeleteToPreviousWordStart { ignore_newlines: false, @@ -2563,7 +2563,7 @@ fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); let del_to_next_word_end = DeleteToNextWordEnd { ignore_newlines: false, @@ -2608,7 +2608,7 @@ fn test_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -2644,7 +2644,7 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { .as_str(), cx, ); - let mut editor = build_editor(buffer.clone(), window, cx); + let mut editor = build_editor(buffer, window, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([ Point::new(2, 4)..Point::new(2, 5), @@ -3175,7 +3175,7 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); - let mut editor = build_editor(buffer.clone(), window, cx); + let mut editor = build_editor(buffer, window, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([3..4, 11..12, 19..20]) }); @@ -5562,7 +5562,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { # ˇThis is a long comment using a pound # sign. "}, - python_language.clone(), + python_language, &mut cx, ); @@ -5669,7 +5669,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { also very long and should not merge with the numbered item.ˇ» "}, - markdown_language.clone(), + markdown_language, &mut cx, ); @@ -5700,7 +5700,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { // This is the second long comment block // to be wrapped.ˇ» "}, - rust_language.clone(), + rust_language, &mut cx, ); @@ -5723,7 +5723,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { «\tThis is a very long indented line \tthat will be wrapped.ˇ» "}, - plaintext_language.clone(), + plaintext_language, &mut cx, ); @@ -8889,7 +8889,7 @@ async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) { )); cx.language_registry().add(html_language.clone()); - cx.language_registry().add(javascript_language.clone()); + cx.language_registry().add(javascript_language); cx.executor().run_until_parked(); cx.update_buffer(|buffer, cx| { @@ -9633,7 +9633,7 @@ async fn test_snippets(cx: &mut TestAppContext) { .selections .all(cx) .iter() - .map(|s| s.range().clone()) + .map(|s| s.range()) .collect::<Vec<_>>(); editor .insert_snippet(&insertion_ranges, snippet, window, cx) @@ -9713,7 +9713,7 @@ async fn test_snippet_indentation(cx: &mut TestAppContext) { .selections .all(cx) .iter() - .map(|s| s.range().clone()) + .map(|s| s.range()) .collect::<Vec<_>>(); editor .insert_snippet(&insertion_ranges, snippet, window, cx) @@ -10782,7 +10782,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { kind: Some("code-action-2".into()), edit: Some(lsp::WorkspaceEdit::new( [( - uri.clone(), + uri, vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), "applied-code-action-2-edit\n".to_string(), @@ -14366,7 +14366,7 @@ async fn test_toggle_block_comment(cx: &mut TestAppContext) { )); cx.language_registry().add(html_language.clone()); - cx.language_registry().add(javascript_language.clone()); + cx.language_registry().add(javascript_language); cx.update_buffer(|buffer, cx| { buffer.set_language(Some(html_language), cx); }); @@ -14543,7 +14543,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { ); let excerpt_ranges = markers.into_iter().map(|marker| { let context = excerpt_ranges.remove(&marker).unwrap()[0].clone(); - ExcerptRange::new(context.clone()) + ExcerptRange::new(context) }); let buffer = cx.new(|cx| Buffer::local(initial_text, cx)); let multibuffer = cx.new(|cx| { @@ -14828,7 +14828,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -15750,8 +15750,7 @@ async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppCon cx.simulate_keystroke("\n"); cx.run_until_parked(); - let buffer_cloned = - cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone()); + let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap()); let mut request = cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| { let buffer_cloned = buffer_cloned.clone(); @@ -19455,7 +19454,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) .collect::<Vec<_>>() }); assert_eq!(hunk_ranges.len(), 2); @@ -19546,7 +19545,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) .collect::<Vec<_>>() }); assert_eq!(hunk_ranges.len(), 2); @@ -19612,7 +19611,7 @@ async fn test_toggle_deletion_hunk_at_start_of_file( let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) .collect::<Vec<_>>() }); assert_eq!(hunk_ranges.len(), 1); @@ -19635,7 +19634,7 @@ async fn test_toggle_deletion_hunk_at_start_of_file( }); executor.run_until_parked(); - cx.assert_state_with_diff(hunk_expanded.clone()); + cx.assert_state_with_diff(hunk_expanded); } #[gpui::test] @@ -21150,7 +21149,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -21168,7 +21167,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -21193,7 +21191,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -21215,7 +21212,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(0, breakpoints.len()); @@ -21267,7 +21263,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -21282,7 +21278,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -21303,7 +21298,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint(&breakpoints, &abs_path, vec![]); @@ -21323,7 +21317,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -21346,7 +21339,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -21369,7 +21361,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -21442,7 +21433,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -21462,7 +21453,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -21494,7 +21484,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let disable_breakpoint = { @@ -21530,7 +21519,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -22509,10 +22497,7 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) { let closing_range = buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8)); let mut linked_ranges = HashMap::default(); - linked_ranges.insert( - buffer_id, - vec![(opening_range.clone(), vec![closing_range.clone()])], - ); + linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]); editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges); }); let mut completion_handle = diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f1ebd2c3dff99c4b555e4cf57abb37d568153b71..b18d1ceae1b7c316df849aca9fd8c028da906fee 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -7209,7 +7209,7 @@ fn render_blame_entry_popover( ) -> Option<AnyElement> { let renderer = cx.global::<GlobalBlameRenderer>().0.clone(); let blame = blame.read(cx); - let repository = blame.repository(cx)?.clone(); + let repository = blame.repository(cx)?; renderer.render_blame_entry_popover( blame_entry, scroll_handle, @@ -9009,7 +9009,7 @@ impl Element for EditorElement { .as_ref() .map(|layout| (layout.bounds, layout.entry.clone())), display_hunks: display_hunks.clone(), - diff_hunk_control_bounds: diff_hunk_control_bounds.clone(), + diff_hunk_control_bounds, }); self.editor.update(cx, |editor, _| { @@ -9894,7 +9894,7 @@ impl CursorLayout { .px_0p5() .line_height(text_size + px(2.)) .text_color(cursor_name.color) - .child(cursor_name.string.clone()) + .child(cursor_name.string) .into_any_element(); name_element.prepaint_as_root(name_origin, AvailableSpace::min_size(), window, cx); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index bb3fd2830da21791444effa954ca28d0eb819dd1..497f193cb4efbabc1b5003bba1ba201a4e0b5586 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -623,7 +623,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { let mut base_text_style = window.text_style(); base_text_style.refine(&TextStyleRefinement { - font_family: Some(ui_font_family.clone()), + font_family: Some(ui_font_family), font_fallbacks: ui_font_fallbacks, color: Some(cx.theme().colors().editor_foreground), ..Default::default() @@ -672,7 +672,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { let mut base_text_style = window.text_style(); base_text_style.refine(&TextStyleRefinement { - font_family: Some(ui_font_family.clone()), + font_family: Some(ui_font_family), font_fallbacks: ui_font_fallbacks, color: Some(cx.theme().colors().editor_foreground), ..Default::default() diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index a3fc41228f2ad9928da2602b97106bb6bd4be6da..83ab02814ffc645107b5e54652c2e6fb0622fc79 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -507,7 +507,7 @@ pub(crate) fn handle_from( { let selections = this - .read_with(cx, |this, _| this.selections.disjoint_anchors().clone()) + .read_with(cx, |this, _| this.selections.disjoint_anchors()) .ok()?; for selection in selections.iter() { let Some(selection_buffer_offset_head) = diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index dbb519c40e544585b82c3f8aa9b1312fe7078590..88721c59e707734f05a74c019d247c4fefff1efe 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -119,13 +119,7 @@ impl EditorTestContext { for excerpt in excerpts.into_iter() { let (text, ranges) = marked_text_ranges(excerpt, false); let buffer = cx.new(|cx| Buffer::local(text, cx)); - multibuffer.push_excerpts( - buffer, - ranges - .into_iter() - .map(|range| ExcerptRange::new(range.clone())), - cx, - ); + multibuffer.push_excerpts(buffer, ranges.into_iter().map(ExcerptRange::new), cx); } multibuffer }); diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 1d2bece5cc1ad99c96b0e161a1c188e3d61680af..c5a072eea15d8176e7141072f4ead9724b4fd61a 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -103,7 +103,7 @@ fn main() { let languages: HashSet<String> = args.languages.into_iter().collect(); let http_client = Arc::new(ReqwestClient::new()); - let app = Application::headless().with_http_client(http_client.clone()); + let app = Application::headless().with_http_client(http_client); let all_threads = examples::all(&examples_dir); app.run(move |cx| { @@ -416,11 +416,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> { language::init(cx); debug_adapter_extension::init(extension_host_proxy.clone(), cx); - language_extension::init( - LspAccess::Noop, - extension_host_proxy.clone(), - languages.clone(), - ); + language_extension::init(LspAccess::Noop, extension_host_proxy, languages.clone()); language_model::init(client.clone(), cx); language_models::init(user_store.clone(), client.clone(), cx); languages::init(languages.clone(), node_runtime.clone(), cx); @@ -530,7 +526,7 @@ async fn judge_example( example_name = example.name.clone(), example_repetition = example.repetition, diff_evaluation = judge_output.diff.clone(), - thread_evaluation = judge_output.thread.clone(), + thread_evaluation = judge_output.thread, tool_metrics = run_output.tool_metrics, response_count = run_output.response_count, token_usage = run_output.token_usage, diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index 53ce6088c02fbdd05d0025e651fabfbcc77b38bb..074cb121d3b588e3c82735de65dc3178a6eacc80 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -90,11 +90,8 @@ impl ExampleInstance { worktrees_dir: &Path, repetition: usize, ) -> Self { - let name = thread.meta().name.to_string(); - let run_directory = run_dir - .join(&name) - .join(repetition.to_string()) - .to_path_buf(); + let name = thread.meta().name; + let run_directory = run_dir.join(&name).join(repetition.to_string()); let repo_path = repo_path_for_url(repos_dir, &thread.meta().url); @@ -772,7 +769,7 @@ pub async fn query_lsp_diagnostics( } fn parse_assertion_result(response: &str) -> Result<RanAssertionResult> { - let analysis = get_tag("analysis", response)?.to_string(); + let analysis = get_tag("analysis", response)?; let passed = match get_tag("passed", response)?.to_lowercase().as_str() { "true" => true, "false" => false, diff --git a/crates/extension_host/src/capability_granter.rs b/crates/extension_host/src/capability_granter.rs index 5a2093c1dd02008b9b7ee8f0155b3aa675806a77..5491967e080fc4d12a52f0360dab1896b77e19d3 100644 --- a/crates/extension_host/src/capability_granter.rs +++ b/crates/extension_host/src/capability_granter.rs @@ -145,7 +145,7 @@ mod tests { command: "*".to_string(), args: vec!["**".to_string()], })], - manifest.clone(), + manifest, ); assert!(granter.grant_exec("ls", &["-la"]).is_ok()); } diff --git a/crates/extensions_ui/src/components/feature_upsell.rs b/crates/extensions_ui/src/components/feature_upsell.rs index 573b0b992d343e04b74531ffeb8579f28c92620c..0515dd46d30ce9f7e87331f99542940c3efa837a 100644 --- a/crates/extensions_ui/src/components/feature_upsell.rs +++ b/crates/extensions_ui/src/components/feature_upsell.rs @@ -61,7 +61,6 @@ impl RenderOnce for FeatureUpsell { .icon_size(IconSize::Small) .icon_position(IconPosition::End) .on_click({ - let docs_url = docs_url.clone(); move |_event, _window, cx| { telemetry::event!( "Documentation Viewed", diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index a6ee84eb60319fd55f2d054d16e8fade1d905548..fd504764b65826ea74e092ea4c11d5576fa51524 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -694,7 +694,7 @@ impl ExtensionsPage { cx.open_url(&repository_url); } })) - .tooltip(Tooltip::text(repository_url.clone())) + .tooltip(Tooltip::text(repository_url)) })), ) } @@ -827,7 +827,7 @@ impl ExtensionsPage { cx.open_url(&repository_url); } })) - .tooltip(Tooltip::text(repository_url.clone())), + .tooltip(Tooltip::text(repository_url)), ) .child( PopoverMenu::new(SharedString::from(format!( diff --git a/crates/feedback/src/system_specs.rs b/crates/feedback/src/system_specs.rs index b5ccaca6895e4ec935e5e5200fa7828e85b3295e..87642ab9294b3dd4faa32f368056a136f4b79e18 100644 --- a/crates/feedback/src/system_specs.rs +++ b/crates/feedback/src/system_specs.rs @@ -31,7 +31,7 @@ impl SystemSpecs { let architecture = env::consts::ARCH; let commit_sha = match release_channel { ReleaseChannel::Dev | ReleaseChannel::Nightly => { - AppCommitSha::try_global(cx).map(|sha| sha.full().clone()) + AppCommitSha::try_global(cx).map(|sha| sha.full()) } _ => None, }; diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 3a08ec08e0551a213c24c75d14d23f616bfe787d..40acf012c915a868c961caeeed44428d370dcaea 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1750,7 +1750,7 @@ impl PickerDelegate for FileFinderDelegate { Some(ContextMenu::build(window, cx, { let focus_handle = focus_handle.clone(); move |menu, _, _| { - menu.context(focus_handle.clone()) + menu.context(focus_handle) .action( "Split Left", pane::SplitLeft.boxed_clone(), diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index ffe3d42a278c63f7da58250c4307c38780057d9a..4625872e46c690701b304351c6648a8e380f181a 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -653,7 +653,7 @@ impl PickerDelegate for OpenPathDelegate { if parent_path == &self.prompt_root { format!("{}{}", self.prompt_root, candidate.path.string) } else { - candidate.path.string.clone() + candidate.path.string }, match_positions, )), @@ -684,7 +684,7 @@ impl PickerDelegate for OpenPathDelegate { }; StyledText::new(label) .with_default_highlights( - &window.text_style().clone(), + &window.text_style(), vec![( delta..delta + label_len, HighlightStyle::color(Color::Conflict.color(cx)), @@ -694,7 +694,7 @@ impl PickerDelegate for OpenPathDelegate { } else { StyledText::new(format!("{label} (create)")) .with_default_highlights( - &window.text_style().clone(), + &window.text_style(), vec![( delta..delta + label_len, HighlightStyle::color(Color::Created.color(cx)), diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 5b093ac6a0f6cb5c5ca5da7262ad17dd4059ce6d..8a67eddcd775746f5dbfc55c3a9a21a5d1f7d8e3 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -345,7 +345,7 @@ impl GitRepository for FakeGitRepository { fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> { self.with_state_async(true, move |state| { - state.branches.insert(name.to_owned()); + state.branches.insert(name); Ok(()) }) } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 11177512c3ea34690d4d6bea123322dc92011b34..75312c5c0cec4a4b285ff0320d90eeda1c0a4c6a 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1960,7 +1960,7 @@ impl FileHandle for FakeHandle { }; if state.try_entry(&target, false).is_some() { - return Ok(target.clone()); + return Ok(target); } anyhow::bail!("fake fd target not found") } @@ -2256,7 +2256,7 @@ impl Fs for FakeFs { async fn load(&self, path: &Path) -> Result<String> { let content = self.load_internal(path).await?; - Ok(String::from_utf8(content.clone())?) + Ok(String::from_utf8(content)?) } async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> { @@ -2412,7 +2412,7 @@ impl Fs for FakeFs { tx, original_path: path.to_owned(), fs_state: self.state.clone(), - prefixes: Mutex::new(vec![path.to_owned()]), + prefixes: Mutex::new(vec![path]), }); ( Box::pin(futures::StreamExt::filter(rx, { diff --git a/crates/fs/src/fs_watcher.rs b/crates/fs/src/fs_watcher.rs index a5ce21294fc65e609428ad95fafb43fe578bc698..6ad03ba6dfa2003b1642cbb542e3a9cf0bf13ec9 100644 --- a/crates/fs/src/fs_watcher.rs +++ b/crates/fs/src/fs_watcher.rs @@ -159,7 +159,7 @@ impl GlobalWatcher { path: path.clone(), }; state.watchers.insert(id, registration_state); - *state.path_registrations.entry(path.clone()).or_insert(0) += 1; + *state.path_registrations.entry(path).or_insert(0) += 1; Ok(id) } diff --git a/crates/git_ui/src/blame_ui.rs b/crates/git_ui/src/blame_ui.rs index f910de7bbe461ea8edf7addda4acad1b712a6f60..2768e3dc6863b008693d1db853f24a2bf87e1b29 100644 --- a/crates/git_ui/src/blame_ui.rs +++ b/crates/git_ui/src/blame_ui.rs @@ -172,7 +172,7 @@ impl BlameRenderer for GitBlameRenderer { .clone() .unwrap_or("<no name>".to_string()) .into(), - author_email: blame.author_mail.clone().unwrap_or("".to_string()).into(), + author_email: blame.author_mail.unwrap_or("".to_string()).into(), message: details, }; @@ -186,7 +186,7 @@ impl BlameRenderer for GitBlameRenderer { .get(0..8) .map(|sha| sha.to_string().into()) .unwrap_or_else(|| commit_details.sha.clone()); - let full_sha = commit_details.sha.to_string().clone(); + let full_sha = commit_details.sha.to_string(); let absolute_timestamp = format_local_timestamp( commit_details.commit_time, OffsetDateTime::now_utc(), @@ -377,7 +377,7 @@ impl BlameRenderer for GitBlameRenderer { has_parent: true, }, repository.downgrade(), - workspace.clone(), + workspace, window, cx, ) diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 6bb84db834ff0c04ff733d00b888aa28f0d70bd6..fb56cdcc5def31765d26545fcfa79b5f8c44e884 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -48,7 +48,7 @@ pub fn open( window: &mut Window, cx: &mut Context<Workspace>, ) { - let repository = workspace.project().read(cx).active_repository(cx).clone(); + let repository = workspace.project().read(cx).active_repository(cx); let style = BranchListStyle::Modal; workspace.toggle_modal(window, cx, |window, cx| { BranchList::new(repository, style, rems(34.), window, cx) @@ -144,7 +144,7 @@ impl BranchList { }) .detach_and_log_err(cx); - let delegate = BranchListDelegate::new(repository.clone(), style); + let delegate = BranchListDelegate::new(repository, style); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); let _subscription = cx.subscribe(&picker, |_, _, _, cx| { diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 4303f53275d5c402d5ff2663629ab8a772052e64..e1e6cee93c36037299b85a4e090f6c25419e2030 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -35,7 +35,7 @@ impl ModalContainerProperties { // Calculate width based on character width let mut modal_width = 460.0; - let style = window.text_style().clone(); + let style = window.text_style(); let font_id = window.text_system().resolve_font(&style.font()); let font_size = style.font_size.to_pixels(window.rem_size()); @@ -179,7 +179,7 @@ impl CommitModal { let commit_editor = git_panel.update(cx, |git_panel, cx| { git_panel.set_modal_open(true, cx); - let buffer = git_panel.commit_message_buffer(cx).clone(); + let buffer = git_panel.commit_message_buffer(cx); let panel_editor = git_panel.commit_editor.clone(); let project = git_panel.project.clone(); @@ -285,7 +285,7 @@ impl CommitModal { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target.clone()) + el.context(keybinding_target) }) .when(has_previous_commit, |this| { this.toggleable_entry( @@ -482,7 +482,7 @@ impl CommitModal { }), self.render_git_commit_menu( ElementId::Name(format!("split-button-right-{}", commit_label).into()), - Some(focus_handle.clone()), + Some(focus_handle), ) .into_any_element(), )), diff --git a/crates/git_ui/src/commit_tooltip.rs b/crates/git_ui/src/commit_tooltip.rs index 00ab911610c92dff4094452c55662447c4291259..a470bc69256068e687542334544bd9d2fc19c0cb 100644 --- a/crates/git_ui/src/commit_tooltip.rs +++ b/crates/git_ui/src/commit_tooltip.rs @@ -181,7 +181,7 @@ impl Render for CommitTooltip { .get(0..8) .map(|sha| sha.to_string().into()) .unwrap_or_else(|| self.commit.sha.clone()); - let full_sha = self.commit.sha.to_string().clone(); + let full_sha = self.commit.sha.to_string(); let absolute_timestamp = format_local_timestamp( self.commit.commit_time, OffsetDateTime::now_utc(), diff --git a/crates/git_ui/src/conflict_view.rs b/crates/git_ui/src/conflict_view.rs index 5c1b1325a5a0c8ac3418d34a640ef780b2912fbe..ee1b82920d7621f6e5b1d4ab9a9b44e151fbf82a 100644 --- a/crates/git_ui/src/conflict_view.rs +++ b/crates/git_ui/src/conflict_view.rs @@ -55,7 +55,7 @@ pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mu buffers: Default::default(), }); - let buffers = buffer.read(cx).all_buffers().clone(); + let buffers = buffer.read(cx).all_buffers(); for buffer in buffers { buffer_added(editor, buffer, cx); } @@ -129,7 +129,7 @@ fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Ed let subscription = cx.subscribe(&conflict_set, conflicts_updated); BufferConflicts { block_ids: Vec::new(), - conflict_set: conflict_set.clone(), + conflict_set, _subscription: subscription, } }); @@ -437,7 +437,6 @@ fn render_conflict_buttons( Button::new("both", "Use Both") .label_size(LabelSize::Small) .on_click({ - let editor = editor.clone(); let conflict = conflict.clone(); let ours = conflict.ours.clone(); let theirs = conflict.theirs.clone(); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 3eae1acb0451fb67bdceb7d452cb6fd40475123c..5a0151418589e792039cac9cde9c2185552a28ee 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1467,7 +1467,6 @@ impl GitPanel { .read(cx) .as_singleton() .unwrap() - .clone() } fn toggle_staged_for_selected( @@ -3207,7 +3206,7 @@ impl GitPanel { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target.clone()) + el.context(keybinding_target) }) .when(has_previous_commit, |this| { this.toggleable_entry( @@ -3387,7 +3386,7 @@ impl GitPanel { let enable_coauthors = self.render_co_authors(cx); let editor_focus_handle = self.commit_editor.focus_handle(cx); - let expand_tooltip_focus_handle = editor_focus_handle.clone(); + let expand_tooltip_focus_handle = editor_focus_handle; let branch = active_repository.read(cx).branch.clone(); let head_commit = active_repository.read(cx).head_commit.clone(); @@ -3416,7 +3415,7 @@ impl GitPanel { display_name, branch, head_commit, - Some(git_panel.clone()), + Some(git_panel), )) .child( panel_editor_container(window, cx) @@ -3567,7 +3566,7 @@ impl GitPanel { }), self.render_git_commit_menu( ElementId::Name(format!("split-button-right-{}", title).into()), - Some(commit_tooltip_focus_handle.clone()), + Some(commit_tooltip_focus_handle), cx, ) .into_any_element(), @@ -3633,7 +3632,7 @@ impl GitPanel { CommitView::open( commit.clone(), repo.clone(), - workspace.clone().clone(), + workspace.clone(), window, cx, ); @@ -4341,7 +4340,7 @@ impl GitPanel { } }) .child( - self.entry_label(display_name.clone(), label_color) + self.entry_label(display_name, label_color) .when(status.is_deleted(), |this| this.strikethrough()), ), ) @@ -4690,7 +4689,7 @@ impl GitPanelMessageTooltip { author_email: details.author_email.clone(), commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?, message: Some(ParsedCommitMessage { - message: details.message.clone(), + message: details.message, ..Default::default() }), }; @@ -4823,7 +4822,7 @@ impl RenderOnce for PanelRepoFooter { }; let truncated_branch_name = if branch_actual_len <= branch_display_len { - branch_name.to_string() + branch_name } else { util::truncate_and_trailoff(branch_name.trim_ascii(), branch_display_len) }; @@ -4836,7 +4835,7 @@ impl RenderOnce for PanelRepoFooter { let repo_selector = PopoverMenu::new("repository-switcher") .menu({ - let project = project.clone(); + let project = project; move |window, cx| { let project = project.clone()?; Some(cx.new(|cx| RepositorySelector::new(project, rems(16.), window, cx))) @@ -5007,10 +5006,7 @@ impl Component for PanelRepoFooter { div() .w(example_width) .overflow_hidden() - .child(PanelRepoFooter::new_preview( - active_repository(1).clone(), - None, - )) + .child(PanelRepoFooter::new_preview(active_repository(1), None)) .into_any_element(), ), single_example( @@ -5019,7 +5015,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(2).clone(), + active_repository(2), Some(branch(unknown_upstream)), )) .into_any_element(), @@ -5030,7 +5026,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(3).clone(), + active_repository(3), Some(branch(no_remote_upstream)), )) .into_any_element(), @@ -5041,7 +5037,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(4).clone(), + active_repository(4), Some(branch(not_ahead_or_behind_upstream)), )) .into_any_element(), @@ -5052,7 +5048,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(5).clone(), + active_repository(5), Some(branch(behind_upstream)), )) .into_any_element(), @@ -5063,7 +5059,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(6).clone(), + active_repository(6), Some(branch(ahead_of_upstream)), )) .into_any_element(), @@ -5074,7 +5070,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(7).clone(), + active_repository(7), Some(branch(ahead_and_behind_upstream)), )) .into_any_element(), diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 3b4196b8ec3191e5e993552c9b97a200bf711a34..5369b8b404ba7005bf738f4515d20b3fe4163a48 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -245,12 +245,12 @@ fn render_remote_button( } (0, 0) => None, (ahead, 0) => Some(remote_button::render_push_button( - keybinding_target.clone(), + keybinding_target, id, ahead, )), (ahead, behind) => Some(remote_button::render_pull_button( - keybinding_target.clone(), + keybinding_target, id, ahead, behind, @@ -425,16 +425,9 @@ mod remote_button { let command = command.into(); if let Some(handle) = focus_handle { - Tooltip::with_meta_in( - label.clone(), - Some(action), - command.clone(), - &handle, - window, - cx, - ) + Tooltip::with_meta_in(label, Some(action), command, &handle, window, cx) } else { - Tooltip::with_meta(label.clone(), Some(action), command.clone(), window, cx) + Tooltip::with_meta(label, Some(action), command, window, cx) } } @@ -457,7 +450,7 @@ mod remote_button { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target.clone()) + el.context(keybinding_target) }) .action("Fetch", git::Fetch.boxed_clone()) .action("Fetch From", git::FetchFrom.boxed_clone()) diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index c1521004a25f3a62141209bc1e75ae18fb7c0e38..524dbf13d30e4539dcc80ec37625333a37cc2206 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -242,7 +242,7 @@ impl ProjectDiff { TRACKED_NAMESPACE }; - let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone()); + let path_key = PathKey::namespaced(namespace, entry.repo_path.0); self.move_to_path(path_key, window, cx) } @@ -448,10 +448,10 @@ impl ProjectDiff { let diff = diff.read(cx); let diff_hunk_ranges = diff .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx) - .map(|diff_hunk| diff_hunk.buffer_range.clone()); + .map(|diff_hunk| diff_hunk.buffer_range); let conflicts = conflict_addon .conflict_set(snapshot.remote_id()) - .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts.clone()) + .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts) .unwrap_or_default(); let conflicts = conflicts.iter().map(|conflict| conflict.range.clone()); @@ -737,7 +737,7 @@ impl Render for ProjectDiff { } else { None }; - let keybinding_focus_handle = self.focus_handle(cx).clone(); + let keybinding_focus_handle = self.focus_handle(cx); el.child( v_flex() .gap_1() diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index e38e3698d54d1d6869002ba10e83f3a8401af219..ebf32d1b994814fa277201176b555efed5e85e66 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -48,7 +48,7 @@ impl TextDiffView { let selection_data = source_editor.update(cx, |editor, cx| { let multibuffer = editor.buffer().read(cx); - let source_buffer = multibuffer.as_singleton()?.clone(); + let source_buffer = multibuffer.as_singleton()?; let selections = editor.selections.all::<Point>(cx); let buffer_snapshot = source_buffer.read(cx); let first_selection = selections.first()?; @@ -259,7 +259,7 @@ async fn update_diff_buffer( let source_buffer_snapshot = source_buffer.read_with(cx, |buffer, _| buffer.snapshot())?; let base_buffer_snapshot = clipboard_buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - let base_text = base_buffer_snapshot.text().to_string(); + let base_text = base_buffer_snapshot.text(); let diff_snapshot = cx .update(|cx| { diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 908e61cac73849601dbca6338e585b96617ace9e..1913646aa1fa4a1d74b4b47a59dde961a7e37def 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -712,7 +712,7 @@ mod tests { ) -> Entity<GoToLine> { cx.dispatch_action(editor::actions::ToggleGoToLine); workspace.update(cx, |workspace, cx| { - workspace.active_modal::<GoToLine>(cx).unwrap().clone() + workspace.active_modal::<GoToLine>(cx).unwrap() }) } diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index ae635c94b81aa2b452d1aff00939108cf0d34bbb..37115feaa551a787562e7299c9d44bcc97b5fca3 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -446,7 +446,7 @@ impl Element for TextElement { let (display_text, text_color) = if content.is_empty() { (input.placeholder.clone(), hsla(0., 0., 0., 0.2)) } else { - (content.clone(), style.color) + (content, style.color) }; let run = TextRun { @@ -474,7 +474,7 @@ impl Element for TextElement { }, TextRun { len: display_text.len() - marked_range.end, - ..run.clone() + ..run }, ] .into_iter() diff --git a/crates/gpui/examples/text.rs b/crates/gpui/examples/text.rs index 1166bb279541c80eb8686b59c85724b4068895ed..66e9cff0aa9773d99b412ea7249ca64ea103b138 100644 --- a/crates/gpui/examples/text.rs +++ b/crates/gpui/examples/text.rs @@ -155,7 +155,7 @@ impl RenderOnce for Specimen { .text_size(px(font_size * scale)) .line_height(relative(line_height)) .p(px(10.0)) - .child(self.string.clone()) + .child(self.string) } } diff --git a/crates/gpui/src/app/async_context.rs b/crates/gpui/src/app/async_context.rs index d9d21c024461cab68d62d685a40b61c9c74d46dd..5eb436290415ed91c89ae85964bbb5093faa38f3 100644 --- a/crates/gpui/src/app/async_context.rs +++ b/crates/gpui/src/app/async_context.rs @@ -465,7 +465,7 @@ impl VisualContext for AsyncWindowContext { V: Focusable, { self.window.update(self, |_, window, cx| { - view.read(cx).focus_handle(cx).clone().focus(window); + view.read(cx).focus_handle(cx).focus(window); }) } } diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index 6099ee58579576c85ed9bd6776b2a80d8419c188..ea52b46d9fce958f8cb6e878581fb988c146c43b 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -231,14 +231,15 @@ impl AnyEntity { Self { entity_id: id, entity_type, - entity_map: entity_map.clone(), #[cfg(any(test, feature = "leak-detection"))] handle_id: entity_map + .clone() .upgrade() .unwrap() .write() .leak_detector .handle_created(id), + entity_map, } } diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index a69d9d1e26935d387f3bdfb46c4a1bd616a3b2e6..c65c045f6bc16310d3772825147ad570f209fd99 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -134,7 +134,7 @@ impl TestAppContext { app: App::new_app(platform.clone(), asset_source, http_client), background_executor, foreground_executor, - dispatcher: dispatcher.clone(), + dispatcher, test_platform: platform, text_system, fn_name, @@ -339,7 +339,7 @@ impl TestAppContext { /// Returns all windows open in the test. pub fn windows(&self) -> Vec<AnyWindowHandle> { - self.app.borrow().windows().clone() + self.app.borrow().windows() } /// Run the given task on the main thread. @@ -619,7 +619,7 @@ impl<V> Entity<V> { } }), cx.subscribe(self, { - let mut tx = tx.clone(); + let mut tx = tx; move |_, _: &Evt, _| { tx.blocking_send(()).ok(); } @@ -1026,7 +1026,7 @@ impl VisualContext for VisualTestContext { fn focus<V: crate::Focusable>(&mut self, view: &Entity<V>) -> Self::Result<()> { self.window .update(&mut self.cx, |_, window, cx| { - view.read(cx).focus_handle(cx).clone().focus(window) + view.read(cx).focus_handle(cx).focus(window) }) .unwrap() } diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index ae63819ca202bae5efe5829512c80b0fb754a567..893860d7e1b781144b2d8de06ae2135420854ed7 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -475,7 +475,7 @@ impl Element for Img { .paint_image( new_bounds, corner_radii, - data.clone(), + data, layout_state.frame_index, self.style.grayscale, ) diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index ef446a073e1854552350456208fd773b7fa7eae0..87cabc8cd9f446fddcb5c98dafbdf956eb1efea2 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -1046,7 +1046,7 @@ where size: self.size.clone() + size( amount.left.clone() + amount.right.clone(), - amount.top.clone() + amount.bottom.clone(), + amount.top.clone() + amount.bottom, ), } } @@ -1159,10 +1159,10 @@ where /// Computes the space available within outer bounds. pub fn space_within(&self, outer: &Self) -> Edges<T> { Edges { - top: self.top().clone() - outer.top().clone(), - right: outer.right().clone() - self.right().clone(), - bottom: outer.bottom().clone() - self.bottom().clone(), - left: self.left().clone() - outer.left().clone(), + top: self.top() - outer.top(), + right: outer.right() - self.right(), + bottom: outer.bottom() - self.bottom(), + left: self.left() - outer.left(), } } } @@ -1712,7 +1712,7 @@ where top: self.top.clone() * rhs.top, right: self.right.clone() * rhs.right, bottom: self.bottom.clone() * rhs.bottom, - left: self.left.clone() * rhs.left, + left: self.left * rhs.left, } } } @@ -2411,7 +2411,7 @@ where top_left: self.top_left.clone() * rhs.top_left, top_right: self.top_right.clone() * rhs.top_right, bottom_right: self.bottom_right.clone() * rhs.bottom_right, - bottom_left: self.bottom_left.clone() * rhs.bottom_left, + bottom_left: self.bottom_left * rhs.bottom_left, } } } diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index d0078765907daa60600576927e871e01c9fd81f0..757205fcc3d2a886744582769951b94abf754352 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -264,7 +264,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let (result, pending) = keymap.bindings_for_input( &[Keystroke::parse("ctrl-a").unwrap()], @@ -290,7 +290,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // binding is only enabled in a specific context assert!( @@ -344,7 +344,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let space = || Keystroke::parse("space").unwrap(); let w = || Keystroke::parse("w").unwrap(); @@ -396,7 +396,7 @@ mod tests { KeyBinding::new("space w x", ActionAlpha {}, Some("editor")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); @@ -410,7 +410,7 @@ mod tests { KeyBinding::new("space w w", NoAction {}, Some("editor")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); @@ -424,7 +424,7 @@ mod tests { KeyBinding::new("space w w", NoAction {}, Some("editor")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); @@ -439,7 +439,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -455,7 +455,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -474,7 +474,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -494,7 +494,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -516,7 +516,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -537,7 +537,7 @@ mod tests { KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -560,7 +560,7 @@ mod tests { KeyBinding::new("ctrl-x 0", NoAction, Some("Workspace")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -579,7 +579,7 @@ mod tests { KeyBinding::new("ctrl-x 0", NoAction, Some("vim_mode == normal")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -602,7 +602,7 @@ mod tests { KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -629,7 +629,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); assert_bindings(&keymap, &ActionAlpha {}, &["ctrl-a"]); assert_bindings(&keymap, &ActionBeta {}, &[]); diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index 976f99c26efdd5de6df0fc3297ad569316ce1e54..960bd1752fe8c1527b9c593658e429af4cd61029 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -668,11 +668,7 @@ mod tests { let contexts = vec![other_context.clone(), child_context.clone()]; assert!(!predicate.eval(&contexts)); - let contexts = vec![ - parent_context.clone(), - other_context.clone(), - child_context.clone(), - ]; + let contexts = vec![parent_context.clone(), other_context, child_context.clone()]; assert!(predicate.eval(&contexts)); assert!(!predicate.eval(&[])); @@ -681,7 +677,7 @@ mod tests { let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap(); assert!(!zany_predicate.eval(slice::from_ref(&child_context))); - assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()])); + assert!(zany_predicate.eval(&[child_context.clone(), child_context])); } #[test] @@ -718,7 +714,7 @@ mod tests { let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap(); assert!(!not_descendant.eval(slice::from_ref(&parent_context))); assert!(!not_descendant.eval(slice::from_ref(&child_context))); - assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); + assert!(!not_descendant.eval(&[parent_context, child_context])); let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap(); assert!(double_not.eval(slice::from_ref(&editor_context))); diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 399411843be7f3e96d9a59eab4fd79a32b76f8a2..3fb1ef45729e1e79f339aa19ad11554e7fce3772 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -108,13 +108,13 @@ impl LinuxCommon { let callbacks = PlatformHandlers::default(); - let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone())); + let dispatcher = Arc::new(LinuxDispatcher::new(main_sender)); let background_executor = BackgroundExecutor::new(dispatcher.clone()); let common = LinuxCommon { background_executor, - foreground_executor: ForegroundExecutor::new(dispatcher.clone()), + foreground_executor: ForegroundExecutor::new(dispatcher), text_system, appearance: WindowAppearance::Light, auto_hide_scrollbars: false, diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 2fe1da067bb3bdd8bde4ee394768f83c0aad6801..189cfa19545f052cf8ebc75b89c1f955d3396859 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -1280,7 +1280,6 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { let Some(focused_window) = focused_window else { return; }; - let focused_window = focused_window.clone(); let keymap_state = state.keymap_state.as_ref().unwrap(); let keycode = Keycode::from(key + MIN_KEYCODE); diff --git a/crates/gpui/src/platform/linux/wayland/cursor.rs b/crates/gpui/src/platform/linux/wayland/cursor.rs index a21263ccfe9c6e654b4f03e781c37398886a281a..c7c9139dea795701e459387a309b1817e2f60971 100644 --- a/crates/gpui/src/platform/linux/wayland/cursor.rs +++ b/crates/gpui/src/platform/linux/wayland/cursor.rs @@ -67,7 +67,7 @@ impl Cursor { { self.loaded_theme = Some(LoadedTheme { theme, - name: theme_name.map(|name| name.to_string()), + name: theme_name, scaled_size: self.scaled_size, }); } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 68198a285f9ebafe495d4f55798c17d6f5c4ee73..d5011708927d89e7f7c66758710f6e8599e9ca8d 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1329,7 +1329,7 @@ impl X11Client { state.composing = false; drop(state); if let Some(mut keystroke) = keystroke { - keystroke.key_char = Some(text.clone()); + keystroke.key_char = Some(text); window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent { keystroke, is_held: false, diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 49a5edceb25c097615e4db3a7a0dbdd221c2d0ed..9e5d6ec5ff02c74b4f0acfada8eee3d002bfd06b 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -332,7 +332,7 @@ impl MetalRenderer { self.path_intermediate_texture = Some(self.device.new_texture(&texture_descriptor)); if self.path_sample_count > 1 { - let mut msaa_descriptor = texture_descriptor.clone(); + let mut msaa_descriptor = texture_descriptor; msaa_descriptor.set_texture_type(metal::MTLTextureType::D2Multisample); msaa_descriptor.set_storage_mode(metal::MTLStorageMode::Private); msaa_descriptor.set_sample_count(self.path_sample_count as _); diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 2b4914baedfbc33a60df3fef0282535e0a9b6b3d..00afcd81b599cc53f12005062e4b87abd9c30e38 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -187,14 +187,14 @@ impl TestPlatform { .push_back(TestPrompt { msg: msg.to_string(), detail: detail.map(|s| s.to_string()), - answers: answers.clone(), + answers, tx, }); rx } pub(crate) fn set_active_window(&self, window: Option<TestWindow>) { - let executor = self.foreground_executor().clone(); + let executor = self.foreground_executor(); let previous_window = self.active_window.borrow_mut().take(); self.active_window.borrow_mut().clone_from(&window); diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 607163b577fbb81c1193f6af16be5edb7b3c17fe..4def6a11a5f16f235b1d7018ecbbdec5565ab951 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -956,7 +956,7 @@ impl WindowsWindowInner { click_count, first_mouse: false, }); - let result = func(input.clone()); + let result = func(input); let handled = !result.propagate || result.default_prevented; self.state.borrow_mut().callbacks.input = Some(func); diff --git a/crates/gpui/src/shared_string.rs b/crates/gpui/src/shared_string.rs index a34b7502f006b3d01323d58b9a1f499bd79ffd69..350184d350aec8c5995fe7d2f0856f1fe1cfea0f 100644 --- a/crates/gpui/src/shared_string.rs +++ b/crates/gpui/src/shared_string.rs @@ -108,7 +108,7 @@ impl From<SharedString> for Arc<str> { fn from(val: SharedString) -> Self { match val.0 { ArcCow::Borrowed(borrowed) => Arc::from(borrowed), - ArcCow::Owned(owned) => owned.clone(), + ArcCow::Owned(owned) => owned, } } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 89c1595a3fb700566a0ce64f86b758affcee1393..0791dcc621a8a71ef350ad32f8cf9ab87fad8db2 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2453,7 +2453,7 @@ impl Window { /// time. pub fn get_asset<A: Asset>(&mut self, source: &A::Source, cx: &mut App) -> Option<A::Output> { let (task, _) = cx.fetch_asset::<A>(source); - task.clone().now_or_never() + task.now_or_never() } /// Obtain the current element offset. This method should only be called during the /// prepaint phase of element drawing. @@ -3044,7 +3044,7 @@ impl Window { let tile = self .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { + .get_or_insert_with(¶ms.into(), &mut || { Ok(Some(( data.size(frame_index), Cow::Borrowed( @@ -3731,7 +3731,7 @@ impl Window { self.dispatch_keystroke_observers( event, Some(binding.action), - match_result.context_stack.clone(), + match_result.context_stack, cx, ); self.pending_input_changed(cx); @@ -4442,7 +4442,7 @@ impl Window { if let Some((_, inspector_id)) = self.hovered_inspector_hitbox(inspector, &self.rendered_frame) { - inspector.set_active_element_id(inspector_id.clone(), self); + inspector.set_active_element_id(inspector_id, self); } } }); @@ -4583,7 +4583,7 @@ impl<V: 'static + Render> WindowHandle<V> { where C: AppContext, { - cx.read_window(self, |root_view, _cx| root_view.clone()) + cx.read_window(self, |root_view, _cx| root_view) } /// Check if this window is 'active'. diff --git a/crates/gpui_macros/tests/derive_inspector_reflection.rs b/crates/gpui_macros/tests/derive_inspector_reflection.rs index 522c0a62c469cd181c44c465547a8c19c4d04f69..aab44a70ce58380f172756a21e69fb19e3eddad5 100644 --- a/crates/gpui_macros/tests/derive_inspector_reflection.rs +++ b/crates/gpui_macros/tests/derive_inspector_reflection.rs @@ -106,9 +106,7 @@ fn test_derive_inspector_reflection() { .invoke(num.clone()); assert_eq!(incremented, Number(6)); - let quadrupled = find_method::<Number>("quadruple") - .unwrap() - .invoke(num.clone()); + let quadrupled = find_method::<Number>("quadruple").unwrap().invoke(num); assert_eq!(quadrupled, Number(20)); // Try to invoke a non-existent method diff --git a/crates/http_client/src/async_body.rs b/crates/http_client/src/async_body.rs index 473849f3cdca785a802590a60cce922c9ee0b5f9..6b99a54a7d941c290f2680bc2a599bc63251e24b 100644 --- a/crates/http_client/src/async_body.rs +++ b/crates/http_client/src/async_body.rs @@ -40,7 +40,7 @@ impl AsyncBody { } pub fn from_bytes(bytes: Bytes) -> Self { - Self(Inner::Bytes(Cursor::new(bytes.clone()))) + Self(Inner::Bytes(Cursor::new(bytes))) } } diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 81dc36093b1eebeabb58f443dc25bc4f93d1607e..c09ab6f764893589945f2c3cc00d71df84b8f77a 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -123,7 +123,7 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap } let app_state = workspace.app_state().clone(); - let view_snapshot = workspace.weak_handle().clone(); + let view_snapshot = workspace.weak_handle(); window .spawn(cx, async move |cx| { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index cc96022e63b86879ada909b05adf30fe45d9d356..b106110c33d12a54cb7c4731598306c5da14abe9 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -974,8 +974,6 @@ impl Buffer { TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot(); let mut syntax = SyntaxMap::new(&text).snapshot(); if let Some(language) = language.clone() { - let text = text.clone(); - let language = language.clone(); let language_registry = language_registry.clone(); syntax.reparse(&text, language_registry, language); } @@ -1020,9 +1018,6 @@ impl Buffer { let text = TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot(); let mut syntax = SyntaxMap::new(&text).snapshot(); if let Some(language) = language.clone() { - let text = text.clone(); - let language = language.clone(); - let language_registry = language_registry.clone(); syntax.reparse(&text, language_registry, language); } BufferSnapshot { @@ -2206,7 +2201,7 @@ impl Buffer { self.remote_selections.insert( AGENT_REPLICA_ID, SelectionSet { - selections: selections.clone(), + selections, lamport_timestamp, line_mode, cursor_shape, @@ -3006,9 +3001,9 @@ impl BufferSnapshot { } let mut error_ranges = Vec::<Range<Point>>::new(); - let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| { - grammar.error_query.as_ref() - }); + let mut matches = self + .syntax + .matches(range, &self.text, |grammar| grammar.error_query.as_ref()); while let Some(mat) = matches.peek() { let node = mat.captures[0].node; let start = Point::from_ts_point(node.start_position()); @@ -4075,7 +4070,7 @@ impl BufferSnapshot { // Get the ranges of the innermost pair of brackets. let mut result: Option<(Range<usize>, Range<usize>)> = None; - for pair in self.enclosing_bracket_ranges(range.clone()) { + for pair in self.enclosing_bracket_ranges(range) { if let Some(range_filter) = range_filter && !range_filter(pair.open_range.clone(), pair.close_range.clone()) { @@ -4248,7 +4243,7 @@ impl BufferSnapshot { .map(|(range, name)| { ( name.to_string(), - self.text_for_range(range.clone()).collect::<String>(), + self.text_for_range(range).collect::<String>(), ) }) .collect(); diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 2e2df7e658596daaca3b338ef830794fd0d3bef8..ce65afa6288767766fa9a1da5c3a24f9ca86e580 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1744,7 +1744,7 @@ fn test_autoindent_block_mode(cx: &mut App) { buffer.edit( [(Point::new(2, 8)..Point::new(2, 8), inserted_text)], Some(AutoindentMode::Block { - original_indent_columns: original_indent_columns.clone(), + original_indent_columns, }), cx, ); @@ -1790,9 +1790,9 @@ fn test_autoindent_block_mode_with_newline(cx: &mut App) { "# .unindent(); buffer.edit( - [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())], + [(Point::new(2, 0)..Point::new(2, 0), inserted_text)], Some(AutoindentMode::Block { - original_indent_columns: original_indent_columns.clone(), + original_indent_columns, }), cx, ); @@ -1843,7 +1843,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut App) { buffer.edit( [(Point::new(2, 0)..Point::new(2, 0), inserted_text)], Some(AutoindentMode::Block { - original_indent_columns: original_indent_columns.clone(), + original_indent_columns, }), cx, ); @@ -2030,7 +2030,7 @@ fn test_autoindent_with_injected_languages(cx: &mut App) { let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); language_registry.add(html_language.clone()); - language_registry.add(javascript_language.clone()); + language_registry.add(javascript_language); cx.new(|cx| { let (text, ranges) = marked_text_ranges( diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 87fc846a537000313ee96e893b4fa6aa1a1e43f1..7ae77c9141d35363975f07b91b45f032da62d21f 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -206,7 +206,7 @@ impl CachedLspAdapter { } pub fn name(&self) -> LanguageServerName { - self.adapter.name().clone() + self.adapter.name() } pub async fn get_language_server_command( diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index be68dc1e9f22f8ae73fb72262e26a136cbd73399..4f07240e44599361bf92188fd410dd674745874a 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -432,7 +432,7 @@ impl LanguageRegistry { let mut state = self.state.write(); state .lsp_adapters - .entry(language_name.clone()) + .entry(language_name) .or_default() .push(adapter.clone()); state.all_lsp_adapters.insert(adapter.name(), adapter); @@ -454,7 +454,7 @@ impl LanguageRegistry { let cached_adapter = CachedLspAdapter::new(Arc::new(adapter)); state .lsp_adapters - .entry(language_name.clone()) + .entry(language_name) .or_default() .push(cached_adapter.clone()); state @@ -1167,8 +1167,7 @@ impl LanguageRegistryState { soft_wrap: language.config.soft_wrap, auto_indent_on_paste: language.config.auto_indent_on_paste, ..Default::default() - } - .clone(), + }, ); self.languages.push(language); self.version += 1; diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index fbb67a98181c8dd19e5b49a59ff17854ad9a9019..90a59ce06600c9ae5961e8e398770d9820586cc4 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -199,7 +199,7 @@ impl LanguageSettings { if language_server.0.as_ref() == Self::REST_OF_LANGUAGE_SERVERS { rest.clone() } else { - vec![language_server.clone()] + vec![language_server] } }) .collect::<Vec<_>>() @@ -1793,7 +1793,7 @@ mod tests { assert!(!settings.enabled_for_file(&dot_env_file, &cx)); // Test tilde expansion - let home = shellexpand::tilde("~").into_owned().to_string(); + let home = shellexpand::tilde("~").into_owned(); let home_file = make_test_file(&[&home, "test.rs"]); let settings = build_settings(&["~/test.rs"]); assert!(!settings.enabled_for_file(&home_file, &cx)); diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index f10056af13f4b0881388e455df43eb1b9530dd6f..38aad007fe16c655a3802bd70c9b709cbe83ea68 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -832,7 +832,7 @@ impl SyntaxSnapshot { query: fn(&Grammar) -> Option<&Query>, ) -> SyntaxMapCaptures<'a> { SyntaxMapCaptures::new( - range.clone(), + range, text, [SyntaxLayer { language, diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index d576c95cd58eb823a7f8bdfdc42be9ba6a743410..622731b7814ce16bfcc026b6723e80d5ba4dda7a 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -58,8 +58,7 @@ fn test_splice_included_ranges() { assert_eq!(change, 0..1); // does not create overlapping ranges - let (new_ranges, change) = - splice_included_ranges(ranges.clone(), &[0..18], &[ts_range(20..32)]); + let (new_ranges, change) = splice_included_ranges(ranges, &[0..18], &[ts_range(20..32)]); assert_eq!( new_ranges, &[ts_range(20..32), ts_range(50..60), ts_range(80..90)] @@ -104,7 +103,7 @@ fn test_syntax_map_layers_for_range(cx: &mut App) { ); let mut syntax_map = SyntaxMap::new(&buffer); - syntax_map.set_language_registry(registry.clone()); + syntax_map.set_language_registry(registry); syntax_map.reparse(language.clone(), &buffer); assert_layers_for_range( @@ -165,7 +164,7 @@ fn test_syntax_map_layers_for_range(cx: &mut App) { // Put the vec! macro back, adding back the syntactic layer. buffer.undo(); syntax_map.interpolate(&buffer); - syntax_map.reparse(language.clone(), &buffer); + syntax_map.reparse(language, &buffer); assert_layers_for_range( &syntax_map, @@ -252,8 +251,8 @@ fn test_dynamic_language_injection(cx: &mut App) { assert!(syntax_map.contains_unknown_injections()); registry.add(Arc::new(html_lang())); - syntax_map.reparse(markdown.clone(), &buffer); - syntax_map.reparse(markdown_inline.clone(), &buffer); + syntax_map.reparse(markdown, &buffer); + syntax_map.reparse(markdown_inline, &buffer); assert_layers_for_range( &syntax_map, &buffer, @@ -862,7 +861,7 @@ fn test_syntax_map_languages_loading_with_erb(cx: &mut App) { log::info!("editing"); buffer.edit_via_marked_text(&text); syntax_map.interpolate(&buffer); - syntax_map.reparse(language.clone(), &buffer); + syntax_map.reparse(language, &buffer); assert_capture_ranges( &syntax_map, @@ -986,7 +985,7 @@ fn test_random_edits( syntax_map.reparse(language.clone(), &buffer); let mut reference_syntax_map = SyntaxMap::new(&buffer); - reference_syntax_map.set_language_registry(registry.clone()); + reference_syntax_map.set_language_registry(registry); log::info!("initial text:\n{}", buffer.text()); diff --git a/crates/language/src/text_diff.rs b/crates/language/src/text_diff.rs index 1e3e12758dde70127a5006025cb8153e645fdb0a..cb2242a6b1a04ce23ab2e232f1235f3680a33aa5 100644 --- a/crates/language/src/text_diff.rs +++ b/crates/language/src/text_diff.rs @@ -88,11 +88,11 @@ pub fn text_diff_with_options( let new_offset = new_byte_range.start; hunk_input.clear(); hunk_input.update_before(tokenize( - &old_text[old_byte_range.clone()], + &old_text[old_byte_range], options.language_scope.clone(), )); hunk_input.update_after(tokenize( - &new_text[new_byte_range.clone()], + &new_text[new_byte_range], options.language_scope.clone(), )); diff_internal(&hunk_input, |old_byte_range, new_byte_range, _, _| { @@ -103,7 +103,7 @@ pub fn text_diff_with_options( let replacement_text = if new_byte_range.is_empty() { empty.clone() } else { - new_text[new_byte_range.clone()].into() + new_text[new_byte_range].into() }; edits.push((old_byte_range, replacement_text)); }); @@ -111,9 +111,9 @@ pub fn text_diff_with_options( let replacement_text = if new_byte_range.is_empty() { empty.clone() } else { - new_text[new_byte_range.clone()].into() + new_text[new_byte_range].into() }; - edits.push((old_byte_range.clone(), replacement_text)); + edits.push((old_byte_range, replacement_text)); } }, ); diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index b10529c3d93c51154531661a4e02e0cdd3061aab..158bebcbbf8843c25f5030f5b4b6e4ae436f371a 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -54,7 +54,7 @@ pub const ZED_CLOUD_PROVIDER_NAME: LanguageModelProviderName = pub fn init(client: Arc<Client>, cx: &mut App) { init_settings(cx); - RefreshLlmTokenListener::register(client.clone(), cx); + RefreshLlmTokenListener::register(client, cx); } pub fn init_settings(cx: &mut App) { @@ -538,7 +538,7 @@ pub trait LanguageModel: Send + Sync { if let Some(first_event) = events.next().await { match first_event { Ok(LanguageModelCompletionEvent::StartMessage { message_id: id }) => { - message_id = Some(id.clone()); + message_id = Some(id); } Ok(LanguageModelCompletionEvent::Text(text)) => { first_item_text = Some(text); diff --git a/crates/language_model/src/model/cloud_model.rs b/crates/language_model/src/model/cloud_model.rs index 0e10050dae92dcdbfcb3138e7cd3981d773c5aeb..8a7f3456fbb54826809e8a25c2c767d387afcd4e 100644 --- a/crates/language_model/src/model/cloud_model.rs +++ b/crates/language_model/src/model/cloud_model.rs @@ -82,7 +82,7 @@ impl LlmApiToken { let response = client.cloud_client().create_llm_token(system_id).await?; *lock = Some(response.token.0.clone()); - Ok(response.token.0.clone()) + Ok(response.token.0) } } diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 18e6f47ed0591256591df578f98dcaf988ed6444..738b72b0c9a6dbb7c9606cc72707b27e66abf09c 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -104,7 +104,7 @@ fn register_language_model_providers( cx: &mut Context<LanguageModelRegistry>, ) { registry.register_provider( - CloudLanguageModelProvider::new(user_store.clone(), client.clone(), cx), + CloudLanguageModelProvider::new(user_store, client.clone(), cx), cx, ); diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 193d218094eed5a86d466a73701ffc00ff27b94e..178c767950e640801ada7291cd8eb317fcba232c 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -917,7 +917,7 @@ pub fn map_to_language_model_completion_events( Some(ContentBlockDelta::ReasoningContent(thinking)) => match thinking { ReasoningContentBlockDelta::Text(thoughts) => { Some(Ok(LanguageModelCompletionEvent::Thinking { - text: thoughts.clone(), + text: thoughts, signature: None, })) } @@ -968,7 +968,7 @@ pub fn map_to_language_model_completion_events( id: tool_use.id.into(), name: tool_use.name.into(), is_input_complete: true, - raw_input: tool_use.input_json.clone(), + raw_input: tool_use.input_json, input, }, )) @@ -1086,21 +1086,18 @@ impl ConfigurationView { .access_key_id_editor .read(cx) .text(cx) - .to_string() .trim() .to_string(); let secret_access_key = self .secret_access_key_editor .read(cx) .text(cx) - .to_string() .trim() .to_string(); let session_token = self .session_token_editor .read(cx) .text(cx) - .to_string() .trim() .to_string(); let session_token = if session_token.is_empty() { @@ -1108,13 +1105,7 @@ impl ConfigurationView { } else { Some(session_token) }; - let region = self - .region_editor - .read(cx) - .text(cx) - .to_string() - .trim() - .to_string(); + let region = self.region_editor.read(cx).text(cx).trim().to_string(); let region = if region.is_empty() { "us-east-1".to_string() } else { diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index d3fee7b63b3689fc815f135af870d85eca6998ec..b1b5ff3eb39695ea521dc986b39ffd2d0d194f99 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -140,7 +140,7 @@ impl State { Self { client: client.clone(), llm_api_token: LlmApiToken::default(), - user_store: user_store.clone(), + user_store, status, accept_terms_of_service_task: None, models: Vec::new(), @@ -307,7 +307,7 @@ impl CloudLanguageModelProvider { Self { client, - state: state.clone(), + state, _maintain_client_status: maintain_client_status, } } @@ -320,7 +320,7 @@ impl CloudLanguageModelProvider { Arc::new(CloudLanguageModel { id: LanguageModelId(SharedString::from(model.id.0.clone())), model, - llm_api_token: llm_api_token.clone(), + llm_api_token, client: self.client.clone(), request_limiter: RateLimiter::new(4), }) diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index a36ce949b177bf49de7bbcd914d9e68955fa4ef4..c8d4151e8b91db14e7ebba5c0e028717ccbed1d5 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -387,7 +387,7 @@ impl LanguageModel for GoogleLanguageModel { cx: &App, ) -> BoxFuture<'static, Result<u64>> { let model_id = self.model.request_id().to_string(); - let request = into_google(request, model_id.clone(), self.model.mode()); + let request = into_google(request, model_id, self.model.mode()); let http_client = self.http_client.clone(); let api_key = self.state.read(cx).api_key.clone(); diff --git a/crates/language_models/src/provider/lmstudio.rs b/crates/language_models/src/provider/lmstudio.rs index 7ac08f2c1529fbffc68e2d49c1e1765181fd51ef..80b28a396b958ab20de3faa0a0f6919c57011e5c 100644 --- a/crates/language_models/src/provider/lmstudio.rs +++ b/crates/language_models/src/provider/lmstudio.rs @@ -210,7 +210,7 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider { .map(|model| { Arc::new(LmStudioLanguageModel { id: LanguageModelId::from(model.name.clone()), - model: model.clone(), + model, http_client: self.http_client.clone(), request_limiter: RateLimiter::new(4), }) as Arc<dyn LanguageModel> diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index 93844542ea68f68479d74b427493cd33cb5e5dc5..3f2d47fba35959e6c26205193dddba45c2df25cc 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -237,7 +237,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider { .map(|model| { Arc::new(OllamaLanguageModel { id: LanguageModelId::from(model.name.clone()), - model: model.clone(), + model, http_client: self.http_client.clone(), request_limiter: RateLimiter::new(4), }) as Arc<dyn LanguageModel> diff --git a/crates/language_models/src/ui/instruction_list_item.rs b/crates/language_models/src/ui/instruction_list_item.rs index 3dee97aff6ca78f97f0e4386e9518f5a5d1f29e0..bdb5fbe242ee902dc98a37addfaa0f103ef9ad20 100644 --- a/crates/language_models/src/ui/instruction_list_item.rs +++ b/crates/language_models/src/ui/instruction_list_item.rs @@ -37,7 +37,7 @@ impl IntoElement for InstructionListItem { let item_content = if let (Some(button_label), Some(button_link)) = (self.button_label, self.button_link) { - let link = button_link.clone(); + let link = button_link; let unique_id = SharedString::from(format!("{}-button", self.label)); h_flex() diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 3285efaaef53d6f31a8db5b3e18240db9027d905..43c0365291c349a8e456c9172e46d121a6f23fe7 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -406,10 +406,7 @@ impl LogStore { server_state.worktree_id = Some(worktree_id); } - if let Some(server) = server - .clone() - .filter(|_| server_state.io_logs_subscription.is_none()) - { + if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) { let io_tx = self.io_tx.clone(); let server_id = server.server_id(); server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| { @@ -930,7 +927,7 @@ impl LspLogView { let state = log_store.language_servers.get(&server_id)?; Some(LogMenuItem { server_id, - server_name: name.clone(), + server_name: name, server_kind: state.kind.clone(), worktree_root_name: "supplementary".to_string(), rpc_trace_enabled: state.rpc_state.is_some(), @@ -1527,7 +1524,7 @@ impl Render for LspLogToolbarItemView { .icon_color(Color::Muted), ) .menu({ - let log_view = log_view.clone(); + let log_view = log_view; move |window, cx| { let id = log_view.read(cx).current_server_id?; @@ -1595,7 +1592,7 @@ impl Render for LspLogToolbarItemView { .icon_color(Color::Muted), ) .menu({ - let log_view = log_view.clone(); + let log_view = log_view; move |window, cx| { let id = log_view.read(cx).current_server_id?; diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 4fe8e11f94b7623882b4e5dbe79b80ed060a99c8..cf84ac34c4af6d04895ba5d1e22c262a1ef8f03c 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -156,7 +156,7 @@ impl SyntaxTreeView { .buffer_snapshot .range_to_buffer_ranges(selection_range) .pop()?; - let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap().clone(); + let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap(); Some((buffer, range, excerpt_id)) })?; diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index 999d4a74c30c776e6e0d83b7b0a4466079bff199..2820f55a497c8b77b679a3ab5368cae6901c89e6 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -22,7 +22,7 @@ impl CLspAdapter { #[async_trait(?Send)] impl super::LspAdapter for CLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn check_if_user_installed( @@ -253,8 +253,7 @@ impl super::LspAdapter for CLspAdapter { .grammar() .and_then(|g| g.highlight_id_for_name(highlight_name?)) { - let mut label = - CodeLabel::plain(label.to_string(), completion.filter_text.as_deref()); + let mut label = CodeLabel::plain(label, completion.filter_text.as_deref()); label.runs.push(( 0..label.text.rfind('(').unwrap_or(label.text.len()), highlight_id, @@ -264,10 +263,7 @@ impl super::LspAdapter for CLspAdapter { } _ => {} } - Some(CodeLabel::plain( - label.to_string(), - completion.filter_text.as_deref(), - )) + Some(CodeLabel::plain(label, completion.filter_text.as_deref())) } async fn label_for_symbol( diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index d6f9538ee4851bb29a38f4108ddbce18929b94e5..24e2ca2f56fff5ba1a3d92ca5e0bf16ac1a9463b 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -53,7 +53,7 @@ const BINARY: &str = if cfg!(target_os = "windows") { #[async_trait(?Send)] impl super::LspAdapter for GoLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( @@ -525,7 +525,7 @@ impl ContextProvider for GoContextProvider { }) .unwrap_or_else(|| format!("{}", buffer_dir.to_string_lossy())); - (GO_PACKAGE_TASK_VARIABLE.clone(), package_name.to_string()) + (GO_PACKAGE_TASK_VARIABLE.clone(), package_name) }); let go_module_root_variable = local_abs_path @@ -702,7 +702,7 @@ impl ContextProvider for GoContextProvider { label: format!("go generate {}", GO_PACKAGE_TASK_VARIABLE.template_value()), command: "go".into(), args: vec!["generate".into()], - cwd: package_cwd.clone(), + cwd: package_cwd, tags: vec!["go-generate".to_owned()], ..TaskTemplate::default() }, @@ -710,7 +710,7 @@ impl ContextProvider for GoContextProvider { label: "go generate ./...".into(), command: "go".into(), args: vec!["generate".into(), "./...".into()], - cwd: module_cwd.clone(), + cwd: module_cwd, ..TaskTemplate::default() }, ]))) diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index ac653d5b2eaea4af497e81707383099384de146a..4fcf865568b11ea0f8b40d6a7d57cc354e85a680 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -488,7 +488,7 @@ impl NodeVersionAdapter { #[async_trait(?Send)] impl LspAdapter for NodeVersionAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 75289dd59daa85de5c0c6ab3e7ec71736212a5ae..d391e67d33fe90ac7e678c1458b8985e221fafc3 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -104,7 +104,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { let typescript_context = Arc::new(typescript::TypeScriptContextProvider::new()); let typescript_lsp_adapter = Arc::new(typescript::TypeScriptLspAdapter::new(node.clone())); let vtsls_adapter = Arc::new(vtsls::VtslsLspAdapter::new(node.clone())); - let yaml_lsp_adapter = Arc::new(yaml::YamlLspAdapter::new(node.clone())); + let yaml_lsp_adapter = Arc::new(yaml::YamlLspAdapter::new(node)); let built_in_languages = [ LanguageInfo { @@ -119,12 +119,12 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { }, LanguageInfo { name: "cpp", - adapters: vec![c_lsp_adapter.clone()], + adapters: vec![c_lsp_adapter], ..Default::default() }, LanguageInfo { name: "css", - adapters: vec![css_lsp_adapter.clone()], + adapters: vec![css_lsp_adapter], ..Default::default() }, LanguageInfo { @@ -146,20 +146,20 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { }, LanguageInfo { name: "gowork", - adapters: vec![go_lsp_adapter.clone()], - context: Some(go_context_provider.clone()), + adapters: vec![go_lsp_adapter], + context: Some(go_context_provider), ..Default::default() }, LanguageInfo { name: "json", - adapters: vec![json_lsp_adapter.clone(), node_version_lsp_adapter.clone()], + adapters: vec![json_lsp_adapter.clone(), node_version_lsp_adapter], context: Some(json_context_provider.clone()), ..Default::default() }, LanguageInfo { name: "jsonc", - adapters: vec![json_lsp_adapter.clone()], - context: Some(json_context_provider.clone()), + adapters: vec![json_lsp_adapter], + context: Some(json_context_provider), ..Default::default() }, LanguageInfo { @@ -174,7 +174,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { }, LanguageInfo { name: "python", - adapters: vec![python_lsp_adapter.clone(), py_lsp_adapter.clone()], + adapters: vec![python_lsp_adapter, py_lsp_adapter], context: Some(python_context_provider), toolchain: Some(python_toolchain_provider), manifest_name: Some(SharedString::new_static("pyproject.toml").into()), @@ -201,7 +201,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { LanguageInfo { name: "javascript", adapters: vec![typescript_lsp_adapter.clone(), vtsls_adapter.clone()], - context: Some(typescript_context.clone()), + context: Some(typescript_context), ..Default::default() }, LanguageInfo { @@ -277,13 +277,13 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { move || adapter.clone() }); languages.register_available_lsp_adapter(LanguageServerName("vtsls".into()), { - let adapter = vtsls_adapter.clone(); + let adapter = vtsls_adapter; move || adapter.clone() }); languages.register_available_lsp_adapter( LanguageServerName("typescript-language-server".into()), { - let adapter = typescript_lsp_adapter.clone(); + let adapter = typescript_lsp_adapter; move || adapter.clone() }, ); diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 6c92d78525f9382770df851bb0a9622183460e32..d21b5dabd34c311d6e08140b2bc7ed363f79f273 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -103,7 +103,7 @@ impl PythonLspAdapter { #[async_trait(?Send)] impl LspAdapter for PythonLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn initialization_options( @@ -1026,7 +1026,7 @@ const BINARY_DIR: &str = if cfg!(target_os = "windows") { #[async_trait(?Send)] impl LspAdapter for PyLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn check_if_user_installed( @@ -1318,7 +1318,7 @@ impl BasedPyrightLspAdapter { #[async_trait(?Send)] impl LspAdapter for BasedPyrightLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn initialization_options( diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index eb5e0cee7ceb423983b24f37eb15440370bd7670..c6c735714854da15194cad6d15c2e3bd124fe308 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -106,7 +106,7 @@ impl ManifestProvider for CargoManifestProvider { #[async_trait(?Send)] impl LspAdapter for RustLspAdapter { fn name(&self) -> LanguageServerName { - SERVER_NAME.clone() + SERVER_NAME } async fn check_if_user_installed( @@ -659,7 +659,7 @@ impl ContextProvider for RustContextProvider { .variables .get(CUSTOM_TARGET_DIR) .cloned(); - let run_task_args = if let Some(package_to_run) = package_to_run.clone() { + let run_task_args = if let Some(package_to_run) = package_to_run { vec!["run".into(), "-p".into(), package_to_run] } else { vec!["run".into()] @@ -1019,8 +1019,8 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ let path = last.context("no cached binary")?; let path = match RustLspAdapter::GITHUB_ASSET_KIND { - AssetKind::TarGz | AssetKind::Gz => path.clone(), // Tar and gzip extract in place. - AssetKind::Zip => path.clone().join("rust-analyzer.exe"), // zip contains a .exe + AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place. + AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe }; anyhow::Ok(LanguageServerBinary { diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 29a96d95151afb74a63cd051fa9bf58f923d8c9f..47eb25405339b81b3c458b2443d4af77da883cf0 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -44,7 +44,7 @@ impl TailwindLspAdapter { #[async_trait(?Send)] impl LspAdapter for TailwindLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn check_if_user_installed( diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index afc84c3affb9964cd2cf00c9a15e3ca8ef6d68e4..77cf1a64f1f5586e727ca2b7ccc55dbe8a6183cf 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -557,7 +557,7 @@ struct TypeScriptVersions { #[async_trait(?Send)] impl LspAdapter for TypeScriptLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( @@ -879,7 +879,7 @@ impl LspAdapter for EsLintLspAdapter { } fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index fd227e267dc585f90f35a815bede32dd9b9f78c8..f7152b0b5df8e2249c43b1f2ffa2b490bf001961 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -67,7 +67,7 @@ const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls"); #[async_trait(?Send)] impl LspAdapter for VtslsLspAdapter { fn name(&self) -> LanguageServerName { - SERVER_NAME.clone() + SERVER_NAME } async fn fetch_latest_server_version( diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 6ac92e0b2bad1351e74a4a514fc8303971424b91..b9197b12ae85af5ee2a9aa2b5807a4f0410a1dda 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -38,7 +38,7 @@ impl YamlLspAdapter { #[async_trait(?Send)] impl LspAdapter for YamlLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( diff --git a/crates/livekit_client/examples/test_app.rs b/crates/livekit_client/examples/test_app.rs index 51f335c2dbefc93a565ce2d6f6c467ca11e5b1eb..75806429905e6bcbfcc17f25a29007f18e78757b 100644 --- a/crates/livekit_client/examples/test_app.rs +++ b/crates/livekit_client/examples/test_app.rs @@ -183,7 +183,7 @@ impl LivekitWindow { match track { livekit_client::RemoteTrack::Audio(track) => { output.audio_output_stream = Some(( - publication.clone(), + publication, room.play_remote_audio_track(&track, cx).unwrap(), )); } diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index d1eec42f8f0df92e78ef63244c7cde400a1f19a6..e13fb7bd8132916800654416e99316e9cf3be074 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -117,7 +117,6 @@ impl AudioStack { let (frame_tx, mut frame_rx) = futures::channel::mpsc::unbounded(); let transmit_task = self.executor.spawn({ - let source = source.clone(); async move { while let Some(frame) = frame_rx.next().await { source.capture_frame(&frame).await.log_err(); @@ -132,12 +131,12 @@ impl AudioStack { drop(transmit_task); drop(capture_task); }); - return Ok(( + Ok(( super::LocalAudioTrack(track), AudioStream::Output { _drop: Box::new(on_drop), }, - )); + )) } fn start_output(&self) -> Arc<Task<()>> { diff --git a/crates/markdown/examples/markdown_as_child.rs b/crates/markdown/examples/markdown_as_child.rs index 862b657c8c50c7adc88642f1af21a4c075ff77f2..16c198601a31707602aea3dd250e3958c4c8f0fb 100644 --- a/crates/markdown/examples/markdown_as_child.rs +++ b/crates/markdown/examples/markdown_as_child.rs @@ -30,7 +30,7 @@ pub fn main() { let node_runtime = NodeRuntime::unavailable(); let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); - languages::init(language_registry.clone(), node_runtime, cx); + languages::init(language_registry, node_runtime, cx); theme::init(LoadThemes::JustBase, cx); Assets.load_fonts(cx).unwrap(); diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index a161ddd074401db53ff428b68347d56f0c3fd856..755506bd126daff19b54888d76021844c5097a1c 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1323,8 +1323,7 @@ fn render_copy_code_block_button( .shape(ui::IconButtonShape::Square) .tooltip(Tooltip::text("Copy Code")) .on_click({ - let id = id.clone(); - let markdown = markdown.clone(); + let markdown = markdown; move |_event, _window, cx| { let id = id.clone(); markdown.update(cx, |this, cx| { diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 8c8d9e177fba440acda8555d33ba9679269a4f1e..b51b98a2ed64c72d76a8ca6e7316b6866bdcd9fe 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -178,7 +178,6 @@ impl<'a> MarkdownParser<'a> { _ => None, }, Event::Rule => { - let source_range = source_range.clone(); self.cursor += 1; Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)]) } @@ -401,7 +400,7 @@ impl<'a> MarkdownParser<'a> { } if !text.is_empty() { markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: source_range.clone(), + source_range, contents: text, highlights, regions, @@ -420,7 +419,7 @@ impl<'a> MarkdownParser<'a> { self.cursor += 1; ParsedMarkdownHeading { - source_range: source_range.clone(), + source_range, level: match level { pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1, pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2, diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index c2b98f69c853798c09376c66294835f7b3e2c30d..1121d64655f6c7e02f0b0d621605c9ba1aae7cde 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -115,8 +115,7 @@ impl MarkdownPreviewView { pane.activate_item(existing_follow_view_idx, true, true, window, cx); }); } else { - let view = - Self::create_following_markdown_view(workspace, editor.clone(), window, cx); + let view = Self::create_following_markdown_view(workspace, editor, window, cx); workspace.active_pane().update(cx, |pane, cx| { pane.add_item(Box::new(view.clone()), true, true, None, window, cx) }); diff --git a/crates/migrator/src/migrations/m_2025_01_02/settings.rs b/crates/migrator/src/migrations/m_2025_01_02/settings.rs index 3ce85e6b2611b69dfaac5479ee3404eeda9c0ebc..a35b1ebd2e9d8e2c658de0623b7c2e8377662b18 100644 --- a/crates/migrator/src/migrations/m_2025_01_02/settings.rs +++ b/crates/migrator/src/migrations/m_2025_01_02/settings.rs @@ -20,14 +20,14 @@ fn replace_deprecated_settings_values( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; let setting_name_ix = query.capture_index_for_name("setting_name")?; let setting_name_range = mat .nodes_for_capture_index(setting_name_ix) .next()? .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; + let setting_name = contents.get(setting_name_range)?; let setting_value_ix = query.capture_index_for_name("setting_value")?; let setting_value_range = mat 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 c32da88229b429ad206168eeee30f401863b39bd..eed2c46e0816452af6813ae699eab6cec1d65eec 100644 --- a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs +++ b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs @@ -279,7 +279,7 @@ fn rename_context_key( new_predicate = new_predicate.replace(old_key, new_key); } if new_predicate != old_predicate { - Some((context_predicate_range, new_predicate.to_string())) + Some((context_predicate_range, new_predicate)) } else { None } diff --git a/crates/migrator/src/migrations/m_2025_01_29/settings.rs b/crates/migrator/src/migrations/m_2025_01_29/settings.rs index 8d3261676b731d00e3dd85f3f5d94737931d74fe..46cfe2f178f1e4416cb404f26b5b77b55663aa29 100644 --- a/crates/migrator/src/migrations/m_2025_01_29/settings.rs +++ b/crates/migrator/src/migrations/m_2025_01_29/settings.rs @@ -57,7 +57,7 @@ pub fn replace_edit_prediction_provider_setting( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; let setting_name_ix = query.capture_index_for_name("setting_name")?; let setting_range = mat diff --git a/crates/migrator/src/migrations/m_2025_01_30/settings.rs b/crates/migrator/src/migrations/m_2025_01_30/settings.rs index 23a3243b827b7d44e673208e56858b6cd2e8f2b7..2d763e4722cb2119f0b2f982b5841aab37e55c12 100644 --- a/crates/migrator/src/migrations/m_2025_01_30/settings.rs +++ b/crates/migrator/src/migrations/m_2025_01_30/settings.rs @@ -25,7 +25,7 @@ fn replace_tab_close_button_setting_key( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; let setting_name_ix = query.capture_index_for_name("setting_name")?; let setting_range = mat @@ -51,14 +51,14 @@ fn replace_tab_close_button_setting_value( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; let setting_name_ix = query.capture_index_for_name("setting_name")?; let setting_name_range = mat .nodes_for_capture_index(setting_name_ix) .next()? .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; + let setting_name = contents.get(setting_name_range)?; let setting_value_ix = query.capture_index_for_name("setting_value")?; let setting_value_range = mat diff --git a/crates/migrator/src/migrations/m_2025_03_29/settings.rs b/crates/migrator/src/migrations/m_2025_03_29/settings.rs index 47f65b407da2b7079fb68a4877275339d6309433..8f83d8e39ea050de0ec9291199804f0e62dab392 100644 --- a/crates/migrator/src/migrations/m_2025_03_29/settings.rs +++ b/crates/migrator/src/migrations/m_2025_03_29/settings.rs @@ -19,7 +19,7 @@ fn replace_setting_value( .nodes_for_capture_index(setting_capture_ix) .next()? .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; + let setting_name = contents.get(setting_name_range)?; if setting_name != "hide_mouse_while_typing" { return None; diff --git a/crates/migrator/src/migrations/m_2025_05_29/settings.rs b/crates/migrator/src/migrations/m_2025_05_29/settings.rs index 56d72836fa396810db2a220f57b8144c939a872a..37ef0e45cc0730c9861ca4362a4b93f025002c6d 100644 --- a/crates/migrator/src/migrations/m_2025_05_29/settings.rs +++ b/crates/migrator/src/migrations/m_2025_05_29/settings.rs @@ -19,7 +19,7 @@ fn replace_preferred_completion_mode_value( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; if parent_object_name != "agent" { return None; @@ -30,7 +30,7 @@ fn replace_preferred_completion_mode_value( .nodes_for_capture_index(setting_name_capture_ix) .next()? .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; + let setting_name = contents.get(setting_name_range)?; if setting_name != "preferred_completion_mode" { return None; diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 0cc2f654eac6dab790731cd1ae4f628797729984..6b6d17a2461de62c9c5fa616ee8ccee6390f812b 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -2427,7 +2427,7 @@ impl MultiBuffer { cx.emit(match event { language::BufferEvent::Edited => Event::Edited { singleton_buffer_edited: true, - edited_buffer: Some(buffer.clone()), + edited_buffer: Some(buffer), }, language::BufferEvent::DirtyChanged => Event::DirtyChanged, language::BufferEvent::Saved => Event::Saved, @@ -3560,9 +3560,7 @@ impl MultiBuffer { let multi = cx.new(|_| Self::new(Capability::ReadWrite)); for (text, ranges) in excerpts { let buffer = cx.new(|cx| Buffer::local(text, cx)); - let excerpt_ranges = ranges - .into_iter() - .map(|range| ExcerptRange::new(range.clone())); + let excerpt_ranges = ranges.into_iter().map(ExcerptRange::new); multi.update(cx, |multi, cx| { multi.push_excerpts(buffer, excerpt_ranges, cx) }); diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 77a70dfc8dbbee90cac1d25b3d8336d6a8588ccb..441d2ca4b748e43c8c5db41a113c54e185b2f1de 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -126,7 +126,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement .gap_1() .child( h_flex() - .id(name.clone()) + .id(name) .relative() .w_full() .border_2() @@ -201,7 +201,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement }); } else { let appearance = *SystemAppearance::global(cx); - settings.set_theme(theme.clone(), appearance); + settings.set_theme(theme, appearance); } }); } diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 60a9856abe3c5f0ef7ee397af7b64a716b78fb17..8fae695854b7771f8fa8e7c19826860838ef844f 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -104,7 +104,7 @@ fn write_ui_font_family(font: SharedString, cx: &mut App) { "Welcome Font Changed", type = "ui font", old = theme_settings.ui_font_family, - new = font.clone() + new = font ); theme_settings.ui_font_family = Some(FontFamilyName(font.into())); }); @@ -134,7 +134,7 @@ fn write_buffer_font_family(font_family: SharedString, cx: &mut App) { "Welcome Font Changed", type = "editor font", old = theme_settings.buffer_font_family, - new = font_family.clone() + new = font_family ); theme_settings.buffer_font_family = Some(FontFamilyName(font_family.into())); @@ -314,7 +314,7 @@ fn render_font_customization_section( .child( PopoverMenu::new("ui-font-picker") .menu({ - let ui_font_picker = ui_font_picker.clone(); + let ui_font_picker = ui_font_picker; move |_window, _cx| Some(ui_font_picker.clone()) }) .trigger( @@ -378,7 +378,7 @@ fn render_font_customization_section( .child( PopoverMenu::new("buffer-font-picker") .menu({ - let buffer_font_picker = buffer_font_picker.clone(); + let buffer_font_picker = buffer_font_picker; move |_window, _cx| Some(buffer_font_picker.clone()) }) .trigger( diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index 6a072b00e94c9b6a995d18c06a6dd19759f192b0..d84bc9b0e5b6e505eed139910db848645ba56c15 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -206,7 +206,7 @@ impl ThemePreviewTile { sidebar_width, skeleton_height.clone(), )) - .child(Self::render_pane(seed, theme, skeleton_height.clone())) + .child(Self::render_pane(seed, theme, skeleton_height)) } fn render_borderless(seed: f32, theme: Arc<Theme>) -> impl IntoElement { @@ -260,7 +260,7 @@ impl ThemePreviewTile { .overflow_hidden() .child(div().size_full().child(Self::render_editor( seed, - theme.clone(), + theme, sidebar_width, Self::SKELETON_HEIGHT_DEFAULT, ))) @@ -329,9 +329,9 @@ impl Component for ThemePreviewTile { let themes_to_preview = vec![ one_dark.clone().ok(), - one_light.clone().ok(), - gruvbox_dark.clone().ok(), - gruvbox_light.clone().ok(), + one_light.ok(), + gruvbox_dark.ok(), + gruvbox_light.ok(), ] .into_iter() .flatten() @@ -348,7 +348,7 @@ impl Component for ThemePreviewTile { div() .w(px(240.)) .h(px(180.)) - .child(ThemePreviewTile::new(one_dark.clone(), 0.42)) + .child(ThemePreviewTile::new(one_dark, 0.42)) .into_any_element(), )])] } else { diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 832b7f09d1d0860671c90f31a36d760bfe718506..59c43f945f2509dd8c54c871760d957ceaf4817e 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -5091,7 +5091,7 @@ impl Panel for OutlinePanel { impl Focusable for OutlinePanel { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.filter_editor.focus_handle(cx).clone() + self.filter_editor.focus_handle(cx) } } diff --git a/crates/panel/src/panel.rs b/crates/panel/src/panel.rs index 658a51167ba7da3f02c49ab77b50e72dabbbae57..1930f654e9b632e52719103e5b0a399cfe94f70a 100644 --- a/crates/panel/src/panel.rs +++ b/crates/panel/src/panel.rs @@ -52,7 +52,7 @@ impl RenderOnce for PanelTab { pub fn panel_button(label: impl Into<SharedString>) -> ui::Button { let label = label.into(); - let id = ElementId::Name(label.clone().to_lowercase().replace(' ', "_").into()); + let id = ElementId::Name(label.to_lowercase().replace(' ', "_").into()); ui::Button::new(id, label) .label_size(ui::LabelSize::Small) .icon_size(ui::IconSize::Small) diff --git a/crates/picker/src/popover_menu.rs b/crates/picker/src/popover_menu.rs index d05308ee71e87a472ffcb33e9727ef74fae70602..baf0918fd6c8e20211d04a150af9220cb2d66839 100644 --- a/crates/picker/src/popover_menu.rs +++ b/crates/picker/src/popover_menu.rs @@ -85,7 +85,7 @@ where .menu(move |_window, _cx| Some(picker.clone())) .trigger_with_tooltip(self.trigger, self.tooltip) .anchor(self.anchor) - .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle)) + .when_some(self.handle, |menu, handle| menu.with_handle(handle)) .offset(gpui::Point { x: px(0.0), y: px(-2.0), diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index d36508937768919902b3a29b321d1650dcd87c61..a171b193d0a70c438a3aa269955aed16202626ed 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -168,7 +168,7 @@ impl RemoteBufferStore { .with_context(|| { format!("no worktree found for id {}", file.worktree_id) })?; - buffer_file = Some(Arc::new(File::from_proto(file, worktree.clone(), cx)?) + buffer_file = Some(Arc::new(File::from_proto(file, worktree, cx)?) as Arc<dyn language::File>); } Buffer::from_proto(replica_id, capability, state, buffer_file) @@ -591,7 +591,7 @@ impl LocalBufferStore { else { return Task::ready(Err(anyhow!("no such worktree"))); }; - self.save_local_buffer(buffer, worktree, path.path.clone(), true, cx) + self.save_local_buffer(buffer, worktree, path.path, true, cx) } fn open_buffer( @@ -845,7 +845,7 @@ impl BufferStore { ) -> Task<Result<()>> { match &mut self.state { BufferStoreState::Local(this) => this.save_buffer(buffer, cx), - BufferStoreState::Remote(this) => this.save_remote_buffer(buffer.clone(), None, cx), + BufferStoreState::Remote(this) => this.save_remote_buffer(buffer, None, cx), } } @@ -1138,7 +1138,7 @@ impl BufferStore { envelope: TypedEnvelope<proto::UpdateBuffer>, mut cx: AsyncApp, ) -> Result<proto::Ack> { - let payload = envelope.payload.clone(); + let payload = envelope.payload; let buffer_id = BufferId::new(payload.buffer_id)?; let ops = payload .operations diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index e826f44b7b029ec22ca5e1484f7ff44d487ba196..49a430c26110fc58a9494d414ecbcf45a6c76c49 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -760,7 +760,7 @@ mod tests { &store, vec![ (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), + (server_1_id, ContextServerStatus::Running), (server_2_id.clone(), ContextServerStatus::Starting), (server_2_id.clone(), ContextServerStatus::Running), (server_2_id.clone(), ContextServerStatus::Stopped), diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 343ee83ccbe1581b551045abd769169cb410c3f3..c47e5d35d5948eb0c176bbc6d14281faa3f60451 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -192,7 +192,7 @@ impl BreakpointStore { } pub(crate) fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) { - self.downstream_client = Some((downstream_client.clone(), project_id)); + self.downstream_client = Some((downstream_client, project_id)); } pub(crate) fn unshared(&mut self, cx: &mut Context<Self>) { @@ -450,9 +450,9 @@ impl BreakpointStore { }); if let Some(found_bp) = found_bp { - found_bp.message = Some(log_message.clone()); + found_bp.message = Some(log_message); } else { - breakpoint.bp.message = Some(log_message.clone()); + breakpoint.bp.message = Some(log_message); // We did not remove any breakpoint, hence let's toggle one. breakpoint_set .breakpoints @@ -482,9 +482,9 @@ impl BreakpointStore { }); if let Some(found_bp) = found_bp { - found_bp.hit_condition = Some(hit_condition.clone()); + found_bp.hit_condition = Some(hit_condition); } else { - breakpoint.bp.hit_condition = Some(hit_condition.clone()); + breakpoint.bp.hit_condition = Some(hit_condition); // We did not remove any breakpoint, hence let's toggle one. breakpoint_set .breakpoints @@ -514,9 +514,9 @@ impl BreakpointStore { }); if let Some(found_bp) = found_bp { - found_bp.condition = Some(condition.clone()); + found_bp.condition = Some(condition); } else { - breakpoint.bp.condition = Some(condition.clone()); + breakpoint.bp.condition = Some(condition); // We did not remove any breakpoint, hence let's toggle one. breakpoint_set .breakpoints @@ -591,7 +591,7 @@ impl BreakpointStore { cx: &mut Context<Self>, ) { if let Some(breakpoints) = self.breakpoints.remove(&old_path) { - self.breakpoints.insert(new_path.clone(), breakpoints); + self.breakpoints.insert(new_path, breakpoints); cx.notify(); } diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 3be3192369452b58fd2382471ca2f41f4aeac75f..772ff2dcfeb98fcda794092f8071fad5c6fcdcd4 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -1454,7 +1454,7 @@ impl DapCommand for EvaluateCommand { variables_reference: message.variable_reference, named_variables: message.named_variables, indexed_variables: message.indexed_variables, - memory_reference: message.memory_reference.clone(), + memory_reference: message.memory_reference, value_location_reference: None, //TODO }) } diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 382e83587a661e467113111791b57b627721bbf3..45e1c7f291cdce6564be3e2493e68589bd0f8cc8 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -721,7 +721,7 @@ impl DapStore { downstream_client: AnyProtoClient, _: &mut Context<Self>, ) { - self.downstream_client = Some((downstream_client.clone(), project_id)); + self.downstream_client = Some((downstream_client, project_id)); } pub fn unshared(&mut self, cx: &mut Context<Self>) { diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index cd792877b66e5897b1ea2f8a88615f544814e815..81cb3ade2e18b6430c4b644495529c4567344da5 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1394,7 +1394,7 @@ impl Session { let breakpoint_store = self.breakpoint_store.clone(); if let Some((local, path)) = self.as_running_mut().and_then(|local| { let breakpoint = local.tmp_breakpoint.take()?; - let path = breakpoint.path.clone(); + let path = breakpoint.path; Some((local, path)) }) { local @@ -1710,7 +1710,7 @@ impl Session { this.threads = result .into_iter() - .map(|thread| (ThreadId(thread.id), Thread::from(thread.clone()))) + .map(|thread| (ThreadId(thread.id), Thread::from(thread))) .collect(); this.invalidate_command_type::<StackTraceCommand>(); @@ -2553,10 +2553,7 @@ impl Session { mode: Option<String>, cx: &mut Context<Self>, ) -> Task<Option<dap::DataBreakpointInfoResponse>> { - let command = DataBreakpointInfoCommand { - context: context.clone(), - mode, - }; + let command = DataBreakpointInfoCommand { context, mode }; self.request(command, |_, response, _| response.ok(), cx) } diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index edc6b00a7be995c08cfea7b4d4cf0e7f46ef74b1..5cf298a8bff1dbecbe5899020e4463ba658ed719 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -769,7 +769,7 @@ impl GitStore { .as_ref() .and_then(|weak| weak.upgrade()) { - let conflict_set = conflict_set.clone(); + let conflict_set = conflict_set; let buffer_snapshot = buffer.read(cx).text_snapshot(); git_state.update(cx, |state, cx| { @@ -912,7 +912,7 @@ impl GitStore { return Task::ready(Err(anyhow!("failed to find a git repository for buffer"))); }; let content = match &version { - Some(version) => buffer.rope_for_version(version).clone(), + Some(version) => buffer.rope_for_version(version), None => buffer.as_rope().clone(), }; let version = version.unwrap_or(buffer.version()); @@ -1506,10 +1506,7 @@ impl GitStore { let mut update = envelope.payload; let id = RepositoryId::from_proto(update.id); - let client = this - .upstream_client() - .context("no upstream client")? - .clone(); + let client = this.upstream_client().context("no upstream client")?; let mut is_new = false; let repo = this.repositories.entry(id).or_insert_with(|| { @@ -3418,7 +3415,6 @@ impl Repository { reset_mode: ResetMode, _cx: &mut App, ) -> oneshot::Receiver<Result<()>> { - let commit = commit.to_string(); let id = self.id; self.send_job(None, move |git_repo, _| async move { @@ -3644,7 +3640,7 @@ impl Repository { let to_stage = self .cached_status() .filter(|entry| !entry.status.staging().is_fully_staged()) - .map(|entry| entry.repo_path.clone()) + .map(|entry| entry.repo_path) .collect(); self.stage_entries(to_stage, cx) } @@ -3653,16 +3649,13 @@ impl Repository { let to_unstage = self .cached_status() .filter(|entry| entry.status.staging().has_staged()) - .map(|entry| entry.repo_path.clone()) + .map(|entry| entry.repo_path) .collect(); self.unstage_entries(to_unstage, cx) } pub fn stash_all(&mut self, cx: &mut Context<Self>) -> Task<anyhow::Result<()>> { - let to_stash = self - .cached_status() - .map(|entry| entry.repo_path.clone()) - .collect(); + let to_stash = self.cached_status().map(|entry| entry.repo_path).collect(); self.stash_entries(to_stash, cx) } diff --git a/crates/project/src/git_store/conflict_set.rs b/crates/project/src/git_store/conflict_set.rs index 9d7bd26a92960774b827ca6c12848050b5afbb8b..313a1e90adc2fde8a62dbe6aa60b4d3a366af22c 100644 --- a/crates/project/src/git_store/conflict_set.rs +++ b/crates/project/src/git_store/conflict_set.rs @@ -369,7 +369,7 @@ mod tests { .unindent(); let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(0, buffer_id, test_content.to_string()); + let buffer = Buffer::new(0, buffer_id, test_content); let snapshot = buffer.snapshot(); let conflict_snapshot = ConflictSet::parse(&snapshot); @@ -400,7 +400,7 @@ mod tests { >>>>>>> "# .unindent(); let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(0, buffer_id, test_content.to_string()); + let buffer = Buffer::new(0, buffer_id, test_content); let snapshot = buffer.snapshot(); let conflict_snapshot = ConflictSet::parse(&snapshot); diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index c5a198954e1d2d30655c140d7d46cf84ccbf0c69..e499d4e026f724f12e023738f12afb2735f9ce2d 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -244,7 +244,7 @@ impl ProjectItem for ImageItem { } fn project_path(&self, cx: &App) -> Option<ProjectPath> { - Some(self.project_path(cx).clone()) + Some(self.project_path(cx)) } fn is_dirty(&self) -> bool { @@ -375,7 +375,6 @@ impl ImageStore { let (mut tx, rx) = postage::watch::channel(); entry.insert(rx.clone()); - let project_path = project_path.clone(); let load_image = self .state .open_image(project_path.path.clone(), worktree, cx); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index de6848701f3cd878afcf9c78aaabaebf721bb901..a91e3fb402ff8d02279cc52d685f286de3fa4358 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2739,7 +2739,7 @@ impl GetCodeActions { Some(lsp::CodeActionProviderCapability::Options(CodeActionOptions { code_action_kinds: Some(supported_action_kinds), .. - })) => Some(supported_action_kinds.clone()), + })) => Some(supported_action_kinds), _ => capabilities.code_action_kinds, } } @@ -3793,7 +3793,7 @@ impl GetDocumentDiagnostics { }, uri: lsp::Url::parse(&info.location_url.unwrap()).unwrap(), }, - message: info.message.clone(), + message: info.message, } }) .collect::<Vec<_>>(); @@ -4491,9 +4491,8 @@ mod tests { data: Some(json!({"detail": "test detail"})), }; - let proto_diagnostic = - GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic.clone()) - .expect("Failed to serialize diagnostic"); + let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic) + .expect("Failed to serialize diagnostic"); let start = proto_diagnostic.start.unwrap(); let end = proto_diagnostic.end.unwrap(); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index aa2398e29b0312c0f13f418e3a279cc745bdbff3..7a44ad3f875a8bfea9bcbf112c1a726610867ad3 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -917,7 +917,7 @@ impl LocalLspStore { message: params.message, actions: vec![], response_channel: tx, - lsp_name: name.clone(), + lsp_name: name, }; let _ = this.update(&mut cx, |_, cx| { @@ -2954,7 +2954,7 @@ impl LocalLspStore { .update(cx, |this, cx| { let path = buffer_to_edit.read(cx).project_path(cx); let active_entry = this.active_entry; - let is_active_entry = path.clone().is_some_and(|project_path| { + let is_active_entry = path.is_some_and(|project_path| { this.worktree_store .read(cx) .entry_for_path(&project_path, cx) @@ -5688,10 +5688,7 @@ impl LspStore { let all_actions_task = self.request_multiple_lsp_locally( buffer, Some(range.start), - GetCodeActions { - range: range.clone(), - kinds: kinds.clone(), - }, + GetCodeActions { range, kinds }, cx, ); cx.background_spawn(async move { @@ -7221,7 +7218,7 @@ impl LspStore { worktree = tree; path = rel_path; } else { - worktree = source_worktree.clone(); + worktree = source_worktree; path = relativize_path(&result.worktree_abs_path, &abs_path); } @@ -10338,7 +10335,7 @@ impl LspStore { let name = self .language_server_statuses .remove(&server_id) - .map(|status| status.name.clone()) + .map(|status| status.name) .or_else(|| { if let Some(LanguageServerState::Running { adapter, .. }) = server_state.as_ref() { Some(adapter.name()) diff --git a/crates/project/src/lsp_store/clangd_ext.rs b/crates/project/src/lsp_store/clangd_ext.rs index 274b1b898086eeddf72710052397dd9963833663..b02f68dd4d1271ca9a8fa97e9ef41e03fdfe9763 100644 --- a/crates/project/src/lsp_store/clangd_ext.rs +++ b/crates/project/src/lsp_store/clangd_ext.rs @@ -58,7 +58,7 @@ pub fn register_notifications( language_server .on_notification::<InactiveRegions, _>({ - let adapter = adapter.clone(); + let adapter = adapter; let this = lsp_store; move |params: InactiveRegionsParams, cx| { diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index 6c425717a82e94985c60db8d1034d470f1aeec35..e5e6338d3c901752e020c634d822903c8e591657 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -34,7 +34,6 @@ pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server: language_server .on_notification::<ServerStatus, _>({ - let name = name.clone(); move |params, cx| { let message = params.message; let log_message = message.as_ref().map(|message| { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9cd83647acd257c5c1d5498d64d32c198b79491d..af5fd0d675096493dd41f09bfb653b2cc3c25d7c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2502,7 +2502,7 @@ impl Project { path: ProjectPath, cx: &mut Context<Self>, ) -> Task<Result<(Option<ProjectEntryId>, Entity<Buffer>)>> { - let task = self.open_buffer(path.clone(), cx); + let task = self.open_buffer(path, cx); cx.spawn(async move |_project, cx| { let buffer = task.await?; let project_entry_id = buffer.read_with(cx, |buffer, cx| { @@ -3170,7 +3170,7 @@ impl Project { if let ImageItemEvent::ReloadNeeded = event && !self.is_via_collab() { - self.reload_images([image.clone()].into_iter().collect(), cx) + self.reload_images([image].into_iter().collect(), cx) .detach_and_log_err(cx); } @@ -3652,7 +3652,7 @@ impl Project { cx: &mut Context<Self>, ) -> Task<Result<Vec<CodeAction>>> { let snapshot = buffer.read(cx).snapshot(); - let range = range.clone().to_owned().to_point(&snapshot); + let range = range.to_point(&snapshot); let range_start = snapshot.anchor_before(range.start); let range_end = if range.start == range.end { range_start diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 70eb6d34f8d07506dabee7f2f41fcf1ba89565dd..8b0b21fcd63e2f10509cb9c41a8cc50ca237791b 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1818,7 +1818,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp buffer .snapshot() .diagnostics_in_range::<_, usize>(0..1, false) - .map(|entry| entry.diagnostic.message.clone()) + .map(|entry| entry.diagnostic.message) .collect::<Vec<_>>(), ["the message".to_string()] ); @@ -1844,7 +1844,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp buffer .snapshot() .diagnostics_in_range::<_, usize>(0..1, false) - .map(|entry| entry.diagnostic.message.clone()) + .map(|entry| entry.diagnostic.message) .collect::<Vec<_>>(), Vec::<String>::new(), ); @@ -3712,7 +3712,7 @@ async fn test_save_file_spawns_language_server(cx: &mut gpui::TestAppContext) { async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/dir"), json!({ @@ -3767,7 +3767,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/dir"), json!({ @@ -5897,7 +5897,7 @@ async fn test_search_with_unicode(cx: &mut gpui::TestAppContext) { async fn test_create_entry(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/one/two", json!({ diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index e51f8e0b3b5ecd4513f1c86ede35d95f6633df4a..15e6024808e0075c853985b6c73ea388c9fea411 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -760,7 +760,7 @@ impl Inventory { TaskSettingsLocation::Global(path) => { previously_existing_scenarios = parsed_scenarios .global_scenarios() - .map(|(_, scenario)| scenario.label.clone()) + .map(|(_, scenario)| scenario.label) .collect::<HashSet<_>>(); parsed_scenarios .global @@ -770,7 +770,7 @@ impl Inventory { TaskSettingsLocation::Worktree(location) => { previously_existing_scenarios = parsed_scenarios .worktree_scenarios(location.worktree_id) - .map(|(_, scenario)| scenario.label.clone()) + .map(|(_, scenario)| scenario.label) .collect::<HashSet<_>>(); if new_templates.is_empty() { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index b2556d7584d07db0c22a246489d9a78328b91fea..e9582e73fd6884f73e8bf7abcc04a7489bad9dc5 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -89,7 +89,7 @@ impl Project { let ssh_client = ssh_client.read(cx); if let Some((SshArgs { arguments, envs }, path_style)) = ssh_client.ssh_info() { return Some(SshDetails { - host: ssh_client.connection_options().host.clone(), + host: ssh_client.connection_options().host, ssh_command: SshCommand { arguments }, envs, path_style, diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 16e42e90cbbe654b0c2a22eb9d7cbee7ad902abd..b8905c73bc66ab3f9a911785fe812401dfdb6ee4 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -457,7 +457,7 @@ impl WorktreeStore { }) .collect::<HashMap<_, _>>(); - let (client, project_id) = self.upstream_client().clone().context("invalid project")?; + let (client, project_id) = self.upstream_client().context("invalid project")?; for worktree in worktrees { if let Some(old_worktree) = diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index bb612ac47594867653c1c80eeb14c81daf51058b..a5bfa883d5a3b78bc2be409ab84079bd918acf60 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -447,7 +447,7 @@ impl ProjectPanel { cx.subscribe(&project, |this, project, event, cx| match event { project::Event::ActiveEntryChanged(Some(entry_id)) => { if ProjectPanelSettings::get_global(cx).auto_reveal_entries { - this.reveal_entry(project.clone(), *entry_id, true, cx).ok(); + this.reveal_entry(project, *entry_id, true, cx).ok(); } } project::Event::ActiveEntryChanged(None) => { @@ -462,10 +462,7 @@ impl ProjectPanel { } } project::Event::RevealInProjectPanel(entry_id) => { - if let Some(()) = this - .reveal_entry(project.clone(), *entry_id, false, cx) - .log_err() - { + if let Some(()) = this.reveal_entry(project, *entry_id, false, cx).log_err() { cx.emit(PanelEvent::Activate); } } @@ -813,7 +810,7 @@ impl ProjectPanel { diagnostic_severity: DiagnosticSeverity, ) { diagnostics - .entry((project_path.worktree_id, path_buffer.clone())) + .entry((project_path.worktree_id, path_buffer)) .and_modify(|strongest_diagnostic_severity| { *strongest_diagnostic_severity = cmp::min(*strongest_diagnostic_severity, diagnostic_severity); @@ -2780,7 +2777,7 @@ impl ProjectPanel { let destination_worktree = self.project.update(cx, |project, cx| { let entry_path = project.path_for_entry(entry_to_move, cx)?; - let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); + let destination_entry_path = project.path_for_entry(destination, cx)?.path; let mut destination_path = destination_entry_path.as_ref(); if destination_is_file { @@ -4023,8 +4020,8 @@ impl ProjectPanel { .as_ref() .map_or(ValidationState::None, |e| e.validation_state.clone()) { - ValidationState::Error(msg) => Some((Color::Error.color(cx), msg.clone())), - ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg.clone())), + ValidationState::Error(msg) => Some((Color::Error.color(cx), msg)), + ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg)), ValidationState::None => None, } } else { @@ -5505,7 +5502,7 @@ impl Render for ProjectPanel { .with_priority(3) })) } else { - let focus_handle = self.focus_handle(cx).clone(); + let focus_handle = self.focus_handle(cx); v_flex() .id("empty-project_panel") diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index de3316e3573e4eb3d9628b19f4f206bc53d4dbca..49b482e02c5c6d88e4f3d832d254ef5db121c2f9 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -17,7 +17,7 @@ use workspace::{ async fn test_visible_list(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -106,7 +106,7 @@ async fn test_visible_list(cx: &mut gpui::TestAppContext) { async fn test_opening_file(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/src"), json!({ @@ -276,7 +276,7 @@ async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) { async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root1"), json!({ @@ -459,7 +459,7 @@ async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) { async fn test_editing_files(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -877,7 +877,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -1010,7 +1010,7 @@ async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root1"), json!({ @@ -1137,7 +1137,7 @@ async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) { async fn test_copy_paste(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -1235,7 +1235,7 @@ async fn test_copy_paste(cx: &mut gpui::TestAppContext) { async fn test_cut_paste(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -1320,7 +1320,7 @@ async fn test_cut_paste(cx: &mut gpui::TestAppContext) { async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -1416,7 +1416,7 @@ async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContex async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -1551,7 +1551,7 @@ async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppConte async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -1692,7 +1692,7 @@ async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) { async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/test", json!({ @@ -1797,7 +1797,7 @@ async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppConte async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/test", json!({ @@ -1876,7 +1876,7 @@ async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/src"), json!({ @@ -1968,7 +1968,7 @@ async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/src", json!({ @@ -2161,7 +2161,7 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { async fn test_select_git_entry(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ @@ -2440,7 +2440,7 @@ async fn test_select_git_entry(cx: &mut gpui::TestAppContext) { async fn test_select_directory(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/project_root", json!({ @@ -2541,7 +2541,7 @@ async fn test_select_directory(cx: &mut gpui::TestAppContext) { async fn test_select_first_last(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/project_root", json!({ @@ -2651,7 +2651,7 @@ async fn test_select_first_last(cx: &mut gpui::TestAppContext) { async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/project_root", json!({ @@ -2693,7 +2693,7 @@ async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) { async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/project_root", json!({ @@ -2751,7 +2751,7 @@ async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { async fn test_new_file_move(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.as_fake().insert_tree(path!("/root"), json!({})).await; let project = Project::test(fs, [path!("/root").as_ref()], cx).await; let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); @@ -2819,7 +2819,7 @@ async fn test_new_file_move(cx: &mut gpui::TestAppContext) { async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -2895,7 +2895,7 @@ async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) { async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -2989,7 +2989,7 @@ async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/project_root", json!({ @@ -3731,7 +3731,7 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { register_project_item::<TestProjectItemView>(cx); }); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -3914,7 +3914,7 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/src", json!({ @@ -3982,7 +3982,7 @@ async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppC async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -4105,7 +4105,7 @@ async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) { async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ @@ -4206,7 +4206,7 @@ async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) { async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ @@ -4271,7 +4271,7 @@ async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) { async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -4382,7 +4382,7 @@ async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) { async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -4457,7 +4457,7 @@ async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) { async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -4523,7 +4523,7 @@ async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) { async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); // First worktree fs.insert_tree( "/root1", @@ -4666,7 +4666,7 @@ async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) { async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -4766,7 +4766,7 @@ async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root_b", json!({ @@ -4859,7 +4859,7 @@ fn toggle_expand_dir( async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ @@ -5050,7 +5050,7 @@ async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) { async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ @@ -5234,7 +5234,7 @@ async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) { async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ @@ -5299,7 +5299,7 @@ async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) { async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ @@ -5448,7 +5448,7 @@ async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppC async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -5516,7 +5516,7 @@ async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) { async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -5647,7 +5647,7 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) async fn test_hide_root(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -5825,7 +5825,7 @@ async fn test_hide_root(cx: &mut gpui::TestAppContext) { async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -5923,7 +5923,7 @@ async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) { async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -6152,7 +6152,7 @@ fn init_test_with_editor(cx: &mut TestAppContext) { language::init(cx); editor::init(cx); crate::init(cx); - workspace::init(app_state.clone(), cx); + workspace::init(app_state, cx); Project::init_settings(cx); cx.update_global::<SettingsStore, _>(|store, cx| { diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 9d0f54bc01c3d4f3e4d19416a20c6903a9f8879b..72029e55a0dad7e2070f4660b86b4b4d1eb4ffba 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -233,7 +233,7 @@ impl PickerDelegate for ProjectSymbolsDelegate { } } let label = symbol.label.text.clone(); - let path = path.to_string().clone(); + let path = path.to_string(); let highlights = gpui::combine_highlights( string_match @@ -257,10 +257,8 @@ impl PickerDelegate for ProjectSymbolsDelegate { v_flex() .child( LabelLike::new().child( - StyledText::new(label).with_default_highlights( - &window.text_style().clone(), - highlights, - ), + StyledText::new(label) + .with_default_highlights(&window.text_style(), highlights), ), ) .child(Label::new(path).color(Color::Muted)), diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs index 4ab867ab64415cf546ffd9d66c1b7aad67df0aae..9a9b2fc3de91cea156b1c0affc2e2a8184aebb42 100644 --- a/crates/prompt_store/src/prompts.rs +++ b/crates/prompt_store/src/prompts.rs @@ -403,7 +403,7 @@ impl PromptBuilder { ContentPromptDiagnosticContext { line_number: (start.row + 1) as usize, error_message: entry.diagnostic.message.clone(), - code_content: buffer.text_for_range(entry.range.clone()).collect(), + code_content: buffer.text_for_range(entry.range).collect(), } }) .collect(); diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 0f43d83d860990008a5c1e685f2f9a4fbbabc387..a9c3284d0bd5d9dc5e279ce317f2280914bd623f 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -119,7 +119,7 @@ impl EditNicknameState { let starting_text = SshSettings::get_global(cx) .ssh_connections() .nth(index) - .and_then(|state| state.nickname.clone()) + .and_then(|state| state.nickname) .filter(|text| !text.is_empty()); this.editor.update(cx, |this, cx| { this.set_placeholder_text("Add a nickname for this server", cx); @@ -165,7 +165,7 @@ impl ProjectPicker { let nickname = connection.nickname.clone().map(|nick| nick.into()); let _path_task = cx .spawn_in(window, { - let workspace = workspace.clone(); + let workspace = workspace; async move |this, cx| { let Ok(Some(paths)) = rx.await else { workspace @@ -520,7 +520,7 @@ impl RemoteServerProjects { self.mode = Mode::CreateRemoteServer(CreateRemoteServer { address_editor: editor, address_error: None, - ssh_prompt: Some(ssh_prompt.clone()), + ssh_prompt: Some(ssh_prompt), _creating: Some(creating), }); } @@ -843,7 +843,7 @@ impl RemoteServerProjects { .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) .child(Label::new("Open Folder")) .on_click(cx.listener({ - let ssh_connection = connection.clone(); + let ssh_connection = connection; let host = host.clone(); move |this, _, window, cx| { let new_ix = this.create_host_from_ssh_config(&host, cx); @@ -1376,7 +1376,7 @@ impl RemoteServerProjects { }; let connection_string = connection.host.clone(); - let nickname = connection.nickname.clone().map(|s| s.into()); + let nickname = connection.nickname.map(|s| s.into()); v_flex() .id("ssh-edit-nickname") diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index 670fcb4800281f0fe825228703bab9be4418d167..d07ea48c7e439651d6612bbea046f7711a6a124a 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -681,7 +681,7 @@ pub async fn open_ssh_project( window .update(cx, |workspace, _, cx| { - if let Some(client) = workspace.project().read(cx).ssh_client().clone() { + if let Some(client) = workspace.project().read(cx).ssh_client() { ExtensionStore::global(cx) .update(cx, |store, cx| store.register_ssh_client(client, cx)); } diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index fddf47660dc49f96d1507c8f45716b559cce1026..5fa3a5f71571280794d915a3b7218b3523eb8c87 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -233,8 +233,8 @@ impl SshConnectionOptions { }; Ok(Self { - host: hostname.to_string(), - username: username.clone(), + host: hostname, + username, port, port_forwards, args: Some(args), @@ -1363,7 +1363,7 @@ impl ConnectionPool { impl From<SshRemoteClient> for AnyProtoClient { fn from(client: SshRemoteClient) -> Self { - AnyProtoClient::new(client.client.clone()) + AnyProtoClient::new(client.client) } } diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 3bcdcbd73c6cdc2a7ab6e4d8c947ace48fa98134..83caebe62fe5d1f306e9302e0c81a998fbf2472d 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -237,11 +237,11 @@ impl HeadlessProject { session.add_entity_message_handler(BufferStore::handle_close_buffer); session.add_request_handler( - extensions.clone().downgrade(), + extensions.downgrade(), HeadlessExtensionStore::handle_sync_extensions, ); session.add_request_handler( - extensions.clone().downgrade(), + extensions.downgrade(), HeadlessExtensionStore::handle_install_extension, ); diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 4ce133cbb102ab9c6137417db9f30146f31211da..b8a735155208097872fc4e8eac71840ad064dfcf 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -160,7 +160,7 @@ fn init_panic_hook(session_id: String) { let panic_data = telemetry_events::Panic { thread: thread_name.into(), - payload: payload.clone(), + payload, location_data: info.location().map(|location| LocationData { file: location.file().into(), line: location.line(), @@ -799,7 +799,6 @@ fn initialize_settings( watch_config_file(cx.background_executor(), fs, paths::settings_file().clone()); handle_settings_file_changes(user_settings_file_rx, cx, { - let session = session.clone(); move |err, _cx| { if let Some(e) = err { log::info!("Server settings failed to change: {}", e); diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs index 714cb3aed33259023b9ff99e3e3819f4a7419e1a..bceefd08cc8f897cc33a3bdb4b1be5c2dc90df10 100644 --- a/crates/repl/src/components/kernel_options.rs +++ b/crates/repl/src/components/kernel_options.rs @@ -187,7 +187,7 @@ impl PickerDelegate for KernelPickerDelegate { .size(LabelSize::Default), ), ) - .when_some(path_or_url.clone(), |flex, path| { + .when_some(path_or_url, |flex, path| { flex.text_ellipsis().child( Label::new(path) .size(LabelSize::Small) diff --git a/crates/repl/src/kernels/remote_kernels.rs b/crates/repl/src/kernels/remote_kernels.rs index 1bef6c24db2b17758002038432bc645bbed2cac2..6bc8b0d1b1c8d1894b61153814dda0307d0e08f5 100644 --- a/crates/repl/src/kernels/remote_kernels.rs +++ b/crates/repl/src/kernels/remote_kernels.rs @@ -95,7 +95,7 @@ pub async fn list_remote_kernelspecs( .kernelspecs .into_iter() .map(|(name, spec)| RemoteKernelSpecification { - name: name.clone(), + name, url: remote_server.base_url.clone(), token: remote_server.token.clone(), kernelspec: spec.spec, @@ -103,7 +103,7 @@ pub async fn list_remote_kernelspecs( .collect::<Vec<RemoteKernelSpecification>>(); anyhow::ensure!(!remote_kernelspecs.is_empty(), "No kernel specs found"); - Ok(remote_kernelspecs.clone()) + Ok(remote_kernelspecs) } impl PartialEq for RemoteKernelSpecification { diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index 1508c2b531cfe846ea74015342f4dc55d92c1e0c..767b103435e1f80b2b6802bdc2525fcd992931bc 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -228,26 +228,23 @@ impl Output { .child(div().flex_1().children(content)) .children(match self { Self::Plain { content, .. } => { - Self::render_output_controls(content.clone(), workspace.clone(), window, cx) + Self::render_output_controls(content.clone(), workspace, window, cx) } Self::Markdown { content, .. } => { - Self::render_output_controls(content.clone(), workspace.clone(), window, cx) + Self::render_output_controls(content.clone(), workspace, window, cx) } Self::Stream { content, .. } => { - Self::render_output_controls(content.clone(), workspace.clone(), window, cx) + Self::render_output_controls(content.clone(), workspace, window, cx) } Self::Image { content, .. } => { - Self::render_output_controls(content.clone(), workspace.clone(), window, cx) + Self::render_output_controls(content.clone(), workspace, window, cx) + } + Self::ErrorOutput(err) => { + Self::render_output_controls(err.traceback.clone(), workspace, window, cx) } - Self::ErrorOutput(err) => Self::render_output_controls( - err.traceback.clone(), - workspace.clone(), - window, - cx, - ), Self::Message(_) => None, Self::Table { content, .. } => { - Self::render_output_controls(content.clone(), workspace.clone(), window, cx) + Self::render_output_controls(content.clone(), workspace, window, cx) } Self::ClearOutputWaitMarker => None, }) diff --git a/crates/repl/src/outputs/markdown.rs b/crates/repl/src/outputs/markdown.rs index 118260ae9454149038d978c601367ae81b5d524f..bd88f4e159f7dbd472ad52a9fa424741400394d7 100644 --- a/crates/repl/src/outputs/markdown.rs +++ b/crates/repl/src/outputs/markdown.rs @@ -35,7 +35,7 @@ impl MarkdownView { }); Self { - raw_text: text.clone(), + raw_text: text, image_cache: RetainAllImageCache::new(cx), contents: None, parsing_markdown_task: Some(task), diff --git a/crates/repl/src/repl_editor.rs b/crates/repl/src/repl_editor.rs index e97223ceb9e4f440f2a57e190d7273834812c2f8..b4c928c33e021229caaa68e11b7cdd7228ed934d 100644 --- a/crates/repl/src/repl_editor.rs +++ b/crates/repl/src/repl_editor.rs @@ -202,7 +202,7 @@ pub fn session(editor: WeakEntity<Editor>, cx: &mut App) -> SessionSupport { return SessionSupport::Unsupported; }; - let worktree_id = worktree_id_for_editor(editor.clone(), cx); + let worktree_id = worktree_id_for_editor(editor, cx); let Some(worktree_id) = worktree_id else { return SessionSupport::Unsupported; @@ -216,7 +216,7 @@ pub fn session(editor: WeakEntity<Editor>, cx: &mut App) -> SessionSupport { Some(kernelspec) => SessionSupport::Inactive(kernelspec), None => { // For language_supported, need to check available kernels for language - if language_supported(&language.clone(), cx) { + if language_supported(&language, cx) { SessionSupport::RequiresSetup(language.name()) } else { SessionSupport::Unsupported @@ -326,7 +326,7 @@ pub fn setup_editor_session_actions(editor: &mut Editor, editor_handle: WeakEnti editor .register_action({ - let editor_handle = editor_handle.clone(); + let editor_handle = editor_handle; move |_: &Restart, window, cx| { if !JupyterSettings::enabled(cx) { return; @@ -420,7 +420,7 @@ fn runnable_ranges( if let Some(language) = buffer.language() && language.name() == "Markdown".into() { - return (markdown_code_blocks(buffer, range.clone(), cx), None); + return (markdown_code_blocks(buffer, range, cx), None); } let (jupytext_snippets, next_cursor) = jupytext_cells(buffer, range.clone()); @@ -685,8 +685,8 @@ mod tests { let python = languages::language("python", tree_sitter_python::LANGUAGE.into()); let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); language_registry.add(markdown.clone()); - language_registry.add(typescript.clone()); - language_registry.add(python.clone()); + language_registry.add(typescript); + language_registry.add(python); // Two code blocks intersecting with selection let buffer = cx.new(|cx| { diff --git a/crates/repl/src/repl_sessions_ui.rs b/crates/repl/src/repl_sessions_ui.rs index f57dd64770f27e6e7b7360a16c3a9ec21912bc86..493b8aa950fb6fe9750fbc31c29d237b26d11d72 100644 --- a/crates/repl/src/repl_sessions_ui.rs +++ b/crates/repl/src/repl_sessions_ui.rs @@ -129,7 +129,6 @@ pub fn init(cx: &mut App) { editor .register_action({ - let editor_handle = editor_handle.clone(); move |_: &RunInPlace, window, cx| { if !JupyterSettings::enabled(cx) { return; diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index f945e5ed9f52a01856734083e145ff0db9d46080..674639c402f0bb81437ce4f2ee440f2edaed4a8e 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -460,7 +460,6 @@ impl Session { Kernel::StartingKernel(task) => { // Queue up the execution as a task to run after the kernel starts let task = task.clone(); - let message = message.clone(); cx.spawn(async move |this, cx| { task.await; @@ -568,7 +567,7 @@ impl Session { match kernel { Kernel::RunningKernel(mut kernel) => { - let mut request_tx = kernel.request_tx().clone(); + let mut request_tx = kernel.request_tx(); let forced = kernel.force_shutdown(window, cx); @@ -605,7 +604,7 @@ impl Session { // Do nothing if already restarting } Kernel::RunningKernel(mut kernel) => { - let mut request_tx = kernel.request_tx().clone(); + let mut request_tx = kernel.request_tx(); let forced = kernel.force_shutdown(window, cx); diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index 379daa4224d5cc930f1411c415aa471088bb53ae..00679d8cf539af5759250dfe6fc7406e192407fb 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -408,7 +408,7 @@ impl<'a> ChunkSlice<'a> { } let row_offset_range = self.offset_range_for_row(point.0.row); - let line = self.slice(row_offset_range.clone()); + let line = self.slice(row_offset_range); if point.0.column == 0 { Point::new(point.0.row, 0) } else if point.0.column >= line.len_utf16().0 as u32 { diff --git a/crates/rpc/src/conn.rs b/crates/rpc/src/conn.rs index 78db80e3983cecdcb1e39a1632dcca8e2b73bf9b..e598e5f7bc863e51a60af83d3ee5213f0371bde4 100644 --- a/crates/rpc/src/conn.rs +++ b/crates/rpc/src/conn.rs @@ -80,7 +80,6 @@ impl Connection { }); let rx = rx.then({ - let executor = executor.clone(); move |msg| { let killed = killed.clone(); let executor = executor.clone(); diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 98f5fa40e9636ae6ba2b5d448859b42a8214ef56..73be0f19fe20ba9228bb3264a812c7404543aeff 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -378,7 +378,6 @@ impl Peer { impl Future<Output = anyhow::Result<()>> + Send + use<>, BoxStream<'static, Box<dyn AnyTypedEnvelope>>, ) { - let executor = executor.clone(); self.add_connection(connection, move |duration| executor.timer(duration)) } diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index bebe4315e444aa100fa70b636e5d23015ed5fbe2..5ad3996e784a27f0552059bf5a2e55addb11f0fd 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -418,7 +418,7 @@ impl RulesLibrary { } else { None }, - store: store.clone(), + store, language_registry, rule_editors: HashMap::default(), active_rule_id: None, @@ -1136,7 +1136,7 @@ impl RulesLibrary { .child( Label::new(format!( "{} tokens", - label_token_count.clone() + label_token_count )) .color(Color::Muted), ) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 75042f184f3d3fe285a700a87d430f384df0127d..a38dc8c35b3a0caef230247505a6131d40170bca 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -716,10 +716,10 @@ impl BufferSearchBar { self.replace_enabled = deploy.replace_enabled; self.selection_search_enabled = deploy.selection_search_enabled; if deploy.focus { - let mut handle = self.query_editor.focus_handle(cx).clone(); + let mut handle = self.query_editor.focus_handle(cx); let mut select_query = true; if deploy.replace_enabled && handle.is_focused(window) { - handle = self.replacement_editor.focus_handle(cx).clone(); + handle = self.replacement_editor.focus_handle(cx); select_query = false; }; diff --git a/crates/search/src/buffer_search/registrar.rs b/crates/search/src/buffer_search/registrar.rs index 4351e38618dc911d0486ccb1313d2c129e2cedff..0e227cbb7c3a465892a2eed867a001aa48b80ff9 100644 --- a/crates/search/src/buffer_search/registrar.rs +++ b/crates/search/src/buffer_search/registrar.rs @@ -42,7 +42,6 @@ impl<T: 'static> SearchActionsRegistrar for DivRegistrar<'_, '_, T> { self.div = self.div.take().map(|div| { div.on_action(self.cx.listener(move |this, action, window, cx| { let should_notify = (getter)(this, window, cx) - .clone() .map(|search_bar| { search_bar.update(cx, |search_bar, cx| { callback.execute(search_bar, action, window, cx) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 0886654d62302a5e6322ecff117f5b6c9a4f8fe0..c4ba9b5154fc91cc4eaa3f9fbf28682d3f584a87 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -3716,7 +3716,7 @@ pub mod tests { window .update(cx, |_, _, cx| { search_view.update(cx, |search_view, cx| { - search_view.query_editor.read(cx).text(cx).to_string() + search_view.query_editor.read(cx).text(cx) }) }) .unwrap() @@ -3883,7 +3883,6 @@ pub mod tests { // Add a project search item to the second pane window .update(cx, { - let search_bar = search_bar.clone(); |workspace, window, cx| { assert_eq!(workspace.panes().len(), 2); second_pane.update(cx, |pane, cx| { diff --git a/crates/semantic_index/examples/index.rs b/crates/semantic_index/examples/index.rs index da27b8ad224d31c4e1de8b7d966a925bc52782d1..86f1e53a606c5a38846e937347f20b6166a7b728 100644 --- a/crates/semantic_index/examples/index.rs +++ b/crates/semantic_index/examples/index.rs @@ -35,7 +35,7 @@ fn main() { None, )); let client = client::Client::new(clock, http.clone(), cx); - Client::set_global(client.clone(), cx); + Client::set_global(client, cx); let args: Vec<String> = std::env::args().collect(); if args.len() < 2 { @@ -49,7 +49,7 @@ fn main() { let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY not set"); let embedding_provider = Arc::new(OpenAiEmbeddingProvider::new( - http.clone(), + http, OpenAiEmbeddingModel::TextEmbedding3Small, open_ai::OPEN_AI_API_URL.to_string(), api_key, diff --git a/crates/semantic_index/src/embedding_index.rs b/crates/semantic_index/src/embedding_index.rs index eeb3c91fcd855da99e645a62a7c86fd4a66b72b1..c54cd9d3c36216a00d5aca898ebe1bb0e3499f2e 100644 --- a/crates/semantic_index/src/embedding_index.rs +++ b/crates/semantic_index/src/embedding_index.rs @@ -88,7 +88,7 @@ impl EmbeddingIndex { let worktree = self.worktree.read(cx).snapshot(); let worktree_abs_path = worktree.abs_path().clone(); - let scan = self.scan_updated_entries(worktree, updated_entries.clone(), cx); + let scan = self.scan_updated_entries(worktree, updated_entries, cx); let chunk = self.chunk_files(worktree_abs_path, scan.updated_entries, cx); let embed = Self::embed_files(self.embedding_provider.clone(), chunk.files, cx); let persist = self.persist_embeddings(scan.deleted_entry_ranges, embed.files, cx); @@ -406,7 +406,7 @@ impl EmbeddingIndex { .context("failed to create read transaction")?; let result = db .iter(&tx)? - .map(|entry| Ok(entry?.1.path.clone())) + .map(|entry| Ok(entry?.1.path)) .collect::<Result<Vec<Arc<Path>>>>(); drop(tx); result @@ -423,8 +423,7 @@ impl EmbeddingIndex { Ok(db .get(&tx, &db_key_for_path(&path))? .context("no such path")? - .chunks - .clone()) + .chunks) }) } } diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 1dafeb072fc944f4356894d7a74197cac3de55f6..439791047a282771f94982c5bad4c690df497cc4 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -434,7 +434,7 @@ mod tests { .await; let range = search_result.range.clone(); - let content = content[range.clone()].to_owned(); + let content = content[range].to_owned(); assert!(content.contains("garbage in, garbage out")); } diff --git a/crates/semantic_index/src/summary_index.rs b/crates/semantic_index/src/summary_index.rs index d1c9a3abaca4be36f18c6aab81565067f6ba032b..9a3eb302edaaef831f515edf3492aecf59bf17f7 100644 --- a/crates/semantic_index/src/summary_index.rs +++ b/crates/semantic_index/src/summary_index.rs @@ -205,7 +205,7 @@ impl SummaryIndex { let worktree = self.worktree.read(cx).snapshot(); let worktree_abs_path = worktree.abs_path().clone(); - backlogged = self.scan_updated_entries(worktree, updated_entries.clone(), cx); + backlogged = self.scan_updated_entries(worktree, updated_entries, cx); digest = self.digest_files(backlogged.paths_to_digest, worktree_abs_path, cx); needs_summary = self.check_summary_cache(digest.files, cx); summaries = self.summarize_files(needs_summary.files, cx); diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index a472c50e6c6c73613b224524466ce90f241a54eb..8080ec8d5f56b8f82a460dd7edcf0c14cd98c9e9 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -361,7 +361,7 @@ pub fn replace_top_level_array_value_in_json_text( let needs_indent = range.start_point.row > 0; if new_value.is_none() && key_path.is_empty() { - let mut remove_range = text_range.clone(); + let mut remove_range = text_range; if index == 0 { while cursor.goto_next_sibling() && (cursor.node().is_extra() || cursor.node().is_missing()) @@ -582,7 +582,7 @@ mod tests { expected: String, ) { let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None); - let mut result_str = input.to_string(); + let mut result_str = input; result_str.replace_range(result.0, &result.1); pretty_assertions::assert_eq!(expected, result_str); } diff --git a/crates/settings_profile_selector/src/settings_profile_selector.rs b/crates/settings_profile_selector/src/settings_profile_selector.rs index 25be67bfd720dbcbd5e8a4e0b29431321806db0a..d7ebd6488dd07647f29fbe05eac4a0eabf74765a 100644 --- a/crates/settings_profile_selector/src/settings_profile_selector.rs +++ b/crates/settings_profile_selector/src/settings_profile_selector.rs @@ -135,7 +135,7 @@ impl SettingsProfileSelectorDelegate { ) -> Option<String> { if let Some(profile_name) = profile_name { cx.set_global(ActiveSettingsProfileName(profile_name.clone())); - return Some(profile_name.clone()); + return Some(profile_name); } if cx.has_global::<ActiveSettingsProfileName>() { diff --git a/crates/settings_ui/src/appearance_settings_controls.rs b/crates/settings_ui/src/appearance_settings_controls.rs index 141ae131826f43bf39dd1a7fa435753f84801e4f..255f5a36b5868b3fd36085e8c980eb7a17fdc163 100644 --- a/crates/settings_ui/src/appearance_settings_controls.rs +++ b/crates/settings_ui/src/appearance_settings_controls.rs @@ -83,7 +83,7 @@ impl RenderOnce for ThemeControl { DropdownMenu::new( "theme", - value.clone(), + value, ContextMenu::build(window, cx, |mut menu, _, cx| { let theme_registry = ThemeRegistry::global(cx); @@ -204,7 +204,7 @@ impl RenderOnce for UiFontFamilyControl { .child(Icon::new(IconName::Font)) .child(DropdownMenu::new( "ui-font-family", - value.clone(), + value, ContextMenu::build(window, cx, |mut menu, _, cx| { let font_family_cache = FontFamilyCache::global(cx); diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 12e3c0c2742aaa0dc1cc31fc7617873f5b541418..9a2d33ef7cf16e87dae90fe8e0b4e3b5283b229c 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -1182,8 +1182,8 @@ impl KeymapEditor { return; }; - telemetry::event!("Keybinding Context Copied", context = context.clone()); - cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone())); + telemetry::event!("Keybinding Context Copied", context = context); + cx.write_to_clipboard(gpui::ClipboardItem::new_string(context)); } fn copy_action_to_clipboard( @@ -1199,8 +1199,8 @@ impl KeymapEditor { return; }; - telemetry::event!("Keybinding Action Copied", action = action.clone()); - cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone())); + telemetry::event!("Keybinding Action Copied", action = action); + cx.write_to_clipboard(gpui::ClipboardItem::new_string(action)); } fn toggle_conflict_filter( @@ -1464,7 +1464,7 @@ impl RenderOnce for KeybindContextString { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { match self { KeybindContextString::Global => { - muted_styled_text(KeybindContextString::GLOBAL.clone(), cx).into_any_element() + muted_styled_text(KeybindContextString::GLOBAL, cx).into_any_element() } KeybindContextString::Local(name, language) => { SyntaxHighlightedText::new(name, language).into_any_element() @@ -1748,7 +1748,7 @@ impl Render for KeymapEditor { } else { const NULL: SharedString = SharedString::new_static("<null>"); - muted_styled_text(NULL.clone(), cx) + muted_styled_text(NULL, cx) .into_any_element() } }) diff --git a/crates/story/src/story.rs b/crates/story/src/story.rs index 6fed0ab12da2fbcae5e7ebfd462283e9403ae94a..b59cb6fb99086de7eb22ab2645dc01dbe15fb959 100644 --- a/crates/story/src/story.rs +++ b/crates/story/src/story.rs @@ -194,7 +194,7 @@ impl RenderOnce for StorySection { // Section title .py_2() // Section description - .when_some(self.description.clone(), |section, description| { + .when_some(self.description, |section, description| { section.child(Story::description(description, cx)) }) .child(div().flex().flex_col().gap_2().children(children)) diff --git a/crates/supermaven/src/supermaven.rs b/crates/supermaven/src/supermaven.rs index 743c0d4c7d82726751822fb12f3ef0df9dfd40a9..7a9963dbc424185c52be6879a0a9e722db7106b2 100644 --- a/crates/supermaven/src/supermaven.rs +++ b/crates/supermaven/src/supermaven.rs @@ -384,9 +384,7 @@ impl SupermavenAgent { match message { SupermavenMessage::ActivationRequest(request) => { self.account_status = match request.activate_url { - Some(activate_url) => AccountStatus::NeedsActivation { - activate_url: activate_url.clone(), - }, + Some(activate_url) => AccountStatus::NeedsActivation { activate_url }, None => AccountStatus::Ready, }; } diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index 1b1fc54a7a335ac436038fe9c254678a6628cb78..eb54c83f8126002a19728e51b282b98191707717 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -45,9 +45,7 @@ fn completion_from_diff( position: Anchor, delete_range: Range<Anchor>, ) -> EditPrediction { - let buffer_text = snapshot - .text_for_range(delete_range.clone()) - .collect::<String>(); + let buffer_text = snapshot.text_for_range(delete_range).collect::<String>(); let mut edits: Vec<(Range<language::Anchor>, String)> = Vec::new(); diff --git a/crates/task/src/static_source.rs b/crates/task/src/static_source.rs index 0e7a021b0685f0bf1003e0cfb99cc8d6763cfc84..9e4051ef9721f4f4ce3b6bbebd38535e4ed8e27b 100644 --- a/crates/task/src/static_source.rs +++ b/crates/task/src/static_source.rs @@ -75,7 +75,6 @@ impl<T: PartialEq + 'static + Sync> TrackedFile<T> { { let parsed_contents: Arc<RwLock<T>> = Arc::default(); cx.background_spawn({ - let parsed_contents = parsed_contents.clone(); async move { while let Some(new_contents) = tracker.next().await { if Arc::strong_count(&parsed_contents) == 1 { diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index aae28ab874544f683bf48f873d4a9a80a529a32b..85e654eff424cc349d70372a1480eed83eec4032 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -100,7 +100,7 @@ impl SpawnInTerminal { command: proto.command.clone(), args: proto.args.clone(), env: proto.env.into_iter().collect(), - cwd: proto.cwd.map(PathBuf::from).clone(), + cwd: proto.cwd.map(PathBuf::from), ..Default::default() } } diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 24e11d771546dc3f4c15310af2f37e1c6ac4d824..3d1d180557fc457e4200a5b246f2a08e2f5dfcf0 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -183,6 +183,10 @@ impl TaskTemplate { &mut substituted_variables, )? } else { + #[allow( + clippy::redundant_clone, + reason = "We want to clone the full_label to avoid borrowing it in the fold closure" + )] full_label.clone() } .lines() @@ -453,7 +457,7 @@ mod tests { TaskTemplate { label: "".to_string(), command: "".to_string(), - ..task_with_all_properties.clone() + ..task_with_all_properties }, ] { assert_eq!( @@ -521,7 +525,7 @@ mod tests { ); let cx = TaskContext { - cwd: Some(context_cwd.clone()), + cwd: Some(context_cwd), task_variables: TaskVariables::default(), project_env: HashMap::default(), }; @@ -768,7 +772,7 @@ mod tests { "test_env_key".to_string(), format!("test_env_var_{}", VariableName::Symbol.template_value()), )]), - ..task_with_all_properties.clone() + ..task_with_all_properties }, ] .into_iter() @@ -871,7 +875,7 @@ mod tests { let context = TaskContext { cwd: None, - task_variables: TaskVariables::from_iter(all_variables.clone()), + task_variables: TaskVariables::from_iter(all_variables), project_env, }; diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index a4fdc24e177d9bebba1106c7df865f3f621b6c10..3f3a4cc11660633df75c0dd896b19910cc06d680 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -434,7 +434,7 @@ mod tests { ) .await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let worktree_store = project.read_with(cx, |project, _| project.worktree_store().clone()); + let worktree_store = project.read_with(cx, |project, _| project.worktree_store()); let rust_language = Arc::new( Language::new( LanguageConfig::default(), diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 16c1efabbabeb3741282e029cad75bda9a0fb5aa..b38a69f095049c80388d3c0ec2ab397fb4d2bec4 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -486,7 +486,7 @@ impl TerminalBuilder { //And connect them together let event_loop = EventLoop::new( term.clone(), - ZedListener(events_tx.clone()), + ZedListener(events_tx), pty, pty_options.drain_on_exit, false, @@ -1661,7 +1661,7 @@ impl Terminal { #[cfg(any(target_os = "linux", target_os = "freebsd"))] MouseButton::Middle => { if let Some(item) = _cx.read_from_primary() { - let text = item.text().unwrap_or_default().to_string(); + let text = item.text().unwrap_or_default(); self.input(text.into_bytes()); } } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 1c38dbc877c9da50f92d8edcf4616a6bc32a21ad..c2fbeb7ee6033f6bdf72f1ec1f5e380cfd39e2d5 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -653,7 +653,7 @@ impl TerminalElement { let terminal = self.terminal.clone(); let hitbox = hitbox.clone(); let focus = focus.clone(); - let terminal_view = terminal_view.clone(); + let terminal_view = terminal_view; move |e: &MouseMoveEvent, phase, window, cx| { if phase != DispatchPhase::Bubble { return; @@ -1838,8 +1838,7 @@ mod tests { }; let font_size = AbsoluteLength::Pixels(px(12.0)); - let batch = - BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style1.clone(), font_size); + let batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style1, font_size); // Should be able to append same style assert!(batch.can_append(&style2)); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 1d76f701520fceaaa2aa1ed88924be31c7a33e40..f40c4870f12f87e3e031ebdb66cd79d3c536589e 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -181,7 +181,6 @@ impl TerminalPanel { .anchor(Corner::TopRight) .with_handle(pane.split_item_context_menu_handle.clone()) .menu({ - let split_context = split_context.clone(); move |window, cx| { ContextMenu::build(window, cx, |menu, _, _| { menu.when_some( diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e02324a14241a89ad9004b6fcff0644c3099d945..c54010b4b0c6fe56168c3e15271d3423921b15da 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -257,9 +257,9 @@ pub fn refine_theme_family(theme_family_content: ThemeFamilyContent) -> ThemeFam let author = theme_family_content.author.clone(); let mut theme_family = ThemeFamily { - id: id.clone(), - name: name.clone().into(), - author: author.clone().into(), + id, + name: name.into(), + author: author.into(), themes: vec![], scales: default_color_scales(), }; diff --git a/crates/theme_importer/src/vscode/converter.rs b/crates/theme_importer/src/vscode/converter.rs index 0249bdc7c94a5008240bde25153203c10d247a82..b3b846d91d5ac3a7e3d88983787d23fe9f0adece 100644 --- a/crates/theme_importer/src/vscode/converter.rs +++ b/crates/theme_importer/src/vscode/converter.rs @@ -158,7 +158,7 @@ impl VsCodeThemeConverter { .tab .active_background .clone() - .or(vscode_tab_inactive_background.clone()), + .or(vscode_tab_inactive_background), search_match_background: vscode_colors.editor.find_match_background.clone(), panel_background: vscode_colors.panel.background.clone(), pane_group_border: vscode_colors.editor_group.border.clone(), @@ -171,22 +171,20 @@ impl VsCodeThemeConverter { .scrollbar_slider .active_background .clone(), - scrollbar_thumb_border: vscode_scrollbar_slider_background.clone(), + scrollbar_thumb_border: vscode_scrollbar_slider_background, scrollbar_track_background: vscode_editor_background.clone(), scrollbar_track_border: vscode_colors.editor_overview_ruler.border.clone(), minimap_thumb_background: vscode_colors.minimap_slider.background.clone(), minimap_thumb_hover_background: vscode_colors.minimap_slider.hover_background.clone(), minimap_thumb_active_background: vscode_colors.minimap_slider.active_background.clone(), - editor_foreground: vscode_editor_foreground - .clone() - .or(vscode_token_colors_foreground.clone()), + editor_foreground: vscode_editor_foreground.or(vscode_token_colors_foreground), editor_background: vscode_editor_background.clone(), - editor_gutter_background: vscode_editor_background.clone(), + editor_gutter_background: vscode_editor_background, editor_active_line_background: vscode_colors.editor.line_highlight_background.clone(), editor_line_number: vscode_colors.editor_line_number.foreground.clone(), editor_active_line_number: vscode_colors.editor.foreground.clone(), editor_wrap_guide: vscode_panel_border.clone(), - editor_active_wrap_guide: vscode_panel_border.clone(), + editor_active_wrap_guide: vscode_panel_border, editor_document_highlight_bracket_background: vscode_colors .editor_bracket_match .background diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index d8b0b8dc6bb62b7547dcf945a73dc45bed9a86fc..4a8cac2435317f02e4aed31105cf3126a9c89e70 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -186,7 +186,7 @@ impl ApplicationMenu { .trigger( Button::new( SharedString::from(format!("{}-menu-trigger", menu_name)), - menu_name.clone(), + menu_name, ) .style(ButtonStyle::Subtle) .label_size(LabelSize::Small), diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index 5be68afeb4c09b4d6536626c255b4918ecc66c01..c667edb509b6e7c5f906f038f19a1e50b5c65032 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -155,7 +155,7 @@ impl TitleBar { .gap_1() .overflow_x_scroll() .when_some( - current_user.clone().zip(client.peer_id()).zip(room.clone()), + current_user.zip(client.peer_id()).zip(room), |this, ((current_user, peer_id), room)| { let player_colors = cx.theme().players(); let room = room.read(cx); diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 5bd6a17e4b86657097c221705c3c48966e09fa41..35b33f39bed0cc528f4bfcb88259d37e550fa7ad 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -305,7 +305,6 @@ impl TitleBar { let nickname = options .nickname - .clone() .map(|nick| nick.into()) .unwrap_or_else(|| host.clone()); @@ -351,11 +350,7 @@ impl TitleBar { .indicator_border_color(Some(cx.theme().colors().title_bar_background)) .into_any_element(), ) - .child( - Label::new(nickname.clone()) - .size(LabelSize::Small) - .truncate(), - ), + .child(Label::new(nickname).size(LabelSize::Small).truncate()), ) .tooltip(move |window, cx| { Tooltip::with_meta( diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs index cdd3db99e0679a30bbdf679459ba26b76f62d383..feeca8cf52a5116d53562826da72a0bb304d16ce 100644 --- a/crates/toolchain_selector/src/toolchain_selector.rs +++ b/crates/toolchain_selector/src/toolchain_selector.rs @@ -167,7 +167,6 @@ impl ToolchainSelectorDelegate { cx: &mut Context<Picker<Self>>, ) -> Self { let _fetch_candidates_task = cx.spawn_in(window, { - let project = project.clone(); async move |this, cx| { let term = project .read_with(cx, |this, _| { diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs index 7ad9400f0d0944a02292813f75c28fb1fcf9d78b..f276d483a6fb391ac0a56f6c4322fc7c8dad221f 100644 --- a/crates/ui/src/components/dropdown_menu.rs +++ b/crates/ui/src/components/dropdown_menu.rs @@ -96,7 +96,7 @@ impl RenderOnce for DropdownMenu { .style(self.style), ) .attach(Corner::BottomLeft) - .when_some(self.handle.clone(), |el, handle| el.with_handle(handle)) + .when_some(self.handle, |el, handle| el.with_handle(handle)) } } @@ -169,7 +169,7 @@ impl Component for DropdownMenu { "States", vec![single_example( "Disabled", - DropdownMenu::new("disabled", "Disabled Dropdown", menu.clone()) + DropdownMenu::new("disabled", "Disabled Dropdown", menu) .disabled(true) .into_any_element(), )], diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs index 5e6f4ee8ba18569a43365830eeb7ed12cbb501cc..60aa23b44c2515b9ee4daa7c6336c30c96a6a9ee 100644 --- a/crates/ui/src/components/indent_guides.rs +++ b/crates/ui/src/components/indent_guides.rs @@ -195,7 +195,7 @@ mod uniform_list { impl UniformListDecoration for IndentGuides { fn compute( &self, - visible_range: Range<usize>, + mut visible_range: Range<usize>, bounds: Bounds<Pixels>, _scroll_offset: Point<Pixels>, item_height: Pixels, @@ -203,7 +203,6 @@ mod uniform_list { window: &mut Window, cx: &mut App, ) -> AnyElement { - let mut visible_range = visible_range.clone(); let includes_trailing_indent = visible_range.end < item_count; // Check if we have entries after the visible range, // if so extend the visible range so we can fetch a trailing indent, diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index bbce6101f469940259ab7046ad1c50a2a23217ac..1e7bb40c400e053788862544287474dfe075758f 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -325,7 +325,7 @@ impl RenderOnce for Key { .text_size(size) .line_height(relative(1.)) .text_color(self.color.unwrap_or(Color::Muted).color(cx)) - .child(self.key.clone()) + .child(self.key) } } diff --git a/crates/ui/src/components/keybinding_hint.rs b/crates/ui/src/components/keybinding_hint.rs index a34ca40ed8c413d2edd6278dd035b93329dc5339..d7491b27b1fdf22dd2466768d81e9a63c343b705 100644 --- a/crates/ui/src/components/keybinding_hint.rs +++ b/crates/ui/src/components/keybinding_hint.rs @@ -269,7 +269,7 @@ impl Component for KeybindingHint { ), single_example( "Large", - KeybindingHint::new(enter.clone(), bg_color) + KeybindingHint::new(enter, bg_color) .size(Pixels::from(20.0)) .prefix("Large:") .suffix("Size") diff --git a/crates/ui/src/components/notification/alert_modal.rs b/crates/ui/src/components/notification/alert_modal.rs index acba0c7e9a4e69fa21a68b081531ef16d995bac9..9990dc1ce5f13e6834a009c4b8d7c14b594ccf36 100644 --- a/crates/ui/src/components/notification/alert_modal.rs +++ b/crates/ui/src/components/notification/alert_modal.rs @@ -64,7 +64,7 @@ impl RenderOnce for AlertModal { ) .child(Button::new( self.primary_action.clone(), - self.primary_action.clone(), + self.primary_action, )), ), ) diff --git a/crates/ui/src/components/sticky_items.rs b/crates/ui/src/components/sticky_items.rs index c3e0886404c08ca555185be811083b6a5b2952f5..bf64622b29a423654c2d89b5773dd30792e04785 100644 --- a/crates/ui/src/components/sticky_items.rs +++ b/crates/ui/src/components/sticky_items.rs @@ -28,7 +28,7 @@ where T: StickyCandidate + Clone + 'static, { let entity_compute = entity.clone(); - let entity_render = entity.clone(); + let entity_render = entity; let compute_fn = Rc::new( move |range: Range<usize>, window: &mut Window, cx: &mut App| -> SmallVec<[T; 8]> { diff --git a/crates/ui/src/utils/format_distance.rs b/crates/ui/src/utils/format_distance.rs index a8f27f01dad22ab59481ef75c82ee07d736237d9..6ec497edee2a971e59faa1f6e99d589b9b27d864 100644 --- a/crates/ui/src/utils/format_distance.rs +++ b/crates/ui/src/utils/format_distance.rs @@ -159,7 +159,6 @@ fn distance_string( } else { format!("about {} hours", hours) } - .to_string() } else if distance < 172_800 { "1 day".to_string() } else if distance < 2_592_000 { @@ -206,21 +205,16 @@ fn distance_string( } else { format!("about {} years", years) } - .to_string() } else if remaining_months < 9 { if hide_prefix { format!("{} years", years) } else { format!("over {} years", years) } - .to_string() + } else if hide_prefix { + format!("{} years", years + 1) } else { - if hide_prefix { - format!("{} years", years + 1) - } else { - format!("almost {} years", years + 1) - } - .to_string() + format!("almost {} years", years + 1) } }; diff --git a/crates/ui_input/src/ui_input.rs b/crates/ui_input/src/ui_input.rs index 1a5bebaf1e952a02106bb05a2ec54055d361cb38..02f8ef89f3cb76d8ebb8f2468d9619c931ab9b9d 100644 --- a/crates/ui_input/src/ui_input.rs +++ b/crates/ui_input/src/ui_input.rs @@ -202,11 +202,11 @@ impl Component for SingleLineInput { .children(vec![example_group(vec![ single_example( "Small Label (Default)", - div().child(input_small.clone()).into_any_element(), + div().child(input_small).into_any_element(), ), single_example( "Regular Label", - div().child(input_regular.clone()).into_any_element(), + div().child(input_regular).into_any_element(), ), ])]) .into_any_element(), diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 7269fc8bec434cfcba434d8e36d21e237254bfa1..680c87f9e56e71b85e494de1f36c8cb7b88e4d9b 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1648,7 +1648,7 @@ impl OnMatchingLines { }); window.dispatch_action(action, cx); cx.defer_in(window, move |editor, window, cx| { - let newest = editor.selections.newest::<Point>(cx).clone(); + let newest = editor.selections.newest::<Point>(cx); editor.change_selections( SelectionEffects::no_scroll(), window, diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 714b74f239cd26dadf6dc70448b2022781ef398d..da2591934284cb29628d8e0c9d225fa1ff473c7d 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -74,11 +74,7 @@ impl ModeIndicator { .map(|count| format!("{}", count)), ) .chain(vim.selected_register.map(|reg| format!("\"{reg}"))) - .chain( - vim.operator_stack - .iter() - .map(|item| item.status().to_string()), - ) + .chain(vim.operator_stack.iter().map(|item| item.status())) .chain( cx.global::<VimGlobals>() .post_count diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 350ffd666b2486cadc72d6fbedf198dbde61cf95..a2f165e9fef2c99e13b277bf92786873ee79a649 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -719,21 +719,14 @@ impl Vim { target: Some(SurroundsType::Motion(motion)), }); } else { - self.normal_motion( - motion.clone(), - active_operator.clone(), - count, - forced_motion, - window, - cx, - ) + self.normal_motion(motion, active_operator, count, forced_motion, window, cx) } } Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - self.visual_motion(motion.clone(), count, window, cx) + self.visual_motion(motion, count, window, cx) } - Mode::HelixNormal => self.helix_normal_motion(motion.clone(), count, window, cx), + Mode::HelixNormal => self.helix_normal_motion(motion, count, window, cx), } self.clear_operator(window, cx); if let Some(operator) = waiting_operator { @@ -1327,7 +1320,7 @@ impl Motion { pub fn range( &self, map: &DisplaySnapshot, - selection: Selection<DisplayPoint>, + mut selection: Selection<DisplayPoint>, times: Option<usize>, text_layout_details: &TextLayoutDetails, forced_motion: bool, @@ -1372,7 +1365,6 @@ impl Motion { (None, true) => Some((selection.head(), selection.goal)), }?; - let mut selection = selection.clone(); selection.set_head(new_head, goal); let mut kind = match (self.default_kind(), forced_motion) { @@ -2401,9 +2393,7 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint let line_range = map.prev_line_boundary(point).0..line_end; let visible_line_range = line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1)); - let ranges = map - .buffer_snapshot - .bracket_ranges(visible_line_range.clone()); + let ranges = map.buffer_snapshot.bracket_ranges(visible_line_range); if let Some(ranges) = ranges { let line_range = line_range.start.to_offset(&map.buffer_snapshot) ..line_range.end.to_offset(&map.buffer_snapshot); diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 0fd17f310e630c1909668319d57921adcf96f492..933b119d3794540681cc096b0fe22b7f70da00cb 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -474,8 +474,7 @@ mod test { Mode::Normal, ); assert_eq!( - cx.read_from_clipboard() - .map(|item| item.text().unwrap().to_string()), + cx.read_from_clipboard().map(|item| item.text().unwrap()), Some("jumps".into()) ); cx.simulate_keystrokes("d d p"); @@ -487,8 +486,7 @@ mod test { Mode::Normal, ); assert_eq!( - cx.read_from_clipboard() - .map(|item| item.text().unwrap().to_string()), + cx.read_from_clipboard().map(|item| item.text().unwrap()), Some("jumps".into()) ); cx.write_to_clipboard(ClipboardItem::new_string("test-copy".to_string())); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index c65da4f90babc04d2ebe733848f2b94e0114ca9e..693de9f6971ff02639eee33e29e22e14902a7d37 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -187,9 +187,7 @@ fn find_mini_delimiters( }; // Try to find delimiters in visible range first - let ranges = map - .buffer_snapshot - .bracket_ranges(visible_line_range.clone()); + let ranges = map.buffer_snapshot.bracket_ranges(visible_line_range); if let Some(candidate) = cover_or_next(ranges, display_point, map, Some(&bracket_filter)) { return Some( DelimiterRange { diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 23efd3913907d3d15806911390b51b6705c7e3b3..c0176cb12c34ac0d58504edde1508bbfd04c6be8 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -400,7 +400,7 @@ impl MarksState { } else { HashMap::default() }; - let old_points = self.serialized_marks.get(&path.clone()); + let old_points = self.serialized_marks.get(&path); if old_points == Some(&new_points) { return; } @@ -543,7 +543,7 @@ impl MarksState { .insert(name.clone(), anchors); if self.is_global_mark(&name) { self.global_marks - .insert(name.clone(), MarkLocation::Buffer(multibuffer.entity_id())); + .insert(name, MarkLocation::Buffer(multibuffer.entity_id())); } if let Some(buffer) = buffer { let buffer_id = buffer.read(cx).remote_id(); @@ -559,7 +559,7 @@ impl MarksState { let buffer_id = buffer.read(cx).remote_id(); self.buffer_marks.entry(buffer_id).or_default().insert( - name.clone(), + name, anchors .into_iter() .map(|anchor| anchor.text_anchor) @@ -654,9 +654,9 @@ impl MarksState { return; } }; - self.global_marks.remove(&mark_name.clone()); + self.global_marks.remove(&mark_name); self.serialized_marks - .get_mut(&path.clone()) + .get_mut(&path) .map(|m| m.remove(&mark_name.clone())); if let Some(workspace_id) = self.workspace_id(cx) { cx.background_spawn(async move { DB.delete_mark(workspace_id, path, mark_name).await }) @@ -1282,7 +1282,7 @@ impl RegistersView { if let Some(register) = register { matches.push(RegisterMatch { name: '%', - contents: register.text.clone(), + contents: register.text, }) } } @@ -1374,7 +1374,7 @@ impl PickerDelegate for MarksViewDelegate { _: &mut Window, cx: &mut Context<Picker<Self>>, ) -> gpui::Task<()> { - let Some(workspace) = self.workspace.upgrade().clone() else { + let Some(workspace) = self.workspace.upgrade() else { return Task::ready(()); }; cx.spawn(async move |picker, cx| { diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 505cdaa9100fcfad0d96bc6a55afe6ff4dc945ea..6c9df164e0fe880c81960a412519347aff5959bd 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -292,12 +292,7 @@ impl NeovimBackedTestContext { register: '"', state: self.shared_state().await, neovim: self.neovim.read_register('"').await, - editor: self - .read_from_clipboard() - .unwrap() - .text() - .unwrap() - .to_owned(), + editor: self.read_from_clipboard().unwrap().text().unwrap(), } } diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index f87ccc283fb9173ff4f283f21e409de25fe6ad11..13b3e8b58d7198f4b92ccdcd6f8673745f1f482b 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -453,7 +453,7 @@ impl NeovimConnection { }; if self.data.back() != Some(&state) { - self.data.push_back(state.clone()); + self.data.push_back(state); } (mode, ranges) diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 5b6cb55e8c1a96db7c82510cecc4daf5cd8d000d..e7ac692df14cb482656d930efa2313e85c27a4bc 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -225,7 +225,7 @@ impl VimTestContext { VimClipboard { editor: self .read_from_clipboard() - .map(|item| item.text().unwrap().to_string()) + .map(|item| item.text().unwrap()) .unwrap_or_default(), } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 11d6d89bac6722fb577310af856de9f327705dd2..9da01e6f444d2284814282f9bf6eecfb0814953d 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1693,7 +1693,7 @@ impl Vim { }) { editor.do_paste( ®ister.text.to_string(), - register.clipboard_selections.clone(), + register.clipboard_selections, false, window, cx, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index ffbae3ff76dbf31d504f5a6df9063ced0d3a8e6e..fcce00f0c0ffee43f1b7980fcf9fe3a70f6e7794 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1203,7 +1203,7 @@ mod test { the lazy dog"}); assert_eq!( cx.read_from_clipboard() - .map(|item| item.text().unwrap().to_string()) + .map(|item| item.text().unwrap()) .unwrap(), "The q" ); diff --git a/crates/watch/src/watch.rs b/crates/watch/src/watch.rs index c0741e4a204950ec4531244e0750b0dc91431b08..f0ed5b4a186b8b524e5d2038b14ff92372374c4e 100644 --- a/crates/watch/src/watch.rs +++ b/crates/watch/src/watch.rs @@ -218,7 +218,7 @@ mod tests { let mut tasks = Vec::new(); tasks.push(cx.background_spawn({ - let executor = cx.executor().clone(); + let executor = cx.executor(); let next_id = next_id.clone(); let closed = closed.clone(); async move { diff --git a/crates/web_search/src/web_search.rs b/crates/web_search/src/web_search.rs index 8578cfe4aaab77fdc731a8dd49c62c5afd514600..c381b91f3941e3852e59769006fa9728502f94c6 100644 --- a/crates/web_search/src/web_search.rs +++ b/crates/web_search/src/web_search.rs @@ -57,7 +57,7 @@ impl WebSearchRegistry { ) { let id = provider.id(); let provider = Arc::new(provider); - self.providers.insert(id.clone(), provider.clone()); + self.providers.insert(id, provider.clone()); if self.active_provider.is_none() { self.active_provider = Some(provider); } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 1d9170684e0261249c3f97567d2b14a8bb9cd487..7a8de6e91040e8949a47668da81f063cb3c0c082 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -171,7 +171,7 @@ where } fn panel_focus_handle(&self, cx: &App) -> FocusHandle { - self.read(cx).focus_handle(cx).clone() + self.read(cx).focus_handle(cx) } fn activation_priority(&self, cx: &App) -> u32 { @@ -340,7 +340,7 @@ impl Dock { pub fn panel<T: Panel>(&self) -> Option<Entity<T>> { self.panel_entries .iter() - .find_map(|entry| entry.panel.to_any().clone().downcast().ok()) + .find_map(|entry| entry.panel.to_any().downcast().ok()) } pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> { diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 8af39be3e74b3a01d198e0929cf9a5034af095b2..039aec51990cda02e99996e9a4029aa42613f492 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -1012,7 +1012,6 @@ where let message: SharedString = format!("Error: {err}").into(); log::error!("Showing error notification in app: {message}"); show_app_notification(workspace_error_notification_id(), cx, { - let message = message.clone(); move |cx| { cx.new({ let message = message.clone(); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index d42b59f08e98618fb5070ed61b33f7f0a9db32d2..e49eb0a34559257c8cc7074370195ed6daca0b6b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -480,7 +480,7 @@ impl Pane { forward_stack: Default::default(), closed_stack: Default::default(), paths_by_item: Default::default(), - pane: handle.clone(), + pane: handle, next_timestamp, }))), toolbar: cx.new(|_| Toolbar::new()), @@ -2516,7 +2516,7 @@ impl Pane { this.handle_external_paths_drop(paths, window, cx) })) .when_some(item.tab_tooltip_content(cx), |tab, content| match content { - TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())), + TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text)), TabTooltipContent::Custom(element_fn) => { tab.tooltip(move |window, cx| element_fn(window, cx)) } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index bd2aafb7f4994cbd44ae5cb4cb737d07c1b205c8..9c2d09fd26308b95ab145b557a516b5e6603a0e4 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1175,7 +1175,7 @@ mod element { bounding_boxes.clear(); let mut layout = PaneAxisLayout { - dragged_handle: dragged_handle.clone(), + dragged_handle, children: Vec::new(), }; for (ix, mut child) in mem::take(&mut self.children).into_iter().enumerate() { diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 4a6b9ccdf4592a7324f244429a4739bd457fcfc1..da8a3070fca1b4dbe0a82920ca91232774c8ed8a 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -620,7 +620,7 @@ mod tests { ]); let order = vec![2, 0, 1]; let serialized = - SerializedWorkspaceLocation::Local(LocalPaths(paths.clone()), LocalPathsOrder(order)); + SerializedWorkspaceLocation::Local(LocalPaths(paths), LocalPathsOrder(order)); assert_eq!( serialized.sorted_paths(), Arc::new(vec![ diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index e89e949f161ebea85daeb7cdb3b228afb57d3e1e..b21ba7a4b1a2ec7cc80521e91b4e5935333615f5 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -371,13 +371,13 @@ impl<T: SearchableItem> SearchableItemHandle for Entity<T> { impl From<Box<dyn SearchableItemHandle>> for AnyView { fn from(this: Box<dyn SearchableItemHandle>) -> Self { - this.to_any().clone() + this.to_any() } } impl From<&Box<dyn SearchableItemHandle>> for AnyView { fn from(this: &Box<dyn SearchableItemHandle>) -> Self { - this.to_any().clone() + this.to_any() } } diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index edeb382de7d386b37d81b2649af85cf97f9e8b31..187e720d9c01fb471d91781435912b6502e2a0a9 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -108,7 +108,7 @@ impl StatusBar { self.left_items .iter() .chain(self.right_items.iter()) - .find_map(|item| item.to_any().clone().downcast().log_err()) + .find_map(|item| item.to_any().downcast().log_err()) } pub fn position_of_item<T>(&self) -> Option<usize> @@ -217,6 +217,6 @@ impl<T: StatusItemView> StatusItemViewHandle for Entity<T> { impl From<&dyn StatusItemViewHandle> for AnyView { fn from(val: &dyn StatusItemViewHandle) -> Self { - val.to_any().clone() + val.to_any() } } diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index 03164e0a647e01d8805b1186ccddf72acd96da99..09a5415ca063d0aab2b2fab97abff3533e113b0b 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -303,7 +303,6 @@ impl ThemePreview { .gap_1() .children(all_colors.into_iter().map(|(color, name)| { let id = ElementId::Name(format!("{:?}-preview", color).into()); - let name = name.clone(); div().size_8().flex_none().child( ButtonLike::new(id) .child( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d64a4472a072739e2bfb7d0a4159ca443f9edbce..64cf77a4fde5690de05fc7a38243e6ab1bafe46e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -903,7 +903,7 @@ impl AppState { let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let clock = Arc::new(clock::FakeSystemClock::new()); let http_client = http_client::FakeHttpClient::with_404_response(); - let client = Client::new(clock, http_client.clone(), cx); + let client = Client::new(clock, http_client, cx); let session = cx.new(|cx| AppSession::new(Session::test(), cx)); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx)); @@ -1323,7 +1323,6 @@ impl Workspace { let mut active_call = None; if let Some(call) = ActiveCall::try_global(cx) { - let call = call.clone(); let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)]; active_call = Some((call, subscriptions)); } @@ -4116,7 +4115,6 @@ impl Workspace { .unwrap_or_else(|| { self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx) }) - .clone() } pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> { @@ -6713,7 +6711,7 @@ impl WorkspaceStore { .update(cx, |workspace, window, cx| { let handler_response = workspace.handle_follow(follower.project_id, window, cx); - if let Some(active_view) = handler_response.active_view.clone() + if let Some(active_view) = handler_response.active_view && workspace.project.read(cx).remote_id() == follower.project_id { response.active_view = Some(active_view) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index b12fd137677d886f8bcf58c2e32c4dc3c46db7b5..cf61ee2669663f172fa1238db4d359970e23e4bc 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1968,7 +1968,7 @@ impl LocalWorktree { cx: &Context<Worktree>, ) -> Option<Task<Result<()>>> { let path = self.entry_for_id(entry_id).unwrap().path.clone(); - let mut rx = self.add_path_prefix_to_scan(path.clone()); + let mut rx = self.add_path_prefix_to_scan(path); Some(cx.background_spawn(async move { rx.next().await; Ok(()) @@ -3952,7 +3952,7 @@ impl BackgroundScanner { .iter() .map(|path| { if path.file_name().is_some() { - root_canonical_path.as_path().join(path).to_path_buf() + root_canonical_path.as_path().join(path) } else { root_canonical_path.as_path().to_path_buf() } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index ca9debb64784b34fc1b5c49befc69d061f5b2c9f..c46e14f077e6f4f527b1fcc616a6560cf9654b18 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -1254,7 +1254,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { let snapshot = Arc::new(Mutex::new(tree.snapshot())); tree.observe_updates(0, cx, { let snapshot = snapshot.clone(); - let settings = tree.settings().clone(); + let settings = tree.settings(); move |update| { snapshot .lock() diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 851c4e79f116b55d1f7c220e973add761a2eddd5..45c67153eb428c0421416251717d191d44cc3cf2 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -242,7 +242,7 @@ pub fn main() { if args.system_specs { let system_specs = feedback::system_specs::SystemSpecs::new_stateless( app_version, - app_commit_sha.clone(), + app_commit_sha, *release_channel::RELEASE_CHANNEL, ); println!("Zed System Specs (from CLI):\n{}", system_specs); @@ -367,7 +367,7 @@ pub fn main() { if let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade()) { cx.spawn({ - let app_state = app_state.clone(); + let app_state = app_state; async move |cx| { if let Err(e) = restore_or_create_workspace(app_state, cx).await { fail_to_open_window_async(e, cx) @@ -523,13 +523,13 @@ pub fn main() { let app_session = cx.new(|cx| AppSession::new(session, cx)); let app_state = Arc::new(AppState { - languages: languages.clone(), + languages, client: client.clone(), - user_store: user_store.clone(), + user_store, fs: fs.clone(), build_window_options, workspace_store, - node_runtime: node_runtime.clone(), + node_runtime, session: app_session, }); AppState::set_global(Arc::downgrade(&app_state), cx); @@ -751,7 +751,6 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut if let Some(kind) = request.kind { match kind { OpenRequestKind::CliConnection(connection) => { - let app_state = app_state.clone(); cx.spawn(async move |cx| handle_cli_connection(connection, app_state, cx).await) .detach(); } @@ -1313,7 +1312,6 @@ fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &App) { .path_to_extension_icon_theme(icon_theme_name) { cx.spawn({ - let theme_registry = theme_registry.clone(); let fs = fs.clone(); async move |cx| { theme_registry @@ -1335,9 +1333,7 @@ fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut App) { cx.spawn({ let fs = fs.clone(); async move |cx| { - if let Some(theme_registry) = - cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err() - { + if let Some(theme_registry) = cx.update(|cx| ThemeRegistry::global(cx)).log_err() { let themes_dir = paths::themes_dir().as_ref(); match fs .metadata(themes_dir) @@ -1376,7 +1372,7 @@ fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut App) { for event in paths { if fs.metadata(&event.path).await.ok().flatten().is_some() && let Some(theme_registry) = - cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err() + cx.update(|cx| ThemeRegistry::global(cx)).log_err() && let Some(()) = theme_registry .load_user_theme(&event.path, fs.clone()) .await diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 232dfc42a3bab5cce78802d16f0440a2d03f2733..0972973b8957e5ad5f605619a805fa2db92be04a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -526,8 +526,6 @@ fn initialize_panels( window: &mut Window, cx: &mut Context<Workspace>, ) { - let prompt_builder = prompt_builder.clone(); - cx.spawn_in(window, async move |workspace_handle, cx| { let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone()); @@ -1394,7 +1392,7 @@ fn show_keymap_file_load_error( cx: &mut App, ) { show_markdown_app_notification( - notification_id.clone(), + notification_id, error_message, "Open Keymap File".into(), |window, cx| { @@ -4786,7 +4784,7 @@ mod tests { cx.background_executor.run_until_parked(); // 5. Critical: Verify .zed is actually excluded from worktree - let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().clone()); + let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap()); let has_zed_entry = cx.update(|cx| worktree.read(cx).entry_for_path(".zed").is_some()); @@ -4822,7 +4820,7 @@ mod tests { .await .unwrap(); - let new_content_str = new_content.clone(); + let new_content_str = new_content; eprintln!("New settings content: {}", new_content_str); // The bug causes the settings to be overwritten with empty settings diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index d855fc3af799016bc634b4c013eb7302c6300ab9..5b3a951d43aaaf9b79bb33aef6308f45c333dca4 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -33,8 +33,6 @@ use workspace::{ pub fn init(app_state: Arc<AppState>, cx: &mut App) { workspace::register_serializable_item::<ComponentPreview>(cx); - let app_state = app_state.clone(); - cx.observe_new(move |workspace: &mut Workspace, _window, cx| { let app_state = app_state.clone(); let project = workspace.project().clone(); @@ -462,12 +460,12 @@ impl ComponentPreview { Vec::new() }; if valid_positions.is_empty() { - Label::new(name.clone()).into_any_element() + Label::new(name).into_any_element() } else { - HighlightedLabel::new(name.clone(), valid_positions).into_any_element() + HighlightedLabel::new(name, valid_positions).into_any_element() } } else { - Label::new(name.clone()).into_any_element() + Label::new(name).into_any_element() }) .selectable(true) .toggle_state(selected) @@ -685,7 +683,7 @@ impl ComponentPreview { .h_full() .py_8() .bg(cx.theme().colors().panel_background) - .children(self.active_thread.clone().map(|thread| thread.clone())) + .children(self.active_thread.clone()) .when_none(&self.active_thread.clone(), |this| { this.child("No active thread") }), @@ -716,7 +714,7 @@ impl Render for ComponentPreview { if input.is_empty(cx) { String::new() } else { - input.editor().read(cx).text(cx).to_string() + input.editor().read(cx).text(cx) } }); @@ -929,7 +927,7 @@ impl SerializableItem for ComponentPreview { Err(_) => ActivePageId::default(), }; - let user_store = project.read(cx).user_store().clone(); + let user_store = project.read(cx).user_store(); let language_registry = project.read(cx).languages().clone(); let preview_page = if deserialized_active_page.0 == ActivePageId::default().0 { Some(PreviewPage::default()) @@ -940,7 +938,7 @@ impl SerializableItem for ComponentPreview { let found_component = all_components.iter().find(|c| c.id().0 == component_str); if let Some(component) = found_component { - Some(PreviewPage::Component(component.id().clone())) + Some(PreviewPage::Component(component.id())) } else { Some(PreviewPage::default()) } @@ -1057,7 +1055,7 @@ impl ComponentPreviewPage { .rounded_sm() .bg(color.color(cx).alpha(0.12)) .child( - Label::new(status.clone().to_string()) + Label::new(status.to_string()) .size(LabelSize::Small) .color(color), ), diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index 1123e53ddd1f1a0affbd5de807886d4bbf4d81ea..a9abd9bc7409e76c7e4fa6e35668535a951496b4 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -60,23 +60,16 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) { cx.subscribe(&user_store, { let editors = editors.clone(); let client = client.clone(); + move |user_store, event, cx| { if let client::user::Event::PrivateUserInfoUpdated = event { - assign_edit_prediction_providers( - &editors, - provider, - &client, - user_store.clone(), - cx, - ); + assign_edit_prediction_providers(&editors, provider, &client, user_store, cx); } } }) .detach(); cx.observe_global::<SettingsStore>({ - let editors = editors.clone(); - let client = client.clone(); let user_store = user_store.clone(); move |cx| { let new_provider = all_language_settings(None, cx).edit_predictions.provider; diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 5baf76b64c6a4d60c2cc631c54fc0facddb02098..827c7754faaefa1a2baff2ab68d15b38d5c08fdc 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -102,11 +102,8 @@ impl OpenRequest { self.open_paths.is_empty(), "cannot open both local and ssh paths" ); - let mut connection_options = SshSettings::get_global(cx).connection_options_for( - host.clone(), - port, - username.clone(), - ); + let mut connection_options = + SshSettings::get_global(cx).connection_options_for(host, port, username); if let Some(password) = url.password() { connection_options.password = Some(password.to_string()); } diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 10d60fcd9d6e6ea4d2bfe133a18321cd6a960ab8..e57d5d3889b97290164eb4b26dd35f4cbc8b6721 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -161,7 +161,7 @@ impl Render for QuickActionBar { IconName::ZedAssistant, false, Box::new(InlineAssist::default()), - focus_handle.clone(), + focus_handle, "Inline Assist", move |_, window, cx| { window.dispatch_action(Box::new(InlineAssist::default()), cx); @@ -215,7 +215,7 @@ impl Render for QuickActionBar { ) }) .on_click({ - let focus = focus.clone(); + let focus = focus; move |_, window, cx| { focus.dispatch_action( &ToggleCodeActions { diff --git a/crates/zed/src/zed/quick_action_bar/repl_menu.rs b/crates/zed/src/zed/quick_action_bar/repl_menu.rs index ca180dccddf3c461b77154761f899dda6c4321fd..eaa989f88dc9e3e3e969841f02fa334a8f6f594e 100644 --- a/crates/zed/src/zed/quick_action_bar/repl_menu.rs +++ b/crates/zed/src/zed/quick_action_bar/repl_menu.rs @@ -196,7 +196,6 @@ impl QuickActionBar { .into_any_element() }, { - let editor = editor.clone(); move |window, cx| { repl::restart(editor.clone(), window, cx); } @@ -346,7 +345,7 @@ impl QuickActionBar { ), Tooltip::text("Select Kernel"), ) - .with_handle(menu_handle.clone()) + .with_handle(menu_handle) .into_any_element() } @@ -362,7 +361,7 @@ impl QuickActionBar { .shape(ui::IconButtonShape::Square) .icon_size(ui::IconSize::Small) .icon_color(Color::Muted) - .tooltip(Tooltip::text(tooltip.clone())) + .tooltip(Tooltip::text(tooltip)) .on_click(|_, _window, cx| { cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION)) }), diff --git a/crates/zeta/src/input_excerpt.rs b/crates/zeta/src/input_excerpt.rs index 8ca6d394075f7d497f6014de2947783167f29d2d..f4add6593e9a2b15679b5b0e6e660b4ce6a52f87 100644 --- a/crates/zeta/src/input_excerpt.rs +++ b/crates/zeta/src/input_excerpt.rs @@ -90,7 +90,7 @@ fn expand_range( range: Range<Point>, mut remaining_tokens: usize, ) -> Range<Point> { - let mut expanded_range = range.clone(); + let mut expanded_range = range; expanded_range.start.column = 0; expanded_range.end.column = snapshot.line_len(expanded_range.end.row); loop { diff --git a/crates/zeta_cli/src/headless.rs b/crates/zeta_cli/src/headless.rs index d6ee085d18be0941eb22d67994a7865fa7eefb56..cfa7d606bababb4509bdaa6fc4d1fbf4e0c9112d 100644 --- a/crates/zeta_cli/src/headless.rs +++ b/crates/zeta_cli/src/headless.rs @@ -107,11 +107,7 @@ pub fn init(cx: &mut App) -> ZetaCliAppState { language::init(cx); debug_adapter_extension::init(extension_host_proxy.clone(), cx); - language_extension::init( - LspAccess::Noop, - extension_host_proxy.clone(), - languages.clone(), - ); + language_extension::init(LspAccess::Noop, extension_host_proxy, languages.clone()); language_model::init(client.clone(), cx); language_models::init(user_store.clone(), client.clone(), cx); languages::init(languages.clone(), node_runtime.clone(), cx); diff --git a/crates/zlog/src/filter.rs b/crates/zlog/src/filter.rs index 36a77e37bda799efe5f2bff64d95ff92f84c3468..ee3c2410798b286795b9fd78b89502a2a7894987 100644 --- a/crates/zlog/src/filter.rs +++ b/crates/zlog/src/filter.rs @@ -293,7 +293,7 @@ impl ScopeMap { sub_items_start + 1, sub_items_end, "Expected one item: got: {:?}", - &items[items_range.clone()] + &items[items_range] ); enabled = Some(items[sub_items_start].1); } else { diff --git a/extensions/glsl/src/glsl.rs b/extensions/glsl/src/glsl.rs index 695fd7a05354991cc47740827ce86fdd1b612269..77865564cc1efead12327715aa77c5a0df2965af 100644 --- a/extensions/glsl/src/glsl.rs +++ b/extensions/glsl/src/glsl.rs @@ -119,7 +119,7 @@ impl zed::Extension for GlslExtension { ) -> Result<Option<serde_json::Value>> { let settings = LspSettings::for_worktree("glsl_analyzer", worktree) .ok() - .and_then(|lsp_settings| lsp_settings.settings.clone()) + .and_then(|lsp_settings| lsp_settings.settings) .unwrap_or_default(); Ok(Some(serde_json::json!({ diff --git a/extensions/html/src/html.rs b/extensions/html/src/html.rs index 07d4642ff404f2450edfcfcf5c52a6f2b373b897..371824c830ae7b0ef0e2d7391f6262825470aa88 100644 --- a/extensions/html/src/html.rs +++ b/extensions/html/src/html.rs @@ -94,7 +94,7 @@ impl zed::Extension for HtmlExtension { ) -> Result<Option<zed::serde_json::Value>> { let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) .ok() - .and_then(|lsp_settings| lsp_settings.settings.clone()) + .and_then(|lsp_settings| lsp_settings.settings) .unwrap_or_default(); Ok(Some(settings)) } diff --git a/extensions/ruff/src/ruff.rs b/extensions/ruff/src/ruff.rs index b918c52686c63b0bcc73be9a7cc508c91a76c3b0..cc3c3f65504390ed96248217cc2dfa6e2124081f 100644 --- a/extensions/ruff/src/ruff.rs +++ b/extensions/ruff/src/ruff.rs @@ -151,7 +151,7 @@ impl zed::Extension for RuffExtension { ) -> Result<Option<zed_extension_api::serde_json::Value>> { let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) .ok() - .and_then(|lsp_settings| lsp_settings.initialization_options.clone()) + .and_then(|lsp_settings| lsp_settings.initialization_options) .unwrap_or_default(); Ok(Some(settings)) } @@ -163,7 +163,7 @@ impl zed::Extension for RuffExtension { ) -> Result<Option<zed_extension_api::serde_json::Value>> { let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) .ok() - .and_then(|lsp_settings| lsp_settings.settings.clone()) + .and_then(|lsp_settings| lsp_settings.settings) .unwrap_or_default(); Ok(Some(settings)) } diff --git a/extensions/snippets/src/snippets.rs b/extensions/snippets/src/snippets.rs index b2d68b6e1a98a39c73ba746af5c452597ac59b82..05e1ebca38ddfa576795e6040ccd2b3dde20cc3e 100644 --- a/extensions/snippets/src/snippets.rs +++ b/extensions/snippets/src/snippets.rs @@ -113,7 +113,7 @@ impl zed::Extension for SnippetExtension { ) -> Result<Option<zed_extension_api::serde_json::Value>> { let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) .ok() - .and_then(|lsp_settings| lsp_settings.settings.clone()) + .and_then(|lsp_settings| lsp_settings.settings) .unwrap_or_else(|| { json!({ "max_completion_items": 20, From f80a0ba056a0e674ef4f5ff1fe0be096bc9787b1 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:26:45 +0300 Subject: [PATCH 183/744] Move clippy lints which aren't apart of the style category (#36579) Move lints which aren't apart of the style category. Motivation: They might get accidentally get reverted when we turn the style category on again and remove the manual lint enforcements. Release Notes: - N/A --- Cargo.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c3c70912792b123e89c0ee50fff6fd729fe93c71..c259a969126f7a742b9ebdbc31db9e2d8d614b5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -806,6 +806,9 @@ todo = "deny" # warning on this rule produces a lot of noise. single_range_in_vec_init = "allow" +redundant_clone = "warn" +declare_interior_mutable_const = "deny" + # These are all of the rules that currently have violations in the Zed # codebase. # @@ -840,12 +843,10 @@ match_like_matches_macro = "warn" module_inception = { level = "deny" } question_mark = { level = "deny" } single_match = "warn" -redundant_clone = "warn" redundant_closure = { level = "deny" } redundant_static_lifetimes = { level = "warn" } redundant_pattern_matching = "warn" redundant_field_names = "warn" -declare_interior_mutable_const = { level = "deny" } collapsible_if = { level = "warn"} collapsible_else_if = { level = "warn" } needless_borrow = { level = "warn"} From 4ee565cd392b0563206eb2d2e61be214fa57ba03 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra <me@as-cii.com> Date: Wed, 20 Aug 2025 14:03:20 +0200 Subject: [PATCH 184/744] Fix mentions roundtrip from/to database and other history bugs (#36575) Release Notes: - N/A --- crates/agent2/src/agent.rs | 170 +++++++++++++++++++++++++++++++++++- crates/agent2/src/thread.rs | 58 ++++++------ 2 files changed, 200 insertions(+), 28 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 5496ecea7b177b406af1745bf05de9a3eb110170..1fa307511fd35f55a9aef9cc82c59b1c8e8430b5 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -577,6 +577,10 @@ impl NativeAgent { } fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) { + if thread.read(cx).is_empty() { + return; + } + let database_future = ThreadsDatabase::connect(cx); let (id, db_thread) = thread.update(cx, |thread, cx| (thread.id().clone(), thread.to_db(cx))); @@ -989,12 +993,19 @@ impl acp_thread::AgentSessionResume for NativeAgentSessionResume { #[cfg(test)] mod tests { + use crate::HistoryEntryId; + use super::*; - use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo}; + use acp_thread::{ + AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo, MentionUri, + }; use fs::FakeFs; use gpui::TestAppContext; + use indoc::indoc; + use language_model::fake_provider::FakeLanguageModel; use serde_json::json; use settings::SettingsStore; + use util::path; #[gpui::test] async fn test_maintaining_project_context(cx: &mut TestAppContext) { @@ -1179,6 +1190,163 @@ mod tests { ); } + #[gpui::test] + #[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows + async fn test_save_load_thread(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/", + json!({ + "a": { + "b.md": "Lorem" + } + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await; + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let agent = NativeAgent::new( + project.clone(), + history_store.clone(), + Templates::new(), + None, + fs.clone(), + &mut cx.to_async(), + ) + .await + .unwrap(); + let connection = Rc::new(NativeAgentConnection(agent.clone())); + + let acp_thread = cx + .update(|cx| { + connection + .clone() + .new_thread(project.clone(), Path::new(""), cx) + }) + .await + .unwrap(); + let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone()); + let thread = agent.read_with(cx, |agent, _| { + agent.sessions.get(&session_id).unwrap().thread.clone() + }); + + // Ensure empty threads are not saved, even if they get mutated. + let model = Arc::new(FakeLanguageModel::default()); + let summary_model = Arc::new(FakeLanguageModel::default()); + thread.update(cx, |thread, cx| { + thread.set_model(model, cx); + thread.set_summarization_model(Some(summary_model), cx); + }); + cx.run_until_parked(); + assert_eq!(history_entries(&history_store, cx), vec![]); + + let model = thread.read_with(cx, |thread, _| thread.model().unwrap().clone()); + let model = model.as_fake(); + let summary_model = thread.read_with(cx, |thread, _| { + thread.summarization_model().unwrap().clone() + }); + let summary_model = summary_model.as_fake(); + let send = acp_thread.update(cx, |thread, cx| { + thread.send( + vec![ + "What does ".into(), + acp::ContentBlock::ResourceLink(acp::ResourceLink { + name: "b.md".into(), + uri: MentionUri::File { + abs_path: path!("/a/b.md").into(), + } + .to_uri() + .to_string(), + annotations: None, + description: None, + mime_type: None, + size: None, + title: None, + }), + " mean?".into(), + ], + cx, + ) + }); + let send = cx.foreground_executor().spawn(send); + cx.run_until_parked(); + + model.send_last_completion_stream_text_chunk("Lorem."); + model.end_last_completion_stream(); + cx.run_until_parked(); + summary_model.send_last_completion_stream_text_chunk("Explaining /a/b.md"); + summary_model.end_last_completion_stream(); + + send.await.unwrap(); + acp_thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User + + What does [@b.md](file:///a/b.md) mean? + + ## Assistant + + Lorem. + + "} + ) + }); + + // Drop the ACP thread, which should cause the session to be dropped as well. + cx.update(|_| { + drop(thread); + drop(acp_thread); + }); + agent.read_with(cx, |agent, _| { + assert_eq!(agent.sessions.keys().cloned().collect::<Vec<_>>(), []); + }); + + // Ensure the thread can be reloaded from disk. + assert_eq!( + history_entries(&history_store, cx), + vec![( + HistoryEntryId::AcpThread(session_id.clone()), + "Explaining /a/b.md".into() + )] + ); + let acp_thread = agent + .update(cx, |agent, cx| agent.open_thread(session_id.clone(), cx)) + .await + .unwrap(); + acp_thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User + + What does [@b.md](file:///a/b.md) mean? + + ## Assistant + + Lorem. + + "} + ) + }); + } + + fn history_entries( + history: &Entity<HistoryStore>, + cx: &mut TestAppContext, + ) -> Vec<(HistoryEntryId, String)> { + history.read_with(cx, |history, cx| { + history + .entries(cx) + .iter() + .map(|e| (e.id(), e.title().to_string())) + .collect::<Vec<_>>() + }) + } + fn init_test(cx: &mut TestAppContext) { env_logger::try_init().ok(); cx.update(|cx| { diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index cd97fa2060da70927bedb23d326db6dd2517c147..c7b1a08b92bb8e46772d8283b6e3d74f6b90cb14 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -720,7 +720,7 @@ impl Thread { pub fn to_db(&self, cx: &App) -> Task<DbThread> { let initial_project_snapshot = self.initial_project_snapshot.clone(); let mut thread = DbThread { - title: self.title.clone().unwrap_or_default(), + title: self.title(), messages: self.messages.clone(), updated_at: self.updated_at, detailed_summary: self.summary.clone(), @@ -870,6 +870,10 @@ impl Thread { &self.action_log } + pub fn is_empty(&self) -> bool { + self.messages.is_empty() && self.title.is_none() + } + pub fn model(&self) -> Option<&Arc<dyn LanguageModel>> { self.model.as_ref() } @@ -884,6 +888,10 @@ impl Thread { cx.notify() } + pub fn summarization_model(&self) -> Option<&Arc<dyn LanguageModel>> { + self.summarization_model.as_ref() + } + pub fn set_summarization_model( &mut self, model: Option<Arc<dyn LanguageModel>>, @@ -1068,6 +1076,7 @@ impl Thread { event_stream: event_stream.clone(), _task: cx.spawn(async move |this, cx| { log::info!("Starting agent turn execution"); + let mut update_title = None; let turn_result: Result<StopReason> = async { let mut completion_intent = CompletionIntent::UserPrompt; loop { @@ -1122,10 +1131,15 @@ impl Thread { this.pending_message() .tool_results .insert(tool_result.tool_use_id.clone(), tool_result); - }) - .ok(); + })?; } + this.update(cx, |this, cx| { + if this.title.is_none() && update_title.is_none() { + update_title = Some(this.update_title(&event_stream, cx)); + } + })?; + if tool_use_limit_reached { log::info!("Tool use limit reached, completing turn"); this.update(cx, |this, _cx| this.tool_use_limit_reached = true)?; @@ -1146,10 +1160,6 @@ impl Thread { Ok(reason) => { log::info!("Turn execution completed: {:?}", reason); - let update_title = this - .update(cx, |this, cx| this.update_title(&event_stream, cx)) - .ok() - .flatten(); if let Some(update_title) = update_title { update_title.await.context("update title failed").log_err(); } @@ -1593,17 +1603,14 @@ impl Thread { &mut self, event_stream: &ThreadEventStream, cx: &mut Context<Self>, - ) -> Option<Task<Result<()>>> { - if self.title.is_some() { - log::debug!("Skipping title generation because we already have one."); - return None; - } - + ) -> Task<Result<()>> { log::info!( "Generating title with model: {:?}", self.summarization_model.as_ref().map(|model| model.name()) ); - let model = self.summarization_model.clone()?; + let Some(model) = self.summarization_model.clone() else { + return Task::ready(Ok(())); + }; let event_stream = event_stream.clone(); let mut request = LanguageModelRequest { intent: Some(CompletionIntent::ThreadSummarization), @@ -1620,7 +1627,7 @@ impl Thread { content: vec![SUMMARIZE_THREAD_PROMPT.into()], cache: false, }); - Some(cx.spawn(async move |this, cx| { + cx.spawn(async move |this, cx| { let mut title = String::new(); let mut messages = model.stream_completion(request, cx).await?; while let Some(event) = messages.next().await { @@ -1655,7 +1662,7 @@ impl Thread { this.title = Some(title); cx.notify(); }) - })) + }) } fn last_user_message(&self) -> Option<&UserMessage> { @@ -2457,18 +2464,15 @@ impl From<UserMessageContent> for acp::ContentBlock { uri: None, }), UserMessageContent::Mention { uri, content } => { - acp::ContentBlock::ResourceLink(acp::ResourceLink { - uri: uri.to_uri().to_string(), - name: uri.name(), + acp::ContentBlock::Resource(acp::EmbeddedResource { + resource: acp::EmbeddedResourceResource::TextResourceContents( + acp::TextResourceContents { + mime_type: None, + text: content, + uri: uri.to_uri().to_string(), + }, + ), annotations: None, - description: if content.is_empty() { - None - } else { - Some(content) - }, - mime_type: None, - size: None, - title: None, }) } } From 6ed29fbc34b0ade21decea68c93ecd88420810a0 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:07:37 +0300 Subject: [PATCH 185/744] Enforce style lints which do not have violations (#36580) Release Notes: - N/A --- Cargo.toml | 96 ++++++++++++++++--- crates/action_log/src/action_log.rs | 2 +- .../src/activity_indicator.rs | 2 +- crates/agent_ui/src/inline_prompt_editor.rs | 8 +- crates/client/src/client.rs | 16 +--- crates/copilot/src/copilot.rs | 12 +-- crates/edit_prediction/src/edit_prediction.rs | 2 +- crates/editor/src/editor.rs | 62 ++++++------ crates/editor/src/element.rs | 2 +- crates/editor/src/scroll.rs | 2 +- crates/editor/src/scroll/actions.rs | 2 +- crates/git_ui/src/git_panel.rs | 4 +- crates/go_to_line/src/cursor_position.rs | 2 +- crates/go_to_line/src/go_to_line.rs | 2 +- crates/onboarding/src/theme_preview.rs | 7 +- crates/project/src/lsp_store.rs | 4 +- crates/project/src/project.rs | 2 +- crates/remote/src/ssh_session.rs | 2 +- crates/title_bar/src/title_bar.rs | 4 +- crates/workspace/src/workspace.rs | 2 +- 20 files changed, 146 insertions(+), 89 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c259a969126f7a742b9ebdbc31db9e2d8d614b5c..d69e87fd6bbe482de391c61c159d567d0a1ea13f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -825,36 +825,106 @@ declare_interior_mutable_const = "deny" style = { level = "allow", priority = -1 } # Temporary list of style lints that we've fixed so far. +# Progress is being tracked in #36577 +blocks_in_conditions = "warn" bool_assert_comparison = "warn" +borrow_interior_mutable_const = "warn" +box_default = "warn" +builtin_type_shadow = "warn" +bytes_nth = "warn" +chars_next_cmp = "warn" +cmp_null = "warn" +collapsible_else_if = "warn" +collapsible_if = "warn" comparison_to_empty = "warn" +default_instead_of_iter_empty = "warn" +disallowed_macros = "warn" +disallowed_methods = "warn" +disallowed_names = "warn" +disallowed_types = "warn" doc_lazy_continuation = "warn" doc_overindented_list_items = "warn" -inherent_to_string = "warn" +duplicate_underscore_argument = "warn" +err_expect = "warn" +fn_to_numeric_cast = "warn" +fn_to_numeric_cast_with_truncation = "warn" for_kv_map = "warn" +implicit_saturating_add = "warn" +implicit_saturating_sub = "warn" +inconsistent_digit_grouping = "warn" +infallible_destructuring_match = "warn" +inherent_to_string = "warn" +init_numbered_fields = "warn" into_iter_on_ref = "warn" io_other_error = "warn" +items_after_test_module = "warn" iter_cloned_collect = "warn" iter_next_slice = "warn" iter_nth = "warn" iter_nth_zero = "warn" iter_skip_next = "warn" +just_underscores_and_digits = "warn" let_and_return = "warn" +main_recursion = "warn" +manual_bits = "warn" +manual_dangling_ptr = "warn" +manual_is_ascii_check = "warn" +manual_is_finite = "warn" +manual_is_infinite = "warn" +manual_next_back = "warn" +manual_non_exhaustive = "warn" +manual_ok_or = "warn" +manual_pattern_char_comparison = "warn" +manual_rotate = "warn" +manual_slice_fill = "warn" +manual_while_let_some = "warn" +map_collect_result_unit = "warn" match_like_matches_macro = "warn" -module_inception = { level = "deny" } -question_mark = { level = "deny" } -single_match = "warn" -redundant_closure = { level = "deny" } -redundant_static_lifetimes = { level = "warn" } -redundant_pattern_matching = "warn" +match_overlapping_arm = "warn" +mem_replace_option_with_none = "warn" +mem_replace_option_with_some = "warn" +missing_enforced_import_renames = "warn" +missing_safety_doc = "warn" +mixed_attributes_style = "warn" +mixed_case_hex_literals = "warn" +module_inception = "warn" +must_use_unit = "warn" +mut_mutex_lock = "warn" +needless_borrow = "warn" +needless_doctest_main = "warn" +needless_else = "warn" +needless_parens_on_range_literals = "warn" +needless_pub_self = "warn" +needless_return = "warn" +needless_return_with_question_mark = "warn" +ok_expect = "warn" +owned_cow = "warn" +print_literal = "warn" +print_with_newline = "warn" +ptr_eq = "warn" +question_mark = "warn" +redundant_closure = "warn" redundant_field_names = "warn" -collapsible_if = { level = "warn"} -collapsible_else_if = { level = "warn" } -needless_borrow = { level = "warn"} -needless_return = { level = "warn" } -unnecessary_mut_passed = {level = "warn"} -unnecessary_map_or = { level = "warn" } +redundant_pattern_matching = "warn" +redundant_static_lifetimes = "warn" +result_map_or_into_option = "warn" +self_named_constructors = "warn" +single_match = "warn" +tabs_in_doc_comments = "warn" +to_digit_is_some = "warn" +toplevel_ref_arg = "warn" +unnecessary_fold = "warn" +unnecessary_map_or = "warn" +unnecessary_mut_passed = "warn" +unnecessary_owned_empty_strings = "warn" +unneeded_struct_pattern = "warn" +unsafe_removed_from_name = "warn" unused_unit = "warn" +unusual_byte_groupings = "warn" +write_literal = "warn" +writeln_empty_string = "warn" wrong_self_convention = "warn" +zero_ptr = "warn" # Individual rules that have violations in the codebase: type_complexity = "allow" diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index a1f332fc7cb838e27f01417b383c3d36f78f9356..9ec10f4dbb0e670bf20d9c033db9cec02e5fda67 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -190,7 +190,7 @@ impl ActionLog { cx: &mut Context<Self>, ) { match event { - BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx), + BufferEvent::Edited => self.handle_buffer_edited(buffer, cx), BufferEvent::FileHandleChanged => { self.handle_buffer_file_changed(buffer, cx); } diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 324480f5b49748eb1f107a520d44a916ac72659f..6641db0805fed2fbade1e66cde143f58123dd3d4 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -104,7 +104,7 @@ impl ActivityIndicator { &workspace_handle, window, |activity_indicator, _, event, window, cx| { - if let workspace::Event::ClearActivityIndicator { .. } = event + if let workspace::Event::ClearActivityIndicator = event && activity_indicator.statuses.pop().is_some() { activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx); diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 56081434644e3b2f3ee7147bdff2874e631ee544..a626122769f656cf6627d104d00f4fa3a368e7db 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -1229,27 +1229,27 @@ pub enum GenerationMode { impl GenerationMode { fn start_label(self) -> &'static str { match self { - GenerationMode::Generate { .. } => "Generate", + GenerationMode::Generate => "Generate", GenerationMode::Transform => "Transform", } } fn tooltip_interrupt(self) -> &'static str { match self { - GenerationMode::Generate { .. } => "Interrupt Generation", + GenerationMode::Generate => "Interrupt Generation", GenerationMode::Transform => "Interrupt Transform", } } fn tooltip_restart(self) -> &'static str { match self { - GenerationMode::Generate { .. } => "Restart Generation", + GenerationMode::Generate => "Restart Generation", GenerationMode::Transform => "Restart Transform", } } fn tooltip_accept(self) -> &'static str { match self { - GenerationMode::Generate { .. } => "Accept Generation", + GenerationMode::Generate => "Accept Generation", GenerationMode::Transform => "Accept Transform", } } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index b6ce9d24e9a86bb5deb2e1f33879d6959f17b677..ed3f1149433a9532ef2c08032ffa494cf09dfb4c 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1029,11 +1029,11 @@ impl Client { Status::SignedOut | Status::Authenticated => true, Status::ConnectionError | Status::ConnectionLost - | Status::Authenticating { .. } + | Status::Authenticating | Status::AuthenticationError - | Status::Reauthenticating { .. } + | Status::Reauthenticating | Status::ReconnectionError { .. } => false, - Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => { + Status::Connected { .. } | Status::Connecting | Status::Reconnecting => { return ConnectionResult::Result(Ok(())); } Status::UpgradeRequired => { @@ -1902,10 +1902,7 @@ mod tests { assert!(matches!(status.next().await, Some(Status::Connecting))); executor.advance_clock(CONNECTION_TIMEOUT); - assert!(matches!( - status.next().await, - Some(Status::ConnectionError { .. }) - )); + assert!(matches!(status.next().await, Some(Status::ConnectionError))); auth_and_connect.await.into_response().unwrap_err(); // Allow the connection to be established. @@ -1929,10 +1926,7 @@ mod tests { }) }); executor.advance_clock(2 * INITIAL_RECONNECTION_DELAY); - assert!(matches!( - status.next().await, - Some(Status::Reconnecting { .. }) - )); + assert!(matches!(status.next().await, Some(Status::Reconnecting))); executor.advance_clock(CONNECTION_TIMEOUT); assert!(matches!( diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 33455f5e52110e157a3d888a2e71b572e39f2fa1..b7d8423fd7d4d601250172a5789cbe83620849af 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -126,7 +126,7 @@ impl CopilotServer { fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> { let server = self.as_running()?; anyhow::ensure!( - matches!(server.sign_in_status, SignInStatus::Authorized { .. }), + matches!(server.sign_in_status, SignInStatus::Authorized), "must sign in before using copilot" ); Ok(server) @@ -578,12 +578,12 @@ impl Copilot { pub(crate) fn sign_in(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> { if let CopilotServer::Running(server) = &mut self.server { let task = match &server.sign_in_status { - SignInStatus::Authorized { .. } => Task::ready(Ok(())).shared(), + SignInStatus::Authorized => Task::ready(Ok(())).shared(), SignInStatus::SigningIn { task, .. } => { cx.notify(); task.clone() } - SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized { .. } => { + SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized => { let lsp = server.lsp.clone(); let task = cx .spawn(async move |this, cx| { @@ -727,7 +727,7 @@ impl Copilot { .. }) = &mut self.server { - if !matches!(status, SignInStatus::Authorized { .. }) { + if !matches!(status, SignInStatus::Authorized) { return; } @@ -1009,8 +1009,8 @@ impl Copilot { CopilotServer::Error(error) => Status::Error(error.clone()), CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => { match sign_in_status { - SignInStatus::Authorized { .. } => Status::Authorized, - SignInStatus::Unauthorized { .. } => Status::Unauthorized, + SignInStatus::Authorized => Status::Authorized, + SignInStatus::Unauthorized => Status::Unauthorized, SignInStatus::SigningIn { prompt, .. } => Status::SigningIn { prompt: prompt.clone(), }, diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs index c8502f75de5adac0a1bfdcb8cd8fe4444bb70f84..964f2029340f425546f8816f94a604cabe2aa294 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -34,7 +34,7 @@ pub enum DataCollectionState { impl DataCollectionState { pub fn is_supported(&self) -> bool { - !matches!(self, DataCollectionState::Unsupported { .. }) + !matches!(self, DataCollectionState::Unsupported) } pub fn is_enabled(&self) -> bool { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5fc017dcfc55c754a466a23edd53873116b00c63..2136d5f4b363c056669f807e6990aa4ffc7ef670 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1854,8 +1854,8 @@ impl Editor { blink_manager }); - let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. }) - .then(|| language_settings::SoftWrap::None); + let soft_wrap_mode_override = + matches!(mode, EditorMode::SingleLine).then(|| language_settings::SoftWrap::None); let mut project_subscriptions = Vec::new(); if full_mode && let Some(project) = project.as_ref() { @@ -1980,14 +1980,12 @@ impl Editor { .detach(); } - let show_indent_guides = if matches!( - mode, - EditorMode::SingleLine { .. } | EditorMode::Minimap { .. } - ) { - Some(false) - } else { - None - }; + let show_indent_guides = + if matches!(mode, EditorMode::SingleLine | EditorMode::Minimap { .. }) { + Some(false) + } else { + None + }; let breakpoint_store = match (&mode, project.as_ref()) { (EditorMode::Full { .. }, Some(project)) => Some(project.read(cx).breakpoint_store()), @@ -2047,7 +2045,7 @@ impl Editor { vertical: full_mode, }, minimap_visibility: MinimapVisibility::for_mode(&mode, cx), - offset_content: !matches!(mode, EditorMode::SingleLine { .. }), + offset_content: !matches!(mode, EditorMode::SingleLine), show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs, show_gutter: full_mode, show_line_numbers: (!full_mode).then_some(false), @@ -2401,7 +2399,7 @@ impl Editor { let mut key_context = KeyContext::new_with_defaults(); key_context.add("Editor"); let mode = match self.mode { - EditorMode::SingleLine { .. } => "single_line", + EditorMode::SingleLine => "single_line", EditorMode::AutoHeight { .. } => "auto_height", EditorMode::Minimap { .. } => "minimap", EditorMode::Full { .. } => "full", @@ -6772,7 +6770,7 @@ impl Editor { &mut self, cx: &mut Context<Editor>, ) -> Option<(String, Range<Anchor>)> { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { return None; } if !EditorSettings::get_global(cx).selection_highlight { @@ -12601,7 +12599,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -12725,7 +12723,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13209,7 +13207,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13230,7 +13228,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13251,7 +13249,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13272,7 +13270,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13293,7 +13291,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13318,7 +13316,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13343,7 +13341,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13368,7 +13366,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13393,7 +13391,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13414,7 +13412,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13435,7 +13433,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13456,7 +13454,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13477,7 +13475,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13502,7 +13500,7 @@ impl Editor { } pub fn move_to_end(&mut self, _: &MoveToEnd, window: &mut Window, cx: &mut Context<Self>) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -14551,7 +14549,7 @@ impl Editor { let advance_downwards = action.advance_downwards && selections_on_single_row && !selections_selecting - && !matches!(this.mode, EditorMode::SingleLine { .. }); + && !matches!(this.mode, EditorMode::SingleLine); if advance_downwards { let snapshot = this.buffer.read(cx).snapshot(cx); @@ -22867,7 +22865,7 @@ impl Render for Editor { let settings = ThemeSettings::get_global(cx); let mut text_style = match self.mode { - EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => TextStyle { + EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle { color: cx.theme().colors().editor_foreground, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), @@ -22893,7 +22891,7 @@ impl Render for Editor { } let background = match self.mode { - EditorMode::SingleLine { .. } => cx.theme().system().transparent, + EditorMode::SingleLine => cx.theme().system().transparent, EditorMode::AutoHeight { .. } => cx.theme().system().transparent, EditorMode::Full { .. } => cx.theme().colors().editor_background, EditorMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7), diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b18d1ceae1b7c316df849aca9fd8c028da906fee..416f35d7a76761351afd9ab749d08745d96deb9c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -8105,7 +8105,7 @@ impl Element for EditorElement { // The max scroll position for the top of the window let max_scroll_top = if matches!( snapshot.mode, - EditorMode::SingleLine { .. } + EditorMode::SingleLine | EditorMode::AutoHeight { .. } | EditorMode::Full { sized_by_content: true, diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index b47f1cd711571d55f61012989c01234aa26609fb..82314486187db99c2ba5c104faa42828dad57cdb 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -675,7 +675,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs index 72827b2fee48c424a632018b5f66015cd058ed79..f8104665f904e08466c72f3c410e58cb941c6b6f 100644 --- a/crates/editor/src/scroll/actions.rs +++ b/crates/editor/src/scroll/actions.rs @@ -16,7 +16,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 5a0151418589e792039cac9cde9c2185552a28ee..79d182eb22b2fb3c015f7ba419e0ba597d4f23e4 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2983,9 +2983,7 @@ impl GitPanel { let status_toast = StatusToast::new(message, cx, move |this, _cx| { use remote_output::SuccessStyle::*; match style { - Toast { .. } => { - this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) - } + Toast => this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)), ToastWithLog { output } => this .icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) .action("View Log", move |window, cx| { diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 23729be062ab64c3367d58ac5e87de67a077ac7e..e60a3651aae3f062b16fdfa7aa01a28e5c845e85 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -106,7 +106,7 @@ impl CursorPosition { cursor_position.selected_count.selections = editor.selections.count(); match editor.mode() { editor::EditorMode::AutoHeight { .. } - | editor::EditorMode::SingleLine { .. } + | editor::EditorMode::SingleLine | editor::EditorMode::Minimap { .. } => { cursor_position.position = None; cursor_position.context = None; diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 1913646aa1fa4a1d74b4b47a59dde961a7e37def..2afc72e989b1a112c481d1c1438d216ececec626 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -157,7 +157,7 @@ impl GoToLine { self.prev_scroll_position.take(); cx.emit(DismissEvent) } - editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx), + editor::EditorEvent::BufferEdited => self.highlight_current_line(cx), _ => {} } } diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index d84bc9b0e5b6e505eed139910db848645ba56c15..8bd65d8a2707acdc53333071486f41741398a82a 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -362,13 +362,12 @@ impl Component for ThemePreviewTile { .gap_4() .children( themes_to_preview - .iter() - .enumerate() - .map(|(_, theme)| { + .into_iter() + .map(|theme| { div() .w(px(200.)) .h(px(140.)) - .child(ThemePreviewTile::new(theme.clone(), 0.42)) + .child(ThemePreviewTile::new(theme, 0.42)) }) .collect::<Vec<_>>(), ) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 7a44ad3f875a8bfea9bcbf112c1a726610867ad3..e989b974e129a71f2f524a56db909a2596783cb0 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3924,9 +3924,7 @@ impl LspStore { _: &mut Context<Self>, ) { match event { - ToolchainStoreEvent::ToolchainActivated { .. } => { - self.request_workspace_config_refresh() - } + ToolchainStoreEvent::ToolchainActivated => self.request_workspace_config_refresh(), } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index af5fd0d675096493dd41f09bfb653b2cc3c25d7c..e47c020a429fca8e6ed99aec6b89ace2a78d8985 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3119,7 +3119,7 @@ impl Project { event: &BufferEvent, cx: &mut Context<Self>, ) -> Option<()> { - if matches!(event, BufferEvent::Edited { .. } | BufferEvent::Reloaded) { + if matches!(event, BufferEvent::Edited | BufferEvent::Reloaded) { self.request_buffer_diff_recalculation(&buffer, cx); } diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 5fa3a5f71571280794d915a3b7218b3523eb8c87..1c4409aec30ace5efc52a5d7a3ac4d13d6274ea6 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -948,7 +948,7 @@ impl SshRemoteClient { if old_state.is_reconnecting() { match &new_state { State::Connecting - | State::Reconnecting { .. } + | State::Reconnecting | State::HeartbeatMissed { .. } | State::ServerNotRunning => {} State::Connected { .. } => { diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 35b33f39bed0cc528f4bfcb88259d37e550fa7ad..b84a2800b65f5a2c280256a4765101ae125f7ec4 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -563,8 +563,8 @@ impl TitleBar { match status { client::Status::ConnectionError | client::Status::ConnectionLost - | client::Status::Reauthenticating { .. } - | client::Status::Reconnecting { .. } + | client::Status::Reauthenticating + | client::Status::Reconnecting | client::Status::ReconnectionError { .. } => Some( div() .id("disconnected") diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 64cf77a4fde5690de05fc7a38243e6ab1bafe46e..b52687f335734330639dca63451c4dffef5ce5c4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7670,7 +7670,7 @@ pub fn client_side_decorations( match decorations { Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW), - Decorations::Server { .. } => window.set_client_inset(px(0.0)), + Decorations::Server => window.set_client_inset(px(0.0)), } struct GlobalResizeEdge(ResizeEdge); From de12633591a79af3bac5fc030ee3d54bb4270920 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra <me@as-cii.com> Date: Wed, 20 Aug 2025 15:02:40 +0200 Subject: [PATCH 186/744] Wait for agent2 feature flag before loading panel (#36583) Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 3c4c403a7703af78f1224fee773a9e912aee6bae..286d3b1c26e100559f6ec31b0f2cd817362a1fad 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -523,6 +523,7 @@ impl AgentPanel { anyhow::Ok(()) })); } + pub fn load( workspace: WeakEntity<Workspace>, prompt_builder: Arc<PromptBuilder>, @@ -572,6 +573,17 @@ impl AgentPanel { None }; + // Wait for the Gemini/Native feature flag to be available. + let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?; + if !client.status().borrow().is_signed_out() { + cx.update(|_, cx| { + cx.wait_for_flag_or_timeout::<feature_flags::GeminiAndNativeFeatureFlag>( + Duration::from_secs(2), + ) + })? + .await; + } + let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| { Self::new( From bc79076ad3767e004bc6c5ff7efa9673400329d2 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Wed, 20 Aug 2025 16:17:28 +0300 Subject: [PATCH 187/744] Fix `clippy::manual_map` lint violations (#36584) #36577 Release Notes: - N/A --- Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 8 +++---- crates/agent_ui/src/acp/thread_view.rs | 7 ++---- crates/agent_ui/src/active_thread.rs | 10 ++++---- crates/agent_ui/src/inline_assistant.rs | 10 ++++---- .../src/edit_agent/streaming_fuzzy_matcher.rs | 8 +++---- crates/editor/src/editor_tests.rs | 8 +------ crates/editor/src/hover_popover.rs | 24 +++++++------------ crates/file_finder/src/file_finder.rs | 7 +++--- crates/git_ui/src/commit_modal.rs | 12 +++------- crates/gpui/src/platform/windows/window.rs | 5 +--- crates/multi_buffer/src/multi_buffer.rs | 10 +++----- crates/project/src/lsp_command.rs | 15 +++++------- crates/project/src/lsp_store.rs | 15 ++++-------- crates/project_panel/src/project_panel.rs | 20 +++++++--------- crates/vim/src/command.rs | 6 +---- crates/workspace/src/pane.rs | 4 +--- crates/workspace/src/workspace.rs | 10 ++++---- 18 files changed, 62 insertions(+), 118 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d69e87fd6bbe482de391c61c159d567d0a1ea13f..9cd206cebf77e704e0e0535d32c93212469e7d06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -871,6 +871,7 @@ manual_dangling_ptr = "warn" manual_is_ascii_check = "warn" manual_is_finite = "warn" manual_is_infinite = "warn" +manual_map = "warn" manual_next_back = "warn" manual_non_exhaustive = "warn" manual_ok_or = "warn" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 4f20dbd587440e4953187c5d306a3584a4cad20c..b8908fa0da5bd4fbfe45922ec21a38fd23686456 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -301,11 +301,9 @@ impl ToolCall { ) -> Option<AgentLocation> { let buffer = project .update(cx, |project, cx| { - if let Some(path) = project.project_path_for_absolute_path(&location.path, cx) { - Some(project.open_buffer(path, cx)) - } else { - None - } + project + .project_path_for_absolute_path(&location.path, cx) + .map(|path| project.open_buffer(path, cx)) }) .ok()??; let buffer = buffer.await.log_err()?; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index b5277758509d5dd4f25ca9f5ceede004c6b817d6..f89198c84b2ebfd94e730c508d4edccb439daa1f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4012,12 +4012,9 @@ impl Render for AcpThreadView { .children( if let Some(usage_callout) = self.render_usage_callout(line_height, cx) { Some(usage_callout.into_any_element()) - } else if let Some(token_limit_callout) = - self.render_token_limit_callout(line_height, cx) - { - Some(token_limit_callout.into_any_element()) } else { - None + self.render_token_limit_callout(line_height, cx) + .map(|token_limit_callout| token_limit_callout.into_any_element()) }, ) .child(self.render_message_editor(window, cx)) diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index bb5b47f0d69b00d89b32be13c29ad71f23abd239..e214986b82b973905307c23319144f4544c36c9c 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -779,13 +779,11 @@ impl ActiveThread { let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.)); - let workspace_subscription = if let Some(workspace) = workspace.upgrade() { - Some(cx.observe_release(&workspace, |this, _, cx| { + let workspace_subscription = workspace.upgrade().map(|workspace| { + cx.observe_release(&workspace, |this, _, cx| { this.dismiss_notifications(cx); - })) - } else { - None - }; + }) + }); let mut this = Self { language_registry, diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 21115533400703e8ffa31f06054d19f46f226727..13f1234b4de0aec6f7540c0f34950b8b5c3ef585 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -1532,13 +1532,11 @@ impl InlineAssistant { .and_then(|item| item.act_as::<Editor>(cx)) { Some(InlineAssistTarget::Editor(workspace_editor)) - } else if let Some(terminal_view) = workspace - .active_item(cx) - .and_then(|item| item.act_as::<TerminalView>(cx)) - { - Some(InlineAssistTarget::Terminal(terminal_view)) } else { - None + workspace + .active_item(cx) + .and_then(|item| item.act_as::<TerminalView>(cx)) + .map(InlineAssistTarget::Terminal) } } } diff --git a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs b/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs index 2dba8a2b6d4dbf22868b7512b1a9675132a0edaf..33b37679f0a345ef070942057b307bd377012d05 100644 --- a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs +++ b/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs @@ -794,10 +794,8 @@ mod tests { fn finish(mut finder: StreamingFuzzyMatcher) -> Option<String> { let snapshot = finder.snapshot.clone(); let matches = finder.finish(); - if let Some(range) = matches.first() { - Some(snapshot.text_for_range(range.clone()).collect::<String>()) - } else { - None - } + matches + .first() + .map(|range| snapshot.text_for_range(range.clone()).collect::<String>()) } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 955ade04cd1fc29d9ae7bfe0060271e698c6e4e2..44c05dbc143849a444737d24f0e519b33bdf56e4 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -21065,13 +21065,7 @@ fn add_log_breakpoint_at_cursor( let (anchor, bp) = editor .breakpoints_at_cursors(window, cx) .first() - .and_then(|(anchor, bp)| { - if let Some(bp) = bp { - Some((*anchor, bp.clone())) - } else { - None - } - }) + .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone()))) .unwrap_or_else(|| { let cursor_position: Point = editor.selections.newest(cx).head(); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 497f193cb4efbabc1b5003bba1ba201a4e0b5586..28a09e947f58ef26f20453e9f36f01e7cd74061e 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -174,11 +174,9 @@ pub fn hover_at_inlay( let subscription = this .update(cx, |_, cx| { - if let Some(parsed_content) = &parsed_content { - Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) - } else { - None - } + parsed_content.as_ref().map(|parsed_content| { + cx.observe(parsed_content, |_, _, cx| cx.notify()) + }) }) .ok() .flatten(); @@ -450,11 +448,9 @@ fn show_hover( let scroll_handle = ScrollHandle::new(); let subscription = this .update(cx, |_, cx| { - if let Some(parsed_content) = &parsed_content { - Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) - } else { - None - } + parsed_content.as_ref().map(|parsed_content| { + cx.observe(parsed_content, |_, _, cx| cx.notify()) + }) }) .ok() .flatten(); @@ -502,11 +498,9 @@ fn show_hover( hover_highlights.push(range.clone()); let subscription = this .update(cx, |_, cx| { - if let Some(parsed_content) = &parsed_content { - Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) - } else { - None - } + parsed_content.as_ref().map(|parsed_content| { + cx.observe(parsed_content, |_, _, cx| cx.notify()) + }) }) .ok() .flatten(); diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 40acf012c915a868c961caeeed44428d370dcaea..8aaaa047292065a9db0c47f980e559ca61c04546 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -267,10 +267,9 @@ impl FileFinder { ) { self.picker.update(cx, |picker, cx| { picker.delegate.include_ignored = match picker.delegate.include_ignored { - Some(true) => match FileFinderSettings::get_global(cx).include_ignored { - Some(_) => Some(false), - None => None, - }, + Some(true) => FileFinderSettings::get_global(cx) + .include_ignored + .map(|_| false), Some(false) => Some(true), None => Some(true), }; diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index e1e6cee93c36037299b85a4e090f6c25419e2030..cae4d28a83b01a8195c891d2a7569803cfecd697 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -391,15 +391,9 @@ impl CommitModal { }); let focus_handle = self.focus_handle(cx); - let close_kb_hint = - if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) { - Some( - KeybindingHint::new(close_kb, cx.theme().colors().editor_background) - .suffix("Cancel"), - ) - } else { - None - }; + let close_kb_hint = ui::KeyBinding::for_action(&menu::Cancel, window, cx).map(|close_kb| { + KeybindingHint::new(close_kb, cx.theme().colors().editor_background).suffix("Cancel") + }); h_flex() .group("commit_editor_footer") diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 32a6da23915d1e2bdf61c662e364119e9c6a8c64..99e50733714ba3e280508585f479a90e7edd76d7 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -592,10 +592,7 @@ impl PlatformWindow for WindowsWindow { ) -> Option<Receiver<usize>> { let (done_tx, done_rx) = oneshot::channel(); let msg = msg.to_string(); - let detail_string = match detail { - Some(info) => Some(info.to_string()), - None => None, - }; + let detail_string = detail.map(|detail| detail.to_string()); let handle = self.0.hwnd; let answers = answers.to_vec(); self.0 diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 6b6d17a2461de62c9c5fa616ee8ccee6390f812b..60e9c14c34437bf79ad5eaa0be60972d95ddff58 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -4069,13 +4069,9 @@ impl MultiBufferSnapshot { buffer_end = buffer_end.min(end_buffer_offset); } - if let Some(iterator) = - get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end) - { - Some(&mut current_excerpt_metadata.insert((excerpt.id, iterator)).1) - } else { - None - } + get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end).map(|iterator| { + &mut current_excerpt_metadata.insert((excerpt.id, iterator)).1 + }) }; // Visit each metadata item. diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index a91e3fb402ff8d02279cc52d685f286de3fa4358..c90d85358a2a4d70ec95ad4c25177026cb2a173c 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2595,11 +2595,9 @@ impl LspCommand for GetCodeActions { server_id: LanguageServerId, cx: AsyncApp, ) -> Result<Vec<CodeAction>> { - let requested_kinds_set = if let Some(kinds) = self.kinds { - Some(kinds.into_iter().collect::<HashSet<_>>()) - } else { - None - }; + let requested_kinds_set = self + .kinds + .map(|kinds| kinds.into_iter().collect::<HashSet<_>>()); let language_server = cx.update(|cx| { lsp_store @@ -3821,12 +3819,11 @@ impl GetDocumentDiagnostics { _ => None, }, code, - code_description: match diagnostic.code_description { - Some(code_description) => Some(CodeDescription { + code_description: diagnostic + .code_description + .map(|code_description| CodeDescription { href: Some(lsp::Url::parse(&code_description).unwrap()), }), - None => None, - }, related_information: Some(related_information), tags: Some(tags), source: diagnostic.source.clone(), diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index e989b974e129a71f2f524a56db909a2596783cb0..1b461178972b6cf9182c3da2a4fcba4595986eb9 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -12270,11 +12270,10 @@ async fn populate_labels_for_completions( let lsp_completions = new_completions .iter() .filter_map(|new_completion| { - if let Some(lsp_completion) = new_completion.source.lsp_completion(true) { - Some(lsp_completion.into_owned()) - } else { - None - } + new_completion + .source + .lsp_completion(true) + .map(|lsp_completion| lsp_completion.into_owned()) }) .collect::<Vec<_>>(); @@ -12294,11 +12293,7 @@ async fn populate_labels_for_completions( for completion in new_completions { match completion.source.lsp_completion(true) { Some(lsp_completion) => { - let documentation = if let Some(docs) = lsp_completion.documentation.clone() { - Some(docs.into()) - } else { - None - }; + let documentation = lsp_completion.documentation.clone().map(|docs| docs.into()); let mut label = labels.next().flatten().unwrap_or_else(|| { CodeLabel::fallback_for_completion(&lsp_completion, language.as_deref()) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index a5bfa883d5a3b78bc2be409ab84079bd918acf60..52ec7a9880089ad127736b0ed4f5732660ba1a6f 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -3895,14 +3895,12 @@ impl ProjectPanel { // Always highlight directory or parent directory if it's file if target_entry.is_dir() { Some(target_entry.id) - } else if let Some(parent_entry) = target_entry - .path - .parent() - .and_then(|parent_path| target_worktree.entry_for_path(parent_path)) - { - Some(parent_entry.id) } else { - None + target_entry + .path + .parent() + .and_then(|parent_path| target_worktree.entry_for_path(parent_path)) + .map(|parent_entry| parent_entry.id) } } @@ -3939,12 +3937,10 @@ impl ProjectPanel { // Always highlight directory or parent directory if it's file if target_entry.is_dir() { Some(target_entry.id) - } else if let Some(parent_entry) = - target_parent_path.and_then(|parent_path| target_worktree.entry_for_path(parent_path)) - { - Some(parent_entry.id) } else { - None + target_parent_path + .and_then(|parent_path| target_worktree.entry_for_path(parent_path)) + .map(|parent_entry| parent_entry.id) } } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 680c87f9e56e71b85e494de1f36c8cb7b88e4d9b..79d18a85e97c22ec82e7daef2ba7c50a7d3c0990 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1408,11 +1408,7 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Vec<CommandInterceptRes start: Position::Line { row: 0, offset: 0 }, end: Some(Position::LastLine { offset: 0 }), }); - if let Some(action) = OnMatchingLines::parse(query, invert, range, cx) { - Some(action.boxed_clone()) - } else { - None - } + OnMatchingLines::parse(query, invert, range, cx).map(|action| action.boxed_clone()) } else if query.contains('!') { ShellExec::parse(query, range.clone()) } else { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e49eb0a34559257c8cc7074370195ed6daca0b6b..dea18ddbe20e77cf25042d51998def8efa4893e6 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2583,10 +2583,8 @@ impl Pane { .children( std::iter::once(if let Some(decorated_icon) = decorated_icon { Some(div().child(decorated_icon.into_any_element())) - } else if let Some(icon) = icon { - Some(div().child(icon.into_any_element())) } else { - None + icon.map(|icon| div().child(icon.into_any_element())) }) .flatten(), ) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b52687f335734330639dca63451c4dffef5ce5c4..499e4f461902f8c2a509c6d40ec3d63843bb27c7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4732,14 +4732,12 @@ impl Workspace { }) }); - if let Some(view) = view { - Some(entry.insert(FollowerView { + view.map(|view| { + entry.insert(FollowerView { view, location: None, - })) - } else { - None - } + }) + }) } }; From c5040bd0a43f5835b3bb93d33ce26139c1dd0e51 Mon Sep 17 00:00:00 2001 From: Lukas Wirth <lukas@zed.dev> Date: Wed, 20 Aug 2025 15:41:58 +0200 Subject: [PATCH 188/744] remote: Do not leave client hanging on unhandled proto message (#36590) Otherwise the client will wait for a response that never arrives, causing the task to lock up Release Notes: - N/A --- crates/remote/src/ssh_session.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 1c4409aec30ace5efc52a5d7a3ac4d13d6274ea6..a26f4be6615ac27468b5135e22f3bafdf9bf3f33 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -2353,6 +2353,7 @@ impl ChannelClient { build_typed_envelope(peer_id, Instant::now(), incoming) { let type_name = envelope.payload_type_name(); + let message_id = envelope.message_id(); if let Some(future) = ProtoMessageHandlerSet::handle_message( &this.message_handlers, envelope, @@ -2391,6 +2392,15 @@ impl ChannelClient { .detach() } else { log::error!("{}:unhandled ssh message name:{type_name}", this.name); + if let Err(e) = AnyProtoClient::from(this.clone()).send_response( + message_id, + anyhow::anyhow!("no handler registered for {type_name}").to_proto(), + ) { + log::error!( + "{}:error sending error response for {type_name}:{e:#}", + this.name + ); + } } } } From 85865fc9509d7c336325a0825f990a2c6d3267ca Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Wed, 20 Aug 2025 15:54:00 +0200 Subject: [PATCH 189/744] agent2: New thread from summary (#36578) Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <agus@zed.dev> Co-authored-by: Cole Miller <cole@zed.dev> --- crates/agent2/src/history_store.rs | 4 ++ crates/agent_ui/src/acp/message_editor.rs | 30 ++++++++ crates/agent_ui/src/acp/thread_view.rs | 25 +++++-- crates/agent_ui/src/agent_panel.rs | 83 +++++++++++++++++++---- crates/agent_ui/src/agent_ui.rs | 7 ++ crates/zed/src/zed.rs | 1 + 6 files changed, 131 insertions(+), 19 deletions(-) diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 3df4eddde4cb7716ca8e6948c2f67fffc6f6ec7b..870c2607c49e9ac43e657bbd5a1970cb342f9005 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -111,6 +111,10 @@ impl HistoryStore { } } + pub fn thread_from_session_id(&self, session_id: &acp::SessionId) -> Option<&DbThreadMetadata> { + self.threads.iter().find(|thread| &thread.id == session_id) + } + pub fn delete_thread( &mut self, id: acp::SessionId, diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index b5282bf8911fc0cfc6c013d17dde1653d1456596..a50e33dc312d6a2892a55065a929c8c068d0a55c 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -163,6 +163,36 @@ impl MessageEditor { } } + pub fn insert_thread_summary( + &mut self, + thread: agent2::DbThreadMetadata, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let start = self.editor.update(cx, |editor, cx| { + editor.set_text(format!("{}\n", thread.title), window, cx); + editor + .buffer() + .read(cx) + .snapshot(cx) + .anchor_before(Point::zero()) + .text_anchor + }); + + self.confirm_completion( + thread.title.clone(), + start, + thread.title.len(), + MentionUri::Thread { + id: thread.id.clone(), + name: thread.title.to_string(), + }, + window, + cx, + ) + .detach(); + } + #[cfg(test)] pub(crate) fn editor(&self) -> &Entity<Editor> { &self.editor diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f89198c84b2ebfd94e730c508d4edccb439daa1f..8d7f9c53ca45ec6602eb78de888849f1159ac2ea 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -155,6 +155,7 @@ impl AcpThreadView { pub fn new( agent: Rc<dyn AgentServer>, resume_thread: Option<DbThreadMetadata>, + summarize_thread: Option<DbThreadMetadata>, workspace: WeakEntity<Workspace>, project: Entity<Project>, history_store: Entity<HistoryStore>, @@ -164,7 +165,7 @@ impl AcpThreadView { ) -> Self { let prevent_slash_commands = agent.clone().downcast::<ClaudeCode>().is_some(); let message_editor = cx.new(|cx| { - MessageEditor::new( + let mut editor = MessageEditor::new( workspace.clone(), project.clone(), history_store.clone(), @@ -177,7 +178,11 @@ impl AcpThreadView { }, window, cx, - ) + ); + if let Some(entry) = summarize_thread { + editor.insert_thread_summary(entry, window, cx); + } + editor }); let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0)); @@ -3636,8 +3641,18 @@ impl AcpThreadView { .child( Button::new("start-new-thread", "Start New Thread") .label_size(LabelSize::Small) - .on_click(cx.listener(|_this, _, _window, _cx| { - // todo: Once thread summarization is implemented, start a new thread from a summary. + .on_click(cx.listener(|this, _, window, cx| { + let Some(thread) = this.thread() else { + return; + }; + let session_id = thread.read(cx).session_id().clone(); + window.dispatch_action( + crate::NewNativeAgentThreadFromSummary { + from_session_id: session_id, + } + .boxed_clone(), + cx, + ); })), ) .when(burn_mode_available, |this| { @@ -4320,6 +4335,7 @@ pub(crate) mod tests { AcpThreadView::new( Rc::new(agent), None, + None, workspace.downgrade(), project, history_store, @@ -4526,6 +4542,7 @@ pub(crate) mod tests { AcpThreadView::new( Rc::new(StubAgentServer::new(connection.as_ref().clone())), None, + None, workspace.downgrade(), project.clone(), history_store.clone(), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 286d3b1c26e100559f6ec31b0f2cd817362a1fad..e2c4acb1ce5c0e85d44693d5d687ee1918fe63ec 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -30,7 +30,7 @@ use crate::{ thread_history::{HistoryEntryElement, ThreadHistory}, ui::{AgentOnboardingModal, EndTrialUpsell}, }; -use crate::{ExternalAgent, NewExternalAgentThread}; +use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary}; use agent::{ Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio, context_store::ContextStore, @@ -98,6 +98,16 @@ pub fn init(cx: &mut App) { workspace.focus_panel::<AgentPanel>(window, cx); } }) + .register_action( + |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| { + if let Some(panel) = workspace.panel::<AgentPanel>(cx) { + panel.update(cx, |panel, cx| { + panel.new_native_agent_thread_from_summary(action, window, cx) + }); + workspace.focus_panel::<AgentPanel>(window, cx); + } + }, + ) .register_action(|workspace, _: &OpenHistory, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); @@ -120,7 +130,7 @@ pub fn init(cx: &mut App) { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| { - panel.external_thread(action.agent, None, window, cx) + panel.external_thread(action.agent, None, None, window, cx) }); } }) @@ -670,6 +680,7 @@ impl AgentPanel { this.external_thread( Some(crate::ExternalAgent::NativeAgent), Some(thread.clone()), + None, window, cx, ); @@ -974,6 +985,29 @@ impl AgentPanel { AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); } + fn new_native_agent_thread_from_summary( + &mut self, + action: &NewNativeAgentThreadFromSummary, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let Some(thread) = self + .acp_history_store + .read(cx) + .thread_from_session_id(&action.from_session_id) + else { + return; + }; + + self.external_thread( + Some(ExternalAgent::NativeAgent), + None, + Some(thread.clone()), + window, + cx, + ); + } + fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) { let context = self .context_store @@ -1015,6 +1049,7 @@ impl AgentPanel { &mut self, agent_choice: Option<crate::ExternalAgent>, resume_thread: Option<DbThreadMetadata>, + summarize_thread: Option<DbThreadMetadata>, window: &mut Window, cx: &mut Context<Self>, ) { @@ -1083,6 +1118,7 @@ impl AgentPanel { crate::acp::AcpThreadView::new( server, resume_thread, + summarize_thread, workspace.clone(), project, this.acp_history_store.clone(), @@ -1754,6 +1790,7 @@ impl AgentPanel { agent2::HistoryEntry::AcpThread(entry) => this.external_thread( Some(ExternalAgent::NativeAgent), Some(entry.clone()), + None, window, cx, ), @@ -1823,15 +1860,23 @@ impl AgentPanel { AgentType::TextThread => { window.dispatch_action(NewTextThread.boxed_clone(), cx); } - AgentType::NativeAgent => { - self.external_thread(Some(crate::ExternalAgent::NativeAgent), None, window, cx) - } + AgentType::NativeAgent => self.external_thread( + Some(crate::ExternalAgent::NativeAgent), + None, + None, + window, + cx, + ), AgentType::Gemini => { - self.external_thread(Some(crate::ExternalAgent::Gemini), None, window, cx) - } - AgentType::ClaudeCode => { - self.external_thread(Some(crate::ExternalAgent::ClaudeCode), None, window, cx) + self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx) } + AgentType::ClaudeCode => self.external_thread( + Some(crate::ExternalAgent::ClaudeCode), + None, + None, + window, + cx, + ), } } @@ -1841,7 +1886,13 @@ impl AgentPanel { window: &mut Window, cx: &mut Context<Self>, ) { - self.external_thread(Some(ExternalAgent::NativeAgent), Some(thread), window, cx); + self.external_thread( + Some(ExternalAgent::NativeAgent), + Some(thread), + None, + window, + cx, + ); } } @@ -2358,8 +2409,10 @@ impl AgentPanel { let focus_handle = self.focus_handle(cx); let active_thread = match &self.active_view { - ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), - ActiveView::ExternalAgentThread { .. } + ActiveView::ExternalAgentThread { thread_view } => { + thread_view.read(cx).as_native_thread(cx) + } + ActiveView::Thread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None, @@ -2396,15 +2449,15 @@ impl AgentPanel { let thread = active_thread.read(cx); if !thread.is_empty() { - let thread_id = thread.id().clone(); + let session_id = thread.id().clone(); this.item( ContextMenuEntry::new("New From Summary") .icon(IconName::ThreadFromSummary) .icon_color(Color::Muted) .handler(move |window, cx| { window.dispatch_action( - Box::new(NewThread { - from_thread_id: Some(thread_id.clone()), + Box::new(NewNativeAgentThreadFromSummary { + from_session_id: session_id.clone(), }), cx, ); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 01a248994db1d615ebccc6b644deff7c06d73c1d..7b6557245fadc04145394ee20308a96749a708db 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -146,6 +146,13 @@ pub struct NewExternalAgentThread { agent: Option<ExternalAgent>, } +#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] +#[action(namespace = agent)] +#[serde(deny_unknown_fields)] +pub struct NewNativeAgentThreadFromSummary { + from_session_id: agent_client_protocol::SessionId, +} + #[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] enum ExternalAgent { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0972973b8957e5ad5f605619a805fa2db92be04a..0f6d236c654c594d52a3650b1f3018b92154d003 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4362,6 +4362,7 @@ mod tests { | "workspace::MoveItemToPaneInDirection" | "workspace::OpenTerminal" | "workspace::SendKeystrokes" + | "agent::NewNativeAgentThreadFromSummary" | "zed::OpenBrowser" | "zed::OpenZedUrl" => {} _ => { From eaf6b56163c2b987e06981e332e06d68aed5608b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra <me@as-cii.com> Date: Wed, 20 Aug 2025 15:56:39 +0200 Subject: [PATCH 190/744] Miscellaneous UX fixes for agent2 (#36591) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 97 ++++++++++++++++++++++++++ crates/agent_ui/src/acp/thread_view.rs | 44 +++++++----- 2 files changed, 123 insertions(+), 18 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index b8908fa0da5bd4fbfe45922ec21a38fd23686456..a1f9b32eba6a92687c676e8f60beba2e9c0453e9 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1394,6 +1394,17 @@ impl AcpThread { this.send_task.take(); } + // Truncate entries if the last prompt was refused. + if let Ok(Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Refusal, + })) = result + && let Some((ix, _)) = this.last_user_message() + { + let range = ix..this.entries.len(); + this.entries.truncate(ix); + cx.emit(AcpThreadEvent::EntriesRemoved(range)); + } + cx.emit(AcpThreadEvent::Stopped); Ok(()) } @@ -2369,6 +2380,92 @@ mod tests { assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]); } + #[gpui::test] + async fn test_refusal(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree(path!("/"), json!({})).await; + let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await; + + let refuse_next = Arc::new(AtomicBool::new(false)); + let connection = Rc::new(FakeAgentConnection::new().on_user_message({ + let refuse_next = refuse_next.clone(); + move |request, thread, mut cx| { + let refuse_next = refuse_next.clone(); + async move { + if refuse_next.load(SeqCst) { + return Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Refusal, + }); + } + + let acp::ContentBlock::Text(content) = &request.prompt[0] else { + panic!("expected text content block"); + }; + thread.update(&mut cx, |thread, cx| { + thread + .handle_session_update( + acp::SessionUpdate::AgentMessageChunk { + content: content.text.to_uppercase().into(), + }, + cx, + ) + .unwrap(); + })?; + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + } + .boxed_local() + } + })); + let thread = cx + .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) + .await + .unwrap(); + + cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx))) + .await + .unwrap(); + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User + + hello + + ## Assistant + + HELLO + + "} + ); + }); + + // Simulate refusing the second message, ensuring the conversation gets + // truncated to before sending it. + refuse_next.store(true, SeqCst); + cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["world".into()], cx))) + .await + .unwrap(); + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User + + hello + + ## Assistant + + HELLO + + "} + ); + }); + } + async fn run_until_first_tool_call( thread: &Entity<AcpThread>, cx: &mut TestAppContext, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 8d7f9c53ca45ec6602eb78de888849f1159ac2ea..9bb5953eafedc47712339197ecc791ab473f8034 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2398,7 +2398,6 @@ impl AcpThreadView { }) .when(!changed_buffers.is_empty(), |this| { this.child(self.render_edits_summary( - action_log, &changed_buffers, self.edits_expanded, pending_edits, @@ -2550,7 +2549,6 @@ impl AcpThreadView { fn render_edits_summary( &self, - action_log: &Entity<ActionLog>, changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>, expanded: bool, pending_edits: bool, @@ -2661,14 +2659,9 @@ impl AcpThreadView { ) .map(|kb| kb.size(rems_from_px(10.))), ) - .on_click({ - let action_log = action_log.clone(); - cx.listener(move |_, _, _, cx| { - action_log.update(cx, |action_log, cx| { - action_log.reject_all_edits(cx).detach(); - }) - }) - }), + .on_click(cx.listener(move |this, _, window, cx| { + this.reject_all(&RejectAll, window, cx); + })), ) .child( Button::new("keep-all-changes", "Keep All") @@ -2681,14 +2674,9 @@ impl AcpThreadView { KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx) .map(|kb| kb.size(rems_from_px(10.))), ) - .on_click({ - let action_log = action_log.clone(); - cx.listener(move |_, _, _, cx| { - action_log.update(cx, |action_log, cx| { - action_log.keep_all_edits(cx); - }) - }) - }), + .on_click(cx.listener(move |this, _, window, cx| { + this.keep_all(&KeepAll, window, cx); + })), ), ) } @@ -3014,6 +3002,24 @@ impl AcpThreadView { }); } + fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) { + let Some(thread) = self.thread() else { + return; + }; + let action_log = thread.read(cx).action_log().clone(); + action_log.update(cx, |action_log, cx| action_log.keep_all_edits(cx)); + } + + fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context<Self>) { + let Some(thread) = self.thread() else { + return; + }; + let action_log = thread.read(cx).action_log().clone(); + action_log + .update(cx, |action_log, cx| action_log.reject_all_edits(cx)) + .detach(); + } + fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> { let thread = self.as_native_thread(cx)?.read(cx); @@ -3952,6 +3958,8 @@ impl Render for AcpThreadView { .key_context("AcpThread") .on_action(cx.listener(Self::open_agent_diff)) .on_action(cx.listener(Self::toggle_burn_mode)) + .on_action(cx.listener(Self::keep_all)) + .on_action(cx.listener(Self::reject_all)) .bg(cx.theme().colors().panel_background) .child(match &self.thread_state { ThreadState::Unauthenticated { From 92352f97ad966df29cbac117b9c9ca6a697676f4 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Wed, 20 Aug 2025 17:34:52 +0300 Subject: [PATCH 191/744] Fix `clippy::map_clone` lint violations (#36585) #36577 Release Notes: - N/A --- Cargo.toml | 1 + crates/git_ui/src/git_panel.rs | 2 +- crates/gpui/src/platform/linux/x11/client.rs | 2 +- crates/workspace/src/pane.rs | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9cd206cebf77e704e0e0535d32c93212469e7d06..a0499407726c3394d4974579d5e072e5b23078a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -879,6 +879,7 @@ manual_pattern_char_comparison = "warn" manual_rotate = "warn" manual_slice_fill = "warn" manual_while_let_some = "warn" +map_clone = "warn" map_collect_result_unit = "warn" match_like_matches_macro = "warn" match_overlapping_arm = "warn" diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 79d182eb22b2fb3c015f7ba419e0ba597d4f23e4..cc947bcb72ff004e9ef0b73c4d21f340c917a3b9 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1335,7 +1335,7 @@ impl GitPanel { section.contains(status_entry, repository) && status_entry.staging.as_bool() != Some(goal_staged_state) }) - .map(|status_entry| status_entry.clone()) + .cloned() .collect::<Vec<_>>(); (goal_staged_state, entries) diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index d5011708927d89e7f7c66758710f6e8599e9ca8d..9a43bd64706ec21905b18b8837af2ddc785cba87 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -2108,7 +2108,7 @@ fn current_pointer_device_states( .classes .iter() .filter_map(|class| class.data.as_scroll()) - .map(|class| *class) + .copied() .rev() .collect::<Vec<_>>(); let old_state = scroll_values_to_preserve.get(&info.deviceid); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index dea18ddbe20e77cf25042d51998def8efa4893e6..23c8c0b1853e155df5d4a9b1cf3b4895bb74fd9a 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -3082,7 +3082,7 @@ impl Pane { .read(cx) .items() .find(|item| item.item_id() == item_id) - .map(|item| item.clone()) + .cloned() else { return; }; From 1e6cefaa56dc3dd62efd29ffc58262e710d6dbc1 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 20 Aug 2025 20:05:59 +0530 Subject: [PATCH 192/744] Fix `clippy::len_zero` lint style violations (#36589) Related: #36577 Release Notes: - N/A --------- Signed-off-by: Umesh Yadav <git@umesh.dev> --- Cargo.toml | 1 + crates/agent2/src/tools/find_path_tool.rs | 2 +- crates/agent_servers/src/claude.rs | 2 +- crates/agent_ui/src/message_editor.rs | 4 ++-- .../src/agent_panel_onboarding_content.rs | 2 +- crates/assistant_tools/src/find_path_tool.rs | 2 +- crates/buffer_diff/src/buffer_diff.rs | 2 +- crates/collab/src/tests/editor_tests.rs | 2 +- crates/debugger_ui/src/session/running.rs | 7 +++---- .../src/session/running/breakpoint_list.rs | 8 ++++---- .../src/session/running/module_list.rs | 8 ++++---- .../src/session/running/stack_frame_list.rs | 8 ++++---- .../src/session/running/variable_list.rs | 4 ++-- crates/diagnostics/src/diagnostics_tests.rs | 2 +- crates/editor/src/editor_tests.rs | 2 +- crates/editor/src/jsx_tag_auto_close.rs | 4 ++-- crates/editor/src/test/editor_test_context.rs | 2 +- crates/git_ui/src/git_panel.rs | 16 ++++++++-------- crates/language_models/src/provider/google.rs | 2 +- crates/project/src/debugger/dap_store.rs | 4 ++-- crates/tasks_ui/src/modal.rs | 2 +- crates/vim/src/digraph.rs | 2 +- crates/workspace/src/persistence/model.rs | 2 +- crates/zed/src/zed.rs | 2 +- 24 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a0499407726c3394d4974579d5e072e5b23078a5..a2de4aaaed97e011a57180c0fa22ef9a460960ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -864,6 +864,7 @@ iter_nth = "warn" iter_nth_zero = "warn" iter_skip_next = "warn" just_underscores_and_digits = "warn" +len_zero = "warn" let_and_return = "warn" main_recursion = "warn" manual_bits = "warn" diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index 552de144a73365d10d4b9a565d852c1a13672be8..deccf37ab71109fadd1d394ee4ee15000c74f2e5 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -116,7 +116,7 @@ impl AgentTool for FindPathTool { ..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())]; event_stream.update_fields(acp::ToolCallUpdateFields { - title: Some(if paginated_matches.len() == 0 { + title: Some(if paginated_matches.is_empty() { "No matches".into() } else if paginated_matches.len() == 1 { "1 match".into() diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index df2a24e6982913aaee343ddf2f9bd88854425f00..6b9732b468f334a31b35a14847908d2bad2192d6 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1117,7 +1117,7 @@ pub(crate) mod tests { thread.read_with(cx, |thread, _| { entries_len = thread.plan().entries.len(); - assert!(thread.plan().entries.len() > 0, "Empty plan"); + assert!(!thread.plan().entries.is_empty(), "Empty plan"); }); thread diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index fdbce14415e4aba230ed1485197a318933fc168e..bed10e90a7315f69f7e89d749d94767276fa1a22 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1682,7 +1682,7 @@ impl Render for MessageEditor { let has_history = self .history_store .as_ref() - .and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok()) + .and_then(|hs| hs.update(cx, |hs, cx| !hs.entries(cx).is_empty()).ok()) .unwrap_or(false) || self .thread @@ -1695,7 +1695,7 @@ impl Render for MessageEditor { !has_history && is_signed_out && has_configured_providers, |this| this.child(cx.new(ApiKeysWithProviders::new)), ) - .when(changed_buffers.len() > 0, |parent| { + .when(!changed_buffers.is_empty(), |parent| { parent.child(self.render_edits_bar(&changed_buffers, window, cx)) }) .child(self.render_editor(window, cx)) diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index 1a44fa3c17acca95ef970050c9eb512a0e3f2334..77f41d1a734afb4d21154cb536c19b4bd04632b1 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -74,7 +74,7 @@ impl Render for AgentPanelOnboarding { }), ) .map(|this| { - if enrolled_in_trial || is_pro_user || self.configured_providers.len() >= 1 { + if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() { this } else { this.child(ApiKeysWithoutProviders::new()) diff --git a/crates/assistant_tools/src/find_path_tool.rs b/crates/assistant_tools/src/find_path_tool.rs index 6b62638a4c33a3a4d29f7af51d3688a06f9c1dee..ac2c7a32abc0a3768caee85cfd25ab109f03aab3 100644 --- a/crates/assistant_tools/src/find_path_tool.rs +++ b/crates/assistant_tools/src/find_path_tool.rs @@ -234,7 +234,7 @@ impl ToolCard for FindPathToolCard { workspace: WeakEntity<Workspace>, cx: &mut Context<Self>, ) -> impl IntoElement { - let matches_label: SharedString = if self.paths.len() == 0 { + let matches_label: SharedString = if self.paths.is_empty() { "No matches".into() } else if self.paths.len() == 1 { "1 match".into() diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 6a9ca026e7a6d546adcd6b67263ab2c1c8947b9f..bef0c5cfc3093126362bcf468b9b22e3319781c1 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -2129,7 +2129,7 @@ mod tests { diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx) .collect::<Vec<_>>() }); - if hunks.len() == 0 { + if hunks.is_empty() { return; } diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 4e7996ce3b3d3b52f1a2fbf49565a0857f62b60c..1b0c581983ac54e9fdea074947bd8eaae4764c81 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -2908,7 +2908,7 @@ async fn test_lsp_pull_diagnostics( { assert!( - diagnostics_pulls_result_ids.lock().await.len() > 0, + !diagnostics_pulls_result_ids.lock().await.is_empty(), "Initial diagnostics pulls should report None at least" ); assert_eq!( diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 4306104877539139fc7522fbadf6da699ec8c7b6..0574091851f8f99e10ab8d1f7ec769177826e41a 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -1113,9 +1113,8 @@ impl RunningState { }; let session = self.session.read(cx); - let cwd = Some(&request.cwd) - .filter(|cwd| cwd.len() > 0) - .map(PathBuf::from) + let cwd = (!request.cwd.is_empty()) + .then(|| PathBuf::from(&request.cwd)) .or_else(|| session.binary().unwrap().cwd.clone()); let mut envs: HashMap<String, String> = @@ -1150,7 +1149,7 @@ impl RunningState { } else { None } - } else if args.len() > 0 { + } else if !args.is_empty() { Some(args.remove(0)) } else { None diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index d04443e20131de5cfec86b77a7d7bb68b186da10..233dba4c52e28c6e1f1b9205cb7481d487040fb1 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -244,7 +244,7 @@ impl BreakpointList { return; } let ix = match self.selected_ix { - _ if self.breakpoints.len() == 0 => None, + _ if self.breakpoints.is_empty() => None, None => Some(0), Some(ix) => { if ix == self.breakpoints.len() - 1 { @@ -268,7 +268,7 @@ impl BreakpointList { return; } let ix = match self.selected_ix { - _ if self.breakpoints.len() == 0 => None, + _ if self.breakpoints.is_empty() => None, None => Some(self.breakpoints.len() - 1), Some(ix) => { if ix == 0 { @@ -286,7 +286,7 @@ impl BreakpointList { cx.propagate(); return; } - let ix = if self.breakpoints.len() > 0 { + let ix = if !self.breakpoints.is_empty() { Some(0) } else { None @@ -299,7 +299,7 @@ impl BreakpointList { cx.propagate(); return; } - let ix = if self.breakpoints.len() > 0 { + let ix = if !self.breakpoints.is_empty() { Some(self.breakpoints.len() - 1) } else { None diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index 1c1e0f3efc552abdcca8d8fce6e215b24fe0b2ac..7743cfbdee7bf200ab25aabad4cfc455dc8b3484 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -223,7 +223,7 @@ impl ModuleList { fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) { let ix = match self.selected_ix { - _ if self.entries.len() == 0 => None, + _ if self.entries.is_empty() => None, None => Some(0), Some(ix) => { if ix == self.entries.len() - 1 { @@ -243,7 +243,7 @@ impl ModuleList { cx: &mut Context<Self>, ) { let ix = match self.selected_ix { - _ if self.entries.len() == 0 => None, + _ if self.entries.is_empty() => None, None => Some(self.entries.len() - 1), Some(ix) => { if ix == 0 { @@ -262,7 +262,7 @@ impl ModuleList { _window: &mut Window, cx: &mut Context<Self>, ) { - let ix = if self.entries.len() > 0 { + let ix = if !self.entries.is_empty() { Some(0) } else { None @@ -271,7 +271,7 @@ impl ModuleList { } fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) { - let ix = if self.entries.len() > 0 { + let ix = if !self.entries.is_empty() { Some(self.entries.len() - 1) } else { None diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index f9b5ed5e3f9c3068ce23bdd9fa3a653d90356523..a4ea4ab654929f00b05e9146bfd662aad2f8bd6d 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -621,7 +621,7 @@ impl StackFrameList { fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) { let ix = match self.selected_ix { - _ if self.entries.len() == 0 => None, + _ if self.entries.is_empty() => None, None => Some(0), Some(ix) => { if ix == self.entries.len() - 1 { @@ -641,7 +641,7 @@ impl StackFrameList { cx: &mut Context<Self>, ) { let ix = match self.selected_ix { - _ if self.entries.len() == 0 => None, + _ if self.entries.is_empty() => None, None => Some(self.entries.len() - 1), Some(ix) => { if ix == 0 { @@ -660,7 +660,7 @@ impl StackFrameList { _window: &mut Window, cx: &mut Context<Self>, ) { - let ix = if self.entries.len() > 0 { + let ix = if !self.entries.is_empty() { Some(0) } else { None @@ -669,7 +669,7 @@ impl StackFrameList { } fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) { - let ix = if self.entries.len() > 0 { + let ix = if !self.entries.is_empty() { Some(self.entries.len() - 1) } else { None diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index 18f574389e0838daf14ea7381d4d3b6415e5f323..b396f0921e5fdf58959e82db54bb8d558249891c 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -291,7 +291,7 @@ impl VariableList { } self.session.update(cx, |session, cx| { - session.variables(scope.variables_reference, cx).len() > 0 + !session.variables(scope.variables_reference, cx).is_empty() }) }) .map(|scope| { @@ -997,7 +997,7 @@ impl VariableList { DapEntry::Watcher { .. } => continue, DapEntry::Variable(dap) => scopes[idx].1.push(dap.clone()), DapEntry::Scope(scope) => { - if scopes.len() > 0 { + if !scopes.is_empty() { idx += 1; } diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 5df1b1389701d28477dc1fa1c435f41bd6079ccb..4a544f9ea718f0df037fb3012c48efec1c804b43 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -862,7 +862,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S 21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| { diagnostics.editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(window, cx); - if snapshot.buffer_snapshot.len() > 0 { + if !snapshot.buffer_snapshot.is_empty() { let position = rng.gen_range(0..snapshot.buffer_snapshot.len()); let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left); log::info!( diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 44c05dbc143849a444737d24f0e519b33bdf56e4..96261fdb2cd82a31b6e0787b738e514c74d7c5aa 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -21030,7 +21030,7 @@ fn assert_breakpoint( path: &Arc<Path>, expected: Vec<(u32, Breakpoint)>, ) { - if expected.len() == 0usize { + if expected.is_empty() { assert!(!breakpoints.contains_key(path), "{}", path.display()); } else { let mut breakpoint = breakpoints diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index 83ab02814ffc645107b5e54652c2e6fb0622fc79..e6c518beae3ecf3741b5f74be6087628f5231c8c 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -181,7 +181,7 @@ pub(crate) fn generate_auto_close_edits( */ { let tag_node_name_equals = |node: &Node, name: &str| { - let is_empty = name.len() == 0; + let is_empty = name.is_empty(); if let Some(node_name) = node.named_child(TS_NODE_TAG_NAME_CHILD_INDEX) { let range = node_name.byte_range(); return buffer.text_for_range(range).equals_str(name); @@ -207,7 +207,7 @@ pub(crate) fn generate_auto_close_edits( cur = descendant; } - assert!(ancestors.len() > 0); + assert!(!ancestors.is_empty()); let mut tree_root_node = open_tag; diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 88721c59e707734f05a74c019d247c4fefff1efe..8c54c265edf7a19af9d17e982a5f4cb6a0079cc3 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -420,7 +420,7 @@ impl EditorTestContext { if expected_text == "[FOLDED]\n" { assert!(is_folded, "excerpt {} should be folded", ix); let is_selected = selections.iter().any(|s| s.head().excerpt_id == excerpt_id); - if expected_selections.len() > 0 { + if !expected_selections.is_empty() { assert!( is_selected, "excerpt {ix} should be selected. got {:?}", diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index cc947bcb72ff004e9ef0b73c4d21f340c917a3b9..4ecb4a8829659ca9a25152db8d1eff529cfff2b1 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2175,7 +2175,7 @@ impl GitPanel { let worktree = if worktrees.len() == 1 { Task::ready(Some(worktrees.first().unwrap().clone())) - } else if worktrees.len() == 0 { + } else if worktrees.is_empty() { let result = window.prompt( PromptLevel::Warning, "Unable to initialize a git repository", @@ -2758,22 +2758,22 @@ impl GitPanel { } } - if conflict_entries.len() == 0 && staged_count == 1 && pending_staged_count == 0 { + if conflict_entries.is_empty() && staged_count == 1 && pending_staged_count == 0 { match pending_status_for_single_staged { Some(TargetStatus::Staged) | None => { self.single_staged_entry = single_staged_entry; } _ => {} } - } else if conflict_entries.len() == 0 && pending_staged_count == 1 { + } else if conflict_entries.is_empty() && pending_staged_count == 1 { self.single_staged_entry = last_pending_staged; } - if conflict_entries.len() == 0 && changed_entries.len() == 1 { + if conflict_entries.is_empty() && changed_entries.len() == 1 { self.single_tracked_entry = changed_entries.first().cloned(); } - if conflict_entries.len() > 0 { + if !conflict_entries.is_empty() { self.entries.push(GitListEntry::Header(GitHeaderEntry { header: Section::Conflict, })); @@ -2781,7 +2781,7 @@ impl GitPanel { .extend(conflict_entries.into_iter().map(GitListEntry::Status)); } - if changed_entries.len() > 0 { + if !changed_entries.is_empty() { if !sort_by_path { self.entries.push(GitListEntry::Header(GitHeaderEntry { header: Section::Tracked, @@ -2790,7 +2790,7 @@ impl GitPanel { self.entries .extend(changed_entries.into_iter().map(GitListEntry::Status)); } - if new_entries.len() > 0 { + if !new_entries.is_empty() { self.entries.push(GitListEntry::Header(GitHeaderEntry { header: Section::New, })); @@ -4476,7 +4476,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn Language impl Render for GitPanel { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let project = self.project.read(cx); - let has_entries = self.entries.len() > 0; + let has_entries = !self.entries.is_empty(); let room = self .workspace .upgrade() diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index c8d4151e8b91db14e7ebba5c0e028717ccbed1d5..1ac12b4cd4c032de48a2eaeb34c8505f8a07cb8f 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -577,7 +577,7 @@ pub fn into_google( top_k: None, }), safety_settings: None, - tools: (request.tools.len() > 0).then(|| { + tools: (!request.tools.is_empty()).then(|| { vec![google_ai::Tool { function_declarations: request .tools diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 45e1c7f291cdce6564be3e2493e68589bd0f8cc8..834bf2c2d2328e0c8b8d4ea211feb9bf255a0546 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -684,7 +684,7 @@ impl DapStore { let shutdown_id = parent_session.update(cx, |parent_session, _| { parent_session.remove_child_session_id(session_id); - if parent_session.child_session_ids().len() == 0 { + if parent_session.child_session_ids().is_empty() { Some(parent_session.session_id()) } else { None @@ -701,7 +701,7 @@ impl DapStore { cx.emit(DapStoreEvent::DebugClientShutdown(session_id)); cx.background_spawn(async move { - if shutdown_children.len() > 0 { + if !shutdown_children.is_empty() { let _ = join_all(shutdown_children).await; } diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 9fbdc152f385c6f603b20ba372a15f9b8ed5eccf..423c28c7100e9dc97339fe1d582b61dbb6d55f6a 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -461,7 +461,7 @@ impl PickerDelegate for TasksModalDelegate { tooltip_label_text.push_str(&resolved_task.resolved.command_label); } - if template.tags.len() > 0 { + if !template.tags.is_empty() { tooltip_label_text.push('\n'); tooltip_label_text.push_str( template diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs index 248047bb550cacf61a4c934867069e241368f046..796dad94c0329ed56c2e1c39f1cc2e2fc102fe4d 100644 --- a/crates/vim/src/digraph.rs +++ b/crates/vim/src/digraph.rs @@ -89,7 +89,7 @@ impl Vim { return; }; - if prefix.len() > 0 { + if !prefix.is_empty() { self.handle_literal_input(prefix, "", window, cx); } else { self.pop_operator(window, cx); diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index da8a3070fca1b4dbe0a82920ca91232774c8ed8a..15a54ac62f6a74e9429dee2e343ebf91054dc528 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -235,7 +235,7 @@ impl SerializedWorkspaceLocation { pub fn sorted_paths(&self) -> Arc<Vec<PathBuf>> { match self { SerializedWorkspaceLocation::Local(paths, order) => { - if order.order().len() == 0 { + if order.order().is_empty() { paths.paths().clone() } else { Arc::new( diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0f6d236c654c594d52a3650b1f3018b92154d003..958149825ac97a3ca74952404d13b73ec15ea11e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4377,7 +4377,7 @@ mod tests { } } } - if errors.len() > 0 { + if !errors.is_empty() { panic!( "Failed to build actions using {{}} as input: {:?}. Errors:\n{}", failing_names, From 699f58aeba56b10e99f789f9fb492c76fbeea81b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra <me@as-cii.com> Date: Wed, 20 Aug 2025 18:04:32 +0200 Subject: [PATCH 193/744] Capture telemetry when requesting completions in agent2 (#36600) Release Notes: - N/A --- Cargo.lock | 1 + crates/agent2/Cargo.toml | 1 + crates/agent2/src/thread.rs | 26 ++++++++++++++++++++++++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdc858ef50fd8f4a58c495698fdaadea25c706b0..342bb1058fb7883d4960de8c6099d2c7f719299b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,6 +239,7 @@ dependencies = [ "smol", "sqlez", "task", + "telemetry", "tempfile", "terminal", "text", diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index bc32a79622249e81d3c94238c60f158db8714929..2a5d879e9ecdb83037411c65647b062cebecad7e 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -58,6 +58,7 @@ settings.workspace = true smol.workspace = true sqlez.workspace = true task.workspace = true +telemetry.workspace = true terminal.workspace = true text.workspace = true ui.workspace = true diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index c7b1a08b92bb8e46772d8283b6e3d74f6b90cb14..f407ee7de5483d75862f5f57647daad8e57f865c 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1195,6 +1195,15 @@ impl Thread { let mut attempt = None; 'retry: loop { + telemetry::event!( + "Agent Thread Completion", + thread_id = this.read_with(cx, |this, _| this.id.to_string())?, + prompt_id = this.read_with(cx, |this, _| this.prompt_id.to_string())?, + model = model.telemetry_id(), + model_provider = model.provider_id().to_string(), + attempt + ); + let mut events = model.stream_completion(request.clone(), cx).await?; let mut tool_uses = FuturesUnordered::new(); while let Some(event) = events.next().await { @@ -1211,8 +1220,21 @@ impl Thread { this.update_model_request_usage(amount, limit, cx) })?; } - Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => { - this.update(cx, |this, cx| this.update_token_usage(token_usage, cx))?; + Ok(LanguageModelCompletionEvent::UsageUpdate(usage)) => { + telemetry::event!( + "Agent Thread Completion Usage Updated", + thread_id = this.read_with(cx, |this, _| this.id.to_string())?, + prompt_id = this.read_with(cx, |this, _| this.prompt_id.to_string())?, + model = model.telemetry_id(), + model_provider = model.provider_id().to_string(), + attempt, + input_tokens = usage.input_tokens, + output_tokens = usage.output_tokens, + cache_creation_input_tokens = usage.cache_creation_input_tokens, + cache_read_input_tokens = usage.cache_read_input_tokens, + ); + + this.update(cx, |this, cx| this.update_token_usage(usage, cx))?; } Ok(LanguageModelCompletionEvent::Stop(StopReason::Refusal)) => { *refusal = true; From d0fb6120d9583fd46b17aed9d2b9a5b08e302f7e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra <me@as-cii.com> Date: Wed, 20 Aug 2025 18:39:46 +0200 Subject: [PATCH 194/744] Fix scrollbar flicker when streaming agent2 response (#36606) This was caused by calling `list_state.splice` on updated entries. We don't need to splice the entry, as we'll recompute its measurements automatically when we render it. Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 9bb5953eafedc47712339197ecc791ab473f8034..87fe133bbaf77abff38e26f7e2fb14e269a35795 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -793,7 +793,6 @@ impl AcpThreadView { self.entry_view_state.update(cx, |view_state, cx| { view_state.sync_entry(*index, thread, window, cx) }); - self.list_state.splice(*index..index + 1, 1); } AcpThreadEvent::EntriesRemoved(range) => { self.entry_view_state From 8334cdb35805ca00c574daa623f62dc1867adb67 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Wed, 20 Aug 2025 19:10:43 +0200 Subject: [PATCH 195/744] agent2: Port feedback (#36603) Release Notes: - N/A --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com> --- crates/acp_thread/src/connection.rs | 17 ++ crates/agent/src/thread.rs | 53 ----- crates/agent2/src/agent.rs | 25 +++ crates/agent_ui/src/acp/thread_view.rs | 283 ++++++++++++++++++++++++- crates/agent_ui/src/active_thread.rs | 1 - 5 files changed, 321 insertions(+), 58 deletions(-) diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 8cae975ce553b185f2a9c7d4d69d568e9c28673a..dc1a41c81eb0dee6fd62318e568e6f3fa2e10eac 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -64,6 +64,10 @@ pub trait AgentConnection { None } + fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> { + None + } + fn into_any(self: Rc<Self>) -> Rc<dyn Any>; } @@ -81,6 +85,19 @@ pub trait AgentSessionResume { fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>; } +pub trait AgentTelemetry { + /// The name of the agent used for telemetry. + fn agent_name(&self) -> String; + + /// A representation of the current thread state that can be serialized for + /// storage with telemetry events. + fn thread_data( + &self, + session_id: &acp::SessionId, + cx: &mut App, + ) -> Task<Result<serde_json::Value>>; +} + #[derive(Debug)] pub struct AuthRequired { pub description: Option<String>, diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index a584fba88169e8b385678643db66b26820428c30..7b70fde56ab1e7acb6705aeace82f142dc28a9f3 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -387,7 +387,6 @@ pub struct Thread { cumulative_token_usage: TokenUsage, exceeded_window_error: Option<ExceededWindowError>, tool_use_limit_reached: bool, - feedback: Option<ThreadFeedback>, retry_state: Option<RetryState>, message_feedback: HashMap<MessageId, ThreadFeedback>, last_received_chunk_at: Option<Instant>, @@ -487,7 +486,6 @@ impl Thread { cumulative_token_usage: TokenUsage::default(), exceeded_window_error: None, tool_use_limit_reached: false, - feedback: None, retry_state: None, message_feedback: HashMap::default(), last_error_context: None, @@ -612,7 +610,6 @@ impl Thread { cumulative_token_usage: serialized.cumulative_token_usage, exceeded_window_error: None, tool_use_limit_reached: serialized.tool_use_limit_reached, - feedback: None, message_feedback: HashMap::default(), last_error_context: None, last_received_chunk_at: None, @@ -2787,10 +2784,6 @@ impl Thread { cx.emit(ThreadEvent::CancelEditing); } - pub fn feedback(&self) -> Option<ThreadFeedback> { - self.feedback - } - pub fn message_feedback(&self, message_id: MessageId) -> Option<ThreadFeedback> { self.message_feedback.get(&message_id).copied() } @@ -2852,52 +2845,6 @@ impl Thread { }) } - pub fn report_feedback( - &mut self, - feedback: ThreadFeedback, - cx: &mut Context<Self>, - ) -> Task<Result<()>> { - let last_assistant_message_id = self - .messages - .iter() - .rev() - .find(|msg| msg.role == Role::Assistant) - .map(|msg| msg.id); - - if let Some(message_id) = last_assistant_message_id { - self.report_message_feedback(message_id, feedback, cx) - } else { - let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx); - let serialized_thread = self.serialize(cx); - let thread_id = self.id().clone(); - let client = self.project.read(cx).client(); - self.feedback = Some(feedback); - cx.notify(); - - cx.background_spawn(async move { - let final_project_snapshot = final_project_snapshot.await; - let serialized_thread = serialized_thread.await?; - let thread_data = serde_json::to_value(serialized_thread) - .unwrap_or_else(|_| serde_json::Value::Null); - - let rating = match feedback { - ThreadFeedback::Positive => "positive", - ThreadFeedback::Negative => "negative", - }; - telemetry::event!( - "Assistant Thread Rated", - rating, - thread_id, - thread_data, - final_project_snapshot - ); - client.telemetry().flush_events().await; - - Ok(()) - }) - } - } - /// Create a snapshot of the current project state including git information and unsaved buffers. fn project_snapshot( project: Entity<Project>, diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 1fa307511fd35f55a9aef9cc82c59b1c8e8430b5..2f5f15399ee6c0805a8ce179a2f308a49f479c4d 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -948,11 +948,36 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }) } + fn telemetry(&self) -> Option<Rc<dyn acp_thread::AgentTelemetry>> { + Some(Rc::new(self.clone()) as Rc<dyn acp_thread::AgentTelemetry>) + } + fn into_any(self: Rc<Self>) -> Rc<dyn Any> { self } } +impl acp_thread::AgentTelemetry for NativeAgentConnection { + fn agent_name(&self) -> String { + "Zed".into() + } + + fn thread_data( + &self, + session_id: &acp::SessionId, + cx: &mut App, + ) -> Task<Result<serde_json::Value>> { + let Some(session) = self.0.read(cx).sessions.get(session_id) else { + return Task::ready(Err(anyhow!("Session not found"))); + }; + + let task = session.thread.read(cx).to_db(cx); + cx.background_spawn(async move { + serde_json::to_value(task.await).context("Failed to serialize thread") + }) + } +} + struct NativeAgentSessionEditor { thread: Entity<Thread>, acp_thread: WeakEntity<AcpThread>, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 87fe133bbaf77abff38e26f7e2fb14e269a35795..4ce55cce5657c4eee84953f84108e1326469f1c9 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -65,6 +65,12 @@ const RESPONSE_PADDING_X: Pixels = px(19.); pub const MIN_EDITOR_LINES: usize = 4; pub const MAX_EDITOR_LINES: usize = 8; +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum ThreadFeedback { + Positive, + Negative, +} + enum ThreadError { PaymentRequired, ModelRequestLimitReached(cloud_llm_client::Plan), @@ -106,6 +112,128 @@ impl ProfileProvider for Entity<agent2::Thread> { } } +#[derive(Default)] +struct ThreadFeedbackState { + feedback: Option<ThreadFeedback>, + comments_editor: Option<Entity<Editor>>, +} + +impl ThreadFeedbackState { + pub fn submit( + &mut self, + thread: Entity<AcpThread>, + feedback: ThreadFeedback, + window: &mut Window, + cx: &mut App, + ) { + let Some(telemetry) = thread.read(cx).connection().telemetry() else { + return; + }; + + if self.feedback == Some(feedback) { + return; + } + + self.feedback = Some(feedback); + match feedback { + ThreadFeedback::Positive => { + self.comments_editor = None; + } + ThreadFeedback::Negative => { + self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx)); + } + } + let session_id = thread.read(cx).session_id().clone(); + let agent_name = telemetry.agent_name(); + let task = telemetry.thread_data(&session_id, cx); + let rating = match feedback { + ThreadFeedback::Positive => "positive", + ThreadFeedback::Negative => "negative", + }; + cx.background_spawn(async move { + let thread = task.await?; + telemetry::event!( + "Agent Thread Rated", + session_id = session_id, + rating = rating, + agent = agent_name, + thread = thread + ); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + pub fn submit_comments(&mut self, thread: Entity<AcpThread>, cx: &mut App) { + let Some(telemetry) = thread.read(cx).connection().telemetry() else { + return; + }; + + let Some(comments) = self + .comments_editor + .as_ref() + .map(|editor| editor.read(cx).text(cx)) + .filter(|text| !text.trim().is_empty()) + else { + return; + }; + + self.comments_editor.take(); + + let session_id = thread.read(cx).session_id().clone(); + let agent_name = telemetry.agent_name(); + let task = telemetry.thread_data(&session_id, cx); + cx.background_spawn(async move { + let thread = task.await?; + telemetry::event!( + "Agent Thread Feedback Comments", + session_id = session_id, + comments = comments, + agent = agent_name, + thread = thread + ); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + pub fn clear(&mut self) { + *self = Self::default() + } + + pub fn dismiss_comments(&mut self) { + self.comments_editor.take(); + } + + fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity<Editor> { + let buffer = cx.new(|cx| { + let empty_string = String::new(); + MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx) + }); + + let editor = cx.new(|cx| { + let mut editor = Editor::new( + editor::EditorMode::AutoHeight { + min_lines: 1, + max_lines: Some(4), + }, + buffer, + None, + window, + cx, + ); + editor.set_placeholder_text( + "What went wrong? Share your feedback so we can improve.", + cx, + ); + editor + }); + + editor.read(cx).focus_handle(cx).focus(window); + editor + } +} + pub struct AcpThreadView { agent: Rc<dyn AgentServer>, workspace: WeakEntity<Workspace>, @@ -120,6 +248,7 @@ pub struct AcpThreadView { notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>, thread_retry_status: Option<RetryStatus>, thread_error: Option<ThreadError>, + thread_feedback: ThreadFeedbackState, list_state: ListState, scrollbar_state: ScrollbarState, auth_task: Option<Task<()>>, @@ -218,6 +347,7 @@ impl AcpThreadView { scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), thread_retry_status: None, thread_error: None, + thread_feedback: Default::default(), auth_task: None, expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), @@ -615,6 +745,7 @@ impl AcpThreadView { ) { self.thread_error.take(); self.editing_message.take(); + self.thread_feedback.clear(); let Some(thread) = self.thread().cloned() else { return; @@ -1087,6 +1218,12 @@ impl AcpThreadView { .w_full() .child(primary) .child(self.render_thread_controls(cx)) + .when_some( + self.thread_feedback.comments_editor.clone(), + |this, editor| { + this.child(Self::render_feedback_feedback_editor(editor, window, cx)) + }, + ) .into_any_element() } else { primary @@ -3556,7 +3693,9 @@ impl AcpThreadView { this.scroll_to_top(cx); })); - h_flex() + let mut container = h_flex() + .id("thread-controls-container") + .group("thread-controls-container") .w_full() .mr_1() .pb_2() @@ -3564,9 +3703,145 @@ impl AcpThreadView { .opacity(0.4) .hover(|style| style.opacity(1.)) .flex_wrap() - .justify_end() - .child(open_as_markdown) - .child(scroll_to_top) + .justify_end(); + + if AgentSettings::get_global(cx).enable_feedback { + let feedback = self.thread_feedback.feedback; + container = container.child( + div().visible_on_hover("thread-controls-container").child( + Label::new( + match feedback { + Some(ThreadFeedback::Positive) => "Thanks for your feedback!", + Some(ThreadFeedback::Negative) => "We appreciate your feedback and will use it to improve.", + None => "Rating the thread sends all of your current conversation to the Zed team.", + } + ) + .color(Color::Muted) + .size(LabelSize::XSmall) + .truncate(), + ), + ).child( + h_flex() + .child( + IconButton::new("feedback-thumbs-up", IconName::ThumbsUp) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(match feedback { + Some(ThreadFeedback::Positive) => Color::Accent, + _ => Color::Ignored, + }) + .tooltip(Tooltip::text("Helpful Response")) + .on_click(cx.listener(move |this, _, window, cx| { + this.handle_feedback_click( + ThreadFeedback::Positive, + window, + cx, + ); + })), + ) + .child( + IconButton::new("feedback-thumbs-down", IconName::ThumbsDown) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(match feedback { + Some(ThreadFeedback::Negative) => Color::Accent, + _ => Color::Ignored, + }) + .tooltip(Tooltip::text("Not Helpful")) + .on_click(cx.listener(move |this, _, window, cx| { + this.handle_feedback_click( + ThreadFeedback::Negative, + window, + cx, + ); + })), + ) + ) + } + + container.child(open_as_markdown).child(scroll_to_top) + } + + fn render_feedback_feedback_editor( + editor: Entity<Editor>, + window: &mut Window, + cx: &Context<Self>, + ) -> Div { + let focus_handle = editor.focus_handle(cx); + v_flex() + .key_context("AgentFeedbackMessageEditor") + .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| { + this.thread_feedback.dismiss_comments(); + cx.notify(); + })) + .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| { + this.submit_feedback_message(cx); + })) + .mb_2() + .mx_4() + .p_2() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_background) + .child(editor) + .child( + h_flex() + .gap_1() + .justify_end() + .child( + Button::new("dismiss-feedback-message", "Cancel") + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .on_click(cx.listener(move |this, _, _window, cx| { + this.thread_feedback.dismiss_comments(); + cx.notify(); + })), + ) + .child( + Button::new("submit-feedback-message", "Share Feedback") + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &menu::Confirm, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .on_click(cx.listener(move |this, _, _window, cx| { + this.submit_feedback_message(cx); + })), + ), + ) + } + + fn handle_feedback_click( + &mut self, + feedback: ThreadFeedback, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let Some(thread) = self.thread().cloned() else { + return; + }; + + self.thread_feedback.submit(thread, feedback, window, cx); + cx.notify(); + } + + fn submit_feedback_message(&mut self, cx: &mut Context<Self>) { + let Some(thread) = self.thread().cloned() else { + return; + }; + + self.thread_feedback.submit_comments(thread, cx); + cx.notify(); } fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> { diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index e214986b82b973905307c23319144f4544c36c9c..2cad9132950787b9e5404e638275fb92147db5a7 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -2349,7 +2349,6 @@ impl ActiveThread { this.submit_feedback_message(message_id, cx); cx.notify(); })) - .on_action(cx.listener(Self::confirm_editing_message)) .mb_2() .mx_4() .p_2() From 41e28a71855c9e5595d3764423e56517e5315931 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Wed, 20 Aug 2025 14:01:18 -0400 Subject: [PATCH 196/744] Add tracked buffers for agent2 mentions (#36608) Release Notes: - N/A --- crates/agent_ui/src/acp/message_editor.rs | 151 ++++++++++++++-------- crates/agent_ui/src/acp/thread_view.rs | 13 +- 2 files changed, 107 insertions(+), 57 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index a50e33dc312d6a2892a55065a929c8c068d0a55c..ccd33c9247ce1b686bbea41e70dcde0039b49df2 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -34,7 +34,7 @@ use settings::Settings; use std::{ cell::Cell, ffi::OsStr, - fmt::{Display, Write}, + fmt::Write, ops::Range, path::{Path, PathBuf}, rc::Rc, @@ -391,30 +391,33 @@ impl MessageEditor { let rope = buffer .read_with(cx, |buffer, _cx| buffer.as_rope().clone()) .log_err()?; - Some(rope) + Some((rope, buffer)) }); cx.background_spawn(async move { - let rope = rope_task.await?; - Some((rel_path, full_path, rope.to_string())) + let (rope, buffer) = rope_task.await?; + Some((rel_path, full_path, rope.to_string(), buffer)) }) })) })?; let contents = cx .background_spawn(async move { - let contents = descendants_future.await.into_iter().flatten(); - contents.collect() + let (contents, tracked_buffers) = descendants_future + .await + .into_iter() + .flatten() + .map(|(rel_path, full_path, rope, buffer)| { + ((rel_path, full_path, rope), buffer) + }) + .unzip(); + (render_directory_contents(contents), tracked_buffers) }) .await; anyhow::Ok(contents) }); let task = cx - .spawn(async move |_, _| { - task.await - .map(|contents| DirectoryContents(contents).to_string()) - .map_err(|e| e.to_string()) - }) + .spawn(async move |_, _| task.await.map_err(|e| e.to_string())) .shared(); self.mention_set @@ -663,7 +666,7 @@ impl MessageEditor { &self, window: &mut Window, cx: &mut Context<Self>, - ) -> Task<Result<Vec<acp::ContentBlock>>> { + ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> { let contents = self.mention_set .contents(&self.project, self.prompt_store.as_ref(), window, cx); @@ -672,6 +675,7 @@ impl MessageEditor { cx.spawn(async move |_, cx| { let contents = contents.await?; + let mut all_tracked_buffers = Vec::new(); editor.update(cx, |editor, cx| { let mut ix = 0; @@ -702,7 +706,12 @@ impl MessageEditor { chunks.push(chunk); } let chunk = match mention { - Mention::Text { uri, content } => { + Mention::Text { + uri, + content, + tracked_buffers, + } => { + all_tracked_buffers.extend(tracked_buffers.iter().cloned()); acp::ContentBlock::Resource(acp::EmbeddedResource { annotations: None, resource: acp::EmbeddedResourceResource::TextResourceContents( @@ -745,7 +754,7 @@ impl MessageEditor { } }); - chunks + (chunks, all_tracked_buffers) }) }) } @@ -1043,7 +1052,7 @@ impl MessageEditor { .add_fetch_result(url, Task::ready(Ok(text)).shared()); } MentionUri::Directory { abs_path } => { - let task = Task::ready(Ok(text)).shared(); + let task = Task::ready(Ok((text, Vec::new()))).shared(); self.mention_set.directories.insert(abs_path, task); } MentionUri::File { .. } @@ -1153,16 +1162,13 @@ impl MessageEditor { } } -struct DirectoryContents(Arc<[(Arc<Path>, PathBuf, String)]>); - -impl Display for DirectoryContents { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for (_relative_path, full_path, content) in self.0.iter() { - let fence = codeblock_fence_for_path(Some(full_path), None); - write!(f, "\n{fence}\n{content}\n```")?; - } - Ok(()) +fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String { + let mut output = String::new(); + for (_relative_path, full_path, content) in entries { + let fence = codeblock_fence_for_path(Some(&full_path), None); + write!(output, "\n{fence}\n{content}\n```").unwrap(); } + output } impl Focusable for MessageEditor { @@ -1328,7 +1334,11 @@ impl Render for ImageHover { #[derive(Debug, Eq, PartialEq)] pub enum Mention { - Text { uri: MentionUri, content: String }, + Text { + uri: MentionUri, + content: String, + tracked_buffers: Vec<Entity<Buffer>>, + }, Image(MentionImage), } @@ -1346,7 +1356,7 @@ pub struct MentionSet { images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>, thread_summaries: HashMap<acp::SessionId, Shared<Task<Result<SharedString, String>>>>, text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>, - directories: HashMap<PathBuf, Shared<Task<Result<String, String>>>>, + directories: HashMap<PathBuf, Shared<Task<Result<(String, Vec<Entity<Buffer>>), String>>>>, } impl MentionSet { @@ -1382,6 +1392,7 @@ impl MentionSet { self.fetch_results.clear(); self.thread_summaries.clear(); self.text_thread_summaries.clear(); + self.directories.clear(); self.uri_by_crease_id .drain() .map(|(id, _)| id) @@ -1424,7 +1435,14 @@ impl MentionSet { let buffer = buffer_task?.await?; let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; - anyhow::Ok((crease_id, Mention::Text { uri, content })) + anyhow::Ok(( + crease_id, + Mention::Text { + uri, + content, + tracked_buffers: vec![buffer], + }, + )) }) } MentionUri::Directory { abs_path } => { @@ -1433,11 +1451,14 @@ impl MentionSet { }; let uri = uri.clone(); cx.spawn(async move |_| { + let (content, tracked_buffers) = + content.await.map_err(|e| anyhow::anyhow!("{e}"))?; Ok(( crease_id, Mention::Text { uri, - content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, + content, + tracked_buffers, }, )) }) @@ -1473,7 +1494,14 @@ impl MentionSet { .collect() })?; - anyhow::Ok((crease_id, Mention::Text { uri, content })) + anyhow::Ok(( + crease_id, + Mention::Text { + uri, + content, + tracked_buffers: vec![buffer], + }, + )) }) } MentionUri::Thread { id, .. } => { @@ -1490,6 +1518,7 @@ impl MentionSet { .await .map_err(|e| anyhow::anyhow!("{e}"))? .to_string(), + tracked_buffers: Vec::new(), }, )) }) @@ -1505,6 +1534,7 @@ impl MentionSet { Mention::Text { uri, content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, + tracked_buffers: Vec::new(), }, )) }) @@ -1518,7 +1548,14 @@ impl MentionSet { cx.spawn(async move |_| { // TODO: report load errors instead of just logging let text = text_task.await?; - anyhow::Ok((crease_id, Mention::Text { uri, content: text })) + anyhow::Ok(( + crease_id, + Mention::Text { + uri, + content: text, + tracked_buffers: Vec::new(), + }, + )) }) } MentionUri::Fetch { url } => { @@ -1532,6 +1569,7 @@ impl MentionSet { Mention::Text { uri, content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, + tracked_buffers: Vec::new(), }, )) }) @@ -1703,6 +1741,7 @@ impl Addon for MessageEditorAddon { mod tests { use std::{ops::Range, path::Path, sync::Arc}; + use acp_thread::MentionUri; use agent_client_protocol as acp; use agent2::HistoryStore; use assistant_context::ContextStore; @@ -1815,7 +1854,7 @@ mod tests { editor.backspace(&Default::default(), window, cx); }); - let content = message_editor + let (content, _) = message_editor .update_in(cx, |message_editor, window, cx| { message_editor.contents(window, cx) }) @@ -2046,13 +2085,13 @@ mod tests { .into_values() .collect::<Vec<_>>(); - pretty_assertions::assert_eq!( - contents, - [Mention::Text { - content: "1".into(), - uri: url_one.parse().unwrap() - }] - ); + { + let [Mention::Text { content, uri, .. }] = contents.as_slice() else { + panic!("Unexpected mentions"); + }; + pretty_assertions::assert_eq!(content, "1"); + pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap()); + } cx.simulate_input(" "); @@ -2098,15 +2137,15 @@ mod tests { .into_values() .collect::<Vec<_>>(); - assert_eq!(contents.len(), 2); let url_eight = uri!("file:///dir/b/eight.txt"); - pretty_assertions::assert_eq!( - contents[1], - Mention::Text { - content: "8".to_string(), - uri: url_eight.parse().unwrap(), - } - ); + + { + let [_, Mention::Text { content, uri, .. }] = contents.as_slice() else { + panic!("Unexpected mentions"); + }; + pretty_assertions::assert_eq!(content, "8"); + pretty_assertions::assert_eq!(uri, &url_eight.parse::<MentionUri>().unwrap()); + } editor.update(&mut cx, |editor, cx| { assert_eq!( @@ -2208,14 +2247,18 @@ mod tests { .into_values() .collect::<Vec<_>>(); - assert_eq!(contents.len(), 3); - pretty_assertions::assert_eq!( - contents[2], - Mention::Text { - content: "1".into(), - uri: format!("{url_one}?symbol=MySymbol#L1:1").parse().unwrap(), - } - ); + { + let [_, _, Mention::Text { content, uri, .. }] = contents.as_slice() else { + panic!("Unexpected mentions"); + }; + pretty_assertions::assert_eq!(content, "1"); + pretty_assertions::assert_eq!( + uri, + &format!("{url_one}?symbol=MySymbol#L1:1") + .parse::<MentionUri>() + .unwrap() + ); + } cx.run_until_parked(); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 4ce55cce5657c4eee84953f84108e1326469f1c9..14f9cacd154f4bcd127a8a3a2714313421bf7b49 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -739,7 +739,7 @@ impl AcpThreadView { fn send_impl( &mut self, - contents: Task<anyhow::Result<Vec<acp::ContentBlock>>>, + contents: Task<anyhow::Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>, window: &mut Window, cx: &mut Context<Self>, ) { @@ -751,7 +751,7 @@ impl AcpThreadView { return; }; let task = cx.spawn_in(window, async move |this, cx| { - let contents = contents.await?; + let (contents, tracked_buffers) = contents.await?; if contents.is_empty() { return Ok(()); @@ -764,7 +764,14 @@ impl AcpThreadView { message_editor.clear(window, cx); }); })?; - let send = thread.update(cx, |thread, cx| thread.send(contents, cx))?; + let send = thread.update(cx, |thread, cx| { + thread.action_log().update(cx, |action_log, cx| { + for buffer in tracked_buffers { + action_log.buffer_read(buffer, cx) + } + }); + thread.send(contents, cx) + })?; send.await }); From ec8106d1dbe8937a0b0cf7c9250b1491c22c1338 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 20 Aug 2025 23:44:30 +0530 Subject: [PATCH 197/744] Fix `clippy::println_empty_string`, `clippy::while_let_on_iterator`, `clippy::while_let_on_iterator` lint style violations (#36613) Related: #36577 Release Notes: - N/A --- Cargo.toml | 3 +++ crates/agent/src/context.rs | 6 +++--- crates/agent2/src/thread.rs | 2 +- crates/buffer_diff/src/buffer_diff.rs | 4 ++-- crates/editor/src/display_map/block_map.rs | 4 ++-- crates/editor/src/editor.rs | 4 ++-- crates/editor/src/indent_guides.rs | 4 ++-- crates/editor/src/items.rs | 4 ++-- crates/eval/src/eval.rs | 2 +- crates/eval/src/instance.rs | 4 ++-- crates/git/src/repository.rs | 2 +- crates/language/src/text_diff.rs | 2 +- crates/multi_buffer/src/multi_buffer_tests.rs | 4 ++-- crates/project/src/git_store/git_traversal.rs | 4 ++-- crates/project/src/lsp_store.rs | 4 ++-- crates/settings/src/settings_json.rs | 2 +- crates/tab_switcher/src/tab_switcher.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 4 ++-- crates/vim/src/command.rs | 2 +- crates/vim/src/normal/increment.rs | 4 ++-- 20 files changed, 35 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a2de4aaaed97e011a57180c0fa22ef9a460960ef..6218e8dbb9d77cb096746a69eaf110ed37ce7777 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -904,6 +904,7 @@ ok_expect = "warn" owned_cow = "warn" print_literal = "warn" print_with_newline = "warn" +println_empty_string = "warn" ptr_eq = "warn" question_mark = "warn" redundant_closure = "warn" @@ -924,7 +925,9 @@ unneeded_struct_pattern = "warn" unsafe_removed_from_name = "warn" unused_unit = "warn" unusual_byte_groupings = "warn" +while_let_on_iterator = "warn" write_literal = "warn" +write_with_newline = "warn" writeln_empty_string = "warn" wrong_self_convention = "warn" zero_ptr = "warn" diff --git a/crates/agent/src/context.rs b/crates/agent/src/context.rs index 9bb8fc0eaef2126687f5e3277016469801af682c..a94a933d864d204037b3d575cbdc4d85870aeddb 100644 --- a/crates/agent/src/context.rs +++ b/crates/agent/src/context.rs @@ -362,7 +362,7 @@ impl Display for DirectoryContext { let mut is_first = true; for descendant in &self.descendants { if !is_first { - write!(f, "\n")?; + writeln!(f)?; } else { is_first = false; } @@ -650,7 +650,7 @@ impl TextThreadContextHandle { impl Display for TextThreadContext { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { // TODO: escape title? - write!(f, "<text_thread title=\"{}\">\n", self.title)?; + writeln!(f, "<text_thread title=\"{}\">", self.title)?; write!(f, "{}", self.text.trim())?; write!(f, "\n</text_thread>") } @@ -716,7 +716,7 @@ impl RulesContextHandle { impl Display for RulesContext { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(title) = &self.title { - write!(f, "Rules title: {}\n", title)?; + writeln!(f, "Rules title: {}", title)?; } let code_block = MarkdownCodeBlock { tag: "", diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index f407ee7de5483d75862f5f57647daad8e57f865c..01c9ab03ba42fe661e148ba2ae1406d2e003d851 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -163,7 +163,7 @@ impl UserMessage { if !content.is_empty() { let _ = write!(&mut markdown, "{}\n\n{}\n", uri.as_link(), content); } else { - let _ = write!(&mut markdown, "{}\n", uri.as_link()); + let _ = writeln!(&mut markdown, "{}", uri.as_link()); } } } diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index bef0c5cfc3093126362bcf468b9b22e3319781c1..10b59d0ba20ee72537406dc4645f8565df6361fc 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -2024,8 +2024,8 @@ mod tests { fn gen_working_copy(rng: &mut StdRng, head: &str) -> String { let mut old_lines = { let mut old_lines = Vec::new(); - let mut old_lines_iter = head.lines(); - while let Some(line) = old_lines_iter.next() { + let old_lines_iter = head.lines(); + for line in old_lines_iter { assert!(!line.ends_with("\n")); old_lines.push(line.to_owned()); } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 1e0cdc34ac6824bfc3500fdfc2b3a0905ac21dbe..e32a4e45dbfb5d929adf980fc97f338e1b445518 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -3183,9 +3183,9 @@ mod tests { // so we special case row 0 to assume a leading '\n'. // // Linehood is the birthright of strings. - let mut input_text_lines = input_text.split('\n').enumerate().peekable(); + let input_text_lines = input_text.split('\n').enumerate().peekable(); let mut block_row = 0; - while let Some((wrap_row, input_line)) = input_text_lines.next() { + for (wrap_row, input_line) in input_text_lines { let wrap_row = wrap_row as u32; let multibuffer_row = wraps_snapshot .to_point(WrapPoint::new(wrap_row, 0), Bias::Left) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2136d5f4b363c056669f807e6990aa4ffc7ef670..45a90b843b6d020e1db7b4937bcd851cb9e8e58a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11021,7 +11021,7 @@ impl Editor { let mut col = 0; let mut changed = false; - while let Some(ch) = chars.next() { + for ch in chars.by_ref() { match ch { ' ' => { reindented_line.push(' '); @@ -11077,7 +11077,7 @@ impl Editor { let mut first_non_indent_char = None; let mut changed = false; - while let Some(ch) = chars.next() { + for ch in chars.by_ref() { match ch { ' ' => { // Keep track of spaces. Append \t when we reach tab_size diff --git a/crates/editor/src/indent_guides.rs b/crates/editor/src/indent_guides.rs index a1de2b604bd51cdae7efaaf19492ade911d6156c..23717eeb158cea0f01e6a4efca6d2ff14a8fa824 100644 --- a/crates/editor/src/indent_guides.rs +++ b/crates/editor/src/indent_guides.rs @@ -164,8 +164,8 @@ pub fn indent_guides_in_range( let end_anchor = snapshot.buffer_snapshot.anchor_after(end_offset); let mut fold_ranges = Vec::<Range<Point>>::new(); - let mut folds = snapshot.folds_in_range(start_offset..end_offset).peekable(); - while let Some(fold) = folds.next() { + let folds = snapshot.folds_in_range(start_offset..end_offset).peekable(); + for fold in folds { let start = fold.range.start.to_point(&snapshot.buffer_snapshot); let end = fold.range.end.to_point(&snapshot.buffer_snapshot); if let Some(last_range) = fold_ranges.last_mut() diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 62889c638fdcd1db43e83f3d7b55a80c8e99d996..afc5767de010d67bdbe3e6fd21e1ffcfe840b801 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -103,9 +103,9 @@ impl FollowableItem for Editor { multibuffer = MultiBuffer::new(project.read(cx).capability()); let mut sorted_excerpts = state.excerpts.clone(); sorted_excerpts.sort_by_key(|e| e.id); - let mut sorted_excerpts = sorted_excerpts.into_iter().peekable(); + let sorted_excerpts = sorted_excerpts.into_iter().peekable(); - while let Some(excerpt) = sorted_excerpts.next() { + for excerpt in sorted_excerpts { let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else { continue; }; diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index c5a072eea15d8176e7141072f4ead9724b4fd61a..9e0504abca479483b4e5f49c41eec1f6ba3834f1 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -706,7 +706,7 @@ fn print_report( println!("Average thread score: {average_thread_score}%"); } - println!(""); + println!(); print_h2("CUMULATIVE TOOL METRICS"); println!("{}", cumulative_tool_metrics); diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index 074cb121d3b588e3c82735de65dc3178a6eacc80..c6e4e0b6ec683b63b90920861f3cd023069666e6 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -913,9 +913,9 @@ impl RequestMarkdown { for tool in &request.tools { write!(&mut tools, "# {}\n\n", tool.name).unwrap(); write!(&mut tools, "{}\n\n", tool.description).unwrap(); - write!( + writeln!( &mut tools, - "{}\n", + "{}", MarkdownCodeBlock { tag: "json", text: &format!("{:#}", tool.input_schema) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 9c125d2c4726fb369274d782137d384c8dfb4c0c..fd12dafa98575b5d8b0190d6ed4e894ac61e8165 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -916,7 +916,7 @@ impl GitRepository for RealGitRepository { .context("no stdin for git cat-file subprocess")?; let mut stdin = BufWriter::new(stdin); for rev in &revs { - write!(&mut stdin, "{rev}\n")?; + writeln!(&mut stdin, "{rev}")?; } stdin.flush()?; drop(stdin); diff --git a/crates/language/src/text_diff.rs b/crates/language/src/text_diff.rs index cb2242a6b1a04ce23ab2e232f1235f3680a33aa5..11d8a070d213852f0a98078f2ed8c76c9cced47b 100644 --- a/crates/language/src/text_diff.rs +++ b/crates/language/src/text_diff.rs @@ -186,7 +186,7 @@ fn tokenize(text: &str, language_scope: Option<LanguageScope>) -> impl Iterator< let mut prev = None; let mut start_ix = 0; iter::from_fn(move || { - while let Some((ix, c)) = chars.next() { + for (ix, c) in chars.by_ref() { let mut token = None; let kind = classifier.kind(c); if let Some((prev_char, prev_kind)) = prev diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 598ee0f9cba32fa13760e14092a423e56860aabe..61b4b0520f23ed50b3b36374710b52c78c37080f 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -2250,11 +2250,11 @@ impl ReferenceMultibuffer { let base_buffer = diff.base_text(); let mut offset = buffer_range.start; - let mut hunks = diff + let hunks = diff .hunks_intersecting_range(excerpt.range.clone(), buffer, cx) .peekable(); - while let Some(hunk) = hunks.next() { + for hunk in hunks { // Ignore hunks that are outside the excerpt range. let mut hunk_range = hunk.buffer_range.to_offset(buffer); diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index 9eadaeac824756bd3b128ef3e0118ceef1c05680..eee492e482daf746c60836cab172f84b2834b468 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -42,8 +42,8 @@ impl<'a> GitTraversal<'a> { // other_repo/ // .git/ // our_query.txt - let mut query = path.ancestors(); - while let Some(query) = query.next() { + let query = path.ancestors(); + for query in query { let (_, snapshot) = self .repo_root_to_snapshot .range(Path::new("")..=query) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 1b461178972b6cf9182c3da2a4fcba4595986eb9..0b58009f37204fa3383bfc73683826aa3b8d7fb3 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -13149,10 +13149,10 @@ fn ensure_uniform_list_compatible_label(label: &mut CodeLabel) { let mut offset_map = vec![0; label.text.len() + 1]; let mut last_char_was_space = false; let mut new_idx = 0; - let mut chars = label.text.char_indices().fuse(); + let chars = label.text.char_indices().fuse(); let mut newlines_removed = false; - while let Some((idx, c)) = chars.next() { + for (idx, c) in chars { offset_map[idx] = new_idx; match c { diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index 8080ec8d5f56b8f82a460dd7edcf0c14cd98c9e9..f112ec811d2828350d41eeab63161c8e345d4d77 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -209,7 +209,7 @@ fn replace_value_in_json_text( if ch == ',' { removal_end = existing_value_range.end + offset + 1; // Also consume whitespace after the comma - while let Some((_, next_ch)) = chars.next() { + for (_, next_ch) in chars.by_ref() { if next_ch.is_whitespace() { removal_end += next_ch.len_utf8(); } else { diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 12af124ec78eb4c2b1bf6915131024d34ee64c93..655b8a2e8f4542fff0a08506e8c635cbd0d7c317 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -307,7 +307,7 @@ impl TabSwitcherDelegate { (Reverse(history.get(&item.item.item_id())), item.item_index) ) } - eprintln!(""); + eprintln!(); all_items .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index)); all_items diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 0c16e3fb9d6b39dad0ca9bd083724c36161db742..5b4d32714097a4c0bb24ebe5c9dcf72acf7f5ebe 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1397,8 +1397,8 @@ fn possible_open_target( let found_entry = worktree .update(cx, |worktree, _| { let worktree_root = worktree.abs_path(); - let mut traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); - while let Some(entry) = traversal.next() { + let traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); + for entry in traversal { if let Some(path_in_worktree) = worktree_paths_to_check .iter() .find(|path_to_check| entry.path.ends_with(&path_to_check.path)) diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 79d18a85e97c22ec82e7daef2ba7c50a7d3c0990..b57c916db988f683cef6bae15ec562392a488be6 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1492,7 +1492,7 @@ impl OnMatchingLines { let mut search = String::new(); let mut escaped = false; - while let Some(c) = chars.next() { + for c in chars.by_ref() { if escaped { escaped = false; // unescape escaped parens diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 115aef1dabd16826e18cf5a39182f0eec3e670e0..1d2a4e9b6180c8130f2126053e5f54694a6d4081 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -274,9 +274,9 @@ fn find_boolean(snapshot: &MultiBufferSnapshot, start: Point) -> Option<(Range<P let mut end = None; let mut word = String::new(); - let mut chars = snapshot.chars_at(offset); + let chars = snapshot.chars_at(offset); - while let Some(ch) = chars.next() { + for ch in chars { if ch.is_ascii_alphabetic() { if begin.is_none() { begin = Some(offset); From b6722ca3c8de3921f150e83294e84fbc9bdb9016 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Wed, 20 Aug 2025 14:43:29 -0400 Subject: [PATCH 198/744] Remove special case for singleton buffers from `MultiBufferSnapshot::anchor_at` (#36524) This may be responsible for a panic that we've been seeing with increased frequency in agent2 threads. Release Notes: - N/A Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com> --- crates/editor/src/editor.rs | 8 +-- crates/multi_buffer/src/multi_buffer.rs | 66 +++++++++++++++---------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 45a90b843b6d020e1db7b4937bcd851cb9e8e58a..25fddf5cf1d44f947e7c4128e82158787cdd28f2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4876,11 +4876,7 @@ impl Editor { cx: &mut Context<Self>, ) -> bool { let position = self.selections.newest_anchor().head(); - let multibuffer = self.buffer.read(cx); - let Some(buffer) = position - .buffer_id - .and_then(|buffer_id| multibuffer.buffer(buffer_id)) - else { + let Some(buffer) = self.buffer.read(cx).buffer_for_anchor(position, cx) else { return false; }; @@ -5844,7 +5840,7 @@ impl Editor { multibuffer_anchor.start.to_offset(&snapshot) ..multibuffer_anchor.end.to_offset(&snapshot) }; - if newest_anchor.head().buffer_id != Some(buffer.remote_id()) { + if snapshot.buffer_id_for_anchor(newest_anchor.head()) != Some(buffer.remote_id()) { return None; } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 60e9c14c34437bf79ad5eaa0be60972d95ddff58..f73014a6ff161d8944741d679ab53a61da035975 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -2196,6 +2196,15 @@ impl MultiBuffer { }) } + pub fn buffer_for_anchor(&self, anchor: Anchor, cx: &App) -> Option<Entity<Buffer>> { + if let Some(buffer_id) = anchor.buffer_id { + self.buffer(buffer_id) + } else { + let (_, buffer, _) = self.excerpt_containing(anchor, cx)?; + Some(buffer) + } + } + // If point is at the end of the buffer, the last excerpt is returned pub fn point_to_buffer_offset<T: ToOffset>( &self, @@ -5228,15 +5237,6 @@ impl MultiBufferSnapshot { excerpt_offset += ExcerptOffset::new(offset_in_transform); }; - if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() { - return Anchor { - buffer_id: Some(buffer_id), - excerpt_id: *excerpt_id, - text_anchor: buffer.anchor_at(excerpt_offset.value, bias), - diff_base_anchor, - }; - } - let mut excerpts = self .excerpts .cursor::<Dimensions<ExcerptOffset, Option<ExcerptId>>>(&()); @@ -5260,10 +5260,17 @@ impl MultiBufferSnapshot { text_anchor, diff_base_anchor, } - } else if excerpt_offset.is_zero() && bias == Bias::Left { - Anchor::min() } else { - Anchor::max() + let mut anchor = if excerpt_offset.is_zero() && bias == Bias::Left { + Anchor::min() + } else { + Anchor::max() + }; + // TODO this is a hack, remove it + if let Some((excerpt_id, _, _)) = self.as_singleton() { + anchor.excerpt_id = *excerpt_id; + } + anchor } } @@ -6305,6 +6312,14 @@ impl MultiBufferSnapshot { }) } + pub fn buffer_id_for_anchor(&self, anchor: Anchor) -> Option<BufferId> { + if let Some(id) = anchor.buffer_id { + return Some(id); + } + let excerpt = self.excerpt_containing(anchor..anchor)?; + Some(excerpt.buffer_id()) + } + pub fn selections_in_range<'a>( &'a self, range: &'a Range<Anchor>, @@ -6983,19 +6998,20 @@ impl Excerpt { } fn contains(&self, anchor: &Anchor) -> bool { - Some(self.buffer_id) == anchor.buffer_id - && self - .range - .context - .start - .cmp(&anchor.text_anchor, &self.buffer) - .is_le() - && self - .range - .context - .end - .cmp(&anchor.text_anchor, &self.buffer) - .is_ge() + anchor.buffer_id == None + || anchor.buffer_id == Some(self.buffer_id) + && self + .range + .context + .start + .cmp(&anchor.text_anchor, &self.buffer) + .is_le() + && self + .range + .context + .end + .cmp(&anchor.text_anchor, &self.buffer) + .is_ge() } /// The [`Excerpt`]'s start offset in its [`Buffer`] From 74ce543d8b16c33fb418db668ae403909eed4c2e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 20 Aug 2025 20:45:40 +0200 Subject: [PATCH 199/744] clippy: println_empty_string & non_minimal_cfg (#36614) - **clippy: Fix println-empty-string** - **clippy: non-minimal-cfg** Related to #36577 Release Notes: - N/A --- Cargo.toml | 1 + crates/agent2/src/thread.rs | 2 +- crates/gpui/src/taffy.rs | 1 - .../tests/derive_inspector_reflection.rs | 14 +------------- crates/tab_switcher/src/tab_switcher.rs | 1 - 5 files changed, 3 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6218e8dbb9d77cb096746a69eaf110ed37ce7777..dcf07b707912e3323c165d5613f221818b1d78a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -900,6 +900,7 @@ needless_parens_on_range_literals = "warn" needless_pub_self = "warn" needless_return = "warn" needless_return_with_question_mark = "warn" +non_minimal_cfg = "warn" ok_expect = "warn" owned_cow = "warn" print_literal = "warn" diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 01c9ab03ba42fe661e148ba2ae1406d2e003d851..62174fd3b475cf14f9259d0a18b6a2685dba6407 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -161,7 +161,7 @@ impl UserMessage { } UserMessageContent::Mention { uri, content } => { if !content.is_empty() { - let _ = write!(&mut markdown, "{}\n\n{}\n", uri.as_link(), content); + let _ = writeln!(&mut markdown, "{}\n\n{}", uri.as_link(), content); } else { let _ = writeln!(&mut markdown, "{}", uri.as_link()); } diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index f198bb771849a0c617d3f4b4a1cf0e5ceda475f5..58386ad1f5031e1427baad05c4db075df1b2d761 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -164,7 +164,6 @@ impl TaffyLayoutEngine { // for (a, b) in self.get_edges(id)? { // println!("N{} --> N{}", u64::from(a), u64::from(b)); // } - // println!(""); // if !self.computed_layouts.insert(id) { diff --git a/crates/gpui_macros/tests/derive_inspector_reflection.rs b/crates/gpui_macros/tests/derive_inspector_reflection.rs index aab44a70ce58380f172756a21e69fb19e3eddad5..a0adcb7801e55d7272191a1e4e831b2c9c6b115c 100644 --- a/crates/gpui_macros/tests/derive_inspector_reflection.rs +++ b/crates/gpui_macros/tests/derive_inspector_reflection.rs @@ -34,13 +34,6 @@ trait Transform: Clone { /// Adds one to the value fn add_one(self) -> Self; - - /// cfg attributes are respected - #[cfg(all())] - fn cfg_included(self) -> Self; - - #[cfg(any())] - fn cfg_omitted(self) -> Self; } #[derive(Debug, Clone, PartialEq)] @@ -70,10 +63,6 @@ impl Transform for Number { fn add_one(self) -> Self { Number(self.0 + 1) } - - fn cfg_included(self) -> Self { - Number(self.0) - } } #[test] @@ -83,14 +72,13 @@ fn test_derive_inspector_reflection() { // Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self let methods = methods::<Number>(); - assert_eq!(methods.len(), 6); + assert_eq!(methods.len(), 5); let method_names: Vec<_> = methods.iter().map(|m| m.name).collect(); assert!(method_names.contains(&"double")); assert!(method_names.contains(&"triple")); assert!(method_names.contains(&"increment")); assert!(method_names.contains(&"quadruple")); assert!(method_names.contains(&"add_one")); - assert!(method_names.contains(&"cfg_included")); // Invoke methods by name let num = Number(5); diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 655b8a2e8f4542fff0a08506e8c635cbd0d7c317..11e32523b4935f464dba81471a5673549c088eda 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -307,7 +307,6 @@ impl TabSwitcherDelegate { (Reverse(history.get(&item.item.item_id())), item.item_index) ) } - eprintln!(); all_items .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index)); all_items From 2813073d7b642bc40c6a2f4188dec8445f9688ae Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Wed, 20 Aug 2025 16:04:10 -0300 Subject: [PATCH 200/744] message editor: Only allow types of content the agent can handle (#36616) Uses the new [`acp::PromptCapabilities`](https://github.com/zed-industries/agent-client-protocol/blob/a39b7f635d67528f0a4e05e086ab283b9fc5cb93/rust/agent.rs#L194-L215) to disable non-file mentions and images for agents that don't support them. Release Notes: - N/A --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 8 ++ crates/acp_thread/src/connection.rs | 10 ++ crates/agent2/src/agent.rs | 8 ++ crates/agent_servers/src/acp/v0.rs | 8 ++ crates/agent_servers/src/acp/v1.rs | 6 + crates/agent_servers/src/claude.rs | 8 ++ .../agent_ui/src/acp/completion_provider.rs | 124 ++++++++++++------ crates/agent_ui/src/acp/message_editor.rs | 93 +++++++++++-- crates/agent_ui/src/acp/thread_view.rs | 13 ++ 11 files changed, 234 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 342bb1058fb7883d4960de8c6099d2c7f719299b..70b8f630f772e804144dba52a647d24e453d2bd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.26" +version = "0.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "160971bb53ca0b2e70ebc857c21e24eb448745f1396371015f4c59e9a9e51ed0" +checksum = "4c887e795097665ab95119580534e7cc1335b59e1a7fec296943e534b970f4ed" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index dcf07b707912e3323c165d5613f221818b1d78a5..436d4a7f5c2b065621cfe7b15351847699c5b182 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -423,7 +423,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.26" +agent-client-protocol = "0.0.28" 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 a1f9b32eba6a92687c676e8f60beba2e9c0453e9..9833e1957c589f99c2051fef4b59c699f9c249f7 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -2598,6 +2598,14 @@ mod tests { } } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + } + } + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { let sessions = self.sessions.lock(); let thread = sessions.get(session_id).unwrap().clone(); diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index dc1a41c81eb0dee6fd62318e568e6f3fa2e10eac..791b16141762dc941729042063c1e5923f35b346 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -38,6 +38,8 @@ pub trait AgentConnection { cx: &mut App, ) -> Task<Result<acp::PromptResponse>>; + fn prompt_capabilities(&self) -> acp::PromptCapabilities; + fn resume( &self, _session_id: &acp::SessionId, @@ -334,6 +336,14 @@ mod test_support { Task::ready(Ok(thread)) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + } + } + fn authenticate( &self, _method_id: acp::AuthMethodId, diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 2f5f15399ee6c0805a8ce179a2f308a49f479c4d..c15048ad8c53943ef57b04ebfe90bb9a43c307f3 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -913,6 +913,14 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: false, + embedded_context: true, + } + } + fn resume( &self, session_id: &acp::SessionId, diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 30643dd00516a9a10391c70b20d984fa47738f95..be960489293f6db935d5d4cba0160ed807079c64 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -498,6 +498,14 @@ impl AgentConnection for AcpConnection { }) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: false, + audio: false, + embedded_context: false, + } + } + fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) { let task = self .connection diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index e0e92f29ba0c55ac5e7c256f9cfc29f96d68d16b..2e70a5f37a439aab03a0c14b945e7e304544d72a 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -21,6 +21,7 @@ pub struct AcpConnection { connection: Rc<acp::ClientSideConnection>, sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>, auth_methods: Vec<acp::AuthMethod>, + prompt_capabilities: acp::PromptCapabilities, _io_task: Task<Result<()>>, } @@ -119,6 +120,7 @@ impl AcpConnection { connection: connection.into(), server_name, sessions, + prompt_capabilities: response.agent_capabilities.prompt_capabilities, _io_task: io_task, }) } @@ -206,6 +208,10 @@ impl AgentConnection for AcpConnection { }) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + self.prompt_capabilities + } + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { let conn = self.connection.clone(); let params = acp::CancelNotification { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 6b9732b468f334a31b35a14847908d2bad2192d6..8d93557e1ca28531ad86df3767ed936aeed50b45 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -319,6 +319,14 @@ impl AgentConnection for ClaudeAgentConnection { cx.foreground_executor().spawn(async move { end_rx.await? }) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: false, + embedded_context: true, + } + } + fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { let sessions = self.sessions.borrow(); let Some(session) = sessions.get(session_id) else { diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index d90520d26acf0a8ce14605dc43a12a72816fd133..bf0a3f7a5ab520b01af302820fa3b29b894041bf 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,8 +1,11 @@ +use std::cell::Cell; use std::ops::Range; +use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::AtomicBool; use acp_thread::MentionUri; +use agent_client_protocol as acp; use agent2::{HistoryEntry, HistoryStore}; use anyhow::Result; use editor::{CompletionProvider, Editor, ExcerptId}; @@ -63,6 +66,7 @@ pub struct ContextPickerCompletionProvider { workspace: WeakEntity<Workspace>, history_store: Entity<HistoryStore>, prompt_store: Option<Entity<PromptStore>>, + prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>, } impl ContextPickerCompletionProvider { @@ -71,12 +75,14 @@ impl ContextPickerCompletionProvider { workspace: WeakEntity<Workspace>, history_store: Entity<HistoryStore>, prompt_store: Option<Entity<PromptStore>>, + prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>, ) -> Self { Self { message_editor, workspace, history_store, prompt_store, + prompt_capabilities, } } @@ -544,17 +550,19 @@ impl ContextPickerCompletionProvider { }), ); - const RECENT_COUNT: usize = 2; - let threads = self - .history_store - .read(cx) - .recently_opened_entries(cx) - .into_iter() - .filter(|thread| !mentions.contains(&thread.mention_uri())) - .take(RECENT_COUNT) - .collect::<Vec<_>>(); - - recent.extend(threads.into_iter().map(Match::RecentThread)); + if self.prompt_capabilities.get().embedded_context { + const RECENT_COUNT: usize = 2; + let threads = self + .history_store + .read(cx) + .recently_opened_entries(cx) + .into_iter() + .filter(|thread| !mentions.contains(&thread.mention_uri())) + .take(RECENT_COUNT) + .collect::<Vec<_>>(); + + recent.extend(threads.into_iter().map(Match::RecentThread)); + } recent } @@ -564,11 +572,17 @@ impl ContextPickerCompletionProvider { workspace: &Entity<Workspace>, cx: &mut App, ) -> Vec<ContextPickerEntry> { - let mut entries = vec![ - ContextPickerEntry::Mode(ContextPickerMode::File), - ContextPickerEntry::Mode(ContextPickerMode::Symbol), - ContextPickerEntry::Mode(ContextPickerMode::Thread), - ]; + let embedded_context = self.prompt_capabilities.get().embedded_context; + let mut entries = if embedded_context { + vec![ + ContextPickerEntry::Mode(ContextPickerMode::File), + ContextPickerEntry::Mode(ContextPickerMode::Symbol), + ContextPickerEntry::Mode(ContextPickerMode::Thread), + ] + } else { + // File is always available, but we don't need a mode entry + vec![] + }; let has_selection = workspace .read(cx) @@ -583,11 +597,13 @@ impl ContextPickerCompletionProvider { )); } - if self.prompt_store.is_some() { - entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules)); - } + if embedded_context { + if self.prompt_store.is_some() { + entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules)); + } - entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch)); + entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch)); + } entries } @@ -625,7 +641,11 @@ impl CompletionProvider for ContextPickerCompletionProvider { let offset_to_line = buffer.point_to_offset(line_start); let mut lines = buffer.text_for_range(line_start..position).lines(); let line = lines.next()?; - MentionCompletion::try_parse(line, offset_to_line) + MentionCompletion::try_parse( + self.prompt_capabilities.get().embedded_context, + line, + offset_to_line, + ) }); let Some(state) = state else { return Task::ready(Ok(Vec::new())); @@ -745,12 +765,16 @@ impl CompletionProvider for ContextPickerCompletionProvider { let offset_to_line = buffer.point_to_offset(line_start); let mut lines = buffer.text_for_range(line_start..position).lines(); if let Some(line) = lines.next() { - MentionCompletion::try_parse(line, offset_to_line) - .map(|completion| { - completion.source_range.start <= offset_to_line + position.column as usize - && completion.source_range.end >= offset_to_line + position.column as usize - }) - .unwrap_or(false) + MentionCompletion::try_parse( + self.prompt_capabilities.get().embedded_context, + line, + offset_to_line, + ) + .map(|completion| { + completion.source_range.start <= offset_to_line + position.column as usize + && completion.source_range.end >= offset_to_line + position.column as usize + }) + .unwrap_or(false) } else { false } @@ -841,7 +865,7 @@ struct MentionCompletion { } impl MentionCompletion { - fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> { + fn try_parse(allow_non_file_mentions: bool, line: &str, offset_to_line: usize) -> Option<Self> { let last_mention_start = line.rfind('@')?; if last_mention_start >= line.len() { return Some(Self::default()); @@ -865,7 +889,9 @@ impl MentionCompletion { if let Some(mode_text) = parts.next() { end += mode_text.len(); - if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() { + if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() + && (allow_non_file_mentions || matches!(parsed_mode, ContextPickerMode::File)) + { mode = Some(parsed_mode); } else { argument = Some(mode_text.to_string()); @@ -898,10 +924,10 @@ mod tests { #[test] fn test_mention_completion_parse() { - assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None); + assert_eq!(MentionCompletion::try_parse(true, "Lorem Ipsum", 0), None); assert_eq!( - MentionCompletion::try_parse("Lorem @", 0), + MentionCompletion::try_parse(true, "Lorem @", 0), Some(MentionCompletion { source_range: 6..7, mode: None, @@ -910,7 +936,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file", 0), + MentionCompletion::try_parse(true, "Lorem @file", 0), Some(MentionCompletion { source_range: 6..11, mode: Some(ContextPickerMode::File), @@ -919,7 +945,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file ", 0), + MentionCompletion::try_parse(true, "Lorem @file ", 0), Some(MentionCompletion { source_range: 6..12, mode: Some(ContextPickerMode::File), @@ -928,7 +954,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file main.rs", 0), + MentionCompletion::try_parse(true, "Lorem @file main.rs", 0), Some(MentionCompletion { source_range: 6..19, mode: Some(ContextPickerMode::File), @@ -937,7 +963,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file main.rs ", 0), + MentionCompletion::try_parse(true, "Lorem @file main.rs ", 0), Some(MentionCompletion { source_range: 6..19, mode: Some(ContextPickerMode::File), @@ -946,7 +972,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0), + MentionCompletion::try_parse(true, "Lorem @file main.rs Ipsum", 0), Some(MentionCompletion { source_range: 6..19, mode: Some(ContextPickerMode::File), @@ -955,7 +981,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @main", 0), + MentionCompletion::try_parse(true, "Lorem @main", 0), Some(MentionCompletion { source_range: 6..11, mode: None, @@ -963,6 +989,28 @@ mod tests { }) ); - assert_eq!(MentionCompletion::try_parse("test@", 0), None); + assert_eq!(MentionCompletion::try_parse(true, "test@", 0), None); + + // Allowed non-file mentions + + assert_eq!( + MentionCompletion::try_parse(true, "Lorem @symbol main", 0), + Some(MentionCompletion { + source_range: 6..18, + mode: Some(ContextPickerMode::Symbol), + argument: Some("main".to_string()), + }) + ); + + // Disallowed non-file mentions + + assert_eq!( + MentionCompletion::try_parse(false, "Lorem @symbol main", 0), + Some(MentionCompletion { + source_range: 6..18, + mode: None, + argument: Some("main".to_string()), + }) + ); } } diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index ccd33c9247ce1b686bbea41e70dcde0039b49df2..5eab1a4e2db5a970c271b6b74a23e6da91bf5362 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -51,7 +51,10 @@ use ui::{ }; use url::Url; use util::ResultExt; -use workspace::{Workspace, notifications::NotifyResultExt as _}; +use workspace::{ + Toast, Workspace, + notifications::{NotificationId, NotifyResultExt as _}, +}; use zed_actions::agent::Chat; const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50); @@ -64,6 +67,7 @@ pub struct MessageEditor { history_store: Entity<HistoryStore>, prompt_store: Option<Entity<PromptStore>>, prevent_slash_commands: bool, + prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>, _subscriptions: Vec<Subscription>, _parse_slash_command_task: Task<()>, } @@ -96,11 +100,13 @@ impl MessageEditor { }, None, ); + let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let completion_provider = ContextPickerCompletionProvider::new( cx.weak_entity(), workspace.clone(), history_store.clone(), prompt_store.clone(), + prompt_capabilities.clone(), ); let semantics_provider = Rc::new(SlashCommandSemanticsProvider { range: Cell::new(None), @@ -158,6 +164,7 @@ impl MessageEditor { history_store, prompt_store, prevent_slash_commands, + prompt_capabilities, _subscriptions: subscriptions, _parse_slash_command_task: Task::ready(()), } @@ -193,6 +200,10 @@ impl MessageEditor { .detach(); } + pub fn set_prompt_capabilities(&mut self, capabilities: acp::PromptCapabilities) { + self.prompt_capabilities.set(capabilities); + } + #[cfg(test)] pub(crate) fn editor(&self) -> &Entity<Editor> { &self.editor @@ -230,7 +241,7 @@ impl MessageEditor { let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { return Task::ready(()); }; - let Some(anchor) = snapshot + let Some(start_anchor) = snapshot .buffer_snapshot .anchor_in_excerpt(*excerpt_id, start) else { @@ -244,6 +255,33 @@ impl MessageEditor { .unwrap_or_default(); if Img::extensions().contains(&extension) && !extension.contains("svg") { + if !self.prompt_capabilities.get().image { + struct ImagesNotAllowed; + + let end_anchor = snapshot.buffer_snapshot.anchor_before( + start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1, + ); + + self.editor.update(cx, |editor, cx| { + // Remove mention + editor.edit([((start_anchor..end_anchor), "")], cx); + }); + + self.workspace + .update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new( + NotificationId::unique::<ImagesNotAllowed>(), + "This agent does not support images yet", + ) + .autohide(), + cx, + ); + }) + .ok(); + return Task::ready(()); + } + let project = self.project.clone(); let Some(project_path) = project .read(cx) @@ -277,7 +315,7 @@ impl MessageEditor { }; return self.confirm_mention_for_image( crease_id, - anchor, + start_anchor, Some(abs_path.clone()), image, window, @@ -301,17 +339,22 @@ impl MessageEditor { match mention_uri { MentionUri::Fetch { url } => { - self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx) + self.confirm_mention_for_fetch(crease_id, start_anchor, url, window, cx) } MentionUri::Directory { abs_path } => { - self.confirm_mention_for_directory(crease_id, anchor, abs_path, window, cx) + self.confirm_mention_for_directory(crease_id, start_anchor, abs_path, window, cx) } MentionUri::Thread { id, name } => { - self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx) - } - MentionUri::TextThread { path, name } => { - self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx) + self.confirm_mention_for_thread(crease_id, start_anchor, id, name, window, cx) } + MentionUri::TextThread { path, name } => self.confirm_mention_for_text_thread( + crease_id, + start_anchor, + path, + name, + window, + cx, + ), MentionUri::File { .. } | MentionUri::Symbol { .. } | MentionUri::Rule { .. } @@ -778,6 +821,10 @@ impl MessageEditor { } fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) { + if !self.prompt_capabilities.get().image { + return; + } + let images = cx .read_from_clipboard() .map(|item| { @@ -2009,6 +2056,34 @@ mod tests { (message_editor, editor) }); + cx.simulate_input("Lorem @"); + + editor.update_in(&mut cx, |editor, window, cx| { + assert_eq!(editor.text(cx), "Lorem @"); + assert!(editor.has_visible_completions_menu()); + + // Only files since we have default capabilities + assert_eq!( + current_completion_labels(editor), + &[ + "eight.txt dir/b/", + "seven.txt dir/b/", + "six.txt dir/b/", + "five.txt dir/b/", + ] + ); + editor.set_text("", window, cx); + }); + + message_editor.update(&mut cx, |editor, _cx| { + // Enable all prompt capabilities + editor.set_prompt_capabilities(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }); + }); + cx.simulate_input("Lorem "); editor.update(&mut cx, |editor, cx| { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 14f9cacd154f4bcd127a8a3a2714313421bf7b49..81a56165c81e7ad794f288d54da04e0ffb2a8ea4 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -492,6 +492,11 @@ impl AcpThreadView { }) }); + this.message_editor.update(cx, |message_editor, _cx| { + message_editor + .set_prompt_capabilities(connection.prompt_capabilities()); + }); + cx.notify(); } Err(err) => { @@ -4762,6 +4767,14 @@ pub(crate) mod tests { &[] } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + } + } + fn authenticate( &self, _method_id: acp::AuthMethodId, From b0bef3a9a279e98abc499d4e2f7850ff7f5959ea Mon Sep 17 00:00:00 2001 From: Ben Brandt <benjamin.j.brandt@gmail.com> Date: Wed, 20 Aug 2025 21:17:07 +0200 Subject: [PATCH 201/744] agent2: Clean up tool descriptions (#36619) schemars was passing along the newlines from the doc comments. This should make these closer to the markdown file versions we had in the old agent. Release Notes: - N/A --- crates/agent2/src/tools/copy_path_tool.rs | 15 ++++----------- .../agent2/src/tools/create_directory_tool.rs | 7 ++----- crates/agent2/src/tools/delete_path_tool.rs | 3 +-- crates/agent2/src/tools/edit_file_tool.rs | 19 ++++++------------- crates/agent2/src/tools/find_path_tool.rs | 1 - crates/agent2/src/tools/grep_tool.rs | 3 +-- .../agent2/src/tools/list_directory_tool.rs | 6 ++---- crates/agent2/src/tools/move_path_tool.rs | 9 +++------ crates/agent2/src/tools/open_tool.rs | 10 +++------- crates/agent2/src/tools/read_file_tool.rs | 5 +---- crates/agent2/src/tools/thinking_tool.rs | 3 +-- crates/agent2/src/tools/web_search_tool.rs | 2 +- 12 files changed, 25 insertions(+), 58 deletions(-) diff --git a/crates/agent2/src/tools/copy_path_tool.rs b/crates/agent2/src/tools/copy_path_tool.rs index f973b86990af76ea923d548f95a4f05b4cd32c18..4b40a9842f69cb87977ae948c4f858a2540b2eb1 100644 --- a/crates/agent2/src/tools/copy_path_tool.rs +++ b/crates/agent2/src/tools/copy_path_tool.rs @@ -8,16 +8,11 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use util::markdown::MarkdownInlineCode; -/// Copies a file or directory in the project, and returns confirmation that the -/// copy succeeded. -/// +/// Copies a file or directory in the project, and returns confirmation that the copy succeeded. /// Directory contents will be copied recursively (like `cp -r`). /// -/// This tool should be used when it's desirable to create a copy of a file or -/// directory without modifying the original. It's much more efficient than -/// doing this by separately reading and then writing the file or directory's -/// contents, so this tool should be preferred over that approach whenever -/// copying is the goal. +/// This tool should be used when it's desirable to create a copy of a file or directory without modifying the original. +/// It's much more efficient than doing this by separately reading and then writing the file or directory's contents, so this tool should be preferred over that approach whenever copying is the goal. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct CopyPathToolInput { /// The source path of the file or directory to copy. @@ -33,12 +28,10 @@ pub struct CopyPathToolInput { /// You can copy the first file by providing a source_path of "directory1/a/something.txt" /// </example> pub source_path: String, - /// The destination path where the file or directory should be copied to. /// /// <example> - /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", - /// provide a destination_path of "directory2/b/copy.txt" + /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", provide a destination_path of "directory2/b/copy.txt" /// </example> pub destination_path: String, } diff --git a/crates/agent2/src/tools/create_directory_tool.rs b/crates/agent2/src/tools/create_directory_tool.rs index c173c5ae67512813b610552c2001dc16ceb38212..7720eb3595d475560b90cc890396b8f6b27600b1 100644 --- a/crates/agent2/src/tools/create_directory_tool.rs +++ b/crates/agent2/src/tools/create_directory_tool.rs @@ -9,12 +9,9 @@ use util::markdown::MarkdownInlineCode; use crate::{AgentTool, ToolCallEventStream}; -/// Creates a new directory at the specified path within the project. Returns -/// confirmation that the directory was created. +/// Creates a new directory at the specified path within the project. Returns confirmation that the directory was created. /// -/// This tool creates a directory and all necessary parent directories (similar -/// to `mkdir -p`). It should be used whenever you need to create new -/// directories within the project. +/// This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct CreateDirectoryToolInput { /// The path of the new directory. diff --git a/crates/agent2/src/tools/delete_path_tool.rs b/crates/agent2/src/tools/delete_path_tool.rs index e013b3a3e755cf6662718d620264cb1e38fa5417..c281f1b5b69e2f3cbc3282a17298e9002f3b7c52 100644 --- a/crates/agent2/src/tools/delete_path_tool.rs +++ b/crates/agent2/src/tools/delete_path_tool.rs @@ -9,8 +9,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; -/// Deletes the file or directory (and the directory's contents, recursively) at -/// the specified path in the project, and returns confirmation of the deletion. +/// Deletes the file or directory (and the directory's contents, recursively) at the specified path in the project, and returns confirmation of the deletion. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct DeletePathToolInput { /// The path of the file or directory to delete. diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 24fedda4eb57d36db9e8769a107a947fed998a05..f89cace9a84e1f9a877daf9a60503b8d78c8c336 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -34,25 +34,21 @@ const DEFAULT_UI_TEXT: &str = "Editing file"; /// - Use the `list_directory` tool to verify the parent directory exists and is the correct location #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct EditFileToolInput { - /// A one-line, user-friendly markdown description of the edit. This will be - /// shown in the UI and also passed to another model to perform the edit. + /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit. /// - /// Be terse, but also descriptive in what you want to achieve with this - /// edit. Avoid generic instructions. + /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions. /// /// NEVER mention the file path in this description. /// /// <example>Fix API endpoint URLs</example> /// <example>Update copyright year in `page_footer`</example> /// - /// Make sure to include this field before all the others in the input object - /// so that we can display it immediately. + /// Make sure to include this field before all the others in the input object so that we can display it immediately. pub display_description: String, /// The full path of the file to create or modify in the project. /// - /// WARNING: When specifying which file path need changing, you MUST - /// start each path with one of the project's root directories. + /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories. /// /// The following examples assume we have two root directories in the project: /// - /a/b/backend @@ -61,22 +57,19 @@ pub struct EditFileToolInput { /// <example> /// `backend/src/main.rs` /// - /// Notice how the file path starts with `backend`. Without that, the path - /// would be ambiguous and the call would fail! + /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail! /// </example> /// /// <example> /// `frontend/db.js` /// </example> pub path: PathBuf, - /// The mode of operation on the file. Possible values: /// - 'edit': Make granular edits to an existing file. /// - 'create': Create a new file if it doesn't exist. /// - 'overwrite': Replace the entire contents of an existing file. /// - /// When a file already exists or you just created it, prefer editing - /// it as opposed to recreating it from scratch. + /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch. pub mode: EditFileMode, } diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index deccf37ab71109fadd1d394ee4ee15000c74f2e5..9e11ca6a37d92d6c189e542611fbc396128e6a15 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -31,7 +31,6 @@ pub struct FindPathToolInput { /// You can get back the first two paths by providing a glob of "*thing*.txt" /// </example> pub glob: String, - /// Optional starting position for paginated results (0-based). /// When not provided, starts from the beginning. #[serde(default)] diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent2/src/tools/grep_tool.rs index 265c26926d816a00a414755ea1193eb22d1c915f..955dae723558c3b8b3324109c18e9448215d66a3 100644 --- a/crates/agent2/src/tools/grep_tool.rs +++ b/crates/agent2/src/tools/grep_tool.rs @@ -27,8 +27,7 @@ use util::paths::PathMatcher; /// - DO NOT use HTML entities solely to escape characters in the tool parameters. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct GrepToolInput { - /// A regex pattern to search for in the entire project. Note that the regex - /// will be parsed by the Rust `regex` crate. + /// A regex pattern to search for in the entire project. Note that the regex will be parsed by the Rust `regex` crate. /// /// Do NOT specify a path here! This will only be matched against the code **content**. pub regex: String, diff --git a/crates/agent2/src/tools/list_directory_tool.rs b/crates/agent2/src/tools/list_directory_tool.rs index 61f21d8f95117f0b0a8efccf7481874037af365c..31575a92e44aa66558175b078b6c8e6087a67653 100644 --- a/crates/agent2/src/tools/list_directory_tool.rs +++ b/crates/agent2/src/tools/list_directory_tool.rs @@ -10,14 +10,12 @@ use std::fmt::Write; use std::{path::Path, sync::Arc}; use util::markdown::MarkdownInlineCode; -/// Lists files and directories in a given path. Prefer the `grep` or -/// `find_path` tools when searching the codebase. +/// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ListDirectoryToolInput { /// The fully-qualified path of the directory to list in the project. /// - /// This path should never be absolute, and the first component - /// of the path should always be a root directory in a project. + /// This path should never be absolute, and the first component of the path should always be a root directory in a project. /// /// <example> /// If the project has the following root directories: diff --git a/crates/agent2/src/tools/move_path_tool.rs b/crates/agent2/src/tools/move_path_tool.rs index f8d5d0d176e5cd53d1f563385797596048c9a87e..2a173a4404fc344051d238c6ba377e63ec2d9acc 100644 --- a/crates/agent2/src/tools/move_path_tool.rs +++ b/crates/agent2/src/tools/move_path_tool.rs @@ -8,14 +8,11 @@ use serde::{Deserialize, Serialize}; use std::{path::Path, sync::Arc}; use util::markdown::MarkdownInlineCode; -/// Moves or rename a file or directory in the project, and returns confirmation -/// that the move succeeded. +/// Moves or rename a file or directory in the project, and returns confirmation that the move succeeded. /// -/// If the source and destination directories are the same, but the filename is -/// different, this performs a rename. Otherwise, it performs a move. +/// If the source and destination directories are the same, but the filename is different, this performs a rename. Otherwise, it performs a move. /// -/// This tool should be used when it's desirable to move or rename a file or -/// directory without changing its contents at all. +/// This tool should be used when it's desirable to move or rename a file or directory without changing its contents at all. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct MovePathToolInput { /// The source path of the file or directory to move/rename. diff --git a/crates/agent2/src/tools/open_tool.rs b/crates/agent2/src/tools/open_tool.rs index 36420560c1832d40496a95c69505ab8eb9cbb2c6..c20369c2d80cadc99f60c8335b5ecdbef763bf5f 100644 --- a/crates/agent2/src/tools/open_tool.rs +++ b/crates/agent2/src/tools/open_tool.rs @@ -8,19 +8,15 @@ use serde::{Deserialize, Serialize}; use std::{path::PathBuf, sync::Arc}; use util::markdown::MarkdownEscaped; -/// This tool opens a file or URL with the default application associated with -/// it on the user's operating system: +/// This tool opens a file or URL with the default application associated with it on the user's operating system: /// /// - On macOS, it's equivalent to the `open` command /// - On Windows, it's equivalent to `start` /// - On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate /// -/// For example, it can open a web browser with a URL, open a PDF file with the -/// default PDF viewer, etc. +/// For example, it can open a web browser with a URL, open a PDF file with the default PDF viewer, etc. /// -/// You MUST ONLY use this tool when the user has explicitly requested opening -/// something. You MUST NEVER assume that the user would like for you to use -/// this tool. +/// You MUST ONLY use this tool when the user has explicitly requested opening something. You MUST NEVER assume that the user would like for you to use this tool. #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct OpenToolInput { /// The path or URL to open with the default application. diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index f37dff4f47adfa65ca8145dd88d591467f60956a..11a57506fb6454ad3c527e68f43089f5b80216e1 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -21,8 +21,7 @@ use crate::{AgentTool, ToolCallEventStream}; pub struct ReadFileToolInput { /// The relative path of the file to read. /// - /// This path should never be absolute, and the first component - /// of the path should always be a root directory in a project. + /// This path should never be absolute, and the first component of the path should always be a root directory in a project. /// /// <example> /// If the project has the following root directories: @@ -34,11 +33,9 @@ pub struct ReadFileToolInput { /// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`. /// </example> pub path: String, - /// Optional line number to start reading on (1-based index) #[serde(default)] pub start_line: Option<u32>, - /// Optional line number to end reading on (1-based index, inclusive) #[serde(default)] pub end_line: Option<u32>, diff --git a/crates/agent2/src/tools/thinking_tool.rs b/crates/agent2/src/tools/thinking_tool.rs index 43647bb468d808b978a1b5176539a3167c5065f6..c5e94511621768868cd56f165b4f50c903874e0d 100644 --- a/crates/agent2/src/tools/thinking_tool.rs +++ b/crates/agent2/src/tools/thinking_tool.rs @@ -11,8 +11,7 @@ use crate::{AgentTool, ToolCallEventStream}; /// Use this tool when you need to work through complex problems, develop strategies, or outline approaches before taking action. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ThinkingToolInput { - /// Content to think about. This should be a description of what to think about or - /// a problem to solve. + /// Content to think about. This should be a description of what to think about or a problem to solve. content: String, } diff --git a/crates/agent2/src/tools/web_search_tool.rs b/crates/agent2/src/tools/web_search_tool.rs index d71a128bfe4f70a95aa71d776b76bd4f5426800a..ffcd4ad3becf0417aa6175808614c71abdd95e8e 100644 --- a/crates/agent2/src/tools/web_search_tool.rs +++ b/crates/agent2/src/tools/web_search_tool.rs @@ -14,7 +14,7 @@ use ui::prelude::*; use web_search::WebSearchRegistry; /// Search the web for information using your query. -/// Use this when you need real-time information, facts, or data that might not be in your training. \ +/// Use this when you need real-time information, facts, or data that might not be in your training. /// Results will include snippets and links from relevant web pages. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct WebSearchToolInput { From 739e4551da857800cf5fb862e98d0e72e9779551 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Wed, 20 Aug 2025 15:30:11 -0400 Subject: [PATCH 202/744] Fix typo in `Excerpt::contains` (#36621) Follow-up to #36524 Release Notes: - N/A --- crates/multi_buffer/src/multi_buffer.rs | 27 ++++++++++++------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index f73014a6ff161d8944741d679ab53a61da035975..a54d38163da211883985d8eee798e29730da1938 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -6998,20 +6998,19 @@ impl Excerpt { } fn contains(&self, anchor: &Anchor) -> bool { - anchor.buffer_id == None - || anchor.buffer_id == Some(self.buffer_id) - && self - .range - .context - .start - .cmp(&anchor.text_anchor, &self.buffer) - .is_le() - && self - .range - .context - .end - .cmp(&anchor.text_anchor, &self.buffer) - .is_ge() + (anchor.buffer_id == None || anchor.buffer_id == Some(self.buffer_id)) + && self + .range + .context + .start + .cmp(&anchor.text_anchor, &self.buffer) + .is_le() + && self + .range + .context + .end + .cmp(&anchor.text_anchor, &self.buffer) + .is_ge() } /// The [`Excerpt`]'s start offset in its [`Buffer`] From fa8bef1496efa8047b600fc65fcd662797ea6fb1 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" <JosephTLyons@gmail.com> Date: Wed, 20 Aug 2025 16:05:30 -0400 Subject: [PATCH 203/744] Bump Zed to v0.202 (#36622) Release Notes: -N/A --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70b8f630f772e804144dba52a647d24e453d2bd1..7df5304d926e1a4db6289dd27f9fdfb0baa18cbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20387,7 +20387,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.201.0" +version = "0.202.0" dependencies = [ "activity_indicator", "agent", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index d69efaf6c0034dc0d3091fc68e074e86454a334f..ac4cd721244ab9abb5e008eb9d6fe32430793b90 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.201.0" +version = "0.202.0" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team <hi@zed.dev>"] From 02dabbb9fa4a87721a76d3d6e498378f2965bd1e Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Wed, 20 Aug 2025 17:05:53 -0300 Subject: [PATCH 204/744] acp thread view: Do not go into editing mode if unsupported (#36623) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 81a56165c81e7ad794f288d54da04e0ffb2a8ea4..2b87144fcd276b71c099b5b135a543fc1d2bc0f6 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -669,8 +669,14 @@ impl AcpThreadView { ) { match &event.view_event { ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => { - self.editing_message = Some(event.entry_index); - cx.notify(); + if let Some(thread) = self.thread() + && let Some(AgentThreadEntry::UserMessage(user_message)) = + thread.read(cx).entries().get(event.entry_index) + && user_message.id.is_some() + { + self.editing_message = Some(event.entry_index); + cx.notify(); + } } ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => { self.regenerate(event.entry_index, editor, window, cx); @@ -1116,16 +1122,18 @@ impl AcpThreadView { .when(editing && !editor_focus, |this| this.border_dashed()) .border_color(cx.theme().colors().border) .map(|this|{ - if editor_focus { + if editing && editor_focus { this.border_color(focus_border) - } else { + } else if message.id.is_some() { this.hover(|s| s.border_color(focus_border.opacity(0.8))) + } else { + this } }) .text_xs() .child(editor.clone().into_any_element()), ) - .when(editor_focus, |this| + .when(editing && editor_focus, |this| this.child( h_flex() .absolute() From fb7edbfb464eb4ae0e66008b8e681ed0360aa474 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:01:22 -0300 Subject: [PATCH 205/744] thread_view: Add recent history entries & adjust empty state (#36625) Release Notes: - N/A --- assets/icons/menu_alt.svg | 2 +- assets/icons/zed_agent.svg | 34 ++-- assets/icons/zed_assistant.svg | 4 +- crates/agent2/src/history_store.rs | 4 + crates/agent2/src/native_agent_server.rs | 2 +- crates/agent_servers/src/gemini.rs | 4 +- crates/agent_ui/src/acp/thread_history.rs | 149 ++++++++++++++- crates/agent_ui/src/acp/thread_view.rs | 198 +++++++++++++++----- crates/agent_ui/src/ui.rs | 2 - crates/agent_ui/src/ui/new_thread_button.rs | 75 -------- 10 files changed, 325 insertions(+), 149 deletions(-) delete mode 100644 crates/agent_ui/src/ui/new_thread_button.rs diff --git a/assets/icons/menu_alt.svg b/assets/icons/menu_alt.svg index 87add13216d9eb8c4c3d8f345ff1695e98be2d5d..b9cc19e22febe045ca9ccf4a7e86d69b258f875c 100644 --- a/assets/icons/menu_alt.svg +++ b/assets/icons/menu_alt.svg @@ -1,3 +1,3 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M13.333 10H8M13.333 6H2.66701" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M2.66699 8H10.667M2.66699 4H13.333M2.66699 12H7.99999" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/assets/icons/zed_agent.svg b/assets/icons/zed_agent.svg index b6e120a0b6c3ca1d7eaf1049c51f2db7e9ff5b97..0c80e22c51233fff40b7605d0835b463786b4e84 100644 --- a/assets/icons/zed_agent.svg +++ b/assets/icons/zed_agent.svg @@ -1,27 +1,27 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M11 8.75V10.5C8.93097 10.5 8.06903 10.5 6 10.5V10L11 6V5.5H6V7.25" stroke="black" stroke-width="1.2"/> +<path d="M11 8.75V10.5C8.93097 10.5 8.06903 10.5 6 10.5V10L11 6V5.5H6V7.25" stroke="black" stroke-width="1.5"/> <path d="M2 8.5C2.27614 8.5 2.5 8.27614 2.5 8C2.5 7.72386 2.27614 7.5 2 7.5C1.72386 7.5 1.5 7.72386 1.5 8C1.5 8.27614 1.72386 8.5 2 8.5Z" fill="black"/> -<path d="M2.99976 6.33002C3.2759 6.33002 3.49976 6.10616 3.49976 5.83002C3.49976 5.55387 3.2759 5.33002 2.99976 5.33002C2.72361 5.33002 2.49976 5.55387 2.49976 5.83002C2.49976 6.10616 2.72361 6.33002 2.99976 6.33002Z" fill="black"/> -<path d="M2.99976 10.66C3.2759 10.66 3.49976 10.4361 3.49976 10.16C3.49976 9.88383 3.2759 9.65997 2.99976 9.65997C2.72361 9.65997 2.49976 9.88383 2.49976 10.16C2.49976 10.4361 2.72361 10.66 2.99976 10.66Z" fill="black"/> +<path opacity="0.6" d="M2.99976 6.33002C3.2759 6.33002 3.49976 6.10616 3.49976 5.83002C3.49976 5.55387 3.2759 5.33002 2.99976 5.33002C2.72361 5.33002 2.49976 5.55387 2.49976 5.83002C2.49976 6.10616 2.72361 6.33002 2.99976 6.33002Z" fill="black"/> +<path opacity="0.6" d="M2.99976 10.66C3.2759 10.66 3.49976 10.4361 3.49976 10.16C3.49976 9.88383 3.2759 9.65997 2.99976 9.65997C2.72361 9.65997 2.49976 9.88383 2.49976 10.16C2.49976 10.4361 2.72361 10.66 2.99976 10.66Z" fill="black"/> <path d="M15 8.5C15.2761 8.5 15.5 8.27614 15.5 8C15.5 7.72386 15.2761 7.5 15 7.5C14.7239 7.5 14.5 7.72386 14.5 8C14.5 8.27614 14.7239 8.5 15 8.5Z" fill="black"/> -<path d="M14 6.33002C14.2761 6.33002 14.5 6.10616 14.5 5.83002C14.5 5.55387 14.2761 5.33002 14 5.33002C13.7239 5.33002 13.5 5.55387 13.5 5.83002C13.5 6.10616 13.7239 6.33002 14 6.33002Z" fill="black"/> -<path d="M14 10.66C14.2761 10.66 14.5 10.4361 14.5 10.16C14.5 9.88383 14.2761 9.65997 14 9.65997C13.7239 9.65997 13.5 9.88383 13.5 10.16C13.5 10.4361 13.7239 10.66 14 10.66Z" fill="black"/> +<path opacity="0.6" d="M14 6.33002C14.2761 6.33002 14.5 6.10616 14.5 5.83002C14.5 5.55387 14.2761 5.33002 14 5.33002C13.7239 5.33002 13.5 5.55387 13.5 5.83002C13.5 6.10616 13.7239 6.33002 14 6.33002Z" fill="black"/> +<path opacity="0.6" d="M14 10.66C14.2761 10.66 14.5 10.4361 14.5 10.16C14.5 9.88383 14.2761 9.65997 14 9.65997C13.7239 9.65997 13.5 9.88383 13.5 10.16C13.5 10.4361 13.7239 10.66 14 10.66Z" fill="black"/> <path d="M8.49219 2C8.76833 2 8.99219 1.77614 8.99219 1.5C8.99219 1.22386 8.76833 1 8.49219 1C8.21605 1 7.99219 1.22386 7.99219 1.5C7.99219 1.77614 8.21605 2 8.49219 2Z" fill="black"/> -<path d="M6 3C6.27614 3 6.5 2.77614 6.5 2.5C6.5 2.22386 6.27614 2 6 2C5.72386 2 5.5 2.22386 5.5 2.5C5.5 2.77614 5.72386 3 6 3Z" fill="black"/> +<path opacity="0.6" d="M6 3C6.27614 3 6.5 2.77614 6.5 2.5C6.5 2.22386 6.27614 2 6 2C5.72386 2 5.5 2.22386 5.5 2.5C5.5 2.77614 5.72386 3 6 3Z" fill="black"/> <path d="M4 4C4.27614 4 4.5 3.77614 4.5 3.5C4.5 3.22386 4.27614 3 4 3C3.72386 3 3.5 3.22386 3.5 3.5C3.5 3.77614 3.72386 4 4 4Z" fill="black"/> <path d="M3.99976 13C4.2759 13 4.49976 12.7761 4.49976 12.5C4.49976 12.2239 4.2759 12 3.99976 12C3.72361 12 3.49976 12.2239 3.49976 12.5C3.49976 12.7761 3.72361 13 3.99976 13Z" fill="black"/> -<path d="M2 12.5C2.27614 12.5 2.5 12.2761 2.5 12C2.5 11.7239 2.27614 11.5 2 11.5C1.72386 11.5 1.5 11.7239 1.5 12C1.5 12.2761 1.72386 12.5 2 12.5Z" fill="black"/> -<path d="M2 4.5C2.27614 4.5 2.5 4.27614 2.5 4C2.5 3.72386 2.27614 3.5 2 3.5C1.72386 3.5 1.5 3.72386 1.5 4C1.5 4.27614 1.72386 4.5 2 4.5Z" fill="black"/> -<path d="M15 12.5C15.2761 12.5 15.5 12.2761 15.5 12C15.5 11.7239 15.2761 11.5 15 11.5C14.7239 11.5 14.5 11.7239 14.5 12C14.5 12.2761 14.7239 12.5 15 12.5Z" fill="black"/> -<path d="M15 4.5C15.2761 4.5 15.5 4.27614 15.5 4C15.5 3.72386 15.2761 3.5 15 3.5C14.7239 3.5 14.5 3.72386 14.5 4C14.5 4.27614 14.7239 4.5 15 4.5Z" fill="black"/> -<path d="M3.99976 15C4.2759 15 4.49976 14.7761 4.49976 14.5C4.49976 14.2239 4.2759 14 3.99976 14C3.72361 14 3.49976 14.2239 3.49976 14.5C3.49976 14.7761 3.72361 15 3.99976 15Z" fill="black"/> -<path d="M4 2C4.27614 2 4.5 1.77614 4.5 1.5C4.5 1.22386 4.27614 1 4 1C3.72386 1 3.5 1.22386 3.5 1.5C3.5 1.77614 3.72386 2 4 2Z" fill="black"/> -<path d="M13 15C13.2761 15 13.5 14.7761 13.5 14.5C13.5 14.2239 13.2761 14 13 14C12.7239 14 12.5 14.2239 12.5 14.5C12.5 14.7761 12.7239 15 13 15Z" fill="black"/> -<path d="M13 2C13.2761 2 13.5 1.77614 13.5 1.5C13.5 1.22386 13.2761 1 13 1C12.7239 1 12.5 1.22386 12.5 1.5C12.5 1.77614 12.7239 2 13 2Z" fill="black"/> +<path opacity="0.2" d="M2 12.5C2.27614 12.5 2.5 12.2761 2.5 12C2.5 11.7239 2.27614 11.5 2 11.5C1.72386 11.5 1.5 11.7239 1.5 12C1.5 12.2761 1.72386 12.5 2 12.5Z" fill="black"/> +<path opacity="0.2" d="M2 4.5C2.27614 4.5 2.5 4.27614 2.5 4C2.5 3.72386 2.27614 3.5 2 3.5C1.72386 3.5 1.5 3.72386 1.5 4C1.5 4.27614 1.72386 4.5 2 4.5Z" fill="black"/> +<path opacity="0.2" d="M15 12.5C15.2761 12.5 15.5 12.2761 15.5 12C15.5 11.7239 15.2761 11.5 15 11.5C14.7239 11.5 14.5 11.7239 14.5 12C14.5 12.2761 14.7239 12.5 15 12.5Z" fill="black"/> +<path opacity="0.2" d="M15 4.5C15.2761 4.5 15.5 4.27614 15.5 4C15.5 3.72386 15.2761 3.5 15 3.5C14.7239 3.5 14.5 3.72386 14.5 4C14.5 4.27614 14.7239 4.5 15 4.5Z" fill="black"/> +<path opacity="0.5" d="M3.99976 15C4.2759 15 4.49976 14.7761 4.49976 14.5C4.49976 14.2239 4.2759 14 3.99976 14C3.72361 14 3.49976 14.2239 3.49976 14.5C3.49976 14.7761 3.72361 15 3.99976 15Z" fill="black"/> +<path opacity="0.5" d="M4 2C4.27614 2 4.5 1.77614 4.5 1.5C4.5 1.22386 4.27614 1 4 1C3.72386 1 3.5 1.22386 3.5 1.5C3.5 1.77614 3.72386 2 4 2Z" fill="black"/> +<path opacity="0.5" d="M13 15C13.2761 15 13.5 14.7761 13.5 14.5C13.5 14.2239 13.2761 14 13 14C12.7239 14 12.5 14.2239 12.5 14.5C12.5 14.7761 12.7239 15 13 15Z" fill="black"/> +<path opacity="0.5" d="M13 2C13.2761 2 13.5 1.77614 13.5 1.5C13.5 1.22386 13.2761 1 13 1C12.7239 1 12.5 1.22386 12.5 1.5C12.5 1.77614 12.7239 2 13 2Z" fill="black"/> <path d="M13 4C13.2761 4 13.5 3.77614 13.5 3.5C13.5 3.22386 13.2761 3 13 3C12.7239 3 12.5 3.22386 12.5 3.5C12.5 3.77614 12.7239 4 13 4Z" fill="black"/> <path d="M13 13C13.2761 13 13.5 12.7761 13.5 12.5C13.5 12.2239 13.2761 12 13 12C12.7239 12 12.5 12.2239 12.5 12.5C12.5 12.7761 12.7239 13 13 13Z" fill="black"/> -<path d="M11 3C11.2761 3 11.5 2.77614 11.5 2.5C11.5 2.22386 11.2761 2 11 2C10.7239 2 10.5 2.22386 10.5 2.5C10.5 2.77614 10.7239 3 11 3Z" fill="black"/> +<path opacity="0.6" d="M11 3C11.2761 3 11.5 2.77614 11.5 2.5C11.5 2.22386 11.2761 2 11 2C10.7239 2 10.5 2.22386 10.5 2.5C10.5 2.77614 10.7239 3 11 3Z" fill="black"/> <path d="M8.5 15C8.77614 15 9 14.7761 9 14.5C9 14.2239 8.77614 14 8.5 14C8.22386 14 8 14.2239 8 14.5C8 14.7761 8.22386 15 8.5 15Z" fill="black"/> -<path d="M6 14C6.27614 14 6.5 13.7761 6.5 13.5C6.5 13.2239 6.27614 13 6 13C5.72386 13 5.5 13.2239 5.5 13.5C5.5 13.7761 5.72386 14 6 14Z" fill="black"/> -<path d="M11 14C11.2761 14 11.5 13.7761 11.5 13.5C11.5 13.2239 11.2761 13 11 13C10.7239 13 10.5 13.2239 10.5 13.5C10.5 13.7761 10.7239 14 11 14Z" fill="black"/> +<path opacity="0.6" d="M6 14C6.27614 14 6.5 13.7761 6.5 13.5C6.5 13.2239 6.27614 13 6 13C5.72386 13 5.5 13.2239 5.5 13.5C5.5 13.7761 5.72386 14 6 14Z" fill="black"/> +<path opacity="0.6" d="M11 14C11.2761 14 11.5 13.7761 11.5 13.5C11.5 13.2239 11.2761 13 11 13C10.7239 13 10.5 13.2239 10.5 13.5C10.5 13.7761 10.7239 14 11 14Z" fill="black"/> </svg> diff --git a/assets/icons/zed_assistant.svg b/assets/icons/zed_assistant.svg index 470eb0fedeab7535287db64b601b5dfd99b6c05d..812277a100b7e6e4ad44de357fc3556b686a90a0 100644 --- a/assets/icons/zed_assistant.svg +++ b/assets/icons/zed_assistant.svg @@ -1,5 +1,5 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M8 2.93652L6.9243 6.20697C6.86924 6.37435 6.77565 6.52646 6.65105 6.65105C6.52646 6.77565 6.37435 6.86924 6.20697 6.9243L2.93652 8L6.20697 9.0757C6.37435 9.13076 6.52646 9.22435 6.65105 9.34895C6.77565 9.47354 6.86924 9.62565 6.9243 9.79306L8 13.0635L9.0757 9.79306C9.13076 9.62565 9.22435 9.47354 9.34895 9.34895C9.47354 9.22435 9.62565 9.13076 9.79306 9.0757L13.0635 8L9.79306 6.9243C9.62565 6.86924 9.47354 6.77565 9.34895 6.65105C9.22435 6.52646 9.13076 6.37435 9.0757 6.20697L8 2.93652Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 870c2607c49e9ac43e657bbd5a1970cb342f9005..2d70164a668dd404cc56b4f4a01db376f520fb19 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -345,4 +345,8 @@ impl HistoryStore { .retain(|old_entry| old_entry != entry); self.save_recently_opened_entries(cx); } + + pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> { + self.entries(cx).into_iter().take(limit).collect() + } } diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 74d24efb13703dedd2e9109e8d19175cac606631..a1f935589af86c56bbddf27f0b887703a25cb983 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -27,7 +27,7 @@ impl AgentServer for NativeAgentServer { } fn empty_state_headline(&self) -> &'static str { - "" + "Welcome to the Agent Panel" } fn empty_state_message(&self) -> &'static str { diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 813f8b1fe0eb0036b74bd441d1a1c827b882b4d0..dcbeaa1d63a353ccfff7b7686ed9489c7cc3e8a0 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -18,11 +18,11 @@ const ACP_ARG: &str = "--experimental-acp"; impl AgentServer for Gemini { fn name(&self) -> &'static str { - "Gemini" + "Gemini CLI" } fn empty_state_headline(&self) -> &'static str { - "Welcome to Gemini" + "Welcome to Gemini CLI" } fn empty_state_message(&self) -> &'static str { diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index 8a058011394739f46028fa46ed57244aab91cc1f..68a41f31d0dfdc2bc2b47aecc3fa29bc94015007 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -1,11 +1,12 @@ -use crate::RemoveSelectedThread; +use crate::acp::AcpThreadView; +use crate::{AgentPanel, RemoveSelectedThread}; use agent2::{HistoryEntry, HistoryStore}; use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use editor::{Editor, EditorEvent}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, - UniformListScrollHandle, Window, uniform_list, + UniformListScrollHandle, WeakEntity, Window, uniform_list, }; use std::{fmt::Display, ops::Range, sync::Arc}; use time::{OffsetDateTime, UtcOffset}; @@ -639,6 +640,150 @@ impl Render for AcpThreadHistory { } } +#[derive(IntoElement)] +pub struct AcpHistoryEntryElement { + entry: HistoryEntry, + thread_view: WeakEntity<AcpThreadView>, + selected: bool, + hovered: bool, + on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>, +} + +impl AcpHistoryEntryElement { + pub fn new(entry: HistoryEntry, thread_view: WeakEntity<AcpThreadView>) -> Self { + Self { + entry, + thread_view, + selected: false, + hovered: false, + on_hover: Box::new(|_, _, _| {}), + } + } + + pub fn hovered(mut self, hovered: bool) -> Self { + self.hovered = hovered; + self + } + + pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { + self.on_hover = Box::new(on_hover); + self + } +} + +impl RenderOnce for AcpHistoryEntryElement { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let (id, title, timestamp) = match &self.entry { + HistoryEntry::AcpThread(thread) => ( + thread.id.to_string(), + thread.title.clone(), + thread.updated_at, + ), + HistoryEntry::TextThread(context) => ( + context.path.to_string_lossy().to_string(), + context.title.clone(), + context.mtime.to_utc(), + ), + }; + + let formatted_time = { + let now = chrono::Utc::now(); + let duration = now.signed_duration_since(timestamp); + + if duration.num_days() > 0 { + format!("{}d", duration.num_days()) + } else if duration.num_hours() > 0 { + format!("{}h ago", duration.num_hours()) + } else if duration.num_minutes() > 0 { + format!("{}m ago", duration.num_minutes()) + } else { + "Just now".to_string() + } + }; + + ListItem::new(SharedString::from(id)) + .rounded() + .toggle_state(self.selected) + .spacing(ListItemSpacing::Sparse) + .start_slot( + h_flex() + .w_full() + .gap_2() + .justify_between() + .child(Label::new(title).size(LabelSize::Small).truncate()) + .child( + Label::new(formatted_time) + .color(Color::Muted) + .size(LabelSize::XSmall), + ), + ) + .on_hover(self.on_hover) + .end_slot::<IconButton>(if self.hovered || self.selected { + Some( + IconButton::new("delete", IconName::Trash) + .shape(IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .tooltip(move |window, cx| { + Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) + }) + .on_click({ + let thread_view = self.thread_view.clone(); + let entry = self.entry.clone(); + + move |_event, _window, cx| { + if let Some(thread_view) = thread_view.upgrade() { + thread_view.update(cx, |thread_view, cx| { + thread_view.delete_history_entry(entry.clone(), cx); + }); + } + } + }), + ) + } else { + None + }) + .on_click({ + let thread_view = self.thread_view.clone(); + let entry = self.entry; + + move |_event, window, cx| { + if let Some(workspace) = thread_view + .upgrade() + .and_then(|view| view.read(cx).workspace().upgrade()) + { + match &entry { + HistoryEntry::AcpThread(thread_metadata) => { + if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) { + panel.update(cx, |panel, cx| { + panel.load_agent_thread( + thread_metadata.clone(), + window, + cx, + ); + }); + } + } + HistoryEntry::TextThread(context) => { + if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) { + panel.update(cx, |panel, cx| { + panel + .open_saved_prompt_editor( + context.path.clone(), + window, + cx, + ) + .detach_and_log_err(cx); + }); + } + } + } + } + } + }) + } +} + #[derive(Clone, Copy)] pub enum EntryTimeFormat { DateAndTime, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2b87144fcd276b71c099b5b135a543fc1d2bc0f6..35da9b8c859868f054ed263d04c6d9a7a16404eb 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -8,7 +8,7 @@ use action_log::ActionLog; use agent_client_protocol::{self as acp}; use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; -use agent2::{DbThreadMetadata, HistoryEntryId, HistoryStore}; +use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore}; use anyhow::bail; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; @@ -54,11 +54,12 @@ use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent}; use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; use crate::profile_selector::{ProfileProvider, ProfileSelector}; + use crate::ui::preview::UsageCallout; use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip}; use crate::{ AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow, - KeepAll, OpenAgentDiff, RejectAll, ToggleBurnMode, ToggleProfileSelector, + KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector, }; const RESPONSE_PADDING_X: Pixels = px(19.); @@ -240,6 +241,7 @@ pub struct AcpThreadView { project: Entity<Project>, thread_state: ThreadState, history_store: Entity<HistoryStore>, + hovered_recent_history_item: Option<usize>, entry_view_state: Entity<EntryViewState>, message_editor: Entity<MessageEditor>, model_selector: Option<Entity<AcpModelSelectorPopover>>, @@ -357,6 +359,7 @@ impl AcpThreadView { editor_expanded: false, terminal_expanded: true, history_store, + hovered_recent_history_item: None, _subscriptions: subscriptions, _cancel_task: None, } @@ -582,6 +585,10 @@ impl AcpThreadView { cx.notify(); } + pub fn workspace(&self) -> &WeakEntity<Workspace> { + &self.workspace + } + pub fn thread(&self) -> Option<&Entity<AcpThread>> { match &self.thread_state { ThreadState::Ready { thread, .. } => Some(thread), @@ -2284,51 +2291,132 @@ impl AcpThreadView { ) } - fn render_empty_state(&self, cx: &App) -> AnyElement { + fn render_empty_state_section_header( + &self, + label: impl Into<SharedString>, + action_slot: Option<AnyElement>, + cx: &mut Context<Self>, + ) -> impl IntoElement { + div().pl_1().pr_1p5().child( + h_flex() + .mt_2() + .pl_1p5() + .pb_1() + .w_full() + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child( + Label::new(label.into()) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .children(action_slot), + ) + } + + fn render_empty_state(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement { let loading = matches!(&self.thread_state, ThreadState::Loading { .. }); + let recent_history = self + .history_store + .update(cx, |history_store, cx| history_store.recent_entries(3, cx)); + let no_history = self + .history_store + .update(cx, |history_store, cx| history_store.is_empty(cx)); v_flex() .size_full() - .items_center() - .justify_center() - .child(if loading { - h_flex() - .justify_center() - .child(self.render_agent_logo()) - .with_animation( - "pulsating_icon", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.4, 1.0)), - |icon, delta| icon.opacity(delta), - ) - .into_any() - } else { - self.render_agent_logo().into_any_element() - }) - .child(h_flex().mt_4().mb_1().justify_center().child(if loading { - div() - .child(LoadingLabel::new("").size(LabelSize::Large)) - .into_any_element() - } else { - Headline::new(self.agent.empty_state_headline()) - .size(HeadlineSize::Medium) - .into_any_element() - })) - .child( - div() - .max_w_1_2() - .text_sm() - .text_center() - .map(|this| { - if loading { - this.invisible() + .when(no_history, |this| { + this.child( + v_flex() + .size_full() + .items_center() + .justify_center() + .child(if loading { + h_flex() + .justify_center() + .child(self.render_agent_logo()) + .with_animation( + "pulsating_icon", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 1.0)), + |icon, delta| icon.opacity(delta), + ) + .into_any() } else { - this.text_color(cx.theme().colors().text_muted) - } - }) - .child(self.agent.empty_state_message()), - ) + self.render_agent_logo().into_any_element() + }) + .child(h_flex().mt_4().mb_2().justify_center().child(if loading { + div() + .child(LoadingLabel::new("").size(LabelSize::Large)) + .into_any_element() + } else { + Headline::new(self.agent.empty_state_headline()) + .size(HeadlineSize::Medium) + .into_any_element() + })), + ) + }) + .when(!no_history, |this| { + this.justify_end().child( + v_flex() + .child( + self.render_empty_state_section_header( + "Recent", + Some( + Button::new("view-history", "View All") + .style(ButtonStyle::Subtle) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &OpenHistory, + &self.focus_handle(cx), + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(move |_event, window, cx| { + window.dispatch_action(OpenHistory.boxed_clone(), cx); + }) + .into_any_element(), + ), + cx, + ), + ) + .child( + v_flex().p_1().pr_1p5().gap_1().children( + recent_history + .into_iter() + .enumerate() + .map(|(index, entry)| { + // TODO: Add keyboard navigation. + let is_hovered = + self.hovered_recent_history_item == Some(index); + crate::acp::thread_history::AcpHistoryEntryElement::new( + entry, + cx.entity().downgrade(), + ) + .hovered(is_hovered) + .on_hover(cx.listener( + move |this, is_hovered, _window, cx| { + if *is_hovered { + this.hovered_recent_history_item = Some(index); + } else if this.hovered_recent_history_item + == Some(index) + { + this.hovered_recent_history_item = None; + } + cx.notify(); + }, + )) + .into_any_element() + }), + ), + ), + ) + }) .into_any() } @@ -2351,9 +2439,11 @@ impl AcpThreadView { .items_center() .justify_center() .child(self.render_error_agent_logo()) - .child(h_flex().mt_4().mb_1().justify_center().child( - Headline::new(self.agent.empty_state_headline()).size(HeadlineSize::Medium), - )) + .child( + h_flex().mt_4().mb_1().justify_center().child( + Headline::new("Authentication Required").size(HeadlineSize::Medium), + ), + ) .into_any(), ) .children(description.map(|desc| { @@ -4234,6 +4324,18 @@ impl AcpThreadView { ); cx.notify(); } + + pub fn delete_history_entry(&mut self, entry: HistoryEntry, cx: &mut Context<Self>) { + let task = match entry { + HistoryEntry::AcpThread(thread) => self.history_store.update(cx, |history, cx| { + history.delete_thread(thread.id.clone(), cx) + }), + HistoryEntry::TextThread(context) => self.history_store.update(cx, |history, cx| { + history.delete_text_thread(context.path.clone(), cx) + }), + }; + task.detach_and_log_err(cx); + } } impl Focusable for AcpThreadView { @@ -4268,7 +4370,9 @@ impl Render for AcpThreadView { window, cx, ), - ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)), + ThreadState::Loading { .. } => { + v_flex().flex_1().child(self.render_empty_state(window, cx)) + } ThreadState::LoadError(e) => v_flex() .p_2() .flex_1() @@ -4310,7 +4414,7 @@ impl Render for AcpThreadView { }, ) } else { - this.child(self.render_empty_state(cx)) + this.child(self.render_empty_state(window, cx)) } }) } diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index beeaf0c43bbaa9384030879654bfaada1e4d9cd1..e27a2242404e4f3b1d721b66ac2aad9119c4447a 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -2,7 +2,6 @@ mod agent_notification; mod burn_mode_tooltip; mod context_pill; mod end_trial_upsell; -// mod new_thread_button; mod onboarding_modal; pub mod preview; @@ -10,5 +9,4 @@ pub use agent_notification::*; pub use burn_mode_tooltip::*; pub use context_pill::*; pub use end_trial_upsell::*; -// pub use new_thread_button::*; pub use onboarding_modal::*; diff --git a/crates/agent_ui/src/ui/new_thread_button.rs b/crates/agent_ui/src/ui/new_thread_button.rs deleted file mode 100644 index 347d6adcaf14221fef31f87303028e30091d2ec4..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/ui/new_thread_button.rs +++ /dev/null @@ -1,75 +0,0 @@ -use gpui::{ClickEvent, ElementId, IntoElement, ParentElement, Styled}; -use ui::prelude::*; - -#[derive(IntoElement)] -pub struct NewThreadButton { - id: ElementId, - label: SharedString, - icon: IconName, - keybinding: Option<ui::KeyBinding>, - on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>, -} - -impl NewThreadButton { - fn new(id: impl Into<ElementId>, label: impl Into<SharedString>, icon: IconName) -> Self { - Self { - id: id.into(), - label: label.into(), - icon, - keybinding: None, - on_click: None, - } - } - - fn keybinding(mut self, keybinding: Option<ui::KeyBinding>) -> Self { - self.keybinding = keybinding; - self - } - - fn on_click<F>(mut self, handler: F) -> Self - where - F: Fn(&mut Window, &mut App) + 'static, - { - self.on_click = Some(Box::new( - move |_: &ClickEvent, window: &mut Window, cx: &mut App| handler(window, cx), - )); - self - } -} - -impl RenderOnce for NewThreadButton { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .id(self.id) - .w_full() - .py_1p5() - .px_2() - .gap_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border.opacity(0.4)) - .bg(cx.theme().colors().element_active.opacity(0.2)) - .hover(|style| { - style - .bg(cx.theme().colors().element_hover) - .border_color(cx.theme().colors().border) - }) - .child( - h_flex() - .gap_1p5() - .child( - Icon::new(self.icon) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child(Label::new(self.label).size(LabelSize::Small)), - ) - .when_some(self.keybinding, |this, keybinding| { - this.child(keybinding.size(rems_from_px(10.))) - }) - .when_some(self.on_click, |this, on_click| { - this.on_click(move |event, window, cx| on_click(event, window, cx)) - }) - } -} From d1820b183a08549927164e9a0791d7e7053ab484 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Wed, 20 Aug 2025 18:26:07 -0300 Subject: [PATCH 206/744] acp: Suggest installing gemini@preview instead of latest (#36629) Release Notes: - N/A --- crates/agent_servers/src/gemini.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index dcbeaa1d63a353ccfff7b7686ed9489c7cc3e8a0..25c654db9b9cea66dac6b353edfe78501a8aa789 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -53,7 +53,7 @@ impl AgentServer for Gemini { return Err(LoadError::NotInstalled { error_message: "Failed to find Gemini CLI binary".into(), install_message: "Install Gemini CLI".into(), - install_command: "npm install -g @google/gemini-cli@latest".into() + install_command: "npm install -g @google/gemini-cli@preview".into() }.into()); }; From 595cf1c6c3ce6980c4937fbb7a17229c31ff398f Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Wed, 20 Aug 2025 17:31:25 -0400 Subject: [PATCH 207/744] acp: Rename `assistant::QuoteSelection` and support it in agent2 threads (#36628) Release Notes: - N/A --- assets/keymaps/default-linux.json | 4 +- assets/keymaps/default-macos.json | 4 +- assets/keymaps/linux/cursor.json | 4 +- assets/keymaps/macos/cursor.json | 4 +- .../agent_ui/src/acp/completion_provider.rs | 122 ++++++++++-------- crates/agent_ui/src/acp/message_editor.rs | 54 ++++++-- crates/agent_ui/src/acp/thread_view.rs | 6 + crates/agent_ui/src/agent_panel.rs | 16 ++- crates/agent_ui/src/agent_ui.rs | 6 + crates/agent_ui/src/text_thread_editor.rs | 3 +- docs/src/ai/text-threads.md | 4 +- 11 files changed, 148 insertions(+), 79 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index b4efa70572bd51650713509b02e4ac4ad2df33b4..955e68f5a9f76483456628604df6e52c24dc2e1a 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -138,7 +138,7 @@ "find": "buffer_search::Deploy", "ctrl-f": "buffer_search::Deploy", "ctrl-h": "buffer_search::DeployReplace", - "ctrl->": "assistant::QuoteSelection", + "ctrl->": "agent::QuoteSelection", "ctrl-<": "assistant::InsertIntoEditor", "ctrl-alt-e": "editor::SelectEnclosingSymbol", "ctrl-shift-backspace": "editor::GoToPreviousChange", @@ -241,7 +241,7 @@ "ctrl-shift-i": "agent::ToggleOptionsMenu", "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", - "ctrl->": "assistant::QuoteSelection", + "ctrl->": "agent::QuoteSelection", "ctrl-alt-e": "agent::RemoveAllContext", "ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-enter": "agent::ContinueThread", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index ad2ab2ba8999e0b2bf40a29af5ca308ca1d604f4..8b18299a911f90507e78e06d4dff411b37276bb5 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -162,7 +162,7 @@ "cmd-alt-f": "buffer_search::DeployReplace", "cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }], "cmd-e": ["buffer_search::Deploy", { "focus": false }], - "cmd->": "assistant::QuoteSelection", + "cmd->": "agent::QuoteSelection", "cmd-<": "assistant::InsertIntoEditor", "cmd-alt-e": "editor::SelectEnclosingSymbol", "alt-enter": "editor::OpenSelectionsInMultibuffer" @@ -281,7 +281,7 @@ "cmd-shift-i": "agent::ToggleOptionsMenu", "cmd-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", - "cmd->": "assistant::QuoteSelection", + "cmd->": "agent::QuoteSelection", "cmd-alt-e": "agent::RemoveAllContext", "cmd-shift-e": "project_panel::ToggleFocus", "cmd-ctrl-b": "agent::ToggleBurnMode", diff --git a/assets/keymaps/linux/cursor.json b/assets/keymaps/linux/cursor.json index 1c381b0cf05531e7fd5743d71be1b4d662bb4c0d..2e27158e1167f0840cadfb0d86dc06614f6076c6 100644 --- a/assets/keymaps/linux/cursor.json +++ b/assets/keymaps/linux/cursor.json @@ -17,8 +17,8 @@ "bindings": { "ctrl-i": "agent::ToggleFocus", "ctrl-shift-i": "agent::ToggleFocus", - "ctrl-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode - "ctrl-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode + "ctrl-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode + "ctrl-l": "agent::QuoteSelection", // In cursor uses "Agent" mode "ctrl-k": "assistant::InlineAssist", "ctrl-shift-k": "assistant::InsertIntoEditor" } diff --git a/assets/keymaps/macos/cursor.json b/assets/keymaps/macos/cursor.json index fdf9c437cf395c074e42ae9c9dc53c1aa6ff66c2..1d723bd75bb788aa1ea63335f9fa555cb50d2df0 100644 --- a/assets/keymaps/macos/cursor.json +++ b/assets/keymaps/macos/cursor.json @@ -17,8 +17,8 @@ "bindings": { "cmd-i": "agent::ToggleFocus", "cmd-shift-i": "agent::ToggleFocus", - "cmd-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode - "cmd-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode + "cmd-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode + "cmd-l": "agent::QuoteSelection", // In cursor uses "Agent" mode "cmd-k": "assistant::InlineAssist", "cmd-shift-k": "assistant::InsertIntoEditor" } diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index bf0a3f7a5ab520b01af302820fa3b29b894041bf..3587e5144eab22ec1ab6cf60c4d6eb59c8c5d409 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -108,62 +108,7 @@ impl ContextPickerCompletionProvider { confirm: Some(Arc::new(|_, _, _| true)), }), ContextPickerEntry::Action(action) => { - let (new_text, on_action) = match action { - ContextPickerAction::AddSelections => { - const PLACEHOLDER: &str = "selection "; - let selections = selection_ranges(workspace, cx) - .into_iter() - .enumerate() - .map(|(ix, (buffer, range))| { - ( - buffer, - range, - (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1), - ) - }) - .collect::<Vec<_>>(); - - let new_text: String = PLACEHOLDER.repeat(selections.len()); - - let callback = Arc::new({ - let source_range = source_range.clone(); - move |_, window: &mut Window, cx: &mut App| { - let selections = selections.clone(); - let message_editor = message_editor.clone(); - let source_range = source_range.clone(); - window.defer(cx, move |window, cx| { - message_editor - .update(cx, |message_editor, cx| { - message_editor.confirm_mention_for_selection( - source_range, - selections, - window, - cx, - ) - }) - .ok(); - }); - false - } - }); - - (new_text, callback) - } - }; - - Some(Completion { - replace_range: source_range, - new_text, - label: CodeLabel::plain(action.label().to_string(), None), - icon_path: Some(action.icon().path().into()), - documentation: None, - source: project::CompletionSource::Custom, - insert_text_mode: None, - // This ensures that when a user accepts this completion, the - // completion menu will still be shown after "@category " is - // inserted - confirm: Some(on_action), - }) + Self::completion_for_action(action, source_range, message_editor, workspace, cx) } } } @@ -359,6 +304,71 @@ impl ContextPickerCompletionProvider { }) } + pub(crate) fn completion_for_action( + action: ContextPickerAction, + source_range: Range<Anchor>, + message_editor: WeakEntity<MessageEditor>, + workspace: &Entity<Workspace>, + cx: &mut App, + ) -> Option<Completion> { + let (new_text, on_action) = match action { + ContextPickerAction::AddSelections => { + const PLACEHOLDER: &str = "selection "; + let selections = selection_ranges(workspace, cx) + .into_iter() + .enumerate() + .map(|(ix, (buffer, range))| { + ( + buffer, + range, + (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1), + ) + }) + .collect::<Vec<_>>(); + + let new_text: String = PLACEHOLDER.repeat(selections.len()); + + let callback = Arc::new({ + let source_range = source_range.clone(); + move |_, window: &mut Window, cx: &mut App| { + let selections = selections.clone(); + let message_editor = message_editor.clone(); + let source_range = source_range.clone(); + window.defer(cx, move |window, cx| { + message_editor + .update(cx, |message_editor, cx| { + message_editor.confirm_mention_for_selection( + source_range, + selections, + window, + cx, + ) + }) + .ok(); + }); + false + } + }); + + (new_text, callback) + } + }; + + Some(Completion { + replace_range: source_range, + new_text, + label: CodeLabel::plain(action.label().to_string(), None), + icon_path: Some(action.icon().path().into()), + documentation: None, + source: project::CompletionSource::Custom, + insert_text_mode: None, + // This ensures that when a user accepts this completion, the + // completion menu will still be shown after "@category " is + // inserted + confirm: Some(on_action), + }) + } + fn search( &self, mode: Option<ContextPickerMode>, diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 5eab1a4e2db5a970c271b6b74a23e6da91bf5362..be133808b7731961f2f5f6cbd4197f64159b713f 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1,6 +1,6 @@ use crate::{ acp::completion_provider::ContextPickerCompletionProvider, - context_picker::fetch_context_picker::fetch_url_content, + context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content}, }; use acp_thread::{MentionUri, selection_name}; use agent_client_protocol as acp; @@ -27,7 +27,7 @@ use gpui::{ }; use language::{Buffer, Language}; use language_model::LanguageModelImage; -use project::{Project, ProjectPath, Worktree}; +use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree}; use prompt_store::PromptStore; use rope::Point; use settings::Settings; @@ -561,21 +561,24 @@ impl MessageEditor { let range = snapshot.anchor_after(offset + range_to_fold.start) ..snapshot.anchor_after(offset + range_to_fold.end); - let path = buffer - .read(cx) - .file() - .map_or(PathBuf::from("untitled"), |file| file.path().to_path_buf()); + // TODO support selections from buffers with no path + let Some(project_path) = buffer.read(cx).project_path(cx) else { + continue; + }; + let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { + continue; + }; let snapshot = buffer.read(cx).snapshot(); let point_range = selection_range.to_point(&snapshot); let line_range = point_range.start.row..point_range.end.row; let uri = MentionUri::Selection { - path: path.clone(), + path: abs_path.clone(), line_range: line_range.clone(), }; let crease = crate::context_picker::crease_for_mention( - selection_name(&path, &line_range).into(), + selection_name(&abs_path, &line_range).into(), uri.icon_path(cx), range, self.editor.downgrade(), @@ -587,8 +590,7 @@ impl MessageEditor { crease_ids.first().copied().unwrap() }); - self.mention_set - .insert_uri(crease_id, MentionUri::Selection { path, line_range }); + self.mention_set.insert_uri(crease_id, uri); } } @@ -948,6 +950,38 @@ impl MessageEditor { .detach(); } + pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) { + let buffer = self.editor.read(cx).buffer().clone(); + let Some(buffer) = buffer.read(cx).as_singleton() else { + return; + }; + let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + let Some(completion) = ContextPickerCompletionProvider::completion_for_action( + ContextPickerAction::AddSelections, + anchor..anchor, + cx.weak_entity(), + &workspace, + cx, + ) else { + return; + }; + self.editor.update(cx, |message_editor, cx| { + message_editor.edit( + [( + multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), + completion.new_text, + )], + cx, + ); + }); + if let Some(confirm) = completion.confirm { + confirm(CompletionIntent::Complete, window, cx); + } + } + pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) { self.editor.update(cx, |message_editor, cx| { message_editor.set_read_only(read_only); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 35da9b8c859868f054ed263d04c6d9a7a16404eb..0dfa3d259e4e58e01e2d9053ebd0ab0c8ca403a6 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4097,6 +4097,12 @@ impl AcpThreadView { }) } + pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) { + self.message_editor.update(cx, |message_editor, cx| { + message_editor.insert_selections(window, cx); + }) + } + fn render_thread_retry_status_callout( &self, _window: &mut Window, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index e2c4acb1ce5c0e85d44693d5d687ee1918fe63ec..65a9da573ad554d9d70f17aaec8b7c430cd6ec4e 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -903,6 +903,16 @@ impl AgentPanel { } } + fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> { + match &self.active_view { + ActiveView::ExternalAgentThread { thread_view } => Some(thread_view), + ActiveView::Thread { .. } + | ActiveView::TextThread { .. } + | ActiveView::History + | ActiveView::Configuration => None, + } + } + fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) { if cx.has_flag::<GeminiAndNativeFeatureFlag>() { return self.new_agent_thread(AgentType::NativeAgent, window, cx); @@ -3882,7 +3892,11 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { // Wait to create a new context until the workspace is no longer // being updated. cx.defer_in(window, move |panel, window, cx| { - if let Some(message_editor) = panel.active_message_editor() { + if let Some(thread_view) = panel.active_thread_view() { + thread_view.update(cx, |thread_view, cx| { + thread_view.insert_selections(window, cx); + }); + } else if let Some(message_editor) = panel.active_message_editor() { message_editor.update(cx, |message_editor, cx| { message_editor.context_store().update(cx, |store, cx| { let buffer = buffer.read(cx); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 7b6557245fadc04145394ee20308a96749a708db..6084fd64235b4c276c841889cb0516661c14b1f7 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -128,6 +128,12 @@ actions!( ] ); +#[derive(Clone, Copy, Debug, PartialEq, Eq, Action)] +#[action(namespace = agent)] +#[action(deprecated_aliases = ["assistant::QuoteSelection"])] +/// Quotes the current selection in the agent panel's message editor. +pub struct QuoteSelection; + /// Creates a new conversation thread, optionally based on an existing thread. #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = agent)] diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index a928f7af545ff3fc8a5fcbfa4acc71eaaaa943ce..9fbd90c4a69694c7d76f39234aef7da3b105c22c 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1,4 +1,5 @@ use crate::{ + QuoteSelection, language_model_selector::{LanguageModelSelector, language_model_selector}, ui::BurnModeTooltip, }; @@ -89,8 +90,6 @@ actions!( CycleMessageRole, /// Inserts the selected text into the active editor. InsertIntoEditor, - /// Quotes the current selection in the assistant conversation. - QuoteSelection, /// Splits the conversation at the current cursor position. Split, ] diff --git a/docs/src/ai/text-threads.md b/docs/src/ai/text-threads.md index 65a5dcba037a73b5f3041297041c21607211df23..ed439252b4d1612ea1b20269c6286e2b94685ac2 100644 --- a/docs/src/ai/text-threads.md +++ b/docs/src/ai/text-threads.md @@ -16,7 +16,7 @@ To begin, type a message in a `You` block. As you type, the remaining tokens count for the selected model is updated. -Inserting text from an editor is as simple as highlighting the text and running `assistant: quote selection` ({#kb assistant::QuoteSelection}); Zed will wrap it in a fenced code block if it is code. +Inserting text from an editor is as simple as highlighting the text and running `agent: quote selection` ({#kb agent::QuoteSelection}); Zed will wrap it in a fenced code block if it is code. ![Quoting a selection](https://zed.dev/img/assistant/quoting-a-selection.png) @@ -148,7 +148,7 @@ Usage: `/terminal [<number>]` The `/selection` command inserts the selected text in the editor into the context. This is useful for referencing specific parts of your code. -This is equivalent to the `assistant: quote selection` command ({#kb assistant::QuoteSelection}). +This is equivalent to the `agent: quote selection` command ({#kb agent::QuoteSelection}). Usage: `/selection` From 9e34bb3f058982f060face485186eba9a739afca Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Wed, 20 Aug 2025 18:35:48 -0300 Subject: [PATCH 208/744] acp: Hide feedback buttons for external agents (#36630) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 0dfa3d259e4e58e01e2d9053ebd0ab0c8ca403a6..f4c0ce97847b267e88e37013ef059ea844208fc5 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -3815,7 +3815,11 @@ impl AcpThreadView { .flex_wrap() .justify_end(); - if AgentSettings::get_global(cx).enable_feedback { + if AgentSettings::get_global(cx).enable_feedback + && self + .thread() + .is_some_and(|thread| thread.read(cx).connection().telemetry().is_some()) + { let feedback = self.thread_feedback.feedback; container = container.child( div().visible_on_hover("thread-controls-container").child( From c9c708ff08571ceab3d8aad7354042230d99750c Mon Sep 17 00:00:00 2001 From: Julia Ryan <juliaryan3.14@gmail.com> Date: Wed, 20 Aug 2025 16:43:53 -0500 Subject: [PATCH 209/744] nix: Re-enable nightly builds (#36632) Release Notes: - N/A --- .github/workflows/release_nightly.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 0cc6737a45106713021c769b75dbbb180008dffe..5d63c34edd28d7e0ab3930132867f40b8f5262e9 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -206,9 +206,6 @@ jobs: runs-on: github-8vcpu-ubuntu-2404 needs: tests name: Build Zed on FreeBSD - # env: - # MYTOKEN : ${{ secrets.MYTOKEN }} - # MYTOKEN2: "value2" steps: - uses: actions/checkout@v4 - name: Build FreeBSD remote-server @@ -243,7 +240,6 @@ jobs: bundle-nix: name: Build and cache Nix package - if: false needs: tests secrets: inherit uses: ./.github/workflows/nix.yml From 5120b6b7f9962daf0000618a06e4e1522c575334 Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Wed, 20 Aug 2025 16:12:41 -0600 Subject: [PATCH 210/744] acp: Handle Gemini Auth Better (#36631) Release Notes: - N/A --------- Co-authored-by: Danilo Leal <daniloleal09@gmail.com> --- crates/agent_servers/src/gemini.rs | 7 +- crates/agent_ui/src/acp/thread_view.rs | 154 ++++++++++++++++-- crates/language_models/src/provider/google.rs | 53 +++++- 3 files changed, 195 insertions(+), 19 deletions(-) diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 25c654db9b9cea66dac6b353edfe78501a8aa789..d30525328bff99a4abf59ba4a88139e8cfda6566 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -5,6 +5,7 @@ use crate::{AgentServer, AgentServerCommand}; use acp_thread::{AgentConnection, LoadError}; use anyhow::Result; use gpui::{Entity, Task}; +use language_models::provider::google::GoogleLanguageModelProvider; use project::Project; use settings::SettingsStore; use ui::App; @@ -47,7 +48,7 @@ impl AgentServer for Gemini { settings.get::<AllAgentServersSettings>(None).gemini.clone() })?; - let Some(command) = + let Some(mut command) = AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await else { return Err(LoadError::NotInstalled { @@ -57,6 +58,10 @@ impl AgentServer for Gemini { }.into()); }; + if let Some(api_key)= cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() { + command.env.get_or_insert_default().insert("GEMINI_API_KEY".to_owned(), api_key.key); + } + let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await; if result.is_err() { let version_fut = util::command::new_smol_command(&command.path) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f4c0ce97847b267e88e37013ef059ea844208fc5..12a33d022e382d3a8639270d4f10f707b9170434 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -278,6 +278,7 @@ enum ThreadState { connection: Rc<dyn AgentConnection>, description: Option<Entity<Markdown>>, configuration_view: Option<AnyView>, + pending_auth_method: Option<acp::AuthMethodId>, _subscription: Option<Subscription>, }, } @@ -563,6 +564,7 @@ impl AcpThreadView { this.update(cx, |this, cx| { this.thread_state = ThreadState::Unauthenticated { + pending_auth_method: None, connection, configuration_view, description: err @@ -999,12 +1001,74 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context<Self>, ) { - let ThreadState::Unauthenticated { ref connection, .. } = self.thread_state else { + let ThreadState::Unauthenticated { + connection, + pending_auth_method, + configuration_view, + .. + } = &mut self.thread_state + else { return; }; + if method.0.as_ref() == "gemini-api-key" { + let registry = LanguageModelRegistry::global(cx); + let provider = registry + .read(cx) + .provider(&language_model::GOOGLE_PROVIDER_ID) + .unwrap(); + if !provider.is_authenticated(cx) { + let this = cx.weak_entity(); + let agent = self.agent.clone(); + let connection = connection.clone(); + window.defer(cx, |window, cx| { + Self::handle_auth_required( + this, + AuthRequired { + description: Some("GEMINI_API_KEY must be set".to_owned()), + provider_id: Some(language_model::GOOGLE_PROVIDER_ID), + }, + agent, + connection, + window, + cx, + ); + }); + return; + } + } else if method.0.as_ref() == "vertex-ai" + && std::env::var("GOOGLE_API_KEY").is_err() + && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err() + || (std::env::var("GOOGLE_CLOUD_PROJECT").is_err())) + { + let this = cx.weak_entity(); + let agent = self.agent.clone(); + let connection = connection.clone(); + + window.defer(cx, |window, cx| { + Self::handle_auth_required( + this, + AuthRequired { + description: Some( + "GOOGLE_API_KEY must be set in the environment to use Vertex AI authentication for Gemini CLI. Please export it and restart Zed." + .to_owned(), + ), + provider_id: None, + }, + agent, + connection, + window, + cx, + ) + }); + return; + } + self.thread_error.take(); + configuration_view.take(); + pending_auth_method.replace(method.clone()); let authenticate = connection.authenticate(method, cx); + cx.notify(); self.auth_task = Some(cx.spawn_in(window, { let project = self.project.clone(); let agent = self.agent.clone(); @@ -2425,6 +2489,7 @@ impl AcpThreadView { connection: &Rc<dyn AgentConnection>, description: Option<&Entity<Markdown>>, configuration_view: Option<&AnyView>, + pending_auth_method: Option<&acp::AuthMethodId>, window: &mut Window, cx: &Context<Self>, ) -> Div { @@ -2456,17 +2521,80 @@ impl AcpThreadView { .cloned() .map(|view| div().px_4().w_full().max_w_128().child(view)), ) - .child(h_flex().mt_1p5().justify_center().children( - connection.auth_methods().iter().map(|method| { - Button::new(SharedString::from(method.id.0.clone()), method.name.clone()) - .on_click({ - let method_id = method.id.clone(); - cx.listener(move |this, _, window, cx| { - this.authenticate(method_id.clone(), window, cx) + .when( + configuration_view.is_none() + && description.is_none() + && pending_auth_method.is_none(), + |el| { + el.child( + div() + .text_ui(cx) + .text_center() + .px_4() + .w_full() + .max_w_128() + .child(Label::new("Authentication required")), + ) + }, + ) + .when_some(pending_auth_method, |el, _| { + let spinner_icon = div() + .px_0p5() + .id("generating") + .tooltip(Tooltip::text("Generating Changes…")) + .child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Small) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage(delta))) + }, + ) + .into_any_element(), + ) + .into_any(); + el.child( + h_flex() + .text_ui(cx) + .text_center() + .justify_center() + .gap_2() + .px_4() + .w_full() + .max_w_128() + .child(Label::new("Authenticating...")) + .child(spinner_icon), + ) + }) + .child( + h_flex() + .mt_1p5() + .gap_1() + .flex_wrap() + .justify_center() + .children(connection.auth_methods().iter().enumerate().rev().map( + |(ix, method)| { + Button::new( + SharedString::from(method.id.0.clone()), + method.name.clone(), + ) + .style(ButtonStyle::Outlined) + .when(ix == 0, |el| { + el.style(ButtonStyle::Tinted(ui::TintColor::Accent)) }) - }) - }), - )) + .size(ButtonSize::Medium) + .label_size(LabelSize::Small) + .on_click({ + let method_id = method.id.clone(); + cx.listener(move |this, _, window, cx| { + this.authenticate(method_id.clone(), window, cx) + }) + }) + }, + )), + ) } fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement { @@ -2551,6 +2679,8 @@ impl AcpThreadView { let install_command = install_command.clone(); container = container.child( Button::new("install", install_message) + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .size(ButtonSize::Medium) .tooltip(Tooltip::text(install_command.clone())) .on_click(cx.listener(move |this, _, window, cx| { let task = this @@ -4372,11 +4502,13 @@ impl Render for AcpThreadView { connection, description, configuration_view, + pending_auth_method, .. } => self.render_auth_required_state( connection, description.as_ref(), configuration_view.as_ref(), + pending_auth_method.as_ref(), window, cx, ), diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index 1ac12b4cd4c032de48a2eaeb34c8505f8a07cb8f..566620675ee759516d767804bff837ec953d8369 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -12,9 +12,9 @@ use gpui::{ }; use http_client::HttpClient; use language_model::{ - AuthenticateError, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelToolChoice, LanguageModelToolSchemaFormat, LanguageModelToolUse, - LanguageModelToolUseId, MessageContent, StopReason, + AuthenticateError, ConfigurationViewTargetAgent, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelToolChoice, LanguageModelToolSchemaFormat, + LanguageModelToolUse, LanguageModelToolUseId, MessageContent, StopReason, }; use language_model::{ LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider, @@ -37,6 +37,8 @@ use util::ResultExt; use crate::AllLanguageModelSettings; use crate::ui::InstructionListItem; +use super::anthropic::ApiKey; + const PROVIDER_ID: LanguageModelProviderId = language_model::GOOGLE_PROVIDER_ID; const PROVIDER_NAME: LanguageModelProviderName = language_model::GOOGLE_PROVIDER_NAME; @@ -198,6 +200,33 @@ impl GoogleLanguageModelProvider { request_limiter: RateLimiter::new(4), }) } + + pub fn api_key(cx: &mut App) -> Task<Result<ApiKey>> { + let credentials_provider = <dyn CredentialsProvider>::global(cx); + let api_url = AllLanguageModelSettings::get_global(cx) + .google + .api_url + .clone(); + + if let Ok(key) = std::env::var(GEMINI_API_KEY_VAR) { + Task::ready(Ok(ApiKey { + key, + from_env: true, + })) + } else { + cx.spawn(async move |cx| { + let (_, api_key) = credentials_provider + .read_credentials(&api_url, cx) + .await? + .ok_or(AuthenticateError::CredentialsNotFound)?; + + Ok(ApiKey { + key: String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?, + from_env: false, + }) + }) + } + } } impl LanguageModelProviderState for GoogleLanguageModelProvider { @@ -279,11 +308,11 @@ impl LanguageModelProvider for GoogleLanguageModelProvider { fn configuration_view( &self, - _target_agent: language_model::ConfigurationViewTargetAgent, + target_agent: language_model::ConfigurationViewTargetAgent, window: &mut Window, cx: &mut App, ) -> AnyView { - cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) + cx.new(|cx| ConfigurationView::new(self.state.clone(), target_agent, window, cx)) .into() } @@ -776,11 +805,17 @@ fn convert_usage(usage: &UsageMetadata) -> language_model::TokenUsage { struct ConfigurationView { api_key_editor: Entity<Editor>, state: gpui::Entity<State>, + target_agent: language_model::ConfigurationViewTargetAgent, load_credentials_task: Option<Task<()>>, } impl ConfigurationView { - fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self { + fn new( + state: gpui::Entity<State>, + target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut Context<Self>, + ) -> Self { cx.observe(&state, |_, _, cx| { cx.notify(); }) @@ -810,6 +845,7 @@ impl ConfigurationView { editor.set_placeholder_text("AIzaSy...", cx); editor }), + target_agent, state, load_credentials_task, } @@ -885,7 +921,10 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's agent with Google AI, you need to add an API key. Follow these steps:")) + .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent { + ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI", + ConfigurationViewTargetAgent::Other(agent) => agent, + }))) .child( List::new() .child(InstructionListItem::new( From ffb995181ef0d1034f89108ce50be4a8c679f41f Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Wed, 20 Aug 2025 19:30:25 -0300 Subject: [PATCH 211/744] acp: Supress gemini aborted errors (#36633) This PR adds a temporary workaround to supress "Aborted" errors from Gemini when cancelling generation. This won't be needed once https://github.com/google-gemini/gemini-cli/pull/6656 is generally available. Release Notes: - N/A --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/agent_servers/src/acp/v1.rs | 61 ++++++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7df5304d926e1a4db6289dd27f9fdfb0baa18cbb..bfb135d32cfcdbb11b13ee79bde65ed51692f350 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.28" +version = "0.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c887e795097665ab95119580534e7cc1335b59e1a7fec296943e534b970f4ed" +checksum = "89a2cd7e0bd2bb7ed27687cfcf6561b91542c1ce23e52fd54ee59b7568c9bd84" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index 436d4a7f5c2b065621cfe7b15351847699c5b182..3f547459002011dfdf1b9597226c1f03fe03f456 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -423,7 +423,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.28" +agent-client-protocol = "0.0.29" 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/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 2e70a5f37a439aab03a0c14b945e7e304544d72a..2cad1b5a87d895821f361ae92107b8d68409e305 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -1,11 +1,12 @@ use action_log::ActionLog; -use agent_client_protocol::{self as acp, Agent as _}; +use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; use anyhow::anyhow; use collections::HashMap; use futures::AsyncBufReadExt as _; use futures::channel::oneshot; use futures::io::BufReader; use project::Project; +use serde::Deserialize; use std::path::Path; use std::rc::Rc; use std::{any::Any, cell::RefCell}; @@ -27,6 +28,7 @@ pub struct AcpConnection { pub struct AcpSession { thread: WeakEntity<AcpThread>, + pending_cancel: bool, } const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; @@ -171,6 +173,7 @@ impl AgentConnection for AcpConnection { let session = AcpSession { thread: thread.downgrade(), + pending_cancel: false, }; sessions.borrow_mut().insert(session_id, session); @@ -202,9 +205,48 @@ impl AgentConnection for AcpConnection { cx: &mut App, ) -> Task<Result<acp::PromptResponse>> { let conn = self.connection.clone(); + let sessions = self.sessions.clone(); + let session_id = params.session_id.clone(); cx.foreground_executor().spawn(async move { - let response = conn.prompt(params).await?; - Ok(response) + match conn.prompt(params).await { + Ok(response) => Ok(response), + Err(err) => { + if err.code != ErrorCode::INTERNAL_ERROR.code { + anyhow::bail!(err) + } + + let Some(data) = &err.data else { + anyhow::bail!(err) + }; + + // Temporary workaround until the following PR is generally available: + // https://github.com/google-gemini/gemini-cli/pull/6656 + + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct ErrorDetails { + details: Box<str>, + } + + match serde_json::from_value(data.clone()) { + Ok(ErrorDetails { details }) => { + if sessions + .borrow() + .get(&session_id) + .is_some_and(|session| session.pending_cancel) + && details.contains("This operation was aborted") + { + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Canceled, + }) + } else { + Err(anyhow!(details)) + } + } + Err(_) => Err(anyhow!(err)), + } + } + } }) } @@ -213,12 +255,23 @@ impl AgentConnection for AcpConnection { } fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { + session.pending_cancel = true; + } let conn = self.connection.clone(); let params = acp::CancelNotification { session_id: session_id.clone(), }; + let sessions = self.sessions.clone(); + let session_id = session_id.clone(); cx.foreground_executor() - .spawn(async move { conn.cancel(params).await }) + .spawn(async move { + let resp = conn.cancel(params).await; + if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { + session.pending_cancel = false; + } + resp + }) .detach(); } From c20233e0b4fcaf0459ef0ff6b7ea3c3f72cce837 Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Wed, 20 Aug 2025 19:09:09 -0400 Subject: [PATCH 212/744] agent_ui: Fix signed-in check in Zed provider configuration (#36639) This PR fixes the check for if the user is signed in in the Agent panel configuration. Supersedes https://github.com/zed-industries/zed/pull/36634. Release Notes: - Fixed the user's plan badge near the Zed provider in the Agent panel not showing despite being signed in. --- crates/agent_ui/src/agent_configuration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 6da84758eee532aefbd720d986ae00dde13725e4..00e48efdacf54ebb108ceb7ae6bb85f7c49bba8f 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -192,7 +192,7 @@ impl AgentConfiguration { let is_signed_in = self .workspace .read_with(cx, |workspace, _| { - workspace.client().status().borrow().is_connected() + !workspace.client().status().borrow().is_signed_out() }) .unwrap_or(false); From 74c0ba980b6a561e514fecd4c93fd8cbe7e045c2 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Wed, 20 Aug 2025 20:32:17 -0300 Subject: [PATCH 213/744] acp: Reliably suppress gemini abort error (#36640) https://github.com/zed-industries/zed/pull/36633 relied on the prompt request responding before cancel, but that's not guaranteed Release Notes: - N/A --- crates/agent_servers/src/acp/v1.rs | 33 ++++++++++++++---------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 2cad1b5a87d895821f361ae92107b8d68409e305..bc11a3748ac3603e50dd6505cdbccaba34244160 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -28,7 +28,7 @@ pub struct AcpConnection { pub struct AcpSession { thread: WeakEntity<AcpThread>, - pending_cancel: bool, + suppress_abort_err: bool, } const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; @@ -173,7 +173,7 @@ impl AgentConnection for AcpConnection { let session = AcpSession { thread: thread.downgrade(), - pending_cancel: false, + suppress_abort_err: false, }; sessions.borrow_mut().insert(session_id, session); @@ -208,7 +208,16 @@ impl AgentConnection for AcpConnection { let sessions = self.sessions.clone(); let session_id = params.session_id.clone(); cx.foreground_executor().spawn(async move { - match conn.prompt(params).await { + let result = conn.prompt(params).await; + + let mut suppress_abort_err = false; + + if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { + suppress_abort_err = session.suppress_abort_err; + session.suppress_abort_err = false; + } + + match result { Ok(response) => Ok(response), Err(err) => { if err.code != ErrorCode::INTERNAL_ERROR.code { @@ -230,11 +239,7 @@ impl AgentConnection for AcpConnection { match serde_json::from_value(data.clone()) { Ok(ErrorDetails { details }) => { - if sessions - .borrow() - .get(&session_id) - .is_some_and(|session| session.pending_cancel) - && details.contains("This operation was aborted") + if suppress_abort_err && details.contains("This operation was aborted") { Ok(acp::PromptResponse { stop_reason: acp::StopReason::Canceled, @@ -256,22 +261,14 @@ impl AgentConnection for AcpConnection { fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { - session.pending_cancel = true; + session.suppress_abort_err = true; } let conn = self.connection.clone(); let params = acp::CancelNotification { session_id: session_id.clone(), }; - let sessions = self.sessions.clone(); - let session_id = session_id.clone(); cx.foreground_executor() - .spawn(async move { - let resp = conn.cancel(params).await; - if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { - session.pending_cancel = false; - } - resp - }) + .spawn(async move { conn.cancel(params).await }) .detach(); } From 3dd362978a2b5fa6c41da9368252491d1c638fab Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Wed, 20 Aug 2025 18:41:06 -0500 Subject: [PATCH 214/744] docs: Add table of all actions (#36642) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/docs_preprocessor/src/main.rs | 66 ++++++++++++++++++++++++++++ docs/src/SUMMARY.md | 1 + docs/src/all-actions.md | 3 ++ 3 files changed, 70 insertions(+) create mode 100644 docs/src/all-actions.md diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 33158577c4e313dab051976854467a1d3c9019bd..c900eb692aee34b13f13f4fb67061b577b28be1d 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -99,6 +99,7 @@ fn handle_preprocessing() -> Result<()> { let mut errors = HashSet::<PreprocessorError>::new(); handle_frontmatter(&mut book, &mut errors); + template_big_table_of_actions(&mut book); template_and_validate_keybindings(&mut book, &mut errors); template_and_validate_actions(&mut book, &mut errors); @@ -147,6 +148,18 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>) }); } +fn template_big_table_of_actions(book: &mut Book) { + for_each_chapter_mut(book, |chapter| { + let needle = "{#ACTIONS_TABLE#}"; + if let Some(start) = chapter.content.rfind(needle) { + chapter.content.replace_range( + start..start + needle.len(), + &generate_big_table_of_actions(), + ); + } + }); +} + fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<PreprocessorError>) { let regex = Regex::new(r"\{#kb (.*?)\}").unwrap(); @@ -277,6 +290,7 @@ struct ActionDef { name: &'static str, human_name: String, deprecated_aliases: &'static [&'static str], + docs: Option<&'static str>, } fn dump_all_gpui_actions() -> Vec<ActionDef> { @@ -285,6 +299,7 @@ fn dump_all_gpui_actions() -> Vec<ActionDef> { name: action.name, human_name: command_palette::humanize_action_name(action.name), deprecated_aliases: action.deprecated_aliases, + docs: action.documentation, }) .collect::<Vec<ActionDef>>(); @@ -418,3 +433,54 @@ fn title_regex() -> &'static Regex { static TITLE_REGEX: OnceLock<Regex> = OnceLock::new(); TITLE_REGEX.get_or_init(|| Regex::new(r"<title>\s*(.*?)\s*").unwrap()) } + +fn generate_big_table_of_actions() -> String { + let actions = &*ALL_ACTIONS; + let mut output = String::new(); + + let mut actions_sorted = actions.iter().collect::>(); + actions_sorted.sort_by_key(|a| a.name); + + // Start the definition list with custom styling for better spacing + output.push_str("
\n"); + + for action in actions_sorted.into_iter() { + // Add the humanized action name as the term with margin + output.push_str( + "
", + ); + output.push_str(&action.human_name); + output.push_str("
\n"); + + // Add the definition with keymap name and description + output.push_str("
\n"); + + // Add the description, escaping HTML if needed + if let Some(description) = action.docs { + output.push_str( + &description + .replace("&", "&") + .replace("<", "<") + .replace(">", ">"), + ); + output.push_str("
\n"); + } + output.push_str("Keymap Name: "); + output.push_str(action.name); + output.push_str("
\n"); + if !action.deprecated_aliases.is_empty() { + output.push_str("Deprecated Aliases:"); + for alias in action.deprecated_aliases.iter() { + output.push_str(""); + output.push_str(alias); + output.push_str(", "); + } + } + output.push_str("\n
\n"); + } + + // Close the definition list + output.push_str("
\n"); + + output +} diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index c7af36f4310f71887adb84a881007fb677b680bb..251cad6234f10d73f423680bcd600500daae65b2 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -16,6 +16,7 @@ - [Configuring Zed](./configuring-zed.md) - [Configuring Languages](./configuring-languages.md) - [Key bindings](./key-bindings.md) + - [All Actions](./all-actions.md) - [Snippets](./snippets.md) - [Themes](./themes.md) - [Icon Themes](./icon-themes.md) diff --git a/docs/src/all-actions.md b/docs/src/all-actions.md new file mode 100644 index 0000000000000000000000000000000000000000..d20f7cfd63c01c03937c85e8f46476711c80e30f --- /dev/null +++ b/docs/src/all-actions.md @@ -0,0 +1,3 @@ +## All Actions + +{#ACTIONS_TABLE#} From 8ef9ecc91f6c6b2eaf65fd0d8a93e2f49af876de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Thu, 21 Aug 2025 08:08:54 +0800 Subject: [PATCH 215/744] windows: Fix `RevealInFileManager` (#36592) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #36314 This PR takes inspiration from [Electron’s implementation](https://github.com/electron/electron/blob/dd54e84a58531b52680f7f736f593ee887eff6a7/shell/common/platform_util_win.cc#L268-L314). Before and after: https://github.com/user-attachments/assets/53eec5d3-23c7-4ee1-8477-e524b0538f60 Release Notes: - N/A --- crates/gpui/src/platform/windows/platform.rs | 111 +++++++++++-------- 1 file changed, 67 insertions(+), 44 deletions(-) diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index b13b9915f135b7ff6c4aafe2d04f670f7416aca1..6202e05fb3b26f10ba8fdf365a185dccbc6ae2ed 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -1,5 +1,6 @@ use std::{ cell::RefCell, + ffi::OsStr, mem::ManuallyDrop, path::{Path, PathBuf}, rc::Rc, @@ -460,13 +461,15 @@ impl Platform for WindowsPlatform { } fn open_url(&self, url: &str) { + if url.is_empty() { + return; + } let url_string = url.to_string(); self.background_executor() .spawn(async move { - if url_string.is_empty() { - return; - } - open_target(url_string.as_str()); + open_target(&url_string) + .with_context(|| format!("Opening url: {}", url_string)) + .log_err(); }) .detach(); } @@ -514,37 +517,29 @@ impl Platform for WindowsPlatform { } fn reveal_path(&self, path: &Path) { - let Ok(file_full_path) = path.canonicalize() else { - log::error!("unable to parse file path"); + if path.as_os_str().is_empty() { return; - }; + } + let path = path.to_path_buf(); self.background_executor() .spawn(async move { - let Some(path) = file_full_path.to_str() else { - return; - }; - if path.is_empty() { - return; - } - open_target_in_explorer(path); + open_target_in_explorer(&path) + .with_context(|| format!("Revealing path {} in explorer", path.display())) + .log_err(); }) .detach(); } fn open_with_system(&self, path: &Path) { - let Ok(full_path) = path.canonicalize() else { - log::error!("unable to parse file full path: {}", path.display()); + if path.as_os_str().is_empty() { return; - }; + } + let path = path.to_path_buf(); self.background_executor() .spawn(async move { - let Some(full_path_str) = full_path.to_str() else { - return; - }; - if full_path_str.is_empty() { - return; - }; - open_target(full_path_str); + open_target(&path) + .with_context(|| format!("Opening {} with system", path.display())) + .log_err(); }) .detach(); } @@ -735,39 +730,67 @@ pub(crate) struct WindowCreationInfo { pub(crate) disable_direct_composition: bool, } -fn open_target(target: &str) { - unsafe { - let ret = ShellExecuteW( +fn open_target(target: impl AsRef) -> Result<()> { + let target = target.as_ref(); + let ret = unsafe { + ShellExecuteW( None, windows::core::w!("open"), &HSTRING::from(target), None, None, SW_SHOWDEFAULT, - ); - if ret.0 as isize <= 32 { - log::error!("Unable to open target: {}", std::io::Error::last_os_error()); - } + ) + }; + if ret.0 as isize <= 32 { + Err(anyhow::anyhow!( + "Unable to open target: {}", + std::io::Error::last_os_error() + )) + } else { + Ok(()) } } -fn open_target_in_explorer(target: &str) { +fn open_target_in_explorer(target: &Path) -> Result<()> { + let dir = target.parent().context("No parent folder found")?; + let desktop = unsafe { SHGetDesktopFolder()? }; + + let mut dir_item = std::ptr::null_mut(); unsafe { - let ret = ShellExecuteW( + desktop.ParseDisplayName( + HWND::default(), None, - windows::core::w!("open"), - windows::core::w!("explorer.exe"), - &HSTRING::from(format!("/select,{}", target).as_str()), + &HSTRING::from(dir), None, - SW_SHOWDEFAULT, - ); - if ret.0 as isize <= 32 { - log::error!( - "Unable to open target in explorer: {}", - std::io::Error::last_os_error() - ); - } + &mut dir_item, + std::ptr::null_mut(), + )?; } + + let mut file_item = std::ptr::null_mut(); + unsafe { + desktop.ParseDisplayName( + HWND::default(), + None, + &HSTRING::from(target), + None, + &mut file_item, + std::ptr::null_mut(), + )?; + } + + let highlight = [file_item as *const _]; + unsafe { SHOpenFolderAndSelectItems(dir_item as _, Some(&highlight), 0) }.or_else(|err| { + if err.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 { + // On some systems, the above call mysteriously fails with "file not + // found" even though the file is there. In these cases, ShellExecute() + // seems to work as a fallback (although it won't select the file). + open_target(dir).context("Opening target parent folder") + } else { + Err(anyhow::anyhow!("Can not open target path: {}", err)) + } + }) } fn file_open_dialog( From 6f242772cccaf3a8b2dc372cc1c2f94713faedf3 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 20 Aug 2025 21:10:36 -0300 Subject: [PATCH 216/744] acp: Update to 0.0.30 (#36643) See: https://github.com/zed-industries/agent-client-protocol/pull/20 Release Notes: - N/A --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 2 +- crates/acp_thread/src/connection.rs | 2 +- crates/agent2/src/tests/mod.rs | 4 ++-- crates/agent2/src/thread.rs | 2 +- crates/agent_servers/src/acp/v1.rs | 4 ++-- crates/agent_servers/src/claude.rs | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bfb135d32cfcdbb11b13ee79bde65ed51692f350..f3e821fb5f322e83bbe90a9122b51e30ee555e37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.29" +version = "0.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a2cd7e0bd2bb7ed27687cfcf6561b91542c1ce23e52fd54ee59b7568c9bd84" +checksum = "5f792e009ba59b137ee1db560bc37e567887ad4b5af6f32181d381fff690e2d4" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index 3f547459002011dfdf1b9597226c1f03fe03f456..d458a4752cfbcb048eadb1cd0c8acecc404613bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -423,7 +423,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.29" +agent-client-protocol = "0.0.30" 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 9833e1957c589f99c2051fef4b59c699f9c249f7..61bc50576abd8a53433d5738acb98f245ec92764 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1381,7 +1381,7 @@ impl AcpThread { let canceled = matches!( result, Ok(Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Canceled + stop_reason: acp::StopReason::Cancelled })) ); diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 791b16141762dc941729042063c1e5923f35b346..2bbd36487392452dc9a179c01f759e9fb68a7384 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -420,7 +420,7 @@ mod test_support { .response_tx .take() { - end_turn_tx.send(acp::StopReason::Canceled).unwrap(); + end_turn_tx.send(acp::StopReason::Cancelled).unwrap(); } } diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 478604b14a8934e6f736f3632cdac6853b7b42bb..3bd1be497ef3edcbbcf0429e75c956e70cba0770 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -975,7 +975,7 @@ async fn test_cancellation(cx: &mut TestAppContext) { assert!( matches!( last_event, - Some(Ok(ThreadEvent::Stop(acp::StopReason::Canceled))) + Some(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled))) ), "unexpected event {last_event:?}" ); @@ -1029,7 +1029,7 @@ async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) { fake_model.end_last_completion_stream(); let events_1 = events_1.collect::>().await; - assert_eq!(stop_events(events_1), vec![acp::StopReason::Canceled]); + assert_eq!(stop_events(events_1), vec![acp::StopReason::Cancelled]); let events_2 = events_2.collect::>().await; assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]); } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 62174fd3b475cf14f9259d0a18b6a2685dba6407..d34c92915228753f6edfe5d90a81428e27f5a562 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -2248,7 +2248,7 @@ impl ThreadEventStream { fn send_canceled(&self) { self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Canceled))) + .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled))) .ok(); } diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index bc11a3748ac3603e50dd6505cdbccaba34244160..29f389547d0ae5daecca85a95cb8b9b63530fc34 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -242,7 +242,7 @@ impl AgentConnection for AcpConnection { if suppress_abort_err && details.contains("This operation was aborted") { Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Canceled, + stop_reason: acp::StopReason::Cancelled, }) } else { Err(anyhow!(details)) @@ -302,7 +302,7 @@ impl acp::Client for ClientDelegate { let outcome = match result { Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, - Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled, + Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, }; Ok(acp::RequestPermissionResponse { outcome }) diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 8d93557e1ca28531ad86df3767ed936aeed50b45..c9290e0ba52b55a82697980624cb69f2f02cfd7f 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -705,7 +705,7 @@ impl ClaudeAgentSession { let stop_reason = match subtype { ResultErrorType::Success => acp::StopReason::EndTurn, ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests, - ResultErrorType::ErrorDuringExecution => acp::StopReason::Canceled, + ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled, }; end_turn_tx .send(Ok(acp::PromptResponse { stop_reason })) From 568e1d0a42a517b62ede343f31cee7779b09e9ea Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 21 Aug 2025 02:36:50 +0200 Subject: [PATCH 217/744] acp: Add e2e test support for NativeAgent (#36635) Release Notes: - N/A --- Cargo.lock | 4 + crates/agent2/Cargo.toml | 2 + crates/agent2/src/native_agent_server.rs | 49 ++++++++ crates/agent_servers/Cargo.toml | 11 +- crates/agent_servers/src/agent_servers.rs | 4 +- crates/agent_servers/src/claude.rs | 2 +- crates/agent_servers/src/e2e_tests.rs | 136 +++++++++++++++++----- crates/agent_servers/src/gemini.rs | 2 +- 8 files changed, 173 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f3e821fb5f322e83bbe90a9122b51e30ee555e37..76f8672d4d0d126f47e12a14e694d84e6a765f9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,11 +268,14 @@ dependencies = [ "agent_settings", "agentic-coding-protocol", "anyhow", + "client", "collections", "context_server", "env_logger 0.11.8", + "fs", "futures 0.3.31", "gpui", + "gpui_tokio", "indoc", "itertools 0.14.0", "language", @@ -284,6 +287,7 @@ dependencies = [ "paths", "project", "rand 0.8.5", + "reqwest_client", "schemars", "semver", "serde", diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 2a5d879e9ecdb83037411c65647b062cebecad7e..8dd79062f89eee6eb6525f8a699612b5ffbf248a 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -10,6 +10,7 @@ path = "src/agent2.rs" [features] test-support = ["db/test-support"] +e2e = [] [lints] workspace = true @@ -72,6 +73,7 @@ zstd.workspace = true [dev-dependencies] agent = { workspace = true, "features" = ["test-support"] } +agent_servers = { workspace = true, "features" = ["test-support"] } assistant_context = { workspace = true, "features" = ["test-support"] } ctor.workspace = true client = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index a1f935589af86c56bbddf27f0b887703a25cb983..ac5aa95c043e5128ba11f4f1f8930950e836474c 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -73,3 +73,52 @@ impl AgentServer for NativeAgentServer { self } } + +#[cfg(test)] +mod tests { + use super::*; + + use assistant_context::ContextStore; + use gpui::AppContext; + + agent_servers::e2e_tests::common_e2e_tests!( + async |fs, project, cx| { + let auth = cx.update(|cx| { + prompt_store::init(cx); + terminal::init(cx); + + let registry = language_model::LanguageModelRegistry::read_global(cx); + let auth = registry + .provider(&language_model::ANTHROPIC_PROVIDER_ID) + .unwrap() + .authenticate(cx); + + cx.spawn(async move |_| auth.await) + }); + + auth.await.unwrap(); + + cx.update(|cx| { + let registry = language_model::LanguageModelRegistry::global(cx); + + registry.update(cx, |registry, cx| { + registry.select_default_model( + Some(&language_model::SelectedModel { + provider: language_model::ANTHROPIC_PROVIDER_ID, + model: language_model::LanguageModelId("claude-sonnet-4-latest".into()), + }), + cx, + ); + }); + }); + + let history = cx.update(|cx| { + let context_store = cx.new(move |cx| ContextStore::fake(project.clone(), cx)); + cx.new(move |cx| HistoryStore::new(context_store, cx)) + }); + + NativeAgentServer::new(fs.clone(), history) + }, + allow_option_id = "allow" + ); +} diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index b654486cb60f46cf15fefbafa5d64e4b2310e1f3..60dd7964639ef59794b6e8bbe11192d9a33cbe01 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -6,7 +6,7 @@ publish.workspace = true license = "GPL-3.0-or-later" [features] -test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support"] +test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "fs", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"] e2e = [] [lints] @@ -23,10 +23,14 @@ agent-client-protocol.workspace = true agent_settings.workspace = true agentic-coding-protocol.workspace = true anyhow.workspace = true +client = { workspace = true, optional = true } collections.workspace = true context_server.workspace = true +env_logger = { workspace = true, optional = true } +fs = { workspace = true, optional = true } futures.workspace = true gpui.workspace = true +gpui_tokio = { workspace = true, optional = true } indoc.workspace = true itertools.workspace = true language.workspace = true @@ -36,6 +40,7 @@ log.workspace = true paths.workspace = true project.workspace = true rand.workspace = true +reqwest_client = { workspace = true, optional = true } schemars.workspace = true semver.workspace = true serde.workspace = true @@ -57,8 +62,12 @@ libc.workspace = true nix.workspace = true [dev-dependencies] +client = { workspace = true, features = ["test-support"] } env_logger.workspace = true +fs.workspace = true language.workspace = true indoc.workspace = true acp_thread = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } +gpui_tokio.workspace = true +reqwest_client = { workspace = true, features = ["test-support"] } diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index cebf82cddb00be91f7b12aca369883cd5cce9fde..2f5ec478ae8288f4e4db3db84c622d2c03c615be 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -3,8 +3,8 @@ mod claude; mod gemini; mod settings; -#[cfg(test)] -mod e2e_tests; +#[cfg(any(test, feature = "test-support"))] +pub mod e2e_tests; pub use claude::*; pub use gemini::*; diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index c9290e0ba52b55a82697980624cb69f2f02cfd7f..ef666974f1e6a29345e469d8cf19fe13a3fd02eb 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1093,7 +1093,7 @@ pub(crate) mod tests { use gpui::TestAppContext; use serde_json::json; - crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow"); + crate::common_e2e_tests!(async |_, _, _| ClaudeCode, allow_option_id = "allow"); pub fn local_command() -> AgentServerCommand { AgentServerCommand { diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 8b2703575d9ea68ff7ba2bf1b71877f02e01c36a..c2710790719251980bc39bb9367a62fdc3cee4a3 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -4,21 +4,30 @@ use std::{ time::Duration, }; -use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings}; +use crate::AgentServer; use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus}; use agent_client_protocol as acp; use futures::{FutureExt, StreamExt, channel::mpsc, select}; -use gpui::{Entity, TestAppContext}; +use gpui::{AppContext, Entity, TestAppContext}; use indoc::indoc; use project::{FakeFs, Project}; -use settings::{Settings, SettingsStore}; use util::path; -pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let fs = init_test(cx).await; - let project = Project::test(fs, [], cx).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; +pub async fn test_basic(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as Arc; + let project = Project::test(fs.clone(), [], cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; thread .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) @@ -42,8 +51,12 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont }); } -pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let _fs = init_test(cx).await; +pub async fn test_path_mentions(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as _; let tempdir = tempfile::tempdir().unwrap(); std::fs::write( @@ -56,7 +69,13 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes ) .expect("failed to write file"); let project = Project::example([tempdir.path()], &mut cx.to_async()).await; - let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + tempdir.path(), + cx, + ) + .await; thread .update(cx, |thread, cx| { thread.send( @@ -110,15 +129,25 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes drop(tempdir); } -pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let _fs = init_test(cx).await; +pub async fn test_tool_call(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as _; let tempdir = tempfile::tempdir().unwrap(); let foo_path = tempdir.path().join("foo"); std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file"); let project = Project::example([tempdir.path()], &mut cx.to_async()).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; thread .update(cx, |thread, cx| { @@ -152,14 +181,23 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp drop(tempdir); } -pub async fn test_tool_call_with_permission( - server: impl AgentServer + 'static, +pub async fn test_tool_call_with_permission( + server: F, allow_option_id: acp::PermissionOptionId, cx: &mut TestAppContext, -) { - let fs = init_test(cx).await; - let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; +) where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as Arc; + let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; let full_turn = thread.update(cx, |thread, cx| { thread.send_raw( r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, @@ -247,11 +285,21 @@ pub async fn test_tool_call_with_permission( }); } -pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let fs = init_test(cx).await; - - let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; +pub async fn test_cancel(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as Arc; + + let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; let _ = thread.update(cx, |thread, cx| { thread.send_raw( r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, @@ -316,10 +364,20 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon }); } -pub async fn test_thread_drop(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let fs = init_test(cx).await; - let project = Project::test(fs, [], cx).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; +pub async fn test_thread_drop(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as Arc; + let project = Project::test(fs.clone(), [], cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; thread .update(cx, |thread, cx| thread.send_raw("Hello from test!", cx)) @@ -386,25 +444,39 @@ macro_rules! common_e2e_tests { } }; } +pub use common_e2e_tests; // Helpers pub async fn init_test(cx: &mut TestAppContext) -> Arc { + #[cfg(test)] + use settings::Settings; + env_logger::try_init().ok(); cx.update(|cx| { - let settings_store = SettingsStore::test(cx); + let settings_store = settings::SettingsStore::test(cx); cx.set_global(settings_store); Project::init_settings(cx); language::init(cx); + gpui_tokio::init(cx); + let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap(); + cx.set_http_client(Arc::new(http_client)); + client::init_settings(cx); + let client = client::Client::production(cx); + let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx)); + language_model::init(client.clone(), cx); + language_models::init(user_store, client, cx); + agent_settings::init(cx); crate::settings::init(cx); + #[cfg(test)] crate::AllAgentServersSettings::override_global( - AllAgentServersSettings { - claude: Some(AgentServerSettings { + crate::AllAgentServersSettings { + claude: Some(crate::AgentServerSettings { command: crate::claude::tests::local_command(), }), - gemini: Some(AgentServerSettings { + gemini: Some(crate::AgentServerSettings { command: crate::gemini::tests::local_command(), }), }, diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index d30525328bff99a4abf59ba4a88139e8cfda6566..1a63322fac2e4577f0998c53cdd2a01ad86e1dd1 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -108,7 +108,7 @@ pub(crate) mod tests { use crate::AgentServerCommand; use std::path::Path; - crate::common_e2e_tests!(Gemini, allow_option_id = "proceed_once"); + crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once"); pub fn local_command() -> AgentServerCommand { let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) From 9a3e4c47d03ab8579601ce55d066518a0e867c3a Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 20 Aug 2025 21:52:38 -0300 Subject: [PATCH 218/744] acp: Suggest upgrading to preview instead of latest (#36648) A previous PR changed the install command from `@latest` to `@preview`, but the upgrade command kept suggesting `@latest`. Release Notes: - N/A --- crates/agent_servers/src/gemini.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 1a63322fac2e4577f0998c53cdd2a01ad86e1dd1..3b892e793140462fdcc9b1f03242c96adcd6e059 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -89,7 +89,7 @@ impl AgentServer for Gemini { current_version ).into(), upgrade_message: "Upgrade Gemini CLI to latest".into(), - upgrade_command: "npm install -g @google/gemini-cli@latest".into(), + upgrade_command: "npm install -g @google/gemini-cli@preview".into(), }.into()) } } From 4b03d791b5ed73d9dd28bf1279b807648d38b399 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Aug 2025 20:38:30 -0600 Subject: [PATCH 219/744] Remove style lints for now (#36651) Closes #36577 Release Notes: - N/A --- Cargo.toml | 151 +++++------------------------------------------------ 1 file changed, 13 insertions(+), 138 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d458a4752cfbcb048eadb1cd0c8acecc404613bf..b6104303b7ee2c7843a3aa20dbfb2ddce1eaba19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -802,147 +802,26 @@ unexpected_cfgs = { level = "allow" } dbg_macro = "deny" todo = "deny" -# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so -# warning on this rule produces a lot of noise. -single_range_in_vec_init = "allow" - -redundant_clone = "warn" -declare_interior_mutable_const = "deny" - -# These are all of the rules that currently have violations in the Zed -# codebase. +# We currently do not restrict any style rules +# as it slows down shipping code to Zed. # -# We'll want to drive this list down by either: -# 1. fixing violations of the rule and begin enforcing it -# 2. deciding we want to allow the rule permanently, at which point -# we should codify that separately above. +# Running ./script/clippy can take several minutes, and so it's +# common to skip that step and let CI do it. Any unexpected failures +# (which also take minutes to discover) thus require switching back +# to an old branch, manual fixing, and re-pushing. # -# This list shouldn't be added to; it should only get shorter. -# ============================================================================= - -# There are a bunch of rules currently failing in the `style` group, so -# allow all of those, for now. +# In the future we could improve this by either making sure +# Zed can surface clippy errors in diagnostics (in addition to the +# rust-analyzer errors), or by having CI fix style nits automatically. style = { level = "allow", priority = -1 } -# Temporary list of style lints that we've fixed so far. -# Progress is being tracked in #36577 -blocks_in_conditions = "warn" -bool_assert_comparison = "warn" -borrow_interior_mutable_const = "warn" -box_default = "warn" -builtin_type_shadow = "warn" -bytes_nth = "warn" -chars_next_cmp = "warn" -cmp_null = "warn" -collapsible_else_if = "warn" -collapsible_if = "warn" -comparison_to_empty = "warn" -default_instead_of_iter_empty = "warn" -disallowed_macros = "warn" -disallowed_methods = "warn" -disallowed_names = "warn" -disallowed_types = "warn" -doc_lazy_continuation = "warn" -doc_overindented_list_items = "warn" -duplicate_underscore_argument = "warn" -err_expect = "warn" -fn_to_numeric_cast = "warn" -fn_to_numeric_cast_with_truncation = "warn" -for_kv_map = "warn" -implicit_saturating_add = "warn" -implicit_saturating_sub = "warn" -inconsistent_digit_grouping = "warn" -infallible_destructuring_match = "warn" -inherent_to_string = "warn" -init_numbered_fields = "warn" -into_iter_on_ref = "warn" -io_other_error = "warn" -items_after_test_module = "warn" -iter_cloned_collect = "warn" -iter_next_slice = "warn" -iter_nth = "warn" -iter_nth_zero = "warn" -iter_skip_next = "warn" -just_underscores_and_digits = "warn" -len_zero = "warn" -let_and_return = "warn" -main_recursion = "warn" -manual_bits = "warn" -manual_dangling_ptr = "warn" -manual_is_ascii_check = "warn" -manual_is_finite = "warn" -manual_is_infinite = "warn" -manual_map = "warn" -manual_next_back = "warn" -manual_non_exhaustive = "warn" -manual_ok_or = "warn" -manual_pattern_char_comparison = "warn" -manual_rotate = "warn" -manual_slice_fill = "warn" -manual_while_let_some = "warn" -map_clone = "warn" -map_collect_result_unit = "warn" -match_like_matches_macro = "warn" -match_overlapping_arm = "warn" -mem_replace_option_with_none = "warn" -mem_replace_option_with_some = "warn" -missing_enforced_import_renames = "warn" -missing_safety_doc = "warn" -mixed_attributes_style = "warn" -mixed_case_hex_literals = "warn" -module_inception = "warn" -must_use_unit = "warn" -mut_mutex_lock = "warn" -needless_borrow = "warn" -needless_doctest_main = "warn" -needless_else = "warn" -needless_parens_on_range_literals = "warn" -needless_pub_self = "warn" -needless_return = "warn" -needless_return_with_question_mark = "warn" -non_minimal_cfg = "warn" -ok_expect = "warn" -owned_cow = "warn" -print_literal = "warn" -print_with_newline = "warn" -println_empty_string = "warn" -ptr_eq = "warn" -question_mark = "warn" -redundant_closure = "warn" -redundant_field_names = "warn" -redundant_pattern_matching = "warn" -redundant_static_lifetimes = "warn" -result_map_or_into_option = "warn" -self_named_constructors = "warn" -single_match = "warn" -tabs_in_doc_comments = "warn" -to_digit_is_some = "warn" -toplevel_ref_arg = "warn" -unnecessary_fold = "warn" -unnecessary_map_or = "warn" -unnecessary_mut_passed = "warn" -unnecessary_owned_empty_strings = "warn" -unneeded_struct_pattern = "warn" -unsafe_removed_from_name = "warn" -unused_unit = "warn" -unusual_byte_groupings = "warn" -while_let_on_iterator = "warn" -write_literal = "warn" -write_with_newline = "warn" -writeln_empty_string = "warn" -wrong_self_convention = "warn" -zero_ptr = "warn" - # Individual rules that have violations in the codebase: type_complexity = "allow" -# We often return trait objects from `new` functions. -new_ret_no_self = { level = "allow" } -# We have a few `next` functions that differ in lifetimes -# compared to Iterator::next. Yet, clippy complains about those. -should_implement_trait = { level = "allow" } let_underscore_future = "allow" -# It doesn't make sense to implement `Default` unilaterally. -new_without_default = "allow" + +# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so +# warning on this rule produces a lot of noise. +single_range_in_vec_init = "allow" # in Rust it can be very tedious to reduce argument count without # running afoul of the borrow checker. @@ -951,10 +830,6 @@ too_many_arguments = "allow" # We often have large enum variants yet we rarely actually bother with splitting them up. large_enum_variant = "allow" -# `enum_variant_names` fires for all enums, even when they derive serde traits. -# Adhering to this lint would be a breaking change. -enum_variant_names = "allow" - [workspace.metadata.cargo-machete] ignored = [ "bindgen", From c731bb6d91d0d8c1c0bf29d17c8cba8eed3b51a5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Aug 2025 21:08:49 -0600 Subject: [PATCH 220/744] Re-add redundant clone (#36652) Although I said I'd do this, I actually didn't... Updates #36651 Release Notes: - N/A --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index b6104303b7ee2c7843a3aa20dbfb2ddce1eaba19..400ce791aa43c197593ba28e9e5114a687f731ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -802,6 +802,9 @@ unexpected_cfgs = { level = "allow" } dbg_macro = "deny" todo = "deny" +# trying this out +redundant_clone = "deny" + # We currently do not restrict any style rules # as it slows down shipping code to Zed. # From 5dcb90858effc47c7f2768b03ddb2a81b443ec8e Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 21 Aug 2025 09:24:34 +0300 Subject: [PATCH 221/744] Stop waiting for part of LSP responses on remote Collab clients' part (#36557) Instead of holding a connection for potentially long LSP queries (e.g. rust-analyzer might take minutes to look up a definition), disconnect right after sending the initial request and handle the follow-up responses later. As a bonus, this allows to cancel previously sent request on the local Collab clients' side due to this, as instead of holding and serving the old connection, local clients now can stop previous requests, if needed. Current PR does not convert all LSP requests to the new paradigm, but the problematic ones, deprecating `MultiLspQuery` and moving all its requests to the new paradigm. Release Notes: - Improved resource usage when querying LSP over Collab --------- Co-authored-by: David Kleingeld Co-authored-by: Mikayla Maki Co-authored-by: David Kleingeld --- crates/agent_ui/src/acp/message_editor.rs | 8 +- crates/collab/src/rpc.rs | 20 + crates/collab/src/tests/editor_tests.rs | 208 ++- crates/collab/src/tests/integration_tests.rs | 12 +- crates/editor/src/editor.rs | 24 +- crates/editor/src/hover_links.rs | 2 +- crates/editor/src/hover_popover.rs | 2 +- crates/editor/src/proposed_changes_editor.rs | 4 +- crates/editor/src/signature_help.rs | 4 +- crates/lsp/src/lsp.rs | 2 +- crates/project/src/lsp_command.rs | 3 +- crates/project/src/lsp_store.rs | 1202 ++++++++++-------- crates/project/src/project.rs | 49 +- crates/project/src/project_tests.rs | 8 +- crates/proto/proto/lsp.proto | 91 +- crates/proto/proto/zed.proto | 5 +- crates/proto/src/macros.rs | 29 + crates/proto/src/proto.rs | 45 + crates/proto/src/typed_envelope.rs | 52 + crates/rpc/src/proto_client.rs | 304 ++++- 20 files changed, 1394 insertions(+), 680 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index be133808b7731961f2f5f6cbd4197f64159b713f..1155285d09e487d5fb87e6a62cefae9bd2aac7e0 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1691,7 +1691,7 @@ impl SemanticsProvider for SlashCommandSemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>> { + ) -> Option>>> { let snapshot = buffer.read(cx).snapshot(); let offset = position.to_offset(&snapshot); let (start, end) = self.range.get()?; @@ -1699,14 +1699,14 @@ impl SemanticsProvider for SlashCommandSemanticsProvider { return None; } let range = snapshot.anchor_after(start)..snapshot.anchor_after(end); - Some(Task::ready(vec![project::Hover { + Some(Task::ready(Some(vec![project::Hover { contents: vec![project::HoverBlock { text: "Slash commands are not supported".into(), kind: project::HoverBlockKind::PlainText, }], range: Some(range), language: None, - }])) + }]))) } fn inline_values( @@ -1756,7 +1756,7 @@ impl SemanticsProvider for SlashCommandSemanticsProvider { _position: text::Anchor, _kind: editor::GotoDefinitionKind, _cx: &mut App, - ) -> Option>>> { + ) -> Option>>>> { None } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 06eb68610f0b0b97f42b54ae00c54754de646b0a..73f327166a3f1fb40a1f232ea2fabcdedd3fb129 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -400,6 +400,8 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(multi_lsp_query) + .add_request_handler(lsp_query) + .add_message_handler(broadcast_project_message_from_host::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) @@ -910,7 +912,9 @@ impl Server { user_id=field::Empty, login=field::Empty, impersonator=field::Empty, + // todo(lsp) remove after Zed Stable hits v0.204.x multi_lsp_query_request=field::Empty, + lsp_query_request=field::Empty, release_channel=field::Empty, { TOTAL_DURATION_MS }=field::Empty, { PROCESSING_DURATION_MS }=field::Empty, @@ -2356,6 +2360,7 @@ where Ok(()) } +// todo(lsp) remove after Zed Stable hits v0.204.x async fn multi_lsp_query( request: MultiLspQuery, response: Response, @@ -2366,6 +2371,21 @@ async fn multi_lsp_query( forward_mutating_project_request(request, response, session).await } +async fn lsp_query( + request: proto::LspQuery, + response: Response, + session: MessageContext, +) -> Result<()> { + let (name, should_write) = request.query_name_and_write_permissions(); + tracing::Span::current().record("lsp_query_request", name); + tracing::info!("lsp_query message received"); + if should_write { + forward_mutating_project_request(request, response, session).await + } else { + forward_read_only_project_request(request, response, session).await + } +} + /// Notify other participants that a new buffer has been created async fn create_buffer_for_peer( request: proto::CreateBufferForPeer, diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 1b0c581983ac54e9fdea074947bd8eaae4764c81..59d66f1821e60ecbf3a7550c1385fa6de7ae047d 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -15,13 +15,14 @@ use editor::{ }, }; use fs::Fs; -use futures::{StreamExt, lock::Mutex}; +use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex}; use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext}; use indoc::indoc; use language::{ FakeLspAdapter, language_settings::{AllLanguageSettings, InlayHintSettings}, }; +use lsp::LSP_REQUEST_TIMEOUT; use project::{ ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT, lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro}, @@ -1017,6 +1018,211 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T }) } +#[gpui::test] +async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let mut server = TestServer::start(cx_a.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + cx_b.update(editor::init); + + let command_name = "test_command"; + let capabilities = lsp::ServerCapabilities { + code_lens_provider: Some(lsp::CodeLensOptions { + resolve_provider: None, + }), + execute_command_provider: Some(lsp::ExecuteCommandOptions { + commands: vec![command_name.to_string()], + ..lsp::ExecuteCommandOptions::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: 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() + .insert_tree( + path!("/dir"), + json!({ + "one.rs": "const ONE: usize = 1;" + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let editor_b = workspace_b + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "one.rs"), None, true, window, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let (lsp_store_b, buffer_b) = editor_b.update(cx_b, |editor, cx| { + let lsp_store = editor.project().unwrap().read(cx).lsp_store(); + let buffer = editor.buffer().read(cx).as_singleton().unwrap(); + (lsp_store, buffer) + }); + let fake_language_server = fake_language_servers.next().await.unwrap(); + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + let long_request_time = LSP_REQUEST_TIMEOUT / 2; + let (request_started_tx, mut request_started_rx) = mpsc::unbounded(); + let requests_started = Arc::new(AtomicUsize::new(0)); + let requests_completed = Arc::new(AtomicUsize::new(0)); + let _lens_requests = fake_language_server + .set_request_handler::({ + let request_started_tx = request_started_tx.clone(); + let requests_started = requests_started.clone(); + let requests_completed = requests_completed.clone(); + move |params, cx| { + let mut request_started_tx = request_started_tx.clone(); + let requests_started = requests_started.clone(); + let requests_completed = requests_completed.clone(); + async move { + assert_eq!( + params.text_document.uri.as_str(), + uri!("file:///dir/one.rs") + ); + requests_started.fetch_add(1, atomic::Ordering::Release); + request_started_tx.send(()).await.unwrap(); + cx.background_executor().timer(long_request_time).await; + let i = requests_completed.fetch_add(1, atomic::Ordering::Release) + 1; + Ok(Some(vec![lsp::CodeLens { + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 9)), + command: Some(lsp::Command { + title: format!("LSP Command {i}"), + command: command_name.to_string(), + arguments: None, + }), + data: None, + }])) + } + } + }); + + // Move cursor to a location, this should trigger the code lens call. + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([7..7]) + }); + }); + let () = request_started_rx.next().await.unwrap(); + assert_eq!( + requests_started.load(atomic::Ordering::Acquire), + 1, + "Selection change should have initiated the first request" + ); + assert_eq!( + requests_completed.load(atomic::Ordering::Acquire), + 0, + "Slow requests should be running still" + ); + let _first_task = lsp_store_b.update(cx_b, |lsp_store, cx| { + lsp_store + .forget_code_lens_task(buffer_b.read(cx).remote_id()) + .expect("Should have the fetch task started") + }); + + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([1..1]) + }); + }); + let () = request_started_rx.next().await.unwrap(); + assert_eq!( + requests_started.load(atomic::Ordering::Acquire), + 2, + "Selection change should have initiated the second request" + ); + assert_eq!( + requests_completed.load(atomic::Ordering::Acquire), + 0, + "Slow requests should be running still" + ); + let _second_task = lsp_store_b.update(cx_b, |lsp_store, cx| { + lsp_store + .forget_code_lens_task(buffer_b.read(cx).remote_id()) + .expect("Should have the fetch task started for the 2nd time") + }); + + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([2..2]) + }); + }); + let () = request_started_rx.next().await.unwrap(); + assert_eq!( + requests_started.load(atomic::Ordering::Acquire), + 3, + "Selection change should have initiated the third request" + ); + assert_eq!( + requests_completed.load(atomic::Ordering::Acquire), + 0, + "Slow requests should be running still" + ); + + _first_task.await.unwrap(); + _second_task.await.unwrap(); + cx_b.run_until_parked(); + assert_eq!( + requests_started.load(atomic::Ordering::Acquire), + 3, + "No selection changes should trigger no more code lens requests" + ); + assert_eq!( + requests_completed.load(atomic::Ordering::Acquire), + 3, + "After enough time, all 3 LSP requests should have been served by the language server" + ); + let resulting_lens_actions = editor_b + .update(cx_b, |editor, cx| { + let lsp_store = editor.project().unwrap().read(cx).lsp_store(); + lsp_store.update(cx, |lsp_store, cx| { + lsp_store.code_lens_actions(&buffer_b, cx) + }) + }) + .await + .unwrap() + .unwrap(); + assert_eq!( + resulting_lens_actions.len(), + 1, + "Should have fetched one code lens action, but got: {resulting_lens_actions:?}" + ); + assert_eq!( + resulting_lens_actions.first().unwrap().lsp_action.title(), + "LSP Command 3", + "Only the final code lens action should be in the data" + ) +} + #[gpui::test(iterations = 10)] async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.executor()).await; diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index e01736f0ef9daced0d35742d08503ea8b188f733..5c732530480a14ab28e231aa0fae1b79ef2703fb 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4850,6 +4850,7 @@ async fn test_definition( let definitions_1 = project_b .update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx)) .await + .unwrap() .unwrap(); cx_b.read(|cx| { assert_eq!( @@ -4885,6 +4886,7 @@ async fn test_definition( let definitions_2 = project_b .update(cx_b, |p, cx| p.definitions(&buffer_b, 33, cx)) .await + .unwrap() .unwrap(); cx_b.read(|cx| { assert_eq!(definitions_2.len(), 1); @@ -4922,6 +4924,7 @@ async fn test_definition( let type_definitions = project_b .update(cx_b, |p, cx| p.type_definitions(&buffer_b, 7, cx)) .await + .unwrap() .unwrap(); cx_b.read(|cx| { assert_eq!( @@ -5060,7 +5063,7 @@ async fn test_references( ]))) .unwrap(); - let references = references.await.unwrap(); + let references = references.await.unwrap().unwrap(); executor.run_until_parked(); project_b.read_with(cx_b, |project, cx| { // User is informed that a request is no longer pending. @@ -5104,7 +5107,7 @@ async fn test_references( lsp_response_tx .unbounded_send(Err(anyhow!("can't find references"))) .unwrap(); - assert_eq!(references.await.unwrap(), []); + assert_eq!(references.await.unwrap().unwrap(), []); // User is informed that the request is no longer pending. executor.run_until_parked(); @@ -5505,7 +5508,8 @@ async fn test_lsp_hover( // Request hover information as the guest. let mut hovers = project_b .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx)) - .await; + .await + .unwrap(); assert_eq!( hovers.len(), 2, @@ -5764,7 +5768,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx)); } - let definitions = definitions.await.unwrap(); + let definitions = definitions.await.unwrap().unwrap(); assert_eq!( definitions.len(), 1, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 25fddf5cf1d44f947e7c4128e82158787cdd28f2..e32ea1cb3a6a11a406fb34fa47e78699cd1e3595 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -15710,7 +15710,9 @@ impl Editor { }; cx.spawn_in(window, async move |editor, cx| { - let definitions = definitions.await?; + let Some(definitions) = definitions.await? else { + return Ok(Navigated::No); + }; let navigated = editor .update_in(cx, |editor, window, cx| { editor.navigate_to_hover_links( @@ -16052,7 +16054,9 @@ impl Editor { } }); - let locations = references.await?; + let Some(locations) = references.await? else { + return anyhow::Ok(Navigated::No); + }; if locations.is_empty() { return anyhow::Ok(Navigated::No); } @@ -21837,7 +21841,7 @@ pub trait SemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>>; + ) -> Option>>>; fn inline_values( &self, @@ -21876,7 +21880,7 @@ pub trait SemanticsProvider { position: text::Anchor, kind: GotoDefinitionKind, cx: &mut App, - ) -> Option>>>; + ) -> Option>>>>; fn range_for_rename( &self, @@ -21989,7 +21993,13 @@ impl CodeActionProvider for Entity { Ok(code_lens_actions .context("code lens fetch")? .into_iter() - .chain(code_actions.context("code action fetch")?) + .flatten() + .chain( + code_actions + .context("code action fetch")? + .into_iter() + .flatten(), + ) .collect()) }) }) @@ -22284,7 +22294,7 @@ impl SemanticsProvider for Entity { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>> { + ) -> Option>>> { Some(self.update(cx, |project, cx| project.hover(buffer, position, cx))) } @@ -22305,7 +22315,7 @@ impl SemanticsProvider for Entity { position: text::Anchor, kind: GotoDefinitionKind, cx: &mut App, - ) -> Option>>> { + ) -> Option>>>> { Some(self.update(cx, |project, cx| match kind { GotoDefinitionKind::Symbol => project.definitions(buffer, position, cx), GotoDefinitionKind::Declaration => project.declarations(buffer, position, cx), diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 04e66a234c0b16131b492264eb9e798e76b24453..1d7d56e67db00e3511c1bf8203f6e757cf2aea6b 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -559,7 +559,7 @@ pub fn show_link_definition( provider.definitions(&buffer, buffer_position, preferred_kind, cx) })?; if let Some(task) = task { - task.await.ok().map(|definition_result| { + task.await.ok().flatten().map(|definition_result| { ( definition_result.iter().find_map(|link| { link.origin.as_ref().and_then(|origin| { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 28a09e947f58ef26f20453e9f36f01e7cd74061e..fab53457876866223be6b7d32f964cd1abd1dd28 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -428,7 +428,7 @@ fn show_hover( }; let hovers_response = if let Some(hover_request) = hover_request { - hover_request.await + hover_request.await.unwrap_or_default() } else { Vec::new() }; diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index c79feccb4b1fb0ef7ad686408358e77319ce446c..2d4710a8d44a023f0c3206ad0c327a34c36fdac4 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -431,7 +431,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>> { + ) -> Option>>> { let buffer = self.to_base(buffer, &[position], cx)?; self.0.hover(&buffer, position, cx) } @@ -490,7 +490,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { position: text::Anchor, kind: crate::GotoDefinitionKind, cx: &mut App, - ) -> Option>>> { + ) -> Option>>>> { let buffer = self.to_base(buffer, &[position], cx)?; self.0.definitions(&buffer, position, kind, cx) } diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index 5c9800ab55e5f1b53b941c205a2e5601f8f22524..cb21f35d7ed7556cf09f9e566286a10f8317ca6c 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -182,7 +182,9 @@ impl Editor { let signature_help = task.await; editor .update(cx, |editor, cx| { - let Some(mut signature_help) = signature_help.into_iter().next() else { + let Some(mut signature_help) = + signature_help.unwrap_or_default().into_iter().next() + else { editor .signature_help_state .hide(SignatureHelpHiddenBy::AutoClose); diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index ce9e2fe229c0aded6fac31c260e334445f987f03..942225d09837c206f54aa324f9b58ec214f92ba2 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -45,7 +45,7 @@ use util::{ConnectionResult, ResultExt, TryFutureExt, redact}; const JSON_RPC_VERSION: &str = "2.0"; const CONTENT_LEN_HEADER: &str = "Content-Length: "; -const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); +pub const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); type NotificationHandler = Box, Value, &mut AsyncApp)>; diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index c90d85358a2a4d70ec95ad4c25177026cb2a173c..ce7a871d1a63107ab4908dccea68dd41d73a319f 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -3444,8 +3444,7 @@ impl LspCommand for GetCodeLens { capabilities .server_capabilities .code_lens_provider - .as_ref() - .is_some_and(|code_lens_options| code_lens_options.resolve_provider.unwrap_or(false)) + .is_some() } fn to_lsp( diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 0b58009f37204fa3383bfc73683826aa3b8d7fb3..bcfd9d386b111dce3e1173fc99b131dfdf317b76 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -72,10 +72,11 @@ use lsp::{ AdapterServerCapabilities, CodeActionKind, CompletionContext, DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, - LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, - LanguageServerName, LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, - OneOf, RenameFilesParams, SymbolKind, TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, - WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, + LSP_REQUEST_TIMEOUT, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, + LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture, + MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind, + TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, + WorkspaceFolder, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; use parking_lot::Mutex; @@ -84,7 +85,7 @@ use rand::prelude::*; use rpc::{ AnyProtoClient, - proto::{FromProto, ToProto}, + proto::{FromProto, LspRequestId, LspRequestMessage as _, ToProto}, }; use serde::Serialize; use settings::{Settings, SettingsLocation, SettingsStore}; @@ -92,7 +93,7 @@ use sha2::{Digest, Sha256}; use smol::channel::Sender; use snippet::Snippet; use std::{ - any::Any, + any::{Any, TypeId}, borrow::Cow, cell::RefCell, cmp::{Ordering, Reverse}, @@ -3490,6 +3491,7 @@ pub struct LspStore { pub(super) lsp_server_capabilities: HashMap, lsp_document_colors: HashMap, lsp_code_lens: HashMap, + running_lsp_requests: HashMap>)>, } #[derive(Debug, Default, Clone)] @@ -3499,7 +3501,7 @@ pub struct DocumentColors { } type DocumentColorTask = Shared>>>; -type CodeLensTask = Shared, Arc>>>; +type CodeLensTask = Shared>, Arc>>>; #[derive(Debug, Default)] struct DocumentColorData { @@ -3579,6 +3581,8 @@ struct CoreSymbol { impl LspStore { pub fn init(client: &AnyProtoClient) { + client.add_entity_request_handler(Self::handle_lsp_query); + client.add_entity_message_handler(Self::handle_lsp_query_response); client.add_entity_request_handler(Self::handle_multi_lsp_query); client.add_entity_request_handler(Self::handle_restart_language_servers); client.add_entity_request_handler(Self::handle_stop_language_servers); @@ -3758,6 +3762,7 @@ impl LspStore { lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), + running_lsp_requests: HashMap::default(), active_entry: None, _maintain_workspace_config, _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx), @@ -3819,6 +3824,7 @@ impl LspStore { lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), + running_lsp_requests: HashMap::default(), active_entry: None, _maintain_workspace_config, @@ -4381,8 +4387,6 @@ impl LspStore { } } - // 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, @@ -5233,154 +5237,130 @@ impl LspStore { pub fn definitions( &mut self, - buffer_handle: &Entity, + buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> 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())); + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(None)); } - 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()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetDefinition( - request.to_proto(project_id, buffer_handle.read(cx)), - )), - }); - let buffer = buffer_handle.clone(); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); + let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetDefinitionResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|definitions_response| { - GetDefinitions { position }.response_from_proto( - definitions_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetDefinitions { position }.response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(), + )) }) } else { let definitions_task = self.request_multiple_lsp_locally( - buffer_handle, + buffer, Some(position), GetDefinitions { position }, cx, ); cx.background_spawn(async move { - Ok(definitions_task - .await - .into_iter() - .flat_map(|(_, definitions)| definitions) - .dedup() - .collect()) + Ok(Some( + definitions_task + .await + .into_iter() + .flat_map(|(_, definitions)| definitions) + .dedup() + .collect(), + )) }) } } pub fn declarations( &mut self, - buffer_handle: &Entity, + buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> 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())); + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(None)); } - 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()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetDeclaration( - request.to_proto(project_id, buffer_handle.read(cx)), - )), - }); - let buffer = buffer_handle.clone(); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); + let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetDeclarationResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|declarations_response| { - GetDeclarations { position }.response_from_proto( - declarations_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetDeclarations { position }.response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(), + )) }) } else { let declarations_task = self.request_multiple_lsp_locally( - buffer_handle, + buffer, Some(position), GetDeclarations { position }, cx, ); cx.background_spawn(async move { - Ok(declarations_task - .await - .into_iter() - .flat_map(|(_, declarations)| declarations) - .dedup() - .collect()) + Ok(Some( + declarations_task + .await + .into_iter() + .flat_map(|(_, declarations)| declarations) + .dedup() + .collect(), + )) }) } } @@ -5390,59 +5370,45 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> 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())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetTypeDefinition( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetTypeDefinitionResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|type_definitions_response| { - GetTypeDefinitions { position }.response_from_proto( - type_definitions_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetTypeDefinitions { position }.response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(), + )) }) } else { let type_definitions_task = self.request_multiple_lsp_locally( @@ -5452,12 +5418,14 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(type_definitions_task - .await - .into_iter() - .flat_map(|(_, type_definitions)| type_definitions) - .dedup() - .collect()) + Ok(Some( + type_definitions_task + .await + .into_iter() + .flat_map(|(_, type_definitions)| type_definitions) + .dedup() + .collect(), + )) }) } } @@ -5467,59 +5435,45 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> 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())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetImplementation( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetImplementationResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|implementations_response| { - GetImplementations { position }.response_from_proto( - implementations_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetImplementations { position }.response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(), + )) }) } else { let implementations_task = self.request_multiple_lsp_locally( @@ -5529,12 +5483,14 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(implementations_task - .await - .into_iter() - .flat_map(|(_, implementations)| implementations) - .dedup() - .collect()) + Ok(Some( + implementations_task + .await + .into_iter() + .flat_map(|(_, implementations)| implementations) + .dedup() + .collect(), + )) }) } } @@ -5544,59 +5500,44 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> 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())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetReferences( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); + }; + let Some(responses) = request_task.await? else { + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetReferencesResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|references_response| { - GetReferences { position }.response_from_proto( - references_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) - .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + let locations = join_all(responses.payload.into_iter().map(|lsp_response| { + GetReferences { position }.response_from_proto( + lsp_response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) + .await + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(); + Ok(Some(locations)) }) } else { let references_task = self.request_multiple_lsp_locally( @@ -5606,12 +5547,14 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(references_task - .await - .into_iter() - .flat_map(|(_, references)| references) - .dedup() - .collect()) + Ok(Some( + references_task + .await + .into_iter() + .flat_map(|(_, references)| references) + .dedup() + .collect(), + )) }) } } @@ -5622,65 +5565,51 @@ impl LspStore { range: Range, kinds: Option>, cx: &mut Context, - ) -> Task>> { + ) -> 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())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetCodeActions( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetCodeActionsResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|code_actions_response| { - GetCodeActions { - range: range.clone(), - kinds: kinds.clone(), - } - .response_from_proto( - code_actions_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetCodeActions { + range: range.clone(), + kinds: kinds.clone(), + } + .response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .collect(), + )) }) } else { let all_actions_task = self.request_multiple_lsp_locally( @@ -5690,11 +5619,13 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(all_actions_task - .await - .into_iter() - .flat_map(|(_, actions)| actions) - .collect()) + Ok(Some( + all_actions_task + .await + .into_iter() + .flat_map(|(_, actions)| actions) + .collect(), + )) }) } } @@ -5719,8 +5650,10 @@ impl LspStore { != cached_data.lens.keys().copied().collect() }); if !has_different_servers { - return Task::ready(Ok(cached_data.lens.values().flatten().cloned().collect())) - .shared(); + return Task::ready(Ok(Some( + cached_data.lens.values().flatten().cloned().collect(), + ))) + .shared(); } } @@ -5758,17 +5691,19 @@ impl LspStore { lsp_store .update(cx, |lsp_store, _| { let lsp_data = lsp_store.lsp_code_lens.entry(buffer_id).or_default(); - if lsp_data.lens_for_version == query_version_queried_for { - lsp_data.lens.extend(fetched_lens.clone()); - } else if !lsp_data - .lens_for_version - .changed_since(&query_version_queried_for) - { - lsp_data.lens_for_version = query_version_queried_for; - lsp_data.lens = fetched_lens.clone(); + if let Some(fetched_lens) = fetched_lens { + if lsp_data.lens_for_version == query_version_queried_for { + lsp_data.lens.extend(fetched_lens.clone()); + } else if !lsp_data + .lens_for_version + .changed_since(&query_version_queried_for) + { + lsp_data.lens_for_version = query_version_queried_for; + lsp_data.lens = fetched_lens.clone(); + } } lsp_data.update = None; - lsp_data.lens.values().flatten().cloned().collect() + Some(lsp_data.lens.values().flatten().cloned().collect()) }) .map_err(Arc::new) }) @@ -5781,64 +5716,40 @@ impl LspStore { &mut self, buffer: &Entity, cx: &mut Context, - ) -> Task>>> { + ) -> 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())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetCodeLens( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_lsp_store, cx| { let Some(lsp_store) = weak_lsp_store.upgrade() else { - return Ok(HashMap::default()); + return Ok(None); }; - let responses = request_task.await?.responses; - let code_lens_actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| { - let response = match lsp_response.response? { - proto::lsp_response::Response::GetCodeLensResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }?; - let server_id = LanguageServerId::from_proto(lsp_response.server_id); - Some((server_id, response)) - }) - .map(|(server_id, code_lens_response)| { - let lsp_store = lsp_store.clone(); - let buffer = buffer.clone(); - let cx = cx.clone(); - async move { - ( - server_id, - GetCodeLens - .response_from_proto( - code_lens_response, - lsp_store, - buffer, - cx, - ) - .await, - ) - } - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + + let code_lens_actions = join_all(responses.payload.into_iter().map(|response| { + let lsp_store = lsp_store.clone(); + let buffer = buffer.clone(); + let cx = cx.clone(); + async move { + ( + LanguageServerId::from_proto(response.server_id), + GetCodeLens + .response_from_proto(response.response, lsp_store, buffer, cx) + .await, + ) + } + })) .await; let mut has_errors = false; @@ -5857,14 +5768,14 @@ impl LspStore { !has_errors || !code_lens_actions.is_empty(), "Failed to fetch code lens" ); - Ok(code_lens_actions) + Ok(Some(code_lens_actions)) }) } else { let code_lens_actions_task = self.request_multiple_lsp_locally(buffer, None::, GetCodeLens, cx); - cx.background_spawn( - async move { Ok(code_lens_actions_task.await.into_iter().collect()) }, - ) + cx.background_spawn(async move { + Ok(Some(code_lens_actions_task.await.into_iter().collect())) + }) } } @@ -6480,48 +6391,23 @@ impl LspStore { 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, - ) { + let request = GetDocumentDiagnostics { + previous_result_id: None, + }; + if !self.is_capable_for_proto_request(&buffer, &request, cx) { return Task::ready(Ok(None)); } - let request_task = client.request(proto::MultiLspQuery { - buffer_id: buffer_id.to_proto(), - version: serialize_version(&buffer.read(cx).version()), - project_id: upstream_project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetDocumentDiagnostics( - proto::GetDocumentDiagnostics { - project_id: upstream_project_id, - buffer_id: buffer_id.to_proto(), - version: serialize_version(&buffer.read(cx).version()), - }, - )), - }); + let request_task = client.request_lsp( + upstream_project_id, + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(upstream_project_id, buffer.read(cx)), + ); cx.background_spawn(async move { - let _proto_responses = request_task - .await? - .responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetDocumentDiagnosticsResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .collect::>(); // Proto requests cause the diagnostics to be pulled from language server(s) on the local side // and then, buffer state updated with the diagnostics received, which will be later propagated to the client. // Do not attempt to further process the dummy responses here. + let _response = request_task.await?; Ok(None) }) } else { @@ -6806,16 +6692,18 @@ impl LspStore { .update(cx, |lsp_store, _| { let lsp_data = lsp_store.lsp_document_colors.entry(buffer_id).or_default(); - if lsp_data.colors_for_version == query_version_queried_for { - lsp_data.colors.extend(fetched_colors.clone()); - lsp_data.cache_version += 1; - } else if !lsp_data - .colors_for_version - .changed_since(&query_version_queried_for) - { - lsp_data.colors_for_version = query_version_queried_for; - lsp_data.colors = fetched_colors.clone(); - lsp_data.cache_version += 1; + if let Some(fetched_colors) = fetched_colors { + if lsp_data.colors_for_version == query_version_queried_for { + lsp_data.colors.extend(fetched_colors.clone()); + lsp_data.cache_version += 1; + } else if !lsp_data + .colors_for_version + .changed_since(&query_version_queried_for) + { + lsp_data.colors_for_version = query_version_queried_for; + lsp_data.colors = fetched_colors.clone(); + lsp_data.cache_version += 1; + } } lsp_data.colors_update = None; let colors = lsp_data @@ -6840,56 +6728,45 @@ impl LspStore { &mut self, buffer: &Entity, cx: &mut Context, - ) -> Task>>> { + ) -> 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())); + return Task::ready(Ok(None)); } - let request_task = client.request(proto::MultiLspQuery { + let request_task = client.request_lsp( project_id, - buffer_id: buffer.read(cx).remote_id().to_proto(), - version: serialize_version(&buffer.read(cx).version()), - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetDocumentColor( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); - cx.spawn(async move |project, cx| { - let Some(project) = project.upgrade() else { - return Ok(HashMap::default()); + cx.spawn(async move |lsp_store, cx| { + let Some(project) = lsp_store.upgrade() else { + return Ok(None); }; let colors = join_all( request_task .await .log_err() - .map(|response| response.responses) + .flatten() + .map(|response| response.payload) .unwrap_or_default() .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetDocumentColorResponse(response) => { - Some(( - LanguageServerId::from_proto(lsp_response.server_id), - response, - )) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|(server_id, color_response)| { + .map(|color_response| { let response = request.response_from_proto( - color_response, + color_response.response, project.clone(), buffer.clone(), cx.clone(), ); - async move { (server_id, response.await.log_err().unwrap_or_default()) } + async move { + ( + LanguageServerId::from_proto(color_response.server_id), + response.await.log_err().unwrap_or_default(), + ) + } }), ) .await @@ -6900,23 +6777,25 @@ impl LspStore { .extend(colors); acc }); - Ok(colors) + Ok(Some(colors)) }) } else { let document_colors_task = self.request_multiple_lsp_locally(buffer, None::, GetDocumentColor, cx); cx.background_spawn(async move { - Ok(document_colors_task - .await - .into_iter() - .fold(HashMap::default(), |mut acc, (server_id, colors)| { - acc.entry(server_id) - .or_insert_with(HashSet::default) - .extend(colors); - acc - }) - .into_iter() - .collect()) + Ok(Some( + document_colors_task + .await + .into_iter() + .fold(HashMap::default(), |mut acc, (server_id, colors)| { + acc.entry(server_id) + .or_insert_with(HashSet::default) + .extend(colors); + acc + }) + .into_iter() + .collect(), + )) }) } } @@ -6926,49 +6805,34 @@ impl LspStore { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task> { + ) -> Task>> { 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()); + return Task::ready(None); } - let request_task = client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), - project_id: upstream_project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetSignatureHelp( - request.to_proto(upstream_project_id, buffer.read(cx)), - )), - }); + let request_task = client.request_lsp( + upstream_project_id, + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(upstream_project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { - let Some(project) = weak_project.upgrade() else { - return Vec::new(); - }; - join_all( + let project = weak_project.upgrade()?; + let signatures = join_all( request_task .await .log_err() - .map(|response| response.responses) + .flatten() + .map(|response| response.payload) .unwrap_or_default() .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetSignatureHelpResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|signature_response| { + .map(|response| { let response = GetSignatureHelp { position }.response_from_proto( - signature_response, + response.response, project.clone(), buffer.clone(), cx.clone(), @@ -6979,7 +6843,8 @@ impl LspStore { .await .into_iter() .flatten() - .collect() + .collect(); + Some(signatures) }) } else { let all_actions_task = self.request_multiple_lsp_locally( @@ -6989,11 +6854,13 @@ impl LspStore { cx, ); cx.background_spawn(async move { - all_actions_task - .await - .into_iter() - .flat_map(|(_, actions)| actions) - .collect::>() + Some( + all_actions_task + .await + .into_iter() + .flat_map(|(_, actions)| actions) + .collect::>(), + ) }) } } @@ -7003,47 +6870,32 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task> { + ) -> 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()); + return Task::ready(None); } - let request_task = client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), - project_id: upstream_project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetHover( - request.to_proto(upstream_project_id, buffer.read(cx)), - )), - }); + let request_task = client.request_lsp( + upstream_project_id, + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(upstream_project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { - let Some(project) = weak_project.upgrade() else { - return Vec::new(); - }; - join_all( + let project = weak_project.upgrade()?; + let hovers = join_all( request_task .await .log_err() - .map(|response| response.responses) + .flatten() + .map(|response| response.payload) .unwrap_or_default() .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetHoverResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|hover_response| { + .map(|response| { let response = GetHover { position }.response_from_proto( - hover_response, + response.response, project.clone(), buffer.clone(), cx.clone(), @@ -7060,7 +6912,8 @@ impl LspStore { .await .into_iter() .flatten() - .collect() + .collect(); + Some(hovers) }) } else { let all_actions_task = self.request_multiple_lsp_locally( @@ -7070,11 +6923,13 @@ impl LspStore { cx, ); cx.background_spawn(async move { - all_actions_task - .await - .into_iter() - .filter_map(|(_, hover)| remove_empty_hover_blocks(hover?)) - .collect::>() + Some( + all_actions_task + .await + .into_iter() + .filter_map(|(_, hover)| remove_empty_hover_blocks(hover?)) + .collect::>(), + ) }) } } @@ -8137,6 +7992,203 @@ impl LspStore { })? } + async fn handle_lsp_query( + lsp_store: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + use proto::lsp_query::Request; + let sender_id = envelope.original_sender_id().unwrap_or_default(); + let lsp_query = envelope.payload; + let lsp_request_id = LspRequestId(lsp_query.lsp_request_id); + match lsp_query.request.context("invalid LSP query request")? { + Request::GetReferences(get_references) => { + let position = get_references.position.clone().and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_references, + position, + cx.clone(), + ) + .await?; + } + Request::GetDocumentColor(get_document_color) => { + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_document_color, + None, + cx.clone(), + ) + .await?; + } + Request::GetHover(get_hover) => { + let position = get_hover.position.clone().and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_hover, + position, + cx.clone(), + ) + .await?; + } + Request::GetCodeActions(get_code_actions) => { + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_code_actions, + None, + cx.clone(), + ) + .await?; + } + Request::GetSignatureHelp(get_signature_help) => { + let position = get_signature_help + .position + .clone() + .and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_signature_help, + position, + cx.clone(), + ) + .await?; + } + Request::GetCodeLens(get_code_lens) => { + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_code_lens, + None, + cx.clone(), + ) + .await?; + } + Request::GetDefinition(get_definition) => { + let position = get_definition.position.clone().and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_definition, + position, + cx.clone(), + ) + .await?; + } + Request::GetDeclaration(get_declaration) => { + let position = get_declaration + .position + .clone() + .and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_declaration, + position, + cx.clone(), + ) + .await?; + } + Request::GetTypeDefinition(get_type_definition) => { + let position = get_type_definition + .position + .clone() + .and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_type_definition, + position, + cx.clone(), + ) + .await?; + } + Request::GetImplementation(get_implementation) => { + let position = get_implementation + .position + .clone() + .and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_implementation, + position, + cx.clone(), + ) + .await?; + } + // Diagnostics pull synchronizes internally via the buffer state, and cannot be handled generically as the other requests. + Request::GetDocumentDiagnostics(get_document_diagnostics) => { + let buffer_id = BufferId::new(get_document_diagnostics.buffer_id())?; + let version = deserialize_version(get_document_diagnostics.buffer_version()); + let buffer = lsp_store.update(&mut cx, |this, cx| { + this.buffer_store.read(cx).get_existing(buffer_id) + })??; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(version.clone()) + })? + .await?; + lsp_store.update(&mut cx, |lsp_store, cx| { + let existing_queries = lsp_store + .running_lsp_requests + .entry(TypeId::of::()) + .or_default(); + if ::ProtoRequest::stop_previous_requests( + ) || buffer.read(cx).version.changed_since(&existing_queries.0) + { + existing_queries.1.clear(); + } + existing_queries.1.insert( + lsp_request_id, + cx.spawn(async move |lsp_store, cx| { + let diagnostics_pull = lsp_store + .update(cx, |lsp_store, cx| { + lsp_store.pull_diagnostics_for_buffer(buffer, cx) + }) + .ok(); + if let Some(diagnostics_pull) = diagnostics_pull { + match diagnostics_pull.await { + Ok(()) => {} + Err(e) => log::error!("Failed to pull diagnostics: {e:#}"), + }; + } + }), + ); + })?; + } + } + Ok(proto::Ack {}) + } + + async fn handle_lsp_query_response( + lsp_store: Entity, + envelope: TypedEnvelope, + cx: AsyncApp, + ) -> Result<()> { + lsp_store.read_with(&cx, |lsp_store, _| { + if let Some((upstream_client, _)) = lsp_store.upstream_client() { + upstream_client.handle_lsp_response(envelope.clone()); + } + })?; + Ok(()) + } + + // todo(lsp) remove after Zed Stable hits v0.204.x async fn handle_multi_lsp_query( lsp_store: Entity, envelope: TypedEnvelope, @@ -12012,6 +12064,88 @@ impl LspStore { Ok(()) } + async fn query_lsp_locally( + lsp_store: Entity, + sender_id: proto::PeerId, + lsp_request_id: LspRequestId, + proto_request: T::ProtoRequest, + position: Option, + mut cx: AsyncApp, + ) -> Result<()> + where + T: LspCommand + Clone, + T::ProtoRequest: proto::LspRequestMessage, + ::Response: + Into<::Response>, + { + let buffer_id = BufferId::new(proto_request.buffer_id())?; + let version = deserialize_version(proto_request.buffer_version()); + let buffer = lsp_store.update(&mut cx, |this, cx| { + this.buffer_store.read(cx).get_existing(buffer_id) + })??; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(version.clone()) + })? + .await?; + let buffer_version = buffer.read_with(&cx, |buffer, _| buffer.version())?; + let request = + T::from_proto(proto_request, lsp_store.clone(), buffer.clone(), cx.clone()).await?; + lsp_store.update(&mut cx, |lsp_store, cx| { + let request_task = + lsp_store.request_multiple_lsp_locally(&buffer, position, request, cx); + let existing_queries = lsp_store + .running_lsp_requests + .entry(TypeId::of::()) + .or_default(); + if T::ProtoRequest::stop_previous_requests() + || buffer_version.changed_since(&existing_queries.0) + { + existing_queries.1.clear(); + } + existing_queries.1.insert( + lsp_request_id, + cx.spawn(async move |lsp_store, cx| { + let response = request_task.await; + lsp_store + .update(cx, |lsp_store, cx| { + if let Some((client, project_id)) = lsp_store.downstream_client.clone() + { + let response = response + .into_iter() + .map(|(server_id, response)| { + ( + server_id.to_proto(), + T::response_to_proto( + response, + lsp_store, + sender_id, + &buffer_version, + cx, + ) + .into(), + ) + }) + .collect::>(); + match client.send_lsp_response::( + project_id, + lsp_request_id, + response, + ) { + Ok(()) => {} + Err(e) => { + log::error!("Failed to send LSP response: {e:#}",) + } + } + } + }) + .ok(); + }), + ); + })?; + Ok(()) + } + fn take_text_document_sync_options( capabilities: &mut lsp::ServerCapabilities, ) -> lsp::TextDocumentSyncOptions { @@ -12025,6 +12159,12 @@ impl LspStore { None => lsp::TextDocumentSyncOptions::default(), } } + + #[cfg(any(test, feature = "test-support"))] + pub fn forget_code_lens_task(&mut self, buffer_id: BufferId) -> Option { + let data = self.lsp_code_lens.get_mut(&buffer_id)?; + Some(data.update.take()?.1) + } } // Registration with registerOptions as null, should fallback to true. diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e47c020a429fca8e6ed99aec6b89ace2a78d8985..ee4bfcb8ccf18417e18c6f4f408a892f5fe816a9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3415,7 +3415,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3433,7 +3433,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3451,7 +3451,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3469,7 +3469,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3487,7 +3487,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3585,23 +3585,12 @@ impl Project { }) } - pub fn signature_help( - &self, - buffer: &Entity, - position: T, - cx: &mut Context, - ) -> Task> { - self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.signature_help(buffer, position, cx) - }) - } - pub fn hover( &self, buffer: &Entity, position: T, cx: &mut Context, - ) -> Task> { + ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); self.lsp_store .update(cx, |lsp_store, cx| lsp_store.hover(buffer, position, cx)) @@ -3637,7 +3626,7 @@ impl Project { range: Range, kinds: Option>, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let buffer = buffer_handle.read(cx); let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); self.lsp_store.update(cx, |lsp_store, cx| { @@ -3650,7 +3639,7 @@ impl Project { buffer: &Entity, range: Range, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let snapshot = buffer.read(cx).snapshot(); let range = range.to_point(&snapshot); let range_start = snapshot.anchor_before(range.start); @@ -3668,16 +3657,18 @@ impl Project { let mut code_lens_actions = code_lens_actions .await .map_err(|e| anyhow!("code lens fetch failed: {e:#}"))?; - code_lens_actions.retain(|code_lens_action| { - range - .start - .cmp(&code_lens_action.range.start, &snapshot) - .is_ge() - && range - .end - .cmp(&code_lens_action.range.end, &snapshot) - .is_le() - }); + if let Some(code_lens_actions) = &mut code_lens_actions { + code_lens_actions.retain(|code_lens_action| { + range + .start + .cmp(&code_lens_action.range.start, &snapshot) + .is_ge() + && range + .end + .cmp(&code_lens_action.range.end, &snapshot) + .is_le() + }); + } Ok(code_lens_actions) }) } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 8b0b21fcd63e2f10509cb9c41a8cc50ca237791b..282f1facc2a9110bd27d249139f4cb4ac644c9c8 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -3005,6 +3005,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { let mut definitions = project .update(cx, |project, cx| project.definitions(&buffer, 22, cx)) .await + .unwrap() .unwrap(); // Assert no new language server started @@ -3519,7 +3520,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { .next() .await; - let action = actions.await.unwrap()[0].clone(); + let action = actions.await.unwrap().unwrap()[0].clone(); let apply = project.update(cx, |project, cx| { project.apply_code_action(buffer.clone(), action, true, cx) }); @@ -6110,6 +6111,7 @@ async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) { hover_task .await .into_iter() + .flatten() .map(|hover| hover.contents.iter().map(|block| &block.text).join("|")) .sorted() .collect::>(), @@ -6183,6 +6185,7 @@ async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) { hover_task .await .into_iter() + .flatten() .map(|hover| hover.contents.iter().map(|block| &block.text).join("|")) .sorted() .collect::>(), @@ -6261,7 +6264,7 @@ async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) { .await .expect("The code action request should have been triggered"); - let code_actions = code_actions_task.await.unwrap(); + let code_actions = code_actions_task.await.unwrap().unwrap(); assert_eq!(code_actions.len(), 1); assert_eq!( code_actions[0].lsp_action.action_kind(), @@ -6420,6 +6423,7 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) { code_actions_task .await .unwrap() + .unwrap() .into_iter() .map(|code_action| code_action.lsp_action.title().to_owned()) .sorted() diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index ea9647feff0cf811f0464dc4eca22059b348be6f..ac9c275aa2d67b3df78fc38d4e88497f9f10e6c9 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -753,26 +753,45 @@ message TextEdit { PointUtf16 lsp_range_end = 3; } -message MultiLspQuery { +message LspQuery { uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; - oneof strategy { - AllLanguageServers all = 4; - } + uint64 lsp_request_id = 2; oneof request { + GetReferences get_references = 3; + GetDocumentColor get_document_color = 4; GetHover get_hover = 5; GetCodeActions get_code_actions = 6; GetSignatureHelp get_signature_help = 7; GetCodeLens get_code_lens = 8; GetDocumentDiagnostics get_document_diagnostics = 9; - GetDocumentColor get_document_color = 10; - GetDefinition get_definition = 11; - GetDeclaration get_declaration = 12; - GetTypeDefinition get_type_definition = 13; - GetImplementation get_implementation = 14; - GetReferences get_references = 15; + GetDefinition get_definition = 10; + GetDeclaration get_declaration = 11; + GetTypeDefinition get_type_definition = 12; + GetImplementation get_implementation = 13; + } +} + +message LspQueryResponse { + uint64 project_id = 1; + uint64 lsp_request_id = 2; + repeated LspResponse responses = 3; +} + +message LspResponse { + oneof response { + GetHoverResponse get_hover_response = 1; + GetCodeActionsResponse get_code_actions_response = 2; + GetSignatureHelpResponse get_signature_help_response = 3; + GetCodeLensResponse get_code_lens_response = 4; + GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5; + GetDocumentColorResponse get_document_color_response = 6; + GetDefinitionResponse get_definition_response = 8; + GetDeclarationResponse get_declaration_response = 9; + GetTypeDefinitionResponse get_type_definition_response = 10; + GetImplementationResponse get_implementation_response = 11; + GetReferencesResponse get_references_response = 12; } + uint64 server_id = 7; } message AllLanguageServers {} @@ -798,27 +817,6 @@ message StopLanguageServers { bool all = 4; } -message MultiLspQueryResponse { - repeated LspResponse responses = 1; -} - -message LspResponse { - oneof response { - GetHoverResponse get_hover_response = 1; - GetCodeActionsResponse get_code_actions_response = 2; - GetSignatureHelpResponse get_signature_help_response = 3; - GetCodeLensResponse get_code_lens_response = 4; - GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5; - GetDocumentColorResponse get_document_color_response = 6; - GetDefinitionResponse get_definition_response = 8; - GetDeclarationResponse get_declaration_response = 9; - GetTypeDefinitionResponse get_type_definition_response = 10; - GetImplementationResponse get_implementation_response = 11; - GetReferencesResponse get_references_response = 12; - } - uint64 server_id = 7; -} - message LspExtRunnables { uint64 project_id = 1; uint64 buffer_id = 2; @@ -909,3 +907,30 @@ message PullWorkspaceDiagnostics { uint64 project_id = 1; uint64 server_id = 2; } + +// todo(lsp) remove after Zed Stable hits v0.204.x +message MultiLspQuery { + uint64 project_id = 1; + uint64 buffer_id = 2; + repeated VectorClockEntry version = 3; + oneof strategy { + AllLanguageServers all = 4; + } + oneof request { + GetHover get_hover = 5; + GetCodeActions get_code_actions = 6; + GetSignatureHelp get_signature_help = 7; + GetCodeLens get_code_lens = 8; + GetDocumentDiagnostics get_document_diagnostics = 9; + GetDocumentColor get_document_color = 10; + GetDefinition get_definition = 11; + GetDeclaration get_declaration = 12; + GetTypeDefinition get_type_definition = 13; + GetImplementation get_implementation = 14; + GetReferences get_references = 15; + } +} + +message MultiLspQueryResponse { + repeated LspResponse responses = 1; +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 310fcf584e99a82606fdfdf39237b808adc61c9f..70689bcd6306195fce0d5c6449bf3dd9f5d43539 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -393,7 +393,10 @@ message Envelope { GetCrashFilesResponse get_crash_files_response = 362; GitClone git_clone = 363; - GitCloneResponse git_clone_response = 364; // current max + GitCloneResponse git_clone_response = 364; + + LspQuery lsp_query = 365; + LspQueryResponse lsp_query_response = 366; // current max } reserved 87 to 88; diff --git a/crates/proto/src/macros.rs b/crates/proto/src/macros.rs index 2ce0c0df259d8d0dc352e118ff53c872852d9fec..59e984d7dbbcd52fb70b7513aea0b5bcb399c204 100644 --- a/crates/proto/src/macros.rs +++ b/crates/proto/src/macros.rs @@ -69,3 +69,32 @@ macro_rules! entity_messages { })* }; } + +#[macro_export] +macro_rules! lsp_messages { + ($(($request_name:ident, $response_name:ident, $stop_previous_requests:expr)),* $(,)?) => { + $(impl LspRequestMessage for $request_name { + type Response = $response_name; + + fn to_proto_query(self) -> $crate::lsp_query::Request { + $crate::lsp_query::Request::$request_name(self) + } + + fn response_to_proto_query(response: Self::Response) -> $crate::lsp_response::Response { + $crate::lsp_response::Response::$response_name(response) + } + + fn buffer_id(&self) -> u64 { + self.buffer_id + } + + fn buffer_version(&self) -> &[$crate::VectorClockEntry] { + &self.version + } + + fn stop_previous_requests() -> bool { + $stop_previous_requests + } + })* + }; +} diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 802db09590a5bb6fc316ce31bd880d394c06c5ca..d38e54685ffb78fe8621b12a0dd25bb6d1ab3f6e 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -169,6 +169,9 @@ messages!( (MarkNotificationRead, Foreground), (MoveChannel, Foreground), (ReorderChannel, Foreground), + (LspQuery, Background), + (LspQueryResponse, Background), + // todo(lsp) remove after Zed Stable hits v0.204.x (MultiLspQuery, Background), (MultiLspQueryResponse, Background), (OnTypeFormatting, Background), @@ -426,7 +429,10 @@ request_messages!( (SetRoomParticipantRole, Ack), (BlameBuffer, BlameBufferResponse), (RejoinRemoteProjects, RejoinRemoteProjectsResponse), + // todo(lsp) remove after Zed Stable hits v0.204.x (MultiLspQuery, MultiLspQueryResponse), + (LspQuery, Ack), + (LspQueryResponse, Ack), (RestartLanguageServers, Ack), (StopLanguageServers, Ack), (OpenContext, OpenContextResponse), @@ -478,6 +484,20 @@ request_messages!( (GitClone, GitCloneResponse) ); +lsp_messages!( + (GetReferences, GetReferencesResponse, true), + (GetDocumentColor, GetDocumentColorResponse, true), + (GetHover, GetHoverResponse, true), + (GetCodeActions, GetCodeActionsResponse, true), + (GetSignatureHelp, GetSignatureHelpResponse, true), + (GetCodeLens, GetCodeLensResponse, true), + (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse, true), + (GetDefinition, GetDefinitionResponse, true), + (GetDeclaration, GetDeclarationResponse, true), + (GetTypeDefinition, GetTypeDefinitionResponse, true), + (GetImplementation, GetImplementationResponse, true), +); + entity_messages!( {project_id, ShareProject}, AddProjectCollaborator, @@ -520,6 +540,9 @@ entity_messages!( LeaveProject, LinkedEditingRange, LoadCommitDiff, + LspQuery, + LspQueryResponse, + // todo(lsp) remove after Zed Stable hits v0.204.x MultiLspQuery, RestartLanguageServers, StopLanguageServers, @@ -777,6 +800,28 @@ pub fn split_repository_update( }]) } +impl LspQuery { + pub fn query_name_and_write_permissions(&self) -> (&str, bool) { + match self.request { + Some(lsp_query::Request::GetHover(_)) => ("GetHover", false), + Some(lsp_query::Request::GetCodeActions(_)) => ("GetCodeActions", true), + Some(lsp_query::Request::GetSignatureHelp(_)) => ("GetSignatureHelp", false), + Some(lsp_query::Request::GetCodeLens(_)) => ("GetCodeLens", true), + Some(lsp_query::Request::GetDocumentDiagnostics(_)) => { + ("GetDocumentDiagnostics", false) + } + Some(lsp_query::Request::GetDefinition(_)) => ("GetDefinition", false), + Some(lsp_query::Request::GetDeclaration(_)) => ("GetDeclaration", false), + Some(lsp_query::Request::GetTypeDefinition(_)) => ("GetTypeDefinition", false), + Some(lsp_query::Request::GetImplementation(_)) => ("GetImplementation", false), + Some(lsp_query::Request::GetReferences(_)) => ("GetReferences", false), + Some(lsp_query::Request::GetDocumentColor(_)) => ("GetDocumentColor", false), + None => ("", true), + } + } +} + +// todo(lsp) remove after Zed Stable hits v0.204.x impl MultiLspQuery { pub fn request_str(&self) -> &str { match self.request { diff --git a/crates/proto/src/typed_envelope.rs b/crates/proto/src/typed_envelope.rs index 381a6379dc95b9f96025315c357fecfe5b8fc937..f677a3b96728a574416cbfc1ec97799ac19184fa 100644 --- a/crates/proto/src/typed_envelope.rs +++ b/crates/proto/src/typed_envelope.rs @@ -31,6 +31,58 @@ pub trait RequestMessage: EnvelopedMessage { type Response: EnvelopedMessage; } +/// A trait to bind LSP request and responses for the proto layer. +/// Should be used for every LSP request that has to traverse through the proto layer. +/// +/// `lsp_messages` macro in the same crate provides a convenient way to implement this. +pub trait LspRequestMessage: EnvelopedMessage { + type Response: EnvelopedMessage; + + fn to_proto_query(self) -> crate::lsp_query::Request; + + fn response_to_proto_query(response: Self::Response) -> crate::lsp_response::Response; + + fn buffer_id(&self) -> u64; + + fn buffer_version(&self) -> &[crate::VectorClockEntry]; + + /// Whether to deduplicate the requests, or keep the previous ones running when another + /// request of the same kind is processed. + fn stop_previous_requests() -> bool; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct LspRequestId(pub u64); + +/// A response from a single language server. +/// There could be multiple responses for a single LSP request, +/// from different servers. +pub struct ProtoLspResponse { + pub server_id: u64, + pub response: R, +} + +impl ProtoLspResponse> { + pub fn into_response(self) -> Result> { + let envelope = self + .response + .into_any() + .downcast::>() + .map_err(|_| { + anyhow::anyhow!( + "cannot downcast LspResponse to {} for message {}", + T::Response::NAME, + T::NAME, + ) + })?; + + Ok(ProtoLspResponse { + server_id: self.server_id, + response: envelope.payload, + }) + } +} + pub trait AnyTypedEnvelope: Any + Send + Sync { fn payload_type_id(&self) -> TypeId; fn payload_type_name(&self) -> &'static str; diff --git a/crates/rpc/src/proto_client.rs b/crates/rpc/src/proto_client.rs index 05b6bd1439c96a3c49dbabe69453e491f23b02da..791b7db9c0ad8e6116ed7fe60a84aeed82a99435 100644 --- a/crates/rpc/src/proto_client.rs +++ b/crates/rpc/src/proto_client.rs @@ -1,35 +1,48 @@ -use anyhow::Context; +use anyhow::{Context, Result}; use collections::HashMap; use futures::{ Future, FutureExt as _, + channel::oneshot, future::{BoxFuture, LocalBoxFuture}, }; -use gpui::{AnyEntity, AnyWeakEntity, AsyncApp, Entity}; +use gpui::{AnyEntity, AnyWeakEntity, AsyncApp, BackgroundExecutor, Entity, FutureExt as _}; +use parking_lot::Mutex; use proto::{ - AnyTypedEnvelope, EntityMessage, Envelope, EnvelopedMessage, RequestMessage, TypedEnvelope, - error::ErrorExt as _, + AnyTypedEnvelope, EntityMessage, Envelope, EnvelopedMessage, LspRequestId, LspRequestMessage, + RequestMessage, TypedEnvelope, error::ErrorExt as _, }; use std::{ any::{Any, TypeId}, - sync::{Arc, Weak}, + sync::{ + Arc, OnceLock, + atomic::{self, AtomicU64}, + }, + time::Duration, }; #[derive(Clone)] -pub struct AnyProtoClient(Arc); +pub struct AnyProtoClient(Arc); -impl AnyProtoClient { - pub fn downgrade(&self) -> AnyWeakProtoClient { - AnyWeakProtoClient(Arc::downgrade(&self.0)) - } -} +type RequestIds = Arc< + Mutex< + HashMap< + LspRequestId, + oneshot::Sender< + Result< + Option>>>>, + >, + >, + >, + >, +>; -#[derive(Clone)] -pub struct AnyWeakProtoClient(Weak); +static NEXT_LSP_REQUEST_ID: OnceLock> = OnceLock::new(); +static REQUEST_IDS: OnceLock = OnceLock::new(); -impl AnyWeakProtoClient { - pub fn upgrade(&self) -> Option { - self.0.upgrade().map(AnyProtoClient) - } +struct State { + client: Arc, + next_lsp_request_id: Arc, + request_ids: RequestIds, } pub trait ProtoClient: Send + Sync { @@ -37,11 +50,11 @@ pub trait ProtoClient: Send + Sync { &self, envelope: Envelope, request_type: &'static str, - ) -> BoxFuture<'static, anyhow::Result>; + ) -> BoxFuture<'static, Result>; - fn send(&self, envelope: Envelope, message_type: &'static str) -> anyhow::Result<()>; + fn send(&self, envelope: Envelope, message_type: &'static str) -> Result<()>; - fn send_response(&self, envelope: Envelope, message_type: &'static str) -> anyhow::Result<()>; + fn send_response(&self, envelope: Envelope, message_type: &'static str) -> Result<()>; fn message_handler_set(&self) -> &parking_lot::Mutex; @@ -65,7 +78,7 @@ pub type ProtoMessageHandler = Arc< Box, AnyProtoClient, AsyncApp, - ) -> LocalBoxFuture<'static, anyhow::Result<()>>, + ) -> LocalBoxFuture<'static, Result<()>>, >; impl ProtoMessageHandlerSet { @@ -113,7 +126,7 @@ impl ProtoMessageHandlerSet { message: Box, client: AnyProtoClient, cx: AsyncApp, - ) -> Option>> { + ) -> Option>> { let payload_type_id = message.payload_type_id(); let mut this = this.lock(); let handler = this.message_handlers.get(&payload_type_id)?.clone(); @@ -169,43 +182,195 @@ where T: ProtoClient + 'static, { fn from(client: Arc) -> Self { - Self(client) + Self::new(client) } } impl AnyProtoClient { pub fn new(client: Arc) -> Self { - Self(client) + Self(Arc::new(State { + client, + next_lsp_request_id: NEXT_LSP_REQUEST_ID + .get_or_init(|| Arc::new(AtomicU64::new(0))) + .clone(), + request_ids: REQUEST_IDS.get_or_init(RequestIds::default).clone(), + })) } pub fn is_via_collab(&self) -> bool { - self.0.is_via_collab() + self.0.client.is_via_collab() } pub fn request( &self, request: T, - ) -> impl Future> + use { + ) -> impl Future> + use { let envelope = request.into_envelope(0, None, None); - let response = self.0.request(envelope, T::NAME); + let response = self.0.client.request(envelope, T::NAME); async move { T::Response::from_envelope(response.await?) .context("received response of the wrong type") } } - pub fn send(&self, request: T) -> anyhow::Result<()> { + pub fn send(&self, request: T) -> Result<()> { let envelope = request.into_envelope(0, None, None); - self.0.send(envelope, T::NAME) + self.0.client.send(envelope, T::NAME) + } + + pub fn send_response(&self, request_id: u32, request: T) -> Result<()> { + let envelope = request.into_envelope(0, Some(request_id), None); + self.0.client.send(envelope, T::NAME) } - pub fn send_response( + pub fn request_lsp( &self, - request_id: u32, + project_id: u64, + timeout: Duration, + executor: BackgroundExecutor, request: T, - ) -> anyhow::Result<()> { - let envelope = request.into_envelope(0, Some(request_id), None); - self.0.send(envelope, T::NAME) + ) -> impl Future< + Output = Result>>>>, + > + use + where + T: LspRequestMessage, + { + let new_id = LspRequestId( + self.0 + .next_lsp_request_id + .fetch_add(1, atomic::Ordering::Acquire), + ); + let (tx, rx) = oneshot::channel(); + { + self.0.request_ids.lock().insert(new_id, tx); + } + + let query = proto::LspQuery { + project_id, + lsp_request_id: new_id.0, + request: Some(request.clone().to_proto_query()), + }; + let request = self.request(query); + let request_ids = self.0.request_ids.clone(); + async move { + match request.await { + Ok(_request_enqueued) => {} + Err(e) => { + request_ids.lock().remove(&new_id); + return Err(e).context("sending LSP proto request"); + } + } + + let response = rx.with_timeout(timeout, &executor).await; + { + request_ids.lock().remove(&new_id); + } + match response { + Ok(Ok(response)) => { + let response = response + .context("waiting for LSP proto response")? + .map(|response| { + anyhow::Ok(TypedEnvelope { + payload: response + .payload + .into_iter() + .map(|lsp_response| lsp_response.into_response::()) + .collect::>>()?, + sender_id: response.sender_id, + original_sender_id: response.original_sender_id, + message_id: response.message_id, + received_at: response.received_at, + }) + }) + .transpose() + .context("converting LSP proto response")?; + Ok(response) + } + Err(_cancelled_due_timeout) => Ok(None), + Ok(Err(_channel_dropped)) => Ok(None), + } + } + } + + pub fn send_lsp_response( + &self, + project_id: u64, + lsp_request_id: LspRequestId, + server_responses: HashMap, + ) -> Result<()> { + self.send(proto::LspQueryResponse { + project_id, + lsp_request_id: lsp_request_id.0, + responses: server_responses + .into_iter() + .map(|(server_id, response)| proto::LspResponse { + server_id, + response: Some(T::response_to_proto_query(response)), + }) + .collect(), + }) + } + + pub fn handle_lsp_response(&self, mut envelope: TypedEnvelope) { + let request_id = LspRequestId(envelope.payload.lsp_request_id); + let mut response_senders = self.0.request_ids.lock(); + if let Some(tx) = response_senders.remove(&request_id) { + let responses = envelope.payload.responses.drain(..).collect::>(); + tx.send(Ok(Some(proto::TypedEnvelope { + sender_id: envelope.sender_id, + original_sender_id: envelope.original_sender_id, + message_id: envelope.message_id, + received_at: envelope.received_at, + payload: responses + .into_iter() + .filter_map(|response| { + use proto::lsp_response::Response; + + let server_id = response.server_id; + let response = match response.response? { + Response::GetReferencesResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetDocumentColorResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetHoverResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetCodeActionsResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetSignatureHelpResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetCodeLensResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetDocumentDiagnosticsResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetDefinitionResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetDeclarationResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetTypeDefinitionResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetImplementationResponse(response) => { + to_any_envelope(&envelope, response) + } + }; + Some(proto::ProtoLspResponse { + server_id, + response, + }) + }) + .collect(), + }))) + .ok(); + } } pub fn add_request_handler(&self, entity: gpui::WeakEntity, handler: H) @@ -213,31 +378,35 @@ impl AnyProtoClient { M: RequestMessage, E: 'static, H: 'static + Sync + Fn(Entity, TypedEnvelope, AsyncApp) -> F + Send + Sync, - F: 'static + Future>, + F: 'static + Future>, { - self.0.message_handler_set().lock().add_message_handler( - TypeId::of::(), - entity.into(), - Arc::new(move |entity, envelope, client, cx| { - let entity = entity.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); - let request_id = envelope.message_id(); - handler(entity, *envelope, cx) - .then(move |result| async move { - match result { - Ok(response) => { - client.send_response(request_id, response)?; - Ok(()) - } - Err(error) => { - client.send_response(request_id, error.to_proto())?; - Err(error) + self.0 + .client + .message_handler_set() + .lock() + .add_message_handler( + TypeId::of::(), + entity.into(), + Arc::new(move |entity, envelope, client, cx| { + let entity = entity.downcast::().unwrap(); + let envelope = envelope.into_any().downcast::>().unwrap(); + let request_id = envelope.message_id(); + handler(entity, *envelope, cx) + .then(move |result| async move { + match result { + Ok(response) => { + client.send_response(request_id, response)?; + Ok(()) + } + Err(error) => { + client.send_response(request_id, error.to_proto())?; + Err(error) + } } - } - }) - .boxed_local() - }), - ) + }) + .boxed_local() + }), + ) } pub fn add_entity_request_handler(&self, handler: H) @@ -245,7 +414,7 @@ impl AnyProtoClient { M: EnvelopedMessage + RequestMessage + EntityMessage, E: 'static, H: 'static + Sync + Send + Fn(gpui::Entity, TypedEnvelope, AsyncApp) -> F, - F: 'static + Future>, + F: 'static + Future>, { let message_type_id = TypeId::of::(); let entity_type_id = TypeId::of::(); @@ -257,6 +426,7 @@ impl AnyProtoClient { .remote_entity_id() }; self.0 + .client .message_handler_set() .lock() .add_entity_message_handler( @@ -290,7 +460,7 @@ impl AnyProtoClient { M: EnvelopedMessage + EntityMessage, E: 'static, H: 'static + Sync + Send + Fn(gpui::Entity, TypedEnvelope, AsyncApp) -> F, - F: 'static + Future>, + F: 'static + Future>, { let message_type_id = TypeId::of::(); let entity_type_id = TypeId::of::(); @@ -302,6 +472,7 @@ impl AnyProtoClient { .remote_entity_id() }; self.0 + .client .message_handler_set() .lock() .add_entity_message_handler( @@ -319,7 +490,7 @@ impl AnyProtoClient { pub fn subscribe_to_entity(&self, remote_id: u64, entity: &Entity) { let id = (TypeId::of::(), remote_id); - let mut message_handlers = self.0.message_handler_set().lock(); + let mut message_handlers = self.0.client.message_handler_set().lock(); if message_handlers .entities_by_type_and_remote_id .contains_key(&id) @@ -335,3 +506,16 @@ impl AnyProtoClient { ); } } + +fn to_any_envelope( + envelope: &TypedEnvelope, + response: T, +) -> Box { + Box::new(proto::TypedEnvelope { + sender_id: envelope.sender_id, + original_sender_id: envelope.original_sender_id, + message_id: envelope.message_id, + received_at: envelope.received_at, + payload: response, + }) as Box<_> +} From 68f97d6069ad7f35929c2e0e2d7265bbc96c6e56 Mon Sep 17 00:00:00 2001 From: Sachith Shetty Date: Wed, 20 Aug 2025 23:27:41 -0700 Subject: [PATCH 222/744] editor: Use `highlight_text` to highlight matching brackets, fix unnecessary inlay hint highlighting (#36540) Closes #35981 Release Notes: - Fixed bracket highlights overly including parts of inlays when highlighting Before - Screenshot from 2025-08-19 17-15-06 After - Screenshot from 2025-08-19 17-24-26 --- .../editor/src/highlight_matching_bracket.rs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index e38197283d4a4e2623ecadb30d90d0363053fdc5..aa4e616924ad6bd47627bfd95e9a5c58587afc25 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -1,6 +1,7 @@ use crate::{Editor, RangeToAnchorExt}; -use gpui::{Context, Window}; +use gpui::{Context, HighlightStyle, Window}; use language::CursorShape; +use theme::ActiveTheme; enum MatchingBracketHighlight {} @@ -9,7 +10,7 @@ pub fn refresh_matching_bracket_highlights( window: &mut Window, cx: &mut Context, ) { - editor.clear_background_highlights::(cx); + editor.clear_highlights::(cx); let newest_selection = editor.selections.newest::(cx); // Don't highlight brackets if the selection isn't empty @@ -35,12 +36,19 @@ pub fn refresh_matching_bracket_highlights( .buffer_snapshot .innermost_enclosing_bracket_ranges(head..tail, None) { - editor.highlight_background::( - &[ + editor.highlight_text::( + vec![ opening_range.to_anchors(&snapshot.buffer_snapshot), closing_range.to_anchors(&snapshot.buffer_snapshot), ], - |theme| theme.colors().editor_document_highlight_bracket_background, + HighlightStyle { + background_color: Some( + cx.theme() + .colors() + .editor_document_highlight_bracket_background, + ), + ..Default::default() + }, cx, ) } @@ -104,7 +112,7 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test«(»"Test argument"«)» { another_test(1, 2, 3); } @@ -115,7 +123,7 @@ mod tests { another_test(1, ˇ2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test("Test argument") { another_test«(»1, 2, 3«)»; } @@ -126,7 +134,7 @@ mod tests { anotherˇ_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test("Test argument") «{» another_test(1, 2, 3); «}» @@ -138,7 +146,7 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test("Test argument") { another_test(1, 2, 3); } @@ -150,8 +158,8 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" - pub fn test("Test argument") { + cx.assert_editor_text_highlights::(indoc! {r#" + pub fn test«("Test argument") { another_test(1, 2, 3); } "#}); From cde0a5dd27c7f29e389cf8d518983d21f3376071 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 21 Aug 2025 09:36:57 +0300 Subject: [PATCH 223/744] Add a non-style lint exclusion (#36658) Follow-up of https://github.com/zed-industries/zed/pull/36651 Restores https://github.com/zed-industries/zed/pull/35955 footgun guard. Release Notes: - N/A --- Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 400ce791aa43c197593ba28e9e5114a687f731ee..b13795e1e191e45e01b45a79287c56eaf397d9fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -802,7 +802,10 @@ unexpected_cfgs = { level = "allow" } dbg_macro = "deny" todo = "deny" -# trying this out +# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454 +# Remove when the lint gets promoted to `suspicious`. +declare_interior_mutable_const = "deny" + redundant_clone = "deny" # We currently do not restrict any style rules From ed84767c9d1d597c8b81e8e927ad1be35bb59add Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 21 Aug 2025 09:48:04 +0300 Subject: [PATCH 224/744] Fix overlooked Clippy lints (#36659) Follow-up of https://github.com/zed-industries/zed/pull/36557 that is needed after https://github.com/zed-industries/zed/pull/36652 Release Notes: - N/A --- crates/project/src/lsp_store.rs | 8 ++++---- crates/rpc/src/proto_client.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index bcfd9d386b111dce3e1173fc99b131dfdf317b76..072f4396c1cd640cfed0484b410b3ad4991539f8 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -5693,13 +5693,13 @@ impl LspStore { let lsp_data = lsp_store.lsp_code_lens.entry(buffer_id).or_default(); if let Some(fetched_lens) = fetched_lens { if lsp_data.lens_for_version == query_version_queried_for { - lsp_data.lens.extend(fetched_lens.clone()); + lsp_data.lens.extend(fetched_lens); } else if !lsp_data .lens_for_version .changed_since(&query_version_queried_for) { lsp_data.lens_for_version = query_version_queried_for; - lsp_data.lens = fetched_lens.clone(); + lsp_data.lens = fetched_lens; } } lsp_data.update = None; @@ -6694,14 +6694,14 @@ impl LspStore { if let Some(fetched_colors) = fetched_colors { if lsp_data.colors_for_version == query_version_queried_for { - lsp_data.colors.extend(fetched_colors.clone()); + lsp_data.colors.extend(fetched_colors); lsp_data.cache_version += 1; } else if !lsp_data .colors_for_version .changed_since(&query_version_queried_for) { lsp_data.colors_for_version = query_version_queried_for; - lsp_data.colors = fetched_colors.clone(); + lsp_data.colors = fetched_colors; lsp_data.cache_version += 1; } } diff --git a/crates/rpc/src/proto_client.rs b/crates/rpc/src/proto_client.rs index 791b7db9c0ad8e6116ed7fe60a84aeed82a99435..a90797ff5dfb44c22fa7aa61751ad3baefd2b745 100644 --- a/crates/rpc/src/proto_client.rs +++ b/crates/rpc/src/proto_client.rs @@ -248,7 +248,7 @@ impl AnyProtoClient { let query = proto::LspQuery { project_id, lsp_request_id: new_id.0, - request: Some(request.clone().to_proto_query()), + request: Some(request.to_proto_query()), }; let request = self.request(query); let request_ids = self.0.request_ids.clone(); From fda6eda3c2abcbe90af48bd112ee560eb63706e7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Aug 2025 10:57:28 +0200 Subject: [PATCH 225/744] Fix @-mentioning threads when their summary isn't ready yet (#36664) Release Notes: - N/A --- crates/agent2/src/agent.rs | 10 ++-------- crates/agent_ui/src/acp/message_editor.rs | 9 +++------ 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index c15048ad8c53943ef57b04ebfe90bb9a43c307f3..d5bc0fea63018c4023ebc94aee4d6d082a88c4cb 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1269,18 +1269,12 @@ mod tests { let model = Arc::new(FakeLanguageModel::default()); let summary_model = Arc::new(FakeLanguageModel::default()); thread.update(cx, |thread, cx| { - thread.set_model(model, cx); - thread.set_summarization_model(Some(summary_model), cx); + thread.set_model(model.clone(), cx); + thread.set_summarization_model(Some(summary_model.clone()), cx); }); cx.run_until_parked(); assert_eq!(history_entries(&history_store, cx), vec![]); - let model = thread.read_with(cx, |thread, _| thread.model().unwrap().clone()); - let model = model.as_fake(); - let summary_model = thread.read_with(cx, |thread, _| { - thread.summarization_model().unwrap().clone() - }); - let summary_model = summary_model.as_fake(); let send = acp_thread.update(cx, |thread, cx| { thread.send( vec![ diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 1155285d09e487d5fb87e6a62cefae9bd2aac7e0..3116a40be55e859bcdc82866023e392566e22102 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -629,15 +629,11 @@ impl MessageEditor { .shared(); self.mention_set.insert_thread(id.clone(), task.clone()); + self.mention_set.insert_uri(crease_id, uri); let editor = self.editor.clone(); cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - this.update(cx, |this, _| { - this.mention_set.insert_uri(crease_id, uri); - }) - .ok(); - } else { + if task.await.notify_async_err(cx).is_none() { editor .update(cx, |editor, cx| { editor.display_map.update(cx, |display_map, cx| { @@ -648,6 +644,7 @@ impl MessageEditor { .ok(); this.update(cx, |this, _| { this.mention_set.thread_summaries.remove(&id); + this.mention_set.uri_by_crease_id.remove(&crease_id); }) .ok(); } From 62f2ef86dca7e4d171050be9951585199a25aa32 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Thu, 21 Aug 2025 11:25:00 +0200 Subject: [PATCH 226/744] agent2: Allow expanding terminals individually (#36670) Release Notes: - N/A --- crates/agent_ui/src/acp/entry_view_state.rs | 10 ++++-- crates/agent_ui/src/acp/thread_view.rs | 36 ++++++++++++++------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 67acbb8b5b8354ecc143216f2ee5bc4303afec1c..fb15d8bed8067bc8f3417540ec05036c5ccca791 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -121,14 +121,19 @@ impl EntryViewState { for terminal in terminals { views.entry(terminal.entity_id()).or_insert_with(|| { - create_terminal( + let element = create_terminal( self.workspace.clone(), self.project.clone(), terminal.clone(), window, cx, ) - .into_any() + .into_any(); + cx.emit(EntryViewEvent { + entry_index: index, + view_event: ViewEvent::NewTerminal(terminal.entity_id()), + }); + element }); } @@ -187,6 +192,7 @@ pub struct EntryViewEvent { } pub enum ViewEvent { + NewTerminal(EntityId), MessageEditorEvent(Entity, MessageEditorEvent), } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 12a33d022e382d3a8639270d4f10f707b9170434..432ba4e0e8991aca710a19599f2ff6e0f52f451c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -20,11 +20,11 @@ use file_icons::FileIcons; use fs::Fs; use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, - EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, - ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, - Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, - WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point, - prelude::*, pulsating_between, + EdgesRefinement, ElementId, Empty, Entity, EntityId, FocusHandle, Focusable, Hsla, Length, + ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, + Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, + Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, + point, prelude::*, pulsating_between, }; use language::Buffer; @@ -256,10 +256,10 @@ pub struct AcpThreadView { auth_task: Option>, expanded_tool_calls: HashSet, expanded_thinking_blocks: HashSet<(usize, usize)>, + expanded_terminals: HashSet, edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, - terminal_expanded: bool, editing_message: Option, _cancel_task: Option>, _subscriptions: [Subscription; 3], @@ -354,11 +354,11 @@ impl AcpThreadView { auth_task: None, expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), + expanded_terminals: HashSet::default(), editing_message: None, edits_expanded: false, plan_expanded: false, editor_expanded: false, - terminal_expanded: true, history_store, hovered_recent_history_item: None, _subscriptions: subscriptions, @@ -677,6 +677,11 @@ impl AcpThreadView { cx: &mut Context, ) { match &event.view_event { + ViewEvent::NewTerminal(terminal_id) => { + if AgentSettings::get_global(cx).expand_terminal_card { + self.expanded_terminals.insert(*terminal_id); + } + } ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => { if let Some(thread) = self.thread() && let Some(AgentThreadEntry::UserMessage(user_message)) = @@ -2009,6 +2014,8 @@ impl AcpThreadView { .map(|path| format!("{}", path.display())) .unwrap_or_else(|| "current directory".to_string()); + let is_expanded = self.expanded_terminals.contains(&terminal.entity_id()); + let header = h_flex() .id(SharedString::from(format!( "terminal-tool-header-{}", @@ -2142,12 +2149,19 @@ impl AcpThreadView { "terminal-tool-disclosure-{}", terminal.entity_id() )), - self.terminal_expanded, + is_expanded, ) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) - .on_click(cx.listener(move |this, _event, _window, _cx| { - this.terminal_expanded = !this.terminal_expanded; + .on_click(cx.listener({ + let terminal_id = terminal.entity_id(); + move |this, _event, _window, _cx| { + if is_expanded { + this.expanded_terminals.remove(&terminal_id); + } else { + this.expanded_terminals.insert(terminal_id); + } + } })), ); @@ -2156,7 +2170,7 @@ impl AcpThreadView { .read(cx) .entry(entry_ix) .and_then(|entry| entry.terminal(terminal)); - let show_output = self.terminal_expanded && terminal_view.is_some(); + let show_output = is_expanded && terminal_view.is_some(); v_flex() .mb_2() From 7f1bd2f15eb6684c7c63c09f2520c9a6a344a6c8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 21 Aug 2025 11:37:45 +0200 Subject: [PATCH 227/744] remote: Fix toolchain RPC messages not being handled because of the entity getting dropped (#36665) Release Notes: - N/A --- crates/project/src/toolchain_store.rs | 73 ++++++++++++------- crates/remote_server/src/headless_project.rs | 4 + .../src/active_toolchain.rs | 11 --- 3 files changed, 49 insertions(+), 39 deletions(-) diff --git a/crates/project/src/toolchain_store.rs b/crates/project/src/toolchain_store.rs index 05531ebe9ae44435a80e371da20bde6a138e13f7..ac87e6424821a5d28dbf48b92b077183a21d8608 100644 --- a/crates/project/src/toolchain_store.rs +++ b/crates/project/src/toolchain_store.rs @@ -34,7 +34,10 @@ enum ToolchainStoreInner { Entity, #[allow(dead_code)] Subscription, ), - Remote(Entity), + Remote( + Entity, + #[allow(dead_code)] Subscription, + ), } impl EventEmitter for ToolchainStore {} @@ -65,10 +68,12 @@ impl ToolchainStore { Self(ToolchainStoreInner::Local(entity, subscription)) } - pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut App) -> Self { - Self(ToolchainStoreInner::Remote( - cx.new(|_| RemoteToolchainStore { client, project_id }), - )) + pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut Context) -> Self { + let entity = cx.new(|_| RemoteToolchainStore { client, project_id }); + let _subscription = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| { + cx.emit(e.clone()) + }); + Self(ToolchainStoreInner::Remote(entity, _subscription)) } pub(crate) fn activate_toolchain( &self, @@ -80,8 +85,8 @@ impl ToolchainStore { ToolchainStoreInner::Local(local, _) => { local.update(cx, |this, cx| this.activate_toolchain(path, toolchain, cx)) } - ToolchainStoreInner::Remote(remote) => { - remote.read(cx).activate_toolchain(path, toolchain, cx) + ToolchainStoreInner::Remote(remote, _) => { + remote.update(cx, |this, cx| this.activate_toolchain(path, toolchain, cx)) } } } @@ -95,7 +100,7 @@ impl ToolchainStore { ToolchainStoreInner::Local(local, _) => { local.update(cx, |this, cx| this.list_toolchains(path, language_name, cx)) } - ToolchainStoreInner::Remote(remote) => { + ToolchainStoreInner::Remote(remote, _) => { remote.read(cx).list_toolchains(path, language_name, cx) } } @@ -112,7 +117,7 @@ impl ToolchainStore { &path.path, language_name, )), - ToolchainStoreInner::Remote(remote) => { + ToolchainStoreInner::Remote(remote, _) => { remote.read(cx).active_toolchain(path, language_name, cx) } } @@ -234,13 +239,13 @@ impl ToolchainStore { pub fn as_language_toolchain_store(&self) -> Arc { match &self.0 { ToolchainStoreInner::Local(local, _) => Arc::new(LocalStore(local.downgrade())), - ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())), + ToolchainStoreInner::Remote(remote, _) => Arc::new(RemoteStore(remote.downgrade())), } } pub fn as_local_store(&self) -> Option<&Entity> { match &self.0 { ToolchainStoreInner::Local(local, _) => Some(local), - ToolchainStoreInner::Remote(_) => None, + ToolchainStoreInner::Remote(_, _) => None, } } } @@ -415,6 +420,8 @@ impl LocalToolchainStore { .cloned() } } + +impl EventEmitter for RemoteToolchainStore {} struct RemoteToolchainStore { client: AnyProtoClient, project_id: u64, @@ -425,27 +432,37 @@ impl RemoteToolchainStore { &self, project_path: ProjectPath, toolchain: Toolchain, - cx: &App, + cx: &mut Context, ) -> Task> { let project_id = self.project_id; let client = self.client.clone(); - cx.background_spawn(async move { - let path = PathBuf::from(toolchain.path.to_string()); - let _ = client - .request(proto::ActivateToolchain { - project_id, - worktree_id: project_path.worktree_id.to_proto(), - language_name: toolchain.language_name.into(), - toolchain: Some(proto::Toolchain { - name: toolchain.name.into(), - path: path.to_proto(), - raw_json: toolchain.as_json.to_string(), - }), - path: Some(project_path.path.to_string_lossy().into_owned()), + cx.spawn(async move |this, cx| { + let did_activate = cx + .background_spawn(async move { + let path = PathBuf::from(toolchain.path.to_string()); + let _ = client + .request(proto::ActivateToolchain { + project_id, + worktree_id: project_path.worktree_id.to_proto(), + language_name: toolchain.language_name.into(), + toolchain: Some(proto::Toolchain { + name: toolchain.name.into(), + path: path.to_proto(), + raw_json: toolchain.as_json.to_string(), + }), + path: Some(project_path.path.to_string_lossy().into_owned()), + }) + .await + .log_err()?; + Some(()) }) - .await - .log_err()?; - Some(()) + .await; + did_activate.and_then(|_| { + this.update(cx, |_, cx| { + cx.emit(ToolchainStoreEvent::ToolchainActivated); + }) + .ok() + }) }) } diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 83caebe62fe5d1f306e9302e0c81a998fbf2472d..6216ff77288938f7e4d424101c572f5bea13b69a 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -46,6 +46,9 @@ pub struct HeadlessProject { pub languages: Arc, pub extensions: Entity, pub git_store: Entity, + // Used mostly to keep alive the toolchain store for RPC handlers. + // Local variant is used within LSP store, but that's a separate entity. + pub _toolchain_store: Entity, } pub struct HeadlessAppState { @@ -269,6 +272,7 @@ impl HeadlessProject { languages, extensions, git_store, + _toolchain_store: toolchain_store, } } diff --git a/crates/toolchain_selector/src/active_toolchain.rs b/crates/toolchain_selector/src/active_toolchain.rs index ea5dcc2a1964a3330069e45b04e5ede8fa5a6972..bf45bffea30791a062e4a130b0f742f3d47c1342 100644 --- a/crates/toolchain_selector/src/active_toolchain.rs +++ b/crates/toolchain_selector/src/active_toolchain.rs @@ -38,7 +38,6 @@ impl ActiveToolchain { .ok() .flatten(); if let Some(editor) = editor { - this.active_toolchain.take(); this.update_lister(editor, window, cx); } }, @@ -124,16 +123,6 @@ impl ActiveToolchain { if let Some((_, buffer, _)) = editor.active_excerpt(cx) && let Some(worktree_id) = buffer.read(cx).file().map(|file| file.worktree_id(cx)) { - if self - .active_buffer - .as_ref() - .is_some_and(|(old_worktree_id, old_buffer, _)| { - (old_worktree_id, old_buffer.entity_id()) == (&worktree_id, buffer.entity_id()) - }) - { - return; - } - let subscription = cx.subscribe_in( &buffer, window, From c5ee3f3e2e51936910f9ad284d14a7974f064616 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Thu, 21 Aug 2025 05:33:45 -0500 Subject: [PATCH 228/744] Avoid suspending panicking thread while crashing (#36645) On the latest build @maxbrunsfeld got a panic that hung zed. It appeared that the hang occured after the minidump had been successfully written, so our theory on what happened is that the `suspend_all_other_threads` call in the crash handler suspended the panicking thread (due to the signal from simulate_exception being received on a different thread), and then when the crash handler returned everything was suspended so the panic hook never made it to the `process::abort`. This change makes the crash handler avoid _both_ the current and the panicking thread which should avoid that scenario. Release Notes: - N/A --- crates/crashes/src/crashes.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index 4e4b69f639f72edae2d1bbfd9c09191f1835345b..b1afc5ae454d30c9c8ee283fce5e03b942fb7c70 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -4,6 +4,8 @@ use minidumper::{Client, LoopAction, MinidumpBinary}; use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; use serde::{Deserialize, Serialize}; +#[cfg(target_os = "macos")] +use std::sync::atomic::AtomicU32; use std::{ env, fs::{self, File}, @@ -26,6 +28,9 @@ pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false); const CRASH_HANDLER_PING_TIMEOUT: Duration = Duration::from_secs(60); const CRASH_HANDLER_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); +#[cfg(target_os = "macos")] +static PANIC_THREAD_ID: AtomicU32 = AtomicU32::new(0); + pub async fn init(crash_init: InitCrashHandler) { if *RELEASE_CHANNEL == ReleaseChannel::Dev && env::var("ZED_GENERATE_MINIDUMPS").is_err() { return; @@ -110,9 +115,10 @@ unsafe fn suspend_all_other_threads() { mach2::task::task_threads(task, &raw mut threads, &raw mut count); } let current = unsafe { mach2::mach_init::mach_thread_self() }; + let panic_thread = PANIC_THREAD_ID.load(Ordering::SeqCst); for i in 0..count { let t = unsafe { *threads.add(i as usize) }; - if t != current { + if t != current && t != panic_thread { unsafe { mach2::thread_act::thread_suspend(t) }; } } @@ -238,6 +244,13 @@ pub fn handle_panic(message: String, span: Option<&Location>) { ) .ok(); log::error!("triggering a crash to generate a minidump..."); + + #[cfg(target_os = "macos")] + PANIC_THREAD_ID.store( + unsafe { mach2::mach_init::mach_thread_self() }, + Ordering::SeqCst, + ); + #[cfg(target_os = "linux")] CrashHandler.simulate_signal(crash_handler::Signal::Trap as u32); #[cfg(not(target_os = "linux"))] From f435af2fdeeda60e24d08bcee56d3b6c5df07ca4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Aug 2025 12:59:51 +0200 Subject: [PATCH 229/744] acp: Use unstaged style for diffs (#36674) Release Notes: - N/A --- crates/acp_thread/src/diff.rs | 55 +++++++++++++++++------------------ 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index 70367e340adbf5f787557f81963de85293273cc1..130bc3ab6bced76320c80e49aef5bdb555c54e7e 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -28,12 +28,7 @@ impl Diff { cx: &mut Context, ) -> Self { let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly)); - - let new_buffer = cx.new(|cx| Buffer::local(new_text, cx)); - let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx)); - let new_buffer_snapshot = new_buffer.read(cx).text_snapshot(); - let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx)); - + let buffer = cx.new(|cx| Buffer::local(new_text, cx)); let task = cx.spawn({ let multibuffer = multibuffer.clone(); let path = path.clone(); @@ -43,42 +38,34 @@ impl Diff { .await .log_err(); - new_buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?; + buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?; - let old_buffer_snapshot = old_buffer.update(cx, |buffer, cx| { - buffer.set_language(language, cx); - buffer.snapshot() - })?; - - buffer_diff - .update(cx, |diff, cx| { - diff.set_base_text( - old_buffer_snapshot, - Some(language_registry), - new_buffer_snapshot, - cx, - ) - })? - .await?; + let diff = build_buffer_diff( + old_text.unwrap_or("".into()).into(), + &buffer, + Some(language_registry.clone()), + cx, + ) + .await?; multibuffer .update(cx, |multibuffer, cx| { let hunk_ranges = { - let buffer = new_buffer.read(cx); - let diff = buffer_diff.read(cx); + let buffer = buffer.read(cx); + let diff = diff.read(cx); diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) .collect::>() }; multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&new_buffer, cx), - new_buffer.clone(), + PathKey::for_buffer(&buffer, cx), + buffer.clone(), hunk_ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, ); - multibuffer.add_diff(buffer_diff, cx); + multibuffer.add_diff(diff, cx); }) .log_err(); @@ -106,6 +93,15 @@ impl Diff { text_snapshot, cx, ); + let snapshot = diff.snapshot(cx); + + let secondary_diff = cx.new(|cx| { + let mut diff = BufferDiff::new(&buffer_snapshot, cx); + diff.set_snapshot(snapshot, &buffer_snapshot, cx); + diff + }); + diff.set_secondary_diff(secondary_diff); + diff }); @@ -204,7 +200,10 @@ impl PendingDiff { ) .await?; buffer_diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot, &text_snapshot, cx) + diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx); + diff.secondary_diff().unwrap().update(cx, |diff, cx| { + diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx); + }); })?; diff.update(cx, |diff, cx| { if let Diff::Pending(diff) = diff { From ad64a71f04fb2b1a585e26dfa6825545728188a6 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Thu, 21 Aug 2025 13:05:41 +0200 Subject: [PATCH 230/744] acp: Allow collapsing edit file tool calls (#36675) Release Notes: - N/A --- crates/agent_ui/src/acp/entry_view_state.rs | 18 ++++++++--- crates/agent_ui/src/acp/thread_view.rs | 34 +++++++++++---------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index fb15d8bed8067bc8f3417540ec05036c5ccca791..c310473259bc07468cd8a062e15dd8025645f90f 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -1,6 +1,7 @@ use std::ops::Range; use acp_thread::{AcpThread, AgentThreadEntry}; +use agent_client_protocol::ToolCallId; use agent2::HistoryStore; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; @@ -106,6 +107,7 @@ impl EntryViewState { } } AgentThreadEntry::ToolCall(tool_call) => { + let id = tool_call.id.clone(); let terminals = tool_call.terminals().cloned().collect::>(); let diffs = tool_call.diffs().cloned().collect::>(); @@ -131,16 +133,21 @@ impl EntryViewState { .into_any(); cx.emit(EntryViewEvent { entry_index: index, - view_event: ViewEvent::NewTerminal(terminal.entity_id()), + view_event: ViewEvent::NewTerminal(id.clone()), }); element }); } for diff in diffs { - views - .entry(diff.entity_id()) - .or_insert_with(|| create_editor_diff(diff.clone(), window, cx).into_any()); + views.entry(diff.entity_id()).or_insert_with(|| { + let element = create_editor_diff(diff.clone(), window, cx).into_any(); + cx.emit(EntryViewEvent { + entry_index: index, + view_event: ViewEvent::NewDiff(id.clone()), + }); + element + }); } } AgentThreadEntry::AssistantMessage(_) => { @@ -192,7 +199,8 @@ pub struct EntryViewEvent { } pub enum ViewEvent { - NewTerminal(EntityId), + NewDiff(ToolCallId), + NewTerminal(ToolCallId), MessageEditorEvent(Entity, MessageEditorEvent), } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 432ba4e0e8991aca710a19599f2ff6e0f52f451c..9c9e2ee4ddf20b5894a7538bfabb596b24c1c931 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -20,11 +20,11 @@ use file_icons::FileIcons; use fs::Fs; use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, - EdgesRefinement, ElementId, Empty, Entity, EntityId, FocusHandle, Focusable, Hsla, Length, - ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, - Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, - Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, - point, prelude::*, pulsating_between, + EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, + ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, + Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, + WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point, + prelude::*, pulsating_between, }; use language::Buffer; @@ -256,7 +256,6 @@ pub struct AcpThreadView { auth_task: Option>, expanded_tool_calls: HashSet, expanded_thinking_blocks: HashSet<(usize, usize)>, - expanded_terminals: HashSet, edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, @@ -354,7 +353,6 @@ impl AcpThreadView { auth_task: None, expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), - expanded_terminals: HashSet::default(), editing_message: None, edits_expanded: false, plan_expanded: false, @@ -677,9 +675,14 @@ impl AcpThreadView { cx: &mut Context, ) { match &event.view_event { - ViewEvent::NewTerminal(terminal_id) => { + ViewEvent::NewDiff(tool_call_id) => { + if AgentSettings::get_global(cx).expand_edit_card { + self.expanded_tool_calls.insert(tool_call_id.clone()); + } + } + ViewEvent::NewTerminal(tool_call_id) => { if AgentSettings::get_global(cx).expand_terminal_card { - self.expanded_terminals.insert(*terminal_id); + self.expanded_tool_calls.insert(tool_call_id.clone()); } } ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => { @@ -1559,10 +1562,9 @@ impl AcpThreadView { matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some(); let use_card_layout = needs_confirmation || is_edit; - let is_collapsible = !tool_call.content.is_empty() && !use_card_layout; + let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; - let is_open = - needs_confirmation || is_edit || self.expanded_tool_calls.contains(&tool_call.id); + let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); let gradient_overlay = |color: Hsla| { div() @@ -2014,7 +2016,7 @@ impl AcpThreadView { .map(|path| format!("{}", path.display())) .unwrap_or_else(|| "current directory".to_string()); - let is_expanded = self.expanded_terminals.contains(&terminal.entity_id()); + let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); let header = h_flex() .id(SharedString::from(format!( @@ -2154,12 +2156,12 @@ impl AcpThreadView { .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) .on_click(cx.listener({ - let terminal_id = terminal.entity_id(); + let id = tool_call.id.clone(); move |this, _event, _window, _cx| { if is_expanded { - this.expanded_terminals.remove(&terminal_id); + this.expanded_tool_calls.remove(&id); } else { - this.expanded_terminals.insert(terminal_id); + this.expanded_tool_calls.insert(id.clone()); } } })), From f63d8e4c538d69d3b76ed7ec93bdd88f57e6cee0 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 21 Aug 2025 09:23:56 -0400 Subject: [PATCH 231/744] Show excerpt dividers in `without_headers` multibuffers (#36647) Release Notes: - Fixed diff cards in agent threads not showing dividers between disjoint edited regions. --- crates/editor/src/display_map/block_map.rs | 99 ++++++++++++++-------- crates/editor/src/element.rs | 72 +++++++++------- crates/editor/src/test.rs | 35 ++++---- 3 files changed, 122 insertions(+), 84 deletions(-) diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index e32a4e45dbfb5d929adf980fc97f338e1b445518..b073fe7be75c82754de6ca7773b68073b213c49c 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -290,7 +290,10 @@ pub enum Block { ExcerptBoundary { excerpt: ExcerptInfo, height: u32, - starts_new_buffer: bool, + }, + BufferHeader { + excerpt: ExcerptInfo, + height: u32, }, } @@ -303,27 +306,37 @@ impl Block { .. } => BlockId::ExcerptBoundary(next_excerpt.id), Block::FoldedBuffer { first_excerpt, .. } => BlockId::FoldedBuffer(first_excerpt.id), + Block::BufferHeader { + excerpt: next_excerpt, + .. + } => BlockId::ExcerptBoundary(next_excerpt.id), } } pub fn has_height(&self) -> bool { match self { Block::Custom(block) => block.height.is_some(), - Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => true, + Block::ExcerptBoundary { .. } + | Block::FoldedBuffer { .. } + | Block::BufferHeader { .. } => true, } } pub fn height(&self) -> u32 { match self { Block::Custom(block) => block.height.unwrap_or(0), - Block::ExcerptBoundary { height, .. } | Block::FoldedBuffer { height, .. } => *height, + Block::ExcerptBoundary { height, .. } + | Block::FoldedBuffer { height, .. } + | Block::BufferHeader { height, .. } => *height, } } pub fn style(&self) -> BlockStyle { match self { Block::Custom(block) => block.style, - Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => BlockStyle::Sticky, + Block::ExcerptBoundary { .. } + | Block::FoldedBuffer { .. } + | Block::BufferHeader { .. } => BlockStyle::Sticky, } } @@ -332,6 +345,7 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Above(_)), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => true, + Block::BufferHeader { .. } => true, } } @@ -340,6 +354,7 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Near(_)), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => false, } } @@ -351,6 +366,7 @@ impl Block { ), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => false, } } @@ -359,6 +375,7 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Replace(_)), Block::FoldedBuffer { .. } => true, Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => false, } } @@ -367,6 +384,7 @@ impl Block { Block::Custom(_) => false, Block::FoldedBuffer { .. } => true, Block::ExcerptBoundary { .. } => true, + Block::BufferHeader { .. } => true, } } @@ -374,9 +392,8 @@ impl Block { match self { Block::Custom(_) => false, Block::FoldedBuffer { .. } => true, - Block::ExcerptBoundary { - starts_new_buffer, .. - } => *starts_new_buffer, + Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => true, } } } @@ -393,14 +410,14 @@ impl Debug for Block { .field("first_excerpt", &first_excerpt) .field("height", height) .finish(), - Self::ExcerptBoundary { - starts_new_buffer, - excerpt, - height, - } => f + Self::ExcerptBoundary { excerpt, height } => f .debug_struct("ExcerptBoundary") .field("excerpt", excerpt) - .field("starts_new_buffer", starts_new_buffer) + .field("height", height) + .finish(), + Self::BufferHeader { excerpt, height } => f + .debug_struct("BufferHeader") + .field("excerpt", excerpt) .field("height", height) .finish(), } @@ -662,13 +679,11 @@ impl BlockMap { }), ); - if buffer.show_headers() { - blocks_in_edit.extend(self.header_and_footer_blocks( - buffer, - (start_bound, end_bound), - wrap_snapshot, - )); - } + blocks_in_edit.extend(self.header_and_footer_blocks( + buffer, + (start_bound, end_bound), + wrap_snapshot, + )); BlockMap::sort_blocks(&mut blocks_in_edit); @@ -771,7 +786,7 @@ impl BlockMap { if self.buffers_with_disabled_headers.contains(&new_buffer_id) { continue; } - if self.folded_buffers.contains(&new_buffer_id) { + if self.folded_buffers.contains(&new_buffer_id) && buffer.show_headers() { let mut last_excerpt_end_row = first_excerpt.end_row; while let Some(next_boundary) = boundaries.peek() { @@ -804,20 +819,24 @@ impl BlockMap { } } - if new_buffer_id.is_some() { + let starts_new_buffer = new_buffer_id.is_some(); + let block = if starts_new_buffer && buffer.show_headers() { height += self.buffer_header_height; - } else { + Block::BufferHeader { + excerpt: excerpt_boundary.next, + height, + } + } else if excerpt_boundary.prev.is_some() { height += self.excerpt_header_height; - } - - return Some(( - BlockPlacement::Above(WrapRow(wrap_row)), Block::ExcerptBoundary { excerpt: excerpt_boundary.next, height, - starts_new_buffer: new_buffer_id.is_some(), - }, - )); + } + } else { + continue; + }; + + return Some((BlockPlacement::Above(WrapRow(wrap_row)), block)); } }) } @@ -842,13 +861,25 @@ impl BlockMap { ( Block::ExcerptBoundary { excerpt: excerpt_a, .. + } + | Block::BufferHeader { + excerpt: excerpt_a, .. }, Block::ExcerptBoundary { excerpt: excerpt_b, .. + } + | Block::BufferHeader { + excerpt: excerpt_b, .. }, ) => Some(excerpt_a.id).cmp(&Some(excerpt_b.id)), - (Block::ExcerptBoundary { .. }, Block::Custom(_)) => Ordering::Less, - (Block::Custom(_), Block::ExcerptBoundary { .. }) => Ordering::Greater, + ( + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }, + Block::Custom(_), + ) => Ordering::Less, + ( + Block::Custom(_), + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }, + ) => Ordering::Greater, (Block::Custom(block_a), Block::Custom(block_b)) => block_a .priority .cmp(&block_b.priority) @@ -1377,7 +1408,9 @@ impl BlockSnapshot { while let Some(transform) = cursor.item() { match &transform.block { - Some(Block::ExcerptBoundary { excerpt, .. }) => { + Some( + Block::ExcerptBoundary { excerpt, .. } | Block::BufferHeader { excerpt, .. }, + ) => { return Some(StickyHeaderExcerpt { excerpt }); } Some(block) if block.is_buffer_header() => return None, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 416f35d7a76761351afd9ab749d08745d96deb9c..797b0d663475cc074a688af8af38f044d0523809 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2749,7 +2749,10 @@ impl EditorElement { let mut block_offset = 0; let mut found_excerpt_header = false; for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) { - if matches!(block, Block::ExcerptBoundary { .. }) { + if matches!( + block, + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. } + ) { found_excerpt_header = true; break; } @@ -2766,7 +2769,10 @@ impl EditorElement { let mut block_height = 0; let mut found_excerpt_header = false; for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) { - if matches!(block, Block::ExcerptBoundary { .. }) { + if matches!( + block, + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. } + ) { found_excerpt_header = true; } block_height += block.height(); @@ -3452,42 +3458,41 @@ impl EditorElement { .into_any_element() } - Block::ExcerptBoundary { - excerpt, - height, - starts_new_buffer, - .. - } => { + Block::ExcerptBoundary { .. } => { let color = cx.theme().colors().clone(); let mut result = v_flex().id(block_id).w_full(); + result = result.child( + h_flex().relative().child( + div() + .top(line_height / 2.) + .absolute() + .w_full() + .h_px() + .bg(color.border_variant), + ), + ); + + result.into_any() + } + + Block::BufferHeader { excerpt, height } => { + let mut result = v_flex().id(block_id).w_full(); + let jump_data = header_jump_data(snapshot, block_row_start, *height, excerpt); - if *starts_new_buffer { - if sticky_header_excerpt_id != Some(excerpt.id) { - let selected = selected_buffer_ids.contains(&excerpt.buffer_id); + if sticky_header_excerpt_id != Some(excerpt.id) { + let selected = selected_buffer_ids.contains(&excerpt.buffer_id); - result = result.child(div().pr(editor_margins.right).child( - self.render_buffer_header( - excerpt, false, selected, false, jump_data, window, cx, - ), - )); - } else { - result = - result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height())); - } - } else { - result = result.child( - h_flex().relative().child( - div() - .top(line_height / 2.) - .absolute() - .w_full() - .h_px() - .bg(color.border_variant), + result = result.child(div().pr(editor_margins.right).child( + self.render_buffer_header( + excerpt, false, selected, false, jump_data, window, cx, ), - ); - }; + )); + } else { + result = + result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height())); + } result.into_any() } @@ -5708,7 +5713,10 @@ impl EditorElement { let end_row_in_current_excerpt = snapshot .blocks_in_range(start_row..end_row) .find_map(|(start_row, block)| { - if matches!(block, Block::ExcerptBoundary { .. }) { + if matches!( + block, + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. } + ) { Some(start_row) } else { None diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index d388e8f3b79d704f5e023901ce59afd281131da2..960fecf59a8ae80d168f5ab82c74f32dfa7d4745 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -230,26 +230,23 @@ pub fn editor_content_with_blocks(editor: &Entity, cx: &mut VisualTestCo lines[row as usize].push_str("§ -----"); } } - Block::ExcerptBoundary { - excerpt, - height, - starts_new_buffer, - } => { - if starts_new_buffer { - lines[row.0 as usize].push_str(&cx.update(|_, cx| { - format!( - "§ {}", - excerpt - .buffer - .file() - .unwrap() - .file_name(cx) - .to_string_lossy() - ) - })); - } else { - lines[row.0 as usize].push_str("§ -----") + Block::ExcerptBoundary { height, .. } => { + for row in row.0..row.0 + height { + lines[row as usize].push_str("§ -----"); } + } + Block::BufferHeader { excerpt, height } => { + lines[row.0 as usize].push_str(&cx.update(|_, cx| { + format!( + "§ {}", + excerpt + .buffer + .file() + .unwrap() + .file_name(cx) + .to_string_lossy() + ) + })); for row in row.0 + 1..row.0 + height { lines[row as usize].push_str("§ -----"); } From 1dd237139cfb4f12982f1db86c87ab8b85c9593f Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 21 Aug 2025 09:24:34 -0400 Subject: [PATCH 232/744] Fix more improper uses of the `buffer_id` field of `Anchor` (#36636) Follow-up to #36524 Release Notes: - N/A --- crates/editor/src/editor.rs | 71 +++++++++------------- crates/editor/src/hover_links.rs | 5 +- crates/editor/src/inlay_hint_cache.rs | 5 +- crates/editor/src/linked_editing_ranges.rs | 2 +- crates/editor/src/mouse_context_menu.rs | 18 +++--- crates/outline_panel/src/outline_panel.rs | 5 +- 6 files changed, 48 insertions(+), 58 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e32ea1cb3a6a11a406fb34fa47e78699cd1e3595..05ee2953604627bc3501fb1c3b8d5722f87f8477 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6697,7 +6697,6 @@ impl Editor { return; } - let buffer_id = cursor_position.buffer_id; let buffer = this.buffer.read(cx); if buffer .text_anchor_for_position(cursor_position, cx) @@ -6710,8 +6709,8 @@ impl Editor { let mut write_ranges = Vec::new(); let mut read_ranges = Vec::new(); for highlight in highlights { - for (excerpt_id, excerpt_range) in - buffer.excerpts_for_buffer(cursor_buffer.read(cx).remote_id(), cx) + let buffer_id = cursor_buffer.read(cx).remote_id(); + for (excerpt_id, excerpt_range) in buffer.excerpts_for_buffer(buffer_id, cx) { let start = highlight .range @@ -6726,12 +6725,12 @@ impl Editor { } let range = Anchor { - buffer_id, + buffer_id: Some(buffer_id), excerpt_id, text_anchor: start, diff_base_anchor: None, }..Anchor { - buffer_id, + buffer_id: Some(buffer_id), excerpt_id, text_anchor: end, diff_base_anchor: None, @@ -9496,17 +9495,21 @@ impl Editor { selection: Range, cx: &mut Context, ) { - let buffer_id = match (&selection.start.buffer_id, &selection.end.buffer_id) { - (Some(a), Some(b)) if a == b => a, - _ => { - log::error!("expected anchor range to have matching buffer IDs"); - return; - } + let Some((_, buffer, _)) = self + .buffer() + .read(cx) + .excerpt_containing(selection.start, cx) + else { + return; }; - let multi_buffer = self.buffer().read(cx); - let Some(buffer) = multi_buffer.buffer(*buffer_id) else { + let Some((_, end_buffer, _)) = self.buffer().read(cx).excerpt_containing(selection.end, cx) + else { return; }; + if buffer != end_buffer { + log::error!("expected anchor range to have matching buffer IDs"); + return; + } let id = post_inc(&mut self.next_completion_id); let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; @@ -10593,16 +10596,12 @@ impl Editor { snapshot: &EditorSnapshot, cx: &mut Context, ) -> Option<(Anchor, Breakpoint)> { - let project = self.project.clone()?; - - let buffer_id = breakpoint_position.buffer_id.or_else(|| { - snapshot - .buffer_snapshot - .buffer_id_for_excerpt(breakpoint_position.excerpt_id) - })?; + let buffer = self + .buffer + .read(cx) + .buffer_for_anchor(breakpoint_position, cx)?; let enclosing_excerpt = breakpoint_position.excerpt_id; - let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?; let buffer_snapshot = buffer.read(cx).snapshot(); let row = buffer_snapshot @@ -10775,21 +10774,11 @@ impl Editor { return; }; - let Some(buffer_id) = breakpoint_position.buffer_id.or_else(|| { - if breakpoint_position == Anchor::min() { - self.buffer() - .read(cx) - .excerpt_buffer_ids() - .into_iter() - .next() - } else { - None - } - }) else { - return; - }; - - let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else { + let Some(buffer) = self + .buffer + .read(cx) + .buffer_for_anchor(breakpoint_position, cx) + else { return; }; @@ -15432,7 +15421,8 @@ impl Editor { return; }; - let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else { + let next_diagnostic_start = buffer.anchor_after(next_diagnostic.range.start); + let Some(buffer_id) = buffer.buffer_id_for_anchor(next_diagnostic_start) else { return; }; self.change_selections(Default::default(), window, cx, |s| { @@ -20425,11 +20415,8 @@ impl Editor { .range_to_buffer_ranges_with_deleted_hunks(selection.range()) { if let Some(anchor) = anchor { - // selection is in a deleted hunk - let Some(buffer_id) = anchor.buffer_id else { - continue; - }; - let Some(buffer_handle) = multi_buffer.buffer(buffer_id) else { + let Some(buffer_handle) = multi_buffer.buffer_for_anchor(anchor, cx) + else { continue; }; let offset = text::ToOffset::to_offset( diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 1d7d56e67db00e3511c1bf8203f6e757cf2aea6b..94f49f601a101cd8ca2556df9ec1568b5e7337fa 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -321,7 +321,10 @@ pub fn update_inlay_link_and_hover_points( if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { match cached_hint.resolve_state { ResolveState::CanResolve(_, _) => { - if let Some(buffer_id) = previous_valid_anchor.buffer_id { + if let Some(buffer_id) = snapshot + .buffer_snapshot + .buffer_id_for_anchor(previous_valid_anchor) + { inlay_hint_cache.spawn_hint_resolve( buffer_id, excerpt_id, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index cea0e32d7fe633afd3835e80a38426c3603c35bb..dbf5ac95b78433c9a67da110e804a8973e51dee1 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -475,10 +475,7 @@ impl InlayHintCache { let excerpt_cached_hints = excerpt_cached_hints.read(); let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable(); shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { - let Some(buffer) = shown_anchor - .buffer_id - .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) - else { + let Some(buffer) = multi_buffer.buffer_for_anchor(*shown_anchor, cx) else { return false; }; let buffer_snapshot = buffer.read(cx).snapshot(); diff --git a/crates/editor/src/linked_editing_ranges.rs b/crates/editor/src/linked_editing_ranges.rs index aaf9032b04a0b4a6f20482f08917c24951aef4d1..4f1313797f97b1d482effced60b6843541c9e3a7 100644 --- a/crates/editor/src/linked_editing_ranges.rs +++ b/crates/editor/src/linked_editing_ranges.rs @@ -72,7 +72,7 @@ pub(super) fn refresh_linked_ranges( // Throw away selections spanning multiple buffers. continue; } - if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) { + if let Some(buffer) = buffer.buffer_for_anchor(end_position, cx) { applicable_selections.push(( buffer, start_position.text_anchor, diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 5cf22de537b6965085179dea522a4313799fa141..3bc334c54c2f58e6dda2b404039369907c275422 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -190,14 +190,16 @@ pub fn deploy_context_menu( .all::(cx) .into_iter() .any(|s| !s.is_empty()); - let has_git_repo = anchor.buffer_id.is_some_and(|buffer_id| { - project - .read(cx) - .git_store() - .read(cx) - .repository_and_path_for_buffer_id(buffer_id, cx) - .is_some() - }); + let has_git_repo = buffer + .buffer_id_for_anchor(anchor) + .is_some_and(|buffer_id| { + project + .read(cx) + .git_store() + .read(cx) + .repository_and_path_for_buffer_id(buffer_id, cx) + .is_some() + }); let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx); let run_to_cursor = window.is_action_available(&RunToCursor, cx); diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 59c43f945f2509dd8c54c871760d957ceaf4817e..10698cead8656885d0ea2d2f98ebd235e29fec1b 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -4393,12 +4393,13 @@ impl OutlinePanel { }) .filter(|(match_range, _)| { let editor = active_editor.read(cx); - if let Some(buffer_id) = match_range.start.buffer_id + let snapshot = editor.buffer().read(cx).snapshot(cx); + if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.start) && editor.is_buffer_folded(buffer_id, cx) { return false; } - if let Some(buffer_id) = match_range.start.buffer_id + if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.end) && editor.is_buffer_folded(buffer_id, cx) { return false; From e0613cbd0f203a845cc622d04f47d9a54931a160 Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Thu, 21 Aug 2025 15:56:16 +0200 Subject: [PATCH 233/744] Add Rodio audio pipeline as alternative to current LiveKit pipeline (#36607) Rodio parts are well tested and need less configuration then the livekit parts. I suspect there is a bug in the livekit configuration regarding resampling. Rather then investigate that it seemed faster & easier to swap in Rodio. This opens the door to using other Rodio parts like: - Decibel based volume control - Limiter (prevents sound from becoming too loud) - Automatic gain control To use this add to settings: ``` "audio": { "experimental.rodio_audio": true } ``` Release Notes: - N/A Co-authored-by: Mikayla Co-authored-by: Antonio Scandurra --- Cargo.lock | 7 +- crates/audio/Cargo.toml | 5 +- crates/audio/src/assets.rs | 54 ------------- crates/audio/src/audio.rs | 76 ++++++++++++------- crates/audio/src/audio_settings.rs | 33 ++++++++ crates/livekit_client/Cargo.toml | 2 + crates/livekit_client/src/lib.rs | 7 +- crates/livekit_client/src/livekit_client.rs | 14 +++- .../src/livekit_client/playback.rs | 64 +++++++++++----- .../src/livekit_client/playback/source.rs | 67 ++++++++++++++++ crates/settings/src/settings_store.rs | 5 ++ crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 2 +- 13 files changed, 226 insertions(+), 112 deletions(-) delete mode 100644 crates/audio/src/assets.rs create mode 100644 crates/audio/src/audio_settings.rs create mode 100644 crates/livekit_client/src/livekit_client/playback/source.rs diff --git a/Cargo.lock b/Cargo.lock index 76f8672d4d0d126f47e12a14e694d84e6a765f9a..ddeaebd0bf656ec82a698ef66906d314b9da6ef6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1379,10 +1379,11 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "derive_more 0.99.19", "gpui", - "parking_lot", "rodio", + "schemars", + "serde", + "settings", "util", "workspace-hack", ] @@ -9621,6 +9622,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "audio", "collections", "core-foundation 0.10.0", "core-video", @@ -9643,6 +9645,7 @@ dependencies = [ "scap", "serde", "serde_json", + "settings", "sha2", "simplelog", "smallvec", diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 5146396b92266e74aaa771c3c789894b33666874..ae7eb52fd377b315151525e7d501c4a33454476f 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -15,9 +15,10 @@ doctest = false [dependencies] anyhow.workspace = true collections.workspace = true -derive_more.workspace = true gpui.workspace = true -parking_lot.workspace = true +settings.workspace = true +schemars.workspace = true +serde.workspace = true rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] } util.workspace = true workspace-hack.workspace = true diff --git a/crates/audio/src/assets.rs b/crates/audio/src/assets.rs deleted file mode 100644 index fd5c935d875960f4fd9bf30494301f4811b22448..0000000000000000000000000000000000000000 --- a/crates/audio/src/assets.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::{io::Cursor, sync::Arc}; - -use anyhow::{Context as _, Result}; -use collections::HashMap; -use gpui::{App, AssetSource, Global}; -use rodio::{Decoder, Source, source::Buffered}; - -type Sound = Buffered>>>; - -pub struct SoundRegistry { - cache: Arc>>, - assets: Box, -} - -struct GlobalSoundRegistry(Arc); - -impl Global for GlobalSoundRegistry {} - -impl SoundRegistry { - pub fn new(source: impl AssetSource) -> Arc { - Arc::new(Self { - cache: Default::default(), - assets: Box::new(source), - }) - } - - pub fn global(cx: &App) -> Arc { - cx.global::().0.clone() - } - - pub(crate) fn set_global(source: impl AssetSource, cx: &mut App) { - cx.set_global(GlobalSoundRegistry(SoundRegistry::new(source))); - } - - pub fn get(&self, name: &str) -> Result + use<>> { - if let Some(wav) = self.cache.lock().get(name) { - return Ok(wav.clone()); - } - - let path = format!("sounds/{}.wav", name); - let bytes = self - .assets - .load(&path)? - .map(anyhow::Ok) - .with_context(|| format!("No asset available for path {path}"))?? - .into_owned(); - let cursor = Cursor::new(bytes); - let source = Decoder::new(cursor)?.buffered(); - - self.cache.lock().insert(name.to_string(), source.clone()); - - Ok(source) - } -} diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs index 44baa16aa20a3e4b7651744974cfc085dcde7fb1..b4f2c24fef119318b7b499c9b1a8501171f9084a 100644 --- a/crates/audio/src/audio.rs +++ b/crates/audio/src/audio.rs @@ -1,16 +1,19 @@ -use assets::SoundRegistry; -use derive_more::{Deref, DerefMut}; -use gpui::{App, AssetSource, BorrowAppContext, Global}; -use rodio::{OutputStream, OutputStreamBuilder}; +use anyhow::{Context as _, Result, anyhow}; +use collections::HashMap; +use gpui::{App, BorrowAppContext, Global}; +use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered}; +use settings::Settings; +use std::io::Cursor; use util::ResultExt; -mod assets; +mod audio_settings; +pub use audio_settings::AudioSettings; -pub fn init(source: impl AssetSource, cx: &mut App) { - SoundRegistry::set_global(source, cx); - cx.set_global(GlobalAudio(Audio::new())); +pub fn init(cx: &mut App) { + AudioSettings::register(cx); } +#[derive(Copy, Clone, Eq, Hash, PartialEq)] pub enum Sound { Joined, Leave, @@ -38,18 +41,12 @@ impl Sound { #[derive(Default)] pub struct Audio { output_handle: Option, + source_cache: HashMap>>>>, } -#[derive(Deref, DerefMut)] -struct GlobalAudio(Audio); - -impl Global for GlobalAudio {} +impl Global for Audio {} impl Audio { - pub fn new() -> Self { - Self::default() - } - fn ensure_output_exists(&mut self) -> Option<&OutputStream> { if self.output_handle.is_none() { self.output_handle = OutputStreamBuilder::open_default_stream().log_err(); @@ -58,26 +55,51 @@ impl Audio { self.output_handle.as_ref() } - pub fn play_sound(sound: Sound, cx: &mut App) { - if !cx.has_global::() { - return; - } + pub fn play_source( + source: impl rodio::Source + Send + 'static, + cx: &mut App, + ) -> anyhow::Result<()> { + cx.update_default_global(|this: &mut Self, _cx| { + let output_handle = this + .ensure_output_exists() + .ok_or_else(|| anyhow!("Could not open audio output"))?; + output_handle.mixer().add(source); + Ok(()) + }) + } - cx.update_global::(|this, cx| { + pub fn play_sound(sound: Sound, cx: &mut App) { + cx.update_default_global(|this: &mut Self, cx| { + let source = this.sound_source(sound, cx).log_err()?; let output_handle = this.ensure_output_exists()?; - let source = SoundRegistry::global(cx).get(sound.file()).log_err()?; output_handle.mixer().add(source); Some(()) }); } pub fn end_call(cx: &mut App) { - if !cx.has_global::() { - return; - } - - cx.update_global::(|this, _| { + cx.update_default_global(|this: &mut Self, _cx| { this.output_handle.take(); }); } + + fn sound_source(&mut self, sound: Sound, cx: &App) -> Result> { + if let Some(wav) = self.source_cache.get(&sound) { + return Ok(wav.clone()); + } + + let path = format!("sounds/{}.wav", sound.file()); + let bytes = cx + .asset_source() + .load(&path)? + .map(anyhow::Ok) + .with_context(|| format!("No asset available for path {path}"))?? + .into_owned(); + let cursor = Cursor::new(bytes); + let source = Decoder::new(cursor)?.buffered(); + + self.source_cache.insert(sound, source.clone()); + + Ok(source) + } } diff --git a/crates/audio/src/audio_settings.rs b/crates/audio/src/audio_settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..807179881c7c3b27aad2e3142a84c730951eb709 --- /dev/null +++ b/crates/audio/src/audio_settings.rs @@ -0,0 +1,33 @@ +use anyhow::Result; +use gpui::App; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsSources}; + +#[derive(Deserialize, Debug)] +pub struct AudioSettings { + /// Opt into the new audio system. + #[serde(rename = "experimental.rodio_audio", default)] + pub rodio_audio: bool, // default is false +} + +/// Configuration of audio in Zed. +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[serde(default)] +pub struct AudioSettingsContent { + /// Whether to use the experimental audio system + #[serde(rename = "experimental.rodio_audio", default)] + pub rodio_audio: bool, +} + +impl Settings for AudioSettings { + const KEY: Option<&'static str> = Some("audio"); + + type FileContent = AudioSettingsContent; + + fn load(sources: SettingsSources, _cx: &mut App) -> Result { + sources.json_merge() + } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} +} diff --git a/crates/livekit_client/Cargo.toml b/crates/livekit_client/Cargo.toml index 58059967b7ab509fd91209a4a0f9873bbbb6b87d..3575325ac04a06ffc577438c6a323cf411cee859 100644 --- a/crates/livekit_client/Cargo.toml +++ b/crates/livekit_client/Cargo.toml @@ -25,6 +25,7 @@ async-trait.workspace = true collections.workspace = true cpal.workspace = true futures.workspace = true +audio.workspace = true gpui = { workspace = true, features = ["screen-capture", "x11", "wayland", "windows-manifest"] } gpui_tokio.workspace = true http_client_tls.workspace = true @@ -35,6 +36,7 @@ nanoid.workspace = true parking_lot.workspace = true postage.workspace = true smallvec.workspace = true +settings.workspace = true tokio-tungstenite.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/livekit_client/src/lib.rs b/crates/livekit_client/src/lib.rs index e3934410e1e59a110d634585003a97c587f80912..055aa3704e06f25a21c69294343539289d8acb49 100644 --- a/crates/livekit_client/src/lib.rs +++ b/crates/livekit_client/src/lib.rs @@ -24,8 +24,11 @@ mod livekit_client; )))] pub use livekit_client::*; -// If you need proper LSP in livekit_client you've got to comment out -// the mocks and test +// If you need proper LSP in livekit_client you've got to comment +// - the cfg blocks above +// - the mods: mock_client & test and their conditional blocks +// - the pub use mock_client::* and their conditional blocks + #[cfg(any( test, feature = "test-support", diff --git a/crates/livekit_client/src/livekit_client.rs b/crates/livekit_client/src/livekit_client.rs index adeea4f51279ece93160d604672eab3962c7a6d7..0751b014f42b2743efc54431a1bac7762ebb7884 100644 --- a/crates/livekit_client/src/livekit_client.rs +++ b/crates/livekit_client/src/livekit_client.rs @@ -1,15 +1,16 @@ use std::sync::Arc; use anyhow::{Context as _, Result}; +use audio::AudioSettings; use collections::HashMap; use futures::{SinkExt, channel::mpsc}; use gpui::{App, AsyncApp, ScreenCaptureSource, ScreenCaptureStream, Task}; use gpui_tokio::Tokio; +use log::info; use playback::capture_local_video_track; +use settings::Settings; mod playback; -#[cfg(feature = "record-microphone")] -mod record; use crate::{LocalTrack, Participant, RemoteTrack, RoomEvent, TrackPublication}; pub use playback::AudioStream; @@ -125,9 +126,14 @@ impl Room { pub fn play_remote_audio_track( &self, track: &RemoteAudioTrack, - _cx: &App, + cx: &mut App, ) -> Result { - Ok(self.playback.play_remote_audio_track(&track.0)) + if AudioSettings::get_global(cx).rodio_audio { + info!("Using experimental.rodio_audio audio pipeline"); + playback::play_remote_audio_track(&track.0, cx) + } else { + Ok(self.playback.play_remote_audio_track(&track.0)) + } } } diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index e13fb7bd8132916800654416e99316e9cf3be074..d6b64dbacaad018b664eb6d196106a80e83229a1 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -18,13 +18,16 @@ use livekit::webrtc::{ video_stream::native::NativeVideoStream, }; use parking_lot::Mutex; +use rodio::Source; use std::cell::RefCell; use std::sync::Weak; -use std::sync::atomic::{self, AtomicI32}; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::time::Duration; use std::{borrow::Cow, collections::VecDeque, sync::Arc, thread}; use util::{ResultExt as _, maybe}; +mod source; + pub(crate) struct AudioStack { executor: BackgroundExecutor, apm: Arc>, @@ -40,6 +43,29 @@ pub(crate) struct AudioStack { const SAMPLE_RATE: u32 = 48000; const NUM_CHANNELS: u32 = 2; +pub(crate) fn play_remote_audio_track( + track: &livekit::track::RemoteAudioTrack, + cx: &mut gpui::App, +) -> Result { + let stop_handle = Arc::new(AtomicBool::new(false)); + let stop_handle_clone = stop_handle.clone(); + let stream = source::LiveKitStream::new(cx.background_executor(), track) + .stoppable() + .periodic_access(Duration::from_millis(50), move |s| { + if stop_handle.load(Ordering::Relaxed) { + s.stop(); + } + }); + audio::Audio::play_source(stream, cx).context("Could not play audio")?; + + let on_drop = util::defer(move || { + stop_handle_clone.store(true, Ordering::Relaxed); + }); + Ok(AudioStream::Output { + _drop: Box::new(on_drop), + }) +} + impl AudioStack { pub(crate) fn new(executor: BackgroundExecutor) -> Self { let apm = Arc::new(Mutex::new(apm::AudioProcessingModule::new( @@ -61,7 +87,7 @@ impl AudioStack { ) -> AudioStream { let output_task = self.start_output(); - let next_ssrc = self.next_ssrc.fetch_add(1, atomic::Ordering::Relaxed); + let next_ssrc = self.next_ssrc.fetch_add(1, Ordering::Relaxed); let source = AudioMixerSource { ssrc: next_ssrc, sample_rate: SAMPLE_RATE, @@ -97,6 +123,23 @@ impl AudioStack { } } + fn start_output(&self) -> Arc> { + if let Some(task) = self._output_task.borrow().upgrade() { + return task; + } + let task = Arc::new(self.executor.spawn({ + let apm = self.apm.clone(); + let mixer = self.mixer.clone(); + async move { + Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS) + .await + .log_err(); + } + })); + *self._output_task.borrow_mut() = Arc::downgrade(&task); + task + } + pub(crate) fn capture_local_microphone_track( &self, ) -> Result<(crate::LocalAudioTrack, AudioStream)> { @@ -139,23 +182,6 @@ impl AudioStack { )) } - fn start_output(&self) -> Arc> { - if let Some(task) = self._output_task.borrow().upgrade() { - return task; - } - let task = Arc::new(self.executor.spawn({ - let apm = self.apm.clone(); - let mixer = self.mixer.clone(); - async move { - Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS) - .await - .log_err(); - } - })); - *self._output_task.borrow_mut() = Arc::downgrade(&task); - task - } - async fn play_output( apm: Arc>, mixer: Arc>, diff --git a/crates/livekit_client/src/livekit_client/playback/source.rs b/crates/livekit_client/src/livekit_client/playback/source.rs new file mode 100644 index 0000000000000000000000000000000000000000..021640247ddc8da17025dc8bf852003ead468852 --- /dev/null +++ b/crates/livekit_client/src/livekit_client/playback/source.rs @@ -0,0 +1,67 @@ +use futures::StreamExt; +use libwebrtc::{audio_stream::native::NativeAudioStream, prelude::AudioFrame}; +use livekit::track::RemoteAudioTrack; +use rodio::{Source, buffer::SamplesBuffer, conversions::SampleTypeConverter}; + +use crate::livekit_client::playback::{NUM_CHANNELS, SAMPLE_RATE}; + +fn frame_to_samplesbuffer(frame: AudioFrame) -> SamplesBuffer { + let samples = frame.data.iter().copied(); + let samples = SampleTypeConverter::<_, _>::new(samples); + let samples: Vec = samples.collect(); + SamplesBuffer::new(frame.num_channels as u16, frame.sample_rate, samples) +} + +pub struct LiveKitStream { + // shared_buffer: SharedBuffer, + inner: rodio::queue::SourcesQueueOutput, + _receiver_task: gpui::Task<()>, +} + +impl LiveKitStream { + pub fn new(executor: &gpui::BackgroundExecutor, track: &RemoteAudioTrack) -> Self { + let mut stream = + NativeAudioStream::new(track.rtc_track(), SAMPLE_RATE as i32, NUM_CHANNELS as i32); + let (queue_input, queue_output) = rodio::queue::queue(true); + // spawn rtc stream + let receiver_task = executor.spawn({ + async move { + while let Some(frame) = stream.next().await { + let samples = frame_to_samplesbuffer(frame); + queue_input.append(samples); + } + } + }); + + LiveKitStream { + _receiver_task: receiver_task, + inner: queue_output, + } + } +} + +impl Iterator for LiveKitStream { + type Item = rodio::Sample; + + fn next(&mut self) -> Option { + self.inner.next() + } +} + +impl Source for LiveKitStream { + fn current_span_len(&self) -> Option { + self.inner.current_span_len() + } + + fn channels(&self) -> rodio::ChannelCount { + self.inner.channels() + } + + fn sample_rate(&self) -> rodio::SampleRate { + self.inner.sample_rate() + } + + fn total_duration(&self) -> Option { + self.inner.total_duration() + } +} diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 211db46c6c78d2fee8b87a0a91ec1c106cb28426..3deaed8b9d0b9cba46a955409f6013d133a08358 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -60,6 +60,11 @@ pub trait Settings: 'static + Send + Sync { /// The logic for combining together values from one or more JSON files into the /// final value for this setting. + /// + /// # Warning + /// `Self::FileContent` deserialized field names should match with `Self` deserialized field names + /// otherwise the field won't be deserialized properly and you will get the error: + /// "A default setting must be added to the `default.json` file" fn load(sources: SettingsSources, cx: &mut App) -> Result where Self: Sized; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 45c67153eb428c0421416251717d191d44cc3cf2..7ab76b71dee308570ba96d0e2039fcd4c6d16efb 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -598,7 +598,7 @@ pub fn main() { repl::notebook::init(cx); diagnostics::init(cx); - audio::init(Assets, cx); + audio::init(cx); workspace::init(app_state.clone(), cx); ui_prompt::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 958149825ac97a3ca74952404d13b73ec15ea11e..3b5f99f9bda24d03db9791cfec16a97ca3de2345 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4614,7 +4614,7 @@ mod tests { gpui_tokio::init(cx); vim_mode_setting::init(cx); theme::init(theme::LoadThemes::JustBase, cx); - audio::init((), cx); + audio::init(cx); channel::init(&app_state.client, app_state.user_store.clone(), cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); From 2781a3097161c7bd5447fe82a4d9f8490b42af68 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Aug 2025 09:59:18 -0400 Subject: [PATCH 234/744] collab: Add Orb subscription status and period to `billing_subscriptions` table (#36682) This PR adds the following new columns to the `billing_subscriptions` table: - `orb_subscription_status` - `orb_current_billing_period_start_date` - `orb_current_billing_period_end_date` Release Notes: - N/A --- ...ubscription_status_and_period_to_billing_subscriptions.sql | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql diff --git a/crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql b/crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql new file mode 100644 index 0000000000000000000000000000000000000000..89a42ab82bd97f487a426ef1fa0a08aa5b0c8396 --- /dev/null +++ b/crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql @@ -0,0 +1,4 @@ +alter table billing_subscriptions + add column orb_subscription_status text, + add column orb_current_billing_period_start_date timestamp without time zone, + add column orb_current_billing_period_end_date timestamp without time zone; From 001ec97c0e0c13deda49c49ded89826fab514a7c Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Thu, 21 Aug 2025 16:18:22 +0200 Subject: [PATCH 235/744] acp: Use file icons for edit tool cards when ToolCallLocation is known (#36684) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 33 ++++++++++++++++---------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 9c9e2ee4ddf20b5894a7538bfabb596b24c1c931..f8c616c9e039ae78fc75d46f4f1f0e8687b185f2 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1469,19 +1469,26 @@ impl AcpThreadView { tool_call: &ToolCall, cx: &Context, ) -> Div { - let tool_icon = Icon::new(match tool_call.kind { - acp::ToolKind::Read => IconName::ToolRead, - acp::ToolKind::Edit => IconName::ToolPencil, - acp::ToolKind::Delete => IconName::ToolDeleteFile, - acp::ToolKind::Move => IconName::ArrowRightLeft, - acp::ToolKind::Search => IconName::ToolSearch, - acp::ToolKind::Execute => IconName::ToolTerminal, - acp::ToolKind::Think => IconName::ToolThink, - acp::ToolKind::Fetch => IconName::ToolWeb, - acp::ToolKind::Other => IconName::ToolHammer, - }) - .size(IconSize::Small) - .color(Color::Muted); + let tool_icon = + if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 { + FileIcons::get_icon(&tool_call.locations[0].path, cx) + .map(Icon::from_path) + .unwrap_or(Icon::new(IconName::ToolPencil)) + } else { + Icon::new(match tool_call.kind { + acp::ToolKind::Read => IconName::ToolRead, + acp::ToolKind::Edit => IconName::ToolPencil, + acp::ToolKind::Delete => IconName::ToolDeleteFile, + acp::ToolKind::Move => IconName::ArrowRightLeft, + acp::ToolKind::Search => IconName::ToolSearch, + acp::ToolKind::Execute => IconName::ToolTerminal, + acp::ToolKind::Think => IconName::ToolThink, + acp::ToolKind::Fetch => IconName::ToolWeb, + acp::ToolKind::Other => IconName::ToolHammer, + }) + } + .size(IconSize::Small) + .color(Color::Muted); let base_container = h_flex().size_4().justify_center(); From d8fc779a6758f6cf3b375af65350e7166e22b0b8 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 21 Aug 2025 08:43:57 -0600 Subject: [PATCH 236/744] acp: Hide history unless in native agent (#36644) Release Notes: - N/A --- crates/agent2/src/history_store.rs | 10 ++++++++++ crates/agent_ui/src/acp/thread_history.rs | 17 ++++------------- crates/agent_ui/src/acp/thread_view.rs | 21 +++++++++++++-------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 2d70164a668dd404cc56b4f4a01db376f520fb19..78d83cc1d05a72d1499c697751d1dfa792143db3 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -10,6 +10,7 @@ use itertools::Itertools; use paths::contexts_dir; use serde::{Deserialize, Serialize}; use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration}; +use ui::ElementId; use util::ResultExt as _; const MAX_RECENTLY_OPENED_ENTRIES: usize = 6; @@ -68,6 +69,15 @@ pub enum HistoryEntryId { TextThread(Arc), } +impl Into for HistoryEntryId { + fn into(self) -> ElementId { + match self { + HistoryEntryId::AcpThread(session_id) => ElementId::Name(session_id.0.into()), + HistoryEntryId::TextThread(path) => ElementId::Path(path), + } + } +} + #[derive(Serialize, Deserialize, Debug)] enum SerializedRecentOpen { AcpThread(String), diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index 68a41f31d0dfdc2bc2b47aecc3fa29bc94015007..d76969378ce2dd30e891618079d7ce60664e2617 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -673,18 +673,9 @@ impl AcpHistoryEntryElement { impl RenderOnce for AcpHistoryEntryElement { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let (id, title, timestamp) = match &self.entry { - HistoryEntry::AcpThread(thread) => ( - thread.id.to_string(), - thread.title.clone(), - thread.updated_at, - ), - HistoryEntry::TextThread(context) => ( - context.path.to_string_lossy().to_string(), - context.title.clone(), - context.mtime.to_utc(), - ), - }; + let id = self.entry.id(); + let title = self.entry.title(); + let timestamp = self.entry.updated_at(); let formatted_time = { let now = chrono::Utc::now(); @@ -701,7 +692,7 @@ impl RenderOnce for AcpHistoryEntryElement { } }; - ListItem::new(SharedString::from(id)) + ListItem::new(id) .rounded() .toggle_state(self.selected) .spacing(ListItemSpacing::Sparse) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f8c616c9e039ae78fc75d46f4f1f0e8687b185f2..090e224b4d11327b5ea8a0cdfb6fcfd1dde31f74 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2404,16 +2404,18 @@ impl AcpThreadView { fn render_empty_state(&self, window: &mut Window, cx: &mut Context) -> AnyElement { let loading = matches!(&self.thread_state, ThreadState::Loading { .. }); - let recent_history = self - .history_store - .update(cx, |history_store, cx| history_store.recent_entries(3, cx)); - let no_history = self - .history_store - .update(cx, |history_store, cx| history_store.is_empty(cx)); + let render_history = self + .agent + .clone() + .downcast::() + .is_some() + && self + .history_store + .update(cx, |history_store, cx| !history_store.is_empty(cx)); v_flex() .size_full() - .when(no_history, |this| { + .when(!render_history, |this| { this.child( v_flex() .size_full() @@ -2445,7 +2447,10 @@ impl AcpThreadView { })), ) }) - .when(!no_history, |this| { + .when(render_history, |this| { + let recent_history = self + .history_store + .update(cx, |history_store, cx| history_store.recent_entries(3, cx)); this.justify_end().child( v_flex() .child( From d9ea97ee9cf1bec19741c597e482aa35eef2f816 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 21 Aug 2025 08:44:04 -0600 Subject: [PATCH 237/744] acp: Detect gemini auth errors and show a button (#36641) Closes #ISSUE Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 63 ++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 090e224b4d11327b5ea8a0cdfb6fcfd1dde31f74..7e330b7e6f0f894542e7b8f1d52841b093b8715e 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -76,11 +76,12 @@ enum ThreadError { PaymentRequired, ModelRequestLimitReached(cloud_llm_client::Plan), ToolUseLimitReached, + AuthenticationRequired(SharedString), Other(SharedString), } impl ThreadError { - fn from_err(error: anyhow::Error) -> Self { + fn from_err(error: anyhow::Error, agent: &Rc) -> Self { if error.is::() { Self::PaymentRequired } else if error.is::() { @@ -90,7 +91,17 @@ impl ThreadError { { Self::ModelRequestLimitReached(error.plan) } else { - Self::Other(error.to_string().into()) + let string = error.to_string(); + // TODO: we should have Gemini return better errors here. + if agent.clone().downcast::().is_some() + && string.contains("Could not load the default credentials") + || string.contains("API key not valid") + || string.contains("Request had invalid authentication credentials") + { + Self::AuthenticationRequired(string.into()) + } else { + Self::Other(error.to_string().into()) + } } } } @@ -930,7 +941,7 @@ impl AcpThreadView { } fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context) { - self.thread_error = Some(ThreadError::from_err(error)); + self.thread_error = Some(ThreadError::from_err(error, &self.agent)); cx.notify(); } @@ -4310,6 +4321,9 @@ impl AcpThreadView { fn render_thread_error(&self, window: &mut Window, cx: &mut Context) -> Option
{ let content = match self.thread_error.as_ref()? { ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx), + ThreadError::AuthenticationRequired(error) => { + self.render_authentication_required_error(error.clone(), cx) + } ThreadError::PaymentRequired => self.render_payment_required_error(cx), ThreadError::ModelRequestLimitReached(plan) => { self.render_model_request_limit_reached_error(*plan, cx) @@ -4348,6 +4362,24 @@ impl AcpThreadView { .dismiss_action(self.dismiss_error_button(cx)) } + fn render_authentication_required_error( + &self, + error: SharedString, + cx: &mut Context, + ) -> Callout { + Callout::new() + .severity(Severity::Error) + .title("Authentication Required") + .description(error.clone()) + .actions_slot( + h_flex() + .gap_0p5() + .child(self.authenticate_button(cx)) + .child(self.create_copy_button(error)), + ) + .dismiss_action(self.dismiss_error_button(cx)) + } + fn render_model_request_limit_reached_error( &self, plan: cloud_llm_client::Plan, @@ -4469,6 +4501,31 @@ impl AcpThreadView { })) } + fn authenticate_button(&self, cx: &mut Context) -> impl IntoElement { + Button::new("authenticate", "Authenticate") + .label_size(LabelSize::Small) + .style(ButtonStyle::Filled) + .on_click(cx.listener({ + move |this, _, window, cx| { + let agent = this.agent.clone(); + let ThreadState::Ready { thread, .. } = &this.thread_state else { + return; + }; + + let connection = thread.read(cx).connection().clone(); + let err = AuthRequired { + description: None, + provider_id: None, + }; + this.clear_thread_error(cx); + let this = cx.weak_entity(); + window.defer(cx, |window, cx| { + Self::handle_auth_required(this, err, agent, connection, window, cx); + }) + } + })) + } + fn upgrade_button(&self, cx: &mut Context) -> impl IntoElement { Button::new("upgrade", "Upgrade") .label_size(LabelSize::Small) From 697a39c2511469e49e8af1974618d552410b1c38 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Thu, 21 Aug 2025 20:19:17 +0530 Subject: [PATCH 238/744] Fix issue where renaming a file would not update imports in related files if they are not open (#36681) Closes #34445 Now we open a multi-buffer consisting of buffers that have updated, renamed file imports. Only local is handled, for now. Release Notes: - Fixed an issue where renaming a file would not update imports in related files if they are not already open. --- crates/editor/src/editor.rs | 58 ++++++++++++++++++++++++++++-- crates/project/src/buffer_store.rs | 11 +++++- crates/project/src/lsp_store.rs | 18 ++++++---- crates/project/src/project.rs | 11 ++++-- 4 files changed, 87 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 05ee2953604627bc3501fb1c3b8d5722f87f8477..2af8e6c0e4d235bd136d1f24a66645af2a6258cd 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1900,6 +1900,60 @@ impl Editor { editor.update_lsp_data(false, Some(*buffer_id), window, cx); } } + + project::Event::EntryRenamed(transaction) => { + let Some(workspace) = editor.workspace() else { + return; + }; + let Some(active_editor) = workspace.read(cx).active_item_as::(cx) + else { + return; + }; + if active_editor.entity_id() == cx.entity_id() { + let edited_buffers_already_open = { + let other_editors: Vec> = workspace + .read(cx) + .panes() + .iter() + .flat_map(|pane| pane.read(cx).items_of_type::()) + .filter(|editor| editor.entity_id() != cx.entity_id()) + .collect(); + + transaction.0.keys().all(|buffer| { + other_editors.iter().any(|editor| { + let multi_buffer = editor.read(cx).buffer(); + multi_buffer.read(cx).is_singleton() + && multi_buffer.read(cx).as_singleton().map_or( + false, + |singleton| { + singleton.entity_id() == buffer.entity_id() + }, + ) + }) + }) + }; + + if !edited_buffers_already_open { + let workspace = workspace.downgrade(); + let transaction = transaction.clone(); + cx.defer_in(window, move |_, window, cx| { + cx.spawn_in(window, async move |editor, cx| { + Self::open_project_transaction( + &editor, + workspace, + transaction, + "Rename".to_string(), + cx, + ) + .await + .ok() + }) + .detach(); + }); + } + } + } + _ => {} }, )); @@ -6282,7 +6336,7 @@ impl Editor { } pub async fn open_project_transaction( - this: &WeakEntity, + editor: &WeakEntity, workspace: WeakEntity, transaction: ProjectTransaction, title: String, @@ -6300,7 +6354,7 @@ impl Editor { if let Some((buffer, transaction)) = entries.first() { if entries.len() == 1 { - let excerpt = this.update(cx, |editor, cx| { + let excerpt = editor.update(cx, |editor, cx| { editor .buffer() .read(cx) diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index a171b193d0a70c438a3aa269955aed16202626ed..295bad6e596252cbbeecb36b587b696ccbab32a0 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -88,9 +88,18 @@ pub enum BufferStoreEvent { }, } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub struct ProjectTransaction(pub HashMap, language::Transaction>); +impl PartialEq for ProjectTransaction { + fn eq(&self, other: &Self) -> bool { + self.0.len() == other.0.len() + && self.0.iter().all(|(buffer, transaction)| { + other.0.get(buffer).is_some_and(|t| t.id == transaction.id) + }) + } +} + impl EventEmitter for BufferStore {} impl RemoteBufferStore { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 072f4396c1cd640cfed0484b410b3ad4991539f8..709bd10358261c79ad4c54a2526495ab899e9b29 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -8762,7 +8762,7 @@ impl LspStore { (root_path.join(&old_path), root_path.join(&new_path)) }; - Self::will_rename_entry( + let _transaction = Self::will_rename_entry( this.downgrade(), worktree_id, &old_abs_path, @@ -9224,7 +9224,7 @@ impl LspStore { new_path: &Path, is_dir: bool, cx: AsyncApp, - ) -> Task<()> { + ) -> Task { let old_uri = lsp::Url::from_file_path(old_path).ok().map(String::from); let new_uri = lsp::Url::from_file_path(new_path).ok().map(String::from); cx.spawn(async move |cx| { @@ -9257,7 +9257,7 @@ impl LspStore { .log_err() .flatten()?; - LocalLspStore::deserialize_workspace_edit( + let transaction = LocalLspStore::deserialize_workspace_edit( this.upgrade()?, edit, false, @@ -9265,8 +9265,8 @@ impl LspStore { cx, ) .await - .ok(); - Some(()) + .ok()?; + Some(transaction) } }); tasks.push(apply_edit); @@ -9276,11 +9276,17 @@ impl LspStore { }) .ok() .flatten(); + let mut merged_transaction = ProjectTransaction::default(); for task in tasks { // Await on tasks sequentially so that the order of application of edits is deterministic // (at least with regards to the order of registration of language servers) - task.await; + if let Some(transaction) = task.await { + for (buffer, buffer_transaction) in transaction.0 { + merged_transaction.0.insert(buffer, buffer_transaction); + } + } } + merged_transaction }) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ee4bfcb8ccf18417e18c6f4f408a892f5fe816a9..9fd4eed641a17bf317c7d69ef0afa0802c8ae276 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -327,6 +327,7 @@ pub enum Event { RevealInProjectPanel(ProjectEntryId), SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>), ExpandedAllForEntry(WorktreeId, ProjectEntryId), + EntryRenamed(ProjectTransaction), AgentLocationChanged, } @@ -2119,7 +2120,7 @@ impl Project { let is_root_entry = self.entry_is_worktree_root(entry_id, cx); let lsp_store = self.lsp_store().downgrade(); - cx.spawn(async move |_, cx| { + cx.spawn(async move |project, cx| { let (old_abs_path, new_abs_path) = { let root_path = worktree.read_with(cx, |this, _| this.abs_path())?; let new_abs_path = if is_root_entry { @@ -2129,7 +2130,7 @@ impl Project { }; (root_path.join(&old_path), new_abs_path) }; - LspStore::will_rename_entry( + let transaction = LspStore::will_rename_entry( lsp_store.clone(), worktree_id, &old_abs_path, @@ -2145,6 +2146,12 @@ impl Project { })? .await?; + project + .update(cx, |_, cx| { + cx.emit(Event::EntryRenamed(transaction)); + }) + .ok(); + lsp_store .read_with(cx, |this, _| { this.did_rename_entry(worktree_id, &old_abs_path, &new_abs_path, is_dir); From f23314bef4514f3208c712d8604278262b310e37 Mon Sep 17 00:00:00 2001 From: Ryan Drew Date: Thu, 21 Aug 2025 08:55:43 -0600 Subject: [PATCH 239/744] editor: Use editorconfig's max_line_length for hard wrap (#36426) PR #20198, "Do not alter soft wrap based on .editorconfig contents" removed support for setting line lengths for both soft and hard wrap, not just soft wrap. This causes the `max_line_length` property within a `.editorconfig` file to be ignored by Zed. This commit restores allowing for hard wrap limits to be set using `max_line_length` without impacting soft wrap limits. This is done by merging the `max_line_length` property from an editorconfig file into Zed's `preferred_line_length` property. Release Notes: - Added support for .editorconfig's `max_line_length` property Signed-off-by: Ryan Drew --- crates/language/src/language_settings.rs | 7 ++++++- crates/project/src/project_tests.rs | 11 ++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 90a59ce06600c9ae5961e8e398770d9820586cc4..386ad19747bfb5066dbf27f07214dcdff4829809 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -5,7 +5,7 @@ use anyhow::Result; use collections::{FxHashMap, HashMap, HashSet}; use ec4rs::{ Properties as EditorconfigProperties, - property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs}, + property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs}, }; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::{App, Modifiers}; @@ -1131,6 +1131,10 @@ impl AllLanguageSettings { } fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) { + let preferred_line_length = cfg.get::().ok().and_then(|v| match v { + MaxLineLen::Value(u) => Some(u as u32), + MaxLineLen::Off => None, + }); let tab_size = cfg.get::().ok().and_then(|v| match v { IndentSize::Value(u) => NonZeroU32::new(u as u32), IndentSize::UseTabWidth => cfg.get::().ok().and_then(|w| match w { @@ -1158,6 +1162,7 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr *target = value; } } + merge(&mut settings.preferred_line_length, preferred_line_length); merge(&mut settings.tab_size, tab_size); merge(&mut settings.hard_tabs, hard_tabs); merge( diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 282f1facc2a9110bd27d249139f4cb4ac644c9c8..7bb1537be80bf885f1fc01f11d1e2e8529904935 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -140,8 +140,10 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true + max_line_length = 120 [*.js] tab_width = 10 + max_line_length = off "#, ".zed": { "settings.json": r#"{ @@ -149,7 +151,8 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { "hard_tabs": false, "ensure_final_newline_on_save": false, "remove_trailing_whitespace_on_save": false, - "soft_wrap": "editor_width" + "preferred_line_length": 64, + "soft_wrap": "editor_width", }"#, }, "a.rs": "fn a() {\n A\n}", @@ -157,6 +160,7 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { ".editorconfig": r#" [*.rs] indent_size = 2 + max_line_length = off, "#, "b.rs": "fn b() {\n B\n}", }, @@ -205,6 +209,7 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { assert_eq!(settings_a.hard_tabs, true); assert_eq!(settings_a.ensure_final_newline_on_save, true); assert_eq!(settings_a.remove_trailing_whitespace_on_save, true); + assert_eq!(settings_a.preferred_line_length, 120); // .editorconfig in b/ overrides .editorconfig in root assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2)); @@ -212,6 +217,10 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { // "indent_size" is not set, so "tab_width" is used assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10)); + // When max_line_length is "off", default to .zed/settings.json + assert_eq!(settings_b.preferred_line_length, 64); + assert_eq!(settings_c.preferred_line_length, 64); + // README.md should not be affected by .editorconfig's globe "*.rs" assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8)); }); From 4bee06e507516d4a72501cb4fc2b9d30612f21d4 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 21 Aug 2025 11:57:46 -0300 Subject: [PATCH 240/744] acp: Use `ResourceLink` for agents that don't support embedded context (#36687) The completion provider was already limiting the mention kinds according to `acp::PromptCapabilities`. However, it was still using `ContentBlock::EmbeddedResource` when `acp::PromptCapabilities::embedded_context` was `false`. We will now use `ResourceLink` in that case making it more complaint with the specification. Release Notes: - N/A --- crates/agent_ui/src/acp/message_editor.rs | 91 ++++++++++++++++++++--- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 3116a40be55e859bcdc82866023e392566e22102..dc31c5fe106d4bb16d5d710710b1db39a1a62631 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -709,9 +709,13 @@ impl MessageEditor { window: &mut Window, cx: &mut Context, ) -> Task, Vec>)>> { - let contents = - self.mention_set - .contents(&self.project, self.prompt_store.as_ref(), window, cx); + let contents = self.mention_set.contents( + &self.project, + self.prompt_store.as_ref(), + &self.prompt_capabilities.get(), + window, + cx, + ); let editor = self.editor.clone(); let prevent_slash_commands = self.prevent_slash_commands; @@ -776,6 +780,17 @@ impl MessageEditor { .map(|path| format!("file://{}", path.display())), }) } + Mention::UriOnly(uri) => { + acp::ContentBlock::ResourceLink(acp::ResourceLink { + name: uri.name(), + uri: uri.to_uri().to_string(), + annotations: None, + description: None, + mime_type: None, + size: None, + title: None, + }) + } }; chunks.push(chunk); ix = crease_range.end; @@ -1418,6 +1433,7 @@ pub enum Mention { tracked_buffers: Vec>, }, Image(MentionImage), + UriOnly(MentionUri), } #[derive(Clone, Debug, Eq, PartialEq)] @@ -1481,9 +1497,20 @@ impl MentionSet { &self, project: &Entity, prompt_store: Option<&Entity>, + prompt_capabilities: &acp::PromptCapabilities, _window: &mut Window, cx: &mut App, ) -> Task>> { + if !prompt_capabilities.embedded_context { + let mentions = self + .uri_by_crease_id + .iter() + .map(|(crease_id, uri)| (*crease_id, Mention::UriOnly(uri.clone()))) + .collect(); + + return Task::ready(Ok(mentions)); + } + let mut processed_image_creases = HashSet::default(); let mut contents = self @@ -2180,11 +2207,21 @@ mod tests { assert_eq!(fold_ranges(editor, cx).len(), 1); }); + let all_prompt_capabilities = acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }; + let contents = message_editor .update_in(&mut cx, |message_editor, window, cx| { - message_editor - .mention_set() - .contents(&project, None, window, cx) + message_editor.mention_set().contents( + &project, + None, + &all_prompt_capabilities, + window, + cx, + ) }) .await .unwrap() @@ -2199,6 +2236,28 @@ mod tests { pretty_assertions::assert_eq!(uri, &url_one.parse::().unwrap()); } + let contents = message_editor + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + &project, + None, + &acp::PromptCapabilities::default(), + window, + cx, + ) + }) + .await + .unwrap() + .into_values() + .collect::>(); + + { + let [Mention::UriOnly(uri)] = contents.as_slice() else { + panic!("Unexpected mentions"); + }; + pretty_assertions::assert_eq!(uri, &url_one.parse::().unwrap()); + } + cx.simulate_input(" "); editor.update(&mut cx, |editor, cx| { @@ -2234,9 +2293,13 @@ mod tests { let contents = message_editor .update_in(&mut cx, |message_editor, window, cx| { - message_editor - .mention_set() - .contents(&project, None, window, cx) + message_editor.mention_set().contents( + &project, + None, + &all_prompt_capabilities, + window, + cx, + ) }) .await .unwrap() @@ -2344,9 +2407,13 @@ mod tests { let contents = message_editor .update_in(&mut cx, |message_editor, window, cx| { - message_editor - .mention_set() - .contents(&project, None, window, cx) + message_editor.mention_set().contents( + &project, + None, + &all_prompt_capabilities, + window, + cx, + ) }) .await .unwrap() From 132daef9f669c1ffc27ff7344649b27090ea2163 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:52:17 +0200 Subject: [PATCH 241/744] lsp: Add basic test for server tree toolchain use (#36692) Closes #ISSUE Release Notes: - N/A --- crates/language/src/toolchain.rs | 2 +- crates/project/src/lsp_store.rs | 2 - .../project/src/manifest_tree/server_tree.rs | 2 + crates/project/src/project_tests.rs | 260 +++++++++++++++++- 4 files changed, 262 insertions(+), 4 deletions(-) diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 979513bc96f9660772517a728077614fbd7f5e7a..73c142c8ca02c986b0602c1f19a8c479c041f6f7 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -96,7 +96,7 @@ impl LanguageToolchainStore for T { } type DefaultIndex = usize; -#[derive(Default, Clone)] +#[derive(Default, Clone, Debug)] pub struct ToolchainList { pub toolchains: Vec, pub default: Option, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 709bd10358261c79ad4c54a2526495ab899e9b29..cc3a0a05bbd31c6524b5d63e220e727b13c2f179 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -4643,7 +4643,6 @@ impl LspStore { Some((file, language, raw_buffer.remote_id())) }) .sorted_by_key(|(file, _, _)| Reverse(file.worktree.read(cx).is_visible())); - for (file, language, buffer_id) in buffers { let worktree_id = file.worktree_id(cx); let Some(worktree) = local @@ -4685,7 +4684,6 @@ impl LspStore { cx, ) .collect::>(); - for node in nodes { let server_id = node.server_id_or_init(|disposition| { let path = &disposition.path; diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index 5e5f4bab49c118078c9dc3ba6af808ec91187c7c..48e2007d47f1ebd3c950f0d03e80dcccad515389 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -181,6 +181,7 @@ impl LanguageServerTree { &root_path.path, language_name.clone(), ); + ( Arc::new(InnerTreeNode::new( adapter.name(), @@ -408,6 +409,7 @@ impl ServerTreeRebase { if live_node.id.get().is_some() { return Some(node); } + let disposition = &live_node.disposition; let Some((existing_node, _)) = self .old_contents diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 7bb1537be80bf885f1fc01f11d1e2e8529904935..6dcd07482e6e3b6ef858f61a04ce312304925dd7 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -4,6 +4,7 @@ use crate::{ Event, git_store::StatusEntry, task_inventory::TaskContexts, task_store::TaskSettingsLocation, *, }; +use async_trait::async_trait; use buffer_diff::{ BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, assert_hunks, @@ -21,7 +22,8 @@ use http_client::Url; use itertools::Itertools; use language::{ Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter, - LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, + LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider, + ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainLister, language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings}, tree_sitter_rust, tree_sitter_typescript, }; @@ -596,6 +598,203 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_running_multiple_instances_of_a_single_server_in_one_worktree( + cx: &mut gpui::TestAppContext, +) { + pub(crate) struct PyprojectTomlManifestProvider; + + impl ManifestProvider for PyprojectTomlManifestProvider { + fn name(&self) -> ManifestName { + SharedString::new_static("pyproject.toml").into() + } + + fn search( + &self, + ManifestQuery { + path, + depth, + delegate, + }: ManifestQuery, + ) -> Option> { + for path in path.ancestors().take(depth) { + let p = path.join("pyproject.toml"); + if delegate.exists(&p, Some(false)) { + return Some(path.into()); + } + } + + None + } + } + + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + fs.insert_tree( + path!("/the-root"), + json!({ + ".zed": { + "settings.json": r#" + { + "languages": { + "Python": { + "language_servers": ["ty"] + } + } + }"# + }, + "project-a": { + ".venv": {}, + "file.py": "", + "pyproject.toml": "" + }, + "project-b": { + ".venv": {}, + "source_file.py":"", + "another_file.py": "", + "pyproject.toml": "" + } + }), + ) + .await; + cx.update(|cx| { + ManifestProvidersStore::global(cx).register(Arc::new(PyprojectTomlManifestProvider)) + }); + + let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + let _fake_python_server = language_registry.register_fake_lsp( + "Python", + FakeLspAdapter { + name: "ty", + capabilities: lsp::ServerCapabilities { + ..Default::default() + }, + ..Default::default() + }, + ); + + language_registry.add(python_lang(fs.clone())); + let (first_buffer, _handle) = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/the-root/project-a/file.py"), cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + let servers = project.update(cx, |project, cx| { + project.lsp_store.update(cx, |this, cx| { + first_buffer.update(cx, |buffer, cx| { + this.language_servers_for_local_buffer(buffer, cx) + .map(|(adapter, server)| (adapter.clone(), server.clone())) + .collect::>() + }) + }) + }); + cx.executor().run_until_parked(); + assert_eq!(servers.len(), 1); + let (adapter, server) = servers.into_iter().next().unwrap(); + assert_eq!(adapter.name(), LanguageServerName::new_static("ty")); + assert_eq!(server.server_id(), LanguageServerId(0)); + // `workspace_folders` are set to the rooting point. + assert_eq!( + server.workspace_folders(), + BTreeSet::from_iter( + [Url::from_file_path(path!("/the-root/project-a")).unwrap()].into_iter() + ) + ); + + let (second_project_buffer, _other_handle) = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/the-root/project-b/source_file.py"), cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + let servers = project.update(cx, |project, cx| { + project.lsp_store.update(cx, |this, cx| { + second_project_buffer.update(cx, |buffer, cx| { + this.language_servers_for_local_buffer(buffer, cx) + .map(|(adapter, server)| (adapter.clone(), server.clone())) + .collect::>() + }) + }) + }); + cx.executor().run_until_parked(); + assert_eq!(servers.len(), 1); + let (adapter, server) = servers.into_iter().next().unwrap(); + assert_eq!(adapter.name(), LanguageServerName::new_static("ty")); + // We're not using venvs at all here, so both folders should fall under the same root. + assert_eq!(server.server_id(), LanguageServerId(0)); + // Now, let's select a different toolchain for one of subprojects. + let (available_toolchains_for_b, root_path) = project + .update(cx, |this, cx| { + let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id(); + this.available_toolchains( + ProjectPath { + worktree_id, + path: Arc::from("project-b/source_file.py".as_ref()), + }, + LanguageName::new("Python"), + cx, + ) + }) + .await + .expect("A toolchain to be discovered"); + assert_eq!(root_path.as_ref(), Path::new("project-b")); + assert_eq!(available_toolchains_for_b.toolchains().len(), 1); + let currently_active_toolchain = project + .update(cx, |this, cx| { + let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id(); + this.active_toolchain( + ProjectPath { + worktree_id, + path: Arc::from("project-b/source_file.py".as_ref()), + }, + LanguageName::new("Python"), + cx, + ) + }) + .await; + + assert!(currently_active_toolchain.is_none()); + let _ = project + .update(cx, |this, cx| { + let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id(); + this.activate_toolchain( + ProjectPath { + worktree_id, + path: root_path, + }, + available_toolchains_for_b + .toolchains + .into_iter() + .next() + .unwrap(), + cx, + ) + }) + .await + .unwrap(); + cx.run_until_parked(); + let servers = project.update(cx, |project, cx| { + project.lsp_store.update(cx, |this, cx| { + second_project_buffer.update(cx, |buffer, cx| { + this.language_servers_for_local_buffer(buffer, cx) + .map(|(adapter, server)| (adapter.clone(), server.clone())) + .collect::>() + }) + }) + }); + cx.executor().run_until_parked(); + assert_eq!(servers.len(), 1); + let (adapter, server) = servers.into_iter().next().unwrap(); + assert_eq!(adapter.name(), LanguageServerName::new_static("ty")); + // There's a new language server in town. + assert_eq!(server.server_id(), LanguageServerId(1)); +} + #[gpui::test] async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -8982,6 +9181,65 @@ fn rust_lang() -> Arc { )) } +fn python_lang(fs: Arc) -> Arc { + struct PythonMootToolchainLister(Arc); + #[async_trait] + impl ToolchainLister for PythonMootToolchainLister { + async fn list( + &self, + worktree_root: PathBuf, + subroot_relative_path: Option>, + _: Option>, + ) -> ToolchainList { + // This lister will always return a path .venv directories within ancestors + let ancestors = subroot_relative_path + .into_iter() + .flat_map(|path| path.ancestors().map(ToOwned::to_owned).collect::>()); + let mut toolchains = vec![]; + for ancestor in ancestors { + let venv_path = worktree_root.join(ancestor).join(".venv"); + if self.0.is_dir(&venv_path).await { + toolchains.push(Toolchain { + name: SharedString::new("Python Venv"), + path: venv_path.to_string_lossy().into_owned().into(), + language_name: LanguageName(SharedString::new_static("Python")), + as_json: serde_json::Value::Null, + }) + } + } + ToolchainList { + toolchains, + ..Default::default() + } + } + // Returns a term which we should use in UI to refer to a toolchain. + fn term(&self) -> SharedString { + SharedString::new_static("virtual environment") + } + /// Returns the name of the manifest file for this toolchain. + fn manifest_name(&self) -> ManifestName { + SharedString::new_static("pyproject.toml").into() + } + } + Arc::new( + Language::new( + LanguageConfig { + name: "Python".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["py".to_string()], + ..Default::default() + }, + ..Default::default() + }, + None, // We're not testing Python parsing with this language. + ) + .with_manifest(Some(ManifestName::from(SharedString::new_static( + "pyproject.toml", + )))) + .with_toolchain_lister(Some(Arc::new(PythonMootToolchainLister(fs)))), + ) +} + fn typescript_lang() -> Arc { Arc::new(Language::new( LanguageConfig { From 190217a43bfc2384ec3cd86d82d0ffd3975b0901 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Aug 2025 18:11:05 +0200 Subject: [PATCH 242/744] acp: Refactor agent2 `send` to have a clearer control flow (#36689) Release Notes: - N/A --- Cargo.lock | 1 + crates/agent2/Cargo.toml | 1 + crates/agent2/src/thread.rs | 295 ++++++++++++++++-------------------- 3 files changed, 134 insertions(+), 163 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ddeaebd0bf656ec82a698ef66906d314b9da6ef6..6063530e9f71284b6a3ecf4394ba088bdfbed8d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,6 +244,7 @@ dependencies = [ "terminal", "text", "theme", + "thiserror 2.0.12", "tree-sitter-rust", "ui", "unindent", diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 8dd79062f89eee6eb6525f8a699612b5ffbf248a..68246a96b0288cb9091a4073a33712c0b69df67d 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -61,6 +61,7 @@ sqlez.workspace = true task.workspace = true telemetry.workspace = true terminal.workspace = true +thiserror.workspace = true text.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index d34c92915228753f6edfe5d90a81428e27f5a562..6f560cd390cae1022d0de37c67c96d76b54da6ae 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -499,6 +499,16 @@ pub struct ToolCallAuthorization { pub response: oneshot::Sender, } +#[derive(Debug, thiserror::Error)] +enum CompletionError { + #[error("max tokens")] + MaxTokens, + #[error("refusal")] + Refusal, + #[error(transparent)] + Other(#[from] anyhow::Error), +} + pub struct Thread { id: acp::SessionId, prompt_id: PromptId, @@ -1077,101 +1087,62 @@ impl Thread { _task: cx.spawn(async move |this, cx| { log::info!("Starting agent turn execution"); let mut update_title = None; - let turn_result: Result = async { - let mut completion_intent = CompletionIntent::UserPrompt; + let turn_result: Result<()> = async { + let mut intent = CompletionIntent::UserPrompt; loop { - log::debug!( - "Building completion request with intent: {:?}", - completion_intent - ); - let request = this.update(cx, |this, cx| { - this.build_completion_request(completion_intent, cx) - })??; - - log::info!("Calling model.stream_completion"); - - let mut tool_use_limit_reached = false; - let mut refused = false; - let mut reached_max_tokens = false; - let mut tool_uses = Self::stream_completion_with_retries( - this.clone(), - model.clone(), - request, - &event_stream, - &mut tool_use_limit_reached, - &mut refused, - &mut reached_max_tokens, - cx, - ) - .await?; - - if refused { - return Ok(StopReason::Refusal); - } else if reached_max_tokens { - return Ok(StopReason::MaxTokens); - } - - let end_turn = tool_uses.is_empty(); - while let Some(tool_result) = tool_uses.next().await { - log::info!("Tool finished {:?}", tool_result); - - event_stream.update_tool_call_fields( - &tool_result.tool_use_id, - acp::ToolCallUpdateFields { - status: Some(if tool_result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - }), - raw_output: tool_result.output.clone(), - ..Default::default() - }, - ); - this.update(cx, |this, _cx| { - this.pending_message() - .tool_results - .insert(tool_result.tool_use_id.clone(), tool_result); - })?; - } + Self::stream_completion(&this, &model, intent, &event_stream, cx).await?; + let mut end_turn = true; this.update(cx, |this, cx| { + // Generate title if needed. if this.title.is_none() && update_title.is_none() { update_title = Some(this.update_title(&event_stream, cx)); } + + // End the turn if the model didn't use tools. + let message = this.pending_message.as_ref(); + end_turn = + message.map_or(true, |message| message.tool_results.is_empty()); + this.flush_pending_message(cx); })?; - if tool_use_limit_reached { + if this.read_with(cx, |this, _| this.tool_use_limit_reached)? { log::info!("Tool use limit reached, completing turn"); - this.update(cx, |this, _cx| this.tool_use_limit_reached = true)?; return Err(language_model::ToolUseLimitReachedError.into()); } else if end_turn { log::info!("No tool uses found, completing turn"); - return Ok(StopReason::EndTurn); + return Ok(()); } else { - this.update(cx, |this, cx| this.flush_pending_message(cx))?; - completion_intent = CompletionIntent::ToolResults; + intent = CompletionIntent::ToolResults; } } } .await; _ = this.update(cx, |this, cx| this.flush_pending_message(cx)); - match turn_result { - Ok(reason) => { - log::info!("Turn execution completed: {:?}", reason); - - if let Some(update_title) = update_title { - update_title.await.context("update title failed").log_err(); - } + if let Some(update_title) = update_title { + update_title.await.context("update title failed").log_err(); + } - event_stream.send_stop(reason); - if reason == StopReason::Refusal { - _ = this.update(cx, |this, _| this.messages.truncate(message_ix)); - } + match turn_result { + Ok(()) => { + log::info!("Turn execution completed"); + event_stream.send_stop(acp::StopReason::EndTurn); } Err(error) => { log::error!("Turn execution failed: {:?}", error); - event_stream.send_error(error); + match error.downcast::() { + Ok(CompletionError::Refusal) => { + event_stream.send_stop(acp::StopReason::Refusal); + _ = this.update(cx, |this, _| this.messages.truncate(message_ix)); + } + Ok(CompletionError::MaxTokens) => { + event_stream.send_stop(acp::StopReason::MaxTokens); + } + Ok(CompletionError::Other(error)) | Err(error) => { + event_stream.send_error(error); + } + } } } @@ -1181,17 +1152,17 @@ impl Thread { Ok(events_rx) } - async fn stream_completion_with_retries( - this: WeakEntity, - model: Arc, - request: LanguageModelRequest, + async fn stream_completion( + this: &WeakEntity, + model: &Arc, + completion_intent: CompletionIntent, event_stream: &ThreadEventStream, - tool_use_limit_reached: &mut bool, - refusal: &mut bool, - max_tokens_reached: &mut bool, cx: &mut AsyncApp, - ) -> Result>> { + ) -> Result<()> { log::debug!("Stream completion started successfully"); + let request = this.update(cx, |this, cx| { + this.build_completion_request(completion_intent, cx) + })??; let mut attempt = None; 'retry: loop { @@ -1204,68 +1175,33 @@ impl Thread { attempt ); - let mut events = model.stream_completion(request.clone(), cx).await?; - let mut tool_uses = FuturesUnordered::new(); + log::info!( + "Calling model.stream_completion, attempt {}", + attempt.unwrap_or(0) + ); + let mut events = model + .stream_completion(request.clone(), cx) + .await + .map_err(|error| anyhow!(error))?; + let mut tool_results = FuturesUnordered::new(); + while let Some(event) = events.next().await { match event { - Ok(LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::ToolUseLimitReached, - )) => { - *tool_use_limit_reached = true; - } - Ok(LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { amount, limit }, - )) => { - this.update(cx, |this, cx| { - this.update_model_request_usage(amount, limit, cx) - })?; - } - Ok(LanguageModelCompletionEvent::UsageUpdate(usage)) => { - telemetry::event!( - "Agent Thread Completion Usage Updated", - thread_id = this.read_with(cx, |this, _| this.id.to_string())?, - prompt_id = this.read_with(cx, |this, _| this.prompt_id.to_string())?, - model = model.telemetry_id(), - model_provider = model.provider_id().to_string(), - attempt, - input_tokens = usage.input_tokens, - output_tokens = usage.output_tokens, - cache_creation_input_tokens = usage.cache_creation_input_tokens, - cache_read_input_tokens = usage.cache_read_input_tokens, - ); - - this.update(cx, |this, cx| this.update_token_usage(usage, cx))?; - } - Ok(LanguageModelCompletionEvent::Stop(StopReason::Refusal)) => { - *refusal = true; - return Ok(FuturesUnordered::default()); - } - Ok(LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)) => { - *max_tokens_reached = true; - return Ok(FuturesUnordered::default()); - } - Ok(LanguageModelCompletionEvent::Stop( - StopReason::ToolUse | StopReason::EndTurn, - )) => break, Ok(event) => { log::trace!("Received completion event: {:?}", event); - this.update(cx, |this, cx| { - tool_uses.extend(this.handle_streamed_completion_event( - event, - event_stream, - cx, - )); - })?; + tool_results.extend(this.update(cx, |this, cx| { + this.handle_streamed_completion_event(event, event_stream, cx) + })??); } Err(error) => { let completion_mode = this.read_with(cx, |thread, _cx| thread.completion_mode())?; if completion_mode == CompletionMode::Normal { - return Err(error.into()); + return Err(anyhow!(error))?; } let Some(strategy) = Self::retry_strategy_for(&error) else { - return Err(error.into()); + return Err(anyhow!(error))?; }; let max_attempts = match &strategy { @@ -1279,7 +1215,7 @@ impl Thread { let attempt = *attempt; if attempt > max_attempts { - return Err(error.into()); + return Err(anyhow!(error))?; } let delay = match &strategy { @@ -1306,7 +1242,29 @@ impl Thread { } } - return Ok(tool_uses); + while let Some(tool_result) = tool_results.next().await { + log::info!("Tool finished {:?}", tool_result); + + event_stream.update_tool_call_fields( + &tool_result.tool_use_id, + acp::ToolCallUpdateFields { + status: Some(if tool_result.is_error { + acp::ToolCallStatus::Failed + } else { + acp::ToolCallStatus::Completed + }), + raw_output: tool_result.output.clone(), + ..Default::default() + }, + ); + this.update(cx, |this, _cx| { + this.pending_message() + .tool_results + .insert(tool_result.tool_use_id.clone(), tool_result); + })?; + } + + return Ok(()); } } @@ -1328,14 +1286,14 @@ impl Thread { } /// A helper method that's called on every streamed completion event. - /// Returns an optional tool result task, which the main agentic loop in - /// send will send back to the model when it resolves. + /// Returns an optional tool result task, which the main agentic loop will + /// send back to the model when it resolves. fn handle_streamed_completion_event( &mut self, event: LanguageModelCompletionEvent, event_stream: &ThreadEventStream, cx: &mut Context, - ) -> Option> { + ) -> Result>> { log::trace!("Handling streamed completion event: {:?}", event); use LanguageModelCompletionEvent::*; @@ -1350,7 +1308,7 @@ impl Thread { } RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx), ToolUse(tool_use) => { - return self.handle_tool_use_event(tool_use, event_stream, cx); + return Ok(self.handle_tool_use_event(tool_use, event_stream, cx)); } ToolUseJsonParseError { id, @@ -1358,18 +1316,46 @@ impl Thread { raw_input, json_parse_error, } => { - return Some(Task::ready(self.handle_tool_use_json_parse_error_event( - id, - tool_name, - raw_input, - json_parse_error, + return Ok(Some(Task::ready( + self.handle_tool_use_json_parse_error_event( + id, + tool_name, + raw_input, + json_parse_error, + ), ))); } - StatusUpdate(_) => {} - UsageUpdate(_) | Stop(_) => unreachable!(), + UsageUpdate(usage) => { + telemetry::event!( + "Agent Thread Completion Usage Updated", + thread_id = self.id.to_string(), + prompt_id = self.prompt_id.to_string(), + model = self.model.as_ref().map(|m| m.telemetry_id()), + model_provider = self.model.as_ref().map(|m| m.provider_id().to_string()), + input_tokens = usage.input_tokens, + output_tokens = usage.output_tokens, + cache_creation_input_tokens = usage.cache_creation_input_tokens, + cache_read_input_tokens = usage.cache_read_input_tokens, + ); + self.update_token_usage(usage, cx); + } + StatusUpdate(CompletionRequestStatus::UsageUpdated { amount, limit }) => { + self.update_model_request_usage(amount, limit, cx); + } + StatusUpdate( + CompletionRequestStatus::Started + | CompletionRequestStatus::Queued { .. } + | CompletionRequestStatus::Failed { .. }, + ) => {} + StatusUpdate(CompletionRequestStatus::ToolUseLimitReached) => { + self.tool_use_limit_reached = true; + } + Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()), + Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()), + Stop(StopReason::ToolUse | StopReason::EndTurn) => {} } - None + Ok(None) } fn handle_text_event( @@ -2225,25 +2211,8 @@ impl ThreadEventStream { self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); } - fn send_stop(&self, reason: StopReason) { - match reason { - StopReason::EndTurn => { - self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::EndTurn))) - .ok(); - } - StopReason::MaxTokens => { - self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::MaxTokens))) - .ok(); - } - StopReason::Refusal => { - self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Refusal))) - .ok(); - } - StopReason::ToolUse => {} - } + fn send_stop(&self, reason: acp::StopReason) { + self.0.unbounded_send(Ok(ThreadEvent::Stop(reason))).ok(); } fn send_canceled(&self) { From 6f32d36ec95f973b6d7866f28d4c4310f4f9f4f9 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Thu, 21 Aug 2025 12:03:30 -0500 Subject: [PATCH 243/744] Upload telemetry event on crashes (#36695) This will let us track crashes-per-launch using the new minidump-based crash reporting. Release Notes: - N/A Co-authored-by: Conrad Irwin Co-authored-by: Marshall Bowers --- crates/zed/src/reliability.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index f55468280ce3d17aa789780c4bfb2a6d6acfb514..646a3af5bbcb110ea9886d52988fa82ad4023ec2 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -251,6 +251,7 @@ pub fn init( endpoint, minidump_contents, &metadata, + installation_id.clone(), ) .await .log_err(); @@ -478,7 +479,9 @@ fn upload_panics_and_crashes( return; } cx.background_spawn(async move { - upload_previous_minidumps(http.clone()).await.warn_on_err(); + upload_previous_minidumps(http.clone(), installation_id.clone()) + .await + .warn_on_err(); let most_recent_panic = upload_previous_panics(http.clone(), &panic_report_url) .await .log_err() @@ -546,7 +549,10 @@ async fn upload_previous_panics( Ok(most_recent_panic) } -pub async fn upload_previous_minidumps(http: Arc) -> anyhow::Result<()> { +pub async fn upload_previous_minidumps( + http: Arc, + installation_id: Option, +) -> anyhow::Result<()> { let Some(minidump_endpoint) = MINIDUMP_ENDPOINT.as_ref() else { log::warn!("Minidump endpoint not set"); return Ok(()); @@ -569,6 +575,7 @@ pub async fn upload_previous_minidumps(http: Arc) -> anyhow:: .await .context("Failed to read minidump")?, &metadata, + installation_id.clone(), ) .await .log_err() @@ -586,6 +593,7 @@ async fn upload_minidump( endpoint: &str, minidump: Vec, metadata: &crashes::CrashInfo, + installation_id: Option, ) -> Result<()> { let mut form = Form::new() .part( @@ -601,7 +609,9 @@ async fn upload_minidump( .text("sentry[tags][version]", metadata.init.zed_version.clone()) .text("sentry[release]", metadata.init.commit_sha.clone()) .text("platform", "rust"); + let mut panic_message = "".to_owned(); if let Some(panic_info) = metadata.panic.as_ref() { + panic_message = panic_info.message.clone(); form = form.text("sentry[logentry][formatted]", panic_info.message.clone()); form = form.text("span", panic_info.span.clone()); // TODO: add gpu-context, feature-flag-context, and more of device-context like gpu @@ -610,6 +620,16 @@ async fn upload_minidump( if let Some(minidump_error) = metadata.minidump_error.clone() { form = form.text("minidump_error", minidump_error); } + if let Some(id) = installation_id.clone() { + form = form.text("sentry[user][id]", id) + } + + ::telemetry::event!( + "Minidump Uploaded", + panic_message = panic_message, + crashed_version = metadata.init.zed_version.clone(), + commit_sha = metadata.init.commit_sha.clone(), + ); let mut response_text = String::new(); let mut response = http.send_multipart_form(endpoint, form).await?; From b284b1a0b86715d9ac945034f6923f2551ce630b Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 21 Aug 2025 19:08:26 +0200 Subject: [PATCH 244/744] remote: Fetch shell on ssh remote to use for preparing commands (#36690) Prerequisite for https://github.com/zed-industries/zed/pull/36576 to allow us to differentiate the shell in a remote. Release Notes: - N/A --- crates/debugger_ui/src/session/running.rs | 7 ++- crates/project/src/debugger/dap_store.rs | 13 +++-- crates/project/src/debugger/locators/cargo.rs | 4 +- crates/project/src/terminals.rs | 36 ++++++++----- crates/remote/src/remote.rs | 4 +- crates/remote/src/ssh_session.rs | 38 ++++++++++++- crates/task/src/shell_builder.rs | 54 +++++++++++-------- crates/task/src/task.rs | 2 +- crates/terminal_view/src/terminal_panel.rs | 13 +++-- crates/util/src/paths.rs | 2 +- 10 files changed, 121 insertions(+), 52 deletions(-) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 0574091851f8f99e10ab8d1f7ec769177826e41a..9991395f351dd3cd6d6a7f6d95ded11024ba6a4e 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -916,7 +916,10 @@ impl RunningState { let task_store = project.read(cx).task_store().downgrade(); let weak_project = project.downgrade(); let weak_workspace = workspace.downgrade(); - let is_local = project.read(cx).is_local(); + let ssh_info = project + .read(cx) + .ssh_client() + .and_then(|it| it.read(cx).ssh_info()); cx.spawn_in(window, async move |this, cx| { let DebugScenario { @@ -1000,7 +1003,7 @@ impl RunningState { None }; - let builder = ShellBuilder::new(is_local, &task.resolved.shell); + let builder = ShellBuilder::new(ssh_info.as_ref().map(|info| &*info.shell), &task.resolved.shell); let command_label = builder.command_label(&task.resolved.command_label); let (command, args) = builder.build(task.resolved.command.clone(), &task.resolved.args); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 834bf2c2d2328e0c8b8d4ea211feb9bf255a0546..2906c32ff4f67ba733d4f0faf8f511d0c433ec91 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -34,7 +34,7 @@ use http_client::HttpClient; use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind}; use node_runtime::NodeRuntime; -use remote::{SshRemoteClient, ssh_session::SshArgs}; +use remote::{SshInfo, SshRemoteClient, ssh_session::SshArgs}; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self}, @@ -254,14 +254,18 @@ impl DapStore { cx.spawn(async move |_, cx| { let response = request.await?; let binary = DebugAdapterBinary::from_proto(response)?; - let (mut ssh_command, envs, path_style) = + let (mut ssh_command, envs, path_style, ssh_shell) = ssh_client.read_with(cx, |ssh, _| { - let (SshArgs { arguments, envs }, path_style) = - ssh.ssh_info().context("SSH arguments not found")?; + let SshInfo { + args: SshArgs { arguments, envs }, + path_style, + shell, + } = ssh.ssh_info().context("SSH arguments not found")?; anyhow::Ok(( SshCommand { arguments }, envs.unwrap_or_default(), path_style, + shell, )) })??; @@ -280,6 +284,7 @@ impl DapStore { } let (program, args) = wrap_for_ssh( + &ssh_shell, &ssh_command, binary .command diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index 3e28fac8af22930876d096d5b7773a0825becf4f..b2f9580f9ced893448f86bfb2f7aab4a0de8a52e 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -117,7 +117,7 @@ impl DapLocator for CargoLocator { .cwd .clone() .context("Couldn't get cwd from debug config which is needed for locators")?; - let builder = ShellBuilder::new(true, &build_config.shell).non_interactive(); + let builder = ShellBuilder::new(None, &build_config.shell).non_interactive(); let (program, args) = builder.build( Some("cargo".into()), &build_config @@ -126,7 +126,7 @@ impl DapLocator for CargoLocator { .cloned() .take_while(|arg| arg != "--") .chain(Some("--message-format=json".to_owned())) - .collect(), + .collect::>(), ); let mut child = util::command::new_smol_command(program) .args(args) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index e9582e73fd6884f73e8bf7abcc04a7489bad9dc5..b009b357fe8eef1a7df61117857251178b437659 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -4,7 +4,7 @@ use collections::HashMap; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; use itertools::Itertools; use language::LanguageName; -use remote::ssh_session::SshArgs; +use remote::{SshInfo, ssh_session::SshArgs}; use settings::{Settings, SettingsLocation}; use smol::channel::bounded; use std::{ @@ -13,7 +13,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use task::{DEFAULT_REMOTE_SHELL, Shell, ShellBuilder, SpawnInTerminal}; +use task::{Shell, ShellBuilder, SpawnInTerminal}; use terminal::{ TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings}, @@ -58,11 +58,13 @@ impl SshCommand { } } +#[derive(Debug)] pub struct SshDetails { pub host: String, pub ssh_command: SshCommand, pub envs: Option>, pub path_style: PathStyle, + pub shell: String, } impl Project { @@ -87,12 +89,18 @@ impl Project { pub fn ssh_details(&self, cx: &App) -> Option { if let Some(ssh_client) = &self.ssh_client { let ssh_client = ssh_client.read(cx); - if let Some((SshArgs { arguments, envs }, path_style)) = ssh_client.ssh_info() { + if let Some(SshInfo { + args: SshArgs { arguments, envs }, + path_style, + shell, + }) = ssh_client.ssh_info() + { return Some(SshDetails { host: ssh_client.connection_options().host, ssh_command: SshCommand { arguments }, envs, path_style, + shell, }); } } @@ -165,7 +173,9 @@ impl Project { let ssh_details = self.ssh_details(cx); let settings = self.terminal_settings(&path, cx).clone(); - let builder = ShellBuilder::new(ssh_details.is_none(), &settings.shell).non_interactive(); + let builder = + ShellBuilder::new(ssh_details.as_ref().map(|ssh| &*ssh.shell), &settings.shell) + .non_interactive(); let (command, args) = builder.build(Some(command), &Vec::new()); let mut env = self @@ -180,9 +190,11 @@ impl Project { ssh_command, envs, path_style, + shell, .. }) => { let (command, args) = wrap_for_ssh( + &shell, &ssh_command, Some((&command, &args)), path.as_deref(), @@ -280,6 +292,7 @@ impl Project { ssh_command, envs, path_style, + shell, }) => { log::debug!("Connecting to a remote server: {ssh_command:?}"); @@ -291,6 +304,7 @@ impl Project { .or_insert_with(|| "xterm-256color".to_string()); let (program, args) = wrap_for_ssh( + &shell, &ssh_command, None, path.as_deref(), @@ -343,11 +357,13 @@ impl Project { ssh_command, envs, path_style, + shell, }) => { log::debug!("Connecting to a remote server: {ssh_command:?}"); env.entry("TERM".to_string()) .or_insert_with(|| "xterm-256color".to_string()); let (program, args) = wrap_for_ssh( + &shell, &ssh_command, spawn_task .command @@ -637,6 +653,7 @@ impl Project { } pub fn wrap_for_ssh( + shell: &str, ssh_command: &SshCommand, command: Option<(&String, &Vec)>, path: Option<&Path>, @@ -645,16 +662,11 @@ pub fn wrap_for_ssh( path_style: PathStyle, ) -> (String, Vec) { let to_run = if let Some((command, args)) = command { - // DEFAULT_REMOTE_SHELL is '"${SHELL:-sh}"' so must not be escaped - let command: Option> = if command == DEFAULT_REMOTE_SHELL { - Some(command.into()) - } else { - shlex::try_quote(command).ok() - }; + let command: Option> = shlex::try_quote(command).ok(); let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok()); command.into_iter().chain(args).join(" ") } else { - "exec ${SHELL:-sh} -l".to_string() + format!("exec {shell} -l") }; let mut env_changes = String::new(); @@ -688,7 +700,7 @@ pub fn wrap_for_ssh( } else { format!("cd; {env_changes} {to_run}") }; - let shell_invocation = format!("sh -c {}", shlex::try_quote(&commands).unwrap()); + let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&commands).unwrap()); let program = "ssh".to_string(); let mut args = ssh_command.arguments.clone(); diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs index 43eb59c0aece4e1ff12b03eda0bd4f6d796544f7..71895f1678c5e71819218f62d3831708c2e4a2bc 100644 --- a/crates/remote/src/remote.rs +++ b/crates/remote/src/remote.rs @@ -4,6 +4,6 @@ pub mod proxy; pub mod ssh_session; pub use ssh_session::{ - ConnectionState, SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteClient, - SshRemoteEvent, + ConnectionState, SshClientDelegate, SshConnectionOptions, SshInfo, SshPlatform, + SshRemoteClient, SshRemoteEvent, }; diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index a26f4be6615ac27468b5135e22f3bafdf9bf3f33..c02d0ad7e776171027dd275d82b5d26eca380a20 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -89,11 +89,19 @@ pub struct SshConnectionOptions { pub upload_binary_over_ssh: bool, } +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SshArgs { pub arguments: Vec, pub envs: Option>, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SshInfo { + pub args: SshArgs, + pub path_style: PathStyle, + pub shell: String, +} + #[macro_export] macro_rules! shell_script { ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{ @@ -471,6 +479,16 @@ impl SshSocket { Ok(SshPlatform { os, arch }) } + + async fn shell(&self) -> String { + match self.run_command("sh", &["-c", "echo $SHELL"]).await { + Ok(shell) => shell.trim().to_owned(), + Err(e) => { + log::error!("Failed to get shell: {e}"); + "sh".to_owned() + } + } + } } const MAX_MISSED_HEARTBEATS: usize = 5; @@ -1152,12 +1170,16 @@ impl SshRemoteClient { cx.notify(); } - pub fn ssh_info(&self) -> Option<(SshArgs, PathStyle)> { + pub fn ssh_info(&self) -> Option { self.state .lock() .as_ref() .and_then(|state| state.ssh_connection()) - .map(|ssh_connection| (ssh_connection.ssh_args(), ssh_connection.path_style())) + .map(|ssh_connection| SshInfo { + args: ssh_connection.ssh_args(), + path_style: ssh_connection.path_style(), + shell: ssh_connection.shell(), + }) } pub fn upload_directory( @@ -1392,6 +1414,7 @@ trait RemoteConnection: Send + Sync { fn ssh_args(&self) -> SshArgs; fn connection_options(&self) -> SshConnectionOptions; fn path_style(&self) -> PathStyle; + fn shell(&self) -> String; #[cfg(any(test, feature = "test-support"))] fn simulate_disconnect(&self, _: &AsyncApp) {} @@ -1403,6 +1426,7 @@ struct SshRemoteConnection { remote_binary_path: Option, ssh_platform: SshPlatform, ssh_path_style: PathStyle, + ssh_shell: String, _temp_dir: TempDir, } @@ -1429,6 +1453,10 @@ impl RemoteConnection for SshRemoteConnection { self.socket.connection_options.clone() } + fn shell(&self) -> String { + self.ssh_shell.clone() + } + fn upload_directory( &self, src_path: PathBuf, @@ -1642,6 +1670,7 @@ impl SshRemoteConnection { "windows" => PathStyle::Windows, _ => PathStyle::Posix, }; + let ssh_shell = socket.shell().await; let mut this = Self { socket, @@ -1650,6 +1679,7 @@ impl SshRemoteConnection { remote_binary_path: None, ssh_path_style, ssh_platform, + ssh_shell, }; let (release_channel, version, commit) = cx.update(|cx| { @@ -2686,6 +2716,10 @@ mod fake { fn path_style(&self) -> PathStyle { PathStyle::current() } + + fn shell(&self) -> String { + "sh".to_owned() + } } pub(super) struct Delegate; diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index 770312bafc3a9249c83f3bc4eca130afd59da95f..de4ddc00f49eded4ba64faa6d94baa1cc6ecf3aa 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -1,26 +1,40 @@ use crate::Shell; -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -enum ShellKind { +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ShellKind { #[default] Posix, + Csh, + Fish, Powershell, Nushell, Cmd, } impl ShellKind { - fn new(program: &str) -> Self { + pub fn system() -> Self { + Self::new(&system_shell()) + } + + pub fn new(program: &str) -> Self { + #[cfg(windows)] + let (_, program) = program.rsplit_once('\\').unwrap_or(("", program)); + #[cfg(not(windows))] + let (_, program) = program.rsplit_once('/').unwrap_or(("", program)); if program == "powershell" - || program.ends_with("powershell.exe") + || program == "powershell.exe" || program == "pwsh" - || program.ends_with("pwsh.exe") + || program == "pwsh.exe" { ShellKind::Powershell - } else if program == "cmd" || program.ends_with("cmd.exe") { + } else if program == "cmd" || program == "cmd.exe" { ShellKind::Cmd } else if program == "nu" { ShellKind::Nushell + } else if program == "fish" { + ShellKind::Fish + } else if program == "csh" { + ShellKind::Csh } else { // Someother shell detected, the user might install and use a // unix-like shell. @@ -33,6 +47,8 @@ impl ShellKind { Self::Powershell => Self::to_powershell_variable(input), Self::Cmd => Self::to_cmd_variable(input), Self::Posix => input.to_owned(), + Self::Fish => input.to_owned(), + Self::Csh => input.to_owned(), Self::Nushell => Self::to_nushell_variable(input), } } @@ -153,7 +169,7 @@ impl ShellKind { match self { ShellKind::Powershell => vec!["-C".to_owned(), combined_command], ShellKind::Cmd => vec!["/C".to_owned(), combined_command], - ShellKind::Posix | ShellKind::Nushell => interactive + ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => interactive .then(|| "-i".to_owned()) .into_iter() .chain(["-c".to_owned(), combined_command]) @@ -184,19 +200,14 @@ pub struct ShellBuilder { kind: ShellKind, } -pub static DEFAULT_REMOTE_SHELL: &str = "\"${SHELL:-sh}\""; - impl ShellBuilder { /// Create a new ShellBuilder as configured. - pub fn new(is_local: bool, shell: &Shell) -> Self { + pub fn new(remote_system_shell: Option<&str>, shell: &Shell) -> Self { let (program, args) = match shell { - Shell::System => { - if is_local { - (system_shell(), Vec::new()) - } else { - (DEFAULT_REMOTE_SHELL.to_string(), Vec::new()) - } - } + Shell::System => match remote_system_shell { + Some(remote_shell) => (remote_shell.to_string(), Vec::new()), + None => (system_shell(), Vec::new()), + }, Shell::Program(shell) => (shell.clone(), Vec::new()), Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()), }; @@ -212,6 +223,7 @@ impl ShellBuilder { self.interactive = false; self } + /// Returns the label to show in the terminal tab pub fn command_label(&self, command_label: &str) -> String { match self.kind { @@ -221,7 +233,7 @@ impl ShellBuilder { ShellKind::Cmd => { format!("{} /C '{}'", self.program, command_label) } - ShellKind::Posix | ShellKind::Nushell => { + ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => { let interactivity = self.interactive.then_some("-i ").unwrap_or_default(); format!( "{} {interactivity}-c '$\"{}\"'", @@ -234,7 +246,7 @@ impl ShellBuilder { pub fn build( mut self, task_command: Option, - task_args: &Vec, + task_args: &[String], ) -> (String, Vec) { if let Some(task_command) = task_command { let combined_command = task_args.iter().fold(task_command, |mut command, arg| { @@ -258,11 +270,11 @@ mod test { #[test] fn test_nu_shell_variable_substitution() { let shell = Shell::Program("nu".to_owned()); - let shell_builder = ShellBuilder::new(true, &shell); + let shell_builder = ShellBuilder::new(None, &shell); let (program, args) = shell_builder.build( Some("echo".into()), - &vec![ + &[ "${hello}".to_string(), "$world".to_string(), "nothing".to_string(), diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index 85e654eff424cc349d70372a1480eed83eec4032..eb9e59f0876048dfeef580606cf0df6f3486ff18 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -22,7 +22,7 @@ pub use debug_format::{ AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, Request, TcpArgumentsTemplate, ZedDebugConfig, }; -pub use shell_builder::{DEFAULT_REMOTE_SHELL, ShellBuilder}; +pub use shell_builder::{ShellBuilder, ShellKind}; pub use task_template::{ DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates, substitute_variables_in_map, substitute_variables_in_str, diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index f40c4870f12f87e3e031ebdb66cd79d3c536589e..6b17911487261a254cd5e7a5e9256358c0b2e696 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -481,14 +481,17 @@ impl TerminalPanel { window: &mut Window, cx: &mut Context, ) -> Task>> { - let Ok(is_local) = self - .workspace - .update(cx, |workspace, cx| workspace.project().read(cx).is_local()) - else { + let Ok((ssh_client, false)) = self.workspace.update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + ( + project.ssh_client().and_then(|it| it.read(cx).ssh_info()), + project.is_via_collab(), + ) + }) else { return Task::ready(Err(anyhow!("Project is not local"))); }; - let builder = ShellBuilder::new(is_local, &task.shell); + let builder = ShellBuilder::new(ssh_client.as_ref().map(|info| &*info.shell), &task.shell); let command_label = builder.command_label(&task.command_label); let (command, args) = builder.build(task.command.clone(), &task.args); diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index b4301203146c876db3f9832b059d15d4321600e0..1192b14812580bf21e262620a3ccefc90c5acd54 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -166,7 +166,7 @@ impl> From for SanitizedPath { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PathStyle { Posix, Windows, From d166ab95a1bca5a4b4351b50ce96faaa585b1784 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 21 Aug 2025 13:09:14 -0400 Subject: [PATCH 245/744] ci: Switch Windows jobs to target explicit tag (#36693) The previous tags are non-customizable (added by default). This will enable us to pull specific runs out of the pool for maintenance. Also disable actionlint invoking shellcheck because it chokes on PowerShell. Release Notes: - N/A --------- Co-authored-by: Cole Miller --- .github/actionlint.yml | 12 ++++++++++++ .github/workflows/ci.yml | 4 ++-- .github/workflows/release_nightly.yml | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/actionlint.yml b/.github/actionlint.yml index 0ee6af8a1d38e005f66b79f6c548d9f79396ea35..bc02d312f80d42f26b706546d914e191ba7eea92 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -30,3 +30,15 @@ self-hosted-runner: # Self Hosted Runners - self-mini-macos - self-32vcpu-windows-2022 + +# Disable shellcheck because it doesn't like powershell +# This should have been triggered with initial rollout of actionlint +# but https://github.com/zed-industries/zed/pull/36693 +# somehow caused actionlint to actually check those windows jobs +# where previously they were being skipped. Likely caused by an +# unknown bug in actionlint where parsing of `runs-on: [ ]` +# breaks something else. (yuck) +paths: + .github/workflows/{ci,release_nightly}.yml: + ignore: + - "shellcheck" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4ba227168fb9cec10e1b5e23223b48e7a4ca222..a45c0a14f1b3e2b1d57169c4b6705eaf6ce55e40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -418,7 +418,7 @@ jobs: if: | github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' - runs-on: [self-hosted, Windows, X64] + runs-on: [self-32vcpu-windows-2022] steps: - name: Environment Setup run: | @@ -784,7 +784,7 @@ jobs: bundle-windows-x64: timeout-minutes: 120 name: Create a Windows installer - runs-on: [self-hosted, Windows, X64] + runs-on: [self-32vcpu-windows-2022] if: contains(github.event.pull_request.labels.*.name, 'run-bundling') # if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling')) needs: [windows_tests] diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 5d63c34edd28d7e0ab3930132867f40b8f5262e9..d646c68cfa2faf427951ddcb5ce648e6ed3de488 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -59,7 +59,7 @@ jobs: timeout-minutes: 60 name: Run tests on Windows if: github.repository_owner == 'zed-industries' - runs-on: [self-hosted, Windows, X64] + runs-on: [self-32vcpu-windows-2022] steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -248,7 +248,7 @@ jobs: timeout-minutes: 60 name: Create a Windows installer if: github.repository_owner == 'zed-industries' - runs-on: [self-hosted, Windows, X64] + runs-on: [self-32vcpu-windows-2022] needs: windows-tests env: AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} From 1b2ceae7efb2b871d19025582cabc4619eee1bdc Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Thu, 21 Aug 2025 12:19:57 -0500 Subject: [PATCH 246/744] Use Tokio::spawn instead of getting an executor handle (#36701) This was causing panics due to the handles being dropped out of order. It doesn't seem possible to guarantee the correct drop ordering given that we're holding them over await points, so lets just spawn on the tokio executor itself which gives us access to the state we needed those handles for in the first place. Fixes: ZED-1R Release Notes: - N/A Co-authored-by: Conrad Irwin Co-authored-by: Marshall Bowers --- Cargo.lock | 1 + crates/client/src/client.rs | 26 ++++++++++--------- .../cloud_api_client/src/cloud_api_client.rs | 8 +----- crates/gpui_tokio/Cargo.toml | 1 + crates/gpui_tokio/src/gpui_tokio.rs | 22 ++++++++++++++++ 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6063530e9f71284b6a3ecf4394ba088bdfbed8d9..61f6f42498978ce0075f95cf98b99fe6b7c0ae73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7539,6 +7539,7 @@ dependencies = [ name = "gpui_tokio" version = "0.1.0" dependencies = [ + "anyhow", "gpui", "tokio", "util", diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index ed3f1149433a9532ef2c08032ffa494cf09dfb4c..f9b8a10610fa429e03e1214a4d3e4560af7bec4e 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1290,19 +1290,21 @@ impl Client { "http" => Http, _ => Err(anyhow!("invalid rpc url: {}", rpc_url))?, }; - let rpc_host = rpc_url - .host_str() - .zip(rpc_url.port_or_known_default()) - .context("missing host in rpc url")?; - - let stream = { - let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap(); - let _guard = handle.enter(); - match proxy { - Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?, - None => Box::new(TcpStream::connect(rpc_host).await?), + + let stream = gpui_tokio::Tokio::spawn_result(cx, { + let rpc_url = rpc_url.clone(); + async move { + let rpc_host = rpc_url + .host_str() + .zip(rpc_url.port_or_known_default()) + .context("missing host in rpc url")?; + Ok(match proxy { + Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?, + None => Box::new(TcpStream::connect(rpc_host).await?), + }) } - }; + })? + .await?; log::info!("connected to rpc endpoint {}", rpc_url); diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index 92417d8319fc1ce0750f0c2b400e49a24b83e073..205f3e243296fdc830f32c2b664e615052ca7611 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -102,13 +102,7 @@ impl CloudApiClient { let credentials = credentials.as_ref().context("no credentials provided")?; let authorization_header = format!("{} {}", credentials.user_id, credentials.access_token); - Ok(cx.spawn(async move |cx| { - let handle = cx - .update(|cx| Tokio::handle(cx)) - .ok() - .context("failed to get Tokio handle")?; - let _guard = handle.enter(); - + Ok(Tokio::spawn_result(cx, async move { let ws = WebSocket::connect(connect_url) .with_request( request::Builder::new() diff --git a/crates/gpui_tokio/Cargo.toml b/crates/gpui_tokio/Cargo.toml index 46d5eafd5adceadadf5fbd942d104ee4249aa941..2d4abf40631a2f011306d7216a5f96864ccdb0da 100644 --- a/crates/gpui_tokio/Cargo.toml +++ b/crates/gpui_tokio/Cargo.toml @@ -13,6 +13,7 @@ path = "src/gpui_tokio.rs" doctest = false [dependencies] +anyhow.workspace = true util.workspace = true gpui.workspace = true tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } diff --git a/crates/gpui_tokio/src/gpui_tokio.rs b/crates/gpui_tokio/src/gpui_tokio.rs index fffe18a616d9b597c9f5ed25b68df2911c8f3886..8384f2a88ec82b96c0490913019b701cdf01239c 100644 --- a/crates/gpui_tokio/src/gpui_tokio.rs +++ b/crates/gpui_tokio/src/gpui_tokio.rs @@ -52,6 +52,28 @@ impl Tokio { }) } + /// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task + /// Note that the Tokio task will be cancelled if the GPUI task is dropped + pub fn spawn_result(cx: &C, f: Fut) -> C::Result>> + where + C: AppContext, + Fut: Future> + Send + 'static, + R: Send + 'static, + { + cx.read_global(|tokio: &GlobalTokio, cx| { + let join_handle = tokio.runtime.spawn(f); + let abort_handle = join_handle.abort_handle(); + let cancel = defer(move || { + abort_handle.abort(); + }); + cx.background_spawn(async move { + let result = join_handle.await?; + drop(cancel); + result + }) + }) + } + pub fn handle(cx: &App) -> tokio::runtime::Handle { GlobalTokio::global(cx).runtime.handle().clone() } From f2899bf34b136ce9dfc14fe1d3531a99b4899a27 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 21 Aug 2025 13:21:37 -0400 Subject: [PATCH 247/744] ci: Switch from ubuntu-latest to namespace (2) (#36702) In response to ongoing [github actions incident](https://www.githubstatus.com/incidents/c7kq3ctclddp) Supercedes: https://github.com/zed-industries/zed/pull/36698 Release Notes: - N/A --- .github/actionlint.yml | 3 ++- .github/workflows/bump_collab_staging.yml | 2 +- .github/workflows/ci.yml | 6 +++--- .github/workflows/danger.yml | 2 +- .github/workflows/release_nightly.yml | 2 +- .github/workflows/script_checks.yml | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/actionlint.yml b/.github/actionlint.yml index bc02d312f80d42f26b706546d914e191ba7eea92..6d8e0107e9b42e71bb7266c0629393b9057e05bc 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -19,11 +19,12 @@ self-hosted-runner: - namespace-profile-16x32-ubuntu-2004-arm - namespace-profile-32x64-ubuntu-2004-arm # Namespace Ubuntu 22.04 (Everything else) - - namespace-profile-2x4-ubuntu-2204 - namespace-profile-4x8-ubuntu-2204 - namespace-profile-8x16-ubuntu-2204 - namespace-profile-16x32-ubuntu-2204 - namespace-profile-32x64-ubuntu-2204 + # Namespace Ubuntu 24.04 (like ubuntu-latest) + - namespace-profile-2x4-ubuntu-2404 # Namespace Limited Preview - namespace-profile-8x16-ubuntu-2004-arm-m4 - namespace-profile-8x32-ubuntu-2004-arm-m4 diff --git a/.github/workflows/bump_collab_staging.yml b/.github/workflows/bump_collab_staging.yml index d8eaa6019ec29b5dd908564d05f430d3e7f01909..d400905b4da3304a8b916d3a38ae9d8a2855dbf5 100644 --- a/.github/workflows/bump_collab_staging.yml +++ b/.github/workflows/bump_collab_staging.yml @@ -8,7 +8,7 @@ on: jobs: update-collab-staging-tag: if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a45c0a14f1b3e2b1d57169c4b6705eaf6ce55e40..a34833d0fddd8ce53e1b06d839d97987b688edfb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: run_nix: ${{ steps.filter.outputs.run_nix }} run_actionlint: ${{ steps.filter.outputs.run_actionlint }} runs-on: - - ubuntu-latest + - namespace-profile-2x4-ubuntu-2404 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -237,7 +237,7 @@ jobs: uses: ./.github/actions/build_docs actionlint: - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 if: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_actionlint == 'true' needs: [job_spec] steps: @@ -458,7 +458,7 @@ jobs: tests_pass: name: Tests Pass - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 needs: - job_spec - style diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 15c82643aef1e14c85daaaf2c8c3c61f62f1b3aa..3f84179278d1baaa7a299e2292b3041830d9ca60 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -12,7 +12,7 @@ on: jobs: danger: if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index d646c68cfa2faf427951ddcb5ce648e6ed3de488..2026ee7b730698cd7e40eebcd141f5b8a6ee9d04 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -290,7 +290,7 @@ jobs: update-nightly-tag: name: Update nightly tag if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 needs: - bundle-mac - bundle-linux-x86 diff --git a/.github/workflows/script_checks.yml b/.github/workflows/script_checks.yml index c32a433e46a6fc5381fa1abbe19b2814fe423c1d..5dbfc9cb7fa9a51b9e0aca972d125c2a27677584 100644 --- a/.github/workflows/script_checks.yml +++ b/.github/workflows/script_checks.yml @@ -12,7 +12,7 @@ jobs: shellcheck: name: "ShellCheck Scripts" if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 From 81cb24810b88080b8cffcb0f75ae6500ef1e654e Mon Sep 17 00:00:00 2001 From: Vitaly Slobodin Date: Thu, 21 Aug 2025 19:23:41 +0200 Subject: [PATCH 248/744] ruby: Improve Ruby test and debug task configurations (#36691) Hi! This pull request adds missing `cwd` field to all Ruby test tasks otherwise `rdbg` will be broken when the user tries to debug a test. Thanks! Release Notes: - N/A --- docs/src/languages/ruby.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/src/languages/ruby.md b/docs/src/languages/ruby.md index 6f530433bd0e15d2ed659dc2e1f0055ad5711cb5..ef4b026db1db85ccf9104fdd3522ea27d2e1b50f 100644 --- a/docs/src/languages/ruby.md +++ b/docs/src/languages/ruby.md @@ -299,6 +299,7 @@ To run tests in your Ruby project, you can set up custom tasks in your local `.z "-n", "\"$ZED_CUSTOM_RUBY_TEST_NAME\"" ], + "cwd": "$ZED_WORKTREE_ROOT", "tags": ["ruby-test"] } ] @@ -321,6 +322,7 @@ Plain minitest does not support running tests by line number, only by name, so w "-n", "\"$ZED_CUSTOM_RUBY_TEST_NAME\"" ], + "cwd": "$ZED_WORKTREE_ROOT", "tags": ["ruby-test"] } ] @@ -334,6 +336,7 @@ Plain minitest does not support running tests by line number, only by name, so w "label": "test $ZED_RELATIVE_FILE:$ZED_ROW", "command": "bundle", "args": ["exec", "rspec", "\"$ZED_RELATIVE_FILE:$ZED_ROW\""], + "cwd": "$ZED_WORKTREE_ROOT", "tags": ["ruby-test"] } ] @@ -369,7 +372,7 @@ The Ruby extension provides a debug adapter for debugging Ruby code. Zed's name "label": "Debug Rails server", "adapter": "rdbg", "request": "launch", - "command": "$ZED_WORKTREE_ROOT/bin/rails", + "command": "./bin/rails", "args": ["server"], "cwd": "$ZED_WORKTREE_ROOT", "env": { From c1e749906febe241a3138280fafcc3ff3fca7416 Mon Sep 17 00:00:00 2001 From: Dave Waggoner Date: Thu, 21 Aug 2025 11:41:32 -0700 Subject: [PATCH 249/744] Add terminal view path like target tests (#35422) Part of - #28238 This PR refactors `Event::NewNavigationTarget` and `Event::Open` handling of `PathLikeTarget` and associated code in `terminal_view.rs` into its own file, `terminal_path_like_target.rs` for improved testability, and adds tests which cover cases from: - #28339 - #28407 - #33498 - #34027 - #34078 Release Notes: - N/A --- .../src/terminal_path_like_target.rs | 825 ++++++++++++++++++ crates/terminal_view/src/terminal_view.rs | 370 +------- 2 files changed, 844 insertions(+), 351 deletions(-) create mode 100644 crates/terminal_view/src/terminal_path_like_target.rs diff --git a/crates/terminal_view/src/terminal_path_like_target.rs b/crates/terminal_view/src/terminal_path_like_target.rs new file mode 100644 index 0000000000000000000000000000000000000000..e20df7f0010480d782eb16375ca3480d4f390742 --- /dev/null +++ b/crates/terminal_view/src/terminal_path_like_target.rs @@ -0,0 +1,825 @@ +use super::{HoverTarget, HoveredWord, TerminalView}; +use anyhow::{Context as _, Result}; +use editor::Editor; +use gpui::{App, AppContext, Context, Task, WeakEntity, Window}; +use itertools::Itertools; +use project::{Entry, Metadata}; +use std::path::PathBuf; +use terminal::PathLikeTarget; +use util::{ResultExt, debug_panic, paths::PathWithPosition}; +use workspace::{OpenOptions, OpenVisible, Workspace}; + +#[derive(Debug, Clone)] +enum OpenTarget { + Worktree(PathWithPosition, Entry), + File(PathWithPosition, Metadata), +} + +impl OpenTarget { + fn is_file(&self) -> bool { + match self { + OpenTarget::Worktree(_, entry) => entry.is_file(), + OpenTarget::File(_, metadata) => !metadata.is_dir, + } + } + + fn is_dir(&self) -> bool { + match self { + OpenTarget::Worktree(_, entry) => entry.is_dir(), + OpenTarget::File(_, metadata) => metadata.is_dir, + } + } + + fn path(&self) -> &PathWithPosition { + match self { + OpenTarget::Worktree(path, _) => path, + OpenTarget::File(path, _) => path, + } + } +} + +pub(super) fn hover_path_like_target( + workspace: &WeakEntity, + hovered_word: HoveredWord, + path_like_target: &PathLikeTarget, + cx: &mut Context, +) -> Task<()> { + let file_to_open_task = possible_open_target(workspace, path_like_target, cx); + cx.spawn(async move |terminal_view, cx| { + let file_to_open = file_to_open_task.await; + terminal_view + .update(cx, |terminal_view, _| match file_to_open { + Some(OpenTarget::File(path, _) | OpenTarget::Worktree(path, _)) => { + terminal_view.hover = Some(HoverTarget { + tooltip: path.to_string(|path| path.to_string_lossy().to_string()), + hovered_word, + }); + } + None => { + terminal_view.hover = None; + } + }) + .ok(); + }) +} + +fn possible_open_target( + workspace: &WeakEntity, + path_like_target: &PathLikeTarget, + cx: &App, +) -> Task> { + let Some(workspace) = workspace.upgrade() else { + return Task::ready(None); + }; + // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too. + // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away. + let mut potential_paths = Vec::new(); + let cwd = path_like_target.terminal_dir.as_ref(); + let maybe_path = &path_like_target.maybe_path; + let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path)); + let path_with_position = PathWithPosition::parse_str(maybe_path); + let worktree_candidates = workspace + .read(cx) + .worktrees(cx) + .sorted_by_key(|worktree| { + let worktree_root = worktree.read(cx).abs_path(); + match cwd.and_then(|cwd| worktree_root.strip_prefix(cwd).ok()) { + Some(cwd_child) => cwd_child.components().count(), + None => usize::MAX, + } + }) + .collect::>(); + // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it. + const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"]; + for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) { + if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() { + potential_paths.push(PathWithPosition { + path: stripped.to_owned(), + row: original_path.row, + column: original_path.column, + }); + } + if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() { + potential_paths.push(PathWithPosition { + path: stripped.to_owned(), + row: path_with_position.row, + column: path_with_position.column, + }); + } + } + + let insert_both_paths = original_path != path_with_position; + potential_paths.insert(0, original_path); + if insert_both_paths { + potential_paths.insert(1, path_with_position); + } + + // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix. + // That will be slow, though, so do the fast checks first. + let mut worktree_paths_to_check = Vec::new(); + for worktree in &worktree_candidates { + let worktree_root = worktree.read(cx).abs_path(); + let mut paths_to_check = Vec::with_capacity(potential_paths.len()); + + for path_with_position in &potential_paths { + let path_to_check = if worktree_root.ends_with(&path_with_position.path) { + let root_path_with_position = PathWithPosition { + path: worktree_root.to_path_buf(), + row: path_with_position.row, + column: path_with_position.column, + }; + match worktree.read(cx).root_entry() { + Some(root_entry) => { + return Task::ready(Some(OpenTarget::Worktree( + root_path_with_position, + root_entry.clone(), + ))); + } + None => root_path_with_position, + } + } else { + PathWithPosition { + path: path_with_position + .path + .strip_prefix(&worktree_root) + .unwrap_or(&path_with_position.path) + .to_owned(), + row: path_with_position.row, + column: path_with_position.column, + } + }; + + if path_to_check.path.is_relative() + && let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) + { + return Task::ready(Some(OpenTarget::Worktree( + PathWithPosition { + path: worktree_root.join(&entry.path), + row: path_to_check.row, + column: path_to_check.column, + }, + entry.clone(), + ))); + } + + paths_to_check.push(path_to_check); + } + + if !paths_to_check.is_empty() { + worktree_paths_to_check.push((worktree.clone(), paths_to_check)); + } + } + + // Before entire worktree traversal(s), make an attempt to do FS checks if available. + let fs_paths_to_check = if workspace.read(cx).project().read(cx).is_local() { + potential_paths + .into_iter() + .flat_map(|path_to_check| { + let mut paths_to_check = Vec::new(); + let maybe_path = &path_to_check.path; + if maybe_path.starts_with("~") { + if let Some(home_path) = + maybe_path + .strip_prefix("~") + .ok() + .and_then(|stripped_maybe_path| { + Some(dirs::home_dir()?.join(stripped_maybe_path)) + }) + { + paths_to_check.push(PathWithPosition { + path: home_path, + row: path_to_check.row, + column: path_to_check.column, + }); + } + } else { + paths_to_check.push(PathWithPosition { + path: maybe_path.clone(), + row: path_to_check.row, + column: path_to_check.column, + }); + if maybe_path.is_relative() { + if let Some(cwd) = &cwd { + paths_to_check.push(PathWithPosition { + path: cwd.join(maybe_path), + row: path_to_check.row, + column: path_to_check.column, + }); + } + for worktree in &worktree_candidates { + paths_to_check.push(PathWithPosition { + path: worktree.read(cx).abs_path().join(maybe_path), + row: path_to_check.row, + column: path_to_check.column, + }); + } + } + } + paths_to_check + }) + .collect() + } else { + Vec::new() + }; + + let worktree_check_task = cx.spawn(async move |cx| { + for (worktree, worktree_paths_to_check) in worktree_paths_to_check { + let found_entry = worktree + .update(cx, |worktree, _| { + let worktree_root = worktree.abs_path(); + let traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); + for entry in traversal { + if let Some(path_in_worktree) = worktree_paths_to_check + .iter() + .find(|path_to_check| entry.path.ends_with(&path_to_check.path)) + { + return Some(OpenTarget::Worktree( + PathWithPosition { + path: worktree_root.join(&entry.path), + row: path_in_worktree.row, + column: path_in_worktree.column, + }, + entry.clone(), + )); + } + } + None + }) + .ok()?; + if let Some(found_entry) = found_entry { + return Some(found_entry); + } + } + None + }); + + let fs = workspace.read(cx).project().read(cx).fs().clone(); + cx.background_spawn(async move { + for mut path_to_check in fs_paths_to_check { + if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() + && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() + { + path_to_check.path = fs_path_to_check; + return Some(OpenTarget::File(path_to_check, metadata)); + } + } + + worktree_check_task.await + }) +} + +pub(super) fn open_path_like_target( + workspace: &WeakEntity, + terminal_view: &mut TerminalView, + path_like_target: &PathLikeTarget, + window: &mut Window, + cx: &mut Context, +) { + possibly_open_target(workspace, terminal_view, path_like_target, window, cx) + .detach_and_log_err(cx) +} + +fn possibly_open_target( + workspace: &WeakEntity, + terminal_view: &mut TerminalView, + path_like_target: &PathLikeTarget, + window: &mut Window, + cx: &mut Context, +) -> Task>> { + if terminal_view.hover.is_none() { + return Task::ready(Ok(None)); + } + let workspace = workspace.clone(); + let path_like_target = path_like_target.clone(); + cx.spawn_in(window, async move |terminal_view, cx| { + let Some(open_target) = terminal_view + .update(cx, |_, cx| { + possible_open_target(&workspace, &path_like_target, cx) + })? + .await + else { + return Ok(None); + }; + + let path_to_open = open_target.path(); + let opened_items = workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_paths( + vec![path_to_open.path.clone()], + OpenOptions { + visible: Some(OpenVisible::OnlyDirectories), + ..Default::default() + }, + None, + window, + cx, + ) + }) + .context("workspace update")? + .await; + if opened_items.len() != 1 { + debug_panic!( + "Received {} items for one path {path_to_open:?}", + opened_items.len(), + ); + } + + if let Some(opened_item) = opened_items.first() { + if open_target.is_file() { + if let Some(Ok(opened_item)) = opened_item { + if let Some(row) = path_to_open.row { + let col = path_to_open.column.unwrap_or(0); + if let Some(active_editor) = opened_item.downcast::() { + active_editor + .downgrade() + .update_in(cx, |editor, window, cx| { + editor.go_to_singleton_buffer_point( + language::Point::new( + row.saturating_sub(1), + col.saturating_sub(1), + ), + window, + cx, + ) + }) + .log_err(); + } + } + return Ok(Some(open_target)); + } + } else if open_target.is_dir() { + workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |_, cx| { + cx.emit(project::Event::ActivateProjectPanel); + }) + })?; + return Ok(Some(open_target)); + } + } + Ok(None) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::TestAppContext; + use project::{Project, terminals::TerminalKind}; + use serde_json::json; + use std::path::{Path, PathBuf}; + use terminal::{HoveredWord, alacritty_terminal::index::Point as AlacPoint}; + use util::path; + use workspace::AppState; + + async fn init_test( + app_cx: &mut TestAppContext, + trees: impl IntoIterator, + worktree_roots: impl IntoIterator, + ) -> impl AsyncFnMut(HoveredWord, PathLikeTarget) -> (Option, Option) + { + let fs = app_cx.update(AppState::test).fs.as_fake().clone(); + + app_cx.update(|cx| { + terminal::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); + Project::init_settings(cx); + language::init(cx); + editor::init(cx); + }); + + for (path, tree) in trees { + fs.insert_tree(path, tree).await; + } + + let project = Project::test( + fs.clone(), + worktree_roots + .into_iter() + .map(Path::new) + .collect::>(), + app_cx, + ) + .await; + + let (workspace, cx) = + app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let terminal = project + .update(cx, |project, cx| { + project.create_terminal(TerminalKind::Shell(None), cx) + }) + .await + .expect("Failed to create a terminal"); + + let workspace_a = workspace.clone(); + let (terminal_view, cx) = app_cx.add_window_view(|window, cx| { + TerminalView::new( + terminal, + workspace_a.downgrade(), + None, + project.downgrade(), + window, + cx, + ) + }); + + async move |hovered_word: HoveredWord, + path_like_target: PathLikeTarget| + -> (Option, Option) { + let workspace_a = workspace.clone(); + terminal_view + .update(cx, |_, cx| { + hover_path_like_target( + &workspace_a.downgrade(), + hovered_word, + &path_like_target, + cx, + ) + }) + .await; + + let hover_target = + terminal_view.read_with(cx, |terminal_view, _| terminal_view.hover.clone()); + + let open_target = terminal_view + .update_in(cx, |terminal_view, window, cx| { + possibly_open_target( + &workspace.downgrade(), + terminal_view, + &path_like_target, + window, + cx, + ) + }) + .await + .expect("Failed to possibly open target"); + + (hover_target, open_target) + } + } + + async fn test_path_like_simple( + test_path_like: &mut impl AsyncFnMut( + HoveredWord, + PathLikeTarget, + ) -> (Option, Option), + maybe_path: &str, + tooltip: &str, + terminal_dir: Option, + file: &str, + line: u32, + ) { + let (hover_target, open_target) = test_path_like( + HoveredWord { + word: maybe_path.to_string(), + word_match: AlacPoint::default()..=AlacPoint::default(), + id: 0, + }, + PathLikeTarget { + maybe_path: maybe_path.to_string(), + terminal_dir, + }, + ) + .await; + + let Some(hover_target) = hover_target else { + assert!( + hover_target.is_some(), + "Hover target should not be `None` at {file}:{line}:" + ); + return; + }; + + assert_eq!( + hover_target.tooltip, tooltip, + "Tooltip mismatch at {file}:{line}:" + ); + assert_eq!( + hover_target.hovered_word.word, maybe_path, + "Hovered word mismatch at {file}:{line}:" + ); + + let Some(open_target) = open_target else { + assert!( + open_target.is_some(), + "Open target should not be `None` at {file}:{line}:" + ); + return; + }; + + assert_eq!( + open_target.path().path, + Path::new(tooltip), + "Open target path mismatch at {file}:{line}:" + ); + } + + macro_rules! none_or_some { + () => { + None + }; + ($some:expr) => { + Some($some) + }; + } + + macro_rules! test_path_like { + ($test_path_like:expr, $maybe_path:literal, $tooltip:literal $(, $cwd:literal)?) => { + test_path_like_simple( + &mut $test_path_like, + path!($maybe_path), + path!($tooltip), + none_or_some!($($crate::PathBuf::from(path!($cwd)))?), + std::file!(), + std::line!(), + ) + .await + }; + } + + #[doc = "test_path_likes!(, , , { $(;)+ })"] + macro_rules! test_path_likes { + ($cx:expr, $trees:expr, $worktrees:expr, { $($tests:expr;)+ }) => { { + let mut test_path_like = init_test($cx, $trees, $worktrees).await; + #[doc ="test!(, , )"] + macro_rules! test { + ($maybe_path:literal, $tooltip:literal) => { + test_path_like!(test_path_like, $maybe_path, $tooltip) + }; + ($maybe_path:literal, $tooltip:literal, $cwd:literal) => { + test_path_like!(test_path_like, $maybe_path, $tooltip, $cwd) + } + } + $($tests);+ + } } + } + + #[gpui::test] + async fn one_folder_worktree(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/test"), + json!({ + "lib.rs": "", + "test.rs": "", + }), + )], + vec![path!("/test")], + { + test!("lib.rs", "/test/lib.rs"); + test!("test.rs", "/test/test.rs"); + } + ) + } + + #[gpui::test] + async fn mixed_worktrees(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![ + ( + path!("/"), + json!({ + "file.txt": "", + }), + ), + ( + path!("/test"), + json!({ + "lib.rs": "", + "test.rs": "", + "file.txt": "", + }), + ), + ], + vec![path!("/file.txt"), path!("/test")], + { + test!("file.txt", "/file.txt", "/"); + test!("lib.rs", "/test/lib.rs", "/test"); + test!("test.rs", "/test/test.rs", "/test"); + test!("file.txt", "/test/file.txt", "/test"); + } + ) + } + + #[gpui::test] + async fn worktree_file_preferred(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![ + ( + path!("/"), + json!({ + "file.txt": "", + }), + ), + ( + path!("/test"), + json!({ + "file.txt": "", + }), + ), + ], + vec![path!("/test")], + { + test!("file.txt", "/test/file.txt", "/test"); + } + ) + } + + mod issues { + use super::*; + + // https://github.com/zed-industries/zed/issues/28407 + #[gpui::test] + async fn issue_28407_siblings(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/dir1"), + json!({ + "dir 2": { + "C.py": "" + }, + "dir 3": { + "C.py": "" + }, + }), + )], + vec![path!("/dir1")], + { + test!("C.py", "/dir1/dir 2/C.py", "/dir1"); + test!("C.py", "/dir1/dir 2/C.py", "/dir1/dir 2"); + test!("C.py", "/dir1/dir 3/C.py", "/dir1/dir 3"); + } + ) + } + + // https://github.com/zed-industries/zed/issues/28407 + // See https://github.com/zed-industries/zed/issues/34027 + // See https://github.com/zed-industries/zed/issues/33498 + #[gpui::test] + #[should_panic(expected = "Tooltip mismatch")] + async fn issue_28407_nesting(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/project"), + json!({ + "lib": { + "src": { + "main.rs": "" + }, + }, + "src": { + "main.rs": "" + }, + }), + )], + vec![path!("/project")], + { + // Failing currently + test!("main.rs", "/project/src/main.rs", "/project"); + test!("main.rs", "/project/src/main.rs", "/project/src"); + test!("main.rs", "/project/lib/src/main.rs", "/project/lib"); + test!("main.rs", "/project/lib/src/main.rs", "/project/lib/src"); + + test!("src/main.rs", "/project/src/main.rs", "/project"); + test!("src/main.rs", "/project/src/main.rs", "/project/src"); + // Failing currently + test!("src/main.rs", "/project/lib/src/main.rs", "/project/lib"); + // Failing currently + test!( + "src/main.rs", + "/project/lib/src/main.rs", + "/project/lib/src" + ); + + test!("lib/src/main.rs", "/project/lib/src/main.rs", "/project"); + test!( + "lib/src/main.rs", + "/project/lib/src/main.rs", + "/project/src" + ); + test!( + "lib/src/main.rs", + "/project/lib/src/main.rs", + "/project/lib" + ); + test!( + "lib/src/main.rs", + "/project/lib/src/main.rs", + "/project/lib/src" + ); + } + ) + } + + // https://github.com/zed-industries/zed/issues/28339 + #[gpui::test] + async fn issue_28339(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/tmp"), + json!({ + "issue28339": { + "foo": { + "bar.txt": "" + }, + }, + }), + )], + vec![path!("/tmp")], + { + test!( + "foo/./bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339" + ); + test!( + "foo/../foo/bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339" + ); + test!( + "foo/..///foo/bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339" + ); + test!( + "issue28339/../issue28339/foo/../foo/bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339" + ); + test!( + "./bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339/foo" + ); + test!( + "../foo/bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339/foo" + ); + } + ) + } + + // https://github.com/zed-industries/zed/issues/34027 + #[gpui::test] + #[should_panic(expected = "Tooltip mismatch")] + async fn issue_34027(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/tmp/issue34027"), + json!({ + "test.txt": "", + "foo": { + "test.txt": "", + } + }), + ),], + vec![path!("/tmp/issue34027")], + { + test!("test.txt", "/tmp/issue34027/test.txt", "/tmp/issue34027"); + test!( + "test.txt", + "/tmp/issue34027/foo/test.txt", + "/tmp/issue34027/foo" + ); + } + ) + } + + // https://github.com/zed-industries/zed/issues/34027 + #[gpui::test] + #[should_panic(expected = "Tooltip mismatch")] + async fn issue_34027_non_worktree_file(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![ + ( + path!("/"), + json!({ + "file.txt": "", + }), + ), + ( + path!("/test"), + json!({ + "file.txt": "", + }), + ), + ], + vec![path!("/test")], + { + test!("file.txt", "/file.txt", "/"); + test!("file.txt", "/test/file.txt", "/test"); + } + ) + } + } +} diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 5b4d32714097a4c0bb24ebe5c9dcf72acf7f5ebe..e2f9ba818dab3e7d5bd05beb842e53b10c72e231 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -2,21 +2,21 @@ mod color_contrast; mod persistence; pub mod terminal_element; pub mod terminal_panel; +mod terminal_path_like_target; pub mod terminal_scrollbar; mod terminal_slash_command; pub mod terminal_tab_tooltip; use assistant_slash_command::SlashCommandRegistry; -use editor::{Editor, EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide}; +use editor::{EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide}; use gpui::{ Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, ScrollWheelEvent, Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div, }; -use itertools::Itertools; use persistence::TERMINAL_DB; -use project::{Entry, Metadata, Project, search::SearchQuery, terminals::TerminalKind}; +use project::{Project, search::SearchQuery, terminals::TerminalKind}; use schemars::JsonSchema; use task::TaskId; use terminal::{ @@ -31,16 +31,17 @@ use terminal::{ }; use terminal_element::TerminalElement; use terminal_panel::TerminalPanel; +use terminal_path_like_target::{hover_path_like_target, open_path_like_target}; use terminal_scrollbar::TerminalScrollHandle; use terminal_slash_command::TerminalSlashCommand; use terminal_tab_tooltip::TerminalTooltip; use ui::{ ContextMenu, Icon, IconName, Label, Scrollbar, ScrollbarState, Tooltip, h_flex, prelude::*, }; -use util::{ResultExt, debug_panic, paths::PathWithPosition}; +use util::ResultExt; use workspace::{ - CloseActiveItem, NewCenterTerminal, NewTerminal, OpenOptions, OpenVisible, ToolbarItemLocation, - Workspace, WorkspaceId, delete_unloaded_items, + CloseActiveItem, NewCenterTerminal, NewTerminal, ToolbarItemLocation, Workspace, WorkspaceId, + delete_unloaded_items, item::{ BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent, }, @@ -48,7 +49,6 @@ use workspace::{ searchable::{Direction, SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, }; -use anyhow::Context as _; use serde::Deserialize; use settings::{Settings, SettingsStore}; use smol::Timer; @@ -64,7 +64,6 @@ use std::{ }; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); -const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"]; const TERMINAL_SCROLLBAR_WIDTH: Pixels = px(12.); /// Event to transmit the scroll from the element to the view @@ -181,6 +180,7 @@ impl ContentMode { } #[derive(Debug)] +#[cfg_attr(test, derive(Clone, Eq, PartialEq))] struct HoverTarget { tooltip: String, hovered_word: HoveredWord, @@ -1066,37 +1066,13 @@ fn subscribe_for_terminal_events( .as_ref() .map(|hover| &hover.hovered_word) { - let valid_files_to_open_task = possible_open_target( + terminal_view.hover = None; + terminal_view.hover_tooltip_update = hover_path_like_target( &workspace, - &path_like_target.terminal_dir, - &path_like_target.maybe_path, + hovered_word.clone(), + path_like_target, cx, ); - let hovered_word = hovered_word.clone(); - - terminal_view.hover = None; - terminal_view.hover_tooltip_update = - cx.spawn(async move |terminal_view, cx| { - let file_to_open = valid_files_to_open_task.await; - terminal_view - .update(cx, |terminal_view, _| match file_to_open { - Some( - OpenTarget::File(path, _) - | OpenTarget::Worktree(path, _), - ) => { - terminal_view.hover = Some(HoverTarget { - tooltip: path.to_string(|path| { - path.to_string_lossy().to_string() - }), - hovered_word, - }); - } - None => { - terminal_view.hover = None; - } - }) - .ok(); - }); cx.notify(); } } @@ -1110,86 +1086,13 @@ fn subscribe_for_terminal_events( Event::Open(maybe_navigation_target) => match maybe_navigation_target { MaybeNavigationTarget::Url(url) => cx.open_url(url), - - MaybeNavigationTarget::PathLike(path_like_target) => { - if terminal_view.hover.is_none() { - return; - } - let task_workspace = workspace.clone(); - let path_like_target = path_like_target.clone(); - cx.spawn_in(window, async move |terminal_view, cx| { - let open_target = terminal_view - .update(cx, |_, cx| { - possible_open_target( - &task_workspace, - &path_like_target.terminal_dir, - &path_like_target.maybe_path, - cx, - ) - })? - .await; - if let Some(open_target) = open_target { - let path_to_open = open_target.path(); - let opened_items = task_workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path_to_open.path.clone()], - OpenOptions { - visible: Some(OpenVisible::OnlyDirectories), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .context("workspace update")? - .await; - if opened_items.len() != 1 { - debug_panic!( - "Received {} items for one path {path_to_open:?}", - opened_items.len(), - ); - } - - if let Some(opened_item) = opened_items.first() { - if open_target.is_file() { - if let Some(Ok(opened_item)) = opened_item - && let Some(row) = path_to_open.row - { - let col = path_to_open.column.unwrap_or(0); - if let Some(active_editor) = - opened_item.downcast::() - { - active_editor - .downgrade() - .update_in(cx, |editor, window, cx| { - editor.go_to_singleton_buffer_point( - language::Point::new( - row.saturating_sub(1), - col.saturating_sub(1), - ), - window, - cx, - ) - }) - .log_err(); - } - } - } else if open_target.is_dir() { - task_workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |_, cx| { - cx.emit(project::Event::ActivateProjectPanel); - }) - })?; - } - } - } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } + MaybeNavigationTarget::PathLike(path_like_target) => open_path_like_target( + &workspace, + terminal_view, + path_like_target, + window, + cx, + ), }, Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs), Event::CloseTerminal => cx.emit(ItemEvent::CloseItem), @@ -1203,241 +1106,6 @@ fn subscribe_for_terminal_events( vec![terminal_subscription, terminal_events_subscription] } -#[derive(Debug, Clone)] -enum OpenTarget { - Worktree(PathWithPosition, Entry), - File(PathWithPosition, Metadata), -} - -impl OpenTarget { - fn is_file(&self) -> bool { - match self { - OpenTarget::Worktree(_, entry) => entry.is_file(), - OpenTarget::File(_, metadata) => !metadata.is_dir, - } - } - - fn is_dir(&self) -> bool { - match self { - OpenTarget::Worktree(_, entry) => entry.is_dir(), - OpenTarget::File(_, metadata) => metadata.is_dir, - } - } - - fn path(&self) -> &PathWithPosition { - match self { - OpenTarget::Worktree(path, _) => path, - OpenTarget::File(path, _) => path, - } - } -} - -fn possible_open_target( - workspace: &WeakEntity, - cwd: &Option, - maybe_path: &str, - cx: &App, -) -> Task> { - let Some(workspace) = workspace.upgrade() else { - return Task::ready(None); - }; - // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too. - // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away. - let mut potential_paths = Vec::new(); - let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path)); - let path_with_position = PathWithPosition::parse_str(maybe_path); - let worktree_candidates = workspace - .read(cx) - .worktrees(cx) - .sorted_by_key(|worktree| { - let worktree_root = worktree.read(cx).abs_path(); - match cwd - .as_ref() - .and_then(|cwd| worktree_root.strip_prefix(cwd).ok()) - { - Some(cwd_child) => cwd_child.components().count(), - None => usize::MAX, - } - }) - .collect::>(); - // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it. - for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) { - if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() { - potential_paths.push(PathWithPosition { - path: stripped.to_owned(), - row: original_path.row, - column: original_path.column, - }); - } - if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() { - potential_paths.push(PathWithPosition { - path: stripped.to_owned(), - row: path_with_position.row, - column: path_with_position.column, - }); - } - } - - let insert_both_paths = original_path != path_with_position; - potential_paths.insert(0, original_path); - if insert_both_paths { - potential_paths.insert(1, path_with_position); - } - - // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix. - // That will be slow, though, so do the fast checks first. - let mut worktree_paths_to_check = Vec::new(); - for worktree in &worktree_candidates { - let worktree_root = worktree.read(cx).abs_path(); - let mut paths_to_check = Vec::with_capacity(potential_paths.len()); - - for path_with_position in &potential_paths { - let path_to_check = if worktree_root.ends_with(&path_with_position.path) { - let root_path_with_position = PathWithPosition { - path: worktree_root.to_path_buf(), - row: path_with_position.row, - column: path_with_position.column, - }; - match worktree.read(cx).root_entry() { - Some(root_entry) => { - return Task::ready(Some(OpenTarget::Worktree( - root_path_with_position, - root_entry.clone(), - ))); - } - None => root_path_with_position, - } - } else { - PathWithPosition { - path: path_with_position - .path - .strip_prefix(&worktree_root) - .unwrap_or(&path_with_position.path) - .to_owned(), - row: path_with_position.row, - column: path_with_position.column, - } - }; - - if path_to_check.path.is_relative() - && let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) - { - return Task::ready(Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree_root.join(&entry.path), - row: path_to_check.row, - column: path_to_check.column, - }, - entry.clone(), - ))); - } - - paths_to_check.push(path_to_check); - } - - if !paths_to_check.is_empty() { - worktree_paths_to_check.push((worktree.clone(), paths_to_check)); - } - } - - // Before entire worktree traversal(s), make an attempt to do FS checks if available. - let fs_paths_to_check = if workspace.read(cx).project().read(cx).is_local() { - potential_paths - .into_iter() - .flat_map(|path_to_check| { - let mut paths_to_check = Vec::new(); - let maybe_path = &path_to_check.path; - if maybe_path.starts_with("~") { - if let Some(home_path) = - maybe_path - .strip_prefix("~") - .ok() - .and_then(|stripped_maybe_path| { - Some(dirs::home_dir()?.join(stripped_maybe_path)) - }) - { - paths_to_check.push(PathWithPosition { - path: home_path, - row: path_to_check.row, - column: path_to_check.column, - }); - } - } else { - paths_to_check.push(PathWithPosition { - path: maybe_path.clone(), - row: path_to_check.row, - column: path_to_check.column, - }); - if maybe_path.is_relative() { - if let Some(cwd) = &cwd { - paths_to_check.push(PathWithPosition { - path: cwd.join(maybe_path), - row: path_to_check.row, - column: path_to_check.column, - }); - } - for worktree in &worktree_candidates { - paths_to_check.push(PathWithPosition { - path: worktree.read(cx).abs_path().join(maybe_path), - row: path_to_check.row, - column: path_to_check.column, - }); - } - } - } - paths_to_check - }) - .collect() - } else { - Vec::new() - }; - - let worktree_check_task = cx.spawn(async move |cx| { - for (worktree, worktree_paths_to_check) in worktree_paths_to_check { - let found_entry = worktree - .update(cx, |worktree, _| { - let worktree_root = worktree.abs_path(); - let traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); - for entry in traversal { - if let Some(path_in_worktree) = worktree_paths_to_check - .iter() - .find(|path_to_check| entry.path.ends_with(&path_to_check.path)) - { - return Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree_root.join(&entry.path), - row: path_in_worktree.row, - column: path_in_worktree.column, - }, - entry.clone(), - )); - } - } - None - }) - .ok()?; - if let Some(found_entry) = found_entry { - return Some(found_entry); - } - } - None - }); - - let fs = workspace.read(cx).project().read(cx).fs().clone(); - cx.background_spawn(async move { - for mut path_to_check in fs_paths_to_check { - if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() - && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() - { - path_to_check.path = fs_path_to_check; - return Some(OpenTarget::File(path_to_check, metadata)); - } - } - - worktree_check_task.await - }) -} - fn regex_search_for_query(query: &project::search::SearchQuery) -> Option { let str = query.as_str(); if query.is_regex() { From 33e05f15b254b9d25aa0ddb03cdcc5a191afb7d7 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 21 Aug 2025 20:50:06 +0200 Subject: [PATCH 250/744] collab_ui: Fix channel text bleeding through buttons on hover (#36710) Release Notes: - N/A --- crates/collab_ui/src/collab_panel.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index cd37549783118910048d1a9bc85adcd0e593209b..d85a6610a5b2fadde46f27be2602f62c6b8b7d62 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2905,6 +2905,8 @@ impl CollabPanel { h_flex().absolute().right(rems(0.)).h_full().child( h_flex() .h_full() + .bg(cx.theme().colors().background) + .rounded_l_sm() .gap_1() .px_1() .child( @@ -2920,8 +2922,7 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, window, cx| { this.join_channel_chat(channel_id, window, cx) })) - .tooltip(Tooltip::text("Open channel chat")) - .visible_on_hover(""), + .tooltip(Tooltip::text("Open channel chat")), ) .child( IconButton::new("channel_notes", IconName::Reader) @@ -2936,9 +2937,9 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, window, cx| { this.open_channel_notes(channel_id, window, cx) })) - .tooltip(Tooltip::text("Open channel notes")) - .visible_on_hover(""), - ), + .tooltip(Tooltip::text("Open channel notes")), + ) + .visible_on_hover(""), ), ) .tooltip({ From d0583ede48fb1918da41beca68dd3aacd7174cb6 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 21 Aug 2025 12:06:27 -0700 Subject: [PATCH 251/744] acp: Move ignored integration tests behind e2e flag (#36711) Release Notes: - N/A --- crates/agent2/src/tests/mod.rs | 44 +++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 3bd1be497ef3edcbbcf0429e75c956e70cba0770..edba227da77c7ad0cd15c7c4c213b5a1f8d3f421 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -32,17 +32,22 @@ mod test_tools; use test_tools::*; #[gpui::test] -#[ignore = "can't run on CI yet"] async fn test_echo(cx: &mut TestAppContext) { - let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); let events = thread .update(cx, |thread, cx| { thread.send(UserMessageId::new(), ["Testing: Reply with 'Hello'"], cx) }) - .unwrap() - .collect() - .await; + .unwrap(); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hello"); + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); + fake_model.end_last_completion_stream(); + + let events = events.collect().await; thread.update(cx, |thread, _cx| { assert_eq!( thread.last_message().unwrap().to_markdown(), @@ -57,9 +62,9 @@ async fn test_echo(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "can't run on CI yet"] async fn test_thinking(cx: &mut TestAppContext) { - let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4Thinking).await; + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); let events = thread .update(cx, |thread, cx| { @@ -74,9 +79,18 @@ async fn test_thinking(cx: &mut TestAppContext) { cx, ) }) - .unwrap() - .collect() - .await; + .unwrap(); + cx.run_until_parked(); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Thinking { + text: "Think".to_string(), + signature: None, + }); + fake_model.send_last_completion_stream_text_chunk("Hello"); + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); + fake_model.end_last_completion_stream(); + + let events = events.collect().await; thread.update(cx, |thread, _cx| { assert_eq!( thread.last_message().unwrap().to_markdown(), @@ -271,7 +285,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "can't run on CI yet"] +#[cfg_attr(not(feature = "e2e"), ignore)] async fn test_basic_tool_calls(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -331,7 +345,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "can't run on CI yet"] +#[cfg_attr(not(feature = "e2e"), ignore)] async fn test_streaming_tool_calls(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -794,7 +808,7 @@ async fn next_tool_call_authorization( } #[gpui::test] -#[ignore = "can't run on CI yet"] +#[cfg_attr(not(feature = "e2e"), ignore)] async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -919,7 +933,7 @@ async fn test_profiles(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "can't run on CI yet"] +#[cfg_attr(not(feature = "e2e"), ignore)] async fn test_cancellation(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -1797,7 +1811,6 @@ struct ThreadTest { enum TestModel { Sonnet4, - Sonnet4Thinking, Fake, } @@ -1805,7 +1818,6 @@ impl TestModel { fn id(&self) -> LanguageModelId { match self { TestModel::Sonnet4 => LanguageModelId("claude-sonnet-4-latest".into()), - TestModel::Sonnet4Thinking => LanguageModelId("claude-sonnet-4-thinking-latest".into()), TestModel::Fake => unreachable!(), } } From 725ed5dd01f18d6b2994435152d7fad37ed9765b Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 21 Aug 2025 16:56:15 -0300 Subject: [PATCH 252/744] acp: Hide loading diff animation for external agents and update in place (#36699) The loading diff animation can be jarring for external agents because they stream the diff at the same time the tool call is pushed, so it's only displayed while we're asynchronously calculating the diff. We'll now only show it for the native agent. Also, we'll now only update the diff when it changes, which avoids unnecessarily hiding it for a few frames. Release Notes: - N/A Co-authored-by: Bennet Bo Fenner --- crates/acp_thread/src/acp_thread.rs | 41 ++++++++++++++++-- crates/acp_thread/src/diff.rs | 59 +++++++++++++++++++------- crates/agent_ui/src/acp/thread_view.rs | 6 ++- 3 files changed, 85 insertions(+), 21 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 61bc50576abd8a53433d5738acb98f245ec92764..a45787f0393818abb5c9b93a65e8a28448b09817 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -238,10 +238,21 @@ impl ToolCall { } if let Some(content) = content { - self.content = content - .into_iter() - .map(|chunk| ToolCallContent::from_acp(chunk, language_registry.clone(), cx)) - .collect(); + let new_content_len = content.len(); + let mut content = content.into_iter(); + + // Reuse existing content if we can + for (old, new) in self.content.iter_mut().zip(content.by_ref()) { + old.update_from_acp(new, language_registry.clone(), cx); + } + for new in content { + self.content.push(ToolCallContent::from_acp( + new, + language_registry.clone(), + cx, + )) + } + self.content.truncate(new_content_len); } if let Some(locations) = locations { @@ -551,6 +562,28 @@ impl ToolCallContent { } } + pub fn update_from_acp( + &mut self, + new: acp::ToolCallContent, + language_registry: Arc, + cx: &mut App, + ) { + let needs_update = match (&self, &new) { + (Self::Diff(old_diff), acp::ToolCallContent::Diff { diff: new_diff }) => { + old_diff.read(cx).needs_update( + new_diff.old_text.as_deref().unwrap_or(""), + &new_diff.new_text, + cx, + ) + } + _ => true, + }; + + if needs_update { + *self = Self::from_acp(new, language_registry, cx); + } + } + pub fn to_markdown(&self, cx: &App) -> String { match self { Self::ContentBlock(content) => content.to_markdown(cx).to_string(), diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index 130bc3ab6bced76320c80e49aef5bdb555c54e7e..59f907dcc42e076060a6f50a2573aa3a0f10f382 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -28,10 +28,12 @@ impl Diff { cx: &mut Context, ) -> Self { let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly)); - let buffer = cx.new(|cx| Buffer::local(new_text, cx)); + let new_buffer = cx.new(|cx| Buffer::local(new_text, cx)); + let base_text = old_text.clone().unwrap_or(String::new()).into(); let task = cx.spawn({ let multibuffer = multibuffer.clone(); let path = path.clone(); + let buffer = new_buffer.clone(); async move |_, cx| { let language = language_registry .language_for_file_path(&path) @@ -76,6 +78,8 @@ impl Diff { Self::Finalized(FinalizedDiff { multibuffer, path, + base_text, + new_buffer, _update_diff: task, }) } @@ -119,7 +123,7 @@ impl Diff { diff.update(cx); } }), - buffer, + new_buffer: buffer, diff: buffer_diff, revealed_ranges: Vec::new(), update_diff: Task::ready(Ok(())), @@ -154,9 +158,9 @@ impl Diff { .map(|buffer| buffer.read(cx).text()) .join("\n"); let path = match self { - Diff::Pending(PendingDiff { buffer, .. }) => { - buffer.read(cx).file().map(|file| file.path().as_ref()) - } + Diff::Pending(PendingDiff { + new_buffer: buffer, .. + }) => buffer.read(cx).file().map(|file| file.path().as_ref()), Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()), }; format!( @@ -169,12 +173,33 @@ impl Diff { pub fn has_revealed_range(&self, cx: &App) -> bool { self.multibuffer().read(cx).excerpt_paths().next().is_some() } + + pub fn needs_update(&self, old_text: &str, new_text: &str, cx: &App) -> bool { + match self { + Diff::Pending(PendingDiff { + base_text, + new_buffer, + .. + }) => { + base_text.as_str() != old_text + || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text) + } + Diff::Finalized(FinalizedDiff { + base_text, + new_buffer, + .. + }) => { + base_text.as_str() != old_text + || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text) + } + } + } } pub struct PendingDiff { multibuffer: Entity, base_text: Arc, - buffer: Entity, + new_buffer: Entity, diff: Entity, revealed_ranges: Vec>, _subscription: Subscription, @@ -183,7 +208,7 @@ pub struct PendingDiff { impl PendingDiff { pub fn update(&mut self, cx: &mut Context) { - let buffer = self.buffer.clone(); + let buffer = self.new_buffer.clone(); let buffer_diff = self.diff.clone(); let base_text = self.base_text.clone(); self.update_diff = cx.spawn(async move |diff, cx| { @@ -221,10 +246,10 @@ impl PendingDiff { fn finalize(&self, cx: &mut Context) -> FinalizedDiff { let ranges = self.excerpt_ranges(cx); let base_text = self.base_text.clone(); - let language_registry = self.buffer.read(cx).language_registry(); + let language_registry = self.new_buffer.read(cx).language_registry(); let path = self - .buffer + .new_buffer .read(cx) .file() .map(|file| file.path().as_ref()) @@ -233,12 +258,12 @@ impl PendingDiff { // Replace the buffer in the multibuffer with the snapshot let buffer = cx.new(|cx| { - let language = self.buffer.read(cx).language().cloned(); + let language = self.new_buffer.read(cx).language().cloned(); let buffer = TextBuffer::new_normalized( 0, cx.entity_id().as_non_zero_u64().into(), - self.buffer.read(cx).line_ending(), - self.buffer.read(cx).as_rope().clone(), + self.new_buffer.read(cx).line_ending(), + self.new_buffer.read(cx).as_rope().clone(), ); let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite); buffer.set_language(language, cx); @@ -274,7 +299,9 @@ impl PendingDiff { FinalizedDiff { path, + base_text: self.base_text.clone(), multibuffer: self.multibuffer.clone(), + new_buffer: self.new_buffer.clone(), _update_diff: update_diff, } } @@ -283,8 +310,8 @@ impl PendingDiff { let ranges = self.excerpt_ranges(cx); self.multibuffer.update(cx, |multibuffer, cx| { multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&self.buffer, cx), - self.buffer.clone(), + PathKey::for_buffer(&self.new_buffer, cx), + self.new_buffer.clone(), ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, @@ -296,7 +323,7 @@ impl PendingDiff { } fn excerpt_ranges(&self, cx: &App) -> Vec> { - let buffer = self.buffer.read(cx); + let buffer = self.new_buffer.read(cx); let diff = self.diff.read(cx); let mut ranges = diff .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) @@ -330,6 +357,8 @@ impl PendingDiff { pub struct FinalizedDiff { path: PathBuf, + base_text: Arc, + new_buffer: Entity, multibuffer: Entity, _update_diff: Task>, } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 7e330b7e6f0f894542e7b8f1d52841b093b8715e..a15f764375ac3b004d70f0a8297de01239def829 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1625,7 +1625,9 @@ impl AcpThreadView { .into_any() } ToolCallStatus::Pending | ToolCallStatus::InProgress - if is_edit && tool_call.content.is_empty() => + if is_edit + && tool_call.content.is_empty() + && self.as_native_connection(cx).is_some() => { self.render_diff_loading(cx).into_any() } @@ -1981,7 +1983,7 @@ impl AcpThreadView { && diff.read(cx).has_revealed_range(cx) { editor.into_any_element() - } else if tool_progress { + } else if tool_progress && self.as_native_connection(cx).is_some() { self.render_diff_loading(cx) } else { Empty.into_any() From 2234f91b7b335f43105a3f323b94db85c11eb126 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 21 Aug 2025 16:56:40 -0300 Subject: [PATCH 253/744] acp: Remove invalid creases on edit (#36708) Release Notes: - N/A Co-authored-by: Bennet Bo Fenner --- crates/agent_ui/src/acp/message_editor.rs | 54 +++++++++++++---------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index dc31c5fe106d4bb16d5d710710b1db39a1a62631..8f5044cb21a7e5cd9f4580ede8b6ddaa2cb51300 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -11,7 +11,7 @@ use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, - EditorEvent, EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, + EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, SemanticsProvider, ToOffset, actions::Paste, display_map::{Crease, CreaseId, FoldId}, @@ -140,11 +140,11 @@ impl MessageEditor { .detach(); let mut subscriptions = Vec::new(); - if prevent_slash_commands { - subscriptions.push(cx.subscribe_in(&editor, window, { - let semantics_provider = semantics_provider.clone(); - move |this, editor, event, window, cx| { - if let EditorEvent::Edited { .. } = event { + subscriptions.push(cx.subscribe_in(&editor, window, { + let semantics_provider = semantics_provider.clone(); + move |this, editor, event, window, cx| { + if let EditorEvent::Edited { .. } = event { + if prevent_slash_commands { this.highlight_slash_command( semantics_provider.clone(), editor.clone(), @@ -152,9 +152,12 @@ impl MessageEditor { cx, ); } + let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx)); + this.mention_set.remove_invalid(snapshot); + cx.notify(); } - })); - } + } + })); Self { editor, @@ -730,11 +733,6 @@ impl MessageEditor { editor.display_map.update(cx, |map, cx| { let snapshot = map.snapshot(cx); for (crease_id, crease) in snapshot.crease_snapshot.creases() { - // Skip creases that have been edited out of the message buffer. - if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { - continue; - } - let Some(mention) = contents.get(&crease_id) else { continue; }; @@ -1482,17 +1480,6 @@ impl MentionSet { self.text_thread_summaries.insert(path, task); } - pub fn drain(&mut self) -> impl Iterator { - self.fetch_results.clear(); - self.thread_summaries.clear(); - self.text_thread_summaries.clear(); - self.directories.clear(); - self.uri_by_crease_id - .drain() - .map(|(id, _)| id) - .chain(self.images.drain().map(|(id, _)| id)) - } - pub fn contents( &self, project: &Entity, @@ -1703,6 +1690,25 @@ impl MentionSet { anyhow::Ok(contents) }) } + + pub fn drain(&mut self) -> impl Iterator { + self.fetch_results.clear(); + self.thread_summaries.clear(); + self.text_thread_summaries.clear(); + self.directories.clear(); + self.uri_by_crease_id + .drain() + .map(|(id, _)| id) + .chain(self.images.drain().map(|(id, _)| id)) + } + + pub fn remove_invalid(&mut self, snapshot: EditorSnapshot) { + for (crease_id, crease) in snapshot.crease_snapshot.creases() { + if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { + self.uri_by_crease_id.remove(&crease_id); + } + } + } } struct SlashCommandSemanticsProvider { From 555692fac6b8e2002296f661ebea8ac50cd42a87 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:05:29 -0300 Subject: [PATCH 254/744] thread view: Add improvements to the UI (#36680) Release Notes: - N/A --- crates/agent_servers/src/claude.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 108 +++++----- crates/ui/src/components/disclosure.rs | 14 +- crates/ui/src/components/label.rs | 2 + .../ui/src/components/label/spinner_label.rs | 192 ++++++++++++++++++ 5 files changed, 269 insertions(+), 49 deletions(-) create mode 100644 crates/ui/src/components/label/spinner_label.rs diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index ef666974f1e6a29345e469d8cf19fe13a3fd02eb..d6ccabb1304a17cf996158edcd2367adbaab46d2 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -44,7 +44,7 @@ pub struct ClaudeCode; impl AgentServer for ClaudeCode { fn name(&self) -> &'static str { - "Claude Code" + "Welcome to Claude Code" } fn empty_state_headline(&self) -> &'static str { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index a15f764375ac3b004d70f0a8297de01239def829..05d31051b2e1a4abd3dc8963ce6267a2a349562c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -41,7 +41,7 @@ use text::Anchor; use theme::ThemeSettings; use ui::{ Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, - Scrollbar, ScrollbarState, Tooltip, prelude::*, + Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -1205,7 +1205,7 @@ impl AcpThreadView { div() .py_3() .px_2() - .rounded_lg() + .rounded_md() .shadow_md() .bg(cx.theme().colors().editor_background) .border_1() @@ -1263,7 +1263,7 @@ impl AcpThreadView { .into_any() } AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { - let style = default_markdown_style(false, window, cx); + let style = default_markdown_style(false, false, window, cx); let message_body = v_flex() .w_full() .gap_2p5() @@ -1398,8 +1398,6 @@ impl AcpThreadView { .relative() .w_full() .gap_1p5() - .opacity(0.8) - .hover(|style| style.opacity(1.)) .child( h_flex() .size_4() @@ -1440,6 +1438,7 @@ impl AcpThreadView { .child( div() .text_size(self.tool_name_font_size()) + .text_color(cx.theme().colors().text_muted) .child("Thinking"), ) .on_click(cx.listener({ @@ -1463,9 +1462,10 @@ impl AcpThreadView { .border_l_1() .border_color(self.tool_card_border_color(cx)) .text_ui_sm(cx) - .child( - self.render_markdown(chunk, default_markdown_style(false, window, cx)), - ), + .child(self.render_markdown( + chunk, + default_markdown_style(false, false, window, cx), + )), ) }) .into_any_element() @@ -1555,11 +1555,11 @@ impl AcpThreadView { | ToolCallStatus::Completed => None, ToolCallStatus::InProgress => Some( Icon::new(IconName::ArrowCircle) - .color(Color::Accent) + .color(Color::Muted) .size(IconSize::Small) .with_animation( "running", - Animation::new(Duration::from_secs(2)).repeat(), + Animation::new(Duration::from_secs(3)).repeat(), |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), ) .into_any(), @@ -1572,6 +1572,10 @@ impl AcpThreadView { ), }; + let failed_tool_call = matches!( + tool_call.status, + ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed + ); let needs_confirmation = matches!( tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } @@ -1652,7 +1656,7 @@ impl AcpThreadView { v_flex() .when(use_card_layout, |this| { - this.rounded_lg() + this.rounded_md() .border_1() .border_color(self.tool_card_border_color(cx)) .bg(cx.theme().colors().editor_background) @@ -1664,20 +1668,16 @@ impl AcpThreadView { .w_full() .gap_1() .justify_between() - .map(|this| { - if use_card_layout { - this.pl_2() - .pr_1p5() - .py_1() - .rounded_t_md() - .when(is_open, |this| { - this.border_b_1() - .border_color(self.tool_card_border_color(cx)) - }) - .bg(self.tool_card_header_bg(cx)) - } else { - this.opacity(0.8).hover(|style| style.opacity(1.)) - } + .when(use_card_layout, |this| { + this.pl_2() + .pr_1p5() + .py_1() + .rounded_t_md() + .when(is_open && !failed_tool_call, |this| { + this.border_b_1() + .border_color(self.tool_card_border_color(cx)) + }) + .bg(self.tool_card_header_bg(cx)) }) .child( h_flex() @@ -1709,13 +1709,15 @@ impl AcpThreadView { .px_1p5() .rounded_sm() .overflow_x_scroll() - .opacity(0.8) .hover(|label| { - label.opacity(1.).bg(cx - .theme() - .colors() - .element_hover - .opacity(0.5)) + label.bg(cx.theme().colors().element_hover.opacity(0.5)) + }) + .map(|this| { + if use_card_layout { + this.text_color(cx.theme().colors().text) + } else { + this.text_color(cx.theme().colors().text_muted) + } }) .child(name) .tooltip(Tooltip::text("Jump to File")) @@ -1738,7 +1740,7 @@ impl AcpThreadView { .overflow_x_scroll() .child(self.render_markdown( tool_call.label.clone(), - default_markdown_style(false, window, cx), + default_markdown_style(false, true, window, cx), )), ) .child(gradient_overlay(gradient_color)) @@ -1804,9 +1806,9 @@ impl AcpThreadView { .border_color(self.tool_card_border_color(cx)) .text_sm() .text_color(cx.theme().colors().text_muted) - .child(self.render_markdown(markdown, default_markdown_style(false, window, cx))) + .child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx))) .child( - Button::new(button_id, "Collapse Output") + Button::new(button_id, "Collapse") .full_width() .style(ButtonStyle::Outlined) .label_size(LabelSize::Small) @@ -2131,7 +2133,7 @@ impl AcpThreadView { .to_string() } else { format!( - "Output is {} long—to avoid unexpected token usage, \ + "Output is {} long, and to avoid unexpected token usage, \ only 16 KB was sent back to the model.", format_file_size(output.original_content_len as u64, true), ) @@ -2199,7 +2201,7 @@ impl AcpThreadView { .border_1() .when(tool_failed || command_failed, |card| card.border_dashed()) .border_color(border_color) - .rounded_lg() + .rounded_md() .overflow_hidden() .child( v_flex() @@ -2553,9 +2555,10 @@ impl AcpThreadView { .into_any(), ) .children(description.map(|desc| { - div().text_ui(cx).text_center().child( - self.render_markdown(desc.clone(), default_markdown_style(false, window, cx)), - ) + div().text_ui(cx).text_center().child(self.render_markdown( + desc.clone(), + default_markdown_style(false, false, window, cx), + )) })) .children( configuration_view @@ -3379,7 +3382,7 @@ impl AcpThreadView { "used-tokens-label", Animation::new(Duration::from_secs(2)) .repeat() - .with_easing(pulsating_between(0.6, 1.)), + .with_easing(pulsating_between(0.3, 0.8)), |label, delta| label.alpha(delta), ) .into_any() @@ -4636,9 +4639,9 @@ impl Render for AcpThreadView { ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => None, ThreadStatus::Generating => div() - .px_5() .py_2() - .child(LoadingLabel::new("").size(LabelSize::Small)) + .px(rems_from_px(22.)) + .child(SpinnerLabel::new().size(LabelSize::Small)) .into(), }, ) @@ -4671,7 +4674,12 @@ impl Render for AcpThreadView { } } -fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle { +fn default_markdown_style( + buffer_font: bool, + muted_text: bool, + window: &Window, + cx: &App, +) -> MarkdownStyle { let theme_settings = ThemeSettings::get_global(cx); let colors = cx.theme().colors(); @@ -4692,20 +4700,26 @@ fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> Markd TextSize::Default.rems(cx) }; + let text_color = if muted_text { + colors.text_muted + } else { + colors.text + }; + text_style.refine(&TextStyleRefinement { font_family: Some(font_family), font_fallbacks: theme_settings.ui_font.fallbacks.clone(), font_features: Some(theme_settings.ui_font.features.clone()), font_size: Some(font_size.into()), line_height: Some(line_height.into()), - color: Some(cx.theme().colors().text), + color: Some(text_color), ..Default::default() }); MarkdownStyle { base_text_style: text_style.clone(), syntax: cx.theme().syntax().clone(), - selection_background_color: cx.theme().colors().element_selection_background, + selection_background_color: colors.element_selection_background, code_block_overflow_x_scroll: true, table_overflow_x_scroll: true, heading_level_styles: Some(HeadingLevelStyles { @@ -4791,7 +4805,7 @@ fn plan_label_markdown_style( window: &Window, cx: &App, ) -> MarkdownStyle { - let default_md_style = default_markdown_style(false, window, cx); + let default_md_style = default_markdown_style(false, false, window, cx); MarkdownStyle { base_text_style: TextStyle { @@ -4811,7 +4825,7 @@ fn plan_label_markdown_style( } fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { - let default_md_style = default_markdown_style(true, window, cx); + let default_md_style = default_markdown_style(true, false, window, cx); MarkdownStyle { base_text_style: TextStyle { diff --git a/crates/ui/src/components/disclosure.rs b/crates/ui/src/components/disclosure.rs index 98406cd1e278b1028587535dc47105ff5d634cf7..4bb3419176eb074c85cc7837d23a10816ce81596 100644 --- a/crates/ui/src/components/disclosure.rs +++ b/crates/ui/src/components/disclosure.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use gpui::{ClickEvent, CursorStyle}; +use gpui::{ClickEvent, CursorStyle, SharedString}; use crate::{Color, IconButton, IconButtonShape, IconName, IconSize, prelude::*}; @@ -14,6 +14,7 @@ pub struct Disclosure { cursor_style: CursorStyle, opened_icon: IconName, closed_icon: IconName, + visible_on_hover: Option, } impl Disclosure { @@ -27,6 +28,7 @@ impl Disclosure { cursor_style: CursorStyle::PointingHand, opened_icon: IconName::ChevronDown, closed_icon: IconName::ChevronRight, + visible_on_hover: None, } } @@ -73,6 +75,13 @@ impl Clickable for Disclosure { } } +impl VisibleOnHover for Disclosure { + fn visible_on_hover(mut self, group_name: impl Into) -> Self { + self.visible_on_hover = Some(group_name.into()); + self + } +} + impl RenderOnce for Disclosure { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { IconButton::new( @@ -87,6 +96,9 @@ impl RenderOnce for Disclosure { .icon_size(IconSize::Small) .disabled(self.disabled) .toggle_state(self.selected) + .when_some(self.visible_on_hover.clone(), |this, group_name| { + this.visible_on_hover(group_name) + }) .when_some(self.on_toggle, move |this, on_toggle| { this.on_click(move |event, window, cx| on_toggle(event, window, cx)) }) diff --git a/crates/ui/src/components/label.rs b/crates/ui/src/components/label.rs index 8c9ea6242472ca3fa9bde88f0dc1e4271063f933..dc830559cad9cd2adcde7a44323482a9933d194b 100644 --- a/crates/ui/src/components/label.rs +++ b/crates/ui/src/components/label.rs @@ -2,8 +2,10 @@ mod highlighted_label; mod label; mod label_like; mod loading_label; +mod spinner_label; pub use highlighted_label::*; pub use label::*; pub use label_like::*; pub use loading_label::*; +pub use spinner_label::*; diff --git a/crates/ui/src/components/label/spinner_label.rs b/crates/ui/src/components/label/spinner_label.rs new file mode 100644 index 0000000000000000000000000000000000000000..b7b65fbcc98c175e4407d72c3e07df236364f552 --- /dev/null +++ b/crates/ui/src/components/label/spinner_label.rs @@ -0,0 +1,192 @@ +use crate::prelude::*; +use gpui::{Animation, AnimationExt, FontWeight}; +use std::time::Duration; + +/// Different types of spinner animations +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub enum SpinnerVariant { + #[default] + Dots, + DotsVariant, +} + +/// A spinner indication, based on the label component, that loops through +/// frames of the specified animation. It implements `LabelCommon` as well. +/// +/// # Default Example +/// +/// ``` +/// use ui::{SpinnerLabel}; +/// +/// SpinnerLabel::new(); +/// ``` +/// +/// # Variant Example +/// +/// ``` +/// use ui::{SpinnerLabel}; +/// +/// SpinnerLabel::dots_variant(); +/// ``` +#[derive(IntoElement, RegisterComponent)] +pub struct SpinnerLabel { + base: Label, + variant: SpinnerVariant, + frames: Vec<&'static str>, + duration: Duration, +} + +impl SpinnerVariant { + fn frames(&self) -> Vec<&'static str> { + match self { + SpinnerVariant::Dots => vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], + SpinnerVariant::DotsVariant => vec!["⣼", "⣹", "⢻", "⠿", "⡟", "⣏", "⣧", "⣶"], + } + } + + fn duration(&self) -> Duration { + match self { + SpinnerVariant::Dots => Duration::from_millis(1000), + SpinnerVariant::DotsVariant => Duration::from_millis(1000), + } + } + + fn animation_id(&self) -> &'static str { + match self { + SpinnerVariant::Dots => "spinner_label_dots", + SpinnerVariant::DotsVariant => "spinner_label_dots_variant", + } + } +} + +impl SpinnerLabel { + pub fn new() -> Self { + Self::with_variant(SpinnerVariant::default()) + } + + pub fn with_variant(variant: SpinnerVariant) -> Self { + let frames = variant.frames(); + let duration = variant.duration(); + + SpinnerLabel { + base: Label::new(frames[0]), + variant, + frames, + duration, + } + } + + pub fn dots() -> Self { + Self::with_variant(SpinnerVariant::Dots) + } + + pub fn dots_variant() -> Self { + Self::with_variant(SpinnerVariant::DotsVariant) + } +} + +impl LabelCommon for SpinnerLabel { + fn size(mut self, size: LabelSize) -> Self { + self.base = self.base.size(size); + self + } + + fn weight(mut self, weight: FontWeight) -> Self { + self.base = self.base.weight(weight); + self + } + + fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { + self.base = self.base.line_height_style(line_height_style); + self + } + + fn color(mut self, color: Color) -> Self { + self.base = self.base.color(color); + self + } + + fn strikethrough(mut self) -> Self { + self.base = self.base.strikethrough(); + self + } + + fn italic(mut self) -> Self { + self.base = self.base.italic(); + self + } + + fn alpha(mut self, alpha: f32) -> Self { + self.base = self.base.alpha(alpha); + self + } + + fn underline(mut self) -> Self { + self.base = self.base.underline(); + self + } + + fn truncate(mut self) -> Self { + self.base = self.base.truncate(); + self + } + + fn single_line(mut self) -> Self { + self.base = self.base.single_line(); + self + } + + fn buffer_font(mut self, cx: &App) -> Self { + self.base = self.base.buffer_font(cx); + self + } + + fn inline_code(mut self, cx: &App) -> Self { + self.base = self.base.inline_code(cx); + self + } +} + +impl RenderOnce for SpinnerLabel { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let frames = self.frames.clone(); + let duration = self.duration; + + self.base.color(Color::Muted).with_animation( + self.variant.animation_id(), + Animation::new(duration).repeat(), + move |mut label, delta| { + let frame_index = (delta * frames.len() as f32) as usize % frames.len(); + + label.set_text(frames[frame_index]); + label + }, + ) + } +} + +impl Component for SpinnerLabel { + fn scope() -> ComponentScope { + ComponentScope::Loading + } + + fn name() -> &'static str { + "Spinner Label" + } + + fn sort_name() -> &'static str { + "Spinner Label" + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option { + let examples = vec![ + single_example("Default", SpinnerLabel::new().into_any_element()), + single_example( + "Dots Variant", + SpinnerLabel::dots_variant().into_any_element(), + ), + ]; + + Some(example_group(examples).vertical().into_any_element()) + } +} From 731b5d0def52d39a2a4fa6a31b9e21160d71fb13 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Aug 2025 22:24:13 +0200 Subject: [PATCH 255/744] acp: Allow editing of thread titles in agent2 (#36706) Release Notes: - N/A --------- Co-authored-by: Richard Feldman --- crates/acp_thread/src/acp_thread.rs | 36 +++++---- crates/acp_thread/src/connection.rs | 28 +++++-- crates/agent2/src/agent.rs | 71 ++++++++++++---- crates/agent2/src/tests/mod.rs | 1 + crates/agent2/src/thread.rs | 107 +++++++++++++------------ crates/agent_ui/src/acp/thread_view.rs | 83 ++++++++++++++++--- crates/agent_ui/src/agent_panel.rs | 31 ++++++- 7 files changed, 253 insertions(+), 104 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index a45787f0393818abb5c9b93a65e8a28448b09817..c748f2227577d37db674bd84748c8a5398da1f42 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1020,10 +1020,19 @@ impl AcpThread { cx.emit(AcpThreadEvent::NewEntry); } - pub fn update_title(&mut self, title: SharedString, cx: &mut Context) -> Result<()> { - self.title = title; - cx.emit(AcpThreadEvent::TitleUpdated); - Ok(()) + pub fn can_set_title(&mut self, cx: &mut Context) -> bool { + self.connection.set_title(&self.session_id, cx).is_some() + } + + pub fn set_title(&mut self, title: SharedString, cx: &mut Context) -> Task> { + if title != self.title { + self.title = title.clone(); + cx.emit(AcpThreadEvent::TitleUpdated); + if let Some(set_title) = self.connection.set_title(&self.session_id, cx) { + return set_title.run(title, cx); + } + } + Task::ready(Ok(())) } pub fn update_token_usage(&mut self, usage: Option, cx: &mut Context) { @@ -1326,11 +1335,7 @@ impl AcpThread { }; let git_store = self.project.read(cx).git_store().clone(); - let message_id = if self - .connection - .session_editor(&self.session_id, cx) - .is_some() - { + let message_id = if self.connection.truncate(&self.session_id, cx).is_some() { Some(UserMessageId::new()) } else { None @@ -1476,7 +1481,7 @@ impl AcpThread { /// Rewinds this thread to before the entry at `index`, removing it and all /// subsequent entries while reverting any changes made from that point. pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context) -> Task> { - let Some(session_editor) = self.connection.session_editor(&self.session_id, cx) else { + let Some(truncate) = self.connection.truncate(&self.session_id, cx) else { return Task::ready(Err(anyhow!("not supported"))); }; let Some(message) = self.user_message(&id) else { @@ -1496,8 +1501,7 @@ impl AcpThread { .await?; } - cx.update(|cx| session_editor.truncate(id.clone(), cx))? - .await?; + cx.update(|cx| truncate.run(id.clone(), cx))?.await?; this.update(cx, |this, cx| { if let Some((ix, _)) = this.user_message_mut(&id) { let range = ix..this.entries.len(); @@ -2652,11 +2656,11 @@ mod tests { .detach(); } - fn session_editor( + fn truncate( &self, session_id: &acp::SessionId, _cx: &mut App, - ) -> Option> { + ) -> Option> { Some(Rc::new(FakeAgentSessionEditor { _session_id: session_id.clone(), })) @@ -2671,8 +2675,8 @@ mod tests { _session_id: acp::SessionId, } - impl AgentSessionEditor for FakeAgentSessionEditor { - fn truncate(&self, _message_id: UserMessageId, _cx: &mut App) -> Task> { + impl AgentSessionTruncate for FakeAgentSessionEditor { + fn run(&self, _message_id: UserMessageId, _cx: &mut App) -> Task> { Task::ready(Ok(())) } } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 2bbd36487392452dc9a179c01f759e9fb68a7384..91e46dbac164b754b07be7597765d13d116673d2 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -50,11 +50,19 @@ pub trait AgentConnection { fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); - fn session_editor( + fn truncate( &self, _session_id: &acp::SessionId, _cx: &mut App, - ) -> Option> { + ) -> Option> { + None + } + + fn set_title( + &self, + _session_id: &acp::SessionId, + _cx: &mut App, + ) -> Option> { None } @@ -79,14 +87,18 @@ impl dyn AgentConnection { } } -pub trait AgentSessionEditor { - fn truncate(&self, message_id: UserMessageId, cx: &mut App) -> Task>; +pub trait AgentSessionTruncate { + fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task>; } pub trait AgentSessionResume { fn run(&self, cx: &mut App) -> Task>; } +pub trait AgentSessionSetTitle { + fn run(&self, title: SharedString, cx: &mut App) -> Task>; +} + pub trait AgentTelemetry { /// The name of the agent used for telemetry. fn agent_name(&self) -> String; @@ -424,11 +436,11 @@ mod test_support { } } - fn session_editor( + fn truncate( &self, _session_id: &agent_client_protocol::SessionId, _cx: &mut App, - ) -> Option> { + ) -> Option> { Some(Rc::new(StubAgentSessionEditor)) } @@ -439,8 +451,8 @@ mod test_support { struct StubAgentSessionEditor; - impl AgentSessionEditor for StubAgentSessionEditor { - fn truncate(&self, _: UserMessageId, _: &mut App) -> Task> { + impl AgentSessionTruncate for StubAgentSessionEditor { + fn run(&self, _: UserMessageId, _: &mut App) -> Task> { Task::ready(Ok(())) } } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index d5bc0fea63018c4023ebc94aee4d6d082a88c4cb..bbc30b74bc3cd29432a6297e8e6ddca4822d42c0 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -2,7 +2,7 @@ use crate::{ ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization, UserMessageContent, templates::Templates, }; -use crate::{HistoryStore, TokenUsageUpdated}; +use crate::{HistoryStore, TitleUpdated, TokenUsageUpdated}; use acp_thread::{AcpThread, AgentModelSelector}; use action_log::ActionLog; use agent_client_protocol as acp; @@ -253,6 +253,7 @@ impl NativeAgent { cx.observe_release(&acp_thread, |this, acp_thread, _cx| { this.sessions.remove(acp_thread.session_id()); }), + cx.subscribe(&thread_handle, Self::handle_thread_title_updated), cx.subscribe(&thread_handle, Self::handle_thread_token_usage_updated), cx.observe(&thread_handle, move |this, thread, cx| { this.save_thread(thread, cx) @@ -441,6 +442,26 @@ impl NativeAgent { }) } + fn handle_thread_title_updated( + &mut self, + thread: Entity, + _: &TitleUpdated, + cx: &mut Context, + ) { + let session_id = thread.read(cx).id(); + let Some(session) = self.sessions.get(session_id) else { + return; + }; + let thread = thread.downgrade(); + let acp_thread = session.acp_thread.clone(); + cx.spawn(async move |_, cx| { + let title = thread.read_with(cx, |thread, _| thread.title())?; + let task = acp_thread.update(cx, |acp_thread, cx| acp_thread.set_title(title, cx))?; + task.await + }) + .detach_and_log_err(cx); + } + fn handle_thread_token_usage_updated( &mut self, thread: Entity, @@ -717,10 +738,6 @@ impl NativeAgentConnection { thread.update_tool_call(update, cx) })??; } - ThreadEvent::TitleUpdate(title) => { - acp_thread - .update(cx, |thread, cx| thread.update_title(title, cx))??; - } ThreadEvent::Retry(status) => { acp_thread.update(cx, |thread, cx| { thread.update_retry_status(status, cx) @@ -856,8 +873,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .models .model_from_id(&LanguageModels::model_id(&default_model.model)) }); - - let thread = cx.new(|cx| { + Ok(cx.new(|cx| { Thread::new( project.clone(), agent.project_context.clone(), @@ -867,9 +883,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { default_model, cx, ) - }); - - Ok(thread) + })) }, )??; agent.update(cx, |agent, cx| agent.register_session(thread, cx)) @@ -941,11 +955,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }); } - fn session_editor( + fn truncate( &self, session_id: &agent_client_protocol::SessionId, cx: &mut App, - ) -> Option> { + ) -> Option> { self.0.update(cx, |agent, _cx| { agent.sessions.get(session_id).map(|session| { Rc::new(NativeAgentSessionEditor { @@ -956,6 +970,17 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }) } + fn set_title( + &self, + session_id: &acp::SessionId, + _cx: &mut App, + ) -> Option> { + Some(Rc::new(NativeAgentSessionSetTitle { + connection: self.clone(), + session_id: session_id.clone(), + }) as _) + } + fn telemetry(&self) -> Option> { Some(Rc::new(self.clone()) as Rc) } @@ -991,8 +1016,8 @@ struct NativeAgentSessionEditor { acp_thread: WeakEntity, } -impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor { - fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task> { +impl acp_thread::AgentSessionTruncate for NativeAgentSessionEditor { + fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task> { match self.thread.update(cx, |thread, cx| { thread.truncate(message_id.clone(), cx)?; Ok(thread.latest_token_usage()) @@ -1024,6 +1049,22 @@ impl acp_thread::AgentSessionResume for NativeAgentSessionResume { } } +struct NativeAgentSessionSetTitle { + connection: NativeAgentConnection, + session_id: acp::SessionId, +} + +impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle { + fn run(&self, title: SharedString, cx: &mut App) -> Task> { + let Some(session) = self.connection.0.read(cx).sessions.get(&self.session_id) else { + return Task::ready(Err(anyhow!("session not found"))); + }; + let thread = session.thread.clone(); + thread.update(cx, |thread, cx| thread.set_title(title, cx)); + Task::ready(Ok(())) + } +} + #[cfg(test)] mod tests { use crate::HistoryEntryId; @@ -1323,6 +1364,8 @@ mod tests { ) }); + cx.run_until_parked(); + // Drop the ACP thread, which should cause the session to be dropped as well. cx.update(|_| { drop(thread); diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index edba227da77c7ad0cd15c7c4c213b5a1f8d3f421..e7e28f495e88e496f4a43e5314a811da112f5248 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1383,6 +1383,7 @@ async fn test_title_generation(cx: &mut TestAppContext) { summary_model.send_last_completion_stream_text_chunk("oodnight Moon"); summary_model.end_last_completion_stream(); send.collect::>().await; + cx.run_until_parked(); thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); // Send another message, ensuring no title is generated this time. diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 6f560cd390cae1022d0de37c67c96d76b54da6ae..f6ef11c20bab5e8699b9ab80a7a6c585de801212 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -487,7 +487,6 @@ pub enum ThreadEvent { ToolCall(acp::ToolCall), ToolCallUpdate(acp_thread::ToolCallUpdate), ToolCallAuthorization(ToolCallAuthorization), - TitleUpdate(SharedString), Retry(acp_thread::RetryStatus), Stop(acp::StopReason), } @@ -514,6 +513,7 @@ pub struct Thread { prompt_id: PromptId, updated_at: DateTime, title: Option, + pending_title_generation: Option>, summary: Option, messages: Vec, completion_mode: CompletionMode, @@ -555,6 +555,7 @@ impl Thread { prompt_id: PromptId::new(), updated_at: Utc::now(), title: None, + pending_title_generation: None, summary: None, messages: Vec::new(), completion_mode: AgentSettings::get_global(cx).preferred_completion_mode, @@ -705,6 +706,7 @@ impl Thread { } else { Some(db_thread.title.clone()) }, + pending_title_generation: None, summary: db_thread.detailed_summary, messages: db_thread.messages, completion_mode: db_thread.completion_mode.unwrap_or_default(), @@ -1086,7 +1088,7 @@ impl Thread { event_stream: event_stream.clone(), _task: cx.spawn(async move |this, cx| { log::info!("Starting agent turn execution"); - let mut update_title = None; + let turn_result: Result<()> = async { let mut intent = CompletionIntent::UserPrompt; loop { @@ -1095,8 +1097,8 @@ impl Thread { let mut end_turn = true; this.update(cx, |this, cx| { // Generate title if needed. - if this.title.is_none() && update_title.is_none() { - update_title = Some(this.update_title(&event_stream, cx)); + if this.title.is_none() && this.pending_title_generation.is_none() { + this.generate_title(cx); } // End the turn if the model didn't use tools. @@ -1120,10 +1122,6 @@ impl Thread { .await; _ = this.update(cx, |this, cx| this.flush_pending_message(cx)); - if let Some(update_title) = update_title { - update_title.await.context("update title failed").log_err(); - } - match turn_result { Ok(()) => { log::info!("Turn execution completed"); @@ -1607,19 +1605,15 @@ impl Thread { }) } - fn update_title( - &mut self, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) -> Task> { + fn generate_title(&mut self, cx: &mut Context) { + let Some(model) = self.summarization_model.clone() else { + return; + }; + log::info!( "Generating title with model: {:?}", self.summarization_model.as_ref().map(|model| model.name()) ); - let Some(model) = self.summarization_model.clone() else { - return Task::ready(Ok(())); - }; - let event_stream = event_stream.clone(); let mut request = LanguageModelRequest { intent: Some(CompletionIntent::ThreadSummarization), temperature: AgentSettings::temperature_for_model(&model, cx), @@ -1635,42 +1629,51 @@ impl Thread { content: vec![SUMMARIZE_THREAD_PROMPT.into()], cache: false, }); - cx.spawn(async move |this, cx| { + self.pending_title_generation = Some(cx.spawn(async move |this, cx| { let mut title = String::new(); - let mut messages = model.stream_completion(request, cx).await?; - while let Some(event) = messages.next().await { - let event = event?; - let text = match event { - LanguageModelCompletionEvent::Text(text) => text, - LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { amount, limit }, - ) => { - this.update(cx, |thread, cx| { - thread.update_model_request_usage(amount, limit, cx); - })?; - continue; - } - _ => continue, - }; - let mut lines = text.lines(); - title.extend(lines.next()); + let generate = async { + let mut messages = model.stream_completion(request, cx).await?; + while let Some(event) = messages.next().await { + let event = event?; + let text = match event { + LanguageModelCompletionEvent::Text(text) => text, + LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::UsageUpdated { amount, limit }, + ) => { + this.update(cx, |thread, cx| { + thread.update_model_request_usage(amount, limit, cx); + })?; + continue; + } + _ => continue, + }; + + let mut lines = text.lines(); + title.extend(lines.next()); - // Stop if the LLM generated multiple lines. - if lines.next().is_some() { - break; + // Stop if the LLM generated multiple lines. + if lines.next().is_some() { + break; + } } - } + anyhow::Ok(()) + }; - log::info!("Setting title: {}", title); + if generate.await.context("failed to generate title").is_ok() { + _ = this.update(cx, |this, cx| this.set_title(title.into(), cx)); + } + _ = this.update(cx, |this, _| this.pending_title_generation = None); + })); + } - this.update(cx, |this, cx| { - let title = SharedString::from(title); - event_stream.send_title_update(title.clone()); - this.title = Some(title); - cx.notify(); - }) - }) + pub fn set_title(&mut self, title: SharedString, cx: &mut Context) { + self.pending_title_generation = None; + if Some(&title) != self.title.as_ref() { + self.title = Some(title); + cx.emit(TitleUpdated); + cx.notify(); + } } fn last_user_message(&self) -> Option<&UserMessage> { @@ -1975,6 +1978,10 @@ pub struct TokenUsageUpdated(pub Option); impl EventEmitter for Thread {} +pub struct TitleUpdated; + +impl EventEmitter for Thread {} + pub trait AgentTool where Self: 'static + Sized, @@ -2132,12 +2139,6 @@ where struct ThreadEventStream(mpsc::UnboundedSender>); impl ThreadEventStream { - fn send_title_update(&self, text: SharedString) { - self.0 - .unbounded_send(Ok(ThreadEvent::TitleUpdate(text))) - .ok(); - } - fn send_user_message(&self, message: &UserMessage) { self.0 .unbounded_send(Ok(ThreadEvent::UserMessage(message.clone()))) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 05d31051b2e1a4abd3dc8963ce6267a2a349562c..936f987864fadfa9b6535c05f7334850c2a6ebdf 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -15,7 +15,7 @@ use buffer_diff::BufferDiff; use client::zed_urls; use collections::{HashMap, HashSet}; use editor::scroll::Autoscroll; -use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects}; +use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; use fs::Fs; use gpui::{ @@ -281,7 +281,8 @@ enum ThreadState { }, Ready { thread: Entity, - _subscription: [Subscription; 2], + title_editor: Option>, + _subscriptions: Vec, }, LoadError(LoadError), Unauthenticated { @@ -445,12 +446,7 @@ impl AcpThreadView { this.update_in(cx, |this, window, cx| { match result { Ok(thread) => { - let thread_subscription = - cx.subscribe_in(&thread, window, Self::handle_thread_event); - let action_log = thread.read(cx).action_log().clone(); - let action_log_subscription = - cx.observe(&action_log, |_, _, cx| cx.notify()); let count = thread.read(cx).entries().len(); this.list_state.splice(0..0, count); @@ -489,9 +485,31 @@ impl AcpThreadView { }) }); + let mut subscriptions = vec![ + cx.subscribe_in(&thread, window, Self::handle_thread_event), + cx.observe(&action_log, |_, _, cx| cx.notify()), + ]; + + let title_editor = + if thread.update(cx, |thread, cx| thread.can_set_title(cx)) { + let editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_text(thread.read(cx).title(), window, cx); + editor + }); + subscriptions.push(cx.subscribe_in( + &editor, + window, + Self::handle_title_editor_event, + )); + Some(editor) + } else { + None + }; this.thread_state = ThreadState::Ready { thread, - _subscription: [thread_subscription, action_log_subscription], + title_editor, + _subscriptions: subscriptions, }; this.profile_selector = this.as_native_thread(cx).map(|thread| { @@ -618,6 +636,14 @@ impl AcpThreadView { } } + pub fn title_editor(&self) -> Option> { + if let ThreadState::Ready { title_editor, .. } = &self.thread_state { + title_editor.clone() + } else { + None + } + } + pub fn cancel_generation(&mut self, cx: &mut Context) { self.thread_error.take(); self.thread_retry_status.take(); @@ -662,6 +688,35 @@ impl AcpThreadView { cx.notify(); } + pub fn handle_title_editor_event( + &mut self, + title_editor: &Entity, + event: &EditorEvent, + window: &mut Window, + cx: &mut Context, + ) { + let Some(thread) = self.thread() else { return }; + + match event { + EditorEvent::BufferEdited => { + let new_title = title_editor.read(cx).text(cx); + thread.update(cx, |thread, cx| { + thread + .set_title(new_title.into(), cx) + .detach_and_log_err(cx); + }) + } + EditorEvent::Blurred => { + if title_editor.read(cx).text(cx).is_empty() { + title_editor.update(cx, |editor, cx| { + editor.set_text("New Thread", window, cx); + }); + } + } + _ => {} + } + } + pub fn handle_message_editor_event( &mut self, _: &Entity, @@ -1009,7 +1064,17 @@ impl AcpThreadView { self.thread_retry_status.take(); self.thread_state = ThreadState::LoadError(error.clone()); } - AcpThreadEvent::TitleUpdated | AcpThreadEvent::TokenUsageUpdated => {} + AcpThreadEvent::TitleUpdated => { + let title = thread.read(cx).title(); + if let Some(title_editor) = self.title_editor() { + title_editor.update(cx, |editor, cx| { + if editor.text(cx) != title { + editor.set_text(title, window, cx); + } + }); + } + } + AcpThreadEvent::TokenUsageUpdated => {} } cx.notify(); } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 65a9da573ad554d9d70f17aaec8b7c430cd6ec4e..d2ff6aa4f393f6610a33179433e9708eef3789dd 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -905,7 +905,7 @@ impl AgentPanel { fn active_thread_view(&self) -> Option<&Entity> { match &self.active_view { - ActiveView::ExternalAgentThread { thread_view } => Some(thread_view), + ActiveView::ExternalAgentThread { thread_view, .. } => Some(thread_view), ActiveView::Thread { .. } | ActiveView::TextThread { .. } | ActiveView::History @@ -2075,9 +2075,32 @@ impl AgentPanel { } } ActiveView::ExternalAgentThread { thread_view } => { - Label::new(thread_view.read(cx).title(cx)) - .truncate() - .into_any_element() + if let Some(title_editor) = thread_view.read(cx).title_editor() { + div() + .w_full() + .on_action({ + let thread_view = thread_view.downgrade(); + move |_: &menu::Confirm, window, cx| { + if let Some(thread_view) = thread_view.upgrade() { + thread_view.focus_handle(cx).focus(window); + } + } + }) + .on_action({ + let thread_view = thread_view.downgrade(); + move |_: &editor::actions::Cancel, window, cx| { + if let Some(thread_view) = thread_view.upgrade() { + thread_view.focus_handle(cx).focus(window); + } + } + }) + .child(title_editor) + .into_any_element() + } else { + Label::new(thread_view.read(cx).title(cx)) + .truncate() + .into_any_element() + } } ActiveView::TextThread { title_editor, From 20a0c3e92050c417f242d5e909d4f9ea548494dc Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Thu, 21 Aug 2025 15:27:09 -0500 Subject: [PATCH 256/744] Disable minidump generation on dev builds (again) (#36716) We accidentally deleted this in #36267 Release Notes: - N/A --- crates/zed/src/reliability.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index 646a3af5bbcb110ea9886d52988fa82ad4023ec2..e9acaa588dab8c19aac4c56e8714ecd5242532bb 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -60,7 +60,9 @@ pub fn init_panic_hook( .or_else(|| info.payload().downcast_ref::().cloned()) .unwrap_or_else(|| "Box".to_string()); - crashes::handle_panic(payload.clone(), info.location()); + if *release_channel::RELEASE_CHANNEL != ReleaseChannel::Dev { + crashes::handle_panic(payload.clone(), info.location()); + } let thread = thread::current(); let thread_name = thread.name().unwrap_or(""); From 0beb919bbb8c662ee7ff3302bfb5e49bec1e3fba Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 21 Aug 2025 17:29:53 -0300 Subject: [PATCH 257/744] acp: Fix `MessageEditor::set_message` for sent messages (#36715) The `PromptCapabilities` introduced in previous PRs were only getting set on the main message editor and not for the editors in user messages. This caused a bug where mentions would disappear after resending the message, and for the completion provider to be limited to files. Release Notes: - N/A --- crates/agent_ui/src/acp/entry_view_state.rs | 9 ++++-- crates/agent_ui/src/acp/message_editor.rs | 34 +++++++++++---------- crates/agent_ui/src/acp/thread_view.rs | 16 ++++++---- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index c310473259bc07468cd8a062e15dd8025645f90f..0e4080d689bd4ae4ff67bdd7c6a9beb3f220f2b9 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -1,7 +1,7 @@ -use std::ops::Range; +use std::{cell::Cell, ops::Range, rc::Rc}; use acp_thread::{AcpThread, AgentThreadEntry}; -use agent_client_protocol::ToolCallId; +use agent_client_protocol::{PromptCapabilities, ToolCallId}; use agent2::HistoryStore; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; @@ -27,6 +27,7 @@ pub struct EntryViewState { prompt_store: Option>, entries: Vec, prevent_slash_commands: bool, + prompt_capabilities: Rc>, } impl EntryViewState { @@ -35,6 +36,7 @@ impl EntryViewState { project: Entity, history_store: Entity, prompt_store: Option>, + prompt_capabilities: Rc>, prevent_slash_commands: bool, ) -> Self { Self { @@ -44,6 +46,7 @@ impl EntryViewState { prompt_store, entries: Vec::new(), prevent_slash_commands, + prompt_capabilities, } } @@ -81,6 +84,7 @@ impl EntryViewState { self.project.clone(), self.history_store.clone(), self.prompt_store.clone(), + self.prompt_capabilities.clone(), "Edit message - @ to include context", self.prevent_slash_commands, editor::EditorMode::AutoHeight { @@ -403,6 +407,7 @@ mod tests { project.clone(), history_store, None, + Default::default(), false, ) }); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 8f5044cb21a7e5cd9f4580ede8b6ddaa2cb51300..7d73ebeb197605b5a0c078fa3bc5b7a283d18641 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -87,6 +87,7 @@ impl MessageEditor { project: Entity, history_store: Entity, prompt_store: Option>, + prompt_capabilities: Rc>, placeholder: impl Into>, prevent_slash_commands: bool, mode: EditorMode, @@ -100,7 +101,6 @@ impl MessageEditor { }, None, ); - let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let completion_provider = ContextPickerCompletionProvider::new( cx.weak_entity(), workspace.clone(), @@ -203,10 +203,6 @@ impl MessageEditor { .detach(); } - pub fn set_prompt_capabilities(&mut self, capabilities: acp::PromptCapabilities) { - self.prompt_capabilities.set(capabilities); - } - #[cfg(test)] pub(crate) fn editor(&self) -> &Entity { &self.editor @@ -1095,15 +1091,21 @@ impl MessageEditor { mentions.push((start..end, mention_uri, resource.text)); } } + acp::ContentBlock::ResourceLink(resource) => { + if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() { + let start = text.len(); + write!(&mut text, "{}", mention_uri.as_link()).ok(); + let end = text.len(); + mentions.push((start..end, mention_uri, resource.uri)); + } + } acp::ContentBlock::Image(content) => { let start = text.len(); text.push_str("image"); let end = text.len(); images.push((start..end, content)); } - acp::ContentBlock::Audio(_) - | acp::ContentBlock::Resource(_) - | acp::ContentBlock::ResourceLink(_) => {} + acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {} } } @@ -1850,7 +1852,7 @@ impl Addon for MessageEditorAddon { #[cfg(test)] mod tests { - use std::{ops::Range, path::Path, sync::Arc}; + use std::{cell::Cell, ops::Range, path::Path, rc::Rc, sync::Arc}; use acp_thread::MentionUri; use agent_client_protocol as acp; @@ -1896,6 +1898,7 @@ mod tests { project.clone(), history_store.clone(), None, + Default::default(), "Test", false, EditorMode::AutoHeight { @@ -2086,6 +2089,7 @@ mod tests { let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { let workspace_handle = cx.weak_entity(); @@ -2095,6 +2099,7 @@ mod tests { project.clone(), history_store.clone(), None, + prompt_capabilities.clone(), "Test", false, EditorMode::AutoHeight { @@ -2139,13 +2144,10 @@ mod tests { editor.set_text("", window, cx); }); - message_editor.update(&mut cx, |editor, _cx| { - // Enable all prompt capabilities - editor.set_prompt_capabilities(acp::PromptCapabilities { - image: true, - audio: true, - embedded_context: true, - }); + prompt_capabilities.set(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, }); cx.simulate_input("Lorem "); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 936f987864fadfa9b6535c05f7334850c2a6ebdf..c7d6bb439f58ff4e75d7a90e09c81d492e34f352 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -5,7 +5,7 @@ use acp_thread::{ }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; -use agent_client_protocol::{self as acp}; +use agent_client_protocol::{self as acp, PromptCapabilities}; use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore}; @@ -34,6 +34,7 @@ use project::{Project, ProjectEntryId}; use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::{Settings as _, SettingsStore}; +use std::cell::Cell; use std::sync::Arc; use std::time::Instant; use std::{collections::BTreeMap, rc::Rc, time::Duration}; @@ -271,6 +272,7 @@ pub struct AcpThreadView { plan_expanded: bool, editor_expanded: bool, editing_message: Option, + prompt_capabilities: Rc>, _cancel_task: Option>, _subscriptions: [Subscription; 3], } @@ -306,6 +308,7 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) -> Self { + let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let prevent_slash_commands = agent.clone().downcast::().is_some(); let message_editor = cx.new(|cx| { let mut editor = MessageEditor::new( @@ -313,6 +316,7 @@ impl AcpThreadView { project.clone(), history_store.clone(), prompt_store.clone(), + prompt_capabilities.clone(), "Message the agent — @ to include context", prevent_slash_commands, editor::EditorMode::AutoHeight { @@ -336,6 +340,7 @@ impl AcpThreadView { project.clone(), history_store.clone(), prompt_store.clone(), + prompt_capabilities.clone(), prevent_slash_commands, ) }); @@ -371,6 +376,7 @@ impl AcpThreadView { editor_expanded: false, history_store, hovered_recent_history_item: None, + prompt_capabilities, _subscriptions: subscriptions, _cancel_task: None, } @@ -448,6 +454,9 @@ impl AcpThreadView { Ok(thread) => { let action_log = thread.read(cx).action_log().clone(); + this.prompt_capabilities + .set(connection.prompt_capabilities()); + let count = thread.read(cx).entries().len(); this.list_state.splice(0..0, count); this.entry_view_state.update(cx, |view_state, cx| { @@ -523,11 +532,6 @@ impl AcpThreadView { }) }); - this.message_editor.update(cx, |message_editor, _cx| { - message_editor - .set_prompt_capabilities(connection.prompt_capabilities()); - }); - cx.notify(); } Err(err) => { From 06c0e593790d7fae184c31b02423a9d3bb0ccfee Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Fri, 22 Aug 2025 00:21:36 +0200 Subject: [PATCH 258/744] Make tab switcher show preview of selected tab (#36718) Similar to nvim's telescope this makes it easier to find the right tab in the list. The preview takes place in the pane where the tab resides. - on dismiss: We restore all panes. - on confirm: We restore all panes except the one where the selected tab resides. For this reason we collect the active item for each pane before the tabswither starts. Release Notes: - Improved tab switcher, it now shows a preview of the selected tab Co-authored-by: Julia Ryan --- crates/tab_switcher/src/tab_switcher.rs | 55 +++++++++++++++++++++---- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 11e32523b4935f464dba81471a5673549c088eda..7c70bcd5b55c8138053ade315dec3113f37a09f8 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -113,7 +113,13 @@ impl TabSwitcher { } let weak_workspace = workspace.weak_handle(); + let project = workspace.project().clone(); + let original_items: Vec<_> = workspace + .panes() + .iter() + .map(|p| (p.clone(), p.read(cx).active_item_index())) + .collect(); workspace.toggle_modal(window, cx, |window, cx| { let delegate = TabSwitcherDelegate::new( project, @@ -124,6 +130,7 @@ impl TabSwitcher { is_global, window, cx, + original_items, ); TabSwitcher::new(delegate, window, is_global, cx) }); @@ -221,7 +228,9 @@ pub struct TabSwitcherDelegate { workspace: WeakEntity, project: Entity, matches: Vec, + original_items: Vec<(Entity, usize)>, is_all_panes: bool, + restored_items: bool, } impl TabSwitcherDelegate { @@ -235,6 +244,7 @@ impl TabSwitcherDelegate { is_all_panes: bool, window: &mut Window, cx: &mut Context, + original_items: Vec<(Entity, usize)>, ) -> Self { Self::subscribe_to_updates(&pane, window, cx); Self { @@ -246,6 +256,8 @@ impl TabSwitcherDelegate { project, matches: Vec::new(), is_all_panes, + original_items, + restored_items: false, } } @@ -300,13 +312,6 @@ impl TabSwitcherDelegate { let matches = if query.is_empty() { let history = workspace.read(cx).recently_activated_items(cx); - for item in &all_items { - eprintln!( - "{:?} {:?}", - item.item.tab_content_text(0, cx), - (Reverse(history.get(&item.item.item_id())), item.item_index) - ) - } all_items .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index)); all_items @@ -473,8 +478,25 @@ impl PickerDelegate for TabSwitcherDelegate { self.selected_index } - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { + fn set_selected_index( + &mut self, + ix: usize, + window: &mut Window, + cx: &mut Context>, + ) { self.selected_index = ix; + + let Some(selected_match) = self.matches.get(self.selected_index()) else { + return; + }; + selected_match + .pane + .update(cx, |pane, cx| { + if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) { + pane.activate_item(index, false, false, window, cx); + } + }) + .ok(); cx.notify(); } @@ -501,6 +523,13 @@ impl PickerDelegate for TabSwitcherDelegate { let Some(selected_match) = self.matches.get(self.selected_index()) else { return; }; + + self.restored_items = true; + for (pane, index) in self.original_items.iter() { + pane.update(cx, |this, cx| { + this.activate_item(*index, false, false, window, cx); + }) + } selected_match .pane .update(cx, |pane, cx| { @@ -511,7 +540,15 @@ impl PickerDelegate for TabSwitcherDelegate { .ok(); } - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { + fn dismissed(&mut self, window: &mut Window, cx: &mut Context>) { + if !self.restored_items { + for (pane, index) in self.original_items.iter() { + pane.update(cx, |this, cx| { + this.activate_item(*index, false, false, window, cx); + }) + } + } + self.tab_switcher .update(cx, |_, cx| cx.emit(DismissEvent)) .log_err(); From a977fbc5b09e3fc23181fe9c30246de6c6e9c9bc Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 21 Aug 2025 18:40:07 -0400 Subject: [PATCH 259/744] Document project_panel.sticky_scroll (#36721) Hat tip to: @watercubz in https://github.com/zed-industries/zed/issues/22869#issuecomment-3183850576 Release Notes: - N/A --- docs/src/configuring-zed.md | 1 + docs/src/visual-customization.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 39d172ea5f22c5f88606bfc5ccaef35b09d47831..696370e310a32cdd3143de554fea5d54dc1edd88 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -3234,6 +3234,7 @@ Run the `theme selector: toggle` action in the command palette to see a current "scrollbar": { "show": null }, + "sticky_scroll": true, "show_diagnostics": "all", "indent_guides": { "show": "always" diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 3ad1e381d9517dd613e58e8a52f664aabbd2cab0..24b2a9d769764215c0868d455ffe6bfd615be158 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -430,6 +430,7 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k "indent_size": 20, // Pixels for each successive indent "auto_reveal_entries": true, // Show file in panel when activating its buffer "auto_fold_dirs": true, // Fold dirs with single subdir + "sticky_scroll": true, // Stick parent directories at top of the project panel. "scrollbar": { // Project panel scrollbar settings "show": null // Show/hide: (auto, system, always, never) }, From 18fe68d991b8f63ef7f5d276eb052e055feea70a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 21 Aug 2025 20:51:36 -0300 Subject: [PATCH 260/744] thread view: Add small refinements to tool call UI (#36723) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 66 ++++++++++++++------------ crates/markdown/src/markdown.rs | 3 +- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c7d6bb439f58ff4e75d7a90e09c81d492e34f352..4d89a55139154b448b9fb33f00fb31fa22bc81cc 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1372,7 +1372,7 @@ impl AcpThreadView { AgentThreadEntry::ToolCall(tool_call) => { let has_terminals = tool_call.terminals().next().is_some(); - div().w_full().py_1p5().px_5().map(|this| { + div().w_full().py_1().px_5().map(|this| { if has_terminals { this.children(tool_call.terminals().map(|terminal| { self.render_terminal_tool_call( @@ -1570,7 +1570,7 @@ impl AcpThreadView { .size(IconSize::Small) .color(Color::Muted); - let base_container = h_flex().size_4().justify_center(); + let base_container = h_flex().flex_shrink_0().size_4().justify_center(); if is_collapsible { base_container @@ -1623,20 +1623,32 @@ impl AcpThreadView { | ToolCallStatus::WaitingForConfirmation { .. } | ToolCallStatus::Completed => None, ToolCallStatus::InProgress => Some( - Icon::new(IconName::ArrowCircle) - .color(Color::Muted) - .size(IconSize::Small) - .with_animation( - "running", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + div() + .absolute() + .right_2() + .child( + Icon::new(IconName::ArrowCircle) + .color(Color::Muted) + .size(IconSize::Small) + .with_animation( + "running", + Animation::new(Duration::from_secs(3)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage(delta))) + }, + ), ) .into_any(), ), ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => Some( - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small) + div() + .absolute() + .right_2() + .child( + Icon::new(IconName::Close) + .color(Color::Error) + .size(IconSize::Small), + ) .into_any_element(), ), }; @@ -1734,13 +1746,14 @@ impl AcpThreadView { .child( h_flex() .id(header_id) + .relative() .w_full() + .max_w_full() .gap_1() - .justify_between() .when(use_card_layout, |this| { - this.pl_2() - .pr_1p5() - .py_1() + this.pl_1p5() + .pr_1() + .py_0p5() .rounded_t_md() .when(is_open && !failed_tool_call, |this| { this.border_b_1() @@ -1753,7 +1766,7 @@ impl AcpThreadView { .group(&card_header_id) .relative() .w_full() - .min_h_6() + .h(window.line_height() - px(2.)) .text_size(self.tool_name_font_size()) .child(self.render_tool_call_icon( card_header_id, @@ -1797,21 +1810,14 @@ impl AcpThreadView { } else { h_flex() .id("non-card-label-container") - .w_full() .relative() + .w_full() + .max_w_full() .ml_1p5() - .overflow_hidden() - .child( - h_flex() - .id("non-card-label") - .pr_8() - .w_full() - .overflow_x_scroll() - .child(self.render_markdown( - tool_call.label.clone(), - default_markdown_style(false, true, window, cx), - )), - ) + .child(h_flex().pr_8().child(self.render_markdown( + tool_call.label.clone(), + default_markdown_style(false, true, window, cx), + ))) .child(gradient_overlay(gradient_color)) .on_click(cx.listener({ let id = tool_call.id.clone(); diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 755506bd126daff19b54888d76021844c5097a1c..39a438c512163766cbda3b89df8421c4b79db9eb 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1089,7 +1089,7 @@ impl Element for MarkdownElement { .absolute() .top_1() .right_1() - .justify_center() + .justify_end() .child(codeblock), ) }); @@ -1320,6 +1320,7 @@ fn render_copy_code_block_button( ) .icon_color(Color::Muted) .icon_size(IconSize::Small) + .style(ButtonStyle::Filled) .shape(ui::IconButtonShape::Square) .tooltip(Tooltip::text("Copy Code")) .on_click({ From eeaadc098f189121d840849d4833dec4398364cb Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 21 Aug 2025 18:59:42 -0500 Subject: [PATCH 261/744] Add GPU info to Sentry crashes (#36624) Closes #ISSUE Adds system GPU collection to crash reporting. Currently this is Linux only. The system GPUs are determined by reading the `/sys/class/drm` directory structure, rather than using the exisiting `gpui::Window::gpu_specs()` method in order to gather more information, and so that the GPU context is not dependent on Vulkan context initialization (i.e. we still get GPU info when Zed fails to start because Vulkan failed to initialize). Unfortunately, the `blade` APIs do not support querying which GPU _will_ be used, so we do not know which GPU was attempted to be used when Vulkan context initialization fails, however, when Vulkan initialization succeeds, we send a message to the crash handler containing the result of `gpui::Window::gpu_specs()` to include the "Active" gpu in any crash report that may occur Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 31 ++++- Cargo.toml | 5 + crates/client/src/telemetry.rs | 2 +- crates/crashes/Cargo.toml | 2 + crates/crashes/src/crashes.rs | 26 +++- crates/feedback/Cargo.toml | 6 +- crates/feedback/src/feedback.rs | 6 +- crates/gpui/src/gpui.rs | 2 +- crates/system_specs/Cargo.toml | 28 ++++ crates/system_specs/LICENSE-GPL | 1 + .../src/system_specs.rs | 122 +++++++++++++++++- crates/workspace/Cargo.toml | 2 +- crates/zed/Cargo.toml | 2 + crates/zed/src/main.rs | 4 +- crates/zed/src/reliability.rs | 93 ++++++++++++- crates/zed/src/zed.rs | 12 +- 16 files changed, 315 insertions(+), 29 deletions(-) create mode 100644 crates/system_specs/Cargo.toml create mode 120000 crates/system_specs/LICENSE-GPL rename crates/{feedback => system_specs}/src/system_specs.rs (59%) diff --git a/Cargo.lock b/Cargo.lock index 61f6f42498978ce0075f95cf98b99fe6b7c0ae73..2b3d7b26917988616ef128648919ea129da2bd3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4050,6 +4050,7 @@ dependencies = [ name = "crashes" version = "0.1.0" dependencies = [ + "bincode", "crash-handler", "log", "mach2 0.5.0", @@ -4059,6 +4060,7 @@ dependencies = [ "serde", "serde_json", "smol", + "system_specs", "workspace-hack", ] @@ -5738,14 +5740,10 @@ dependencies = [ name = "feedback" version = "0.1.0" dependencies = [ - "client", "editor", "gpui", - "human_bytes", "menu", - "release_channel", - "serde", - "sysinfo", + "system_specs", "ui", "urlencoding", "util", @@ -11634,6 +11632,12 @@ dependencies = [ "hmac", ] +[[package]] +name = "pciid-parser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0008e816fcdaf229cdd540e9b6ca2dc4a10d65c31624abb546c6420a02846e61" + [[package]] name = "pem" version = "3.0.5" @@ -16154,6 +16158,21 @@ dependencies = [ "winx", ] +[[package]] +name = "system_specs" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "gpui", + "human_bytes", + "pciid-parser", + "release_channel", + "serde", + "sysinfo", + "workspace-hack", +] + [[package]] name = "tab_switcher" version = "0.1.0" @@ -20413,6 +20432,7 @@ dependencies = [ "auto_update", "auto_update_ui", "backtrace", + "bincode", "breadcrumbs", "call", "channel", @@ -20511,6 +20531,7 @@ dependencies = [ "supermaven", "svg_preview", "sysinfo", + "system_specs", "tab_switcher", "task", "tasks_ui", diff --git a/Cargo.toml b/Cargo.toml index b13795e1e191e45e01b45a79287c56eaf397d9fb..84de9b30adddac8a46366fd4760e243cc7ceab90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,6 +155,7 @@ members = [ "crates/streaming_diff", "crates/sum_tree", "crates/supermaven", + "crates/system_specs", "crates/supermaven_api", "crates/svg_preview", "crates/tab_switcher", @@ -381,6 +382,7 @@ streaming_diff = { path = "crates/streaming_diff" } sum_tree = { path = "crates/sum_tree" } supermaven = { path = "crates/supermaven" } supermaven_api = { path = "crates/supermaven_api" } +system_specs = { path = "crates/system_specs" } tab_switcher = { path = "crates/tab_switcher" } task = { path = "crates/task" } tasks_ui = { path = "crates/tasks_ui" } @@ -450,6 +452,7 @@ aws-sdk-bedrockruntime = { version = "1.80.0", features = [ aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] } aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] } base64 = "0.22" +bincode = "1.2.1" bitflags = "2.6.0" blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } @@ -493,6 +496,7 @@ handlebars = "4.3" heck = "0.5" heed = { version = "0.21.0", features = ["read-txn-no-tls"] } hex = "0.4.3" +human_bytes = "0.4.1" html5ever = "0.27.0" http = "1.1" http-body = "1.0" @@ -532,6 +536,7 @@ palette = { version = "0.7.5", default-features = false, features = ["std"] } parking_lot = "0.12.1" partial-json-fixer = "0.5.3" parse_int = "0.9" +pciid-parser = "0.8.0" pathdiff = "0.2" pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index f3142a0af667d2022dfddd1c0f3f7b309e803d46..a5c1532c7563ab4bcb5f8826dcc18f3d52daf222 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -76,7 +76,7 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock>> = LazyLock::new(|| { pub static MINIDUMP_ENDPOINT: LazyLock> = LazyLock::new(|| { option_env!("ZED_MINIDUMP_ENDPOINT") - .map(|s| s.to_owned()) + .map(str::to_string) .or_else(|| env::var("ZED_MINIDUMP_ENDPOINT").ok()) }); diff --git a/crates/crashes/Cargo.toml b/crates/crashes/Cargo.toml index f12913d1cbda34219430d8aba8e56431a056da59..370f0bb5f6b1088e0a5af90e786b937283072e73 100644 --- a/crates/crashes/Cargo.toml +++ b/crates/crashes/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true license = "GPL-3.0-or-later" [dependencies] +bincode.workspace = true crash-handler.workspace = true log.workspace = true minidumper.workspace = true @@ -14,6 +15,7 @@ release_channel.workspace = true smol.workspace = true serde.workspace = true serde_json.workspace = true +system_specs.workspace = true workspace-hack.workspace = true [target.'cfg(target_os = "macos")'.dependencies] diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index b1afc5ae454d30c9c8ee283fce5e03b942fb7c70..f7bc96bff93afff70970e1a5f9488048005ec4b3 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -127,6 +127,7 @@ unsafe fn suspend_all_other_threads() { pub struct CrashServer { initialization_params: OnceLock, panic_info: OnceLock, + active_gpu: OnceLock, has_connection: Arc, } @@ -135,6 +136,8 @@ pub struct CrashInfo { pub init: InitCrashHandler, pub panic: Option, pub minidump_error: Option, + pub gpus: Vec, + pub active_gpu: Option, } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -143,7 +146,6 @@ pub struct InitCrashHandler { pub zed_version: String, pub release_channel: String, pub commit_sha: String, - // pub gpu: String, } #[derive(Deserialize, Serialize, Debug, Clone)] @@ -178,6 +180,18 @@ impl minidumper::ServerHandler for CrashServer { Err(e) => Some(format!("{e:?}")), }; + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + let gpus = vec![]; + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + let gpus = match system_specs::read_gpu_info_from_sys_class_drm() { + Ok(gpus) => gpus, + Err(err) => { + log::warn!("Failed to collect GPU information for crash report: {err}"); + vec![] + } + }; + let crash_info = CrashInfo { init: self .initialization_params @@ -186,6 +200,8 @@ impl minidumper::ServerHandler for CrashServer { .clone(), panic: self.panic_info.get().cloned(), minidump_error, + active_gpu: self.active_gpu.get().cloned(), + gpus, }; let crash_data_path = paths::logs_dir() @@ -211,6 +227,13 @@ impl minidumper::ServerHandler for CrashServer { serde_json::from_slice::(&buffer).expect("invalid panic data"); self.panic_info.set(panic_data).expect("already panicked"); } + 3 => { + let gpu_specs: system_specs::GpuSpecs = + bincode::deserialize(&buffer).expect("gpu specs"); + self.active_gpu + .set(gpu_specs) + .expect("already set active gpu"); + } _ => { panic!("invalid message kind"); } @@ -287,6 +310,7 @@ pub fn crash_server(socket: &Path) { initialization_params: OnceLock::new(), panic_info: OnceLock::new(), has_connection, + active_gpu: OnceLock::new(), }), &shutdown, Some(CRASH_HANDLER_PING_TIMEOUT), diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index 3a2c1fd7131ef7b5d7b07b8ec036fff4f1bba621..db872f7a15035c5012d42680c2d812d3486c6a89 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -15,13 +15,9 @@ path = "src/feedback.rs" test-support = [] [dependencies] -client.workspace = true gpui.workspace = true -human_bytes = "0.4.1" menu.workspace = true -release_channel.workspace = true -serde.workspace = true -sysinfo.workspace = true +system_specs.workspace = true ui.workspace = true urlencoding.workspace = true util.workspace = true diff --git a/crates/feedback/src/feedback.rs b/crates/feedback/src/feedback.rs index 40c2707d34c9f5ab50bdb51c8b82183be2106285..3822dd7ba38ac8131df4f391b8b0a5c05978fe8d 100644 --- a/crates/feedback/src/feedback.rs +++ b/crates/feedback/src/feedback.rs @@ -1,18 +1,14 @@ use gpui::{App, ClipboardItem, PromptLevel, actions}; -use system_specs::SystemSpecs; +use system_specs::{CopySystemSpecsIntoClipboard, SystemSpecs}; use util::ResultExt; use workspace::Workspace; use zed_actions::feedback::FileBugReport; pub mod feedback_modal; -pub mod system_specs; - actions!( zed, [ - /// Copies system specifications to the clipboard for bug reports. - CopySystemSpecsIntoClipboard, /// Opens email client to send feedback to Zed support. EmailZed, /// Opens the Zed repository on GitHub. diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 5e4b5fe6e9d221a2915e1f8234eb01e14c177a46..0f5b98df39712157845b56d9c167f59d3f8831ab 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -352,7 +352,7 @@ impl Flatten for Result { } /// Information about the GPU GPUI is running on. -#[derive(Default, Debug)] +#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)] pub struct GpuSpecs { /// Whether the GPU is really a fake (like `llvmpipe`) running on the CPU. pub is_software_emulated: bool, diff --git a/crates/system_specs/Cargo.toml b/crates/system_specs/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..8ef1b581ae21632c4894b132c9c52f617e016e7f --- /dev/null +++ b/crates/system_specs/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "system_specs" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/system_specs.rs" + +[features] +default = [] + +[dependencies] +anyhow.workspace = true +client.workspace = true +gpui.workspace = true +human_bytes.workspace = true +release_channel.workspace = true +serde.workspace = true +sysinfo.workspace = true +workspace-hack.workspace = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] +pciid-parser.workspace = true diff --git a/crates/system_specs/LICENSE-GPL b/crates/system_specs/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/system_specs/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/feedback/src/system_specs.rs b/crates/system_specs/src/system_specs.rs similarity index 59% rename from crates/feedback/src/system_specs.rs rename to crates/system_specs/src/system_specs.rs index 87642ab9294b3dd4faa32f368056a136f4b79e18..731d335232cd5f7c2b9190e458aae9f1b96722b6 100644 --- a/crates/feedback/src/system_specs.rs +++ b/crates/system_specs/src/system_specs.rs @@ -1,11 +1,22 @@ +//! # system_specs + use client::telemetry; -use gpui::{App, AppContext as _, SemanticVersion, Task, Window}; +pub use gpui::GpuSpecs; +use gpui::{App, AppContext as _, SemanticVersion, Task, Window, actions}; use human_bytes::human_bytes; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use serde::Serialize; use std::{env, fmt::Display}; use sysinfo::{MemoryRefreshKind, RefreshKind, System}; +actions!( + zed, + [ + /// Copies system specifications to the clipboard for bug reports. + CopySystemSpecsIntoClipboard, + ] +); + #[derive(Clone, Debug, Serialize)] pub struct SystemSpecs { app_version: String, @@ -158,6 +169,115 @@ fn try_determine_available_gpus() -> Option { } } +#[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize, Clone)] +pub struct GpuInfo { + pub device_name: Option, + pub device_pci_id: u16, + pub vendor_name: Option, + pub vendor_pci_id: u16, + pub driver_version: Option, + pub driver_name: Option, +} + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +pub fn read_gpu_info_from_sys_class_drm() -> anyhow::Result> { + use anyhow::Context as _; + use pciid_parser; + let dir_iter = std::fs::read_dir("/sys/class/drm").context("Failed to read /sys/class/drm")?; + let mut pci_addresses = vec![]; + let mut gpus = Vec::::new(); + let pci_db = pciid_parser::Database::read().ok(); + for entry in dir_iter { + let Ok(entry) = entry else { + continue; + }; + + let device_path = entry.path().join("device"); + let Some(pci_address) = device_path.read_link().ok().and_then(|pci_address| { + pci_address + .file_name() + .and_then(std::ffi::OsStr::to_str) + .map(str::trim) + .map(str::to_string) + }) else { + continue; + }; + let Ok(device_pci_id) = read_pci_id_from_path(device_path.join("device")) else { + continue; + }; + let Ok(vendor_pci_id) = read_pci_id_from_path(device_path.join("vendor")) else { + continue; + }; + let driver_name = std::fs::read_link(device_path.join("driver")) + .ok() + .and_then(|driver_link| { + driver_link + .file_name() + .and_then(std::ffi::OsStr::to_str) + .map(str::trim) + .map(str::to_string) + }); + let driver_version = driver_name + .as_ref() + .and_then(|driver_name| { + std::fs::read_to_string(format!("/sys/module/{driver_name}/version")).ok() + }) + .as_deref() + .map(str::trim) + .map(str::to_string); + + let already_found = gpus + .iter() + .zip(&pci_addresses) + .any(|(gpu, gpu_pci_address)| { + gpu_pci_address == &pci_address + && gpu.driver_version == driver_version + && gpu.driver_name == driver_name + }); + + if already_found { + continue; + } + + let vendor = pci_db + .as_ref() + .and_then(|db| db.vendors.get(&vendor_pci_id)); + let vendor_name = vendor.map(|vendor| vendor.name.clone()); + let device_name = vendor + .and_then(|vendor| vendor.devices.get(&device_pci_id)) + .map(|device| device.name.clone()); + + gpus.push(GpuInfo { + device_name, + device_pci_id, + vendor_name, + vendor_pci_id, + driver_version, + driver_name, + }); + pci_addresses.push(pci_address); + } + + Ok(gpus) +} + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +fn read_pci_id_from_path(path: impl AsRef) -> anyhow::Result { + use anyhow::Context as _; + let id = std::fs::read_to_string(path)?; + let id = id + .trim() + .strip_prefix("0x") + .context("Not a device ID") + .context(id.clone())?; + anyhow::ensure!( + id.len() == 4, + "Not a device id, expected 4 digits, found {}", + id.len() + ); + u16::from_str_radix(id, 16).context("Failed to parse device ID") +} + /// Returns value of `ZED_BUNDLE_TYPE` set at compiletime or else at runtime. /// /// The compiletime value is used by flatpak since it doesn't seem to have a way to provide a diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index e1bda7ad3621f308a5b1a9f2f465394e5200c583..570657ba8f9ee8edb540df6a1bb4c8637cff64ac 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -29,7 +29,7 @@ test-support = [ any_vec.workspace = true anyhow.workspace = true async-recursion.workspace = true -bincode = "1.2.1" +bincode.workspace = true call.workspace = true client.workspace = true clock.workspace = true diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index ac4cd721244ab9abb5e008eb9d6fe32430793b90..c61e23f0a1a4dec7f56c8fe418f2cd0097db07ca 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -33,6 +33,7 @@ audio.workspace = true auto_update.workspace = true auto_update_ui.workspace = true backtrace = "0.3" +bincode.workspace = true breadcrumbs.workspace = true call.workspace = true channel.workspace = true @@ -60,6 +61,7 @@ extensions_ui.workspace = true feature_flags.workspace = true feedback.workspace = true file_finder.workspace = true +system_specs.workspace = true fs.workspace = true futures.workspace = true git.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 7ab76b71dee308570ba96d0e2039fcd4c6d16efb..8beefd58912a703097cabc0d2260b9c7ad5258d6 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -16,7 +16,7 @@ use extension_host::ExtensionStore; use fs::{Fs, RealFs}; use futures::{StreamExt, channel::oneshot, future}; use git::GitHostingProviderRegistry; -use gpui::{App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGlobal as _}; +use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, UpdateGlobal as _}; use gpui_tokio::Tokio; use http_client::{Url, read_proxy_from_env}; @@ -240,7 +240,7 @@ pub fn main() { option_env!("ZED_COMMIT_SHA").map(|commit_sha| AppCommitSha::new(commit_sha.to_string())); if args.system_specs { - let system_specs = feedback::system_specs::SystemSpecs::new_stateless( + let system_specs = system_specs::SystemSpecs::new_stateless( app_version, app_commit_sha, *release_channel::RELEASE_CHANNEL, diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index e9acaa588dab8c19aac4c56e8714ecd5242532bb..ac06f1fd9f4d9842eb34a67abeef2432074ecc91 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -89,7 +89,9 @@ pub fn init_panic_hook( }, backtrace, ); - std::process::exit(-1); + if MINIDUMP_ENDPOINT.is_none() { + std::process::exit(-1); + } } let main_module_base_address = get_main_module_base_address(); @@ -148,7 +150,9 @@ pub fn init_panic_hook( } zlog::flush(); - if !is_pty && let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { + if (!is_pty || MINIDUMP_ENDPOINT.is_some()) + && 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 = fs::OpenOptions::new() @@ -614,10 +618,9 @@ async fn upload_minidump( let mut panic_message = "".to_owned(); if let Some(panic_info) = metadata.panic.as_ref() { panic_message = panic_info.message.clone(); - form = form.text("sentry[logentry][formatted]", panic_info.message.clone()); - form = form.text("span", panic_info.span.clone()); - // TODO: add gpu-context, feature-flag-context, and more of device-context like gpu - // name, screen resolution, available ram, device model, etc + form = form + .text("sentry[logentry][formatted]", panic_info.message.clone()) + .text("span", panic_info.span.clone()); } if let Some(minidump_error) = metadata.minidump_error.clone() { form = form.text("minidump_error", minidump_error); @@ -633,6 +636,63 @@ async fn upload_minidump( commit_sha = metadata.init.commit_sha.clone(), ); + let gpu_count = metadata.gpus.len(); + for (index, gpu) in metadata.gpus.iter().cloned().enumerate() { + let system_specs::GpuInfo { + device_name, + device_pci_id, + vendor_name, + vendor_pci_id, + driver_version, + driver_name, + } = gpu; + let num = if gpu_count == 1 && metadata.active_gpu.is_none() { + String::new() + } else { + index.to_string() + }; + let name = format!("gpu{num}"); + let root = format!("sentry[contexts][{name}]"); + form = form + .text( + format!("{root}[Description]"), + "A GPU found on the users system. May or may not be the GPU Zed is running on", + ) + .text(format!("{root}[type]"), "gpu") + .text(format!("{root}[name]"), device_name.unwrap_or(name)) + .text(format!("{root}[id]"), format!("{:#06x}", device_pci_id)) + .text( + format!("{root}[vendor_id]"), + format!("{:#06x}", vendor_pci_id), + ) + .text_if_some(format!("{root}[vendor_name]"), vendor_name) + .text_if_some(format!("{root}[driver_version]"), driver_version) + .text_if_some(format!("{root}[driver_name]"), driver_name); + } + if let Some(active_gpu) = metadata.active_gpu.clone() { + form = form + .text( + "sentry[contexts][Active_GPU][Description]", + "The GPU Zed is running on", + ) + .text("sentry[contexts][Active_GPU][type]", "gpu") + .text("sentry[contexts][Active_GPU][name]", active_gpu.device_name) + .text( + "sentry[contexts][Active_GPU][driver_version]", + active_gpu.driver_info, + ) + .text( + "sentry[contexts][Active_GPU][driver_name]", + active_gpu.driver_name, + ) + .text( + "sentry[contexts][Active_GPU][is_software_emulated]", + active_gpu.is_software_emulated.to_string(), + ); + } + + // TODO: feature-flag-context, and more of device-context like screen resolution, available ram, device model, etc + let mut response_text = String::new(); let mut response = http.send_multipart_form(endpoint, form).await?; response @@ -646,6 +706,27 @@ async fn upload_minidump( Ok(()) } +trait FormExt { + fn text_if_some( + self, + label: impl Into>, + value: Option>>, + ) -> Self; +} + +impl FormExt for Form { + fn text_if_some( + self, + label: impl Into>, + value: Option>>, + ) -> Self { + match value { + Some(value) => self.text(label.into(), value.into()), + None => self, + } + } +} + async fn upload_panic( http: &Arc, panic_report_url: &Url, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 3b5f99f9bda24d03db9791cfec16a97ca3de2345..638e1dca0e261dcb7d66c7ac2b8df9ed9ac78ff9 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -344,7 +344,17 @@ pub fn initialize_workspace( if let Some(specs) = window.gpu_specs() { log::info!("Using GPU: {:?}", specs); - show_software_emulation_warning_if_needed(specs, window, cx); + show_software_emulation_warning_if_needed(specs.clone(), window, cx); + if let Some((crash_server, message)) = crashes::CRASH_HANDLER + .get() + .zip(bincode::serialize(&specs).ok()) + && let Err(err) = crash_server.send_message(3, message) + { + log::warn!( + "Failed to store active gpu info for crash reporting: {}", + err + ); + } } let edit_prediction_menu_handle = PopoverMenuHandle::default(); From ca139b701e20517260baf31602e2840e483b772b Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 21 Aug 2025 19:18:25 -0500 Subject: [PATCH 262/744] keymap_ui: Improve conflict resolution for semantically equal contexts (#36204) Closes #ISSUE Creates a function named `normalized_ctx_eq` that compares `gpui::KeybindContextPredicate`'s while taking into account the associativity of the binary operators. This function is now used to compare context predicates in the keymap editor, greatly improving the number of cases caught by our overloading and conflict detection Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings_ui/src/keybindings.rs | 397 +++++++++++++++++++++++--- 1 file changed, 353 insertions(+), 44 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 9a2d33ef7cf16e87dae90fe8e0b4e3b5283b229c..9c76725972cfeab2751cb05e968f9cf3e7211418 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -12,9 +12,11 @@ use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, - EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyContext, Keystroke, MouseButton, - Point, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, - TextStyleRefinement, WeakEntity, actions, anchored, deferred, div, + EventEmitter, FocusHandle, Focusable, Global, IsZero, + KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or}, + KeyContext, Keystroke, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful, + StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred, + div, }; use language::{Language, LanguageConfig, ToOffset as _}; use notifications::status_toast::{StatusToast, ToastIcon}; @@ -182,15 +184,6 @@ struct KeybindConflict { remaining_conflict_amount: usize, } -impl KeybindConflict { - fn from_iter<'a>(mut indices: impl Iterator) -> Option { - indices.next().map(|origin| Self { - first_conflict_index: origin.index, - remaining_conflict_amount: indices.count(), - }) - } -} - #[derive(Clone, Copy, PartialEq)] struct ConflictOrigin { override_source: KeybindSource, @@ -238,13 +231,21 @@ impl ConflictOrigin { #[derive(Default)] struct ConflictState { conflicts: Vec>, - keybind_mapping: HashMap>, + keybind_mapping: ConflictKeybindMapping, has_user_conflicts: bool, } +type ConflictKeybindMapping = HashMap< + Vec, + Vec<( + Option, + Vec, + )>, +>; + impl ConflictState { fn new(key_bindings: &[ProcessedBinding]) -> Self { - let mut action_keybind_mapping: HashMap<_, Vec> = HashMap::default(); + let mut action_keybind_mapping = ConflictKeybindMapping::default(); let mut largest_index = 0; for (index, binding) in key_bindings @@ -252,29 +253,48 @@ impl ConflictState { .enumerate() .flat_map(|(index, binding)| Some(index).zip(binding.keybind_information())) { - action_keybind_mapping - .entry(binding.get_action_mapping()) - .or_default() - .push(ConflictOrigin::new(binding.source, index)); + let mapping = binding.get_action_mapping(); + let predicate = mapping + .context + .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok()); + let entry = action_keybind_mapping + .entry(mapping.keystrokes) + .or_default(); + let origin = ConflictOrigin::new(binding.source, index); + if let Some((_, origins)) = + entry + .iter_mut() + .find(|(other_predicate, _)| match (&predicate, other_predicate) { + (None, None) => true, + (Some(a), Some(b)) => normalized_ctx_eq(a, b), + _ => false, + }) + { + origins.push(origin); + } else { + entry.push((predicate, vec![origin])); + } largest_index = index; } let mut conflicts = vec![None; largest_index + 1]; let mut has_user_conflicts = false; - for indices in action_keybind_mapping.values_mut() { - indices.sort_unstable_by_key(|origin| origin.override_source); - let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else { - continue; - }; + for entries in action_keybind_mapping.values_mut() { + for (_, indices) in entries.iter_mut() { + indices.sort_unstable_by_key(|origin| origin.override_source); + let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else { + continue; + }; - for origin in indices.iter() { - conflicts[origin.index] = - origin.get_conflict_with(if origin == fst { snd } else { fst }) - } + for origin in indices.iter() { + conflicts[origin.index] = + origin.get_conflict_with(if origin == fst { snd } else { fst }) + } - has_user_conflicts |= fst.override_source == KeybindSource::User - && snd.override_source == KeybindSource::User; + has_user_conflicts |= fst.override_source == KeybindSource::User + && snd.override_source == KeybindSource::User; + } } Self { @@ -289,15 +309,34 @@ impl ConflictState { action_mapping: &ActionMapping, keybind_idx: Option, ) -> Option { - self.keybind_mapping - .get(action_mapping) - .and_then(|indices| { - KeybindConflict::from_iter( - indices + let ActionMapping { + keystrokes, + context, + } = action_mapping; + let predicate = context + .as_deref() + .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok()); + self.keybind_mapping.get(keystrokes).and_then(|entries| { + entries + .iter() + .find_map(|(other_predicate, indices)| { + match (&predicate, other_predicate) { + (None, None) => true, + (Some(pred), Some(other)) => normalized_ctx_eq(pred, other), + _ => false, + } + .then_some(indices) + }) + .and_then(|indices| { + let mut indices = indices .iter() - .filter(|&conflict| Some(conflict.index) != keybind_idx), - ) - }) + .filter(|&conflict| Some(conflict.index) != keybind_idx); + indices.next().map(|origin| KeybindConflict { + first_conflict_index: origin.index, + remaining_conflict_amount: indices.count(), + }) + }) + }) } fn conflict_for_idx(&self, idx: usize) -> Option { @@ -3089,29 +3128,29 @@ fn collect_contexts_from_assets() -> Vec { queue.push(root_context); while let Some(context) = queue.pop() { match context { - gpui::KeyBindingContextPredicate::Identifier(ident) => { + Identifier(ident) => { contexts.insert(ident); } - gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => { + Equal(ident_a, ident_b) => { contexts.insert(ident_a); contexts.insert(ident_b); } - gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => { + NotEqual(ident_a, ident_b) => { contexts.insert(ident_a); contexts.insert(ident_b); } - gpui::KeyBindingContextPredicate::Descendant(ctx_a, ctx_b) => { + Descendant(ctx_a, ctx_b) => { queue.push(*ctx_a); queue.push(*ctx_b); } - gpui::KeyBindingContextPredicate::Not(ctx) => { + Not(ctx) => { queue.push(*ctx); } - gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => { + And(ctx_a, ctx_b) => { queue.push(*ctx_a); queue.push(*ctx_b); } - gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => { + Or(ctx_a, ctx_b) => { queue.push(*ctx_a); queue.push(*ctx_b); } @@ -3126,6 +3165,127 @@ fn collect_contexts_from_assets() -> Vec { contexts } +fn normalized_ctx_eq( + a: &gpui::KeyBindingContextPredicate, + b: &gpui::KeyBindingContextPredicate, +) -> bool { + use gpui::KeyBindingContextPredicate::*; + return match (a, b) { + (Identifier(_), Identifier(_)) => a == b, + (Equal(a_left, a_right), Equal(b_left, b_right)) => { + (a_left == b_left && a_right == b_right) || (a_left == b_right && a_right == b_left) + } + (NotEqual(a_left, a_right), NotEqual(b_left, b_right)) => { + (a_left == b_left && a_right == b_right) || (a_left == b_right && a_right == b_left) + } + (Descendant(a_parent, a_child), Descendant(b_parent, b_child)) => { + normalized_ctx_eq(a_parent, b_parent) && normalized_ctx_eq(a_child, b_child) + } + (Not(a_expr), Not(b_expr)) => normalized_ctx_eq(a_expr, b_expr), + // Handle double negation: !(!a) == a + (Not(a_expr), b) if matches!(a_expr.as_ref(), Not(_)) => { + let Not(a_inner) = a_expr.as_ref() else { + unreachable!(); + }; + normalized_ctx_eq(b, a_inner) + } + (a, Not(b_expr)) if matches!(b_expr.as_ref(), Not(_)) => { + let Not(b_inner) = b_expr.as_ref() else { + unreachable!(); + }; + normalized_ctx_eq(a, b_inner) + } + (And(a_left, a_right), And(b_left, b_right)) + if matches!(a_left.as_ref(), And(_, _)) + || matches!(a_right.as_ref(), And(_, _)) + || matches!(b_left.as_ref(), And(_, _)) + || matches!(b_right.as_ref(), And(_, _)) => + { + let mut a_operands = Vec::new(); + flatten_and(a, &mut a_operands); + let mut b_operands = Vec::new(); + flatten_and(b, &mut b_operands); + compare_operand_sets(&a_operands, &b_operands) + } + (And(a_left, a_right), And(b_left, b_right)) => { + (normalized_ctx_eq(a_left, b_left) && normalized_ctx_eq(a_right, b_right)) + || (normalized_ctx_eq(a_left, b_right) && normalized_ctx_eq(a_right, b_left)) + } + (Or(a_left, a_right), Or(b_left, b_right)) + if matches!(a_left.as_ref(), Or(_, _)) + || matches!(a_right.as_ref(), Or(_, _)) + || matches!(b_left.as_ref(), Or(_, _)) + || matches!(b_right.as_ref(), Or(_, _)) => + { + let mut a_operands = Vec::new(); + flatten_or(a, &mut a_operands); + let mut b_operands = Vec::new(); + flatten_or(b, &mut b_operands); + compare_operand_sets(&a_operands, &b_operands) + } + (Or(a_left, a_right), Or(b_left, b_right)) => { + (normalized_ctx_eq(a_left, b_left) && normalized_ctx_eq(a_right, b_right)) + || (normalized_ctx_eq(a_left, b_right) && normalized_ctx_eq(a_right, b_left)) + } + _ => false, + }; + + fn flatten_and<'a>( + pred: &'a gpui::KeyBindingContextPredicate, + operands: &mut Vec<&'a gpui::KeyBindingContextPredicate>, + ) { + use gpui::KeyBindingContextPredicate::*; + match pred { + And(left, right) => { + flatten_and(left, operands); + flatten_and(right, operands); + } + _ => operands.push(pred), + } + } + + fn flatten_or<'a>( + pred: &'a gpui::KeyBindingContextPredicate, + operands: &mut Vec<&'a gpui::KeyBindingContextPredicate>, + ) { + use gpui::KeyBindingContextPredicate::*; + match pred { + Or(left, right) => { + flatten_or(left, operands); + flatten_or(right, operands); + } + _ => operands.push(pred), + } + } + + fn compare_operand_sets( + a: &[&gpui::KeyBindingContextPredicate], + b: &[&gpui::KeyBindingContextPredicate], + ) -> bool { + if a.len() != b.len() { + return false; + } + + // For each operand in a, find a matching operand in b + let mut b_matched = vec![false; b.len()]; + for a_operand in a { + let mut found = false; + for (b_idx, b_operand) in b.iter().enumerate() { + if !b_matched[b_idx] && normalized_ctx_eq(a_operand, b_operand) { + b_matched[b_idx] = true; + found = true; + break; + } + } + if !found { + return false; + } + } + + true + } +} + impl SerializableItem for KeymapEditor { fn serialized_item_kind() -> &'static str { "KeymapEditor" @@ -3228,3 +3388,152 @@ mod persistence { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalized_ctx_cmp() { + #[track_caller] + fn cmp(a: &str, b: &str) -> bool { + let a = gpui::KeyBindingContextPredicate::parse(a) + .expect("Failed to parse keybinding context a"); + let b = gpui::KeyBindingContextPredicate::parse(b) + .expect("Failed to parse keybinding context b"); + normalized_ctx_eq(&a, &b) + } + + // Basic equality - identical expressions + assert!(cmp("a && b", "a && b")); + assert!(cmp("a || b", "a || b")); + assert!(cmp("a == b", "a == b")); + assert!(cmp("a != b", "a != b")); + assert!(cmp("a > b", "a > b")); + assert!(cmp("!a", "!a")); + + // AND operator - associative/commutative + assert!(cmp("a && b", "b && a")); + assert!(cmp("a && b && c", "c && b && a")); + assert!(cmp("a && b && c", "b && a && c")); + assert!(cmp("a && b && c && d", "d && c && b && a")); + + // OR operator - associative/commutative + assert!(cmp("a || b", "b || a")); + assert!(cmp("a || b || c", "c || b || a")); + assert!(cmp("a || b || c", "b || a || c")); + assert!(cmp("a || b || c || d", "d || c || b || a")); + + // Equality operator - associative/commutative + assert!(cmp("a == b", "b == a")); + assert!(cmp("x == y", "y == x")); + + // Inequality operator - associative/commutative + assert!(cmp("a != b", "b != a")); + assert!(cmp("x != y", "y != x")); + + // Complex nested expressions with associative operators + assert!(cmp("(a && b) || c", "c || (a && b)")); + assert!(cmp("(a && b) || c", "c || (b && a)")); + assert!(cmp("(a || b) && c", "c && (a || b)")); + assert!(cmp("(a || b) && c", "c && (b || a)")); + assert!(cmp("(a && b) || (c && d)", "(c && d) || (a && b)")); + assert!(cmp("(a && b) || (c && d)", "(d && c) || (b && a)")); + + // Multiple levels of nesting + assert!(cmp("((a && b) || c) && d", "d && ((a && b) || c)")); + assert!(cmp("((a && b) || c) && d", "d && (c || (b && a))")); + assert!(cmp("a && (b || (c && d))", "(b || (c && d)) && a")); + assert!(cmp("a && (b || (c && d))", "(b || (d && c)) && a")); + + // Negation with associative operators + assert!(cmp("!a && b", "b && !a")); + assert!(cmp("!a || b", "b || !a")); + assert!(cmp("!(a && b) || c", "c || !(a && b)")); + assert!(cmp("!(a && b) || c", "c || !(b && a)")); + + // Descendant operator (>) - NOT associative/commutative + assert!(cmp("a > b", "a > b")); + assert!(!cmp("a > b", "b > a")); + assert!(!cmp("a > b > c", "c > b > a")); + assert!(!cmp("a > b > c", "a > c > b")); + + // Mixed operators with descendant + assert!(cmp("(a > b) && c", "c && (a > b)")); + assert!(!cmp("(a > b) && c", "c && (b > a)")); + assert!(cmp("(a > b) || (c > d)", "(c > d) || (a > b)")); + assert!(!cmp("(a > b) || (c > d)", "(b > a) || (d > c)")); + + // Negative cases - different operators + assert!(!cmp("a && b", "a || b")); + assert!(!cmp("a == b", "a != b")); + assert!(!cmp("a && b", "a > b")); + assert!(!cmp("a || b", "a > b")); + assert!(!cmp("a == b", "a && b")); + assert!(!cmp("a != b", "a || b")); + + // Negative cases - different operands + assert!(!cmp("a && b", "a && c")); + assert!(!cmp("a && b", "c && d")); + assert!(!cmp("a || b", "a || c")); + assert!(!cmp("a || b", "c || d")); + assert!(!cmp("a == b", "a == c")); + assert!(!cmp("a != b", "a != c")); + assert!(!cmp("a > b", "a > c")); + assert!(!cmp("a > b", "c > b")); + + // Negative cases - with negation + assert!(!cmp("!a", "a")); + assert!(!cmp("!a && b", "a && b")); + assert!(!cmp("!(a && b)", "a && b")); + assert!(!cmp("!a || b", "a || b")); + assert!(!cmp("!(a || b)", "a || b")); + + // Negative cases - complex expressions + assert!(!cmp("(a && b) || c", "(a || b) && c")); + assert!(!cmp("a && (b || c)", "a || (b && c)")); + assert!(!cmp("(a && b) || (c && d)", "(a || b) && (c || d)")); + assert!(!cmp("a > b && c", "a && b > c")); + + // Edge cases - multiple same operands + assert!(cmp("a && a", "a && a")); + assert!(cmp("a || a", "a || a")); + assert!(cmp("a && a && b", "b && a && a")); + assert!(cmp("a || a || b", "b || a || a")); + + // Edge cases - deeply nested + assert!(cmp( + "((a && b) || (c && d)) && ((e || f) && g)", + "((e || f) && g) && ((c && d) || (a && b))" + )); + assert!(cmp( + "((a && b) || (c && d)) && ((e || f) && g)", + "(g && (f || e)) && ((d && c) || (b && a))" + )); + + // Edge cases - repeated patterns + assert!(cmp("(a && b) || (a && b)", "(b && a) || (b && a)")); + assert!(cmp("(a || b) && (a || b)", "(b || a) && (b || a)")); + + // Negative cases - subtle differences + assert!(!cmp("a && b && c", "a && b")); + assert!(!cmp("a || b || c", "a || b")); + assert!(!cmp("(a && b) || c", "a && (b || c)")); + + // a > b > c is not the same as a > c, should not be equal + assert!(!cmp("a > b > c", "a > c")); + + // Double negation with complex expressions + assert!(cmp("!(!(a && b))", "a && b")); + assert!(cmp("!(!(a || b))", "a || b")); + assert!(cmp("!(!(a > b))", "a > b")); + assert!(cmp("!(!a) && b", "a && b")); + assert!(cmp("!(!a) || b", "a || b")); + assert!(cmp("!(!(a && b)) || c", "(a && b) || c")); + assert!(cmp("!(!(a && b)) || c", "(b && a) || c")); + assert!(cmp("!(!a)", "a")); + assert!(cmp("a", "!(!a)")); + assert!(cmp("!(!(!a))", "!a")); + assert!(cmp("!(!(!(!a)))", "a")); + } +} From e1a96b68f0e3d995e57cd7ca5c7d8fd5b313944d Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 21 Aug 2025 17:37:41 -0700 Subject: [PATCH 263/744] acp: Tool name prep (#36726) Prep work for deduping tool names Release Notes: - N/A --- crates/agent2/src/agent.rs | 2 - crates/agent2/src/tests/mod.rs | 55 +++++++++---------- crates/agent2/src/tests/test_tools.rs | 30 +++++----- crates/agent2/src/thread.rs | 16 +++--- crates/agent2/src/tools.rs | 25 +++++++++ crates/agent2/src/tools/copy_path_tool.rs | 8 +-- .../agent2/src/tools/create_directory_tool.rs | 6 +- crates/agent2/src/tools/delete_path_tool.rs | 6 +- crates/agent2/src/tools/diagnostics_tool.rs | 6 +- crates/agent2/src/tools/edit_file_tool.rs | 29 ++-------- crates/agent2/src/tools/fetch_tool.rs | 6 +- crates/agent2/src/tools/find_path_tool.rs | 6 +- crates/agent2/src/tools/grep_tool.rs | 6 +- .../agent2/src/tools/list_directory_tool.rs | 6 +- crates/agent2/src/tools/move_path_tool.rs | 6 +- crates/agent2/src/tools/now_tool.rs | 6 +- crates/agent2/src/tools/open_tool.rs | 6 +- crates/agent2/src/tools/read_file_tool.rs | 6 +- crates/agent2/src/tools/terminal_tool.rs | 6 +- crates/agent2/src/tools/thinking_tool.rs | 6 +- crates/agent2/src/tools/web_search_tool.rs | 6 +- 21 files changed, 126 insertions(+), 123 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index bbc30b74bc3cd29432a6297e8e6ddca4822d42c0..215f8f454b0e26dee673e3f5057229f130a2e567 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -857,7 +857,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection { cx.spawn(async move |cx| { log::debug!("Starting thread creation in async context"); - let action_log = cx.new(|_cx| ActionLog::new(project.clone()))?; // Create Thread let thread = agent.update( cx, @@ -878,7 +877,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection { project.clone(), agent.project_context.clone(), agent.context_server_registry.clone(), - action_log.clone(), agent.templates.clone(), default_model, cx, diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index e7e28f495e88e496f4a43e5314a811da112f5248..ac7b40c64f678d9ce5318d4b1921fc4de630029f 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1,6 +1,5 @@ use super::*; use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList, UserMessageId}; -use action_log::ActionLog; use agent_client_protocol::{self as acp}; use agent_settings::AgentProfileId; use anyhow::Result; @@ -224,7 +223,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { let tool_use = LanguageModelToolUse { id: "tool_1".into(), - name: EchoTool.name().into(), + name: EchoTool::name().into(), raw_input: json!({"text": "test"}).to_string(), input: json!({"text": "test"}), is_input_complete: true, @@ -237,7 +236,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { let completion = fake_model.pending_completions().pop().unwrap(); let tool_result = LanguageModelToolResult { tool_use_id: "tool_1".into(), - tool_name: EchoTool.name().into(), + tool_name: EchoTool::name().into(), is_error: false, content: "test".into(), output: Some("test".into()), @@ -307,7 +306,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { // Test a tool calls that's likely to complete *after* streaming stops. let events = thread .update(cx, |thread, cx| { - thread.remove_tool(&AgentTool::name(&EchoTool)); + thread.remove_tool(&EchoTool::name()); thread.add_tool(DelayTool); thread.send( UserMessageId::new(), @@ -411,7 +410,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "tool_id_1".into(), - name: ToolRequiringPermission.name().into(), + name: ToolRequiringPermission::name().into(), raw_input: "{}".into(), input: json!({}), is_input_complete: true, @@ -420,7 +419,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "tool_id_2".into(), - name: ToolRequiringPermission.name().into(), + name: ToolRequiringPermission::name().into(), raw_input: "{}".into(), input: json!({}), is_input_complete: true, @@ -451,14 +450,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { vec![ language_model::MessageContent::ToolResult(LanguageModelToolResult { tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(), - tool_name: ToolRequiringPermission.name().into(), + tool_name: ToolRequiringPermission::name().into(), is_error: false, content: "Allowed".into(), output: Some("Allowed".into()) }), language_model::MessageContent::ToolResult(LanguageModelToolResult { tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(), - tool_name: ToolRequiringPermission.name().into(), + tool_name: ToolRequiringPermission::name().into(), is_error: true, content: "Permission to run tool denied by user".into(), output: None @@ -470,7 +469,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "tool_id_3".into(), - name: ToolRequiringPermission.name().into(), + name: ToolRequiringPermission::name().into(), raw_input: "{}".into(), input: json!({}), is_input_complete: true, @@ -492,7 +491,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { vec![language_model::MessageContent::ToolResult( LanguageModelToolResult { tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(), - tool_name: ToolRequiringPermission.name().into(), + tool_name: ToolRequiringPermission::name().into(), is_error: false, content: "Allowed".into(), output: Some("Allowed".into()) @@ -504,7 +503,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "tool_id_4".into(), - name: ToolRequiringPermission.name().into(), + name: ToolRequiringPermission::name().into(), raw_input: "{}".into(), input: json!({}), is_input_complete: true, @@ -519,7 +518,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { vec![language_model::MessageContent::ToolResult( LanguageModelToolResult { tool_use_id: "tool_id_4".into(), - tool_name: ToolRequiringPermission.name().into(), + tool_name: ToolRequiringPermission::name().into(), is_error: false, content: "Allowed".into(), output: Some("Allowed".into()) @@ -571,7 +570,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { cx.run_until_parked(); let tool_use = LanguageModelToolUse { id: "tool_id_1".into(), - name: EchoTool.name().into(), + name: EchoTool::name().into(), raw_input: "{}".into(), input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), is_input_complete: true, @@ -584,7 +583,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { let completion = fake_model.pending_completions().pop().unwrap(); let tool_result = LanguageModelToolResult { tool_use_id: "tool_id_1".into(), - tool_name: EchoTool.name().into(), + tool_name: EchoTool::name().into(), is_error: false, content: "def".into(), output: Some("def".into()), @@ -690,14 +689,14 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) { let tool_use = LanguageModelToolUse { id: "tool_id_1".into(), - name: EchoTool.name().into(), + name: EchoTool::name().into(), raw_input: "{}".into(), input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), is_input_complete: true, }; let tool_result = LanguageModelToolResult { tool_use_id: "tool_id_1".into(), - tool_name: EchoTool.name().into(), + tool_name: EchoTool::name().into(), is_error: false, content: "def".into(), output: Some("def".into()), @@ -874,14 +873,14 @@ async fn test_profiles(cx: &mut TestAppContext) { "test-1": { "name": "Test Profile 1", "tools": { - EchoTool.name(): true, - DelayTool.name(): true, + EchoTool::name(): true, + DelayTool::name(): true, } }, "test-2": { "name": "Test Profile 2", "tools": { - InfiniteTool.name(): true, + InfiniteTool::name(): true, } } } @@ -910,7 +909,7 @@ async fn test_profiles(cx: &mut TestAppContext) { .iter() .map(|tool| tool.name.clone()) .collect(); - assert_eq!(tool_names, vec![DelayTool.name(), EchoTool.name()]); + assert_eq!(tool_names, vec![DelayTool::name(), EchoTool::name()]); fake_model.end_last_completion_stream(); // Switch to test-2 profile, and verify that it has only the infinite tool. @@ -929,7 +928,7 @@ async fn test_profiles(cx: &mut TestAppContext) { .iter() .map(|tool| tool.name.clone()) .collect(); - assert_eq!(tool_names, vec![InfiniteTool.name()]); + assert_eq!(tool_names, vec![InfiniteTool::name()]); } #[gpui::test] @@ -1552,7 +1551,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "1".into(), - name: ThinkingTool.name().into(), + name: ThinkingTool::name().into(), raw_input: input.to_string(), input, is_input_complete: false, @@ -1840,11 +1839,11 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { "test-profile": { "name": "Test Profile", "tools": { - EchoTool.name(): true, - DelayTool.name(): true, - WordListTool.name(): true, - ToolRequiringPermission.name(): true, - InfiniteTool.name(): true, + EchoTool::name(): true, + DelayTool::name(): true, + WordListTool::name(): true, + ToolRequiringPermission::name(): true, + InfiniteTool::name(): true, } } } @@ -1903,13 +1902,11 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { let project_context = cx.new(|_cx| ProjectContext::default()); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let thread = cx.new(|cx| { Thread::new( project, project_context.clone(), context_server_registry, - action_log, templates, Some(model.clone()), cx, diff --git a/crates/agent2/src/tests/test_tools.rs b/crates/agent2/src/tests/test_tools.rs index cbff44cedfc28a0d24ea4fa12e3ac71c9135c0d8..27be7b6ac384219cdd06e6dc971078c3ff0b9a7b 100644 --- a/crates/agent2/src/tests/test_tools.rs +++ b/crates/agent2/src/tests/test_tools.rs @@ -16,11 +16,11 @@ impl AgentTool for EchoTool { type Input = EchoToolInput; type Output = String; - fn name(&self) -> SharedString { - "echo".into() + fn name() -> &'static str { + "echo" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } @@ -51,8 +51,8 @@ impl AgentTool for DelayTool { type Input = DelayToolInput; type Output = String; - fn name(&self) -> SharedString { - "delay".into() + fn name() -> &'static str { + "delay" } fn initial_title(&self, input: Result) -> SharedString { @@ -63,7 +63,7 @@ impl AgentTool for DelayTool { } } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } @@ -92,11 +92,11 @@ impl AgentTool for ToolRequiringPermission { type Input = ToolRequiringPermissionInput; type Output = String; - fn name(&self) -> SharedString { - "tool_requiring_permission".into() + fn name() -> &'static str { + "tool_requiring_permission" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } @@ -127,11 +127,11 @@ impl AgentTool for InfiniteTool { type Input = InfiniteToolInput; type Output = String; - fn name(&self) -> SharedString { - "infinite".into() + fn name() -> &'static str { + "infinite" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } @@ -178,11 +178,11 @@ impl AgentTool for WordListTool { type Input = WordListInput; type Output = String; - fn name(&self) -> SharedString { - "word_list".into() + fn name() -> &'static str { + "word_list" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index f6ef11c20bab5e8699b9ab80a7a6c585de801212..af18afa05583dc65b7e0b7e90696a6754c2de3ee 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -544,12 +544,12 @@ impl Thread { project: Entity, project_context: Entity, context_server_registry: Entity, - action_log: Entity, templates: Arc, model: Option>, cx: &mut Context, ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); + let action_log = cx.new(|_cx| ActionLog::new(project.clone())); Self { id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()), prompt_id: PromptId::new(), @@ -959,11 +959,11 @@ impl Thread { )); self.add_tool(TerminalTool::new(self.project.clone(), cx)); self.add_tool(ThinkingTool); - self.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model. + self.add_tool(WebSearchTool); } - pub fn add_tool(&mut self, tool: impl AgentTool) { - self.tools.insert(tool.name(), tool.erase()); + pub fn add_tool(&mut self, tool: T) { + self.tools.insert(T::name().into(), tool.erase()); } pub fn remove_tool(&mut self, name: &str) -> bool { @@ -1989,7 +1989,7 @@ where type Input: for<'de> Deserialize<'de> + Serialize + JsonSchema; type Output: for<'de> Deserialize<'de> + Serialize + Into; - fn name(&self) -> SharedString; + fn name() -> &'static str; fn description(&self) -> SharedString { let schema = schemars::schema_for!(Self::Input); @@ -2001,7 +2001,7 @@ where ) } - fn kind(&self) -> acp::ToolKind; + fn kind() -> acp::ToolKind; /// The initial tool title to display. Can be updated during the tool run. fn initial_title(&self, input: Result) -> SharedString; @@ -2077,7 +2077,7 @@ where T: AgentTool, { fn name(&self) -> SharedString { - self.0.name() + T::name().into() } fn description(&self) -> SharedString { @@ -2085,7 +2085,7 @@ where } fn kind(&self) -> agent_client_protocol::ToolKind { - self.0.kind() + T::kind() } fn initial_title(&self, input: serde_json::Value) -> SharedString { diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs index d1f2b3b1c7ad3ed7ade2324c61c1e72d7e7e4006..bcca7eecd185b9381afded26fb573d14f50bc5be 100644 --- a/crates/agent2/src/tools.rs +++ b/crates/agent2/src/tools.rs @@ -16,6 +16,29 @@ mod terminal_tool; mod thinking_tool; mod web_search_tool; +/// A list of all built in tool names, for use in deduplicating MCP tool names +pub fn default_tool_names() -> impl Iterator { + [ + CopyPathTool::name(), + CreateDirectoryTool::name(), + DeletePathTool::name(), + DiagnosticsTool::name(), + EditFileTool::name(), + FetchTool::name(), + FindPathTool::name(), + GrepTool::name(), + ListDirectoryTool::name(), + MovePathTool::name(), + NowTool::name(), + OpenTool::name(), + ReadFileTool::name(), + TerminalTool::name(), + ThinkingTool::name(), + WebSearchTool::name(), + ] + .into_iter() +} + pub use context_server_registry::*; pub use copy_path_tool::*; pub use create_directory_tool::*; @@ -33,3 +56,5 @@ pub use read_file_tool::*; pub use terminal_tool::*; pub use thinking_tool::*; pub use web_search_tool::*; + +use crate::AgentTool; diff --git a/crates/agent2/src/tools/copy_path_tool.rs b/crates/agent2/src/tools/copy_path_tool.rs index 4b40a9842f69cb87977ae948c4f858a2540b2eb1..819a6ff20931a42f892d60df91f665aac3694401 100644 --- a/crates/agent2/src/tools/copy_path_tool.rs +++ b/crates/agent2/src/tools/copy_path_tool.rs @@ -1,7 +1,7 @@ use crate::{AgentTool, ToolCallEventStream}; use agent_client_protocol::ToolKind; use anyhow::{Context as _, Result, anyhow}; -use gpui::{App, AppContext, Entity, SharedString, Task}; +use gpui::{App, AppContext, Entity, Task}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -50,11 +50,11 @@ impl AgentTool for CopyPathTool { type Input = CopyPathToolInput; type Output = String; - fn name(&self) -> SharedString { - "copy_path".into() + fn name() -> &'static str { + "copy_path" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Move } diff --git a/crates/agent2/src/tools/create_directory_tool.rs b/crates/agent2/src/tools/create_directory_tool.rs index 7720eb3595d475560b90cc890396b8f6b27600b1..652363d5fa2320819076f5465b701eec04d9cd9f 100644 --- a/crates/agent2/src/tools/create_directory_tool.rs +++ b/crates/agent2/src/tools/create_directory_tool.rs @@ -41,11 +41,11 @@ impl AgentTool for CreateDirectoryTool { type Input = CreateDirectoryToolInput; type Output = String; - fn name(&self) -> SharedString { - "create_directory".into() + fn name() -> &'static str { + "create_directory" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Read } diff --git a/crates/agent2/src/tools/delete_path_tool.rs b/crates/agent2/src/tools/delete_path_tool.rs index c281f1b5b69e2f3cbc3282a17298e9002f3b7c52..0f9641127f1ffdbfec72d6d404acf5186a2bf12f 100644 --- a/crates/agent2/src/tools/delete_path_tool.rs +++ b/crates/agent2/src/tools/delete_path_tool.rs @@ -44,11 +44,11 @@ impl AgentTool for DeletePathTool { type Input = DeletePathToolInput; type Output = String; - fn name(&self) -> SharedString { - "delete_path".into() + fn name() -> &'static str { + "delete_path" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Delete } diff --git a/crates/agent2/src/tools/diagnostics_tool.rs b/crates/agent2/src/tools/diagnostics_tool.rs index 6ba8b7b377a770fa3af35b725b4427e7102d70c1..558bb918ced71a1777dde919a59de9eab4129d45 100644 --- a/crates/agent2/src/tools/diagnostics_tool.rs +++ b/crates/agent2/src/tools/diagnostics_tool.rs @@ -63,11 +63,11 @@ impl AgentTool for DiagnosticsTool { type Input = DiagnosticsToolInput; type Output = String; - fn name(&self) -> SharedString { - "diagnostics".into() + fn name() -> &'static str { + "diagnostics" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Read } diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index f89cace9a84e1f9a877daf9a60503b8d78c8c336..5a68d0c70a04aea7367b8264c34d139d3602cc44 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -186,11 +186,11 @@ impl AgentTool for EditFileTool { type Input = EditFileToolInput; type Output = EditFileToolOutput; - fn name(&self) -> SharedString { - "edit_file".into() + fn name() -> &'static str { + "edit_file" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Edit } @@ -517,7 +517,6 @@ fn resolve_path( mod tests { use super::*; use crate::{ContextServerRegistry, Templates}; - use action_log::ActionLog; use client::TelemetrySettings; use fs::Fs; use gpui::{TestAppContext, UpdateGlobal}; @@ -535,7 +534,6 @@ mod tests { fs.insert_tree("/root", json!({})).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -544,7 +542,6 @@ mod tests { project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log, Templates::new(), Some(model), cx, @@ -735,7 +732,6 @@ mod tests { } }); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -744,7 +740,6 @@ mod tests { project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -801,7 +796,9 @@ mod tests { "Code should be formatted when format_on_save is enabled" ); - let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count()); + let stale_buffer_count = thread + .read_with(cx, |thread, _cx| thread.action_log.clone()) + .read_with(cx, |log, cx| log.stale_buffers(cx).count()); assert_eq!( stale_buffer_count, 0, @@ -879,14 +876,12 @@ mod tests { let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { Thread::new( project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1008,14 +1003,12 @@ mod tests { let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { Thread::new( project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1146,14 +1139,12 @@ mod tests { let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { Thread::new( project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1254,7 +1245,6 @@ mod tests { ) .await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -1263,7 +1253,6 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1336,7 +1325,6 @@ mod tests { .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -1345,7 +1333,6 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1421,7 +1408,6 @@ mod tests { .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -1430,7 +1416,6 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1503,7 +1488,6 @@ mod tests { let fs = project::FakeFs::new(cx.executor()); let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -1512,7 +1496,6 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, diff --git a/crates/agent2/src/tools/fetch_tool.rs b/crates/agent2/src/tools/fetch_tool.rs index ae26c5fe195da3d73a8ae1da47d072a3bfc3706f..0313c4e4c2f03a2ec40afdf9325ca2c84c45cf3c 100644 --- a/crates/agent2/src/tools/fetch_tool.rs +++ b/crates/agent2/src/tools/fetch_tool.rs @@ -118,11 +118,11 @@ impl AgentTool for FetchTool { type Input = FetchToolInput; type Output = String; - fn name(&self) -> SharedString { - "fetch".into() + fn name() -> &'static str { + "fetch" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Fetch } diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index 9e11ca6a37d92d6c189e542611fbc396128e6a15..5b35c40f859099f8fc49d2664fb8fec63dbf36ee 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -85,11 +85,11 @@ impl AgentTool for FindPathTool { type Input = FindPathToolInput; type Output = FindPathToolOutput; - fn name(&self) -> SharedString { - "find_path".into() + fn name() -> &'static str { + "find_path" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Search } diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent2/src/tools/grep_tool.rs index 955dae723558c3b8b3324109c18e9448215d66a3..b24e773903e76fe8e11287d054dd758670669ca2 100644 --- a/crates/agent2/src/tools/grep_tool.rs +++ b/crates/agent2/src/tools/grep_tool.rs @@ -67,11 +67,11 @@ impl AgentTool for GrepTool { type Input = GrepToolInput; type Output = String; - fn name(&self) -> SharedString { - "grep".into() + fn name() -> &'static str { + "grep" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Search } diff --git a/crates/agent2/src/tools/list_directory_tool.rs b/crates/agent2/src/tools/list_directory_tool.rs index 31575a92e44aa66558175b078b6c8e6087a67653..e6fa8d743122ec117f4307a1c4a37ddd79bd574a 100644 --- a/crates/agent2/src/tools/list_directory_tool.rs +++ b/crates/agent2/src/tools/list_directory_tool.rs @@ -51,11 +51,11 @@ impl AgentTool for ListDirectoryTool { type Input = ListDirectoryToolInput; type Output = String; - fn name(&self) -> SharedString { - "list_directory".into() + fn name() -> &'static str { + "list_directory" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Read } diff --git a/crates/agent2/src/tools/move_path_tool.rs b/crates/agent2/src/tools/move_path_tool.rs index 2a173a4404fc344051d238c6ba377e63ec2d9acc..d9fb60651b8cbb9d38713d660cf2e43070ef1f53 100644 --- a/crates/agent2/src/tools/move_path_tool.rs +++ b/crates/agent2/src/tools/move_path_tool.rs @@ -52,11 +52,11 @@ impl AgentTool for MovePathTool { type Input = MovePathToolInput; type Output = String; - fn name(&self) -> SharedString { - "move_path".into() + fn name() -> &'static str { + "move_path" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Move } diff --git a/crates/agent2/src/tools/now_tool.rs b/crates/agent2/src/tools/now_tool.rs index a72ede26fea1ee42eddb08e2b22b2b2b89c77075..9467e7db68b7338d2f0ccd941aeaa551368eb1e1 100644 --- a/crates/agent2/src/tools/now_tool.rs +++ b/crates/agent2/src/tools/now_tool.rs @@ -32,11 +32,11 @@ impl AgentTool for NowTool { type Input = NowToolInput; type Output = String; - fn name(&self) -> SharedString { - "now".into() + fn name() -> &'static str { + "now" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } diff --git a/crates/agent2/src/tools/open_tool.rs b/crates/agent2/src/tools/open_tool.rs index c20369c2d80cadc99f60c8335b5ecdbef763bf5f..df7b04c787df27cb8f4f1fccac0017b8d71994a8 100644 --- a/crates/agent2/src/tools/open_tool.rs +++ b/crates/agent2/src/tools/open_tool.rs @@ -37,11 +37,11 @@ impl AgentTool for OpenTool { type Input = OpenToolInput; type Output = String; - fn name(&self) -> SharedString { - "open".into() + fn name() -> &'static str { + "open" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Execute } diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index 11a57506fb6454ad3c527e68f43089f5b80216e1..903e1582ac4dec2b8060b7070368991c865c716c 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -59,11 +59,11 @@ impl AgentTool for ReadFileTool { type Input = ReadFileToolInput; type Output = LanguageModelToolResultContent; - fn name(&self) -> SharedString { - "read_file".into() + fn name() -> &'static str { + "read_file" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Read } diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index 3d4faf2e031aca522af8c7787b05bf302e45969e..f41b909d0b286b80bb3c9e8e8c18d0d03f3e05c7 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -63,11 +63,11 @@ impl AgentTool for TerminalTool { type Input = TerminalToolInput; type Output = String; - fn name(&self) -> SharedString { - "terminal".into() + fn name() -> &'static str { + "terminal" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Execute } diff --git a/crates/agent2/src/tools/thinking_tool.rs b/crates/agent2/src/tools/thinking_tool.rs index c5e94511621768868cd56f165b4f50c903874e0d..61fb9eb0d6ea95f1aa299f1d226c7f2c5b750767 100644 --- a/crates/agent2/src/tools/thinking_tool.rs +++ b/crates/agent2/src/tools/thinking_tool.rs @@ -21,11 +21,11 @@ impl AgentTool for ThinkingTool { type Input = ThinkingToolInput; type Output = String; - fn name(&self) -> SharedString { - "thinking".into() + fn name() -> &'static str { + "thinking" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Think } diff --git a/crates/agent2/src/tools/web_search_tool.rs b/crates/agent2/src/tools/web_search_tool.rs index ffcd4ad3becf0417aa6175808614c71abdd95e8e..d7a34bec29e10476b31051d71d6d2f74b640ad5d 100644 --- a/crates/agent2/src/tools/web_search_tool.rs +++ b/crates/agent2/src/tools/web_search_tool.rs @@ -40,11 +40,11 @@ impl AgentTool for WebSearchTool { type Input = WebSearchToolInput; type Output = WebSearchToolOutput; - fn name(&self) -> SharedString { - "web_search".into() + fn name() -> &'static str { + "web_search" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Fetch } From f5fd4ac6701239ec7620651bb2185fc3b1774bfa Mon Sep 17 00:00:00 2001 From: Kaem <46230985+kaem-e@users.noreply.github.com> Date: Fri, 22 Aug 2025 08:02:47 +0500 Subject: [PATCH 264/744] vim: Implement partial increment/decrement for visual selection (#36553) This change adds the ability to increment / decrement numbers that are part of a visual selection. Previously Zed would resolve to the entire number under visual selection for increment as oppposed to only incrementing the part of the number that is selected Release Notes: - vim: Fixed increment/decrement in visual mode --- crates/vim/src/normal/increment.rs | 111 +++++++++++++++++- .../test_increment_visual_partial_number.json | 20 ++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 crates/vim/test_data/test_increment_visual_partial_number.json diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 1d2a4e9b6180c8130f2126053e5f54694a6d4081..34ac4aab1f11c547ed1335e1a9da12fe52be9b08 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -70,8 +70,19 @@ impl Vim { } else { Point::new(row, 0) }; + let end = if row == selection.end.row { + selection.end + } else { + Point::new(row, snapshot.line_len(multi_buffer::MultiBufferRow(row))) + }; + + let number_result = if !selection.is_empty() { + find_number_in_range(&snapshot, start, end) + } else { + find_number(&snapshot, start) + }; - if let Some((range, num, radix)) = find_number(&snapshot, start) { + if let Some((range, num, radix)) = number_result { let replace = match radix { 10 => increment_decimal_string(&num, delta), 16 => increment_hex_string(&num, delta), @@ -189,6 +200,90 @@ fn increment_binary_string(num: &str, delta: i64) -> String { format!("{:0width$b}", result, width = num.len()) } +fn find_number_in_range( + snapshot: &MultiBufferSnapshot, + start: Point, + end: Point, +) -> Option<(Range, String, u32)> { + let start_offset = start.to_offset(snapshot); + let end_offset = end.to_offset(snapshot); + + let mut offset = start_offset; + + // Backward scan to find the start of the number, but stop at start_offset + for ch in snapshot.reversed_chars_at(offset) { + if ch.is_ascii_hexdigit() || ch == '-' || ch == 'b' || ch == 'x' { + if offset == 0 { + break; + } + offset -= ch.len_utf8(); + if offset < start_offset { + offset = start_offset; + break; + } + } else { + break; + } + } + + let mut begin = None; + let mut end_num = None; + let mut num = String::new(); + let mut radix = 10; + + let mut chars = snapshot.chars_at(offset).peekable(); + + while let Some(ch) = chars.next() { + if offset >= end_offset { + break; // stop at end of selection + } + + if num == "0" && ch == 'b' && chars.peek().is_some() && chars.peek().unwrap().is_digit(2) { + radix = 2; + begin = None; + num = String::new(); + } else if num == "0" + && ch == 'x' + && chars.peek().is_some() + && chars.peek().unwrap().is_ascii_hexdigit() + { + radix = 16; + begin = None; + num = String::new(); + } + + if ch.is_digit(radix) + || (begin.is_none() + && ch == '-' + && chars.peek().is_some() + && chars.peek().unwrap().is_digit(radix)) + { + if begin.is_none() { + begin = Some(offset); + } + num.push(ch); + } else if begin.is_some() { + end_num = Some(offset); + break; + } else if ch == '\n' { + break; + } + + offset += ch.len_utf8(); + } + + if let Some(begin) = begin { + let end_num = end_num.unwrap_or(offset); + Some(( + begin.to_point(snapshot)..end_num.to_point(snapshot), + num, + radix, + )) + } else { + None + } +} + fn find_number( snapshot: &MultiBufferSnapshot, start: Point, @@ -764,4 +859,18 @@ mod test { cx.simulate_keystrokes("v b ctrl-a"); cx.assert_state("let enabled = ˇOff;", Mode::Normal); } + + #[gpui::test] + async fn test_increment_visual_partial_number(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇ123").await; + cx.simulate_shared_keystrokes("v l ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {"ˇ133"}); + cx.simulate_shared_keystrokes("l v l ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {"1ˇ34"}); + cx.simulate_shared_keystrokes("shift-v y p p ctrl-v k k l ctrl-a") + .await; + cx.shared_state().await.assert_eq(indoc! {"ˇ144\n144\n144"}); + } } diff --git a/crates/vim/test_data/test_increment_visual_partial_number.json b/crates/vim/test_data/test_increment_visual_partial_number.json new file mode 100644 index 0000000000000000000000000000000000000000..ebb4eece7866fb64a93c2980144a4289a6ada53c --- /dev/null +++ b/crates/vim/test_data/test_increment_visual_partial_number.json @@ -0,0 +1,20 @@ +{"Put":{"state":"ˇ123"}} +{"Key":"v"} +{"Key":"l"} +{"Key":"ctrl-a"} +{"Get":{"state":"ˇ133","mode":"Normal"}} +{"Key":"l"} +{"Key":"v"} +{"Key":"l"} +{"Key":"ctrl-a"} +{"Get":{"state":"1ˇ34","mode":"Normal"}} +{"Key":"shift-v"} +{"Key":"y"} +{"Key":"p"} +{"Key":"p"} +{"Key":"ctrl-v"} +{"Key":"k"} +{"Key":"k"} +{"Key":"l"} +{"Key":"ctrl-a"} +{"Get":{"state":"ˇ144\n144\n144","mode":"Normal"}} From 852439452cb5816e4afa5bd42e2b98a2edae0bec Mon Sep 17 00:00:00 2001 From: Adam Mulvany Date: Fri, 22 Aug 2025 13:20:22 +1000 Subject: [PATCH 265/744] vim: Fix cursor jumping past empty lines with inlay hints in visual mode (#35757) **Summary** Fixes #29134 - Visual mode cursor incorrectly jumps past empty lines that contain inlay hints (type hints). **Problem** When in VIM visual mode, pressing j to move down from a longer line to an empty line that contains an inlay hint would cause the cursor to skip the empty line entirely and jump to the next line. This only occurred when moving down (not up) and only in visual mode. **Root Cause** The issue was introduced by commit f9ee28db5e which added bias-based navigation for handling multi-line inlay hints. When using Bias::Right while moving down, the clipping logic would place the cursor past the inlay hint, causing it to jump to the next line. **Solution** Added logic in up_down_buffer_rows to detect when clipping would place the cursor within an inlay hint position. When detected, it uses the buffer column position instead of the display column to avoid jumping past the hint. **Testing** - Added comprehensive test case test_visual_mode_with_inlay_hints_on_empty_line that reproduces the exact scenario - Manually verified the fix with the reproduction case from the issue - All 356 tests pass with `cargo test -p vim` **Release Notes:** - Fixed VIM visual mode cursor jumping past empty lines with type hints when navigating down --- crates/vim/src/motion.rs | 96 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index a2f165e9fef2c99e13b277bf92786873ee79a649..a54d3caa60e58d7bb61b3dfe0daa83affeba29b7 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1610,10 +1610,20 @@ fn up_down_buffer_rows( map.line_len(begin_folded_line.row()) }; - ( - map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias), - goal, - ) + let point = DisplayPoint::new(begin_folded_line.row(), new_col); + let mut clipped_point = map.clip_point(point, bias); + + // When navigating vertically in vim mode with inlay hints present, + // we need to handle the case where clipping moves us to a different row. + // This can happen when moving down (Bias::Right) and hitting an inlay hint. + // Re-clip with opposite bias to stay on the intended line. + // + // See: https://github.com/zed-industries/zed/issues/29134 + if clipped_point.row() > point.row() { + clipped_point = map.clip_point(point, Bias::Left); + } + + (clipped_point, goal) } fn down_display( @@ -3842,6 +3852,84 @@ mod test { ); } + #[gpui::test] + async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // Test the exact scenario from issue #29134 + cx.set_state( + indoc! {" + fn main() { + let this_is_a_long_name = Vec::::new(); + let new_oneˇ = this_is_a_long_name + .iter() + .map(|i| i + 1) + .map(|i| i * 2) + .collect::>(); + } + "}, + Mode::Normal, + ); + + // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name") + cx.update_editor(|editor, _window, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + // The empty line is at line 3 (0-indexed) + let line_start = snapshot.anchor_after(Point::new(3, 0)); + let inlay_text = ": Vec"; + let inlay = Inlay::edit_prediction(1, line_start, inlay_text); + editor.splice_inlays(&[], vec![inlay], cx); + }); + + // Enter visual mode + cx.simulate_keystrokes("v"); + cx.assert_state( + indoc! {" + fn main() { + let this_is_a_long_name = Vec::::new(); + let new_one« ˇ»= this_is_a_long_name + .iter() + .map(|i| i + 1) + .map(|i| i * 2) + .collect::>(); + } + "}, + Mode::Visual, + ); + + // Move down - should go to the beginning of line 4, not skip to line 5 + cx.simulate_keystrokes("j"); + cx.assert_state( + indoc! {" + fn main() { + let this_is_a_long_name = Vec::::new(); + let new_one« = this_is_a_long_name + ˇ» .iter() + .map(|i| i + 1) + .map(|i| i * 2) + .collect::>(); + } + "}, + Mode::Visual, + ); + + // Test with multiple movements + cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal); + + // Add type hint on the empty line + cx.update_editor(|editor, _window, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let empty_line_start = snapshot.anchor_after(Point::new(2, 0)); + let inlay_text = ": i32"; + let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text); + editor.splice_inlays(&[], vec![inlay], cx); + }); + + // Enter visual mode and move down twice + cx.simulate_keystrokes("v j j"); + cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual); + } + #[gpui::test] async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; From e15856a37f5668433cbfb61a1e7950cf27ec3793 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Fri, 22 Aug 2025 10:17:37 +0530 Subject: [PATCH 266/744] Move APCA contrast from terminal_view to ui utils (#36731) In prep for using this in the editor search/select highlighting. Release Notes: - N/A --- crates/terminal_view/src/terminal_element.rs | 25 ++++++++++--------- crates/terminal_view/src/terminal_view.rs | 1 - crates/ui/src/utils.rs | 2 ++ .../src/utils/apca_contrast.rs} | 0 4 files changed, 15 insertions(+), 13 deletions(-) rename crates/{terminal_view/src/color_contrast.rs => ui/src/utils/apca_contrast.rs} (100%) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index c2fbeb7ee6033f6bdf72f1ec1f5e380cfd39e2d5..fe3301fb89095a7cf9f1a841395b47e9caf1475b 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1,4 +1,3 @@ -use crate::color_contrast; use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine}; use gpui::{ AbsoluteLength, AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, @@ -27,6 +26,7 @@ use terminal::{ terminal_settings::TerminalSettings, }; use theme::{ActiveTheme, Theme, ThemeSettings}; +use ui::utils::ensure_minimum_contrast; use ui::{ParentElement, Tooltip}; use util::ResultExt; use workspace::Workspace; @@ -534,7 +534,7 @@ impl TerminalElement { // Only apply contrast adjustment to non-decorative characters if !Self::is_decorative_character(indexed.c) { - fg = color_contrast::ensure_minimum_contrast(fg, bg, minimum_contrast); + fg = ensure_minimum_contrast(fg, bg, minimum_contrast); } // Ghostty uses (175/255) as the multiplier (~0.69), Alacritty uses 0.66, Kitty @@ -1598,6 +1598,7 @@ pub fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: mod tests { use super::*; use gpui::{AbsoluteLength, Hsla, font}; + use ui::utils::apca_contrast; #[test] fn test_is_decorative_character() { @@ -1713,7 +1714,7 @@ mod tests { }; // Should have poor contrast - let actual_contrast = color_contrast::apca_contrast(white_fg, light_gray_bg).abs(); + let actual_contrast = apca_contrast(white_fg, light_gray_bg).abs(); assert!( actual_contrast < 30.0, "White on light gray should have poor APCA contrast: {}", @@ -1721,12 +1722,12 @@ mod tests { ); // After adjustment with minimum APCA contrast of 45, should be darker - let adjusted = color_contrast::ensure_minimum_contrast(white_fg, light_gray_bg, 45.0); + let adjusted = ensure_minimum_contrast(white_fg, light_gray_bg, 45.0); assert!( adjusted.l < white_fg.l, "Adjusted color should be darker than original" ); - let adjusted_contrast = color_contrast::apca_contrast(adjusted, light_gray_bg).abs(); + let adjusted_contrast = apca_contrast(adjusted, light_gray_bg).abs(); assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast"); // Test case 2: Dark colors (poor contrast) @@ -1744,7 +1745,7 @@ mod tests { }; // Should have poor contrast - let actual_contrast = color_contrast::apca_contrast(black_fg, dark_gray_bg).abs(); + let actual_contrast = apca_contrast(black_fg, dark_gray_bg).abs(); assert!( actual_contrast < 30.0, "Black on dark gray should have poor APCA contrast: {}", @@ -1752,16 +1753,16 @@ mod tests { ); // After adjustment with minimum APCA contrast of 45, should be lighter - let adjusted = color_contrast::ensure_minimum_contrast(black_fg, dark_gray_bg, 45.0); + let adjusted = ensure_minimum_contrast(black_fg, dark_gray_bg, 45.0); assert!( adjusted.l > black_fg.l, "Adjusted color should be lighter than original" ); - let adjusted_contrast = color_contrast::apca_contrast(adjusted, dark_gray_bg).abs(); + let adjusted_contrast = apca_contrast(adjusted, dark_gray_bg).abs(); assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast"); // Test case 3: Already good contrast - let good_contrast = color_contrast::ensure_minimum_contrast(black_fg, white_fg, 45.0); + let good_contrast = ensure_minimum_contrast(black_fg, white_fg, 45.0); assert_eq!( good_contrast, black_fg, "Good contrast should not be adjusted" @@ -1788,11 +1789,11 @@ mod tests { }; // With minimum contrast of 0.0, no adjustment should happen - let no_adjust = color_contrast::ensure_minimum_contrast(white_fg, white_bg, 0.0); + let no_adjust = ensure_minimum_contrast(white_fg, white_bg, 0.0); assert_eq!(no_adjust, white_fg, "No adjustment with min_contrast 0.0"); // With minimum APCA contrast of 15, it should adjust to a darker color - let adjusted = color_contrast::ensure_minimum_contrast(white_fg, white_bg, 15.0); + let adjusted = ensure_minimum_contrast(white_fg, white_bg, 15.0); assert!( adjusted.l < white_fg.l, "White on white should become darker, got l={}", @@ -1800,7 +1801,7 @@ mod tests { ); // Verify the contrast is now acceptable - let new_contrast = color_contrast::apca_contrast(adjusted, white_bg).abs(); + let new_contrast = apca_contrast(adjusted, white_bg).abs(); assert!( new_contrast >= 15.0, "Adjusted APCA contrast {} should be >= 15.0", diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index e2f9ba818dab3e7d5bd05beb842e53b10c72e231..9aa855acb7aa18a0431fcfc07e7a32932162e4f2 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1,4 +1,3 @@ -mod color_contrast; mod persistence; pub mod terminal_element; pub mod terminal_panel; diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs index 26a59001f6e59f5d675363b7c45b701514785e7a..cd7d8eb497328baed356692e1d88d0286568d344 100644 --- a/crates/ui/src/utils.rs +++ b/crates/ui/src/utils.rs @@ -3,12 +3,14 @@ use gpui::App; use theme::ActiveTheme; +mod apca_contrast; mod color_contrast; mod corner_solver; mod format_distance; mod search_input; mod with_rem_size; +pub use apca_contrast::*; pub use color_contrast::*; pub use corner_solver::{CornerSolver, inner_corner_radius}; pub use format_distance::*; diff --git a/crates/terminal_view/src/color_contrast.rs b/crates/ui/src/utils/apca_contrast.rs similarity index 100% rename from crates/terminal_view/src/color_contrast.rs rename to crates/ui/src/utils/apca_contrast.rs From b349a8f34c9bbd2297633aae820bf8432b4f9c63 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 22 Aug 2025 01:12:12 -0400 Subject: [PATCH 267/744] ai: Auto select user model when there's no default (#36722) This PR identifies automatic configuration options that users can select from the agent panel. If no default provider is set in their settings, the PR defaults to the first recommended option. Additionally, it updates the selected provider for a thread when a user changes the default provider through the settings file, if the thread hasn't had any queries yet. Release Notes: - agent: automatically select a language model provider if there's no user set provider. --------- Co-authored-by: Michael Sloan --- crates/agent/src/thread.rs | 17 ++- crates/agent2/src/agent.rs | 4 +- crates/agent2/src/tests/mod.rs | 4 +- .../agent_ui/src/language_model_selector.rs | 55 +-------- crates/git_ui/src/git_panel.rs | 2 +- crates/language_model/src/registry.rs | 114 ++++++++++-------- crates/language_models/Cargo.toml | 1 + crates/language_models/src/language_models.rs | 103 +++++++++++++++- crates/language_models/src/provider/cloud.rs | 6 +- 9 files changed, 184 insertions(+), 122 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 7b70fde56ab1e7acb6705aeace82f142dc28a9f3..899e360ab01226cdb6b47b37713f99e45c0ef6b7 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -664,7 +664,7 @@ impl Thread { } pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option { - if self.configured_model.is_none() { + if self.configured_model.is_none() || self.messages.is_empty() { self.configured_model = LanguageModelRegistry::read_global(cx).default_model(); } self.configured_model.clone() @@ -2097,7 +2097,7 @@ impl Thread { } pub fn summarize(&mut self, cx: &mut Context) { - let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else { + let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else { println!("No thread summary model"); return; }; @@ -2416,7 +2416,7 @@ impl Thread { } let Some(ConfiguredModel { model, provider }) = - LanguageModelRegistry::read_global(cx).thread_summary_model() + LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else { return; }; @@ -5410,13 +5410,10 @@ fn main() {{ }), cx, ); - registry.set_thread_summary_model( - Some(ConfiguredModel { - provider, - model: model.clone(), - }), - cx, - ); + registry.set_thread_summary_model(Some(ConfiguredModel { + provider, + model: model.clone(), + })); }) }); diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 215f8f454b0e26dee673e3f5057229f130a2e567..3502cf0ba9fcd951a4785b2ded644a0b76ab99e9 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -228,7 +228,7 @@ impl NativeAgent { ) -> Entity { let connection = Rc::new(NativeAgentConnection(cx.entity())); let registry = LanguageModelRegistry::read_global(cx); - let summarization_model = registry.thread_summary_model().map(|c| c.model); + let summarization_model = registry.thread_summary_model(cx).map(|c| c.model); thread_handle.update(cx, |thread, cx| { thread.set_summarization_model(summarization_model, cx); @@ -521,7 +521,7 @@ impl NativeAgent { let registry = LanguageModelRegistry::read_global(cx); let default_model = registry.default_model().map(|m| m.model); - let summarization_model = registry.thread_summary_model().map(|m| m.model); + let summarization_model = registry.thread_summary_model(cx).map(|m| m.model); for session in self.sessions.values_mut() { session.thread.update(cx, |thread, cx| { diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index ac7b40c64f678d9ce5318d4b1921fc4de630029f..09048488c88db947c3f63deeab59d70df4c43ca6 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1414,11 +1414,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) { let clock = Arc::new(clock::FakeSystemClock::new()); let client = Client::new(clock, http_client, cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + Project::init_settings(cx); + agent_settings::init(cx); language_model::init(client.clone(), cx); language_models::init(user_store, client.clone(), cx); - Project::init_settings(cx); LanguageModelRegistry::test(cx); - agent_settings::init(cx); }); cx.executor().forbid_parking(); diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 3633e533da97b2b80e5c8d62c271da7121d3582b..aceca79dbf95cd64bbf68b89907d0903f4aba9ff 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -6,8 +6,7 @@ use feature_flags::ZedProFeatureFlag; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task}; use language_model::{ - AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId, - LanguageModelRegistry, + ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry, }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; @@ -77,7 +76,6 @@ pub struct LanguageModelPickerDelegate { all_models: Arc, filtered_entries: Vec, selected_index: usize, - _authenticate_all_providers_task: Task<()>, _subscriptions: Vec, } @@ -98,7 +96,6 @@ impl LanguageModelPickerDelegate { selected_index: Self::get_active_model_index(&entries, get_active_model(cx)), filtered_entries: entries, get_active_model: Arc::new(get_active_model), - _authenticate_all_providers_task: Self::authenticate_all_providers(cx), _subscriptions: vec![cx.subscribe_in( &LanguageModelRegistry::global(cx), window, @@ -142,56 +139,6 @@ impl LanguageModelPickerDelegate { .unwrap_or(0) } - /// Authenticates all providers in the [`LanguageModelRegistry`]. - /// - /// We do this so that we can populate the language selector with all of the - /// models from the configured providers. - fn authenticate_all_providers(cx: &mut App) -> Task<()> { - let authenticate_all_providers = LanguageModelRegistry::global(cx) - .read(cx) - .providers() - .iter() - .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) - .collect::>(); - - cx.spawn(async move |_cx| { - for (provider_id, provider_name, authenticate_task) in authenticate_all_providers { - if let Err(err) = authenticate_task.await { - if matches!(err, AuthenticateError::CredentialsNotFound) { - // Since we're authenticating these providers in the - // background for the purposes of populating the - // language selector, we don't care about providers - // where the credentials are not found. - } else { - // Some providers have noisy failure states that we - // don't want to spam the logs with every time the - // language model selector is initialized. - // - // Ideally these should have more clear failure modes - // that we know are safe to ignore here, like what we do - // with `CredentialsNotFound` above. - match provider_id.0.as_ref() { - "lmstudio" | "ollama" => { - // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". - // - // These fail noisily, so we don't log them. - } - "copilot_chat" => { - // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. - } - _ => { - log::error!( - "Failed to authenticate provider: {}: {err}", - provider_name.0 - ); - } - } - } - } - } - }) - } - pub fn active_model(&self, cx: &App) -> Option { (self.get_active_model)(cx) } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 4ecb4a8829659ca9a25152db8d1eff529cfff2b1..958a609a096173eef379d4788d9e0dc64bbfbe5a 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -4466,7 +4466,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option, - default_fast_model: Option, + /// This model is automatically configured by a user's environment after + /// authenticating all providers. It's only used when default_model is not available. + environment_fallback_model: Option, inline_assistant_model: Option, commit_message_model: Option, thread_summary_model: Option, @@ -104,9 +105,6 @@ impl ConfiguredModel { pub enum Event { DefaultModelChanged, - InlineAssistantModelChanged, - CommitMessageModelChanged, - ThreadSummaryModelChanged, ProviderStateChanged(LanguageModelProviderId), AddedProvider(LanguageModelProviderId), RemovedProvider(LanguageModelProviderId), @@ -238,7 +236,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_inline_assistant_model(configured_model, cx); + self.set_inline_assistant_model(configured_model); } pub fn select_commit_message_model( @@ -247,7 +245,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_commit_message_model(configured_model, cx); + self.set_commit_message_model(configured_model); } pub fn select_thread_summary_model( @@ -256,7 +254,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_thread_summary_model(configured_model, cx); + self.set_thread_summary_model(configured_model); } /// Selects and sets the inline alternatives for language models based on @@ -290,68 +288,60 @@ impl LanguageModelRegistry { } pub fn set_default_model(&mut self, model: Option, cx: &mut Context) { - match (self.default_model.as_ref(), model.as_ref()) { + match (self.default_model(), model.as_ref()) { (Some(old), Some(new)) if old.is_same_as(new) => {} (None, None) => {} _ => cx.emit(Event::DefaultModelChanged), } - self.default_fast_model = maybe!({ - let provider = &model.as_ref()?.provider; - let fast_model = provider.default_fast_model(cx)?; - Some(ConfiguredModel { - provider: provider.clone(), - model: fast_model, - }) - }); self.default_model = model; } - pub fn set_inline_assistant_model( + pub fn set_environment_fallback_model( &mut self, model: Option, cx: &mut Context, ) { - match (self.inline_assistant_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::InlineAssistantModelChanged), + if self.default_model.is_none() { + match (self.environment_fallback_model.as_ref(), model.as_ref()) { + (Some(old), Some(new)) if old.is_same_as(new) => {} + (None, None) => {} + _ => cx.emit(Event::DefaultModelChanged), + } } + self.environment_fallback_model = model; + } + + pub fn set_inline_assistant_model(&mut self, model: Option) { self.inline_assistant_model = model; } - pub fn set_commit_message_model( - &mut self, - model: Option, - cx: &mut Context, - ) { - match (self.commit_message_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::CommitMessageModelChanged), - } + pub fn set_commit_message_model(&mut self, model: Option) { self.commit_message_model = model; } - pub fn set_thread_summary_model( - &mut self, - model: Option, - cx: &mut Context, - ) { - match (self.thread_summary_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::ThreadSummaryModelChanged), - } + pub fn set_thread_summary_model(&mut self, model: Option) { self.thread_summary_model = model; } + #[track_caller] pub fn default_model(&self) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; } - self.default_model.clone() + self.default_model + .clone() + .or_else(|| self.environment_fallback_model.clone()) + } + + pub fn default_fast_model(&self, cx: &App) -> Option { + let provider = self.default_model()?.provider; + let fast_model = provider.default_fast_model(cx)?; + Some(ConfiguredModel { + provider, + model: fast_model, + }) } pub fn inline_assistant_model(&self) -> Option { @@ -365,7 +355,7 @@ impl LanguageModelRegistry { .or_else(|| self.default_model.clone()) } - pub fn commit_message_model(&self) -> Option { + pub fn commit_message_model(&self, cx: &App) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; @@ -373,11 +363,11 @@ impl LanguageModelRegistry { self.commit_message_model .clone() - .or_else(|| self.default_fast_model.clone()) + .or_else(|| self.default_fast_model(cx)) .or_else(|| self.default_model.clone()) } - pub fn thread_summary_model(&self) -> Option { + pub fn thread_summary_model(&self, cx: &App) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; @@ -385,7 +375,7 @@ impl LanguageModelRegistry { self.thread_summary_model .clone() - .or_else(|| self.default_fast_model.clone()) + .or_else(|| self.default_fast_model(cx)) .or_else(|| self.default_model.clone()) } @@ -422,4 +412,34 @@ mod tests { let providers = registry.read(cx).providers(); assert!(providers.is_empty()); } + + #[gpui::test] + async fn test_configure_environment_fallback_model(cx: &mut gpui::TestAppContext) { + let registry = cx.new(|_| LanguageModelRegistry::default()); + + let provider = FakeLanguageModelProvider::default(); + registry.update(cx, |registry, cx| { + registry.register_provider(provider.clone(), cx); + }); + + cx.update(|cx| provider.authenticate(cx)).await.unwrap(); + + registry.update(cx, |registry, cx| { + let provider = registry.provider(&provider.id()).unwrap(); + + registry.set_environment_fallback_model( + Some(ConfiguredModel { + provider: provider.clone(), + model: provider.default_model(cx).unwrap(), + }), + cx, + ); + + let default_model = registry.default_model().unwrap(); + let fallback_model = registry.environment_fallback_model.clone().unwrap(); + + assert_eq!(default_model.model.id(), fallback_model.model.id()); + assert_eq!(default_model.provider.id(), fallback_model.provider.id()); + }); + } } diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index b5bfb870f643452bd5be248c9910d99f16a8101e..cd41478668b17e1c680127625bf47a03604eec60 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -44,6 +44,7 @@ ollama = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] } open_router = { workspace = true, features = ["schemars"] } partial-json-fixer.workspace = true +project.workspace = true release_channel.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 738b72b0c9a6dbb7c9606cc72707b27e66abf09c..beed306e740bfdc8170776e187b9ea59d3a59fbb 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -3,8 +3,12 @@ use std::sync::Arc; use ::settings::{Settings, SettingsStore}; use client::{Client, UserStore}; use collections::HashSet; -use gpui::{App, Context, Entity}; -use language_model::{LanguageModelProviderId, LanguageModelRegistry}; +use futures::future; +use gpui::{App, AppContext as _, Context, Entity}; +use language_model::{ + AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry, +}; +use project::DisableAiSettings; use provider::deepseek::DeepSeekLanguageModelProvider; pub mod provider; @@ -13,7 +17,7 @@ pub mod ui; use crate::provider::anthropic::AnthropicLanguageModelProvider; use crate::provider::bedrock::BedrockLanguageModelProvider; -use crate::provider::cloud::CloudLanguageModelProvider; +use crate::provider::cloud::{self, CloudLanguageModelProvider}; use crate::provider::copilot_chat::CopilotChatLanguageModelProvider; use crate::provider::google::GoogleLanguageModelProvider; use crate::provider::lmstudio::LmStudioLanguageModelProvider; @@ -48,6 +52,13 @@ pub fn init(user_store: Entity, client: Arc, cx: &mut App) { cx, ); }); + + let mut already_authenticated = false; + if !DisableAiSettings::get_global(cx).disable_ai { + authenticate_all_providers(registry.clone(), cx); + already_authenticated = true; + } + cx.observe_global::(move |cx| { let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx) .openai_compatible @@ -65,6 +76,12 @@ pub fn init(user_store: Entity, client: Arc, cx: &mut App) { ); }); openai_compatible_providers = openai_compatible_providers_new; + already_authenticated = false; + } + + if !DisableAiSettings::get_global(cx).disable_ai && !already_authenticated { + authenticate_all_providers(registry.clone(), cx); + already_authenticated = true; } }) .detach(); @@ -151,3 +168,83 @@ fn register_language_model_providers( registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx); registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx); } + +/// Authenticates all providers in the [`LanguageModelRegistry`]. +/// +/// We do this so that we can populate the language selector with all of the +/// models from the configured providers. +/// +/// This function won't do anything if AI is disabled. +fn authenticate_all_providers(registry: Entity, cx: &mut App) { + let providers_to_authenticate = registry + .read(cx) + .providers() + .iter() + .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) + .collect::>(); + + let mut tasks = Vec::with_capacity(providers_to_authenticate.len()); + + for (provider_id, provider_name, authenticate_task) in providers_to_authenticate { + tasks.push(cx.background_spawn(async move { + if let Err(err) = authenticate_task.await { + if matches!(err, AuthenticateError::CredentialsNotFound) { + // Since we're authenticating these providers in the + // background for the purposes of populating the + // language selector, we don't care about providers + // where the credentials are not found. + } else { + // Some providers have noisy failure states that we + // don't want to spam the logs with every time the + // language model selector is initialized. + // + // Ideally these should have more clear failure modes + // that we know are safe to ignore here, like what we do + // with `CredentialsNotFound` above. + match provider_id.0.as_ref() { + "lmstudio" | "ollama" => { + // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". + // + // These fail noisily, so we don't log them. + } + "copilot_chat" => { + // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. + } + _ => { + log::error!( + "Failed to authenticate provider: {}: {err}", + provider_name.0 + ); + } + } + } + } + })); + } + + let all_authenticated_future = future::join_all(tasks); + + cx.spawn(async move |cx| { + all_authenticated_future.await; + + registry + .update(cx, |registry, cx| { + let cloud_provider = registry.provider(&cloud::PROVIDER_ID); + let fallback_model = cloud_provider + .iter() + .chain(registry.providers().iter()) + .find(|provider| provider.is_authenticated(cx)) + .and_then(|provider| { + Some(ConfiguredModel { + provider: provider.clone(), + model: provider + .default_model(cx) + .or_else(|| provider.recommended_models(cx).first().cloned())?, + }) + }); + registry.set_environment_fallback_model(fallback_model, cx); + }) + .ok(); + }) + .detach(); +} diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index b1b5ff3eb39695ea521dc986b39ffd2d0d194f99..8e4b7869353cc84bf78f84562ee348f37491b2a6 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -44,8 +44,8 @@ use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, i use crate::provider::google::{GoogleEventMapper, into_google}; use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai}; -const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID; -const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; +pub const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID; +pub const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; #[derive(Default, Clone, Debug, PartialEq)] pub struct ZedDotDevSettings { @@ -148,7 +148,7 @@ impl State { default_fast_model: None, recommended_models: Vec::new(), _fetch_models_task: cx.spawn(async move |this, cx| { - maybe!(async move { + maybe!(async { let (client, llm_api_token) = this .read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?; From e36069110659ad113876c7fa5c85338176bf7172 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 22 Aug 2025 01:54:25 -0400 Subject: [PATCH 268/744] telemetry: Add panel button clicked event (#36735) The event has two fields 1. name: The name of the panel being clicked 2. toggle_state: true if clicking to open, otherwise false cc @katie-z-geer Release Notes: - N/A --- crates/workspace/src/dock.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 7a8de6e91040e8949a47668da81f063cb3c0c082..149a122c0c6a31b7c4713601acca5091accb96ac 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -915,6 +915,11 @@ impl Render for PanelButtons { .on_click({ let action = action.boxed_clone(); move |_, window, cx| { + telemetry::event!( + "Panel Button Clicked", + name = name, + toggle_state = !is_open + ); window.focus(&focus_handle); window.dispatch_action(action.boxed_clone(), cx) } From f4ba7997a7d7e9da61b98fda8e28542d3e29f518 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 21 Aug 2025 23:57:30 -0600 Subject: [PATCH 269/744] acp: Fix history search (#36734) Release Notes: - N/A --- crates/agent2/src/agent.rs | 5 +- crates/agent2/src/history_store.rs | 31 +- .../agent_ui/src/acp/completion_provider.rs | 2 +- crates/agent_ui/src/acp/thread_history.rs | 485 ++++++++---------- crates/agent_ui/src/acp/thread_view.rs | 6 +- 5 files changed, 223 insertions(+), 306 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 3502cf0ba9fcd951a4785b2ded644a0b76ab99e9..4eaf87e218e52500470e3cece86f8f946dee2dea 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1406,10 +1406,9 @@ mod tests { history: &Entity, cx: &mut TestAppContext, ) -> Vec<(HistoryEntryId, String)> { - history.read_with(cx, |history, cx| { + history.read_with(cx, |history, _| { history - .entries(cx) - .iter() + .entries() .map(|e| (e.id(), e.title().to_string())) .collect::>() }) diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 78d83cc1d05a72d1499c697751d1dfa792143db3..c656456e01780505c355c878c26d2405286e56b2 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -86,6 +86,7 @@ enum SerializedRecentOpen { pub struct HistoryStore { threads: Vec, + entries: Vec, context_store: Entity, recently_opened_entries: VecDeque, _subscriptions: Vec, @@ -97,7 +98,7 @@ impl HistoryStore { context_store: Entity, cx: &mut Context, ) -> Self { - let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())]; + let subscriptions = vec![cx.observe(&context_store, |this, _, cx| this.update_entries(cx))]; cx.spawn(async move |this, cx| { let entries = Self::load_recently_opened_entries(cx).await; @@ -116,6 +117,7 @@ impl HistoryStore { context_store, recently_opened_entries: VecDeque::default(), threads: Vec::default(), + entries: Vec::default(), _subscriptions: subscriptions, _save_recently_opened_entries_task: Task::ready(()), } @@ -181,20 +183,18 @@ impl HistoryStore { } } this.threads = threads; - cx.notify(); + this.update_entries(cx); }) }) .detach_and_log_err(cx); } - pub fn entries(&self, cx: &App) -> Vec { - let mut history_entries = Vec::new(); - + fn update_entries(&mut self, cx: &mut Context) { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { - return history_entries; + return; } - + let mut history_entries = Vec::new(); history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread)); history_entries.extend( self.context_store @@ -205,17 +205,12 @@ impl HistoryStore { ); history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at())); - history_entries + self.entries = history_entries; + cx.notify() } - pub fn is_empty(&self, cx: &App) -> bool { - self.threads.is_empty() - && self - .context_store - .read(cx) - .unordered_contexts() - .next() - .is_none() + pub fn is_empty(&self, _cx: &App) -> bool { + self.entries.is_empty() } pub fn recently_opened_entries(&self, cx: &App) -> Vec { @@ -356,7 +351,7 @@ impl HistoryStore { self.save_recently_opened_entries(cx); } - pub fn recent_entries(&self, limit: usize, cx: &mut Context) -> Vec { - self.entries(cx).into_iter().take(limit).collect() + pub fn entries(&self) -> impl Iterator { + self.entries.iter().cloned() } } diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 3587e5144eab22ec1ab6cf60c4d6eb59c8c5d409..22a9ea677334a63999be2c258f691cef23d34ac1 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -805,7 +805,7 @@ pub(crate) fn search_threads( history_store: &Entity, cx: &mut App, ) -> Task> { - let threads = history_store.read(cx).entries(cx); + let threads = history_store.read(cx).entries().collect(); if query.is_empty() { return Task::ready(threads); } diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index d76969378ce2dd30e891618079d7ce60664e2617..5d852f0ddc5802bbf873f8bf7438e3e2427e5f78 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -3,18 +3,18 @@ use crate::{AgentPanel, RemoveSelectedThread}; use agent2::{HistoryEntry, HistoryStore}; use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use editor::{Editor, EditorEvent}; -use fuzzy::{StringMatch, StringMatchCandidate}; +use fuzzy::StringMatchCandidate; use gpui::{ - App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, + App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, UniformListScrollHandle, WeakEntity, Window, uniform_list, }; -use std::{fmt::Display, ops::Range, sync::Arc}; +use std::{fmt::Display, ops::Range}; +use text::Bias; use time::{OffsetDateTime, UtcOffset}; use ui::{ HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip, prelude::*, }; -use util::ResultExt; pub struct AcpThreadHistory { pub(crate) history_store: Entity, @@ -22,38 +22,38 @@ pub struct AcpThreadHistory { selected_index: usize, hovered_index: Option, search_editor: Entity, - all_entries: Arc>, - // When the search is empty, we display date separators between history entries - // This vector contains an enum of either a separator or an actual entry - separated_items: Vec, - // Maps entry indexes to list item indexes - separated_item_indexes: Vec, - _separated_items_task: Option>, - search_state: SearchState, + search_query: SharedString, + + visible_items: Vec, + scrollbar_visibility: bool, scrollbar_state: ScrollbarState, local_timezone: UtcOffset, - _subscriptions: Vec, -} -enum SearchState { - Empty, - Searching { - query: SharedString, - _task: Task<()>, - }, - Searched { - query: SharedString, - matches: Vec, - }, + _update_task: Task<()>, + _subscriptions: Vec, } enum ListItemType { BucketSeparator(TimeBucket), Entry { - index: usize, + entry: HistoryEntry, format: EntryTimeFormat, }, + SearchResult { + entry: HistoryEntry, + positions: Vec, + }, +} + +impl ListItemType { + fn history_entry(&self) -> Option<&HistoryEntry> { + match self { + ListItemType::Entry { entry, .. } => Some(entry), + ListItemType::SearchResult { entry, .. } => Some(entry), + _ => None, + } + } } pub enum ThreadHistoryEvent { @@ -78,12 +78,15 @@ impl AcpThreadHistory { cx.subscribe(&search_editor, |this, search_editor, event, cx| { if let EditorEvent::BufferEdited = event { let query = search_editor.read(cx).text(cx); - this.search(query.into(), cx); + if this.search_query != query { + this.search_query = query.into(); + this.update_visible_items(false, cx); + } } }); let history_store_subscription = cx.observe(&history_store, |this, _, cx| { - this.update_all_entries(cx); + this.update_visible_items(true, cx); }); let scroll_handle = UniformListScrollHandle::default(); @@ -94,10 +97,7 @@ impl AcpThreadHistory { scroll_handle, selected_index: 0, hovered_index: None, - search_state: SearchState::Empty, - all_entries: Default::default(), - separated_items: Default::default(), - separated_item_indexes: Default::default(), + visible_items: Default::default(), search_editor, scrollbar_visibility: true, scrollbar_state, @@ -105,29 +105,61 @@ impl AcpThreadHistory { chrono::Local::now().offset().local_minus_utc(), ) .unwrap(), + search_query: SharedString::default(), _subscriptions: vec![search_editor_subscription, history_store_subscription], - _separated_items_task: None, + _update_task: Task::ready(()), }; - this.update_all_entries(cx); + this.update_visible_items(false, cx); this } - fn update_all_entries(&mut self, cx: &mut Context) { - let new_entries: Arc> = self + fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context) { + let entries = self .history_store - .update(cx, |store, cx| store.entries(cx)) - .into(); + .update(cx, |store, _| store.entries().collect()); + let new_list_items = if self.search_query.is_empty() { + self.add_list_separators(entries, cx) + } else { + self.filter_search_results(entries, cx) + }; + let selected_history_entry = if preserve_selected_item { + self.selected_history_entry().cloned() + } else { + None + }; - self._separated_items_task.take(); + self._update_task = cx.spawn(async move |this, cx| { + let new_visible_items = new_list_items.await; + this.update(cx, |this, cx| { + let new_selected_index = if let Some(history_entry) = selected_history_entry { + let history_entry_id = history_entry.id(); + new_visible_items + .iter() + .position(|visible_entry| { + visible_entry + .history_entry() + .is_some_and(|entry| entry.id() == history_entry_id) + }) + .unwrap_or(0) + } else { + 0 + }; - let mut items = Vec::with_capacity(new_entries.len() + 1); - let mut indexes = Vec::with_capacity(new_entries.len() + 1); + this.visible_items = new_visible_items; + this.set_selected_index(new_selected_index, Bias::Right, cx); + cx.notify(); + }) + .ok(); + }); + } - let bg_task = cx.background_spawn(async move { + fn add_list_separators(&self, entries: Vec, cx: &App) -> Task> { + cx.background_spawn(async move { + let mut items = Vec::with_capacity(entries.len() + 1); let mut bucket = None; let today = Local::now().naive_local().date(); - for (index, entry) in new_entries.iter().enumerate() { + for entry in entries.into_iter() { let entry_date = entry .updated_at() .with_timezone(&Local) @@ -140,75 +172,33 @@ impl AcpThreadHistory { items.push(ListItemType::BucketSeparator(entry_bucket)); } - indexes.push(items.len() as u32); items.push(ListItemType::Entry { - index, + entry, format: entry_bucket.into(), }); } - (new_entries, items, indexes) - }); - - let task = cx.spawn(async move |this, cx| { - let (new_entries, items, indexes) = bg_task.await; - this.update(cx, |this, cx| { - let previously_selected_entry = - this.all_entries.get(this.selected_index).map(|e| e.id()); - - this.all_entries = new_entries; - this.separated_items = items; - this.separated_item_indexes = indexes; - - match &this.search_state { - SearchState::Empty => { - if this.selected_index >= this.all_entries.len() { - this.set_selected_entry_index( - this.all_entries.len().saturating_sub(1), - cx, - ); - } else if let Some(prev_id) = previously_selected_entry - && let Some(new_ix) = this - .all_entries - .iter() - .position(|probe| probe.id() == prev_id) - { - this.set_selected_entry_index(new_ix, cx); - } - } - SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => { - this.search(query.clone(), cx); - } - } - - cx.notify(); - }) - .log_err(); - }); - self._separated_items_task = Some(task); + items + }) } - fn search(&mut self, query: SharedString, cx: &mut Context) { - if query.is_empty() { - self.search_state = SearchState::Empty; - cx.notify(); - return; - } - - let all_entries = self.all_entries.clone(); - - let fuzzy_search_task = cx.background_spawn({ - let query = query.clone(); + fn filter_search_results( + &self, + entries: Vec, + cx: &App, + ) -> Task> { + let query = self.search_query.clone(); + cx.background_spawn({ let executor = cx.background_executor().clone(); async move { - let mut candidates = Vec::with_capacity(all_entries.len()); + let mut candidates = Vec::with_capacity(entries.len()); - for (idx, entry) in all_entries.iter().enumerate() { + for (idx, entry) in entries.iter().enumerate() { candidates.push(StringMatchCandidate::new(idx, entry.title())); } const MAX_MATCHES: usize = 100; - fuzzy::match_strings( + let matches = fuzzy::match_strings( &candidates, &query, false, @@ -217,74 +207,61 @@ impl AcpThreadHistory { &Default::default(), executor, ) - .await - } - }); + .await; - let task = cx.spawn({ - let query = query.clone(); - async move |this, cx| { - let matches = fuzzy_search_task.await; - - this.update(cx, |this, cx| { - let SearchState::Searching { - query: current_query, - _task, - } = &this.search_state - else { - return; - }; - - if &query == current_query { - this.search_state = SearchState::Searched { - query: query.clone(), - matches, - }; - - this.set_selected_entry_index(0, cx); - cx.notify(); - }; - }) - .log_err(); + matches + .into_iter() + .map(|search_match| ListItemType::SearchResult { + entry: entries[search_match.candidate_id].clone(), + positions: search_match.positions, + }) + .collect() } - }); - - self.search_state = SearchState::Searching { query, _task: task }; - cx.notify(); + }) } - fn matched_count(&self) -> usize { - match &self.search_state { - SearchState::Empty => self.all_entries.len(), - SearchState::Searching { .. } => 0, - SearchState::Searched { matches, .. } => matches.len(), - } + fn search_produced_no_matches(&self) -> bool { + self.visible_items.is_empty() && !self.search_query.is_empty() } - fn list_item_count(&self) -> usize { - match &self.search_state { - SearchState::Empty => self.separated_items.len(), - SearchState::Searching { .. } => 0, - SearchState::Searched { matches, .. } => matches.len(), - } + fn selected_history_entry(&self) -> Option<&HistoryEntry> { + self.get_history_entry(self.selected_index) } - fn search_produced_no_matches(&self) -> bool { - match &self.search_state { - SearchState::Empty => false, - SearchState::Searching { .. } => false, - SearchState::Searched { matches, .. } => matches.is_empty(), - } + fn get_history_entry(&self, visible_items_ix: usize) -> Option<&HistoryEntry> { + self.visible_items.get(visible_items_ix)?.history_entry() } - fn get_match(&self, ix: usize) -> Option<&HistoryEntry> { - match &self.search_state { - SearchState::Empty => self.all_entries.get(ix), - SearchState::Searching { .. } => None, - SearchState::Searched { matches, .. } => matches - .get(ix) - .and_then(|m| self.all_entries.get(m.candidate_id)), + fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context) { + if self.visible_items.len() == 0 { + self.selected_index = 0; + return; } + while matches!( + self.visible_items.get(index), + None | Some(ListItemType::BucketSeparator(..)) + ) { + index = match bias { + Bias::Left => { + if index == 0 { + self.visible_items.len() - 1 + } else { + index - 1 + } + } + Bias::Right => { + if index >= self.visible_items.len() - 1 { + 0 + } else { + index + 1 + } + } + }; + } + self.selected_index = index; + self.scroll_handle + .scroll_to_item(index, ScrollStrategy::Top); + cx.notify() } pub fn select_previous( @@ -293,13 +270,10 @@ impl AcpThreadHistory { _window: &mut Window, cx: &mut Context, ) { - let count = self.matched_count(); - if count > 0 { - if self.selected_index == 0 { - self.set_selected_entry_index(count - 1, cx); - } else { - self.set_selected_entry_index(self.selected_index - 1, cx); - } + if self.selected_index == 0 { + self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx); + } else { + self.set_selected_index(self.selected_index - 1, Bias::Left, cx); } } @@ -309,13 +283,10 @@ impl AcpThreadHistory { _window: &mut Window, cx: &mut Context, ) { - let count = self.matched_count(); - if count > 0 { - if self.selected_index == count - 1 { - self.set_selected_entry_index(0, cx); - } else { - self.set_selected_entry_index(self.selected_index + 1, cx); - } + if self.selected_index == self.visible_items.len() - 1 { + self.set_selected_index(0, Bias::Right, cx); + } else { + self.set_selected_index(self.selected_index + 1, Bias::Right, cx); } } @@ -325,35 +296,47 @@ impl AcpThreadHistory { _window: &mut Window, cx: &mut Context, ) { - let count = self.matched_count(); - if count > 0 { - self.set_selected_entry_index(0, cx); - } + self.set_selected_index(0, Bias::Right, cx); } fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { - let count = self.matched_count(); - if count > 0 { - self.set_selected_entry_index(count - 1, cx); - } + self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx); } - fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context) { - self.selected_index = entry_index; + fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { + self.confirm_entry(self.selected_index, cx); + } - let scroll_ix = match self.search_state { - SearchState::Empty | SearchState::Searching { .. } => self - .separated_item_indexes - .get(entry_index) - .map(|ix| *ix as usize) - .unwrap_or(entry_index + 1), - SearchState::Searched { .. } => entry_index, + fn confirm_entry(&mut self, ix: usize, cx: &mut Context) { + let Some(entry) = self.get_history_entry(ix) else { + return; }; + cx.emit(ThreadHistoryEvent::Open(entry.clone())); + } - self.scroll_handle - .scroll_to_item(scroll_ix, ScrollStrategy::Top); + fn remove_selected_thread( + &mut self, + _: &RemoveSelectedThread, + _window: &mut Window, + cx: &mut Context, + ) { + self.remove_thread(self.selected_index, cx) + } - cx.notify(); + fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context) { + let Some(entry) = self.get_history_entry(visible_item_ix) else { + return; + }; + + let task = match entry { + HistoryEntry::AcpThread(thread) => self + .history_store + .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)), + HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| { + this.delete_text_thread(context.path.clone(), cx) + }), + }; + task.detach_and_log_err(cx); } fn render_scrollbar(&self, cx: &mut Context) -> Option> { @@ -393,91 +376,33 @@ impl AcpThreadHistory { ) } - fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { - self.confirm_entry(self.selected_index, cx); - } - - fn confirm_entry(&mut self, ix: usize, cx: &mut Context) { - let Some(entry) = self.get_match(ix) else { - return; - }; - cx.emit(ThreadHistoryEvent::Open(entry.clone())); - } - - fn remove_selected_thread( - &mut self, - _: &RemoveSelectedThread, - _window: &mut Window, - cx: &mut Context, - ) { - self.remove_thread(self.selected_index, cx) - } - - fn remove_thread(&mut self, ix: usize, cx: &mut Context) { - let Some(entry) = self.get_match(ix) else { - return; - }; - - let task = match entry { - HistoryEntry::AcpThread(thread) => self - .history_store - .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)), - HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| { - this.delete_text_thread(context.path.clone(), cx) - }), - }; - task.detach_and_log_err(cx); - } - - fn list_items( + fn render_list_items( &mut self, range: Range, _window: &mut Window, cx: &mut Context, ) -> Vec { - match &self.search_state { - SearchState::Empty => self - .separated_items - .get(range) - .iter() - .flat_map(|items| { - items - .iter() - .map(|item| self.render_list_item(item, vec![], cx)) - }) - .collect(), - SearchState::Searched { matches, .. } => matches[range] - .iter() - .filter_map(|m| { - let entry = self.all_entries.get(m.candidate_id)?; - Some(self.render_history_entry( - entry, - EntryTimeFormat::DateAndTime, - m.candidate_id, - m.positions.clone(), - cx, - )) - }) - .collect(), - SearchState::Searching { .. } => { - vec![] - } - } + self.visible_items + .get(range.clone()) + .into_iter() + .flatten() + .enumerate() + .map(|(ix, item)| self.render_list_item(item, range.start + ix, cx)) + .collect() } - fn render_list_item( - &self, - item: &ListItemType, - highlight_positions: Vec, - cx: &Context, - ) -> AnyElement { + fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context) -> AnyElement { match item { - ListItemType::Entry { index, format } => match self.all_entries.get(*index) { - Some(entry) => self - .render_history_entry(entry, *format, *index, highlight_positions, cx) - .into_any(), - None => Empty.into_any_element(), - }, + ListItemType::Entry { entry, format } => self + .render_history_entry(entry, *format, ix, Vec::default(), cx) + .into_any(), + ListItemType::SearchResult { entry, positions } => self.render_history_entry( + entry, + EntryTimeFormat::DateAndTime, + ix, + positions.clone(), + cx, + ), ListItemType::BucketSeparator(bucket) => div() .px(DynamicSpacing::Base06.rems(cx)) .pt_2() @@ -495,12 +420,12 @@ impl AcpThreadHistory { &self, entry: &HistoryEntry, format: EntryTimeFormat, - list_entry_ix: usize, + ix: usize, highlight_positions: Vec, cx: &Context, ) -> AnyElement { - let selected = list_entry_ix == self.selected_index; - let hovered = Some(list_entry_ix) == self.hovered_index; + let selected = ix == self.selected_index; + let hovered = Some(ix) == self.hovered_index; let timestamp = entry.updated_at().timestamp(); let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone); @@ -508,7 +433,7 @@ impl AcpThreadHistory { .w_full() .pb_1() .child( - ListItem::new(list_entry_ix) + ListItem::new(ix) .rounded() .toggle_state(selected) .spacing(ListItemSpacing::Sparse) @@ -530,8 +455,8 @@ impl AcpThreadHistory { ) .on_hover(cx.listener(move |this, is_hovered, _window, cx| { if *is_hovered { - this.hovered_index = Some(list_entry_ix); - } else if this.hovered_index == Some(list_entry_ix) { + this.hovered_index = Some(ix); + } else if this.hovered_index == Some(ix) { this.hovered_index = None; } @@ -546,16 +471,14 @@ impl AcpThreadHistory { .tooltip(move |window, cx| { Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) }) - .on_click(cx.listener(move |this, _, _, cx| { - this.remove_thread(list_entry_ix, cx) - })), + .on_click( + cx.listener(move |this, _, _, cx| this.remove_thread(ix, cx)), + ), ) } else { None }) - .on_click( - cx.listener(move |this, _, _, cx| this.confirm_entry(list_entry_ix, cx)), - ), + .on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))), ) .into_any_element() } @@ -578,7 +501,7 @@ impl Render for AcpThreadHistory { .on_action(cx.listener(Self::select_last)) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::remove_selected_thread)) - .when(!self.all_entries.is_empty(), |parent| { + .when(!self.history_store.read(cx).is_empty(cx), |parent| { parent.child( h_flex() .h(px(41.)) // Match the toolbar perfectly @@ -604,7 +527,7 @@ impl Render for AcpThreadHistory { .overflow_hidden() .flex_grow(); - if self.all_entries.is_empty() { + if self.history_store.read(cx).is_empty(cx) { view.justify_center() .child( h_flex().w_full().justify_center().child( @@ -623,9 +546,9 @@ impl Render for AcpThreadHistory { .child( uniform_list( "thread-history", - self.list_item_count(), + self.visible_items.len(), cx.processor(|this, range: Range, window, cx| { - this.list_items(range, window, cx) + this.render_list_items(range, window, cx) }), ) .p_1() diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 4d89a55139154b448b9fb33f00fb31fa22bc81cc..dae89b32830f5f8eb7ec5701e38a3a70026cfea5 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2538,9 +2538,9 @@ impl AcpThreadView { ) }) .when(render_history, |this| { - let recent_history = self - .history_store - .update(cx, |history_store, cx| history_store.recent_entries(3, cx)); + let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| { + history_store.entries().take(3).collect() + }); this.justify_end().child( v_flex() .child( From d88fd00e87673263eefdbe6fa5b3d582a05f2aee Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 22 Aug 2025 03:48:47 -0400 Subject: [PATCH 270/744] acp: Fix panic with edit file tool (#36732) We had a frequent panic when the agent was using our edit file tool. The root cause was that we were constructing a `BufferDiff` with `BufferDiff::new`, then calling `set_base_text`, but not waiting for that asynchronous operation to finish. This means there was a window of time where the diff's base text was set to the initial value of `""`--that's not a problem in itself, but it was possible for us to call `PendingDiff::update` during that window, which calls `BufferDiff::update_diff`, which calls `BufferDiffSnapshot::new_with_base_buffer`, which takes two arguments `base_text` and `base_text_snapshot` that are supposed to represent the same text. We were getting the first of those arguments from the `base_text` field of `PendingDiff`, which is set immediately to the target base text without waiting for `BufferDiff::set_base_text` to run to completion; and the second from the `BufferDiff` itself, which still has the empty base text during that window. As a result of that mismatch, we could end up adding `DeletedHunk` diff transforms to the multibuffer for the diff card even though the multibuffer's base text was empty, ultimately leading to a panic very far away in rendering code. I've fixed this by adding a new `BufferDiff` constructor for the case where the buffer contents and the base text are (initially) the same, like for the diff cards, and so we don't need an async diff calculation. I also added a debug assertion to catch the basic issue here earlier, when `BufferDiffSnapshot::new_with_base_buffer` is called with two base texts that don't match. Release Notes: - N/A --------- Co-authored-by: Conrad --- crates/acp_thread/src/diff.rs | 40 +++++++++++++++++---------- crates/buffer_diff/src/buffer_diff.rs | 33 +++++++++++++++++++++- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index 59f907dcc42e076060a6f50a2573aa3a0f10f382..0fec6809e01ff3f85acc7ad80effe95197200d60 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -85,27 +85,19 @@ impl Diff { } pub fn new(buffer: Entity, cx: &mut Context) -> Self { - let buffer_snapshot = buffer.read(cx).snapshot(); - let base_text = buffer_snapshot.text(); - let language_registry = buffer.read(cx).language_registry(); - let text_snapshot = buffer.read(cx).text_snapshot(); + let buffer_text_snapshot = buffer.read(cx).text_snapshot(); + let base_text_snapshot = buffer.read(cx).snapshot(); + let base_text = base_text_snapshot.text(); + debug_assert_eq!(buffer_text_snapshot.text(), base_text); let buffer_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&text_snapshot, cx); - let _ = diff.set_base_text( - buffer_snapshot.clone(), - language_registry, - text_snapshot, - cx, - ); + let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot); let snapshot = diff.snapshot(cx); - let secondary_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer_snapshot, cx); - diff.set_snapshot(snapshot, &buffer_snapshot, cx); + let mut diff = BufferDiff::new(&buffer_text_snapshot, cx); + diff.set_snapshot(snapshot, &buffer_text_snapshot, cx); diff }); diff.set_secondary_diff(secondary_diff); - diff }); @@ -412,3 +404,21 @@ async fn build_buffer_diff( diff }) } + +#[cfg(test)] +mod tests { + use gpui::{AppContext as _, TestAppContext}; + use language::Buffer; + + use crate::Diff; + + #[gpui::test] + async fn test_pending_diff(cx: &mut TestAppContext) { + let buffer = cx.new(|cx| Buffer::local("hello!", cx)); + let _diff = cx.new(|cx| Diff::new(buffer.clone(), cx)); + buffer.update(cx, |buffer, cx| { + buffer.set_text("HELLO!", cx); + }); + cx.run_until_parked(); + } +} diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 10b59d0ba20ee72537406dc4645f8565df6361fc..b20dad4ebbcc5990bd0a6a165375ca62481e609f 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -162,6 +162,22 @@ impl BufferDiffSnapshot { } } + fn unchanged( + buffer: &text::BufferSnapshot, + base_text: language::BufferSnapshot, + ) -> BufferDiffSnapshot { + debug_assert_eq!(buffer.text(), base_text.text()); + BufferDiffSnapshot { + inner: BufferDiffInner { + base_text, + hunks: SumTree::new(buffer), + pending_hunks: SumTree::new(buffer), + base_text_exists: false, + }, + secondary_diff: None, + } + } + fn new_with_base_text( buffer: text::BufferSnapshot, base_text: Option>, @@ -213,7 +229,10 @@ impl BufferDiffSnapshot { cx: &App, ) -> impl Future + use<> { let base_text_exists = base_text.is_some(); - let base_text_pair = base_text.map(|text| (text, base_text_snapshot.as_rope().clone())); + let base_text_pair = base_text.map(|text| { + debug_assert_eq!(&*text, &base_text_snapshot.text()); + (text, base_text_snapshot.as_rope().clone()) + }); cx.background_executor() .spawn_labeled(*CALCULATE_DIFF_TASK, async move { Self { @@ -873,6 +892,18 @@ impl BufferDiff { } } + pub fn new_unchanged( + buffer: &text::BufferSnapshot, + base_text: language::BufferSnapshot, + ) -> Self { + debug_assert_eq!(buffer.text(), base_text.text()); + BufferDiff { + buffer_id: buffer.remote_id(), + inner: BufferDiffSnapshot::unchanged(buffer, base_text).inner, + secondary_diff: None, + } + } + #[cfg(any(test, feature = "test-support"))] pub fn new_with_base_text( base_text: &str, From 27a26d53b1ea1d83ab16c840a5ba1f05da96edea Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 22 Aug 2025 08:28:03 -0300 Subject: [PATCH 271/744] thread view: Inform when editing previous messages is unavailable (#36727) Release Notes: - N/A --- assets/icons/pencil_unavailable.svg | 6 ++ crates/agent_ui/src/acp/thread_view.rs | 97 ++++++++++++------- crates/agent_ui/src/ui.rs | 2 + .../src/ui/unavailable_editing_tooltip.rs | 29 ++++++ crates/icons/src/icons.rs | 1 + 5 files changed, 98 insertions(+), 37 deletions(-) create mode 100644 assets/icons/pencil_unavailable.svg create mode 100644 crates/agent_ui/src/ui/unavailable_editing_tooltip.rs diff --git a/assets/icons/pencil_unavailable.svg b/assets/icons/pencil_unavailable.svg new file mode 100644 index 0000000000000000000000000000000000000000..4241d766ace9ec5873553e0c1d77b8c19f6caa79 --- /dev/null +++ b/assets/icons/pencil_unavailable.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index dae89b32830f5f8eb7ec5701e38a3a70026cfea5..619885144ab306298af40ef7c472b0f69045c5f9 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -57,7 +57,9 @@ use crate::agent_diff::AgentDiff; use crate::profile_selector::{ProfileProvider, ProfileSelector}; use crate::ui::preview::UsageCallout; -use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip}; +use crate::ui::{ + AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip, +}; use crate::{ AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector, @@ -1239,6 +1241,8 @@ impl AcpThreadView { None }; + let agent_name = self.agent.name(); + v_flex() .id(("user_message", entry_ix)) .pt_2() @@ -1292,42 +1296,61 @@ impl AcpThreadView { .text_xs() .child(editor.clone().into_any_element()), ) - .when(editing && editor_focus, |this| - this.child( - h_flex() - .absolute() - .top_neg_3p5() - .right_3() - .gap_1() - .rounded_sm() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_background) - .overflow_hidden() - .child( - IconButton::new("cancel", IconName::Close) - .icon_color(Color::Error) - .icon_size(IconSize::XSmall) - .on_click(cx.listener(Self::cancel_editing)) - ) - .child( - IconButton::new("regenerate", IconName::Return) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - .tooltip(Tooltip::text( - "Editing will restart the thread from this point." - )) - .on_click(cx.listener({ - let editor = editor.clone(); - move |this, _, window, cx| { - this.regenerate( - entry_ix, &editor, window, cx, - ); - } - })), - ) - ) - ), + .when(editor_focus, |this| { + let base_container = h_flex() + .absolute() + .top_neg_3p5() + .right_3() + .gap_1() + .rounded_sm() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_background) + .overflow_hidden(); + + if message.id.is_some() { + this.child( + base_container + .child( + IconButton::new("cancel", IconName::Close) + .icon_color(Color::Error) + .icon_size(IconSize::XSmall) + .on_click(cx.listener(Self::cancel_editing)) + ) + .child( + IconButton::new("regenerate", IconName::Return) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .tooltip(Tooltip::text( + "Editing will restart the thread from this point." + )) + .on_click(cx.listener({ + let editor = editor.clone(); + move |this, _, window, cx| { + this.regenerate( + entry_ix, &editor, window, cx, + ); + } + })), + ) + ) + } else { + this.child( + base_container + .border_dashed() + .child( + IconButton::new("editing_unavailable", IconName::PencilUnavailable) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .style(ButtonStyle::Transparent) + .tooltip(move |_window, cx| { + cx.new(|_| UnavailableEditingTooltip::new(agent_name.into())) + .into() + }) + ) + ) + } + }), ) .into_any() } diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index e27a2242404e4f3b1d721b66ac2aad9119c4447a..ada973cddfc847c67b805ee053fb50e6d9cd99d7 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -4,9 +4,11 @@ mod context_pill; mod end_trial_upsell; mod onboarding_modal; pub mod preview; +mod unavailable_editing_tooltip; pub use agent_notification::*; pub use burn_mode_tooltip::*; pub use context_pill::*; pub use end_trial_upsell::*; pub use onboarding_modal::*; +pub use unavailable_editing_tooltip::*; diff --git a/crates/agent_ui/src/ui/unavailable_editing_tooltip.rs b/crates/agent_ui/src/ui/unavailable_editing_tooltip.rs new file mode 100644 index 0000000000000000000000000000000000000000..78d4c64e0acc7bff86516657f76007e78a54d304 --- /dev/null +++ b/crates/agent_ui/src/ui/unavailable_editing_tooltip.rs @@ -0,0 +1,29 @@ +use gpui::{Context, IntoElement, Render, Window}; +use ui::{prelude::*, tooltip_container}; + +pub struct UnavailableEditingTooltip { + agent_name: SharedString, +} + +impl UnavailableEditingTooltip { + pub fn new(agent_name: SharedString) -> Self { + Self { agent_name } + } +} + +impl Render for UnavailableEditingTooltip { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + tooltip_container(window, cx, |this, _, _| { + this.child(Label::new("Unavailable Editing")).child( + div().max_w_64().child( + Label::new(format!( + "Editing previous messages is not available for {} yet.", + self.agent_name + )) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + }) + } +} diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 38f02c2206b3a876b68585d8961f0a7e679a8f32..b5f891713ab70a1fc32fbca3ec0f613af0338640 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -164,6 +164,7 @@ pub enum IconName { PageDown, PageUp, Pencil, + PencilUnavailable, Person, Pin, PlayOutlined, From 3b7c1744b424c9127267e8935ac668ece52394e4 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 22 Aug 2025 09:52:44 -0300 Subject: [PATCH 272/744] thread view: Add more UI improvements (#36750) Release Notes: - N/A --- assets/icons/attach.svg | 3 ++ assets/icons/tool_think.svg | 2 +- crates/agent_servers/src/claude.rs | 2 +- crates/agent_servers/src/gemini.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 68 +++++++++----------------- crates/agent_ui/src/agent_panel.rs | 5 ++ crates/icons/src/icons.rs | 1 + 7 files changed, 36 insertions(+), 47 deletions(-) create mode 100644 assets/icons/attach.svg diff --git a/assets/icons/attach.svg b/assets/icons/attach.svg new file mode 100644 index 0000000000000000000000000000000000000000..f923a3c7c8841fd358cf940d99e7371f010a6f4d --- /dev/null +++ b/assets/icons/attach.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/tool_think.svg b/assets/icons/tool_think.svg index efd5908a907b21c573ebc69fc13f5a210ab5d848..773f5e7fa7795d7bc56bba061d808418897f9287 100644 --- a/assets/icons/tool_think.svg +++ b/assets/icons/tool_think.svg @@ -1,3 +1,3 @@ - + diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index d6ccabb1304a17cf996158edcd2367adbaab46d2..ef666974f1e6a29345e469d8cf19fe13a3fd02eb 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -44,7 +44,7 @@ pub struct ClaudeCode; impl AgentServer for ClaudeCode { fn name(&self) -> &'static str { - "Welcome to Claude Code" + "Claude Code" } fn empty_state_headline(&self) -> &'static str { diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 3b892e793140462fdcc9b1f03242c96adcd6e059..29120fff6eec542855e376122e74097c4cad56b4 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -23,7 +23,7 @@ impl AgentServer for Gemini { } fn empty_state_headline(&self) -> &'static str { - "Welcome to Gemini CLI" + self.name() } fn empty_state_message(&self) -> &'static str { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 619885144ab306298af40ef7c472b0f69045c5f9..d27dee1fe6af47e358120d969509714b7d737d04 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1697,7 +1697,7 @@ impl AcpThreadView { .absolute() .top_0() .right_0() - .w_12() + .w_16() .h_full() .bg(linear_gradient( 90., @@ -1837,6 +1837,7 @@ impl AcpThreadView { .w_full() .max_w_full() .ml_1p5() + .overflow_hidden() .child(h_flex().pr_8().child(self.render_markdown( tool_call.label.clone(), default_markdown_style(false, true, window, cx), @@ -1906,13 +1907,10 @@ impl AcpThreadView { .text_color(cx.theme().colors().text_muted) .child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx))) .child( - Button::new(button_id, "Collapse") + IconButton::new(button_id, IconName::ChevronUp) .full_width() .style(ButtonStyle::Outlined) - .label_size(LabelSize::Small) - .icon(IconName::ChevronUp) .icon_color(Color::Muted) - .icon_position(IconPosition::Start) .on_click(cx.listener({ move |this: &mut Self, _, _, cx: &mut Context| { this.expanded_tool_calls.remove(&tool_call_id); @@ -2414,39 +2412,32 @@ impl AcpThreadView { return None; } + let has_both = user_rules_text.is_some() && rules_file_text.is_some(); + Some( - v_flex() + h_flex() .px_2p5() - .gap_1() + .pb_1() + .child( + Icon::new(IconName::Attach) + .size(IconSize::XSmall) + .color(Color::Disabled), + ) .when_some(user_rules_text, |parent, user_rules_text| { parent.child( h_flex() - .group("user-rules") .id("user-rules") - .w_full() - .child( - Icon::new(IconName::Reader) - .size(IconSize::XSmall) - .color(Color::Disabled), - ) + .ml_1() + .mr_1p5() .child( Label::new(user_rules_text) .size(LabelSize::XSmall) .color(Color::Muted) .truncate() - .buffer_font(cx) - .ml_1p5() - .mr_0p5(), - ) - .child( - IconButton::new("open-prompt-library", IconName::ArrowUpRight) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Ignored) - .visible_on_hover("user-rules") - // TODO: Figure out a way to pass focus handle here so we can display the `OpenRulesLibrary` keybinding - .tooltip(Tooltip::text("View User Rules")), + .buffer_font(cx), ) + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .tooltip(Tooltip::text("View User Rules")) .on_click(move |_event, window, cx| { window.dispatch_action( Box::new(OpenRulesLibrary { @@ -2457,33 +2448,20 @@ impl AcpThreadView { }), ) }) + .when(has_both, |this| this.child(Divider::vertical())) .when_some(rules_file_text, |parent, rules_file_text| { parent.child( h_flex() - .group("project-rules") .id("project-rules") - .w_full() - .child( - Icon::new(IconName::Reader) - .size(IconSize::XSmall) - .color(Color::Disabled), - ) + .ml_1p5() .child( Label::new(rules_file_text) .size(LabelSize::XSmall) .color(Color::Muted) - .buffer_font(cx) - .ml_1p5() - .mr_0p5(), - ) - .child( - IconButton::new("open-rule", IconName::ArrowUpRight) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Ignored) - .visible_on_hover("project-rules") - .tooltip(Tooltip::text("View Project Rules")), + .buffer_font(cx), ) + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .tooltip(Tooltip::text("View Project Rules")) .on_click(cx.listener(Self::handle_open_rules)), ) }) @@ -4080,8 +4058,10 @@ impl AcpThreadView { .group("thread-controls-container") .w_full() .mr_1() + .pt_1() .pb_2() .px(RESPONSE_PADDING_X) + .gap_px() .opacity(0.4) .hover(|style| style.opacity(1.)) .flex_wrap() diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index d2ff6aa4f393f6610a33179433e9708eef3789dd..469898d10fd99b9b6be8013ab9064c6cfaf6e95c 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2041,9 +2041,11 @@ impl AgentPanel { match state { ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT) .truncate() + .color(Color::Muted) .into_any_element(), ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER) .truncate() + .color(Color::Muted) .into_any_element(), ThreadSummary::Ready(_) => div() .w_full() @@ -2098,6 +2100,7 @@ impl AgentPanel { .into_any_element() } else { Label::new(thread_view.read(cx).title(cx)) + .color(Color::Muted) .truncate() .into_any_element() } @@ -2111,6 +2114,7 @@ impl AgentPanel { match summary { ContextSummary::Pending => Label::new(ContextSummary::DEFAULT) + .color(Color::Muted) .truncate() .into_any_element(), ContextSummary::Content(summary) => { @@ -2122,6 +2126,7 @@ impl AgentPanel { } else { Label::new(LOADING_SUMMARY_PLACEHOLDER) .truncate() + .color(Color::Muted) .into_any_element() } } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index b5f891713ab70a1fc32fbca3ec0f613af0338640..4fc6039fd76e753b5a515d8917c22b27c97737fc 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -34,6 +34,7 @@ pub enum IconName { ArrowRightLeft, ArrowUp, ArrowUpRight, + Attach, AudioOff, AudioOn, Backspace, From 4f0fad69960d0aad5cfd9840592d70fa82df5d91 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 22 Aug 2025 15:16:42 +0200 Subject: [PATCH 273/744] acp: Support calling tools provided by MCP servers (#36752) Release Notes: - N/A --- crates/agent2/src/tests/mod.rs | 441 +++++++++++++++++++++++++++++- crates/agent2/src/thread.rs | 142 +++++++--- crates/context_server/src/test.rs | 36 ++- 3 files changed, 558 insertions(+), 61 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 09048488c88db947c3f63deeab59d70df4c43ca6..60b31980812a0d4fa3580d85128b0f39509326ce 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -4,26 +4,35 @@ use agent_client_protocol::{self as acp}; use agent_settings::AgentProfileId; use anyhow::Result; use client::{Client, UserStore}; +use context_server::{ContextServer, ContextServerCommand, ContextServerId}; use fs::{FakeFs, Fs}; -use futures::{StreamExt, channel::mpsc::UnboundedReceiver}; +use futures::{ + StreamExt, + channel::{ + mpsc::{self, UnboundedReceiver}, + oneshot, + }, +}; use gpui::{ App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient, }; use indoc::indoc; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, - LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequestMessage, - LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason, - fake_provider::FakeLanguageModel, + LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolSchemaFormat, + LanguageModelToolUse, MessageContent, Role, StopReason, fake_provider::FakeLanguageModel, }; use pretty_assertions::assert_eq; -use project::Project; +use project::{ + Project, context_server_store::ContextServerStore, project_settings::ProjectSettings, +}; use prompt_store::ProjectContext; use reqwest_client::ReqwestClient; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::json; -use settings::SettingsStore; +use settings::{Settings, SettingsStore}; use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; use util::path; @@ -931,6 +940,334 @@ async fn test_profiles(cx: &mut TestAppContext) { assert_eq!(tool_names, vec![InfiniteTool::name()]); } +#[gpui::test] +async fn test_mcp_tools(cx: &mut TestAppContext) { + let ThreadTest { + model, + thread, + context_server_store, + fs, + .. + } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + // Override profiles and wait for settings to be loaded. + fs.insert_file( + paths::settings_file(), + json!({ + "agent": { + "profiles": { + "test": { + "name": "Test Profile", + "enable_all_context_servers": true, + "tools": { + EchoTool::name(): true, + } + }, + } + } + }) + .to_string() + .into_bytes(), + ) + .await; + cx.run_until_parked(); + thread.update(cx, |thread, _| { + thread.set_profile(AgentProfileId("test".into())) + }); + + let mut mcp_tool_calls = setup_context_server( + "test_server", + vec![context_server::types::Tool { + name: "echo".into(), + description: None, + input_schema: serde_json::to_value( + EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), + ) + .unwrap(), + output_schema: None, + annotations: None, + }], + &context_server_store, + cx, + ); + + let events = thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hey"], cx).unwrap() + }); + cx.run_until_parked(); + + // Simulate the model calling the MCP tool. + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!(tool_names_for_completion(&completion), vec!["echo"]); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "tool_1".into(), + name: "echo".into(), + raw_input: json!({"text": "test"}).to_string(), + input: json!({"text": "test"}), + is_input_complete: true, + }, + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap(); + assert_eq!(tool_call_params.name, "echo"); + assert_eq!(tool_call_params.arguments, Some(json!({"text": "test"}))); + tool_call_response + .send(context_server::types::CallToolResponse { + content: vec![context_server::types::ToolResponseContent::Text { + text: "test".into(), + }], + is_error: None, + meta: None, + structured_content: None, + }) + .unwrap(); + cx.run_until_parked(); + + assert_eq!(tool_names_for_completion(&completion), vec!["echo"]); + fake_model.send_last_completion_stream_text_chunk("Done!"); + fake_model.end_last_completion_stream(); + events.collect::>().await; + + // Send again after adding the echo tool, ensuring the name collision is resolved. + let events = thread.update(cx, |thread, cx| { + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["Go"], cx).unwrap() + }); + cx.run_until_parked(); + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + tool_names_for_completion(&completion), + vec!["echo", "test_server_echo"] + ); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "tool_2".into(), + name: "test_server_echo".into(), + raw_input: json!({"text": "mcp"}).to_string(), + input: json!({"text": "mcp"}), + is_input_complete: true, + }, + )); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "tool_3".into(), + name: "echo".into(), + raw_input: json!({"text": "native"}).to_string(), + input: json!({"text": "native"}), + is_input_complete: true, + }, + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap(); + assert_eq!(tool_call_params.name, "echo"); + assert_eq!(tool_call_params.arguments, Some(json!({"text": "mcp"}))); + tool_call_response + .send(context_server::types::CallToolResponse { + content: vec![context_server::types::ToolResponseContent::Text { text: "mcp".into() }], + is_error: None, + meta: None, + structured_content: None, + }) + .unwrap(); + cx.run_until_parked(); + + // Ensure the tool results were inserted with the correct names. + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + completion.messages.last().unwrap().content, + vec![ + MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id: "tool_3".into(), + tool_name: "echo".into(), + is_error: false, + content: "native".into(), + output: Some("native".into()), + },), + MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id: "tool_2".into(), + tool_name: "test_server_echo".into(), + is_error: false, + content: "mcp".into(), + output: Some("mcp".into()), + },), + ] + ); + fake_model.end_last_completion_stream(); + events.collect::>().await; +} + +#[gpui::test] +async fn test_mcp_tool_truncation(cx: &mut TestAppContext) { + let ThreadTest { + model, + thread, + context_server_store, + fs, + .. + } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + // Set up a profile with all tools enabled + fs.insert_file( + paths::settings_file(), + json!({ + "agent": { + "profiles": { + "test": { + "name": "Test Profile", + "enable_all_context_servers": true, + "tools": { + EchoTool::name(): true, + DelayTool::name(): true, + WordListTool::name(): true, + ToolRequiringPermission::name(): true, + InfiniteTool::name(): true, + } + }, + } + } + }) + .to_string() + .into_bytes(), + ) + .await; + cx.run_until_parked(); + + thread.update(cx, |thread, _| { + thread.set_profile(AgentProfileId("test".into())); + thread.add_tool(EchoTool); + thread.add_tool(DelayTool); + thread.add_tool(WordListTool); + thread.add_tool(ToolRequiringPermission); + thread.add_tool(InfiniteTool); + }); + + // Set up multiple context servers with some overlapping tool names + let _server1_calls = setup_context_server( + "xxx", + vec![ + context_server::types::Tool { + name: "echo".into(), // Conflicts with native EchoTool + description: None, + input_schema: serde_json::to_value( + EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), + ) + .unwrap(), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "unique_tool_1".into(), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + ], + &context_server_store, + cx, + ); + + let _server2_calls = setup_context_server( + "yyy", + vec![ + context_server::types::Tool { + name: "echo".into(), // Also conflicts with native EchoTool + description: None, + input_schema: serde_json::to_value( + EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), + ) + .unwrap(), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "unique_tool_2".into(), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + ], + &context_server_store, + cx, + ); + let _server3_calls = setup_context_server( + "zzz", + vec![ + context_server::types::Tool { + name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "c".repeat(MAX_TOOL_NAME_LENGTH + 1), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + ], + &context_server_store, + cx, + ); + + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Go"], cx) + }) + .unwrap(); + cx.run_until_parked(); + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + tool_names_for_completion(&completion), + vec![ + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "delay", + "echo", + "infinite", + "tool_requiring_permission", + "unique_tool_1", + "unique_tool_2", + "word_list", + "xxx_echo", + "y_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "yyy_echo", + "z_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ] + ); +} + #[gpui::test] #[cfg_attr(not(feature = "e2e"), ignore)] async fn test_cancellation(cx: &mut TestAppContext) { @@ -1806,6 +2143,7 @@ struct ThreadTest { model: Arc, thread: Entity, project_context: Entity, + context_server_store: Entity, fs: Arc, } @@ -1844,6 +2182,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { WordListTool::name(): true, ToolRequiringPermission::name(): true, InfiniteTool::name(): true, + ThinkingTool::name(): true, } } } @@ -1900,8 +2239,9 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { .await; let project_context = cx.new(|_cx| ProjectContext::default()); + let context_server_store = project.read_with(cx, |project, _| project.context_server_store()); let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); + cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx)); let thread = cx.new(|cx| { Thread::new( project, @@ -1916,6 +2256,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { model, thread, project_context, + context_server_store, fs, } } @@ -1950,3 +2291,89 @@ fn watch_settings(fs: Arc, cx: &mut App) { }) .detach(); } + +fn tool_names_for_completion(completion: &LanguageModelRequest) -> Vec { + completion + .tools + .iter() + .map(|tool| tool.name.clone()) + .collect() +} + +fn setup_context_server( + name: &'static str, + tools: Vec, + context_server_store: &Entity, + cx: &mut TestAppContext, +) -> mpsc::UnboundedReceiver<( + context_server::types::CallToolParams, + oneshot::Sender, +)> { + cx.update(|cx| { + let mut settings = ProjectSettings::get_global(cx).clone(); + settings.context_servers.insert( + name.into(), + project::project_settings::ContextServerSettings::Custom { + enabled: true, + command: ContextServerCommand { + path: "somebinary".into(), + args: Vec::new(), + env: None, + }, + }, + ); + ProjectSettings::override_global(settings, cx); + }); + + let (mcp_tool_calls_tx, mcp_tool_calls_rx) = mpsc::unbounded(); + let fake_transport = context_server::test::create_fake_transport(name, cx.executor()) + .on_request::(move |_params| async move { + context_server::types::InitializeResponse { + protocol_version: context_server::types::ProtocolVersion( + context_server::types::LATEST_PROTOCOL_VERSION.to_string(), + ), + server_info: context_server::types::Implementation { + name: name.into(), + version: "1.0.0".to_string(), + }, + capabilities: context_server::types::ServerCapabilities { + tools: Some(context_server::types::ToolsCapabilities { + list_changed: Some(true), + }), + ..Default::default() + }, + meta: None, + } + }) + .on_request::(move |_params| { + let tools = tools.clone(); + async move { + context_server::types::ListToolsResponse { + tools, + next_cursor: None, + meta: None, + } + } + }) + .on_request::(move |params| { + let mcp_tool_calls_tx = mcp_tool_calls_tx.clone(); + async move { + let (response_tx, response_rx) = oneshot::channel(); + mcp_tool_calls_tx + .unbounded_send((params, response_tx)) + .unwrap(); + response_rx.await.unwrap() + } + }); + context_server_store.update(cx, |store, cx| { + store.start_server( + Arc::new(ContextServer::new( + ContextServerId(name.into()), + Arc::new(fake_transport), + )), + cx, + ); + }); + cx.run_until_parked(); + mcp_tool_calls_rx +} diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index af18afa05583dc65b7e0b7e90696a6754c2de3ee..c89e5875f98534b7b13b0d9e5da905b481c82c9b 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -9,15 +9,15 @@ use action_log::ActionLog; use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot}; use agent_client_protocol as acp; use agent_settings::{ - AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT, - SUMMARIZE_THREAD_PROMPT, + AgentProfileId, AgentProfileSettings, AgentSettings, CompletionMode, + SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT, }; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; use chrono::{DateTime, Utc}; use client::{ModelRequestUsage, RequestUsage}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; -use collections::{HashMap, IndexMap}; +use collections::{HashMap, HashSet, IndexMap}; use fs::Fs; use futures::{ FutureExt, @@ -56,6 +56,7 @@ use util::{ResultExt, markdown::MarkdownCodeBlock}; use uuid::Uuid; const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user"; +pub const MAX_TOOL_NAME_LENGTH: usize = 64; /// The ID of the user prompt that initiated a request. /// @@ -627,7 +628,20 @@ impl Thread { stream: &ThreadEventStream, cx: &mut Context, ) { - let Some(tool) = self.tools.get(tool_use.name.as_ref()) else { + let tool = self.tools.get(tool_use.name.as_ref()).cloned().or_else(|| { + self.context_server_registry + .read(cx) + .servers() + .find_map(|(_, tools)| { + if let Some(tool) = tools.get(tool_use.name.as_ref()) { + Some(tool.clone()) + } else { + None + } + }) + }); + + let Some(tool) = tool else { stream .0 .unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall { @@ -1079,6 +1093,10 @@ impl Thread { self.cancel(cx); let model = self.model.clone().context("No language model configured")?; + let profile = AgentSettings::get_global(cx) + .profiles + .get(&self.profile_id) + .context("Profile not found")?; let (events_tx, events_rx) = mpsc::unbounded::>(); let event_stream = ThreadEventStream(events_tx); let message_ix = self.messages.len().saturating_sub(1); @@ -1086,6 +1104,7 @@ impl Thread { self.summary = None; self.running_turn = Some(RunningTurn { event_stream: event_stream.clone(), + tools: self.enabled_tools(profile, &model, cx), _task: cx.spawn(async move |this, cx| { log::info!("Starting agent turn execution"); @@ -1417,7 +1436,7 @@ impl Thread { ) -> Option> { cx.notify(); - let tool = self.tools.get(tool_use.name.as_ref()).cloned(); + let tool = self.tool(tool_use.name.as_ref()); let mut title = SharedString::from(&tool_use.name); let mut kind = acp::ToolKind::Other; if let Some(tool) = tool.as_ref() { @@ -1727,30 +1746,28 @@ impl Thread { cx: &mut App, ) -> Result { let model = self.model().context("No language model configured")?; - - log::debug!("Building completion request"); - log::debug!("Completion intent: {:?}", completion_intent); - log::debug!("Completion mode: {:?}", self.completion_mode); - - let messages = self.build_request_messages(cx); - log::info!("Request will include {} messages", messages.len()); - - let tools = if let Some(tools) = self.tools(cx).log_err() { - tools - .filter_map(|tool| { - let tool_name = tool.name().to_string(); + let tools = if let Some(turn) = self.running_turn.as_ref() { + turn.tools + .iter() + .filter_map(|(tool_name, tool)| { log::trace!("Including tool: {}", tool_name); Some(LanguageModelRequestTool { - name: tool_name, + name: tool_name.to_string(), description: tool.description().to_string(), input_schema: tool.input_schema(model.tool_input_format()).log_err()?, }) }) - .collect() + .collect::>() } else { Vec::new() }; + log::debug!("Building completion request"); + log::debug!("Completion intent: {:?}", completion_intent); + log::debug!("Completion mode: {:?}", self.completion_mode); + + let messages = self.build_request_messages(cx); + log::info!("Request will include {} messages", messages.len()); log::info!("Request includes {} tools", tools.len()); let request = LanguageModelRequest { @@ -1770,37 +1787,76 @@ impl Thread { Ok(request) } - fn tools<'a>(&'a self, cx: &'a App) -> Result>> { - let model = self.model().context("No language model configured")?; - - let profile = AgentSettings::get_global(cx) - .profiles - .get(&self.profile_id) - .context("profile not found")?; - let provider_id = model.provider_id(); + fn enabled_tools( + &self, + profile: &AgentProfileSettings, + model: &Arc, + cx: &App, + ) -> BTreeMap> { + fn truncate(tool_name: &SharedString) -> SharedString { + if tool_name.len() > MAX_TOOL_NAME_LENGTH { + let mut truncated = tool_name.to_string(); + truncated.truncate(MAX_TOOL_NAME_LENGTH); + truncated.into() + } else { + tool_name.clone() + } + } - Ok(self + let mut tools = self .tools .iter() - .filter(move |(_, tool)| tool.supported_provider(&provider_id)) .filter_map(|(tool_name, tool)| { - if profile.is_tool_enabled(tool_name) { - Some(tool) + if tool.supported_provider(&model.provider_id()) + && profile.is_tool_enabled(tool_name) + { + Some((truncate(tool_name), tool.clone())) } else { None } }) - .chain(self.context_server_registry.read(cx).servers().flat_map( - |(server_id, tools)| { - tools.iter().filter_map(|(tool_name, tool)| { - if profile.is_context_server_tool_enabled(&server_id.0, tool_name) { - Some(tool) - } else { - None - } - }) - }, - ))) + .collect::>(); + + let mut context_server_tools = Vec::new(); + let mut seen_tools = tools.keys().cloned().collect::>(); + let mut duplicate_tool_names = HashSet::default(); + for (server_id, server_tools) in self.context_server_registry.read(cx).servers() { + for (tool_name, tool) in server_tools { + if profile.is_context_server_tool_enabled(&server_id.0, &tool_name) { + let tool_name = truncate(tool_name); + if !seen_tools.insert(tool_name.clone()) { + duplicate_tool_names.insert(tool_name.clone()); + } + context_server_tools.push((server_id.clone(), tool_name, tool.clone())); + } + } + } + + // When there are duplicate tool names, disambiguate by prefixing them + // with the server ID. In the rare case there isn't enough space for the + // disambiguated tool name, keep only the last tool with this name. + for (server_id, tool_name, tool) in context_server_tools { + if duplicate_tool_names.contains(&tool_name) { + let available = MAX_TOOL_NAME_LENGTH.saturating_sub(tool_name.len()); + if available >= 2 { + let mut disambiguated = server_id.0.to_string(); + disambiguated.truncate(available - 1); + disambiguated.push('_'); + disambiguated.push_str(&tool_name); + tools.insert(disambiguated.into(), tool.clone()); + } else { + tools.insert(tool_name, tool.clone()); + } + } else { + tools.insert(tool_name, tool.clone()); + } + } + + tools + } + + fn tool(&self, name: &str) -> Option> { + self.running_turn.as_ref()?.tools.get(name).cloned() } fn build_request_messages(&self, cx: &App) -> Vec { @@ -1965,6 +2021,8 @@ struct RunningTurn { /// The current event stream for the running turn. Used to report a final /// cancellation event if we cancel the turn. event_stream: ThreadEventStream, + /// The tools that were enabled for this turn. + tools: BTreeMap>, } impl RunningTurn { diff --git a/crates/context_server/src/test.rs b/crates/context_server/src/test.rs index dedf589664215a733b7d6bd5c2273af246863f42..008542ab246bc2d68a62d779e985e5941ac16856 100644 --- a/crates/context_server/src/test.rs +++ b/crates/context_server/src/test.rs @@ -1,6 +1,6 @@ use anyhow::Context as _; use collections::HashMap; -use futures::{Stream, StreamExt as _, lock::Mutex}; +use futures::{FutureExt, Stream, StreamExt as _, future::BoxFuture, lock::Mutex}; use gpui::BackgroundExecutor; use std::{pin::Pin, sync::Arc}; @@ -14,9 +14,12 @@ pub fn create_fake_transport( executor: BackgroundExecutor, ) -> FakeTransport { let name = name.into(); - FakeTransport::new(executor).on_request::(move |_params| { - create_initialize_response(name.clone()) - }) + FakeTransport::new(executor).on_request::( + move |_params| { + let name = name.clone(); + async move { create_initialize_response(name.clone()) } + }, + ) } fn create_initialize_response(server_name: String) -> InitializeResponse { @@ -32,8 +35,10 @@ fn create_initialize_response(server_name: String) -> InitializeResponse { } pub struct FakeTransport { - request_handlers: - HashMap<&'static str, Arc serde_json::Value + Send + Sync>>, + request_handlers: HashMap< + &'static str, + Arc BoxFuture<'static, serde_json::Value>>, + >, tx: futures::channel::mpsc::UnboundedSender, rx: Arc>>, executor: BackgroundExecutor, @@ -50,18 +55,25 @@ impl FakeTransport { } } - pub fn on_request( + pub fn on_request( mut self, - handler: impl Fn(T::Params) -> T::Response + Send + Sync + 'static, - ) -> Self { + handler: impl 'static + Send + Sync + Fn(T::Params) -> Fut, + ) -> Self + where + T: crate::types::Request, + Fut: 'static + Send + Future, + { self.request_handlers.insert( T::METHOD, Arc::new(move |value| { - let params = value.get("params").expect("Missing parameters").clone(); + let params = value + .get("params") + .cloned() + .unwrap_or(serde_json::Value::Null); let params: T::Params = serde_json::from_value(params).expect("Invalid parameters received"); let response = handler(params); - serde_json::to_value(response).unwrap() + async move { serde_json::to_value(response.await).unwrap() }.boxed() }), ); self @@ -77,7 +89,7 @@ impl Transport for FakeTransport { if let Some(method) = msg.get("method") { let method = method.as_str().expect("Invalid method received"); if let Some(handler) = self.request_handlers.get(method) { - let payload = handler(msg); + let payload = handler(msg).await; let response = serde_json::json!({ "jsonrpc": "2.0", "id": id, From 54df43e06f5340c6e9ae5540550a3a2f102a521f Mon Sep 17 00:00:00 2001 From: Sarah Price <83782422+Louis454545@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:18:46 +0200 Subject: [PATCH 274/744] Fix cursor movement in protected files on backspace/delete (#36753) ## Summary Fixes cursor movement behavior in protected files (like Default Settings) when pressing backspace or delete keys. Previously, these keys would cause unwanted cursor movement instead of being ignored as expected in read-only files. ## Changes - Added read-only checks to `backspace()` and `delete()` methods in the editor - Consistent with existing pattern used by other editing methods (`indent()`, `outdent()`, `undo()`, etc.) ## Test Plan 1. Open Default Settings in Zed 2. Place cursor at arbitrary position (not at start/end of file) 3. Press backspace - cursor should remain in place (no movement) 4. Press delete - cursor should remain in place (no movement) Fixes #36302 Release Notes: - Fixed backspace and delete keys moving caret in protected files Co-authored-by: Claude --- crates/editor/src/editor.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2af8e6c0e4d235bd136d1f24a66645af2a6258cd..216aa2463bd67911f3b9e3471a3d214e3d3fbe1b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9809,6 +9809,9 @@ impl Editor { } pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_autoclose_pair(window, cx); @@ -9902,6 +9905,9 @@ impl Editor { } pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.change_selections(Default::default(), window, cx, |s| { From 92bbcdeb7daeaaea5dbb2148a457861fa7947603 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:34:55 +0200 Subject: [PATCH 275/744] workspace: Do not prompt for hanging up current call when replacing last visible project (#36697) This fixes a bug where in order to open a new project in a call (even if it's not shared), you need to hang up. Release Notes: - N/A --- crates/workspace/src/workspace.rs | 50 ++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 499e4f461902f8c2a509c6d40ec3d63843bb27c7..44aa94fe61cb45eda6dc22012ab8545bd6bff908 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2249,27 +2249,43 @@ impl Workspace { })?; if let Some(active_call) = active_call - && close_intent != CloseIntent::Quit && workspace_count == 1 && active_call.read_with(cx, |call, _| call.room().is_some())? { - let answer = cx.update(|window, cx| { - window.prompt( - PromptLevel::Warning, - "Do you want to leave the current call?", - None, - &["Close window and hang up", "Cancel"], - cx, - ) - })?; + if close_intent == CloseIntent::CloseWindow { + let answer = cx.update(|window, cx| { + window.prompt( + PromptLevel::Warning, + "Do you want to leave the current call?", + None, + &["Close window and hang up", "Cancel"], + cx, + ) + })?; - if answer.await.log_err() == Some(1) { - return anyhow::Ok(false); - } else { - active_call - .update(cx, |call, cx| call.hang_up(cx))? - .await - .log_err(); + if answer.await.log_err() == Some(1) { + return anyhow::Ok(false); + } else { + active_call + .update(cx, |call, cx| call.hang_up(cx))? + .await + .log_err(); + } + } + if close_intent == CloseIntent::ReplaceWindow { + _ = active_call.update(cx, |this, cx| { + let workspace = cx + .windows() + .iter() + .filter_map(|window| window.downcast::()) + .next() + .unwrap(); + let project = workspace.read(cx)?.project.clone(); + if project.read(cx).is_shared() { + this.unshare_project(project, cx)?; + } + Ok::<_, anyhow::Error>(()) + })?; } } From 3d2fa72d1fcf177e2beee1433c97e3bfa7adc09a Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 22 Aug 2025 16:58:17 +0300 Subject: [PATCH 276/744] Make word completions less intrusive (#36745) Introduce `min_words_query_len` threshold for automatic word completion display, and set it to 3 by default. Re-enable word completions in Markdown and Plaintext. Release Notes: - Introduced `min_words_query_len` threshold for automatic word completion display, and set it to 3 by default to make them less intrusive --- assets/settings/default.json | 11 ++-- .../src/copilot_completion_provider.rs | 2 + crates/editor/src/editor.rs | 35 ++++++++---- crates/editor/src/editor_tests.rs | 57 +++++++++++++++++++ crates/language/src/language_settings.rs | 13 ++++- docs/src/configuring-zed.md | 12 ++++ 6 files changed, 109 insertions(+), 21 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index c290baf0038d1b731f041e9c828746758bf9ffe3..014b4832505887046d70db7cad2c21d308422643 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1503,6 +1503,11 @@ // // Default: fallback "words": "fallback", + // Minimum number of characters required to automatically trigger word-based completions. + // Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command. + // + // Default: 3 + "words_min_length": 3, // Whether to fetch LSP completions or not. // // Default: true @@ -1642,9 +1647,6 @@ "use_on_type_format": false, "allow_rewrap": "anywhere", "soft_wrap": "editor_width", - "completions": { - "words": "disabled" - }, "prettier": { "allowed": true } @@ -1658,9 +1660,6 @@ } }, "Plain Text": { - "completions": { - "words": "disabled" - }, "allow_rewrap": "anywhere" }, "Python": { diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 9308500ed49f8f619f87aba585ee5e6b00a350ae..52d75175e5b5ba265bb32c6c15c713e1bd8faecd 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -301,6 +301,7 @@ mod tests { init_test(cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -533,6 +534,7 @@ mod tests { init_test(cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 216aa2463bd67911f3b9e3471a3d214e3d3fbe1b..a59eb930c3777fcc1b5cca1555dbe768a18d41d0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5576,6 +5576,11 @@ impl Editor { .as_ref() .is_none_or(|query| !query.chars().any(|c| c.is_digit(10))); + let omit_word_completions = match &query { + Some(query) => query.chars().count() < completion_settings.words_min_length, + None => completion_settings.words_min_length != 0, + }; + let (mut words, provider_responses) = match &provider { Some(provider) => { let provider_responses = provider.completions( @@ -5587,9 +5592,11 @@ impl Editor { cx, ); - let words = match completion_settings.words { - WordsCompletionMode::Disabled => Task::ready(BTreeMap::default()), - WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx + let words = match (omit_word_completions, completion_settings.words) { + (true, _) | (_, WordsCompletionMode::Disabled) => { + Task::ready(BTreeMap::default()) + } + (false, WordsCompletionMode::Enabled | WordsCompletionMode::Fallback) => cx .background_spawn(async move { buffer_snapshot.words_in_range(WordsQuery { fuzzy_contents: None, @@ -5601,16 +5608,20 @@ impl Editor { (words, provider_responses) } - None => ( - cx.background_spawn(async move { - buffer_snapshot.words_in_range(WordsQuery { - fuzzy_contents: None, - range: word_search_range, - skip_digits, + None => { + let words = if omit_word_completions { + Task::ready(BTreeMap::default()) + } else { + cx.background_spawn(async move { + buffer_snapshot.words_in_range(WordsQuery { + fuzzy_contents: None, + range: word_search_range, + skip_digits, + }) }) - }), - Task::ready(Ok(Vec::new())), - ), + }; + (words, Task::ready(Ok(Vec::new()))) + } }; let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 96261fdb2cd82a31b6e0787b738e514c74d7c5aa..5b854e3a97eabcc13072360dab7e63577ad5b8d2 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -12237,6 +12237,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { settings.defaults.completions = Some(CompletionSettings { lsp_insert_mode, words: WordsCompletionMode::Disabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, }); @@ -12295,6 +12296,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) update_test_language_settings(&mut cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, // set the opposite here to ensure that the action is overriding the default behavior lsp_insert_mode: LspInsertMode::Insert, lsp: true, @@ -12331,6 +12333,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) update_test_language_settings(&mut cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, // set the opposite here to ensure that the action is overriding the default behavior lsp_insert_mode: LspInsertMode::Replace, lsp: true, @@ -13072,6 +13075,7 @@ async fn test_word_completion(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Fallback, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 10, lsp_insert_mode: LspInsertMode::Insert, @@ -13168,6 +13172,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Enabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13231,6 +13236,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13304,6 +13310,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Fallback, + words_min_length: 0, lsp: false, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13361,6 +13368,56 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) { + init_test(cx, |language_settings| { + language_settings.defaults.completions = Some(CompletionSettings { + words: WordsCompletionMode::Enabled, + words_min_length: 3, + lsp: true, + lsp_fetch_timeout_ms: 0, + lsp_insert_mode: LspInsertMode::Insert, + }); + }); + + let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await; + cx.set_state(indoc! {"ˇ + wow + wowen + wowser + "}); + cx.simulate_keystroke("w"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _, _| { + if editor.context_menu.borrow_mut().is_some() { + panic!( + "expected completion menu to be hidden, as words completion threshold is not met" + ); + } + }); + + cx.simulate_keystroke("o"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _, _| { + if editor.context_menu.borrow_mut().is_some() { + panic!( + "expected completion menu to be hidden, as words completion threshold is not met still" + ); + } + }); + + cx.simulate_keystroke("w"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _, _| { + if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() + { + assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word"); + } else { + panic!("expected completion menu to be open after the word completions threshold is met"); + } + }); +} + fn gen_text_edit(params: &CompletionParams, text: &str) -> Option { let position = || lsp::Position { line: params.text_document_position.position.line, diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 386ad19747bfb5066dbf27f07214dcdff4829809..0f82d3997f981286c81dc18c29f8763b0402ddd2 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -350,6 +350,12 @@ pub struct CompletionSettings { /// Default: `fallback` #[serde(default = "default_words_completion_mode")] pub words: WordsCompletionMode, + /// How many characters has to be in the completions query to automatically show the words-based completions. + /// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command. + /// + /// Default: 3 + #[serde(default = "default_3")] + pub words_min_length: usize, /// Whether to fetch LSP completions or not. /// /// Default: true @@ -359,7 +365,7 @@ pub struct CompletionSettings { /// When set to 0, waits indefinitely. /// /// Default: 0 - #[serde(default = "default_lsp_fetch_timeout_ms")] + #[serde(default)] pub lsp_fetch_timeout_ms: u64, /// Controls how LSP completions are inserted. /// @@ -405,8 +411,8 @@ fn default_lsp_insert_mode() -> LspInsertMode { LspInsertMode::ReplaceSuffix } -fn default_lsp_fetch_timeout_ms() -> u64 { - 0 +fn default_3() -> usize { + 3 } /// The settings for a particular language. @@ -1468,6 +1474,7 @@ impl settings::Settings for AllLanguageSettings { } else { d.completions = Some(CompletionSettings { words: mode, + words_min_length: 3, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::ReplaceSuffix, diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 696370e310a32cdd3143de554fea5d54dc1edd88..fb139db6e404a22a25c63967ea2c46f94a9ca648 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -2425,6 +2425,7 @@ Examples: { "completions": { "words": "fallback", + "words_min_length": 3, "lsp": true, "lsp_fetch_timeout_ms": 0, "lsp_insert_mode": "replace_suffix" @@ -2444,6 +2445,17 @@ Examples: 2. `fallback` - Only if LSP response errors or times out, use document's words to show completions 3. `disabled` - Never fetch or complete document's words for completions (word-based completions can still be queried via a separate action) +### Min Words Query Length + +- Description: Minimum number of characters required to automatically trigger word-based completions. + Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command. +- Setting: `words_min_length` +- Default: `3` + +**Options** + +Positive integer values + ### LSP - Description: Whether to fetch LSP completions or not. From 8204ef1e51cb89dc46415e5efe12c8705d51dfdf Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:45:47 -0400 Subject: [PATCH 277/744] onboarding: Remove accept AI ToS from within Zed (#36612) Users now accept ToS from Zed's website when they sign in to Zed the first time. So it's no longer possible that a signed in account could not have accepted the ToS. Release Notes: - N/A --------- Co-authored-by: Mikayla Maki --- crates/agent_ui/src/active_thread.rs | 5 - crates/agent_ui/src/agent_configuration.rs | 10 +- crates/agent_ui/src/agent_panel.rs | 15 +- crates/agent_ui/src/message_editor.rs | 7 +- crates/agent_ui/src/text_thread_editor.rs | 16 -- crates/ai_onboarding/src/ai_onboarding.rs | 77 +------ crates/client/src/user.rs | 44 +--- .../cloud_api_client/src/cloud_api_client.rs | 28 --- crates/edit_prediction/src/edit_prediction.rs | 8 - .../src/edit_prediction_button.rs | 8 +- crates/editor/src/editor.rs | 41 ---- crates/language_model/src/language_model.rs | 12 +- crates/language_model/src/registry.rs | 12 - crates/language_models/src/provider/cloud.rs | 213 ++---------------- .../zed/src/zed/edit_prediction_registry.rs | 25 -- crates/zeta/src/zeta.rs | 22 +- 16 files changed, 44 insertions(+), 499 deletions(-) diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 2cad9132950787b9e5404e638275fb92147db5a7..e0cecad6e2e8b37d649a9dbc0d91268096670365 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1595,11 +1595,6 @@ impl ActiveThread { return; }; - if model.provider.must_accept_terms(cx) { - cx.notify(); - return; - } - let edited_text = state.editor.read(cx).text(cx); let creases = state.editor.update(cx, extract_message_creases); diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 00e48efdacf54ebb108ceb7ae6bb85f7c49bba8f..f33f0ba0321b9928b92974e835cd8d34553fc447 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -93,14 +93,6 @@ impl AgentConfiguration { let scroll_handle = ScrollHandle::new(); let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); - let mut expanded_provider_configurations = HashMap::default(); - if LanguageModelRegistry::read_global(cx) - .provider(&ZED_CLOUD_PROVIDER_ID) - .is_some_and(|cloud_provider| cloud_provider.must_accept_terms(cx)) - { - expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true); - } - let mut this = Self { fs, language_registry, @@ -109,7 +101,7 @@ impl AgentConfiguration { configuration_views_by_provider: HashMap::default(), context_server_store, expanded_context_server_tools: HashMap::default(), - expanded_provider_configurations, + expanded_provider_configurations: HashMap::default(), tools, _registry_subscription: registry_subscription, scroll_handle, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 469898d10fd99b9b6be8013ab9064c6cfaf6e95c..d0fb676fd2970affdbde701e6eec47e6ddeba810 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -54,9 +54,7 @@ use gpui::{ Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, }; use language::LanguageRegistry; -use language_model::{ - ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry, -}; +use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry}; use project::{DisableAiSettings, Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use rules_library::{RulesLibrary, open_rules_library}; @@ -3203,17 +3201,6 @@ impl AgentPanel { ConfigurationError::ModelNotFound | ConfigurationError::ProviderNotAuthenticated(_) | ConfigurationError::NoProvider => callout.into_any_element(), - ConfigurationError::ProviderPendingTermsAcceptance(provider) => { - Banner::new() - .severity(Severity::Warning) - .child(h_flex().w_full().children( - provider.render_accept_terms( - LanguageModelProviderTosView::ThreadEmptyState, - cx, - ), - )) - .into_any_element() - } } } diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index bed10e90a7315f69f7e89d749d94767276fa1a22..45e7529ec21c576354a556bdc27112da4d57e085 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -378,18 +378,13 @@ impl MessageEditor { } fn send_to_model(&mut self, window: &mut Window, cx: &mut Context) { - let Some(ConfiguredModel { model, provider }) = self + let Some(ConfiguredModel { model, .. }) = self .thread .update(cx, |thread, cx| thread.get_or_init_configured_model(cx)) else { return; }; - if provider.must_accept_terms(cx) { - cx.notify(); - return; - } - let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| { let creases = extract_message_creases(editor, cx); let text = editor.text(cx); diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 9fbd90c4a69694c7d76f39234aef7da3b105c22c..edb672a872f99f1f996aa799111fc520fd623c4a 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -190,7 +190,6 @@ pub struct TextThreadEditor { invoked_slash_command_creases: HashMap, _subscriptions: Vec, last_error: Option, - show_accept_terms: bool, pub(crate) slash_menu_handle: PopoverMenuHandle>, // dragged_file_worktrees is used to keep references to worktrees that were added @@ -289,7 +288,6 @@ impl TextThreadEditor { invoked_slash_command_creases: HashMap::default(), _subscriptions, last_error: None, - show_accept_terms: false, slash_menu_handle: Default::default(), dragged_file_worktrees: Vec::new(), language_model_selector: cx.new(|cx| { @@ -367,20 +365,7 @@ impl TextThreadEditor { } fn send_to_model(&mut self, window: &mut Window, cx: &mut Context) { - let provider = LanguageModelRegistry::read_global(cx) - .default_model() - .map(|default| default.provider); - if provider - .as_ref() - .is_some_and(|provider| provider.must_accept_terms(cx)) - { - self.show_accept_terms = true; - cx.notify(); - return; - } - self.last_error = None; - if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) { let new_selection = { let cursor = user_message @@ -1930,7 +1915,6 @@ impl TextThreadEditor { ConfigurationError::NoProvider | ConfigurationError::ModelNotFound | ConfigurationError::ProviderNotAuthenticated(_) => true, - ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms, } } diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 717abebfd1dd59ee277f7ae2343024aaafeb825e..6d8ac6472563ac0abd79d59f44b36a924eee1757 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -19,7 +19,7 @@ use std::sync::Arc; use client::{Client, UserStore, zed_urls}; use gpui::{AnyElement, Entity, IntoElement, ParentElement}; -use ui::{Divider, RegisterComponent, TintColor, Tooltip, prelude::*}; +use ui::{Divider, RegisterComponent, Tooltip, prelude::*}; #[derive(PartialEq)] pub enum SignInStatus { @@ -43,12 +43,10 @@ impl From for SignInStatus { #[derive(RegisterComponent, IntoElement)] pub struct ZedAiOnboarding { pub sign_in_status: SignInStatus, - pub has_accepted_terms_of_service: bool, pub plan: Option, pub account_too_young: bool, pub continue_with_zed_ai: Arc, pub sign_in: Arc, - pub accept_terms_of_service: Arc, pub dismiss_onboarding: Option>, } @@ -64,17 +62,9 @@ impl ZedAiOnboarding { Self { sign_in_status: status.into(), - has_accepted_terms_of_service: store.has_accepted_terms_of_service(), plan: store.plan(), account_too_young: store.account_too_young(), continue_with_zed_ai, - accept_terms_of_service: Arc::new({ - let store = user_store.clone(); - move |_window, cx| { - let task = store.update(cx, |store, cx| store.accept_terms_of_service(cx)); - task.detach_and_log_err(cx); - } - }), sign_in: Arc::new(move |_window, cx| { cx.spawn({ let client = client.clone(); @@ -94,42 +84,6 @@ impl ZedAiOnboarding { self } - fn render_accept_terms_of_service(&self) -> AnyElement { - v_flex() - .gap_1() - .w_full() - .child(Headline::new("Accept Terms of Service")) - .child( - Label::new("We don’t sell your data, track you across the web, or compromise your privacy.") - .color(Color::Muted) - .mb_2(), - ) - .child( - Button::new("terms_of_service", "Review Terms of Service") - .full_width() - .style(ButtonStyle::Outlined) - .icon(IconName::ArrowUpRight) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .on_click(move |_, _window, cx| { - telemetry::event!("Review Terms of Service Clicked"); - cx.open_url(&zed_urls::terms_of_service(cx)) - }), - ) - .child( - Button::new("accept_terms", "Accept") - .full_width() - .style(ButtonStyle::Tinted(TintColor::Accent)) - .on_click({ - let callback = self.accept_terms_of_service.clone(); - move |_, window, cx| { - telemetry::event!("Terms of Service Accepted"); - (callback)(window, cx)} - }), - ) - .into_any_element() - } - fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement { let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn); let plan_definitions = PlanDefinitions; @@ -359,14 +313,10 @@ impl ZedAiOnboarding { impl RenderOnce for ZedAiOnboarding { fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement { if matches!(self.sign_in_status, SignInStatus::SignedIn) { - if self.has_accepted_terms_of_service { - match self.plan { - None | Some(Plan::ZedFree) => self.render_free_plan_state(cx), - Some(Plan::ZedProTrial) => self.render_trial_state(cx), - Some(Plan::ZedPro) => self.render_pro_plan_state(cx), - } - } else { - self.render_accept_terms_of_service() + match self.plan { + None | Some(Plan::ZedFree) => self.render_free_plan_state(cx), + Some(Plan::ZedProTrial) => self.render_trial_state(cx), + Some(Plan::ZedPro) => self.render_pro_plan_state(cx), } } else { self.render_sign_in_disclaimer(cx) @@ -390,18 +340,15 @@ impl Component for ZedAiOnboarding { fn preview(_window: &mut Window, _cx: &mut App) -> Option { fn onboarding( sign_in_status: SignInStatus, - has_accepted_terms_of_service: bool, plan: Option, account_too_young: bool, ) -> AnyElement { ZedAiOnboarding { sign_in_status, - has_accepted_terms_of_service, plan, account_too_young, continue_with_zed_ai: Arc::new(|_, _| {}), sign_in: Arc::new(|_, _| {}), - accept_terms_of_service: Arc::new(|_, _| {}), dismiss_onboarding: None, } .into_any_element() @@ -415,27 +362,23 @@ impl Component for ZedAiOnboarding { .children(vec![ single_example( "Not Signed-in", - onboarding(SignInStatus::SignedOut, false, None, false), - ), - single_example( - "Not Accepted ToS", - onboarding(SignInStatus::SignedIn, false, None, false), + onboarding(SignInStatus::SignedOut, None, false), ), single_example( "Young Account", - onboarding(SignInStatus::SignedIn, true, None, true), + onboarding(SignInStatus::SignedIn, None, true), ), single_example( "Free Plan", - onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false), + onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false), ), single_example( "Pro Trial", - onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false), + onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false), ), single_example( "Pro Plan", - onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false), + onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false), ), ]) .into_any_element(), diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 20f99e394460f2f594c078dfde6cd568dfc7906b..1f8174dbc3c2e0bf428eb70abbc3161608589166 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,5 +1,5 @@ use super::{Client, Status, TypedEnvelope, proto}; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Context as _, Result}; use chrono::{DateTime, Utc}; use cloud_api_client::websocket_protocol::MessageToClient; use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo}; @@ -116,7 +116,6 @@ pub struct UserStore { edit_prediction_usage: Option, plan_info: Option, current_user: watch::Receiver>>, - accepted_tos_at: Option>, contacts: Vec>, incoming_contact_requests: Vec>, outgoing_contact_requests: Vec>, @@ -194,7 +193,6 @@ impl UserStore { plan_info: None, model_request_usage: None, edit_prediction_usage: None, - accepted_tos_at: None, contacts: Default::default(), incoming_contact_requests: Default::default(), participant_indices: Default::default(), @@ -271,7 +269,6 @@ impl UserStore { Status::SignedOut => { current_user_tx.send(None).await.ok(); this.update(cx, |this, cx| { - this.accepted_tos_at = None; cx.emit(Event::PrivateUserInfoUpdated); cx.notify(); this.clear_contacts() @@ -791,19 +788,6 @@ impl UserStore { .set_authenticated_user_info(Some(response.user.metrics_id.clone()), staff); } - let accepted_tos_at = { - #[cfg(debug_assertions)] - if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() { - None - } else { - response.user.accepted_tos_at - } - - #[cfg(not(debug_assertions))] - response.user.accepted_tos_at - }; - - self.accepted_tos_at = Some(accepted_tos_at); self.model_request_usage = Some(ModelRequestUsage(RequestUsage { limit: response.plan.usage.model_requests.limit, amount: response.plan.usage.model_requests.used as i32, @@ -846,32 +830,6 @@ impl UserStore { self.current_user.clone() } - pub fn has_accepted_terms_of_service(&self) -> bool { - self.accepted_tos_at - .is_some_and(|accepted_tos_at| accepted_tos_at.is_some()) - } - - pub fn accept_terms_of_service(&self, cx: &Context) -> Task> { - if self.current_user().is_none() { - return Task::ready(Err(anyhow!("no current user"))); - }; - - let client = self.client.clone(); - cx.spawn(async move |this, cx| -> anyhow::Result<()> { - let client = client.upgrade().context("client not found")?; - let response = client - .cloud_client() - .accept_terms_of_service() - .await - .context("error accepting tos")?; - this.update(cx, |this, cx| { - this.accepted_tos_at = Some(response.user.accepted_tos_at); - cx.emit(Event::PrivateUserInfoUpdated); - })?; - Ok(()) - }) - } - fn load_users( &self, request: impl RequestMessage, diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index 205f3e243296fdc830f32c2b664e615052ca7611..7fd96fcef0e8fd764bbcaa8ab59a9666095f9db9 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -115,34 +115,6 @@ impl CloudApiClient { })) } - pub async fn accept_terms_of_service(&self) -> Result { - let request = self.build_request( - Request::builder().method(Method::POST).uri( - self.http_client - .build_zed_cloud_url("/client/terms_of_service/accept", &[])? - .as_ref(), - ), - AsyncBody::default(), - )?; - - let mut response = self.http_client.send(request).await?; - - if !response.status().is_success() { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - anyhow::bail!( - "Failed to accept terms of service.\nStatus: {:?}\nBody: {body}", - response.status() - ) - } - - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - Ok(serde_json::from_str(&body)?) - } - pub async fn create_llm_token( &self, system_id: Option, diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs index 964f2029340f425546f8816f94a604cabe2aa294..6b695af1ae0e4807c9aa93af34a5d07de0c15795 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -89,9 +89,6 @@ pub trait EditPredictionProvider: 'static + Sized { debounce: bool, cx: &mut Context, ); - fn needs_terms_acceptance(&self, _cx: &App) -> bool { - false - } fn cycle( &mut self, buffer: Entity, @@ -124,7 +121,6 @@ pub trait EditPredictionProviderHandle { fn data_collection_state(&self, cx: &App) -> DataCollectionState; fn usage(&self, cx: &App) -> Option; fn toggle_data_collection(&self, cx: &mut App); - fn needs_terms_acceptance(&self, cx: &App) -> bool; fn is_refreshing(&self, cx: &App) -> bool; fn refresh( &self, @@ -196,10 +192,6 @@ where self.read(cx).is_enabled(buffer, cursor_position, cx) } - fn needs_terms_acceptance(&self, cx: &App) -> bool { - self.read(cx).needs_terms_acceptance(cx) - } - fn is_refreshing(&self, cx: &App) -> bool { self.read(cx).is_refreshing() } diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 4f69af7ee414a664af623d1bf980520ade6a4a49..0e3fe8cb1a449e494592d8f517feb26131d89f65 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -242,13 +242,9 @@ impl Render for EditPredictionButton { IconName::ZedPredictDisabled }; - if zeta::should_show_upsell_modal(&self.user_store, cx) { + if zeta::should_show_upsell_modal() { let tooltip_meta = if self.user_store.read(cx).current_user().is_some() { - if self.user_store.read(cx).has_accepted_terms_of_service() { - "Choose a Plan" - } else { - "Accept the Terms of Service" - } + "Choose a Plan" } else { "Sign In" }; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a59eb930c3777fcc1b5cca1555dbe768a18d41d0..29e009fdf8d5a8c06d12e36253db59886dd0b9be 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -253,7 +253,6 @@ pub type RenderDiffHunkControlsFn = Arc< enum ReportEditorEvent { Saved { auto_saved: bool }, EditorOpened, - ZetaTosClicked, Closed, } @@ -262,7 +261,6 @@ impl ReportEditorEvent { match self { Self::Saved { .. } => "Editor Saved", Self::EditorOpened => "Editor Opened", - Self::ZetaTosClicked => "Edit Prediction Provider ToS Clicked", Self::Closed => "Editor Closed", } } @@ -9180,45 +9178,6 @@ impl Editor { let provider = self.edit_prediction_provider.as_ref()?; let provider_icon = Self::get_prediction_provider_icon_name(&self.edit_prediction_provider); - if provider.provider.needs_terms_acceptance(cx) { - return Some( - h_flex() - .min_w(min_width) - .flex_1() - .px_2() - .py_1() - .gap_3() - .elevation_2(cx) - .hover(|style| style.bg(cx.theme().colors().element_hover)) - .id("accept-terms") - .cursor_pointer() - .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) - .on_click(cx.listener(|this, _event, window, cx| { - cx.stop_propagation(); - this.report_editor_event(ReportEditorEvent::ZetaTosClicked, None, cx); - window.dispatch_action( - zed_actions::OpenZedPredictOnboarding.boxed_clone(), - cx, - ); - })) - .child( - h_flex() - .flex_1() - .gap_2() - .child(Icon::new(provider_icon)) - .child(Label::new("Accept Terms of Service")) - .child(div().w_full()) - .child( - Icon::new(IconName::ArrowUpRight) - .color(Color::Muted) - .size(IconSize::Small), - ) - .into_any_element(), - ) - .into_any(), - ); - } - let is_refreshing = provider.provider.is_refreshing(cx); fn pending_completion_container(icon: IconName) -> Div { diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 158bebcbbf8843c25f5030f5b4b6e4ae436f371a..e0a3866443eee342ed17b8bf517eaa81604757c1 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -14,7 +14,7 @@ use client::Client; use cloud_llm_client::{CompletionMode, CompletionRequestStatus}; use futures::FutureExt; use futures::{StreamExt, future::BoxFuture, stream::BoxStream}; -use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window}; +use gpui::{AnyView, App, AsyncApp, SharedString, Task, Window}; use http_client::{StatusCode, http}; use icons::IconName; use parking_lot::Mutex; @@ -640,16 +640,6 @@ pub trait LanguageModelProvider: 'static { window: &mut Window, cx: &mut App, ) -> AnyView; - fn must_accept_terms(&self, _cx: &App) -> bool { - false - } - fn render_accept_terms( - &self, - _view: LanguageModelProviderTosView, - _cx: &mut App, - ) -> Option { - None - } fn reset_credentials(&self, cx: &mut App) -> Task>; } diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index bcbb3404a8e219808f1457fc7b2505ae703ec2f6..c7693a64c75efe63ec12df6a15ba5ad1cc9b9eba 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -24,9 +24,6 @@ pub enum ConfigurationError { ModelNotFound, #[error("{} LLM provider is not configured.", .0.name().0)] ProviderNotAuthenticated(Arc), - #[error("Using the {} LLM provider requires accepting the Terms of Service.", - .0.name().0)] - ProviderPendingTermsAcceptance(Arc), } impl std::fmt::Debug for ConfigurationError { @@ -37,9 +34,6 @@ impl std::fmt::Debug for ConfigurationError { Self::ProviderNotAuthenticated(provider) => { write!(f, "ProviderNotAuthenticated({})", provider.id()) } - Self::ProviderPendingTermsAcceptance(provider) => { - write!(f, "ProviderPendingTermsAcceptance({})", provider.id()) - } } } } @@ -198,12 +192,6 @@ impl LanguageModelRegistry { return Some(ConfigurationError::ProviderNotAuthenticated(model.provider)); } - if model.provider.must_accept_terms(cx) { - return Some(ConfigurationError::ProviderPendingTermsAcceptance( - model.provider, - )); - } - None } diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 8e4b7869353cc84bf78f84562ee348f37491b2a6..fb6e2fb1e463ffa6d06591e90d11674b1cb091a8 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -23,9 +23,9 @@ use language_model::{ AuthenticateError, LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelProviderTosView, LanguageModelRequest, - LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken, - ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, + LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, + LanguageModelToolSchemaFormat, LlmApiToken, ModelRequestLimitReachedError, + PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, }; use release_channel::AppVersion; use schemars::JsonSchema; @@ -118,7 +118,6 @@ pub struct State { llm_api_token: LlmApiToken, user_store: Entity, status: client::Status, - accept_terms_of_service_task: Option>>, models: Vec>, default_model: Option>, default_fast_model: Option>, @@ -142,7 +141,6 @@ impl State { llm_api_token: LlmApiToken::default(), user_store, status, - accept_terms_of_service_task: None, models: Vec::new(), default_model: None, default_fast_model: None, @@ -197,24 +195,6 @@ impl State { state.update(cx, |_, cx| cx.notify()) }) } - - fn has_accepted_terms_of_service(&self, cx: &App) -> bool { - self.user_store.read(cx).has_accepted_terms_of_service() - } - - fn accept_terms_of_service(&mut self, cx: &mut Context) { - let user_store = self.user_store.clone(); - self.accept_terms_of_service_task = Some(cx.spawn(async move |this, cx| { - let _ = user_store - .update(cx, |store, cx| store.accept_terms_of_service(cx))? - .await; - this.update(cx, |this, cx| { - this.accept_terms_of_service_task = None; - cx.notify() - }) - })); - } - fn update_models(&mut self, response: ListModelsResponse, cx: &mut Context) { let mut models = Vec::new(); @@ -384,7 +364,7 @@ impl LanguageModelProvider for CloudLanguageModelProvider { fn is_authenticated(&self, cx: &App) -> bool { let state = self.state.read(cx); - !state.is_signed_out(cx) && state.has_accepted_terms_of_service(cx) + !state.is_signed_out(cx) } fn authenticate(&self, _cx: &mut App) -> Task> { @@ -401,112 +381,11 @@ impl LanguageModelProvider for CloudLanguageModelProvider { .into() } - fn must_accept_terms(&self, cx: &App) -> bool { - !self.state.read(cx).has_accepted_terms_of_service(cx) - } - - fn render_accept_terms( - &self, - view: LanguageModelProviderTosView, - cx: &mut App, - ) -> Option { - let state = self.state.read(cx); - if state.has_accepted_terms_of_service(cx) { - return None; - } - Some( - render_accept_terms(view, state.accept_terms_of_service_task.is_some(), { - let state = self.state.clone(); - move |_window, cx| { - state.update(cx, |state, cx| state.accept_terms_of_service(cx)); - } - }) - .into_any_element(), - ) - } - fn reset_credentials(&self, _cx: &mut App) -> Task> { Task::ready(Ok(())) } } -fn render_accept_terms( - view_kind: LanguageModelProviderTosView, - accept_terms_of_service_in_progress: bool, - accept_terms_callback: impl Fn(&mut Window, &mut App) + 'static, -) -> impl IntoElement { - let thread_fresh_start = matches!(view_kind, LanguageModelProviderTosView::ThreadFreshStart); - let thread_empty_state = matches!(view_kind, LanguageModelProviderTosView::ThreadEmptyState); - - let terms_button = Button::new("terms_of_service", "Terms of Service") - .style(ButtonStyle::Subtle) - .icon(IconName::ArrowUpRight) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .when(thread_empty_state, |this| this.label_size(LabelSize::Small)) - .on_click(move |_, _window, cx| cx.open_url("https://zed.dev/terms-of-service")); - - let button_container = h_flex().child( - Button::new("accept_terms", "I accept the Terms of Service") - .when(!thread_empty_state, |this| { - this.full_width() - .style(ButtonStyle::Tinted(TintColor::Accent)) - .icon(IconName::Check) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - }) - .when(thread_empty_state, |this| { - this.style(ButtonStyle::Tinted(TintColor::Warning)) - .label_size(LabelSize::Small) - }) - .disabled(accept_terms_of_service_in_progress) - .on_click(move |_, window, cx| (accept_terms_callback)(window, cx)), - ); - - if thread_empty_state { - h_flex() - .w_full() - .flex_wrap() - .justify_between() - .child( - h_flex() - .child( - Label::new("To start using Zed AI, please read and accept the") - .size(LabelSize::Small), - ) - .child(terms_button), - ) - .child(button_container) - } else { - v_flex() - .w_full() - .gap_2() - .child( - h_flex() - .flex_wrap() - .when(thread_fresh_start, |this| this.justify_center()) - .child(Label::new( - "To start using Zed AI, please read and accept the", - )) - .child(terms_button), - ) - .child({ - match view_kind { - LanguageModelProviderTosView::TextThreadPopup => { - button_container.w_full().justify_end() - } - LanguageModelProviderTosView::Configuration => { - button_container.w_full().justify_start() - } - LanguageModelProviderTosView::ThreadFreshStart => { - button_container.w_full().justify_center() - } - LanguageModelProviderTosView::ThreadEmptyState => div().w_0(), - } - }) - } -} - pub struct CloudLanguageModel { id: LanguageModelId, model: Arc, @@ -1107,10 +986,7 @@ struct ZedAiConfiguration { plan: Option, subscription_period: Option<(DateTime, DateTime)>, eligible_for_trial: bool, - has_accepted_terms_of_service: bool, account_too_young: bool, - accept_terms_of_service_in_progress: bool, - accept_terms_of_service_callback: Arc, sign_in_callback: Arc, } @@ -1176,58 +1052,30 @@ impl RenderOnce for ZedAiConfiguration { ); } - v_flex() - .gap_2() - .w_full() - .when(!self.has_accepted_terms_of_service, |this| { - this.child(render_accept_terms( - LanguageModelProviderTosView::Configuration, - self.accept_terms_of_service_in_progress, - { - let callback = self.accept_terms_of_service_callback.clone(); - move |window, cx| (callback)(window, cx) - }, - )) - }) - .map(|this| { - if self.has_accepted_terms_of_service && self.account_too_young { - this.child(young_account_banner).child( - Button::new("upgrade", "Upgrade to Pro") - .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) - .full_width() - .on_click(|_, _, cx| { - cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)) - }), - ) - } else if self.has_accepted_terms_of_service { - this.text_sm() - .child(subscription_text) - .child(manage_subscription_buttons) - } else { - this - } - }) - .when(self.has_accepted_terms_of_service, |this| this) + v_flex().gap_2().w_full().map(|this| { + if self.account_too_young { + this.child(young_account_banner).child( + Button::new("upgrade", "Upgrade to Pro") + .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) + .full_width() + .on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))), + ) + } else { + this.text_sm() + .child(subscription_text) + .child(manage_subscription_buttons) + } + }) } } struct ConfigurationView { state: Entity, - accept_terms_of_service_callback: Arc, sign_in_callback: Arc, } impl ConfigurationView { fn new(state: Entity) -> Self { - let accept_terms_of_service_callback = Arc::new({ - let state = state.clone(); - move |_window: &mut Window, cx: &mut App| { - state.update(cx, |state, cx| { - state.accept_terms_of_service(cx); - }); - } - }); - let sign_in_callback = Arc::new({ let state = state.clone(); move |_window: &mut Window, cx: &mut App| { @@ -1239,7 +1087,6 @@ impl ConfigurationView { Self { state, - accept_terms_of_service_callback, sign_in_callback, } } @@ -1255,10 +1102,7 @@ impl Render for ConfigurationView { plan: user_store.plan(), subscription_period: user_store.subscription_period(), eligible_for_trial: user_store.trial_started_at().is_none(), - has_accepted_terms_of_service: state.has_accepted_terms_of_service(cx), account_too_young: user_store.account_too_young(), - accept_terms_of_service_in_progress: state.accept_terms_of_service_task.is_some(), - accept_terms_of_service_callback: self.accept_terms_of_service_callback.clone(), sign_in_callback: self.sign_in_callback.clone(), } } @@ -1283,7 +1127,6 @@ impl Component for ZedAiConfiguration { plan: Option, eligible_for_trial: bool, account_too_young: bool, - has_accepted_terms_of_service: bool, ) -> AnyElement { ZedAiConfiguration { is_connected, @@ -1292,10 +1135,7 @@ impl Component for ZedAiConfiguration { .is_some() .then(|| (Utc::now(), Utc::now() + chrono::Duration::days(7))), eligible_for_trial, - has_accepted_terms_of_service, account_too_young, - accept_terms_of_service_in_progress: false, - accept_terms_of_service_callback: Arc::new(|_, _| {}), sign_in_callback: Arc::new(|_, _| {}), } .into_any_element() @@ -1306,33 +1146,30 @@ impl Component for ZedAiConfiguration { .p_4() .gap_4() .children(vec![ - single_example( - "Not connected", - configuration(false, None, false, false, true), - ), + single_example("Not connected", configuration(false, None, false, false)), single_example( "Accept Terms of Service", - configuration(true, None, true, false, false), + configuration(true, None, true, false), ), single_example( "No Plan - Not eligible for trial", - configuration(true, None, false, false, true), + configuration(true, None, false, false), ), single_example( "No Plan - Eligible for trial", - configuration(true, None, true, false, true), + configuration(true, None, true, false), ), single_example( "Free Plan", - configuration(true, Some(Plan::ZedFree), true, false, true), + configuration(true, Some(Plan::ZedFree), true, false), ), single_example( "Zed Pro Trial Plan", - configuration(true, Some(Plan::ZedProTrial), true, false, true), + configuration(true, Some(Plan::ZedProTrial), true, false), ), single_example( "Zed Pro Plan", - configuration(true, Some(Plan::ZedPro), true, false, true), + configuration(true, Some(Plan::ZedPro), true, false), ), ]) .into_any_element(), diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index a9abd9bc7409e76c7e4fa6e35668535a951496b4..bc2d757fd1900ef0c3ce015a33476fd2c9df9eec 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -75,13 +75,10 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { let new_provider = all_language_settings(None, cx).edit_predictions.provider; if new_provider != provider { - let tos_accepted = user_store.read(cx).has_accepted_terms_of_service(); - telemetry::event!( "Edit Prediction Provider Changed", from = provider, to = new_provider, - zed_ai_tos_accepted = tos_accepted, ); provider = new_provider; @@ -92,28 +89,6 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { user_store.clone(), cx, ); - - if !tos_accepted { - match provider { - EditPredictionProvider::Zed => { - let Some(window) = cx.active_window() else { - return; - }; - - window - .update(cx, |_, window, cx| { - window.dispatch_action( - Box::new(zed_actions::OpenZedPredictOnboarding), - cx, - ); - }) - .ok(); - } - EditPredictionProvider::None - | EditPredictionProvider::Copilot - | EditPredictionProvider::Supermaven => {} - } - } } } }) diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 916699d29b73f4b158c2179360fd2319eb711de7..7b14d1279604bc8915e552a879cb6406f4a3948c 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -118,12 +118,8 @@ impl Dismissable for ZedPredictUpsell { } } -pub fn should_show_upsell_modal(user_store: &Entity, cx: &App) -> bool { - if user_store.read(cx).has_accepted_terms_of_service() { - !ZedPredictUpsell::dismissed() - } else { - true - } +pub fn should_show_upsell_modal() -> bool { + !ZedPredictUpsell::dismissed() } #[derive(Clone)] @@ -1547,16 +1543,6 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { ) -> bool { true } - - fn needs_terms_acceptance(&self, cx: &App) -> bool { - !self - .zeta - .read(cx) - .user_store - .read(cx) - .has_accepted_terms_of_service() - } - fn is_refreshing(&self) -> bool { !self.pending_completions.is_empty() } @@ -1569,10 +1555,6 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { _debounce: bool, cx: &mut Context, ) { - if self.needs_terms_acceptance(cx) { - return; - } - if self.zeta.read(cx).update_required { return; } From ac9fdaa1dad22e67731315f972177d860278600f Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 22 Aug 2025 11:51:01 -0400 Subject: [PATCH 278/744] onboarding: Improve Windows/Linux keyboard shortcuts; example ligature (#36712) Small fixes to onboarding. Correct ligature example. Replace`ctrl-escape` and `alt-tab` since they are reserved on windows (and often on linux) and so are caught by the OS. Release Notes: - N/A --- assets/keymaps/default-linux.json | 5 ++--- crates/onboarding/src/editing_page.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 955e68f5a9f76483456628604df6e52c24dc2e1a..fdc1403eb829ded9db1c908ca822fa777494548b 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -16,7 +16,6 @@ "up": "menu::SelectPrevious", "enter": "menu::Confirm", "ctrl-enter": "menu::SecondaryConfirm", - "ctrl-escape": "menu::Cancel", "ctrl-c": "menu::Cancel", "escape": "menu::Cancel", "alt-shift-enter": "menu::Restart", @@ -1195,8 +1194,8 @@ "ctrl-1": "onboarding::ActivateBasicsPage", "ctrl-2": "onboarding::ActivateEditingPage", "ctrl-3": "onboarding::ActivateAISetupPage", - "ctrl-escape": "onboarding::Finish", - "alt-tab": "onboarding::SignIn", + "ctrl-enter": "onboarding::Finish", + "alt-shift-l": "onboarding::SignIn", "alt-shift-a": "onboarding::OpenAccount" } } diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 8fae695854b7771f8fa8e7c19826860838ef844f..47dfd84894bf0ca5e7fd4a5a9ad0785d80b07ac5 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -606,7 +606,7 @@ fn render_popular_settings_section( cx: &mut App, ) -> impl IntoElement { const LIGATURE_TOOLTIP: &str = - "Font ligatures combine two characters into one. For example, turning =/= into ≠."; + "Font ligatures combine two characters into one. For example, turning != into ≠."; v_flex() .pt_6() From eb0f9ddcdc1305991b59adee2d87b3b1bea5b562 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Fri, 22 Aug 2025 19:03:47 +0300 Subject: [PATCH 279/744] themes: Implement Bright Black and Bright White colors (#36761) Before: image After: image Release Notes: - Fixed ANSI Bright Black and Bright White colors --- assets/themes/ayu/ayu.json | 6 +++--- assets/themes/gruvbox/gruvbox.json | 12 ++++++------ assets/themes/one/one.json | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/assets/themes/ayu/ayu.json b/assets/themes/ayu/ayu.json index f9f8720729008efb9a17cf45bd23ce51df7d3657..0ffbb9f61e76ba8e9bc1335de6b7ae4eb2e00418 100644 --- a/assets/themes/ayu/ayu.json +++ b/assets/themes/ayu/ayu.json @@ -93,7 +93,7 @@ "terminal.ansi.bright_cyan": "#4c806fff", "terminal.ansi.dim_cyan": "#cbf2e4ff", "terminal.ansi.white": "#bfbdb6ff", - "terminal.ansi.bright_white": "#bfbdb6ff", + "terminal.ansi.bright_white": "#fafafaff", "terminal.ansi.dim_white": "#787876ff", "link_text.hover": "#5ac1feff", "conflict": "#feb454ff", @@ -479,7 +479,7 @@ "terminal.ansi.bright_cyan": "#ace0cbff", "terminal.ansi.dim_cyan": "#2a5f4aff", "terminal.ansi.white": "#fcfcfcff", - "terminal.ansi.bright_white": "#fcfcfcff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#bcbec0ff", "link_text.hover": "#3b9ee5ff", "conflict": "#f1ad49ff", @@ -865,7 +865,7 @@ "terminal.ansi.bright_cyan": "#4c806fff", "terminal.ansi.dim_cyan": "#cbf2e4ff", "terminal.ansi.white": "#cccac2ff", - "terminal.ansi.bright_white": "#cccac2ff", + "terminal.ansi.bright_white": "#fafafaff", "terminal.ansi.dim_white": "#898a8aff", "link_text.hover": "#72cffeff", "conflict": "#fecf72ff", diff --git a/assets/themes/gruvbox/gruvbox.json b/assets/themes/gruvbox/gruvbox.json index 459825c733dbf2eae1e5269885b1b2c135bd72c4..f0f0358b764526fd1d45aae3b710d20f3cce1ce8 100644 --- a/assets/themes/gruvbox/gruvbox.json +++ b/assets/themes/gruvbox/gruvbox.json @@ -94,7 +94,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -494,7 +494,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -894,7 +894,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -1294,7 +1294,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", @@ -1694,7 +1694,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#f9f5d7ff", - "terminal.ansi.bright_white": "#f9f5d7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", @@ -2094,7 +2094,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#f2e5bcff", - "terminal.ansi.bright_white": "#f2e5bcff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json index 23ebbcc67efaa9ca45748a5726ac1fd72488c451..33f6d3c6221969f1453e0dea43dcde12c5549088 100644 --- a/assets/themes/one/one.json +++ b/assets/themes/one/one.json @@ -93,7 +93,7 @@ "terminal.ansi.bright_cyan": "#3a565bff", "terminal.ansi.dim_cyan": "#b9d9dfff", "terminal.ansi.white": "#dce0e5ff", - "terminal.ansi.bright_white": "#dce0e5ff", + "terminal.ansi.bright_white": "#fafafaff", "terminal.ansi.dim_white": "#575d65ff", "link_text.hover": "#74ade8ff", "version_control.added": "#27a657ff", @@ -468,7 +468,7 @@ "terminal.bright_foreground": "#242529ff", "terminal.dim_foreground": "#fafafaff", "terminal.ansi.black": "#242529ff", - "terminal.ansi.bright_black": "#242529ff", + "terminal.ansi.bright_black": "#747579ff", "terminal.ansi.dim_black": "#97979aff", "terminal.ansi.red": "#d36151ff", "terminal.ansi.bright_red": "#f0b0a4ff", @@ -489,7 +489,7 @@ "terminal.ansi.bright_cyan": "#a3bedaff", "terminal.ansi.dim_cyan": "#254058ff", "terminal.ansi.white": "#fafafaff", - "terminal.ansi.bright_white": "#fafafaff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#aaaaaaff", "link_text.hover": "#5c78e2ff", "version_control.added": "#27a657ff", From 42ae3301d01602514daf09b10b2ba5396fe5a731 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 22 Aug 2025 20:04:39 +0300 Subject: [PATCH 280/744] Show file open error view instead of the modal (#36764) Closes https://github.com/zed-industries/zed/issues/36672 Before: either image (when opening from the project panel) or image (for the rest of the cases) After: Screenshot 2025-08-22 at 19 34 10 (the unified error view) Release Notes: - Improved unsupported file opening in Zed --------- Co-authored-by: Conrad Irwin --- assets/keymaps/default-linux.json | 9 +- assets/keymaps/default-macos.json | 9 +- assets/keymaps/vim.json | 2 +- crates/editor/src/editor_tests.rs | 37 +++++++ crates/editor/src/items.rs | 11 ++ crates/project_panel/src/project_panel.rs | 3 +- crates/workspace/src/invalid_buffer_view.rs | 111 ++++++++++++++++++++ crates/workspace/src/item.rs | 18 ++++ crates/workspace/src/pane.rs | 100 +++++++++++++----- crates/workspace/src/workspace.rs | 56 +++++++--- crates/zed_actions/src/lib.rs | 5 +- 11 files changed, 316 insertions(+), 45 deletions(-) create mode 100644 crates/workspace/src/invalid_buffer_view.rs diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index fdc1403eb829ded9db1c908ca822fa777494548b..e84f4834af58ffcac596a73264260d7d1f922d89 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -855,7 +855,7 @@ "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-ctrl-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "project_panel::OpenWithSystem", + "ctrl-shift-enter": "workspace::OpenWithSystem", "alt-d": "project_panel::CompareMarkedFiles", "shift-find": "project_panel::NewSearchInDirectory", "ctrl-alt-shift-f": "project_panel::NewSearchInDirectory", @@ -1198,5 +1198,12 @@ "alt-shift-l": "onboarding::SignIn", "alt-shift-a": "onboarding::OpenAccount" } + }, + { + "context": "InvalidBuffer", + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-enter": "workspace::OpenWithSystem" + } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 8b18299a911f90507e78e06d4dff411b37276bb5..e72f4174ffe2afbd2605ed2bd859842f2c586107 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -915,7 +915,7 @@ "cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }], "cmd-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-cmd-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "project_panel::OpenWithSystem", + "ctrl-shift-enter": "workspace::OpenWithSystem", "alt-d": "project_panel::CompareMarkedFiles", "cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }], "cmd-alt-shift-f": "project_panel::NewSearchInDirectory", @@ -1301,5 +1301,12 @@ "alt-tab": "onboarding::SignIn", "alt-shift-a": "onboarding::OpenAccount" } + }, + { + "context": "InvalidBuffer", + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-enter": "workspace::OpenWithSystem" + } } ] diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index be6d34a1342b6fabe0561643c74034d3c99a04b6..62e50b3c8c21a39f6185e7e1c293c949ce6c6af8 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -819,7 +819,7 @@ "v": "project_panel::OpenPermanent", "p": "project_panel::Open", "x": "project_panel::RevealInFileManager", - "s": "project_panel::OpenWithSystem", + "s": "workspace::OpenWithSystem", "z d": "project_panel::CompareMarkedFiles", "] c": "project_panel::SelectNextGitEntry", "[ c": "project_panel::SelectPrevGitEntry", diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 5b854e3a97eabcc13072360dab7e63577ad5b8d2..03f5da9a20905782e5aec6ce0b3904aabf30d1a3 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -57,7 +57,9 @@ use util::{ use workspace::{ CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry, OpenOptions, ViewId, + invalid_buffer_view::InvalidBufferView, item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, + register_project_item, }; #[gpui::test] @@ -24348,6 +24350,41 @@ async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_non_utf_8_opens(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + cx.update(|cx| { + register_project_item::(cx); + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/root1", json!({})).await; + fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd]) + .await; + + let project = Project::test(fs, ["/root1".as_ref()], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let worktree_id = project.update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + + let handle = workspace + .update_in(cx, |workspace, window, cx| { + let project_path = (worktree_id, "one.pdf"); + workspace.open_path(project_path, None, true, window, cx) + }) + .await + .unwrap(); + + assert_eq!( + handle.to_any().entity_type(), + TypeId::of::() + ); +} + #[track_caller] fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec { editor diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index afc5767de010d67bdbe3e6fd21e1ffcfe840b801..641e8a97ed0ef45cab42e33fb40677dd7cb21fb4 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -42,6 +42,7 @@ use ui::{IconDecorationKind, prelude::*}; use util::{ResultExt, TryFutureExt, paths::PathExt}; use workspace::{ CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, + invalid_buffer_view::InvalidBufferView, item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, }; @@ -1401,6 +1402,16 @@ impl ProjectItem for Editor { editor } + + fn for_broken_project_item( + abs_path: PathBuf, + is_local: bool, + e: &anyhow::Error, + window: &mut Window, + cx: &mut App, + ) -> Option { + Some(InvalidBufferView::new(abs_path, is_local, e, window, cx)) + } } fn clip_ranges<'a>( diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 52ec7a9880089ad127736b0ed4f5732660ba1a6f..c99f5f8172b0fe51c49d23e7cbc5c6f9e714d7f0 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -69,6 +69,7 @@ use workspace::{ notifications::{DetachAndPromptErr, NotifyTaskExt}, }; use worktree::CreatedEntry; +use zed_actions::workspace::OpenWithSystem; const PROJECT_PANEL_KEY: &str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; @@ -255,8 +256,6 @@ actions!( RevealInFileManager, /// Removes the selected folder from the project. RemoveFromProject, - /// Opens the selected file with the system's default application. - OpenWithSystem, /// Cuts the selected file or directory. Cut, /// Pastes the previously cut or copied item. diff --git a/crates/workspace/src/invalid_buffer_view.rs b/crates/workspace/src/invalid_buffer_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..e2361d59671e8aa6192a9a333c8b679fb76f0e2d --- /dev/null +++ b/crates/workspace/src/invalid_buffer_view.rs @@ -0,0 +1,111 @@ +use std::{path::PathBuf, sync::Arc}; + +use gpui::{EventEmitter, FocusHandle, Focusable}; +use ui::{ + App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement, + KeyBinding, ParentElement, Render, SharedString, Styled as _, Window, h_flex, v_flex, +}; +use zed_actions::workspace::OpenWithSystem; + +use crate::Item; + +/// A view to display when a certain buffer fails to open. +pub struct InvalidBufferView { + /// Which path was attempted to open. + pub abs_path: Arc, + /// An error message, happened when opening the buffer. + pub error: SharedString, + is_local: bool, + focus_handle: FocusHandle, +} + +impl InvalidBufferView { + pub fn new( + abs_path: PathBuf, + is_local: bool, + e: &anyhow::Error, + _: &mut Window, + cx: &mut App, + ) -> Self { + Self { + is_local, + abs_path: Arc::new(abs_path), + error: format!("{e}").into(), + focus_handle: cx.focus_handle(), + } + } +} + +impl Item for InvalidBufferView { + type Event = (); + + fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString { + // Ensure we always render at least the filename. + detail += 1; + + let path = self.abs_path.as_path(); + + let mut prefix = path; + while detail > 0 { + if let Some(parent) = prefix.parent() { + prefix = parent; + detail -= 1; + } else { + break; + } + } + + let path = if detail > 0 { + path + } else { + path.strip_prefix(prefix).unwrap_or(path) + }; + + SharedString::new(path.to_string_lossy()) + } +} + +impl EventEmitter<()> for InvalidBufferView {} + +impl Focusable for InvalidBufferView { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for InvalidBufferView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { + let abs_path = self.abs_path.clone(); + v_flex() + .size_full() + .track_focus(&self.focus_handle(cx)) + .flex_none() + .justify_center() + .overflow_hidden() + .key_context("InvalidBuffer") + .child( + h_flex().size_full().justify_center().child( + v_flex() + .justify_center() + .gap_2() + .child("Cannot display the file contents in Zed") + .when(self.is_local, |contents| { + contents.child( + h_flex().justify_center().child( + Button::new("open-with-system", "Open in Default App") + .on_click(move |_, _, cx| { + cx.open_with_system(&abs_path); + }) + .style(ButtonStyle::Outlined) + .key_binding(KeyBinding::for_action( + &OpenWithSystem, + window, + cx, + )), + ), + ) + }), + ), + ) + } +} diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 5a497398f9189cbba1be734d3ba383e59b9fcc71..3485fcca439912a2db86b25a25295c42c198253a 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -1,6 +1,7 @@ use crate::{ CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, + invalid_buffer_view::InvalidBufferView, pane::{self, Pane}, persistence::model::ItemId, searchable::SearchableItemHandle, @@ -22,6 +23,7 @@ use std::{ any::{Any, TypeId}, cell::RefCell, ops::Range, + path::PathBuf, rc::Rc, sync::Arc, time::Duration, @@ -1161,6 +1163,22 @@ pub trait ProjectItem: Item { ) -> Self where Self: Sized; + + /// A fallback handler, which will be called after [`project::ProjectItem::try_open`] fails, + /// with the error from that failure as an argument. + /// Allows to open an item that can gracefully display and handle errors. + fn for_broken_project_item( + _abs_path: PathBuf, + _is_local: bool, + _e: &anyhow::Error, + _window: &mut Window, + _cx: &mut App, + ) -> Option + where + Self: Sized, + { + None + } } #[derive(Debug)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 23c8c0b1853e155df5d4a9b1cf3b4895bb74fd9a..e88402adc0036b27fd4a79acfaee9d8a5f76f074 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,6 +2,7 @@ use crate::{ CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace, WorkspaceItemBuilder, + invalid_buffer_view::InvalidBufferView, item::{ ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, ProjectItemKind, SaveOptions, ShowCloseButton, ShowDiagnostics, TabContentParams, @@ -897,19 +898,43 @@ impl Pane { } } } - if let Some((index, existing_item)) = existing_item { - // If the item is already open, and the item is a preview item - // and we are not allowing items to open as preview, mark the item as persistent. - if let Some(preview_item_id) = self.preview_item_id - && let Some(tab) = self.items.get(index) - && tab.item_id() == preview_item_id - && !allow_preview - { - self.set_preview_item_id(None, cx); - } - if activate { - self.activate_item(index, focus_item, focus_item, window, cx); + + let set_up_existing_item = + |index: usize, pane: &mut Self, window: &mut Window, cx: &mut Context| { + // If the item is already open, and the item is a preview item + // and we are not allowing items to open as preview, mark the item as persistent. + if let Some(preview_item_id) = pane.preview_item_id + && let Some(tab) = pane.items.get(index) + && tab.item_id() == preview_item_id + && !allow_preview + { + pane.set_preview_item_id(None, cx); + } + if activate { + pane.activate_item(index, focus_item, focus_item, window, cx); + } + }; + let set_up_new_item = |new_item: Box, + destination_index: Option, + pane: &mut Self, + window: &mut Window, + cx: &mut Context| { + if allow_preview { + pane.set_preview_item_id(Some(new_item.item_id()), cx); } + pane.add_item_inner( + new_item, + true, + focus_item, + activate, + destination_index, + window, + cx, + ); + }; + + if let Some((index, existing_item)) = existing_item { + set_up_existing_item(index, self, window, cx); existing_item } else { // If the item is being opened as preview and we have an existing preview tab, @@ -921,21 +946,46 @@ impl Pane { }; let new_item = build_item(self, window, cx); + // A special case that won't ever get a `project_entry_id` but has to be deduplicated nonetheless. + if let Some(invalid_buffer_view) = new_item.downcast::() { + let mut already_open_view = None; + let mut views_to_close = HashSet::default(); + for existing_error_view in self + .items_of_type::() + .filter(|item| item.read(cx).abs_path == invalid_buffer_view.read(cx).abs_path) + { + if already_open_view.is_none() + && existing_error_view.read(cx).error == invalid_buffer_view.read(cx).error + { + already_open_view = Some(existing_error_view); + } else { + views_to_close.insert(existing_error_view.item_id()); + } + } - if allow_preview { - self.set_preview_item_id(Some(new_item.item_id()), cx); - } - self.add_item_inner( - new_item.clone(), - true, - focus_item, - activate, - destination_index, - window, - cx, - ); + let resulting_item = match already_open_view { + Some(already_open_view) => { + if let Some(index) = self.index_for_item_id(already_open_view.item_id()) { + set_up_existing_item(index, self, window, cx); + } + Box::new(already_open_view) as Box<_> + } + None => { + set_up_new_item(new_item.clone(), destination_index, self, window, cx); + new_item + } + }; - new_item + self.close_items(window, cx, SaveIntent::Skip, |existing_item| { + views_to_close.contains(&existing_item) + }) + .detach(); + + resulting_item + } else { + set_up_new_item(new_item.clone(), destination_index, self, window, cx); + new_item + } } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 44aa94fe61cb45eda6dc22012ab8545bd6bff908..d31aae2c59775cc097f6fa3e34886ff69563f58e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1,5 +1,6 @@ pub mod dock; pub mod history_manager; +pub mod invalid_buffer_view; pub mod item; mod modal_layer; pub mod notifications; @@ -612,21 +613,49 @@ impl ProjectItemRegistry { ); self.build_project_item_for_path_fns .push(|project, project_path, window, cx| { + let project_path = project_path.clone(); + let abs_path = project.read(cx).absolute_path(&project_path, cx); + let is_local = project.read(cx).is_local(); let project_item = - ::try_open(project, project_path, cx)?; + ::try_open(project, &project_path, cx)?; let project = project.clone(); - Some(window.spawn(cx, async move |cx| { - let project_item = project_item.await?; - let project_entry_id: Option = - project_item.read_with(cx, project::ProjectItem::entry_id)?; - let build_workspace_item = Box::new( - |pane: &mut Pane, window: &mut Window, cx: &mut Context| { - Box::new(cx.new(|cx| { - T::for_project_item(project, Some(pane), project_item, window, cx) - })) as Box + Some(window.spawn(cx, async move |cx| match project_item.await { + Ok(project_item) => { + let project_item = project_item; + let project_entry_id: Option = + project_item.read_with(cx, project::ProjectItem::entry_id)?; + let build_workspace_item = Box::new( + |pane: &mut Pane, window: &mut Window, cx: &mut Context| { + Box::new(cx.new(|cx| { + T::for_project_item( + project, + Some(pane), + project_item, + window, + cx, + ) + })) as Box + }, + ) as Box<_>; + Ok((project_entry_id, build_workspace_item)) + } + Err(e) => match abs_path { + Some(abs_path) => match cx.update(|window, cx| { + T::for_broken_project_item(abs_path, is_local, &e, window, cx) + })? { + Some(broken_project_item_view) => { + let build_workspace_item = Box::new( + move |_: &mut Pane, _: &mut Window, cx: &mut Context| { + cx.new(|_| broken_project_item_view).boxed_clone() + }, + ) + as Box<_>; + Ok((None, build_workspace_item)) + } + None => Err(e)?, }, - ) as Box<_>; - Ok((project_entry_id, build_workspace_item)) + None => Err(e)?, + }, })) }); } @@ -3379,9 +3408,8 @@ impl Workspace { window: &mut Window, cx: &mut App, ) -> Task, WorkspaceItemBuilder)>> { - let project = self.project().clone(); let registry = cx.default_global::().clone(); - registry.open_path(&project, &path, window, cx) + registry.open_path(self.project(), &path, window, cx) } pub fn find_project_item( diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 9455369e9a2c234ba39572642d382d6d8dd76c46..069abc0a12736564bb72849ab54cc2bfbce11a2b 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -156,7 +156,10 @@ pub mod workspace { #[action(deprecated_aliases = ["editor::CopyPath", "outline_panel::CopyPath", "project_panel::CopyPath"])] CopyPath, #[action(deprecated_aliases = ["editor::CopyRelativePath", "outline_panel::CopyRelativePath", "project_panel::CopyRelativePath"])] - CopyRelativePath + CopyRelativePath, + /// Opens the selected file with the system's default application. + #[action(deprecated_aliases = ["project_panel::OpenWithSystem"])] + OpenWithSystem, ] ); } From 72bd248544c58f7bee885bf2ec3f527772e25db5 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 22 Aug 2025 20:49:12 +0200 Subject: [PATCH 281/744] editor: Fix multi buffer header context menu not handling absolute paths (#36769) Release Notes: - N/A --- crates/editor/src/element.rs | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 797b0d663475cc074a688af8af38f044d0523809..32582ba9411411230c9ff3e7339a951ff1fd33ff 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -74,6 +74,7 @@ use std::{ fmt::{self, Write}, iter, mem, ops::{Deref, Range}, + path::Path, rc::Rc, sync::Arc, time::{Duration, Instant}, @@ -3693,7 +3694,12 @@ impl EditorElement { }) .take(1), ) - .children(indicator) + .child( + h_flex() + .size(Pixels(12.0)) + .justify_center() + .children(indicator), + ) .child( h_flex() .cursor_pointer() @@ -3782,25 +3788,31 @@ impl EditorElement { && let Some(worktree) = project.read(cx).worktree_for_id(file.worktree_id(cx), cx) { + let worktree = worktree.read(cx); let relative_path = file.path(); - let entry_for_path = worktree.read(cx).entry_for_path(relative_path); - let abs_path = entry_for_path.and_then(|e| e.canonical_path.as_deref()); - let has_relative_path = - worktree.read(cx).root_entry().is_some_and(Entry::is_dir); + let entry_for_path = worktree.entry_for_path(relative_path); + let abs_path = entry_for_path.map(|e| { + e.canonical_path.as_deref().map_or_else( + || worktree.abs_path().join(relative_path), + Path::to_path_buf, + ) + }); + let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir); - let parent_abs_path = - abs_path.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); + let parent_abs_path = abs_path + .as_ref() + .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); let relative_path = has_relative_path .then_some(relative_path) .map(ToOwned::to_owned); let visible_in_project_panel = - relative_path.is_some() && worktree.read(cx).is_visible(); + relative_path.is_some() && worktree.is_visible(); let reveal_in_project_panel = entry_for_path .filter(|_| visible_in_project_panel) .map(|entry| entry.id); menu = menu - .when_some(abs_path.map(ToOwned::to_owned), |menu, abs_path| { + .when_some(abs_path, |menu, abs_path| { menu.entry( "Copy Path", Some(Box::new(zed_actions::workspace::CopyPath)), From 18ac4ac5ef0548e66cd6785ab218dce7eb1de267 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Fri, 22 Aug 2025 16:32:49 -0300 Subject: [PATCH 282/744] ACP debug tools pane (#36768) Adds a new "acp: open debug tools" action that opens a new workspace item with a log of ACP messages for the active connection. Release Notes: - N/A --- Cargo.lock | 27 +- Cargo.toml | 4 +- crates/acp_tools/Cargo.toml | 30 ++ crates/acp_tools/LICENSE-GPL | 1 + crates/acp_tools/src/acp_tools.rs | 494 +++++++++++++++++++++++++++++ crates/agent_servers/Cargo.toml | 1 + crates/agent_servers/src/acp/v1.rs | 11 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + crates/zed/src/zed.rs | 1 + script/squawk | 12 +- tooling/workspace-hack/Cargo.toml | 8 +- 12 files changed, 574 insertions(+), 17 deletions(-) create mode 100644 crates/acp_tools/Cargo.toml create mode 120000 crates/acp_tools/LICENSE-GPL create mode 100644 crates/acp_tools/src/acp_tools.rs diff --git a/Cargo.lock b/Cargo.lock index 2b3d7b26917988616ef128648919ea129da2bd3f..cd1018d4c9577cf0da7600b95721b60bd6611431 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,26 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "acp_tools" +version = "0.1.0" +dependencies = [ + "agent-client-protocol", + "collections", + "gpui", + "language", + "markdown", + "project", + "serde", + "serde_json", + "settings", + "theme", + "ui", + "util", + "workspace", + "workspace-hack", +] + [[package]] name = "action_log" version = "0.1.0" @@ -171,11 +191,12 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.30" +version = "0.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f792e009ba59b137ee1db560bc37e567887ad4b5af6f32181d381fff690e2d4" +checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860" dependencies = [ "anyhow", + "async-broadcast", "futures 0.3.31", "log", "parking_lot", @@ -264,6 +285,7 @@ name = "agent_servers" version = "0.1.0" dependencies = [ "acp_thread", + "acp_tools", "action_log", "agent-client-protocol", "agent_settings", @@ -20417,6 +20439,7 @@ dependencies = [ name = "zed" version = "0.202.0" dependencies = [ + "acp_tools", "activity_indicator", "agent", "agent_servers", diff --git a/Cargo.toml b/Cargo.toml index 84de9b30adddac8a46366fd4760e243cc7ceab90..7668d1875249938b249bfcef360a0a90d9421a77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "crates/acp_tools", "crates/acp_thread", "crates/action_log", "crates/activity_indicator", @@ -227,6 +228,7 @@ edition = "2024" # Workspace member crates # +acp_tools = { path = "crates/acp_tools" } acp_thread = { path = "crates/acp_thread" } action_log = { path = "crates/action_log" } agent = { path = "crates/agent" } @@ -425,7 +427,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.30" +agent-client-protocol = "0.0.31" 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_tools/Cargo.toml b/crates/acp_tools/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..7a6d8c21a096364a8468671f4186048559ec8a61 --- /dev/null +++ b/crates/acp_tools/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "acp_tools" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + + +[lints] +workspace = true + +[lib] +path = "src/acp_tools.rs" +doctest = false + +[dependencies] +agent-client-protocol.workspace = true +collections.workspace = true +gpui.workspace = true +language.workspace= true +markdown.workspace = true +project.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +theme.workspace = true +ui.workspace = true +util.workspace = true +workspace-hack.workspace = true +workspace.workspace = true diff --git a/crates/acp_tools/LICENSE-GPL b/crates/acp_tools/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/acp_tools/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs new file mode 100644 index 0000000000000000000000000000000000000000..ca5e57e85aa8e96dde5362fe0608e60f4fa12a81 --- /dev/null +++ b/crates/acp_tools/src/acp_tools.rs @@ -0,0 +1,494 @@ +use std::{ + cell::RefCell, + collections::HashSet, + fmt::Display, + rc::{Rc, Weak}, + sync::Arc, +}; + +use agent_client_protocol as acp; +use collections::HashMap; +use gpui::{ + App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState, + StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*, +}; +use language::LanguageRegistry; +use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle}; +use project::Project; +use settings::Settings; +use theme::ThemeSettings; +use ui::prelude::*; +use util::ResultExt as _; +use workspace::{Item, Workspace}; + +actions!(acp, [OpenDebugTools]); + +pub fn init(cx: &mut App) { + cx.observe_new( + |workspace: &mut Workspace, _window, _cx: &mut Context| { + workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| { + let acp_tools = + Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx))); + workspace.add_item_to_active_pane(acp_tools, None, true, window, cx); + }); + }, + ) + .detach(); +} + +struct GlobalAcpConnectionRegistry(Entity); + +impl Global for GlobalAcpConnectionRegistry {} + +#[derive(Default)] +pub struct AcpConnectionRegistry { + active_connection: RefCell>, +} + +struct ActiveConnection { + server_name: &'static str, + connection: Weak, +} + +impl AcpConnectionRegistry { + pub fn default_global(cx: &mut App) -> Entity { + if cx.has_global::() { + cx.global::().0.clone() + } else { + let registry = cx.new(|_cx| AcpConnectionRegistry::default()); + cx.set_global(GlobalAcpConnectionRegistry(registry.clone())); + registry + } + } + + pub fn set_active_connection( + &self, + server_name: &'static str, + connection: &Rc, + cx: &mut Context, + ) { + self.active_connection.replace(Some(ActiveConnection { + server_name, + connection: Rc::downgrade(connection), + })); + cx.notify(); + } +} + +struct AcpTools { + project: Entity, + focus_handle: FocusHandle, + expanded: HashSet, + watched_connection: Option, + connection_registry: Entity, + _subscription: Subscription, +} + +struct WatchedConnection { + server_name: &'static str, + messages: Vec, + list_state: ListState, + connection: Weak, + incoming_request_methods: HashMap>, + outgoing_request_methods: HashMap>, + _task: Task<()>, +} + +impl AcpTools { + fn new(project: Entity, cx: &mut Context) -> Self { + let connection_registry = AcpConnectionRegistry::default_global(cx); + + let subscription = cx.observe(&connection_registry, |this, _, cx| { + this.update_connection(cx); + cx.notify(); + }); + + let mut this = Self { + project, + focus_handle: cx.focus_handle(), + expanded: HashSet::default(), + watched_connection: None, + connection_registry, + _subscription: subscription, + }; + this.update_connection(cx); + this + } + + fn update_connection(&mut self, cx: &mut Context) { + let active_connection = self.connection_registry.read(cx).active_connection.borrow(); + let Some(active_connection) = active_connection.as_ref() else { + return; + }; + + if let Some(watched_connection) = self.watched_connection.as_ref() { + if Weak::ptr_eq( + &watched_connection.connection, + &active_connection.connection, + ) { + return; + } + } + + if let Some(connection) = active_connection.connection.upgrade() { + let mut receiver = connection.subscribe(); + let task = cx.spawn(async move |this, cx| { + while let Ok(message) = receiver.recv().await { + this.update(cx, |this, cx| { + this.push_stream_message(message, cx); + }) + .ok(); + } + }); + + self.watched_connection = Some(WatchedConnection { + server_name: active_connection.server_name, + messages: vec![], + list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)), + connection: active_connection.connection.clone(), + incoming_request_methods: HashMap::default(), + outgoing_request_methods: HashMap::default(), + _task: task, + }); + } + } + + fn push_stream_message(&mut self, stream_message: acp::StreamMessage, cx: &mut Context) { + let Some(connection) = self.watched_connection.as_mut() else { + return; + }; + let language_registry = self.project.read(cx).languages().clone(); + let index = connection.messages.len(); + + let (request_id, method, message_type, params) = match stream_message.message { + acp::StreamMessageContent::Request { id, method, params } => { + let method_map = match stream_message.direction { + acp::StreamMessageDirection::Incoming => { + &mut connection.incoming_request_methods + } + acp::StreamMessageDirection::Outgoing => { + &mut connection.outgoing_request_methods + } + }; + + method_map.insert(id, method.clone()); + (Some(id), method.into(), MessageType::Request, Ok(params)) + } + acp::StreamMessageContent::Response { id, result } => { + let method_map = match stream_message.direction { + acp::StreamMessageDirection::Incoming => { + &mut connection.outgoing_request_methods + } + acp::StreamMessageDirection::Outgoing => { + &mut connection.incoming_request_methods + } + }; + + if let Some(method) = method_map.remove(&id) { + (Some(id), method.into(), MessageType::Response, result) + } else { + ( + Some(id), + "[unrecognized response]".into(), + MessageType::Response, + result, + ) + } + } + acp::StreamMessageContent::Notification { method, params } => { + (None, method.into(), MessageType::Notification, Ok(params)) + } + }; + + let message = WatchedConnectionMessage { + name: method, + message_type, + request_id, + direction: stream_message.direction, + collapsed_params_md: match params.as_ref() { + Ok(params) => params + .as_ref() + .map(|params| collapsed_params_md(params, &language_registry, cx)), + Err(err) => { + if let Ok(err) = &serde_json::to_value(err) { + Some(collapsed_params_md(&err, &language_registry, cx)) + } else { + None + } + } + }, + + expanded_params_md: None, + params, + }; + + connection.messages.push(message); + connection.list_state.splice(index..index, 1); + cx.notify(); + } + + fn render_message( + &mut self, + index: usize, + window: &mut Window, + cx: &mut Context, + ) -> AnyElement { + let Some(connection) = self.watched_connection.as_ref() else { + return Empty.into_any(); + }; + + let Some(message) = connection.messages.get(index) else { + return Empty.into_any(); + }; + + let base_size = TextSize::Editor.rems(cx); + + let theme_settings = ThemeSettings::get_global(cx); + let text_style = window.text_style(); + + let colors = cx.theme().colors(); + let expanded = self.expanded.contains(&index); + + v_flex() + .w_full() + .px_4() + .py_3() + .border_color(colors.border) + .border_b_1() + .gap_2() + .items_start() + .font_buffer(cx) + .text_size(base_size) + .id(index) + .group("message") + .hover(|this| this.bg(colors.element_background.opacity(0.5))) + .on_click(cx.listener(move |this, _, _, cx| { + if this.expanded.contains(&index) { + this.expanded.remove(&index); + } else { + this.expanded.insert(index); + let Some(connection) = &mut this.watched_connection else { + return; + }; + let Some(message) = connection.messages.get_mut(index) else { + return; + }; + message.expanded(this.project.read(cx).languages().clone(), cx); + connection.list_state.scroll_to_reveal_item(index); + } + cx.notify() + })) + .child( + h_flex() + .w_full() + .gap_2() + .items_center() + .flex_shrink_0() + .child(match message.direction { + acp::StreamMessageDirection::Incoming => { + ui::Icon::new(ui::IconName::ArrowDown).color(Color::Error) + } + acp::StreamMessageDirection::Outgoing => { + ui::Icon::new(ui::IconName::ArrowUp).color(Color::Success) + } + }) + .child( + Label::new(message.name.clone()) + .buffer_font(cx) + .color(Color::Muted), + ) + .child(div().flex_1()) + .child( + div() + .child(ui::Chip::new(message.message_type.to_string())) + .visible_on_hover("message"), + ) + .children( + message + .request_id + .map(|req_id| div().child(ui::Chip::new(req_id.to_string()))), + ), + ) + // I'm aware using markdown is a hack. Trying to get something working for the demo. + // Will clean up soon! + .when_some( + if expanded { + message.expanded_params_md.clone() + } else { + message.collapsed_params_md.clone() + }, + |this, params| { + this.child( + div().pl_6().w_full().child( + MarkdownElement::new( + params, + MarkdownStyle { + base_text_style: text_style, + selection_background_color: colors.element_selection_background, + syntax: cx.theme().syntax().clone(), + code_block_overflow_x_scroll: true, + code_block: StyleRefinement { + text: Some(TextStyleRefinement { + font_family: Some( + theme_settings.buffer_font.family.clone(), + ), + font_size: Some((base_size * 0.8).into()), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }, + ) + .code_block_renderer( + CodeBlockRenderer::Default { + copy_button: false, + copy_button_on_hover: expanded, + border: false, + }, + ), + ), + ) + }, + ) + .into_any() + } +} + +struct WatchedConnectionMessage { + name: SharedString, + request_id: Option, + direction: acp::StreamMessageDirection, + message_type: MessageType, + params: Result, acp::Error>, + collapsed_params_md: Option>, + expanded_params_md: Option>, +} + +impl WatchedConnectionMessage { + fn expanded(&mut self, language_registry: Arc, cx: &mut App) { + let params_md = match &self.params { + Ok(Some(params)) => Some(expanded_params_md(params, &language_registry, cx)), + Err(err) => { + if let Some(err) = &serde_json::to_value(err).log_err() { + Some(expanded_params_md(&err, &language_registry, cx)) + } else { + None + } + } + _ => None, + }; + self.expanded_params_md = params_md; + } +} + +fn collapsed_params_md( + params: &serde_json::Value, + language_registry: &Arc, + cx: &mut App, +) -> Entity { + let params_json = serde_json::to_string(params).unwrap_or_default(); + let mut spaced_out_json = String::with_capacity(params_json.len() + params_json.len() / 4); + + for ch in params_json.chars() { + match ch { + '{' => spaced_out_json.push_str("{ "), + '}' => spaced_out_json.push_str(" }"), + ':' => spaced_out_json.push_str(": "), + ',' => spaced_out_json.push_str(", "), + c => spaced_out_json.push(c), + } + } + + let params_md = format!("```json\n{}\n```", spaced_out_json); + cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx)) +} + +fn expanded_params_md( + params: &serde_json::Value, + language_registry: &Arc, + cx: &mut App, +) -> Entity { + let params_json = serde_json::to_string_pretty(params).unwrap_or_default(); + let params_md = format!("```json\n{}\n```", params_json); + cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx)) +} + +enum MessageType { + Request, + Response, + Notification, +} + +impl Display for MessageType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MessageType::Request => write!(f, "Request"), + MessageType::Response => write!(f, "Response"), + MessageType::Notification => write!(f, "Notification"), + } + } +} + +enum AcpToolsEvent {} + +impl EventEmitter for AcpTools {} + +impl Item for AcpTools { + type Event = AcpToolsEvent; + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString { + format!( + "ACP: {}", + self.watched_connection + .as_ref() + .map_or("Disconnected", |connection| connection.server_name) + ) + .into() + } + + fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { + Some(ui::Icon::new(IconName::Thread)) + } +} + +impl Focusable for AcpTools { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for AcpTools { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .track_focus(&self.focus_handle) + .size_full() + .bg(cx.theme().colors().editor_background) + .child(match self.watched_connection.as_ref() { + Some(connection) => { + if connection.messages.is_empty() { + h_flex() + .size_full() + .justify_center() + .items_center() + .child("No messages recorded yet") + .into_any() + } else { + list( + connection.list_state.clone(), + cx.processor(Self::render_message), + ) + .with_sizing_behavior(gpui::ListSizingBehavior::Auto) + .flex_grow() + .into_any() + } + } + None => h_flex() + .size_full() + .justify_center() + .items_center() + .child("No active connection") + .into_any(), + }) + } +} diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 60dd7964639ef59794b6e8bbe11192d9a33cbe01..8ea4a27f4cc85e0c19401e86a825eda394c6a34a 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -17,6 +17,7 @@ path = "src/agent_servers.rs" doctest = false [dependencies] +acp_tools.workspace = true acp_thread.workspace = true action_log.workspace = true agent-client-protocol.workspace = true diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 29f389547d0ae5daecca85a95cb8b9b63530fc34..1945ad24834f8d175bebf9b49f856403edcb2622 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -1,3 +1,4 @@ +use acp_tools::AcpConnectionRegistry; use action_log::ActionLog; use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; use anyhow::anyhow; @@ -101,6 +102,14 @@ impl AcpConnection { }) .detach(); + let connection = Rc::new(connection); + + cx.update(|cx| { + AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { + registry.set_active_connection(server_name, &connection, cx) + }); + })?; + let response = connection .initialize(acp::InitializeRequest { protocol_version: acp::VERSION, @@ -119,7 +128,7 @@ impl AcpConnection { Ok(Self { auth_methods: response.auth_methods, - connection: connection.into(), + connection, server_name, sessions, prompt_capabilities: response.agent_capabilities.prompt_capabilities, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index c61e23f0a1a4dec7f56c8fe418f2cd0097db07ca..6f4ead9ebb6d39d0cd14a5c6ad1fca07b9c1e83a 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -20,6 +20,7 @@ path = "src/main.rs" [dependencies] activity_indicator.workspace = true +acp_tools.workspace = true agent.workspace = true agent_ui.workspace = true agent_settings.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 8beefd58912a703097cabc0d2260b9c7ad5258d6..b8150a600daeb4d718777aba5c8888c6187583ed 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -566,6 +566,7 @@ pub fn main() { language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); agent_settings::init(cx); agent_servers::init(cx); + acp_tools::init(cx); web_search::init(cx); web_search_providers::init(app_state.client.clone(), cx); snippet_provider::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 638e1dca0e261dcb7d66c7ac2b8df9ed9ac78ff9..1b9657dcc69592aff2a5633b674bd40cf31be468 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4434,6 +4434,7 @@ mod tests { assert_eq!(actions_without_namespace, Vec::<&str>::new()); let expected_namespaces = vec![ + "acp", "activity_indicator", "agent", #[cfg(not(target_os = "macos"))] diff --git a/script/squawk b/script/squawk index 8489206f14562c957575d695436ec08434c4dad4..497fcff0899a398ac075812a4c299d4a8ec15c70 100755 --- a/script/squawk +++ b/script/squawk @@ -15,13 +15,11 @@ SQUAWK_VERSION=0.26.0 SQUAWK_BIN="./target/squawk-$SQUAWK_VERSION" SQUAWK_ARGS="--assume-in-transaction --config script/lib/squawk.toml" -if [ ! -f "$SQUAWK_BIN" ]; then - pkgutil --pkg-info com.apple.pkg.RosettaUpdateAuto || /usr/sbin/softwareupdate --install-rosetta --agree-to-license - # When bootstrapping a brand new CI machine, the `target` directory may not exist yet. - mkdir -p "./target" - curl -L -o "$SQUAWK_BIN" "https://github.com/sbdchd/squawk/releases/download/v$SQUAWK_VERSION/squawk-darwin-x86_64" - chmod +x "$SQUAWK_BIN" -fi +pkgutil --pkg-info com.apple.pkg.RosettaUpdateAuto || /usr/sbin/softwareupdate --install-rosetta --agree-to-license +# When bootstrapping a brand new CI machine, the `target` directory may not exist yet. +mkdir -p "./target" +curl -L -o "$SQUAWK_BIN" "https://github.com/sbdchd/squawk/releases/download/v$SQUAWK_VERSION/squawk-darwin-x86_64" +chmod +x "$SQUAWK_BIN" if [ -n "$SQUAWK_GITHUB_TOKEN" ]; then export SQUAWK_GITHUB_REPO_OWNER=$(echo $GITHUB_REPOSITORY | awk -F/ '{print $1}') diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 054e757056e31cacec66b46618de2e431706833f..bf44fc195e0cdcd8a9440306fb2b1053db8a596a 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -54,6 +54,7 @@ digest = { version = "0.10", features = ["mac", "oid", "std"] } either = { version = "1", features = ["serde", "use_std"] } euclid = { version = "0.22" } event-listener = { version = "5" } +event-listener-strategy = { version = "0.5" } flate2 = { version = "1", features = ["zlib-rs"] } form_urlencoded = { version = "1" } futures = { version = "0.3", features = ["io-compat"] } @@ -183,6 +184,7 @@ digest = { version = "0.10", features = ["mac", "oid", "std"] } either = { version = "1", features = ["serde", "use_std"] } euclid = { version = "0.22" } event-listener = { version = "5" } +event-listener-strategy = { version = "0.5" } flate2 = { version = "1", features = ["zlib-rs"] } form_urlencoded = { version = "1" } futures = { version = "0.3", features = ["io-compat"] } @@ -403,7 +405,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -444,7 +445,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -483,7 +483,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -524,7 +523,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -610,7 +608,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -651,7 +648,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } From 4560d1ec58af7bbd4eed1eae55fca0854c455fc8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 22 Aug 2025 23:09:37 +0300 Subject: [PATCH 283/744] Use a better message for the InvalidBufferView (#36770) Follow-up of https://github.com/zed-industries/zed/pull/36764 Release Notes: - N/A --- crates/workspace/src/invalid_buffer_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/workspace/src/invalid_buffer_view.rs b/crates/workspace/src/invalid_buffer_view.rs index e2361d59671e8aa6192a9a333c8b679fb76f0e2d..b017373474c041929c09c7bdaec70ec2f1b5b6fb 100644 --- a/crates/workspace/src/invalid_buffer_view.rs +++ b/crates/workspace/src/invalid_buffer_view.rs @@ -88,7 +88,7 @@ impl Render for InvalidBufferView { v_flex() .justify_center() .gap_2() - .child("Cannot display the file contents in Zed") + .child(h_flex().justify_center().child("Unsupported file type")) .when(self.is_local, |contents| { contents.child( h_flex().justify_center().child( From 896a35f7befce468427a30489adf88c851b9507d Mon Sep 17 00:00:00 2001 From: Jonathan Andersson Date: Fri, 22 Aug 2025 22:16:43 +0200 Subject: [PATCH 284/744] Capture `shorthand_field_initializer` and modules in Rust highlights (#35842) Currently shorthand field initializers are not captured the same way as the full initializers, leading to awkward and mismatching highlighting. This PR addresses this fact, in addition to capturing new highlights: - Tags the `!` as part of a macro invocation. - Tags the identifier part of a lifetime as `@lifetime`. - Tag module definitions as a new capture group, `@module`. - Shorthand initializers are now properly tagged as `@property`. Here's what the current version of Zed looks like: image With the new highlighting applied: image Release Notes: - Improved highlighting of Rust files, including new highlight groups for modules and shorthand initializers. --- crates/languages/src/rust/highlights.scm | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/languages/src/rust/highlights.scm b/crates/languages/src/rust/highlights.scm index 1c46061827cd504df669aadacd0a489172d1ce5a..9c02fbedaa6bc9013fe889daae156cc130eda4f3 100644 --- a/crates/languages/src/rust/highlights.scm +++ b/crates/languages/src/rust/highlights.scm @@ -6,6 +6,9 @@ (self) @variable.special (field_identifier) @property +(shorthand_field_initializer + (identifier) @property) + (trait_item name: (type_identifier) @type.interface) (impl_item trait: (type_identifier) @type.interface) (abstract_type trait: (type_identifier) @type.interface) @@ -38,11 +41,20 @@ (identifier) @function.special (scoped_identifier name: (identifier) @function.special) - ]) + ] + "!" @function.special) (macro_definition name: (identifier) @function.special.definition) +(mod_item + name: (identifier) @module) + +(visibility_modifier [ + (crate) @keyword + (super) @keyword +]) + ; Identifier conventions ; Assume uppercase names are types/enum-constructors @@ -115,9 +127,7 @@ "where" "while" "yield" - (crate) (mutable_specifier) - (super) ] @keyword [ @@ -189,6 +199,7 @@ operator: "/" @operator (lifetime) @lifetime +(lifetime (identifier) @lifetime) (parameter (identifier) @variable.parameter) From 639417c2bc2dec345b79024f243ce15bd60638a9 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:40:52 -0300 Subject: [PATCH 285/744] thread_view: Adjust empty state and error displays (#36774) Also changes the message editor placeholder depending on the agent. Release Notes: - N/A --------- Co-authored-by: Conrad Irwin --- crates/agent2/src/native_agent_server.rs | 4 +- crates/agent_ui/src/acp/thread_view.rs | 456 +++++++++++------------ crates/agent_ui/src/agent_panel.rs | 2 +- crates/ui/src/components/callout.rs | 1 + 4 files changed, 225 insertions(+), 238 deletions(-) diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index ac5aa95c043e5128ba11f4f1f8930950e836474c..4ce467d6fdec953397a38ed2c5133d89a2e6adc1 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -23,11 +23,11 @@ impl NativeAgentServer { impl AgentServer for NativeAgentServer { fn name(&self) -> &'static str { - "Native Agent" + "Zed Agent" } fn empty_state_headline(&self) -> &'static str { - "Welcome to the Agent Panel" + self.name() } fn empty_state_message(&self) -> &'static str { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index d27dee1fe6af47e358120d969509714b7d737d04..2a83a4ab5bbfd63ec1e4c994086f81a45ffa9629 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -258,6 +258,7 @@ pub struct AcpThreadView { hovered_recent_history_item: Option, entry_view_state: Entity, message_editor: Entity, + focus_handle: FocusHandle, model_selector: Option>, profile_selector: Option>, notifications: Vec>, @@ -312,6 +313,13 @@ impl AcpThreadView { ) -> Self { let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let prevent_slash_commands = agent.clone().downcast::().is_some(); + + let placeholder = if agent.name() == "Zed Agent" { + format!("Message the {} — @ to include context", agent.name()) + } else { + format!("Message {} — @ to include context", agent.name()) + }; + let message_editor = cx.new(|cx| { let mut editor = MessageEditor::new( workspace.clone(), @@ -319,7 +327,7 @@ impl AcpThreadView { history_store.clone(), prompt_store.clone(), prompt_capabilities.clone(), - "Message the agent — @ to include context", + placeholder, prevent_slash_commands, editor::EditorMode::AutoHeight { min_lines: MIN_EDITOR_LINES, @@ -381,6 +389,7 @@ impl AcpThreadView { prompt_capabilities, _subscriptions: subscriptions, _cancel_task: None, + focus_handle: cx.focus_handle(), } } @@ -404,8 +413,12 @@ impl AcpThreadView { let connection = match connect_task.await { Ok(connection) => connection, Err(err) => { - this.update(cx, |this, cx| { - this.handle_load_error(err, cx); + this.update_in(cx, |this, window, cx| { + if err.downcast_ref::().is_some() { + this.handle_load_error(err, window, cx); + } else { + this.handle_thread_error(err, cx); + } cx.notify(); }) .log_err(); @@ -522,6 +535,7 @@ impl AcpThreadView { title_editor, _subscriptions: subscriptions, }; + this.message_editor.focus_handle(cx).focus(window); this.profile_selector = this.as_native_thread(cx).map(|thread| { cx.new(|cx| { @@ -537,7 +551,7 @@ impl AcpThreadView { cx.notify(); } Err(err) => { - this.handle_load_error(err, cx); + this.handle_load_error(err, window, cx); } }; }) @@ -606,17 +620,28 @@ impl AcpThreadView { .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))), _subscription: subscription, }; + if this.message_editor.focus_handle(cx).is_focused(window) { + this.focus_handle.focus(window) + } cx.notify(); }) .ok(); } - fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context) { + fn handle_load_error( + &mut self, + err: anyhow::Error, + window: &mut Window, + cx: &mut Context, + ) { if let Some(load_err) = err.downcast_ref::() { self.thread_state = ThreadState::LoadError(load_err.clone()); } else { self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into())) } + if self.message_editor.focus_handle(cx).is_focused(window) { + self.focus_handle.focus(window) + } cx.notify(); } @@ -633,12 +658,11 @@ impl AcpThreadView { } } - pub fn title(&self, cx: &App) -> SharedString { + pub fn title(&self) -> SharedString { match &self.thread_state { - ThreadState::Ready { thread, .. } => thread.read(cx).title(), + ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(), ThreadState::Loading { .. } => "Loading…".into(), ThreadState::LoadError(_) => "Failed to load".into(), - ThreadState::Unauthenticated { .. } => "Authentication Required".into(), } } @@ -1069,6 +1093,9 @@ impl AcpThreadView { AcpThreadEvent::LoadError(error) => { self.thread_retry_status.take(); self.thread_state = ThreadState::LoadError(error.clone()); + if self.message_editor.focus_handle(cx).is_focused(window) { + self.focus_handle.focus(window) + } } AcpThreadEvent::TitleUpdated => { let title = thread.read(cx).title(); @@ -2338,33 +2365,6 @@ impl AcpThreadView { .into_any() } - fn render_agent_logo(&self) -> AnyElement { - Icon::new(self.agent.logo()) - .color(Color::Muted) - .size(IconSize::XLarge) - .into_any_element() - } - - fn render_error_agent_logo(&self) -> AnyElement { - let logo = Icon::new(self.agent.logo()) - .color(Color::Muted) - .size(IconSize::XLarge) - .into_any_element(); - - h_flex() - .relative() - .justify_center() - .child(div().opacity(0.3).child(logo)) - .child( - h_flex() - .absolute() - .right_1() - .bottom_0() - .child(Icon::new(IconName::XCircleFilled).color(Color::Error)), - ) - .into_any_element() - } - fn render_rules_item(&self, cx: &Context) -> Option { let project_context = self .as_native_thread(cx)? @@ -2493,8 +2493,7 @@ impl AcpThreadView { ) } - fn render_empty_state(&self, window: &mut Window, cx: &mut Context) -> AnyElement { - let loading = matches!(&self.thread_state, ThreadState::Loading { .. }); + fn render_recent_history(&self, window: &mut Window, cx: &mut Context) -> AnyElement { let render_history = self .agent .clone() @@ -2506,38 +2505,6 @@ impl AcpThreadView { v_flex() .size_full() - .when(!render_history, |this| { - this.child( - v_flex() - .size_full() - .items_center() - .justify_center() - .child(if loading { - h_flex() - .justify_center() - .child(self.render_agent_logo()) - .with_animation( - "pulsating_icon", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.4, 1.0)), - |icon, delta| icon.opacity(delta), - ) - .into_any() - } else { - self.render_agent_logo().into_any_element() - }) - .child(h_flex().mt_4().mb_2().justify_center().child(if loading { - div() - .child(LoadingLabel::new("").size(LabelSize::Large)) - .into_any_element() - } else { - Headline::new(self.agent.empty_state_headline()) - .size(HeadlineSize::Medium) - .into_any_element() - })), - ) - }) .when(render_history, |this| { let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| { history_store.entries().take(3).collect() @@ -2612,142 +2579,118 @@ impl AcpThreadView { window: &mut Window, cx: &Context, ) -> Div { - v_flex() - .p_2() - .gap_2() - .flex_1() - .items_center() - .justify_center() - .child( - v_flex() - .items_center() - .justify_center() - .child(self.render_error_agent_logo()) - .child( - h_flex().mt_4().mb_1().justify_center().child( - Headline::new("Authentication Required").size(HeadlineSize::Medium), + v_flex().flex_1().size_full().justify_end().child( + v_flex() + .p_2() + .pr_3() + .w_full() + .border_t_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().status().warning.opacity(0.04)) + .child( + h_flex() + .gap_1p5() + .child( + Icon::new(IconName::Warning) + .color(Color::Warning) + .size(IconSize::Small), + ) + .child(Label::new("Authentication Required")), + ) + .children(description.map(|desc| { + div().text_ui(cx).child(self.render_markdown( + desc.clone(), + default_markdown_style(false, false, window, cx), + )) + })) + .children( + configuration_view + .cloned() + .map(|view| div().w_full().child(view)), + ) + .when( + configuration_view.is_none() + && description.is_none() + && pending_auth_method.is_none(), + |el| { + el.child( + Label::new(format!( + "You are not currently authenticated with {}. Please choose one of the following options:", + self.agent.name() + )) + .color(Color::Muted) + .mb_1() + .ml_5(), + ) + }, + ) + .when(!connection.auth_methods().is_empty(), |this| { + this.child( + h_flex().justify_end().flex_wrap().gap_1().children( + connection.auth_methods().iter().enumerate().rev().map( + |(ix, method)| { + Button::new( + SharedString::from(method.id.0.clone()), + method.name.clone(), + ) + .when(ix == 0, |el| { + el.style(ButtonStyle::Tinted(ui::TintColor::Warning)) + }) + .on_click({ + let method_id = method.id.clone(); + cx.listener(move |this, _, window, cx| { + this.authenticate(method_id.clone(), window, cx) + }) + }) + }, + ), ), ) - .into_any(), - ) - .children(description.map(|desc| { - div().text_ui(cx).text_center().child(self.render_markdown( - desc.clone(), - default_markdown_style(false, false, window, cx), - )) - })) - .children( - configuration_view - .cloned() - .map(|view| div().px_4().w_full().max_w_128().child(view)), - ) - .when( - configuration_view.is_none() - && description.is_none() - && pending_auth_method.is_none(), - |el| { + }) + .when_some(pending_auth_method, |el, _| { el.child( - div() - .text_ui(cx) - .text_center() - .px_4() + h_flex() + .py_4() .w_full() - .max_w_128() - .child(Label::new("Authentication required")), - ) - }, - ) - .when_some(pending_auth_method, |el, _| { - let spinner_icon = div() - .px_0p5() - .id("generating") - .tooltip(Tooltip::text("Generating Changes…")) - .child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, + .justify_center() + .gap_1() + .child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Small) + .color(Color::Muted) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage( + delta, + ))) + }, + ) + .into_any_element(), ) - .into_any_element(), + .child(Label::new("Authenticating…")), ) - .into_any(); - el.child( - h_flex() - .text_ui(cx) - .text_center() - .justify_center() - .gap_2() - .px_4() - .w_full() - .max_w_128() - .child(Label::new("Authenticating...")) - .child(spinner_icon), - ) - }) - .child( - h_flex() - .mt_1p5() - .gap_1() - .flex_wrap() - .justify_center() - .children(connection.auth_methods().iter().enumerate().rev().map( - |(ix, method)| { - Button::new( - SharedString::from(method.id.0.clone()), - method.name.clone(), - ) - .style(ButtonStyle::Outlined) - .when(ix == 0, |el| { - el.style(ButtonStyle::Tinted(ui::TintColor::Accent)) - }) - .size(ButtonSize::Medium) - .label_size(LabelSize::Small) - .on_click({ - let method_id = method.id.clone(); - cx.listener(move |this, _, window, cx| { - this.authenticate(method_id.clone(), window, cx) - }) - }) - }, - )), - ) + }), + ) } fn render_load_error(&self, e: &LoadError, cx: &Context) -> AnyElement { - let mut container = v_flex() - .items_center() - .justify_center() - .child(self.render_error_agent_logo()) - .child( - v_flex() - .mt_4() - .mb_2() - .gap_0p5() - .text_center() - .items_center() - .child(Headline::new("Failed to launch").size(HeadlineSize::Medium)) - .child( - Label::new(e.to_string()) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ); - - if let LoadError::Unsupported { - upgrade_message, - upgrade_command, - .. - } = &e - { - let upgrade_message = upgrade_message.clone(); - let upgrade_command = upgrade_command.clone(); - container = container.child( - Button::new("upgrade", upgrade_message) - .tooltip(Tooltip::text(upgrade_command.clone())) + let (message, action_slot) = match e { + LoadError::NotInstalled { + error_message, + install_message, + install_command, + } => { + let install_command = install_command.clone(); + let button = Button::new("install", install_message) + .tooltip(Tooltip::text(install_command.clone())) + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .icon(IconName::Download) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) .on_click(cx.listener(move |this, _, window, cx| { let task = this .workspace @@ -2756,12 +2699,12 @@ impl AcpThreadView { let cwd = project.first_project_directory(cx); let shell = project.terminal_settings(&cwd, cx).shell.clone(); let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId("upgrade".to_string()), - full_label: upgrade_command.clone(), - label: upgrade_command.clone(), - command: Some(upgrade_command.clone()), + id: task::TaskId("install".to_string()), + full_label: install_command.clone(), + label: install_command.clone(), + command: Some(install_command.clone()), args: Vec::new(), - command_label: upgrade_command.clone(), + command_label: install_command.clone(), cwd, env: Default::default(), use_new_terminal: true, @@ -2787,21 +2730,24 @@ impl AcpThreadView { } }) .detach() - })), - ); - } else if let LoadError::NotInstalled { - install_message, - install_command, - .. - } = e - { - let install_message = install_message.clone(); - let install_command = install_command.clone(); - container = container.child( - Button::new("install", install_message) - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .size(ButtonSize::Medium) - .tooltip(Tooltip::text(install_command.clone())) + })); + + (error_message.clone(), Some(button.into_any_element())) + } + LoadError::Unsupported { + error_message, + upgrade_message, + upgrade_command, + } => { + let upgrade_command = upgrade_command.clone(); + let button = Button::new("upgrade", upgrade_message) + .tooltip(Tooltip::text(upgrade_command.clone())) + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .icon(IconName::Download) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) .on_click(cx.listener(move |this, _, window, cx| { let task = this .workspace @@ -2810,12 +2756,12 @@ impl AcpThreadView { let cwd = project.first_project_directory(cx); let shell = project.terminal_settings(&cwd, cx).shell.clone(); let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId("install".to_string()), - full_label: install_command.clone(), - label: install_command.clone(), - command: Some(install_command.clone()), + id: task::TaskId("upgrade".to_string()), + full_label: upgrade_command.clone(), + label: upgrade_command.clone(), + command: Some(upgrade_command.clone()), args: Vec::new(), - command_label: install_command.clone(), + command_label: upgrade_command.clone(), cwd, env: Default::default(), use_new_terminal: true, @@ -2841,11 +2787,24 @@ impl AcpThreadView { } }) .detach() - })), - ); - } + })); + + (error_message.clone(), Some(button.into_any_element())) + } + LoadError::Exited { .. } => ("Server exited with status {status}".into(), None), + LoadError::Other(msg) => ( + msg.into(), + Some(self.create_copy_button(msg.to_string()).into_any_element()), + ), + }; - container.into_any() + Callout::new() + .severity(Severity::Error) + .icon(IconName::XCircleFilled) + .title("Failed to Launch") + .description(message) + .actions_slot(div().children(action_slot)) + .into_any_element() } fn render_activity_bar( @@ -3336,6 +3295,19 @@ impl AcpThreadView { (IconName::Maximize, "Expand Message Editor") }; + let backdrop = div() + .size_full() + .absolute() + .inset_0() + .bg(cx.theme().colors().panel_background) + .opacity(0.8) + .block_mouse_except_scroll(); + + let enable_editor = match self.thread_state { + ThreadState::Loading { .. } | ThreadState::Ready { .. } => true, + ThreadState::Unauthenticated { .. } | ThreadState::LoadError(..) => false, + }; + v_flex() .on_action(cx.listener(Self::expand_message_editor)) .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| { @@ -3411,6 +3383,7 @@ impl AcpThreadView { .child(self.render_send_button(cx)), ), ) + .when(!enable_editor, |this| this.child(backdrop)) .into_any() } @@ -3913,18 +3886,19 @@ impl AcpThreadView { return; } - let title = self.title(cx); + // TODO: Change this once we have title summarization for external agents. + let title = self.agent.name(); match AgentSettings::get_global(cx).notify_when_agent_waiting { NotifyWhenAgentWaiting::PrimaryScreen => { if let Some(primary) = cx.primary_display() { - self.pop_up(icon, caption.into(), title, window, primary, cx); + self.pop_up(icon, caption.into(), title.into(), window, primary, cx); } } NotifyWhenAgentWaiting::AllScreens => { let caption = caption.into(); for screen in cx.displays() { - self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx); + self.pop_up(icon, caption.clone(), title.into(), window, screen, cx); } } NotifyWhenAgentWaiting::Never => { @@ -4423,6 +4397,7 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) .title("Error") + .icon(IconName::XCircle) .description(error.clone()) .actions_slot(self.create_copy_button(error.to_string())) .dismiss_action(self.dismiss_error_button(cx)) @@ -4434,6 +4409,7 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) + .icon(IconName::XCircle) .title("Free Usage Exceeded") .description(ERROR_MESSAGE) .actions_slot( @@ -4453,6 +4429,7 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) .title("Authentication Required") + .icon(IconName::XCircle) .description(error.clone()) .actions_slot( h_flex() @@ -4478,6 +4455,7 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) .title("Model Prompt Limit Reached") + .icon(IconName::XCircle) .description(error_message) .actions_slot( h_flex() @@ -4648,7 +4626,14 @@ impl AcpThreadView { impl Focusable for AcpThreadView { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.message_editor.focus_handle(cx) + match self.thread_state { + ThreadState::Loading { .. } | ThreadState::Ready { .. } => { + self.message_editor.focus_handle(cx) + } + ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => { + self.focus_handle.clone() + } + } } } @@ -4664,6 +4649,7 @@ impl Render for AcpThreadView { .on_action(cx.listener(Self::toggle_burn_mode)) .on_action(cx.listener(Self::keep_all)) .on_action(cx.listener(Self::reject_all)) + .track_focus(&self.focus_handle) .bg(cx.theme().colors().panel_background) .child(match &self.thread_state { ThreadState::Unauthenticated { @@ -4680,14 +4666,14 @@ impl Render for AcpThreadView { window, cx, ), - ThreadState::Loading { .. } => { - v_flex().flex_1().child(self.render_empty_state(window, cx)) - } + ThreadState::Loading { .. } => v_flex() + .flex_1() + .child(self.render_recent_history(window, cx)), ThreadState::LoadError(e) => v_flex() - .p_2() .flex_1() + .size_full() .items_center() - .justify_center() + .justify_end() .child(self.render_load_error(e, cx)), ThreadState::Ready { thread, .. } => { let thread_clone = thread.clone(); @@ -4724,7 +4710,7 @@ impl Render for AcpThreadView { }, ) } else { - this.child(self.render_empty_state(window, cx)) + this.child(self.render_recent_history(window, cx)) } }) } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index d0fb676fd2970affdbde701e6eec47e6ddeba810..0e611d0db95a0dcb319be79f82086d762e9c2fb1 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2097,7 +2097,7 @@ impl AgentPanel { .child(title_editor) .into_any_element() } else { - Label::new(thread_view.read(cx).title(cx)) + Label::new(thread_view.read(cx).title()) .color(Color::Muted) .truncate() .into_any_element() diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs index 7ffeda881c9ed5c0e9c14c100909db4a91693dec..b1ead18ee71fbae90bf4d2d633a980f8eb9f3638 100644 --- a/crates/ui/src/components/callout.rs +++ b/crates/ui/src/components/callout.rs @@ -132,6 +132,7 @@ impl RenderOnce for Callout { h_flex() .min_w_0() + .w_full() .p_2() .gap_2() .items_start() From f649c31bf94ac56757aff1394c5a0926232285af Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Aug 2025 14:10:45 -0700 Subject: [PATCH 286/744] Restructure persistence of remote workspaces to make room for WSL and other non-ssh remote projects (#36714) This is another pure refactor, to prepare for adding direct WSL support. ### Todo * [x] Represent `paths` in the same way for all workspaces, instead of having a completely separate SSH representation * [x] Adjust sqlite tables * [x] `ssh_projects` -> `ssh_connections` (drop paths) * [x] `workspaces.local_paths` -> `paths` * [x] remove duplicate path columns on `workspaces` * [x] Add migrations for backward-compatibility Release Notes: - N/A --------- Co-authored-by: Mikayla Maki --- Cargo.lock | 2 +- crates/client/src/user.rs | 5 - .../src/disconnected_overlay.rs | 14 +- crates/recent_projects/src/recent_projects.rs | 62 +- crates/workspace/Cargo.toml | 2 +- crates/workspace/src/history_manager.rs | 18 +- crates/workspace/src/path_list.rs | 121 ++ crates/workspace/src/persistence.rs | 1117 ++++++++--------- crates/workspace/src/persistence/model.rs | 305 +---- crates/workspace/src/workspace.rs | 182 +-- crates/zed/src/main.rs | 15 +- crates/zed/src/zed/open_listener.rs | 17 +- 12 files changed, 782 insertions(+), 1078 deletions(-) create mode 100644 crates/workspace/src/path_list.rs diff --git a/Cargo.lock b/Cargo.lock index cd1018d4c9577cf0da7600b95721b60bd6611431..4043666823790cfc8f9e495fc15975992c0fac3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19829,7 +19829,6 @@ dependencies = [ "any_vec", "anyhow", "async-recursion", - "bincode", "call", "client", "clock", @@ -19848,6 +19847,7 @@ dependencies = [ "node_runtime", "parking_lot", "postage", + "pretty_assertions", "project", "remote", "schemars", diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 1f8174dbc3c2e0bf428eb70abbc3161608589166..d23eb37519c00c2567683e5417aa2d82f10a2f58 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -46,11 +46,6 @@ impl ProjectId { } } -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, -)] -pub struct DevServerProjectId(pub u64); - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ParticipantIndex(pub u32); diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index dd4d788cfdb4f8ee60efdc7568ba16697822a58c..8ffe0ef07cf2e0c635794383ae08203c87a33f44 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, WeakEntity}; use project::project_settings::ProjectSettings; use remote::SshConnectionOptions; @@ -103,17 +101,17 @@ impl DisconnectedOverlay { return; }; - let Some(ssh_project) = workspace.read(cx).serialized_ssh_project() else { - return; - }; - let Some(window_handle) = window.window_handle().downcast::() else { return; }; let app_state = workspace.read(cx).app_state().clone(); - - let paths = ssh_project.paths.iter().map(PathBuf::from).collect(); + let paths = workspace + .read(cx) + .root_paths(cx) + .iter() + .map(|path| path.to_path_buf()) + .collect(); cx.spawn_in(window, async move |_, cx| { open_ssh_project( diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 2093e96caeed5bebb2ec2e833efa61574c4be0a1..fa57b588cd8457788adc0226264a4871c3305b85 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -19,15 +19,12 @@ use picker::{ pub use remote_servers::RemoteServerProjects; use settings::Settings; pub use ssh_connections::SshSettings; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; +use std::{path::Path, sync::Arc}; use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container}; use util::{ResultExt, paths::PathExt}; use workspace::{ - CloseIntent, HistoryManager, ModalView, OpenOptions, SerializedWorkspaceLocation, WORKSPACE_DB, - Workspace, WorkspaceId, with_active_or_new_workspace, + CloseIntent, HistoryManager, ModalView, OpenOptions, PathList, SerializedWorkspaceLocation, + WORKSPACE_DB, Workspace, WorkspaceId, with_active_or_new_workspace, }; use zed_actions::{OpenRecent, OpenRemote}; @@ -154,7 +151,7 @@ impl Render for RecentProjects { pub struct RecentProjectsDelegate { workspace: WeakEntity, - workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>, + workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>, selected_match_index: usize, matches: Vec, render_paths: bool, @@ -178,12 +175,15 @@ impl RecentProjectsDelegate { } } - pub fn set_workspaces(&mut self, workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>) { + pub fn set_workspaces( + &mut self, + workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>, + ) { self.workspaces = workspaces; self.has_any_non_local_projects = !self .workspaces .iter() - .all(|(_, location)| matches!(location, SerializedWorkspaceLocation::Local(_, _))); + .all(|(_, location, _)| matches!(location, SerializedWorkspaceLocation::Local)); } } impl EventEmitter for RecentProjectsDelegate {} @@ -236,15 +236,14 @@ impl PickerDelegate for RecentProjectsDelegate { .workspaces .iter() .enumerate() - .filter(|(_, (id, _))| !self.is_current_workspace(*id, cx)) - .map(|(id, (_, location))| { - let combined_string = location - .sorted_paths() + .filter(|(_, (id, _, _))| !self.is_current_workspace(*id, cx)) + .map(|(id, (_, _, paths))| { + let combined_string = paths + .paths() .iter() .map(|path| path.compact().to_string_lossy().into_owned()) .collect::>() .join(""); - StringMatchCandidate::new(id, &combined_string) }) .collect::>(); @@ -279,7 +278,7 @@ impl PickerDelegate for RecentProjectsDelegate { .get(self.selected_index()) .zip(self.workspace.upgrade()) { - let (candidate_workspace_id, candidate_workspace_location) = + let (candidate_workspace_id, candidate_workspace_location, candidate_workspace_paths) = &self.workspaces[selected_match.candidate_id]; let replace_current_window = if self.create_new_window { secondary @@ -292,8 +291,8 @@ impl PickerDelegate for RecentProjectsDelegate { Task::ready(Ok(())) } else { match candidate_workspace_location { - SerializedWorkspaceLocation::Local(paths, _) => { - let paths = paths.paths().to_vec(); + SerializedWorkspaceLocation::Local => { + let paths = candidate_workspace_paths.paths().to_vec(); if replace_current_window { cx.spawn_in(window, async move |workspace, cx| { let continue_replacing = workspace @@ -321,7 +320,7 @@ impl PickerDelegate for RecentProjectsDelegate { workspace.open_workspace_for_paths(false, paths, window, cx) } } - SerializedWorkspaceLocation::Ssh(ssh_project) => { + SerializedWorkspaceLocation::Ssh(connection) => { let app_state = workspace.app_state().clone(); let replace_window = if replace_current_window { @@ -337,12 +336,12 @@ impl PickerDelegate for RecentProjectsDelegate { let connection_options = SshSettings::get_global(cx) .connection_options_for( - ssh_project.host.clone(), - ssh_project.port, - ssh_project.user.clone(), + connection.host.clone(), + connection.port, + connection.user.clone(), ); - let paths = ssh_project.paths.iter().map(PathBuf::from).collect(); + let paths = candidate_workspace_paths.paths().to_vec(); cx.spawn_in(window, async move |_, cx| { open_ssh_project( @@ -383,12 +382,12 @@ impl PickerDelegate for RecentProjectsDelegate { ) -> Option { let hit = self.matches.get(ix)?; - let (_, location) = self.workspaces.get(hit.candidate_id)?; + let (_, location, paths) = self.workspaces.get(hit.candidate_id)?; let mut path_start_offset = 0; - let (match_labels, paths): (Vec<_>, Vec<_>) = location - .sorted_paths() + let (match_labels, paths): (Vec<_>, Vec<_>) = paths + .paths() .iter() .map(|p| p.compact()) .map(|path| { @@ -416,11 +415,9 @@ impl PickerDelegate for RecentProjectsDelegate { .gap_3() .when(self.has_any_non_local_projects, |this| { this.child(match location { - SerializedWorkspaceLocation::Local(_, _) => { - Icon::new(IconName::Screen) - .color(Color::Muted) - .into_any_element() - } + SerializedWorkspaceLocation::Local => Icon::new(IconName::Screen) + .color(Color::Muted) + .into_any_element(), SerializedWorkspaceLocation::Ssh(_) => Icon::new(IconName::Server) .color(Color::Muted) .into_any_element(), @@ -568,7 +565,7 @@ impl RecentProjectsDelegate { cx: &mut Context>, ) { if let Some(selected_match) = self.matches.get(ix) { - let (workspace_id, _) = self.workspaces[selected_match.candidate_id]; + let (workspace_id, _, _) = self.workspaces[selected_match.candidate_id]; cx.spawn_in(window, async move |this, cx| { let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await; let workspaces = WORKSPACE_DB @@ -707,7 +704,8 @@ mod tests { }]; delegate.set_workspaces(vec![( WorkspaceId::default(), - SerializedWorkspaceLocation::from_local_paths(vec![path!("/test/path/")]), + SerializedWorkspaceLocation::Local, + PathList::new(&[path!("/test/path")]), )]); }); }) diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 570657ba8f9ee8edb540df6a1bb4c8637cff64ac..869aa5322eba7fdaf417606dd62ae73a0c3702b3 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -29,7 +29,6 @@ test-support = [ any_vec.workspace = true anyhow.workspace = true async-recursion.workspace = true -bincode.workspace = true call.workspace = true client.workspace = true clock.workspace = true @@ -80,5 +79,6 @@ project = { workspace = true, features = ["test-support"] } session = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } +pretty_assertions.workspace = true tempfile.workspace = true zlog.workspace = true diff --git a/crates/workspace/src/history_manager.rs b/crates/workspace/src/history_manager.rs index a8387369f424c17dd667b029b67c9d2e7b73cd3f..f68b58ff8289c68883393e7be5087322ca76d480 100644 --- a/crates/workspace/src/history_manager.rs +++ b/crates/workspace/src/history_manager.rs @@ -5,7 +5,9 @@ use smallvec::SmallVec; use ui::App; use util::{ResultExt, paths::PathExt}; -use crate::{NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId}; +use crate::{ + NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId, path_list::PathList, +}; pub fn init(cx: &mut App) { let manager = cx.new(|_| HistoryManager::new()); @@ -44,7 +46,13 @@ impl HistoryManager { .unwrap_or_default() .into_iter() .rev() - .map(|(id, location)| HistoryManagerEntry::new(id, &location)) + .filter_map(|(id, location, paths)| { + if matches!(location, SerializedWorkspaceLocation::Local) { + Some(HistoryManagerEntry::new(id, &paths)) + } else { + None + } + }) .collect::>(); this.update(cx, |this, cx| { this.history = recent_folders; @@ -118,9 +126,9 @@ impl HistoryManager { } impl HistoryManagerEntry { - pub fn new(id: WorkspaceId, location: &SerializedWorkspaceLocation) -> Self { - let path = location - .sorted_paths() + pub fn new(id: WorkspaceId, paths: &PathList) -> Self { + let path = paths + .paths() .iter() .map(|path| path.compact()) .collect::>(); diff --git a/crates/workspace/src/path_list.rs b/crates/workspace/src/path_list.rs new file mode 100644 index 0000000000000000000000000000000000000000..4f9ed4231289516a5434cc429adc7242731d9a27 --- /dev/null +++ b/crates/workspace/src/path_list.rs @@ -0,0 +1,121 @@ +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use util::paths::SanitizedPath; + +/// A list of absolute paths, in a specific order. +/// +/// The paths are stored in lexicographic order, so that they can be compared to +/// other path lists without regard to the order of the paths. +#[derive(Default, PartialEq, Eq, Debug, Clone)] +pub struct PathList { + paths: Arc<[PathBuf]>, + order: Arc<[usize]>, +} + +#[derive(Debug)] +pub struct SerializedPathList { + pub paths: String, + pub order: String, +} + +impl PathList { + pub fn new>(paths: &[P]) -> Self { + let mut indexed_paths: Vec<(usize, PathBuf)> = paths + .iter() + .enumerate() + .map(|(ix, path)| (ix, SanitizedPath::from(path).into())) + .collect(); + indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b)); + let order = indexed_paths.iter().map(|e| e.0).collect::>().into(); + let paths = indexed_paths + .into_iter() + .map(|e| e.1) + .collect::>() + .into(); + Self { order, paths } + } + + pub fn is_empty(&self) -> bool { + self.paths.is_empty() + } + + pub fn paths(&self) -> &[PathBuf] { + self.paths.as_ref() + } + + pub fn order(&self) -> &[usize] { + self.order.as_ref() + } + + pub fn is_lexicographically_ordered(&self) -> bool { + self.order.iter().enumerate().all(|(i, &j)| i == j) + } + + pub fn deserialize(serialized: &SerializedPathList) -> Self { + let mut paths: Vec = if serialized.paths.is_empty() { + Vec::new() + } else { + serde_json::from_str::>(&serialized.paths) + .unwrap_or(Vec::new()) + .into_iter() + .map(|s| SanitizedPath::from(s).into()) + .collect() + }; + + let mut order: Vec = serialized + .order + .split(',') + .filter_map(|s| s.parse().ok()) + .collect(); + + if !paths.is_sorted() || order.len() != paths.len() { + order = (0..paths.len()).collect(); + paths.sort(); + } + + Self { + paths: paths.into(), + order: order.into(), + } + } + + pub fn serialize(&self) -> SerializedPathList { + use std::fmt::Write as _; + + let paths = serde_json::to_string(&self.paths).unwrap_or_default(); + + let mut order = String::new(); + for ix in self.order.iter() { + if !order.is_empty() { + order.push(','); + } + write!(&mut order, "{}", *ix).unwrap(); + } + SerializedPathList { paths, order } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_path_list() { + let list1 = PathList::new(&["a/d", "a/c"]); + let list2 = PathList::new(&["a/c", "a/d"]); + + assert_eq!(list1.paths(), list2.paths()); + assert_ne!(list1, list2); + assert_eq!(list1.order(), &[1, 0]); + assert_eq!(list2.order(), &[0, 1]); + + let list1_deserialized = PathList::deserialize(&list1.serialize()); + assert_eq!(list1_deserialized, list1); + + let list2_deserialized = PathList::deserialize(&list2.serialize()); + assert_eq!(list2_deserialized, list2); + } +} diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index b2d1340a7b84d7fac4f3a7376044dc373e8200ca..de8f63957c73714569016bc647fb8dc670ddedd2 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -9,10 +9,8 @@ use std::{ }; use anyhow::{Context as _, Result, bail}; -use client::DevServerProjectId; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; -use itertools::Itertools; use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; use language::{LanguageName, Toolchain}; @@ -28,14 +26,17 @@ use ui::{App, px}; use util::{ResultExt, maybe}; use uuid::Uuid; -use crate::WorkspaceId; +use crate::{ + WorkspaceId, + path_list::{PathList, SerializedPathList}, +}; use model::{ - GroupId, ItemId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, - SerializedSshProject, SerializedWorkspace, + GroupId, ItemId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, + SerializedSshConnection, SerializedWorkspace, }; -use self::model::{DockStructure, LocalPathsOrder, SerializedWorkspaceLocation}; +use self::model::{DockStructure, SerializedWorkspaceLocation}; #[derive(Copy, Clone, Debug, PartialEq)] pub(crate) struct SerializedAxis(pub(crate) gpui::Axis); @@ -275,70 +276,9 @@ impl sqlez::bindable::Bind for SerializedPixels { } define_connection! { - // Current schema shape using pseudo-rust syntax: - // - // workspaces( - // workspace_id: usize, // Primary key for workspaces - // local_paths: Bincode>, - // local_paths_order: Bincode>, - // dock_visible: bool, // Deprecated - // dock_anchor: DockAnchor, // Deprecated - // dock_pane: Option, // Deprecated - // left_sidebar_open: boolean, - // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS - // window_state: String, // WindowBounds Discriminant - // window_x: Option, // WindowBounds::Fixed RectF x - // window_y: Option, // WindowBounds::Fixed RectF y - // window_width: Option, // WindowBounds::Fixed RectF width - // window_height: Option, // WindowBounds::Fixed RectF height - // display: Option, // Display id - // fullscreen: Option, // Is the window fullscreen? - // centered_layout: Option, // Is the Centered Layout mode activated? - // session_id: Option, // Session id - // window_id: Option, // Window Id - // ) - // - // pane_groups( - // group_id: usize, // Primary key for pane_groups - // workspace_id: usize, // References workspaces table - // parent_group_id: Option, // None indicates that this is the root node - // position: Option, // None indicates that this is the root node - // axis: Option, // 'Vertical', 'Horizontal' - // flexes: Option>, // A JSON array of floats - // ) - // - // panes( - // pane_id: usize, // Primary key for panes - // workspace_id: usize, // References workspaces table - // active: bool, - // ) - // - // center_panes( - // pane_id: usize, // Primary key for center_panes - // parent_group_id: Option, // References pane_groups. If none, this is the root - // position: Option, // None indicates this is the root - // ) - // - // CREATE TABLE items( - // item_id: usize, // This is the item's view id, so this is not unique - // workspace_id: usize, // References workspaces table - // pane_id: usize, // References panes table - // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global - // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column - // active: bool, // Indicates if this item is the active one in the pane - // preview: bool // Indicates if this item is a preview item - // ) - // - // CREATE TABLE breakpoints( - // workspace_id: usize Foreign Key, // References workspace table - // path: PathBuf, // The absolute path of the file that this breakpoint belongs to - // breakpoint_location: Vec, // A list of the locations of breakpoints - // kind: int, // The kind of breakpoint (standard, log) - // log_message: String, // log message for log breakpoints, otherwise it's Null - // ) pub static ref DB: WorkspaceDb<()> = &[ - sql!( + sql!( CREATE TABLE workspaces( workspace_id INTEGER PRIMARY KEY, workspace_location BLOB UNIQUE, @@ -555,7 +495,109 @@ define_connection! { SELECT * FROM toolchains; DROP TABLE toolchains; ALTER TABLE toolchains2 RENAME TO toolchains; - ) + ), + sql!( + CREATE TABLE ssh_connections ( + id INTEGER PRIMARY KEY, + host TEXT NOT NULL, + port INTEGER, + user TEXT + ); + + INSERT INTO ssh_connections (host, port, user) + SELECT DISTINCT host, port, user + FROM ssh_projects; + + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + paths TEXT, + paths_order TEXT, + ssh_connection_id INTEGER REFERENCES ssh_connections(id), + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB, + left_dock_visible INTEGER, + left_dock_active_panel TEXT, + right_dock_visible INTEGER, + right_dock_active_panel TEXT, + bottom_dock_visible INTEGER, + bottom_dock_active_panel TEXT, + left_dock_zoom INTEGER, + right_dock_zoom INTEGER, + bottom_dock_zoom INTEGER, + fullscreen INTEGER, + centered_layout INTEGER, + session_id TEXT, + window_id INTEGER + ) STRICT; + + INSERT + INTO workspaces_2 + SELECT + workspaces.workspace_id, + CASE + WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths + ELSE + CASE + WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN + NULL + ELSE + json('[' || '"' || replace(workspaces.local_paths_array, ',', '"' || "," || '"') || '"' || ']') + END + END as paths, + + CASE + WHEN ssh_projects.id IS NOT NULL THEN "" + ELSE workspaces.local_paths_order_array + END as paths_order, + + CASE + WHEN ssh_projects.id IS NOT NULL THEN ( + SELECT ssh_connections.id + FROM ssh_connections + WHERE + ssh_connections.host IS ssh_projects.host AND + ssh_connections.port IS ssh_projects.port AND + ssh_connections.user IS ssh_projects.user + ) + ELSE NULL + END as ssh_connection_id, + + workspaces.timestamp, + workspaces.window_state, + workspaces.window_x, + workspaces.window_y, + workspaces.window_width, + workspaces.window_height, + workspaces.display, + workspaces.left_dock_visible, + workspaces.left_dock_active_panel, + workspaces.right_dock_visible, + workspaces.right_dock_active_panel, + workspaces.bottom_dock_visible, + workspaces.bottom_dock_active_panel, + workspaces.left_dock_zoom, + workspaces.right_dock_zoom, + workspaces.bottom_dock_zoom, + workspaces.fullscreen, + workspaces.centered_layout, + workspaces.session_id, + workspaces.window_id + FROM + workspaces LEFT JOIN + ssh_projects ON + workspaces.ssh_project_id = ssh_projects.id; + + DROP TABLE ssh_projects; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + + CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths); + ), ]; } @@ -566,17 +608,33 @@ impl WorkspaceDb { pub(crate) fn workspace_for_roots>( &self, worktree_roots: &[P], + ) -> Option { + self.workspace_for_roots_internal(worktree_roots, None) + } + + pub(crate) fn ssh_workspace_for_roots>( + &self, + worktree_roots: &[P], + ssh_project_id: SshProjectId, + ) -> Option { + self.workspace_for_roots_internal(worktree_roots, Some(ssh_project_id)) + } + + pub(crate) fn workspace_for_roots_internal>( + &self, + worktree_roots: &[P], + ssh_connection_id: Option, ) -> Option { // paths are sorted before db interactions to ensure that the order of the paths // doesn't affect the workspace selection for existing workspaces - let local_paths = LocalPaths::new(worktree_roots); + let root_paths = PathList::new(worktree_roots); // Note that we re-assign the workspace_id here in case it's empty // and we've grabbed the most recent workspace let ( workspace_id, - local_paths, - local_paths_order, + paths, + paths_order, window_bounds, display, centered_layout, @@ -584,8 +642,8 @@ impl WorkspaceDb { window_id, ): ( WorkspaceId, - Option, - Option, + String, + String, Option, Option, Option, @@ -595,8 +653,8 @@ impl WorkspaceDb { .select_row_bound(sql! { SELECT workspace_id, - local_paths, - local_paths_order, + paths, + paths_order, window_state, window_x, window_y, @@ -615,25 +673,31 @@ impl WorkspaceDb { bottom_dock_zoom, window_id FROM workspaces - WHERE local_paths = ? + WHERE + paths IS ? AND + ssh_connection_id IS ? + LIMIT 1 + }) + .map(|mut prepared_statement| { + (prepared_statement)(( + root_paths.serialize().paths, + ssh_connection_id.map(|id| id.0 as i32), + )) + .unwrap() }) - .and_then(|mut prepared_statement| (prepared_statement)(&local_paths)) .context("No workspaces found") .warn_on_err() .flatten()?; - let local_paths = local_paths?; - let location = match local_paths_order { - Some(order) => SerializedWorkspaceLocation::Local(local_paths, order), - None => { - let order = LocalPathsOrder::default_for_paths(&local_paths); - SerializedWorkspaceLocation::Local(local_paths, order) - } - }; + let paths = PathList::deserialize(&SerializedPathList { + paths, + order: paths_order, + }); Some(SerializedWorkspace { id: workspace_id, - location, + location: SerializedWorkspaceLocation::Local, + paths, center_group: self .get_center_pane_group(workspace_id) .context("Getting center group") @@ -648,63 +712,6 @@ impl WorkspaceDb { }) } - pub(crate) fn workspace_for_ssh_project( - &self, - ssh_project: &SerializedSshProject, - ) -> Option { - let (workspace_id, window_bounds, display, centered_layout, docks, window_id): ( - WorkspaceId, - Option, - Option, - Option, - DockStructure, - Option, - ) = self - .select_row_bound(sql! { - SELECT - workspace_id, - window_state, - window_x, - window_y, - window_width, - window_height, - display, - centered_layout, - left_dock_visible, - left_dock_active_panel, - left_dock_zoom, - right_dock_visible, - right_dock_active_panel, - right_dock_zoom, - bottom_dock_visible, - bottom_dock_active_panel, - bottom_dock_zoom, - window_id - FROM workspaces - WHERE ssh_project_id = ? - }) - .and_then(|mut prepared_statement| (prepared_statement)(ssh_project.id.0)) - .context("No workspaces found") - .warn_on_err() - .flatten()?; - - Some(SerializedWorkspace { - id: workspace_id, - location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()), - center_group: self - .get_center_pane_group(workspace_id) - .context("Getting center group") - .log_err()?, - window_bounds, - centered_layout: centered_layout.unwrap_or(false), - breakpoints: self.breakpoints(workspace_id), - display, - docks, - session_id: None, - window_id, - }) - } - fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap, Vec> { let breakpoints: Result> = self .select_bound(sql! { @@ -754,6 +761,13 @@ impl WorkspaceDb { /// Saves a workspace using the worktree roots. Will garbage collect any workspaces /// that used this workspace previously pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) { + let paths = workspace.paths.serialize(); + let ssh_connection_id = match &workspace.location { + SerializedWorkspaceLocation::Local => None, + SerializedWorkspaceLocation::Ssh(serialized_ssh_connection) => { + Some(serialized_ssh_connection.id.0) + } + }; log::debug!("Saving workspace at location: {:?}", workspace.location); self.write(move |conn| { conn.with_savepoint("update_worktrees", || { @@ -763,7 +777,12 @@ impl WorkspaceDb { DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) .context("Clearing old panes")?; - conn.exec_bound(sql!(DELETE FROM breakpoints WHERE workspace_id = ?1))?(workspace.id).context("Clearing old breakpoints")?; + conn.exec_bound( + sql!( + DELETE FROM breakpoints WHERE workspace_id = ?1; + DELETE FROM toolchains WHERE workspace_id = ?1; + ) + )?(workspace.id).context("Clearing old breakpoints")?; for (path, breakpoints) in workspace.breakpoints { for bp in breakpoints { @@ -790,115 +809,73 @@ impl WorkspaceDb { } } } - } + conn.exec_bound(sql!( + DELETE + FROM workspaces + WHERE + workspace_id != ?1 AND + paths IS ?2 AND + ssh_connection_id IS ?3 + ))?(( + workspace.id, + paths.paths.clone(), + ssh_connection_id, + )) + .context("clearing out old locations")?; - match workspace.location { - SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => { - conn.exec_bound(sql!( - DELETE FROM toolchains WHERE workspace_id = ?1; - DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ? - ))?((&local_paths, workspace.id)) - .context("clearing out old locations")?; - - // Upsert - let query = sql!( - INSERT INTO workspaces( - workspace_id, - local_paths, - local_paths_order, - left_dock_visible, - left_dock_active_panel, - left_dock_zoom, - right_dock_visible, - right_dock_active_panel, - right_dock_zoom, - bottom_dock_visible, - bottom_dock_active_panel, - bottom_dock_zoom, - session_id, - window_id, - timestamp, - local_paths_array, - local_paths_order_array - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, CURRENT_TIMESTAMP, ?15, ?16) - ON CONFLICT DO - UPDATE SET - local_paths = ?2, - local_paths_order = ?3, - left_dock_visible = ?4, - left_dock_active_panel = ?5, - left_dock_zoom = ?6, - right_dock_visible = ?7, - right_dock_active_panel = ?8, - right_dock_zoom = ?9, - bottom_dock_visible = ?10, - bottom_dock_active_panel = ?11, - bottom_dock_zoom = ?12, - session_id = ?13, - window_id = ?14, - timestamp = CURRENT_TIMESTAMP, - local_paths_array = ?15, - local_paths_order_array = ?16 - ); - let mut prepared_query = conn.exec_bound(query)?; - let args = (workspace.id, &local_paths, &local_paths_order, workspace.docks, workspace.session_id, workspace.window_id, local_paths.paths().iter().map(|path| path.to_string_lossy().to_string()).join(","), local_paths_order.order().iter().map(|order| order.to_string()).join(",")); - - prepared_query(args).context("Updating workspace")?; - } - SerializedWorkspaceLocation::Ssh(ssh_project) => { - conn.exec_bound(sql!( - DELETE FROM toolchains WHERE workspace_id = ?1; - DELETE FROM workspaces WHERE ssh_project_id = ? AND workspace_id != ? - ))?((ssh_project.id.0, workspace.id)) - .context("clearing out old locations")?; - - // Upsert - conn.exec_bound(sql!( - INSERT INTO workspaces( - workspace_id, - ssh_project_id, - left_dock_visible, - left_dock_active_panel, - left_dock_zoom, - right_dock_visible, - right_dock_active_panel, - right_dock_zoom, - bottom_dock_visible, - bottom_dock_active_panel, - bottom_dock_zoom, - session_id, - window_id, - timestamp - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, CURRENT_TIMESTAMP) - ON CONFLICT DO - UPDATE SET - ssh_project_id = ?2, - left_dock_visible = ?3, - left_dock_active_panel = ?4, - left_dock_zoom = ?5, - right_dock_visible = ?6, - right_dock_active_panel = ?7, - right_dock_zoom = ?8, - bottom_dock_visible = ?9, - bottom_dock_active_panel = ?10, - bottom_dock_zoom = ?11, - session_id = ?12, - window_id = ?13, - timestamp = CURRENT_TIMESTAMP - ))?(( - workspace.id, - ssh_project.id.0, - workspace.docks, - workspace.session_id, - workspace.window_id - )) - .context("Updating workspace")?; - } - } + // Upsert + let query = sql!( + INSERT INTO workspaces( + workspace_id, + paths, + paths_order, + ssh_connection_id, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + session_id, + window_id, + timestamp + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP) + ON CONFLICT DO + UPDATE SET + paths = ?2, + paths_order = ?3, + ssh_connection_id = ?4, + left_dock_visible = ?5, + left_dock_active_panel = ?6, + left_dock_zoom = ?7, + right_dock_visible = ?8, + right_dock_active_panel = ?9, + right_dock_zoom = ?10, + bottom_dock_visible = ?11, + bottom_dock_active_panel = ?12, + bottom_dock_zoom = ?13, + session_id = ?14, + window_id = ?15, + timestamp = CURRENT_TIMESTAMP + ); + let mut prepared_query = conn.exec_bound(query)?; + let args = ( + workspace.id, + paths.paths.clone(), + paths.order.clone(), + ssh_connection_id, + workspace.docks, + workspace.session_id, + workspace.window_id, + ); + + prepared_query(args).context("Updating workspace")?; // Save center pane group Self::save_pane_group(conn, workspace.id, &workspace.center_group, None) @@ -911,89 +888,100 @@ impl WorkspaceDb { .await; } - pub(crate) async fn get_or_create_ssh_project( + pub(crate) async fn get_or_create_ssh_connection( &self, host: String, port: Option, - paths: Vec, user: Option, - ) -> Result { - let paths = serde_json::to_string(&paths)?; - if let Some(project) = self - .get_ssh_project(host.clone(), port, paths.clone(), user.clone()) + ) -> Result { + if let Some(id) = self + .get_ssh_connection(host.clone(), port, user.clone()) .await? { - Ok(project) + Ok(SshProjectId(id)) } else { log::debug!("Inserting SSH project at host {host}"); - self.insert_ssh_project(host, port, paths, user) + let id = self + .insert_ssh_connection(host, port, user) .await? - .context("failed to insert ssh project") + .context("failed to insert ssh project")?; + Ok(SshProjectId(id)) } } query! { - async fn get_ssh_project(host: String, port: Option, paths: String, user: Option) -> Result> { - SELECT id, host, port, paths, user - FROM ssh_projects - WHERE host IS ? AND port IS ? AND paths IS ? AND user IS ? + async fn get_ssh_connection(host: String, port: Option, user: Option) -> Result> { + SELECT id + FROM ssh_connections + WHERE host IS ? AND port IS ? AND user IS ? LIMIT 1 } } query! { - async fn insert_ssh_project(host: String, port: Option, paths: String, user: Option) -> Result> { - INSERT INTO ssh_projects( + async fn insert_ssh_connection(host: String, port: Option, user: Option) -> Result> { + INSERT INTO ssh_connections ( host, port, - paths, user - ) VALUES (?1, ?2, ?3, ?4) - RETURNING id, host, port, paths, user + ) VALUES (?1, ?2, ?3) + RETURNING id } } - query! { - pub async fn update_ssh_project_paths_query(ssh_project_id: u64, paths: String) -> Result> { - UPDATE ssh_projects - SET paths = ?2 - WHERE id = ?1 - RETURNING id, host, port, paths, user - } - } - - pub(crate) async fn update_ssh_project_paths( - &self, - ssh_project_id: SshProjectId, - new_paths: Vec, - ) -> Result { - let paths = serde_json::to_string(&new_paths)?; - self.update_ssh_project_paths_query(ssh_project_id.0, paths) - .await? - .context("failed to update ssh project paths") - } - query! { pub async fn next_id() -> Result { INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id } } + fn recent_workspaces(&self) -> Result)>> { + Ok(self + .recent_workspaces_query()? + .into_iter() + .map(|(id, paths, order, ssh_connection_id)| { + ( + id, + PathList::deserialize(&SerializedPathList { paths, order }), + ssh_connection_id, + ) + }) + .collect()) + } + query! { - fn recent_workspaces() -> Result)>> { - SELECT workspace_id, local_paths, local_paths_order, ssh_project_id + fn recent_workspaces_query() -> Result)>> { + SELECT workspace_id, paths, paths_order, ssh_connection_id FROM workspaces - WHERE local_paths IS NOT NULL - OR ssh_project_id IS NOT NULL + WHERE + paths IS NOT NULL OR + ssh_connection_id IS NOT NULL ORDER BY timestamp DESC } } + fn session_workspaces( + &self, + session_id: String, + ) -> Result, Option)>> { + Ok(self + .session_workspaces_query(session_id)? + .into_iter() + .map(|(paths, order, window_id, ssh_connection_id)| { + ( + PathList::deserialize(&SerializedPathList { paths, order }), + window_id, + ssh_connection_id.map(SshProjectId), + ) + }) + .collect()) + } + query! { - fn session_workspaces(session_id: String) -> Result, Option)>> { - SELECT local_paths, local_paths_order, window_id, ssh_project_id + fn session_workspaces_query(session_id: String) -> Result, Option)>> { + SELECT paths, paths_order, window_id, ssh_connection_id FROM workspaces - WHERE session_id = ?1 AND dev_server_project_id IS NULL + WHERE session_id = ?1 ORDER BY timestamp DESC } } @@ -1013,17 +1001,40 @@ impl WorkspaceDb { } } + fn ssh_connections(&self) -> Result> { + Ok(self + .ssh_connections_query()? + .into_iter() + .map(|(id, host, port, user)| SerializedSshConnection { + id: SshProjectId(id), + host, + port, + user, + }) + .collect()) + } + query! { - fn ssh_projects() -> Result> { - SELECT id, host, port, paths, user - FROM ssh_projects + pub fn ssh_connections_query() -> Result, Option)>> { + SELECT id, host, port, user + FROM ssh_connections } } + pub fn ssh_connection(&self, id: SshProjectId) -> Result { + let row = self.ssh_connection_query(id.0)?; + Ok(SerializedSshConnection { + id: SshProjectId(row.0), + host: row.1, + port: row.2, + user: row.3, + }) + } + query! { - fn ssh_project(id: u64) -> Result { - SELECT id, host, port, paths, user - FROM ssh_projects + fn ssh_connection_query(id: u64) -> Result<(u64, String, Option, Option)> { + SELECT id, host, port, user + FROM ssh_connections WHERE id = ? } } @@ -1037,7 +1048,7 @@ impl WorkspaceDb { display, window_state, window_x, window_y, window_width, window_height FROM workspaces - WHERE local_paths + WHERE paths IS NOT NULL ORDER BY timestamp DESC LIMIT 1 @@ -1054,46 +1065,35 @@ impl WorkspaceDb { } } - pub async fn delete_workspace_by_dev_server_project_id( - &self, - id: DevServerProjectId, - ) -> Result<()> { - self.write(move |conn| { - conn.exec_bound(sql!( - DELETE FROM dev_server_projects WHERE id = ? - ))?(id.0)?; - conn.exec_bound(sql!( - DELETE FROM toolchains WHERE workspace_id = ?1; - DELETE FROM workspaces - WHERE dev_server_project_id IS ? - ))?(id.0) - }) - .await - } - // Returns the recent locations which are still valid on disk and deletes ones which no longer // exist. pub async fn recent_workspaces_on_disk( &self, - ) -> Result> { + ) -> Result> { let mut result = Vec::new(); let mut delete_tasks = Vec::new(); - let ssh_projects = self.ssh_projects()?; - - for (id, location, order, ssh_project_id) in self.recent_workspaces()? { - if let Some(ssh_project_id) = ssh_project_id.map(SshProjectId) { - if let Some(ssh_project) = ssh_projects.iter().find(|rp| rp.id == ssh_project_id) { - result.push((id, SerializedWorkspaceLocation::Ssh(ssh_project.clone()))); + let ssh_connections = self.ssh_connections()?; + + for (id, paths, ssh_connection_id) in self.recent_workspaces()? { + if let Some(ssh_connection_id) = ssh_connection_id.map(SshProjectId) { + if let Some(ssh_connection) = + ssh_connections.iter().find(|rp| rp.id == ssh_connection_id) + { + result.push(( + id, + SerializedWorkspaceLocation::Ssh(ssh_connection.clone()), + paths, + )); } else { delete_tasks.push(self.delete_workspace_by_id(id)); } continue; } - if location.paths().iter().all(|path| path.exists()) - && location.paths().iter().any(|path| path.is_dir()) + if paths.paths().iter().all(|path| path.exists()) + && paths.paths().iter().any(|path| path.is_dir()) { - result.push((id, SerializedWorkspaceLocation::Local(location, order))); + result.push((id, SerializedWorkspaceLocation::Local, paths)); } else { delete_tasks.push(self.delete_workspace_by_id(id)); } @@ -1103,13 +1103,13 @@ impl WorkspaceDb { Ok(result) } - pub async fn last_workspace(&self) -> Result> { + pub async fn last_workspace(&self) -> Result> { Ok(self .recent_workspaces_on_disk() .await? .into_iter() .next() - .map(|(_, location)| location)) + .map(|(_, location, paths)| (location, paths))) } // Returns the locations of the workspaces that were still opened when the last @@ -1120,25 +1120,31 @@ impl WorkspaceDb { &self, last_session_id: &str, last_session_window_stack: Option>, - ) -> Result> { + ) -> Result> { let mut workspaces = Vec::new(); - for (location, order, window_id, ssh_project_id) in + for (paths, window_id, ssh_connection_id) in self.session_workspaces(last_session_id.to_owned())? { - if let Some(ssh_project_id) = ssh_project_id { - let location = SerializedWorkspaceLocation::Ssh(self.ssh_project(ssh_project_id)?); - workspaces.push((location, window_id.map(WindowId::from))); - } else if location.paths().iter().all(|path| path.exists()) - && location.paths().iter().any(|path| path.is_dir()) + if let Some(ssh_connection_id) = ssh_connection_id { + workspaces.push(( + SerializedWorkspaceLocation::Ssh(self.ssh_connection(ssh_connection_id)?), + paths, + window_id.map(WindowId::from), + )); + } else if paths.paths().iter().all(|path| path.exists()) + && paths.paths().iter().any(|path| path.is_dir()) { - let location = SerializedWorkspaceLocation::Local(location, order); - workspaces.push((location, window_id.map(WindowId::from))); + workspaces.push(( + SerializedWorkspaceLocation::Local, + paths, + window_id.map(WindowId::from), + )); } } if let Some(stack) = last_session_window_stack { - workspaces.sort_by_key(|(_, window_id)| { + workspaces.sort_by_key(|(_, _, window_id)| { window_id .and_then(|id| stack.iter().position(|&order_id| order_id == id)) .unwrap_or(usize::MAX) @@ -1147,7 +1153,7 @@ impl WorkspaceDb { Ok(workspaces .into_iter() - .map(|(paths, _)| paths) + .map(|(location, paths, _)| (location, paths)) .collect::>()) } @@ -1499,13 +1505,13 @@ pub fn delete_unloaded_items( #[cfg(test)] mod tests { - use std::thread; - use std::time::Duration; - use super::*; - use crate::persistence::model::SerializedWorkspace; - use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup}; + use crate::persistence::model::{ + SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, + }; use gpui; + use pretty_assertions::assert_eq; + use std::{thread, time::Duration}; #[gpui::test] async fn test_breakpoints() { @@ -1558,7 +1564,8 @@ mod tests { let workspace = SerializedWorkspace { id, - location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), + paths: PathList::new(&["/tmp"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1711,7 +1718,8 @@ mod tests { let workspace = SerializedWorkspace { id, - location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), + paths: PathList::new(&["/tmp"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1757,7 +1765,8 @@ mod tests { let workspace_without_breakpoint = SerializedWorkspace { id, - location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), + paths: PathList::new(&["/tmp"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1851,7 +1860,8 @@ mod tests { let mut workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]), + paths: PathList::new(&["/tmp", "/tmp2"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1864,7 +1874,8 @@ mod tests { let workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), + paths: PathList::new(&["/tmp"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1893,7 +1904,7 @@ mod tests { }) .await; - workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]); + workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]); db.save_workspace(workspace_1.clone()).await; db.save_workspace(workspace_1).await; db.save_workspace(workspace_2).await; @@ -1969,10 +1980,8 @@ mod tests { let workspace = SerializedWorkspace { id: WorkspaceId(5), - location: SerializedWorkspaceLocation::Local( - LocalPaths::new(["/tmp", "/tmp2"]), - LocalPathsOrder::new([1, 0]), - ), + paths: PathList::new(&["/tmp", "/tmp2"]), + location: SerializedWorkspaceLocation::Local, center_group, window_bounds: Default::default(), breakpoints: Default::default(), @@ -2004,10 +2013,8 @@ mod tests { let workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - location: SerializedWorkspaceLocation::Local( - LocalPaths::new(["/tmp", "/tmp2"]), - LocalPathsOrder::new([0, 1]), - ), + paths: PathList::new(&["/tmp", "/tmp2"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2020,7 +2027,8 @@ mod tests { let mut workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), + paths: PathList::new(&["/tmp"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2049,7 +2057,7 @@ mod tests { assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None); // Test 'mutate' case of updating a pre-existing id - workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]); + workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]); db.save_workspace(workspace_2.clone()).await; assert_eq!( @@ -2060,10 +2068,8 @@ mod tests { // Test other mechanism for mutating let mut workspace_3 = SerializedWorkspace { id: WorkspaceId(3), - location: SerializedWorkspaceLocation::Local( - LocalPaths::new(["/tmp", "/tmp2"]), - LocalPathsOrder::new([1, 0]), - ), + paths: PathList::new(&["/tmp2", "/tmp"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2081,8 +2087,7 @@ mod tests { ); // Make sure that updating paths differently also works - workspace_3.location = - SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]); + workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]); db.save_workspace(workspace_3.clone()).await; assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None); assert_eq!( @@ -2100,7 +2105,8 @@ mod tests { let workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]), + paths: PathList::new(&["/tmp1"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2113,7 +2119,8 @@ mod tests { let workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]), + paths: PathList::new(&["/tmp2"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2126,7 +2133,8 @@ mod tests { let workspace_3 = SerializedWorkspace { id: WorkspaceId(3), - location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]), + paths: PathList::new(&["/tmp3"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2139,7 +2147,8 @@ mod tests { let workspace_4 = SerializedWorkspace { id: WorkspaceId(4), - location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]), + paths: PathList::new(&["/tmp4"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2150,14 +2159,15 @@ mod tests { window_id: None, }; - let ssh_project = db - .get_or_create_ssh_project("my-host".to_string(), Some(1234), vec![], None) + let connection_id = db + .get_or_create_ssh_connection("my-host".to_string(), Some(1234), None) .await .unwrap(); let workspace_5 = SerializedWorkspace { id: WorkspaceId(5), - location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()), + paths: PathList::default(), + location: SerializedWorkspaceLocation::Ssh(db.ssh_connection(connection_id).unwrap()), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2170,10 +2180,8 @@ mod tests { let workspace_6 = SerializedWorkspace { id: WorkspaceId(6), - location: SerializedWorkspaceLocation::Local( - LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]), - LocalPathsOrder::new([2, 1, 0]), - ), + paths: PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2195,41 +2203,36 @@ mod tests { let locations = db.session_workspaces("session-id-1".to_owned()).unwrap(); assert_eq!(locations.len(), 2); - assert_eq!(locations[0].0, LocalPaths::new(["/tmp2"])); - assert_eq!(locations[0].1, LocalPathsOrder::new([0])); - assert_eq!(locations[0].2, Some(20)); - assert_eq!(locations[1].0, LocalPaths::new(["/tmp1"])); - assert_eq!(locations[1].1, LocalPathsOrder::new([0])); - assert_eq!(locations[1].2, Some(10)); + assert_eq!(locations[0].0, PathList::new(&["/tmp2"])); + assert_eq!(locations[0].1, Some(20)); + assert_eq!(locations[1].0, PathList::new(&["/tmp1"])); + assert_eq!(locations[1].1, Some(10)); let locations = db.session_workspaces("session-id-2".to_owned()).unwrap(); assert_eq!(locations.len(), 2); - let empty_paths: Vec<&str> = Vec::new(); - assert_eq!(locations[0].0, LocalPaths::new(empty_paths.iter())); - assert_eq!(locations[0].1, LocalPathsOrder::new([])); - assert_eq!(locations[0].2, Some(50)); - assert_eq!(locations[0].3, Some(ssh_project.id.0)); - assert_eq!(locations[1].0, LocalPaths::new(["/tmp3"])); - assert_eq!(locations[1].1, LocalPathsOrder::new([0])); - assert_eq!(locations[1].2, Some(30)); + assert_eq!(locations[0].0, PathList::default()); + assert_eq!(locations[0].1, Some(50)); + assert_eq!(locations[0].2, Some(connection_id)); + assert_eq!(locations[1].0, PathList::new(&["/tmp3"])); + assert_eq!(locations[1].1, Some(30)); let locations = db.session_workspaces("session-id-3".to_owned()).unwrap(); assert_eq!(locations.len(), 1); assert_eq!( locations[0].0, - LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]), + PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]), ); - assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0])); - assert_eq!(locations[0].2, Some(60)); + assert_eq!(locations[0].1, Some(60)); } fn default_workspace>( - workspace_id: &[P], + paths: &[P], center_group: &SerializedPaneGroup, ) -> SerializedWorkspace { SerializedWorkspace { id: WorkspaceId(4), - location: SerializedWorkspaceLocation::from_local_paths(workspace_id), + paths: PathList::new(paths), + location: SerializedWorkspaceLocation::Local, center_group: center_group.clone(), window_bounds: Default::default(), display: Default::default(), @@ -2252,30 +2255,18 @@ mod tests { WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await; let workspaces = [ - (1, vec![dir1.path()], vec![0], 9), - (2, vec![dir2.path()], vec![0], 5), - (3, vec![dir3.path()], vec![0], 8), - (4, vec![dir4.path()], vec![0], 2), - ( - 5, - vec![dir1.path(), dir2.path(), dir3.path()], - vec![0, 1, 2], - 3, - ), - ( - 6, - vec![dir2.path(), dir3.path(), dir4.path()], - vec![2, 1, 0], - 4, - ), + (1, vec![dir1.path()], 9), + (2, vec![dir2.path()], 5), + (3, vec![dir3.path()], 8), + (4, vec![dir4.path()], 2), + (5, vec![dir1.path(), dir2.path(), dir3.path()], 3), + (6, vec![dir4.path(), dir3.path(), dir2.path()], 4), ] .into_iter() - .map(|(id, locations, order, window_id)| SerializedWorkspace { + .map(|(id, paths, window_id)| SerializedWorkspace { id: WorkspaceId(id), - location: SerializedWorkspaceLocation::Local( - LocalPaths::new(locations), - LocalPathsOrder::new(order), - ), + paths: PathList::new(paths.as_slice()), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2300,39 +2291,37 @@ mod tests { WindowId::from(4), // Bottom ])); - let have = db + let locations = db .last_session_workspace_locations("one-session", stack) .unwrap(); - assert_eq!(have.len(), 6); - assert_eq!( - have[0], - SerializedWorkspaceLocation::from_local_paths(&[dir4.path()]) - ); - assert_eq!( - have[1], - SerializedWorkspaceLocation::from_local_paths([dir3.path()]) - ); assert_eq!( - have[2], - SerializedWorkspaceLocation::from_local_paths([dir2.path()]) - ); - assert_eq!( - have[3], - SerializedWorkspaceLocation::from_local_paths([dir1.path()]) - ); - assert_eq!( - have[4], - SerializedWorkspaceLocation::Local( - LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]), - LocalPathsOrder::new([0, 1, 2]), - ), - ); - assert_eq!( - have[5], - SerializedWorkspaceLocation::Local( - LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]), - LocalPathsOrder::new([2, 1, 0]), - ), + locations, + [ + ( + SerializedWorkspaceLocation::Local, + PathList::new(&[dir4.path()]) + ), + ( + SerializedWorkspaceLocation::Local, + PathList::new(&[dir3.path()]) + ), + ( + SerializedWorkspaceLocation::Local, + PathList::new(&[dir2.path()]) + ), + ( + SerializedWorkspaceLocation::Local, + PathList::new(&[dir1.path()]) + ), + ( + SerializedWorkspaceLocation::Local, + PathList::new(&[dir1.path(), dir2.path(), dir3.path()]) + ), + ( + SerializedWorkspaceLocation::Local, + PathList::new(&[dir4.path(), dir3.path(), dir2.path()]) + ), + ] ); } @@ -2343,7 +2332,7 @@ mod tests { ) .await; - let ssh_projects = [ + let ssh_connections = [ ("host-1", "my-user-1"), ("host-2", "my-user-2"), ("host-3", "my-user-3"), @@ -2351,24 +2340,32 @@ mod tests { ] .into_iter() .map(|(host, user)| async { - db.get_or_create_ssh_project(host.to_string(), None, vec![], Some(user.to_string())) + let id = db + .get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string())) .await - .unwrap() + .unwrap(); + SerializedSshConnection { + id, + host: host.into(), + port: None, + user: Some(user.into()), + } }) .collect::>(); - let ssh_projects = futures::future::join_all(ssh_projects).await; + let ssh_connections = futures::future::join_all(ssh_connections).await; let workspaces = [ - (1, ssh_projects[0].clone(), 9), - (2, ssh_projects[1].clone(), 5), - (3, ssh_projects[2].clone(), 8), - (4, ssh_projects[3].clone(), 2), + (1, ssh_connections[0].clone(), 9), + (2, ssh_connections[1].clone(), 5), + (3, ssh_connections[2].clone(), 8), + (4, ssh_connections[3].clone(), 2), ] .into_iter() - .map(|(id, ssh_project, window_id)| SerializedWorkspace { + .map(|(id, ssh_connection, window_id)| SerializedWorkspace { id: WorkspaceId(id), - location: SerializedWorkspaceLocation::Ssh(ssh_project), + paths: PathList::default(), + location: SerializedWorkspaceLocation::Ssh(ssh_connection), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2397,19 +2394,31 @@ mod tests { assert_eq!(have.len(), 4); assert_eq!( have[0], - SerializedWorkspaceLocation::Ssh(ssh_projects[3].clone()) + ( + SerializedWorkspaceLocation::Ssh(ssh_connections[3].clone()), + PathList::default() + ) ); assert_eq!( have[1], - SerializedWorkspaceLocation::Ssh(ssh_projects[2].clone()) + ( + SerializedWorkspaceLocation::Ssh(ssh_connections[2].clone()), + PathList::default() + ) ); assert_eq!( have[2], - SerializedWorkspaceLocation::Ssh(ssh_projects[1].clone()) + ( + SerializedWorkspaceLocation::Ssh(ssh_connections[1].clone()), + PathList::default() + ) ); assert_eq!( have[3], - SerializedWorkspaceLocation::Ssh(ssh_projects[0].clone()) + ( + SerializedWorkspaceLocation::Ssh(ssh_connections[0].clone()), + PathList::default() + ) ); } @@ -2417,116 +2426,102 @@ mod tests { async fn test_get_or_create_ssh_project() { let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await; - let (host, port, paths, user) = ( - "example.com".to_string(), - Some(22_u16), - vec!["/home/user".to_string(), "/etc/nginx".to_string()], - Some("user".to_string()), - ); + let host = "example.com".to_string(); + let port = Some(22_u16); + let user = Some("user".to_string()); - let project = db - .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) + let connection_id = db + .get_or_create_ssh_connection(host.clone(), port, user.clone()) .await .unwrap(); - assert_eq!(project.host, host); - assert_eq!(project.paths, paths); - assert_eq!(project.user, user); - // Test that calling the function again with the same parameters returns the same project - let same_project = db - .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) + let same_connection = db + .get_or_create_ssh_connection(host.clone(), port, user.clone()) .await .unwrap(); - assert_eq!(project.id, same_project.id); + assert_eq!(connection_id, same_connection); // Test with different parameters - let (host2, paths2, user2) = ( - "otherexample.com".to_string(), - vec!["/home/otheruser".to_string()], - Some("otheruser".to_string()), - ); + let host2 = "otherexample.com".to_string(); + let port2 = None; + let user2 = Some("otheruser".to_string()); - let different_project = db - .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone()) + let different_connection = db + .get_or_create_ssh_connection(host2.clone(), port2, user2.clone()) .await .unwrap(); - assert_ne!(project.id, different_project.id); - assert_eq!(different_project.host, host2); - assert_eq!(different_project.paths, paths2); - assert_eq!(different_project.user, user2); + assert_ne!(connection_id, different_connection); } #[gpui::test] async fn test_get_or_create_ssh_project_with_null_user() { let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await; - let (host, port, paths, user) = ( - "example.com".to_string(), - None, - vec!["/home/user".to_string()], - None, - ); + let (host, port, user) = ("example.com".to_string(), None, None); - let project = db - .get_or_create_ssh_project(host.clone(), port, paths.clone(), None) + let connection_id = db + .get_or_create_ssh_connection(host.clone(), port, None) .await .unwrap(); - assert_eq!(project.host, host); - assert_eq!(project.paths, paths); - assert_eq!(project.user, None); - - // Test that calling the function again with the same parameters returns the same project - let same_project = db - .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) + let same_connection_id = db + .get_or_create_ssh_connection(host.clone(), port, user.clone()) .await .unwrap(); - assert_eq!(project.id, same_project.id); + assert_eq!(connection_id, same_connection_id); } #[gpui::test] - async fn test_get_ssh_projects() { - let db = WorkspaceDb::open_test_db("test_get_ssh_projects").await; + async fn test_get_ssh_connections() { + let db = WorkspaceDb::open_test_db("test_get_ssh_connections").await; - let projects = vec![ - ( - "example.com".to_string(), - None, - vec!["/home/user".to_string()], - None, - ), + let connections = [ + ("example.com".to_string(), None, None), ( "anotherexample.com".to_string(), Some(123_u16), - vec!["/home/user2".to_string()], Some("user2".to_string()), ), - ( - "yetanother.com".to_string(), - Some(345_u16), - vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()], - None, - ), + ("yetanother.com".to_string(), Some(345_u16), None), ]; - for (host, port, paths, user) in projects.iter() { - let project = db - .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone()) - .await - .unwrap(); - - assert_eq!(&project.host, host); - assert_eq!(&project.port, port); - assert_eq!(&project.paths, paths); - assert_eq!(&project.user, user); + let mut ids = Vec::new(); + for (host, port, user) in connections.iter() { + ids.push( + db.get_or_create_ssh_connection(host.clone(), *port, user.clone()) + .await + .unwrap(), + ); } - let stored_projects = db.ssh_projects().unwrap(); - assert_eq!(stored_projects.len(), projects.len()); + let stored_projects = db.ssh_connections().unwrap(); + assert_eq!( + stored_projects, + &[ + SerializedSshConnection { + id: ids[0], + host: "example.com".into(), + port: None, + user: None, + }, + SerializedSshConnection { + id: ids[1], + host: "anotherexample.com".into(), + port: Some(123), + user: Some("user2".into()), + }, + SerializedSshConnection { + id: ids[2], + host: "yetanother.com".into(), + port: Some(345), + user: None, + }, + ] + ); } #[gpui::test] @@ -2659,56 +2654,4 @@ mod tests { assert_eq!(workspace.center_group, new_workspace.center_group); } - - #[gpui::test] - async fn test_update_ssh_project_paths() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_update_ssh_project_paths").await; - - let (host, port, initial_paths, user) = ( - "example.com".to_string(), - Some(22_u16), - vec!["/home/user".to_string(), "/etc/nginx".to_string()], - Some("user".to_string()), - ); - - let project = db - .get_or_create_ssh_project(host.clone(), port, initial_paths.clone(), user.clone()) - .await - .unwrap(); - - assert_eq!(project.host, host); - assert_eq!(project.paths, initial_paths); - assert_eq!(project.user, user); - - let new_paths = vec![ - "/home/user".to_string(), - "/etc/nginx".to_string(), - "/var/log".to_string(), - "/opt/app".to_string(), - ]; - - let updated_project = db - .update_ssh_project_paths(project.id, new_paths.clone()) - .await - .unwrap(); - - assert_eq!(updated_project.id, project.id); - assert_eq!(updated_project.paths, new_paths); - - let retrieved_project = db - .get_ssh_project( - host.clone(), - port, - serde_json::to_string(&new_paths).unwrap(), - user.clone(), - ) - .await - .unwrap() - .unwrap(); - - assert_eq!(retrieved_project.id, project.id); - assert_eq!(retrieved_project.paths, new_paths); - } } diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 15a54ac62f6a74e9429dee2e343ebf91054dc528..afe4ae62356c83daa25eabc520c4dfc96cfbb1cb 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -1,15 +1,16 @@ use super::{SerializedAxis, SerializedWindowBounds}; use crate::{ Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId, item::ItemHandle, + path_list::PathList, }; -use anyhow::{Context as _, Result}; +use anyhow::Result; use async_recursion::async_recursion; use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use gpui::{AsyncWindowContext, Entity, WeakEntity}; -use itertools::Itertools as _; + use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; use remote::ssh_session::SshProjectId; use serde::{Deserialize, Serialize}; @@ -18,239 +19,27 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use util::{ResultExt, paths::SanitizedPath}; +use util::ResultExt; use uuid::Uuid; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct SerializedSshProject { +pub struct SerializedSshConnection { pub id: SshProjectId, pub host: String, pub port: Option, - pub paths: Vec, pub user: Option, } -impl SerializedSshProject { - pub fn ssh_urls(&self) -> Vec { - self.paths - .iter() - .map(|path| { - let mut result = String::new(); - if let Some(user) = &self.user { - result.push_str(user); - result.push('@'); - } - result.push_str(&self.host); - if let Some(port) = &self.port { - result.push(':'); - result.push_str(&port.to_string()); - } - result.push_str(path); - PathBuf::from(result) - }) - .collect() - } -} - -impl StaticColumnCount for SerializedSshProject { - fn column_count() -> usize { - 5 - } -} - -impl Bind for &SerializedSshProject { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = statement.bind(&self.id.0, start_index)?; - let next_index = statement.bind(&self.host, next_index)?; - let next_index = statement.bind(&self.port, next_index)?; - let raw_paths = serde_json::to_string(&self.paths)?; - let next_index = statement.bind(&raw_paths, next_index)?; - statement.bind(&self.user, next_index) - } -} - -impl Column for SerializedSshProject { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let id = statement.column_int64(start_index)?; - let host = statement.column_text(start_index + 1)?.to_string(); - let (port, _) = Option::::column(statement, start_index + 2)?; - let raw_paths = statement.column_text(start_index + 3)?.to_string(); - let paths: Vec = serde_json::from_str(&raw_paths)?; - - let (user, _) = Option::::column(statement, start_index + 4)?; - - Ok(( - Self { - id: SshProjectId(id as u64), - host, - port, - paths, - user, - }, - start_index + 5, - )) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct LocalPaths(Arc>); - -impl LocalPaths { - pub fn new>(paths: impl IntoIterator) -> Self { - let mut paths: Vec = paths - .into_iter() - .map(|p| SanitizedPath::from(p).into()) - .collect(); - // Ensure all future `zed workspace1 workspace2` and `zed workspace2 workspace1` calls are using the same workspace. - // The actual workspace order is stored in the `LocalPathsOrder` struct. - paths.sort(); - Self(Arc::new(paths)) - } - - pub fn paths(&self) -> &Arc> { - &self.0 - } -} - -impl StaticColumnCount for LocalPaths {} -impl Bind for &LocalPaths { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement.bind(&bincode::serialize(&self.0)?, start_index) - } -} - -impl Column for LocalPaths { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let path_blob = statement.column_blob(start_index)?; - let paths: Arc> = if path_blob.is_empty() { - Default::default() - } else { - bincode::deserialize(path_blob).context("Bincode deserialization of paths failed")? - }; - - Ok((Self(paths), start_index + 1)) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct LocalPathsOrder(Vec); - -impl LocalPathsOrder { - pub fn new(order: impl IntoIterator) -> Self { - Self(order.into_iter().collect()) - } - - pub fn order(&self) -> &[usize] { - self.0.as_slice() - } - - pub fn default_for_paths(paths: &LocalPaths) -> Self { - Self::new(0..paths.0.len()) - } -} - -impl StaticColumnCount for LocalPathsOrder {} -impl Bind for &LocalPathsOrder { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement.bind(&bincode::serialize(&self.0)?, start_index) - } -} - -impl Column for LocalPathsOrder { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let order_blob = statement.column_blob(start_index)?; - let order = if order_blob.is_empty() { - Vec::new() - } else { - bincode::deserialize(order_blob).context("deserializing workspace root order")? - }; - - Ok((Self(order), start_index + 1)) - } -} - #[derive(Debug, PartialEq, Clone)] pub enum SerializedWorkspaceLocation { - Local(LocalPaths, LocalPathsOrder), - Ssh(SerializedSshProject), + Local, + Ssh(SerializedSshConnection), } impl SerializedWorkspaceLocation { - /// Create a new `SerializedWorkspaceLocation` from a list of local paths. - /// - /// The paths will be sorted and the order will be stored in the `LocalPathsOrder` struct. - /// - /// # Examples - /// - /// ``` - /// use std::path::Path; - /// use zed_workspace::SerializedWorkspaceLocation; - /// - /// let location = SerializedWorkspaceLocation::from_local_paths(vec![ - /// Path::new("path/to/workspace1"), - /// Path::new("path/to/workspace2"), - /// ]); - /// assert_eq!(location, SerializedWorkspaceLocation::Local( - /// LocalPaths::new(vec![ - /// Path::new("path/to/workspace1"), - /// Path::new("path/to/workspace2"), - /// ]), - /// LocalPathsOrder::new(vec![0, 1]), - /// )); - /// ``` - /// - /// ``` - /// use std::path::Path; - /// use zed_workspace::SerializedWorkspaceLocation; - /// - /// let location = SerializedWorkspaceLocation::from_local_paths(vec![ - /// Path::new("path/to/workspace2"), - /// Path::new("path/to/workspace1"), - /// ]); - /// - /// assert_eq!(location, SerializedWorkspaceLocation::Local( - /// LocalPaths::new(vec![ - /// Path::new("path/to/workspace1"), - /// Path::new("path/to/workspace2"), - /// ]), - /// LocalPathsOrder::new(vec![1, 0]), - /// )); - /// ``` - pub fn from_local_paths>(paths: impl IntoIterator) -> Self { - let mut indexed_paths: Vec<_> = paths - .into_iter() - .map(|p| p.as_ref().to_path_buf()) - .enumerate() - .collect(); - - indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b)); - - let sorted_paths: Vec<_> = indexed_paths.iter().map(|(_, path)| path.clone()).collect(); - let order: Vec<_> = indexed_paths.iter().map(|(index, _)| *index).collect(); - - Self::Local(LocalPaths::new(sorted_paths), LocalPathsOrder::new(order)) - } - /// Get sorted paths pub fn sorted_paths(&self) -> Arc> { - match self { - SerializedWorkspaceLocation::Local(paths, order) => { - if order.order().is_empty() { - paths.paths().clone() - } else { - Arc::new( - order - .order() - .iter() - .zip(paths.paths().iter()) - .sorted_by_key(|(i, _)| **i) - .map(|(_, p)| p.clone()) - .collect(), - ) - } - } - SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()), - } + unimplemented!() } } @@ -258,6 +47,7 @@ impl SerializedWorkspaceLocation { pub(crate) struct SerializedWorkspace { pub(crate) id: WorkspaceId, pub(crate) location: SerializedWorkspaceLocation, + pub(crate) paths: PathList, pub(crate) center_group: SerializedPaneGroup, pub(crate) window_bounds: Option, pub(crate) centered_layout: bool, @@ -581,80 +371,3 @@ impl Column for SerializedItem { )) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_serialize_local_paths() { - let paths = vec!["b", "a", "c"]; - let serialized = SerializedWorkspaceLocation::from_local_paths(paths); - - assert_eq!( - serialized, - SerializedWorkspaceLocation::Local( - LocalPaths::new(vec!["a", "b", "c"]), - LocalPathsOrder::new(vec![1, 0, 2]) - ) - ); - } - - #[test] - fn test_sorted_paths() { - let paths = vec!["b", "a", "c"]; - let serialized = SerializedWorkspaceLocation::from_local_paths(paths); - assert_eq!( - serialized.sorted_paths(), - Arc::new(vec![ - PathBuf::from("b"), - PathBuf::from("a"), - PathBuf::from("c"), - ]) - ); - - let paths = Arc::new(vec![ - PathBuf::from("a"), - PathBuf::from("b"), - PathBuf::from("c"), - ]); - let order = vec![2, 0, 1]; - let serialized = - SerializedWorkspaceLocation::Local(LocalPaths(paths), LocalPathsOrder(order)); - assert_eq!( - serialized.sorted_paths(), - Arc::new(vec![ - PathBuf::from("b"), - PathBuf::from("c"), - PathBuf::from("a"), - ]) - ); - - let paths = Arc::new(vec![ - PathBuf::from("a"), - PathBuf::from("b"), - PathBuf::from("c"), - ]); - let order = vec![]; - let serialized = - SerializedWorkspaceLocation::Local(LocalPaths(paths.clone()), LocalPathsOrder(order)); - assert_eq!(serialized.sorted_paths(), paths); - - let urls = ["/a", "/b", "/c"]; - let serialized = SerializedWorkspaceLocation::Ssh(SerializedSshProject { - id: SshProjectId(0), - host: "host".to_string(), - port: Some(22), - paths: urls.iter().map(|s| s.to_string()).collect(), - user: Some("user".to_string()), - }); - assert_eq!( - serialized.sorted_paths(), - Arc::new( - urls.iter() - .map(|p| PathBuf::from(format!("user@host:22{}", p))) - .collect() - ) - ); - } -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d31aae2c59775cc097f6fa3e34886ff69563f58e..d07ea30cf9ef9dbd74d0bfee0991091de8ce5884 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6,6 +6,7 @@ mod modal_layer; pub mod notifications; pub mod pane; pub mod pane_group; +mod path_list; mod persistence; pub mod searchable; pub mod shared_screen; @@ -18,6 +19,7 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; +pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; use anyhow::{Context as _, Result, anyhow}; @@ -62,20 +64,20 @@ use notifications::{ }; pub use pane::*; pub use pane_group::*; -use persistence::{ - DB, SerializedWindowBounds, - model::{SerializedSshProject, SerializedWorkspace}, -}; +use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace}; pub use persistence::{ DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items, - model::{ItemId, LocalPaths, SerializedWorkspaceLocation}, + model::{ItemId, SerializedSshConnection, SerializedWorkspaceLocation}, }; use postage::stream::Stream; use project::{ DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId, debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, }; -use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIdentifier}; +use remote::{ + SshClientDelegate, SshConnectionOptions, + ssh_session::{ConnectionIdentifier, SshProjectId}, +}; use schemars::JsonSchema; use serde::Deserialize; use session::AppSession; @@ -1042,7 +1044,7 @@ pub enum OpenVisible { enum WorkspaceLocation { // Valid local paths or SSH project to serialize - Location(SerializedWorkspaceLocation), + Location(SerializedWorkspaceLocation, PathList), // No valid location found hence clear session id DetachFromSession, // No valid location found to serialize @@ -1126,7 +1128,7 @@ pub struct Workspace { terminal_provider: Option>, debugger_provider: Option>, serializable_items_tx: UnboundedSender>, - serialized_ssh_project: Option, + serialized_ssh_connection_id: Option, _items_serializer: Task>, session_id: Option, scheduled_tasks: Vec>, @@ -1175,8 +1177,6 @@ impl Workspace { project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => { this.update_window_title(window, cx); - this.update_ssh_paths(cx); - this.serialize_ssh_paths(window, cx); this.serialize_workspace(window, cx); // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`. this.update_history(cx); @@ -1461,7 +1461,7 @@ impl Workspace { serializable_items_tx, _items_serializer, session_id: Some(session_id), - serialized_ssh_project: None, + serialized_ssh_connection_id: None, scheduled_tasks: Vec::new(), } } @@ -1501,20 +1501,9 @@ impl Workspace { let serialized_workspace = persistence::DB.workspace_for_roots(paths_to_open.as_slice()); - let workspace_location = serialized_workspace - .as_ref() - .map(|ws| &ws.location) - .and_then(|loc| match loc { - SerializedWorkspaceLocation::Local(_, order) => { - Some((loc.sorted_paths(), order.order())) - } - _ => None, - }); - - if let Some((paths, order)) = workspace_location { - paths_to_open = paths.iter().cloned().collect(); - - if order.iter().enumerate().any(|(i, &j)| i != j) { + if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) { + paths_to_open = paths.paths().to_vec(); + if !paths.is_lexicographically_ordered() { project_handle .update(cx, |project, cx| { project.set_worktrees_reordered(true, cx); @@ -2034,14 +2023,6 @@ impl Workspace { self.debugger_provider.clone() } - pub fn serialized_ssh_project(&self) -> Option { - self.serialized_ssh_project.clone() - } - - pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) { - self.serialized_ssh_project = Some(serialized_ssh_project); - } - pub fn prompt_for_open_path( &mut self, path_prompt_options: PathPromptOptions, @@ -5088,59 +5069,12 @@ impl Workspace { self.session_id.clone() } - fn local_paths(&self, cx: &App) -> Option>> { - let project = self.project().read(cx); - - if project.is_local() { - Some( - project - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path()) - .collect::>(), - ) - } else { - None - } - } - - fn update_ssh_paths(&mut self, cx: &App) { + pub fn root_paths(&self, cx: &App) -> Vec> { let project = self.project().read(cx); - if !project.is_local() { - let paths: Vec = project - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string()) - .collect(); - if let Some(ssh_project) = &mut self.serialized_ssh_project { - ssh_project.paths = paths; - } - } - } - - fn serialize_ssh_paths(&mut self, window: &mut Window, cx: &mut Context) { - if self._schedule_serialize_ssh_paths.is_none() { - self._schedule_serialize_ssh_paths = - Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(SERIALIZATION_THROTTLE_TIME) - .await; - this.update_in(cx, |this, window, cx| { - let task = if let Some(ssh_project) = &this.serialized_ssh_project { - let ssh_project_id = ssh_project.id; - let ssh_project_paths = ssh_project.paths.clone(); - window.spawn(cx, async move |_| { - persistence::DB - .update_ssh_project_paths(ssh_project_id, ssh_project_paths) - .await - }) - } else { - Task::ready(Err(anyhow::anyhow!("No SSH project to serialize"))) - }; - task.detach(); - this._schedule_serialize_ssh_paths.take(); - }) - .log_err(); - })); - } + project + .visible_worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path()) + .collect::>() } fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context) { @@ -5313,7 +5247,7 @@ impl Workspace { } match self.serialize_workspace_location(cx) { - WorkspaceLocation::Location(location) => { + WorkspaceLocation::Location(location, paths) => { let breakpoints = self.project.update(cx, |project, cx| { project .breakpoint_store() @@ -5327,6 +5261,7 @@ impl Workspace { let serialized_workspace = SerializedWorkspace { id: database_id, location, + paths, center_group, window_bounds, display: Default::default(), @@ -5352,13 +5287,21 @@ impl Workspace { } fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { - if let Some(ssh_project) = &self.serialized_ssh_project { - WorkspaceLocation::Location(SerializedWorkspaceLocation::Ssh(ssh_project.clone())) - } else if let Some(local_paths) = self.local_paths(cx) { - if !local_paths.is_empty() { - WorkspaceLocation::Location(SerializedWorkspaceLocation::from_local_paths( - local_paths, - )) + let paths = PathList::new(&self.root_paths(cx)); + let connection = self.project.read(cx).ssh_connection_options(cx); + if let Some((id, connection)) = self.serialized_ssh_connection_id.zip(connection) { + WorkspaceLocation::Location( + SerializedWorkspaceLocation::Ssh(SerializedSshConnection { + id, + host: connection.host, + port: connection.port, + user: connection.username, + }), + paths, + ) + } else if self.project.read(cx).is_local() { + if !paths.is_empty() { + WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths) } else { WorkspaceLocation::DetachFromSession } @@ -5371,13 +5314,13 @@ impl Workspace { let Some(id) = self.database_id() else { return; }; - let location = match self.serialize_workspace_location(cx) { - WorkspaceLocation::Location(location) => location, - _ => return, - }; + if !self.project.read(cx).is_local() { + return; + } if let Some(manager) = HistoryManager::global(cx) { + let paths = PathList::new(&self.root_paths(cx)); manager.update(cx, |this, cx| { - this.update_history(id, HistoryManagerEntry::new(id, &location), cx); + this.update_history(id, HistoryManagerEntry::new(id, &paths), cx); }); } } @@ -6843,14 +6786,14 @@ impl WorkspaceHandle for Entity { } } -pub async fn last_opened_workspace_location() -> Option { +pub async fn last_opened_workspace_location() -> Option<(SerializedWorkspaceLocation, PathList)> { DB.last_workspace().await.log_err().flatten() } pub fn last_session_workspace_locations( last_session_id: &str, last_session_window_stack: Option>, -) -> Option> { +) -> Option> { DB.last_session_workspace_locations(last_session_id, last_session_window_stack) .log_err() } @@ -7353,7 +7296,7 @@ pub fn open_ssh_project_with_new_connection( cx: &mut App, ) -> Task> { cx.spawn(async move |cx| { - let (serialized_ssh_project, workspace_id, serialized_workspace) = + let (workspace_id, serialized_workspace) = serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; let session = match cx @@ -7387,7 +7330,6 @@ pub fn open_ssh_project_with_new_connection( open_ssh_project_inner( project, paths, - serialized_ssh_project, workspace_id, serialized_workspace, app_state, @@ -7407,13 +7349,12 @@ pub fn open_ssh_project_with_existing_connection( cx: &mut AsyncApp, ) -> Task> { cx.spawn(async move |cx| { - let (serialized_ssh_project, workspace_id, serialized_workspace) = + let (workspace_id, serialized_workspace) = serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; open_ssh_project_inner( project, paths, - serialized_ssh_project, workspace_id, serialized_workspace, app_state, @@ -7427,7 +7368,6 @@ pub fn open_ssh_project_with_existing_connection( async fn open_ssh_project_inner( project: Entity, paths: Vec, - serialized_ssh_project: SerializedSshProject, workspace_id: WorkspaceId, serialized_workspace: Option, app_state: Arc, @@ -7480,7 +7420,6 @@ async fn open_ssh_project_inner( let mut workspace = Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx); - workspace.set_serialized_ssh_project(serialized_ssh_project); workspace.update_history(cx); if let Some(ref serialized) = serialized_workspace { @@ -7517,28 +7456,18 @@ fn serialize_ssh_project( connection_options: SshConnectionOptions, paths: Vec, cx: &AsyncApp, -) -> Task< - Result<( - SerializedSshProject, - WorkspaceId, - Option, - )>, -> { +) -> Task)>> { cx.background_spawn(async move { - let serialized_ssh_project = persistence::DB - .get_or_create_ssh_project( + let ssh_connection_id = persistence::DB + .get_or_create_ssh_connection( connection_options.host.clone(), connection_options.port, - paths - .iter() - .map(|path| path.to_string_lossy().to_string()) - .collect::>(), connection_options.username.clone(), ) .await?; let serialized_workspace = - persistence::DB.workspace_for_ssh_project(&serialized_ssh_project); + persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id); let workspace_id = if let Some(workspace_id) = serialized_workspace.as_ref().map(|workspace| workspace.id) @@ -7548,7 +7477,7 @@ fn serialize_ssh_project( persistence::DB.next_id().await? }; - Ok((serialized_ssh_project, workspace_id, serialized_workspace)) + Ok((workspace_id, serialized_workspace)) }) } @@ -8095,18 +8024,15 @@ pub fn ssh_workspace_position_from_db( paths_to_open: &[PathBuf], cx: &App, ) -> Task> { - let paths = paths_to_open - .iter() - .map(|path| path.to_string_lossy().to_string()) - .collect::>(); + let paths = paths_to_open.to_vec(); cx.background_spawn(async move { - let serialized_ssh_project = persistence::DB - .get_or_create_ssh_project(host, port, paths, user) + let ssh_connection_id = persistence::DB + .get_or_create_ssh_connection(host, port, user) .await .context("fetching serialized ssh project")?; let serialized_workspace = - persistence::DB.workspace_for_ssh_project(&serialized_ssh_project); + persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id); let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() { (Some(WindowBounds::Windowed(bounds)), None) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b8150a600daeb4d718777aba5c8888c6187583ed..e99c8b564b2cd7915fc352f9caee97b77eccaaf5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -47,8 +47,8 @@ use theme::{ use util::{ResultExt, TryFutureExt, maybe}; use uuid::Uuid; use workspace::{ - AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore, - notifications::NotificationId, + AppState, PathList, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, + WorkspaceStore, notifications::NotificationId, }; use zed::{ OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options, @@ -949,15 +949,14 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp if let Some(locations) = restorable_workspace_locations(cx, &app_state).await { let mut tasks = Vec::new(); - for location in locations { + for (location, paths) in locations { match location { - SerializedWorkspaceLocation::Local(location, _) => { + SerializedWorkspaceLocation::Local => { let app_state = app_state.clone(); - let paths = location.paths().to_vec(); let task = cx.spawn(async move |cx| { let open_task = cx.update(|cx| { workspace::open_paths( - &paths, + &paths.paths(), app_state, workspace::OpenOptions::default(), cx, @@ -979,7 +978,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp match connection_options { Ok(connection_options) => recent_projects::open_ssh_project( connection_options, - ssh.paths.into_iter().map(PathBuf::from).collect(), + paths.paths().into_iter().map(PathBuf::from).collect(), app_state, workspace::OpenOptions::default(), cx, @@ -1070,7 +1069,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp pub(crate) async fn restorable_workspace_locations( cx: &mut AsyncApp, app_state: &Arc, -) -> Option> { +) -> Option> { let mut restore_behavior = cx .update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup) .ok()?; diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 827c7754faaefa1a2baff2ab68d15b38d5c08fdc..2194fb7af5d48577a4316b99418df7dbce0a0375 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -26,6 +26,7 @@ use std::thread; use std::time::Duration; use util::ResultExt; use util::paths::PathWithPosition; +use workspace::PathList; use workspace::item::ItemHandle; use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace}; @@ -361,12 +362,14 @@ async fn open_workspaces( if open_new_workspace == Some(true) { Vec::new() } else { - let locations = restorable_workspace_locations(cx, &app_state).await; - locations.unwrap_or_default() + restorable_workspace_locations(cx, &app_state) + .await + .unwrap_or_default() } } else { - vec![SerializedWorkspaceLocation::from_local_paths( - paths.into_iter().map(PathBuf::from), + vec![( + SerializedWorkspaceLocation::Local, + PathList::new(&paths.into_iter().map(PathBuf::from).collect::>()), )] }; @@ -394,9 +397,9 @@ async fn open_workspaces( // If there are paths to open, open a workspace for each grouping of paths let mut errored = false; - for location in grouped_locations { + for (location, workspace_paths) in grouped_locations { match location { - SerializedWorkspaceLocation::Local(workspace_paths, _) => { + SerializedWorkspaceLocation::Local => { let workspace_paths = workspace_paths .paths() .iter() @@ -429,7 +432,7 @@ async fn open_workspaces( cx.spawn(async move |cx| { open_ssh_project( connection_options, - ssh.paths.into_iter().map(PathBuf::from).collect(), + workspace_paths.paths().to_vec(), app_state, OpenOptions::default(), cx, From e6267c42f70233542f09429337d688cc96ceee90 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 22 Aug 2025 23:28:55 +0200 Subject: [PATCH 287/744] Ensure `pane: swap item right` does not panic (#36765) This fixes a panic I randomly ran into whilst mistyping in the command palette: I accidentally ran `pane: swap item right`in a state where no items were opened in my active pane. We were checking for `index + 1 == self.items.len()` there when it really should be `>=`, as otherwise in the case of no items this panics. This PR fixes the bug, adds a test for both the panic as well as the actions themselves (they were untested previously). Lastly (and mostly), this also cleans up a bit around existing actions to update them with how we generally handle actions now. Release Notes: - Fixed a panic that could occur with the `pane: swap item right` action. --- crates/collab/src/tests/following_tests.rs | 8 +- crates/editor/src/editor_tests.rs | 4 +- crates/search/src/project_search.rs | 6 +- crates/workspace/src/pane.rs | 149 ++++++++++++++------- 4 files changed, 110 insertions(+), 57 deletions(-) diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index d9fd8ffeb2a6c693c3570409070f7a0fbfe33ea2..1e0c915bcbe142fe4f86c54907391c9708e9af7a 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -970,7 +970,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // the follow. workspace_b.update_in(cx_b, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, window, cx); + pane.activate_previous_item(&Default::default(), window, cx); }); }); executor.run_until_parked(); @@ -1073,7 +1073,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // Client A cycles through some tabs. workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, window, cx); + pane.activate_previous_item(&Default::default(), window, cx); }); }); executor.run_until_parked(); @@ -1117,7 +1117,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, window, cx); + pane.activate_previous_item(&Default::default(), window, cx); }); }); executor.run_until_parked(); @@ -1164,7 +1164,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, window, cx); + pane.activate_previous_item(&Default::default(), window, cx); }); }); executor.run_until_parked(); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 03f5da9a20905782e5aec6ce0b3904aabf30d1a3..2cfdb92593e2250a5615eb4d4d545c1552d13ecc 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -22715,7 +22715,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) { .await .unwrap(); pane.update_in(cx, |pane, window, cx| { - pane.navigate_backward(window, cx); + pane.navigate_backward(&Default::default(), window, cx); }); cx.run_until_parked(); pane.update(cx, |pane, cx| { @@ -24302,7 +24302,7 @@ async fn test_document_colors(cx: &mut TestAppContext) { workspace .update(cx, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.navigate_backward(window, cx); + pane.navigate_backward(&Default::default(), window, cx); }) }) .unwrap(); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index c4ba9b5154fc91cc4eaa3f9fbf28682d3f584a87..8ac12588afa2acf75aba1091407bd6f8b83d51ce 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -3905,7 +3905,7 @@ pub mod tests { assert_eq!(workspace.active_pane(), &second_pane); second_pane.update(cx, |this, cx| { assert_eq!(this.active_item_index(), 1); - this.activate_prev_item(false, window, cx); + this.activate_previous_item(&Default::default(), window, cx); assert_eq!(this.active_item_index(), 0); }); workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx); @@ -3940,7 +3940,9 @@ pub mod tests { // Focus the second pane's non-search item window .update(cx, |_workspace, window, cx| { - second_pane.update(cx, |pane, cx| pane.activate_next_item(true, window, cx)); + second_pane.update(cx, |pane, cx| { + pane.activate_next_item(&Default::default(), window, cx) + }); }) .unwrap(); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e88402adc0036b27fd4a79acfaee9d8a5f76f074..fe8014d9f7b8ba8a85d4a9c97f0d99ff2dc669eb 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -514,7 +514,7 @@ impl Pane { } } - fn alternate_file(&mut self, window: &mut Window, cx: &mut Context) { + fn alternate_file(&mut self, _: &AlternateFile, window: &mut Window, cx: &mut Context) { let (_, alternative) = &self.alternate_file_items; if let Some(alternative) = alternative { let existing = self @@ -788,7 +788,7 @@ impl Pane { !self.nav_history.0.lock().forward_stack.is_empty() } - pub fn navigate_backward(&mut self, window: &mut Window, cx: &mut Context) { + pub fn navigate_backward(&mut self, _: &GoBack, window: &mut Window, cx: &mut Context) { if let Some(workspace) = self.workspace.upgrade() { let pane = cx.entity().downgrade(); window.defer(cx, move |window, cx| { @@ -799,7 +799,7 @@ impl Pane { } } - fn navigate_forward(&mut self, window: &mut Window, cx: &mut Context) { + fn navigate_forward(&mut self, _: &GoForward, window: &mut Window, cx: &mut Context) { if let Some(workspace) = self.workspace.upgrade() { let pane = cx.entity().downgrade(); window.defer(cx, move |window, cx| { @@ -1283,9 +1283,9 @@ impl Pane { } } - pub fn activate_prev_item( + pub fn activate_previous_item( &mut self, - activate_pane: bool, + _: &ActivatePreviousItem, window: &mut Window, cx: &mut Context, ) { @@ -1295,12 +1295,12 @@ impl Pane { } else if !self.items.is_empty() { index = self.items.len() - 1; } - self.activate_item(index, activate_pane, activate_pane, window, cx); + self.activate_item(index, true, true, window, cx); } pub fn activate_next_item( &mut self, - activate_pane: bool, + _: &ActivateNextItem, window: &mut Window, cx: &mut Context, ) { @@ -1310,10 +1310,15 @@ impl Pane { } else { index = 0; } - self.activate_item(index, activate_pane, activate_pane, window, cx); + self.activate_item(index, true, true, window, cx); } - pub fn swap_item_left(&mut self, window: &mut Window, cx: &mut Context) { + pub fn swap_item_left( + &mut self, + _: &SwapItemLeft, + window: &mut Window, + cx: &mut Context, + ) { let index = self.active_item_index; if index == 0 { return; @@ -1323,9 +1328,14 @@ impl Pane { self.activate_item(index - 1, true, true, window, cx); } - pub fn swap_item_right(&mut self, window: &mut Window, cx: &mut Context) { + pub fn swap_item_right( + &mut self, + _: &SwapItemRight, + window: &mut Window, + cx: &mut Context, + ) { let index = self.active_item_index; - if index + 1 == self.items.len() { + if index + 1 >= self.items.len() { return; } @@ -1333,6 +1343,16 @@ impl Pane { self.activate_item(index + 1, true, true, window, cx); } + pub fn activate_last_item( + &mut self, + _: &ActivateLastItem, + window: &mut Window, + cx: &mut Context, + ) { + let index = self.items.len().saturating_sub(1); + self.activate_item(index, true, true, window, cx); + } + pub fn close_active_item( &mut self, action: &CloseActiveItem, @@ -2881,7 +2901,9 @@ impl Pane { .on_click({ let entity = cx.entity(); move |_, window, cx| { - entity.update(cx, |pane, cx| pane.navigate_backward(window, cx)) + entity.update(cx, |pane, cx| { + pane.navigate_backward(&Default::default(), window, cx) + }) } }) .disabled(!self.can_navigate_backward()) @@ -2896,7 +2918,11 @@ impl Pane { .icon_size(IconSize::Small) .on_click({ let entity = cx.entity(); - move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx)) + move |_, window, cx| { + entity.update(cx, |pane, cx| { + pane.navigate_forward(&Default::default(), window, cx) + }) + } }) .disabled(!self.can_navigate_forward()) .tooltip({ @@ -3528,9 +3554,6 @@ impl Render for Pane { .size_full() .flex_none() .overflow_hidden() - .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| { - pane.alternate_file(window, cx); - })) .on_action( cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)), ) @@ -3547,12 +3570,6 @@ impl Render for Pane { .on_action( cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)), ) - .on_action( - cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)), - ) - .on_action( - cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)), - ) .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| { cx.emit(Event::JoinIntoNext); })) @@ -3560,6 +3577,8 @@ impl Render for Pane { cx.emit(Event::JoinAll); })) .on_action(cx.listener(Pane::toggle_zoom)) + .on_action(cx.listener(Self::navigate_backward)) + .on_action(cx.listener(Self::navigate_forward)) .on_action( cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| { pane.activate_item( @@ -3571,33 +3590,14 @@ impl Render for Pane { ); }), ) - .on_action( - cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| { - pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx); - }), - ) - .on_action( - cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| { - pane.activate_prev_item(true, window, cx); - }), - ) - .on_action( - cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| { - pane.activate_next_item(true, window, cx); - }), - ) - .on_action( - cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)), - ) - .on_action( - cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)), - ) - .on_action(cx.listener(|pane, action, window, cx| { - pane.toggle_pin_tab(action, window, cx); - })) - .on_action(cx.listener(|pane, action, window, cx| { - pane.unpin_all_tabs(action, window, cx); - })) + .on_action(cx.listener(Self::alternate_file)) + .on_action(cx.listener(Self::activate_last_item)) + .on_action(cx.listener(Self::activate_previous_item)) + .on_action(cx.listener(Self::activate_next_item)) + .on_action(cx.listener(Self::swap_item_left)) + .on_action(cx.listener(Self::swap_item_right)) + .on_action(cx.listener(Self::toggle_pin_tab)) + .on_action(cx.listener(Self::unpin_all_tabs)) .when(PreviewTabsSettings::get_global(cx).enabled, |this| { this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| { if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) { @@ -6452,6 +6452,57 @@ mod tests { .unwrap(); } + #[gpui::test] + async fn test_item_swapping_actions(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, None, cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + assert_item_labels(&pane, [], cx); + + // Test that these actions do not panic + pane.update_in(cx, |pane, window, cx| { + pane.swap_item_right(&Default::default(), window, cx); + }); + + pane.update_in(cx, |pane, window, cx| { + pane.swap_item_left(&Default::default(), window, cx); + }); + + add_labeled_item(&pane, "A", false, cx); + add_labeled_item(&pane, "B", false, cx); + add_labeled_item(&pane, "C", false, cx); + assert_item_labels(&pane, ["A", "B", "C*"], cx); + + pane.update_in(cx, |pane, window, cx| { + pane.swap_item_right(&Default::default(), window, cx); + }); + assert_item_labels(&pane, ["A", "B", "C*"], cx); + + pane.update_in(cx, |pane, window, cx| { + pane.swap_item_left(&Default::default(), window, cx); + }); + assert_item_labels(&pane, ["A", "C*", "B"], cx); + + pane.update_in(cx, |pane, window, cx| { + pane.swap_item_left(&Default::default(), window, cx); + }); + assert_item_labels(&pane, ["C*", "A", "B"], cx); + + pane.update_in(cx, |pane, window, cx| { + pane.swap_item_left(&Default::default(), window, cx); + }); + assert_item_labels(&pane, ["C*", "A", "B"], cx); + + pane.update_in(cx, |pane, window, cx| { + pane.swap_item_right(&Default::default(), window, cx); + }); + assert_item_labels(&pane, ["A", "C*", "B"], cx); + } + fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); From 91b2a84001930c00e41462d87279d8ddc87a3b5b Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 22 Aug 2025 15:17:02 -0700 Subject: [PATCH 288/744] Add a few more testing features (#36778) Release Notes: - N/A --------- Co-authored-by: Marshall --- Procfile.web | 2 ++ crates/client/src/client.rs | 12 ++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 Procfile.web diff --git a/Procfile.web b/Procfile.web new file mode 100644 index 0000000000000000000000000000000000000000..814055514498124d1f20b1fed51f23a5809819a9 --- /dev/null +++ b/Procfile.web @@ -0,0 +1,2 @@ +postgrest_llm: postgrest crates/collab/postgrest_llm.conf +website: cd ../zed.dev; npm run dev -- --port=3000 diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index f9b8a10610fa429e03e1214a4d3e4560af7bec4e..2bbe7dd1b5a838c1f4e3bace2d91c396692983f4 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -66,6 +66,8 @@ pub static IMPERSONATE_LOGIN: LazyLock> = LazyLock::new(|| { .and_then(|s| if s.is_empty() { None } else { Some(s) }) }); +pub static USE_WEB_LOGIN: LazyLock = LazyLock::new(|| std::env::var("ZED_WEB_LOGIN").is_ok()); + pub static ADMIN_API_TOKEN: LazyLock> = LazyLock::new(|| { std::env::var("ZED_ADMIN_API_TOKEN") .ok() @@ -1392,11 +1394,13 @@ impl Client { if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) { - eprintln!("authenticate as admin {login}, {token}"); + if !*USE_WEB_LOGIN { + eprintln!("authenticate as admin {login}, {token}"); - return this - .authenticate_as_admin(http, login.clone(), token.clone()) - .await; + return this + .authenticate_as_admin(http, login.clone(), token.clone()) + .await; + } } // Start an HTTP server to receive the redirect from Zed's sign-in page. From bc566fe18e2e7fe84df7475029ad480561e87d78 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Sat, 23 Aug 2025 00:35:26 +0200 Subject: [PATCH 289/744] agent2: Tweak usage callout border (#36777) Release Notes: - N/A --- .../agent_ui/src/ui/preview/usage_callouts.rs | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/crates/agent_ui/src/ui/preview/usage_callouts.rs b/crates/agent_ui/src/ui/preview/usage_callouts.rs index 29b12ea62772fc21b49526ff0fcd06d053dc9c48..d4d037b9765e5bd20bbcd547f5cc906285d26711 100644 --- a/crates/agent_ui/src/ui/preview/usage_callouts.rs +++ b/crates/agent_ui/src/ui/preview/usage_callouts.rs @@ -86,23 +86,18 @@ impl RenderOnce for UsageCallout { (IconName::Warning, Severity::Warning) }; - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .child( - Callout::new() - .icon(icon) - .severity(severity) - .icon(icon) - .title(title) - .description(message) - .actions_slot( - Button::new("upgrade", button_text) - .label_size(LabelSize::Small) - .on_click(move |_, _, cx| { - cx.open_url(&url); - }), - ), + Callout::new() + .icon(icon) + .severity(severity) + .icon(icon) + .title(title) + .description(message) + .actions_slot( + Button::new("upgrade", button_text) + .label_size(LabelSize::Small) + .on_click(move |_, _, cx| { + cx.open_url(&url); + }), ) .into_any_element() } From 153724aad3709abc8bbbc59d584fe139d4ec801f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Aug 2025 15:44:58 -0700 Subject: [PATCH 290/744] Clean up handling of serialized ssh connection ids (#36781) Small follow-up to #36714 Release Notes: - N/A --- crates/remote/src/ssh_session.rs | 5 - crates/workspace/src/persistence.rs | 166 +++++++++++----------- crates/workspace/src/persistence/model.rs | 7 +- crates/workspace/src/workspace.rs | 12 +- 4 files changed, 93 insertions(+), 97 deletions(-) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index c02d0ad7e776171027dd275d82b5d26eca380a20..b9af5286439e9f757d53062b8c003eb85e69fbee 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -52,11 +52,6 @@ use util::{ paths::{PathStyle, RemotePathBuf}, }; -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, -)] -pub struct SshProjectId(pub u64); - #[derive(Clone)] pub struct SshSocket { connection_options: SshConnectionOptions, diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index de8f63957c73714569016bc647fb8dc670ddedd2..39a1e08c9315e08c2f243ac5b97c1db32bcd639f 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -9,13 +9,13 @@ use std::{ }; use anyhow::{Context as _, Result, bail}; +use collections::HashMap; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; use language::{LanguageName, Toolchain}; use project::WorktreeId; -use remote::ssh_session::SshProjectId; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::{SqlType, Statement}, @@ -33,7 +33,7 @@ use crate::{ use model::{ GroupId, ItemId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, - SerializedSshConnection, SerializedWorkspace, + SerializedSshConnection, SerializedWorkspace, SshConnectionId, }; use self::model::{DockStructure, SerializedWorkspaceLocation}; @@ -615,7 +615,7 @@ impl WorkspaceDb { pub(crate) fn ssh_workspace_for_roots>( &self, worktree_roots: &[P], - ssh_project_id: SshProjectId, + ssh_project_id: SshConnectionId, ) -> Option { self.workspace_for_roots_internal(worktree_roots, Some(ssh_project_id)) } @@ -623,7 +623,7 @@ impl WorkspaceDb { pub(crate) fn workspace_for_roots_internal>( &self, worktree_roots: &[P], - ssh_connection_id: Option, + ssh_connection_id: Option, ) -> Option { // paths are sorted before db interactions to ensure that the order of the paths // doesn't affect the workspace selection for existing workspaces @@ -762,15 +762,21 @@ impl WorkspaceDb { /// that used this workspace previously pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) { let paths = workspace.paths.serialize(); - let ssh_connection_id = match &workspace.location { - SerializedWorkspaceLocation::Local => None, - SerializedWorkspaceLocation::Ssh(serialized_ssh_connection) => { - Some(serialized_ssh_connection.id.0) - } - }; log::debug!("Saving workspace at location: {:?}", workspace.location); self.write(move |conn| { conn.with_savepoint("update_worktrees", || { + let ssh_connection_id = match &workspace.location { + SerializedWorkspaceLocation::Local => None, + SerializedWorkspaceLocation::Ssh(connection) => { + Some(Self::get_or_create_ssh_connection_query( + conn, + connection.host.clone(), + connection.port, + connection.user.clone(), + )?.0) + } + }; + // Clear out panes and pane_groups conn.exec_bound(sql!( DELETE FROM pane_groups WHERE workspace_id = ?1; @@ -893,39 +899,34 @@ impl WorkspaceDb { host: String, port: Option, user: Option, - ) -> Result { - if let Some(id) = self - .get_ssh_connection(host.clone(), port, user.clone()) - .await? + ) -> Result { + self.write(move |conn| Self::get_or_create_ssh_connection_query(conn, host, port, user)) + .await + } + + fn get_or_create_ssh_connection_query( + this: &Connection, + host: String, + port: Option, + user: Option, + ) -> Result { + if let Some(id) = this.select_row_bound(sql!( + SELECT id FROM ssh_connections WHERE host IS ? AND port IS ? AND user IS ? LIMIT 1 + ))?((host.clone(), port, user.clone()))? { - Ok(SshProjectId(id)) + Ok(SshConnectionId(id)) } else { log::debug!("Inserting SSH project at host {host}"); - let id = self - .insert_ssh_connection(host, port, user) - .await? - .context("failed to insert ssh project")?; - Ok(SshProjectId(id)) - } - } - - query! { - async fn get_ssh_connection(host: String, port: Option, user: Option) -> Result> { - SELECT id - FROM ssh_connections - WHERE host IS ? AND port IS ? AND user IS ? - LIMIT 1 - } - } - - query! { - async fn insert_ssh_connection(host: String, port: Option, user: Option) -> Result> { - INSERT INTO ssh_connections ( - host, - port, - user - ) VALUES (?1, ?2, ?3) - RETURNING id + let id = this.select_row_bound(sql!( + INSERT INTO ssh_connections ( + host, + port, + user + ) VALUES (?1, ?2, ?3) + RETURNING id + ))?((host, port, user))? + .context("failed to insert ssh project")?; + Ok(SshConnectionId(id)) } } @@ -963,7 +964,7 @@ impl WorkspaceDb { fn session_workspaces( &self, session_id: String, - ) -> Result, Option)>> { + ) -> Result, Option)>> { Ok(self .session_workspaces_query(session_id)? .into_iter() @@ -971,7 +972,7 @@ impl WorkspaceDb { ( PathList::deserialize(&SerializedPathList { paths, order }), window_id, - ssh_connection_id.map(SshProjectId), + ssh_connection_id.map(SshConnectionId), ) }) .collect()) @@ -1001,15 +1002,15 @@ impl WorkspaceDb { } } - fn ssh_connections(&self) -> Result> { + fn ssh_connections(&self) -> Result> { Ok(self .ssh_connections_query()? .into_iter() - .map(|(id, host, port, user)| SerializedSshConnection { - id: SshProjectId(id), - host, - port, - user, + .map(|(id, host, port, user)| { + ( + SshConnectionId(id), + SerializedSshConnection { host, port, user }, + ) }) .collect()) } @@ -1021,19 +1022,18 @@ impl WorkspaceDb { } } - pub fn ssh_connection(&self, id: SshProjectId) -> Result { + pub(crate) fn ssh_connection(&self, id: SshConnectionId) -> Result { let row = self.ssh_connection_query(id.0)?; Ok(SerializedSshConnection { - id: SshProjectId(row.0), - host: row.1, - port: row.2, - user: row.3, + host: row.0, + port: row.1, + user: row.2, }) } query! { - fn ssh_connection_query(id: u64) -> Result<(u64, String, Option, Option)> { - SELECT id, host, port, user + fn ssh_connection_query(id: u64) -> Result<(String, Option, Option)> { + SELECT host, port, user FROM ssh_connections WHERE id = ? } @@ -1075,10 +1075,8 @@ impl WorkspaceDb { let ssh_connections = self.ssh_connections()?; for (id, paths, ssh_connection_id) in self.recent_workspaces()? { - if let Some(ssh_connection_id) = ssh_connection_id.map(SshProjectId) { - if let Some(ssh_connection) = - ssh_connections.iter().find(|rp| rp.id == ssh_connection_id) - { + if let Some(ssh_connection_id) = ssh_connection_id.map(SshConnectionId) { + if let Some(ssh_connection) = ssh_connections.get(&ssh_connection_id) { result.push(( id, SerializedWorkspaceLocation::Ssh(ssh_connection.clone()), @@ -2340,12 +2338,10 @@ mod tests { ] .into_iter() .map(|(host, user)| async { - let id = db - .get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string())) + db.get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string())) .await .unwrap(); SerializedSshConnection { - id, host: host.into(), port: None, user: Some(user.into()), @@ -2501,26 +2497,34 @@ mod tests { let stored_projects = db.ssh_connections().unwrap(); assert_eq!( stored_projects, - &[ - SerializedSshConnection { - id: ids[0], - host: "example.com".into(), - port: None, - user: None, - }, - SerializedSshConnection { - id: ids[1], - host: "anotherexample.com".into(), - port: Some(123), - user: Some("user2".into()), - }, - SerializedSshConnection { - id: ids[2], - host: "yetanother.com".into(), - port: Some(345), - user: None, - }, + [ + ( + ids[0], + SerializedSshConnection { + host: "example.com".into(), + port: None, + user: None, + } + ), + ( + ids[1], + SerializedSshConnection { + host: "anotherexample.com".into(), + port: Some(123), + user: Some("user2".into()), + } + ), + ( + ids[2], + SerializedSshConnection { + host: "yetanother.com".into(), + port: Some(345), + user: None, + } + ), ] + .into_iter() + .collect::>(), ); } diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index afe4ae62356c83daa25eabc520c4dfc96cfbb1cb..04757d04950ac1ca200096d7b46d04abb18ce8f9 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -12,7 +12,6 @@ use db::sqlez::{ use gpui::{AsyncWindowContext, Entity, WeakEntity}; use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; -use remote::ssh_session::SshProjectId; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeMap, @@ -22,9 +21,13 @@ use std::{ use util::ResultExt; use uuid::Uuid; +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, +)] +pub(crate) struct SshConnectionId(pub u64); + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct SerializedSshConnection { - pub id: SshProjectId, pub host: String, pub port: Option, pub user: Option, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d07ea30cf9ef9dbd74d0bfee0991091de8ce5884..bf58786d677f424eb4c3ea39a9b7bcd79ef46083 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -74,10 +74,7 @@ use project::{ DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId, debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, }; -use remote::{ - SshClientDelegate, SshConnectionOptions, - ssh_session::{ConnectionIdentifier, SshProjectId}, -}; +use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIdentifier}; use schemars::JsonSchema; use serde::Deserialize; use session::AppSession; @@ -1128,7 +1125,6 @@ pub struct Workspace { terminal_provider: Option>, debugger_provider: Option>, serializable_items_tx: UnboundedSender>, - serialized_ssh_connection_id: Option, _items_serializer: Task>, session_id: Option, scheduled_tasks: Vec>, @@ -1461,7 +1457,7 @@ impl Workspace { serializable_items_tx, _items_serializer, session_id: Some(session_id), - serialized_ssh_connection_id: None, + scheduled_tasks: Vec::new(), } } @@ -5288,11 +5284,9 @@ impl Workspace { fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { let paths = PathList::new(&self.root_paths(cx)); - let connection = self.project.read(cx).ssh_connection_options(cx); - if let Some((id, connection)) = self.serialized_ssh_connection_id.zip(connection) { + if let Some(connection) = self.project.read(cx).ssh_connection_options(cx) { WorkspaceLocation::Location( SerializedWorkspaceLocation::Ssh(SerializedSshConnection { - id, host: connection.host, port: connection.port, user: connection.username, From d24cad30f3805f03a4030703701ef77639a028bc Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 23 Aug 2025 01:55:50 +0300 Subject: [PATCH 291/744] Be more lenient when dealing with rust-analyzer's flycheck commands (#36782) Flycheck commands are global and makes sense to fall back to looking up project's rust-analyzer even if the commands are run on a non-rust buffer. If multiple rust-analyzers are found in the project, avoid ambiguous commands and bail (as before). Closes #ISSUE Release Notes: - Made it possible to run rust-analyzer's flycheck actions from anywhere in the project --- crates/diagnostics/src/diagnostics.rs | 4 +- crates/editor/src/rust_analyzer_ext.rs | 35 +++--- crates/project/src/lsp_store.rs | 23 ++-- .../src/lsp_store/rust_analyzer_ext.rs | 108 ++++++++++++------ crates/proto/proto/lsp.proto | 8 +- 5 files changed, 114 insertions(+), 64 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 2e20118381de2e814ef2361280763eeab48606de..037e4fc0fd7d89c12daa26569387eddebf6c0fd1 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -438,7 +438,7 @@ impl ProjectDiagnosticsEditor { for buffer_path in diagnostics_sources.iter().cloned() { if cx .update(|cx| { - fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx)); + fetch_tasks.push(run_flycheck(project.clone(), Some(buffer_path), cx)); }) .is_err() { @@ -462,7 +462,7 @@ impl ProjectDiagnosticsEditor { .iter() .cloned() { - cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx)); + cancel_gasks.push(cancel_flycheck(self.project.clone(), Some(buffer_path), cx)); } self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move { diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index e3d83ab1609907834083f2c6a0be6640ce110f3e..cf74ee0a9eb5f6baaf6b1a1289173bbc3e173719 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -26,6 +26,17 @@ fn is_rust_language(language: &Language) -> bool { } pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: &mut App) { + if editor.read(cx).project().is_some_and(|project| { + project + .read(cx) + .language_server_statuses(cx) + .any(|(_, status)| status.name == RUST_ANALYZER_NAME) + }) { + register_action(editor, window, cancel_flycheck_action); + register_action(editor, window, run_flycheck_action); + register_action(editor, window, clear_flycheck_action); + } + if editor .read(cx) .buffer() @@ -38,9 +49,6 @@ pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: & register_action(editor, window, go_to_parent_module); register_action(editor, window, expand_macro_recursively); register_action(editor, window, open_docs); - register_action(editor, window, cancel_flycheck_action); - register_action(editor, window, run_flycheck_action); - register_action(editor, window, clear_flycheck_action); } } @@ -309,7 +317,7 @@ fn cancel_flycheck_action( let Some(project) = &editor.project else { return; }; - let Some(buffer_id) = editor + let buffer_id = editor .selections .disjoint_anchors() .iter() @@ -321,10 +329,7 @@ fn cancel_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }) - else { - return; - }; + }); cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } @@ -337,7 +342,7 @@ fn run_flycheck_action( let Some(project) = &editor.project else { return; }; - let Some(buffer_id) = editor + let buffer_id = editor .selections .disjoint_anchors() .iter() @@ -349,10 +354,7 @@ fn run_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }) - else { - return; - }; + }); run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } @@ -365,7 +367,7 @@ fn clear_flycheck_action( let Some(project) = &editor.project else { return; }; - let Some(buffer_id) = editor + let buffer_id = editor .selections .disjoint_anchors() .iter() @@ -377,9 +379,6 @@ fn clear_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }) - else { - return; - }; + }); clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index cc3a0a05bbd31c6524b5d63e220e727b13c2f179..fb1fae373635ab30dc737709c26cef0e0393a649 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -9029,13 +9029,22 @@ impl LspStore { lsp_store.update(&mut cx, |lsp_store, cx| { if let Some(server) = lsp_store.language_server_for_id(server_id) { let text_document = if envelope.payload.current_file_only { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - lsp_store - .buffer_store() - .read(cx) - .get(buffer_id) - .and_then(|buffer| Some(buffer.read(cx).file()?.as_local()?.abs_path(cx))) - .map(|path| make_text_document_identifier(&path)) + let buffer_id = envelope + .payload + .buffer_id + .map(|id| BufferId::new(id)) + .transpose()?; + buffer_id + .and_then(|buffer_id| { + lsp_store + .buffer_store() + .read(cx) + .get(buffer_id) + .and_then(|buffer| { + Some(buffer.read(cx).file()?.as_local()?.abs_path(cx)) + }) + .map(|path| make_text_document_identifier(&path)) + }) .transpose()? } else { None diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index e5e6338d3c901752e020c634d822903c8e591657..54f63220b1ef8bab1db22a0808fd2ccb9277b73c 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -1,8 +1,8 @@ use ::serde::{Deserialize, Serialize}; use anyhow::Context as _; -use gpui::{App, Entity, Task, WeakEntity}; -use language::ServerHealth; -use lsp::{LanguageServer, LanguageServerName}; +use gpui::{App, AsyncApp, Entity, Task, WeakEntity}; +use language::{Buffer, ServerHealth}; +use lsp::{LanguageServer, LanguageServerId, LanguageServerName}; use rpc::proto; use crate::{LspStore, LspStoreEvent, Project, ProjectPath, lsp_store}; @@ -83,31 +83,32 @@ pub fn register_notifications(lsp_store: WeakEntity, language_server: pub fn cancel_flycheck( project: Entity, - buffer_path: ProjectPath, + buffer_path: Option, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) + let buffer = buffer_path.map(|buffer_path| { + project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) + }) }) }); cx.spawn(async move |cx| { - let buffer = buffer.await?; - let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { - project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) - })? + let buffer = match buffer { + Some(buffer) => Some(buffer.await?), + None => None, + }; + let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) else { return Ok(()); }; - let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { let request = proto::LspExtCancelFlycheck { project_id, - buffer_id, language_server_id: rust_analyzer_server.to_proto(), }; client @@ -130,28 +131,33 @@ pub fn cancel_flycheck( pub fn run_flycheck( project: Entity, - buffer_path: ProjectPath, + buffer_path: Option, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) + let buffer = buffer_path.map(|buffer_path| { + project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) + }) }) }); cx.spawn(async move |cx| { - let buffer = buffer.await?; - let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { - project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) - })? + let buffer = match buffer { + Some(buffer) => Some(buffer.await?), + None => None, + }; + let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) else { return Ok(()); }; - let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { + let buffer_id = buffer + .map(|buffer| buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())) + .transpose()?; let request = proto::LspExtRunFlycheck { project_id, buffer_id, @@ -182,31 +188,32 @@ pub fn run_flycheck( pub fn clear_flycheck( project: Entity, - buffer_path: ProjectPath, + buffer_path: Option, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) + let buffer = buffer_path.map(|buffer_path| { + project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) + }) }) }); cx.spawn(async move |cx| { - let buffer = buffer.await?; - let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { - project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) - })? + let buffer = match buffer { + Some(buffer) => Some(buffer.await?), + None => None, + }; + let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) else { return Ok(()); }; - let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { let request = proto::LspExtClearFlycheck { project_id, - buffer_id, language_server_id: rust_analyzer_server.to_proto(), }; client @@ -226,3 +233,40 @@ pub fn clear_flycheck( anyhow::Ok(()) }) } + +fn find_rust_analyzer_server( + project: &Entity, + buffer: Option<&Entity>, + cx: &mut AsyncApp, +) -> Option { + project + .read_with(cx, |project, cx| { + buffer + .and_then(|buffer| { + project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) + }) + // If no rust-analyzer found for the current buffer (e.g. `settings.json`), fall back to the project lookup + // and use project's rust-analyzer if it's the only one. + .or_else(|| { + let rust_analyzer_servers = project + .lsp_store() + .read(cx) + .language_server_statuses + .iter() + .filter_map(|(server_id, server_status)| { + if server_status.name == RUST_ANALYZER_NAME { + Some(*server_id) + } else { + None + } + }) + .collect::>(); + if rust_analyzer_servers.len() == 1 { + rust_analyzer_servers.first().copied() + } else { + None + } + }) + }) + .ok()? +} diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index ac9c275aa2d67b3df78fc38d4e88497f9f10e6c9..473ef5c38cc6f401a05556c1f02271e83bd8fa97 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -834,21 +834,19 @@ message LspRunnable { message LspExtCancelFlycheck { uint64 project_id = 1; - uint64 buffer_id = 2; - uint64 language_server_id = 3; + uint64 language_server_id = 2; } message LspExtRunFlycheck { uint64 project_id = 1; - uint64 buffer_id = 2; + optional uint64 buffer_id = 2; uint64 language_server_id = 3; bool current_file_only = 4; } message LspExtClearFlycheck { uint64 project_id = 1; - uint64 buffer_id = 2; - uint64 language_server_id = 3; + uint64 language_server_id = 2; } message LspDiagnosticRelatedInformation { From f48a8f2b6a702fe1051016817097ee2c08ad7e22 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 22 Aug 2025 20:10:26 -0300 Subject: [PATCH 292/744] thread view: Simplify tool call & improve required auth state UIs (#36783) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 145 ++++++++++++++----------- 1 file changed, 82 insertions(+), 63 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2a83a4ab5bbfd63ec1e4c994086f81a45ffa9629..0e1d4123b9ffc01e386804e85cb4bd75e1e49180 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1668,39 +1668,14 @@ impl AcpThreadView { let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix)); let card_header_id = SharedString::from("inner-tool-call-header"); - let status_icon = match &tool_call.status { - ToolCallStatus::Pending - | ToolCallStatus::WaitingForConfirmation { .. } - | ToolCallStatus::Completed => None, - ToolCallStatus::InProgress => Some( - div() - .absolute() - .right_2() - .child( - Icon::new(IconName::ArrowCircle) - .color(Color::Muted) - .size(IconSize::Small) - .with_animation( - "running", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ), - ) - .into_any(), - ), - ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => Some( - div() - .absolute() - .right_2() - .child( - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small), - ) - .into_any_element(), - ), + let in_progress = match &tool_call.status { + ToolCallStatus::InProgress => true, + _ => false, + }; + + let failed_or_canceled = match &tool_call.status { + ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true, + _ => false, }; let failed_tool_call = matches!( @@ -1884,7 +1859,33 @@ impl AcpThreadView { .into_any() }), ) - .children(status_icon), + .when(in_progress && use_card_layout, |this| { + this.child( + div().absolute().right_2().child( + Icon::new(IconName::ArrowCircle) + .color(Color::Muted) + .size(IconSize::Small) + .with_animation( + "running", + Animation::new(Duration::from_secs(3)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage( + delta, + ))) + }, + ), + ), + ) + }) + .when(failed_or_canceled, |this| { + this.child( + div().absolute().right_2().child( + Icon::new(IconName::Close) + .color(Color::Error) + .size(IconSize::Small), + ), + ) + }), ) .children(tool_output_display) } @@ -2579,11 +2580,15 @@ impl AcpThreadView { window: &mut Window, cx: &Context, ) -> Div { + let show_description = + configuration_view.is_none() && description.is_none() && pending_auth_method.is_none(); + v_flex().flex_1().size_full().justify_end().child( v_flex() .p_2() .pr_3() .w_full() + .gap_1() .border_t_1() .border_color(cx.theme().colors().border) .bg(cx.theme().status().warning.opacity(0.04)) @@ -2595,7 +2600,7 @@ impl AcpThreadView { .color(Color::Warning) .size(IconSize::Small), ) - .child(Label::new("Authentication Required")), + .child(Label::new("Authentication Required").size(LabelSize::Small)), ) .children(description.map(|desc| { div().text_ui(cx).child(self.render_markdown( @@ -2609,44 +2614,20 @@ impl AcpThreadView { .map(|view| div().w_full().child(view)), ) .when( - configuration_view.is_none() - && description.is_none() - && pending_auth_method.is_none(), + show_description, |el| { el.child( Label::new(format!( "You are not currently authenticated with {}. Please choose one of the following options:", self.agent.name() )) + .size(LabelSize::Small) .color(Color::Muted) .mb_1() .ml_5(), ) }, ) - .when(!connection.auth_methods().is_empty(), |this| { - this.child( - h_flex().justify_end().flex_wrap().gap_1().children( - connection.auth_methods().iter().enumerate().rev().map( - |(ix, method)| { - Button::new( - SharedString::from(method.id.0.clone()), - method.name.clone(), - ) - .when(ix == 0, |el| { - el.style(ButtonStyle::Tinted(ui::TintColor::Warning)) - }) - .on_click({ - let method_id = method.id.clone(); - cx.listener(move |this, _, window, cx| { - this.authenticate(method_id.clone(), window, cx) - }) - }) - }, - ), - ), - ) - }) .when_some(pending_auth_method, |el, _| { el.child( h_flex() @@ -2669,9 +2650,47 @@ impl AcpThreadView { ) .into_any_element(), ) - .child(Label::new("Authenticating…")), + .child(Label::new("Authenticating…").size(LabelSize::Small)), ) - }), + }) + .when(!connection.auth_methods().is_empty(), |this| { + this.child( + h_flex() + .justify_end() + .flex_wrap() + .gap_1() + .when(!show_description, |this| { + this.border_t_1() + .mt_1() + .pt_2() + .border_color(cx.theme().colors().border.opacity(0.8)) + }) + .children( + connection + .auth_methods() + .iter() + .enumerate() + .rev() + .map(|(ix, method)| { + Button::new( + SharedString::from(method.id.0.clone()), + method.name.clone(), + ) + .when(ix == 0, |el| { + el.style(ButtonStyle::Tinted(ui::TintColor::Warning)) + }) + .label_size(LabelSize::Small) + .on_click({ + let method_id = method.id.clone(); + cx.listener(move |this, _, window, cx| { + this.authenticate(method_id.clone(), window, cx) + }) + }) + }), + ), + ) + }) + ) } From 5da31fdb725d41f62900a8317e0919d30fa54f15 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 22 Aug 2025 22:09:08 -0600 Subject: [PATCH 293/744] acp: Remove ACP v0 (#36785) We had a few people confused about why some features weren't working due to the fallback logic. It's gone. Release Notes: - N/A --- Cargo.lock | 61 +---- Cargo.toml | 1 - crates/agent_servers/Cargo.toml | 1 - crates/agent_servers/src/acp.rs | 389 ++++++++++++++++++++++++++++-- tooling/workspace-hack/Cargo.toml | 2 - 5 files changed, 382 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4043666823790cfc8f9e495fc15975992c0fac3d..aa3a910390f4d74d135de602c8ec8c1f51f76e4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -289,7 +289,6 @@ dependencies = [ "action_log", "agent-client-protocol", "agent_settings", - "agentic-coding-protocol", "anyhow", "client", "collections", @@ -443,24 +442,6 @@ dependencies = [ "zed_actions", ] -[[package]] -name = "agentic-coding-protocol" -version = "0.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4" -dependencies = [ - "anyhow", - "chrono", - "derive_more 2.0.1", - "futures 0.3.31", - "log", - "parking_lot", - "schemars", - "semver", - "serde", - "serde_json", -] - [[package]] name = "ahash" version = "0.7.8" @@ -876,7 +857,7 @@ dependencies = [ "anyhow", "async-trait", "collections", - "derive_more 0.99.19", + "derive_more", "extension", "futures 0.3.31", "gpui", @@ -939,7 +920,7 @@ dependencies = [ "clock", "collections", "ctor", - "derive_more 0.99.19", + "derive_more", "gpui", "icons", "indoc", @@ -976,7 +957,7 @@ dependencies = [ "cloud_llm_client", "collections", "component", - "derive_more 0.99.19", + "derive_more", "diffy", "editor", "feature_flags", @@ -3089,7 +3070,7 @@ dependencies = [ "cocoa 0.26.0", "collections", "credentials_provider", - "derive_more 0.99.19", + "derive_more", "feature_flags", "fs", "futures 0.3.31", @@ -3521,7 +3502,7 @@ name = "command_palette_hooks" version = "0.1.0" dependencies = [ "collections", - "derive_more 0.99.19", + "derive_more", "gpui", "workspace-hack", ] @@ -4684,27 +4665,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", - "unicode-xid", -] - [[package]] name = "derive_refineable" version = "0.1.0" @@ -6441,7 +6401,7 @@ dependencies = [ "askpass", "async-trait", "collections", - "derive_more 0.99.19", + "derive_more", "futures 0.3.31", "git2", "gpui", @@ -7471,7 +7431,7 @@ dependencies = [ "core-video", "cosmic-text", "ctor", - "derive_more 0.99.19", + "derive_more", "embed-resource", "env_logger 0.11.8", "etagere", @@ -7996,7 +7956,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes 1.10.1", - "derive_more 0.99.19", + "derive_more", "futures 0.3.31", "http 1.3.1", "http-body 1.0.1", @@ -14399,12 +14359,10 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984" dependencies = [ - "chrono", "dyn-clone", "indexmap", "ref-cast", "schemars_derive", - "semver", "serde", "serde_json", ] @@ -16488,7 +16446,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "derive_more 0.99.19", + "derive_more", "fs", "futures 0.3.31", "gpui", @@ -20003,7 +19961,6 @@ dependencies = [ "rustix 1.0.7", "rustls 0.23.26", "rustls-webpki 0.103.1", - "schemars", "scopeguard", "sea-orm", "sea-query-binder", diff --git a/Cargo.toml b/Cargo.toml index 7668d1875249938b249bfcef360a0a90d9421a77..6ec243a9b9de4d2ab322e0466e804fa542a1ed35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -426,7 +426,6 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agentic-coding-protocol = "0.0.10" agent-client-protocol = "0.0.31" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 8ea4a27f4cc85e0c19401e86a825eda394c6a34a..9f90f3a78aed825c372cc8bffc67d194b7ec2027 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -22,7 +22,6 @@ acp_thread.workspace = true action_log.workspace = true agent-client-protocol.workspace = true agent_settings.workspace = true -agentic-coding-protocol.workspace = true anyhow.workspace = true client = { workspace = true, optional = true } collections.workspace = true diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 1cfb1fcabf3a0a4cc009ab0912188437aef17c2c..a99a4014316c8fce6404289c8125840674be6b91 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -1,34 +1,391 @@ -use std::{path::Path, rc::Rc}; - use crate::AgentServerCommand; use acp_thread::AgentConnection; -use anyhow::Result; -use gpui::AsyncApp; +use acp_tools::AcpConnectionRegistry; +use action_log::ActionLog; +use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; +use anyhow::anyhow; +use collections::HashMap; +use futures::AsyncBufReadExt as _; +use futures::channel::oneshot; +use futures::io::BufReader; +use project::Project; +use serde::Deserialize; +use std::{any::Any, cell::RefCell}; +use std::{path::Path, rc::Rc}; use thiserror::Error; -mod v0; -mod v1; +use anyhow::{Context as _, Result}; +use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; + +use acp_thread::{AcpThread, AuthRequired, LoadError}; #[derive(Debug, Error)] #[error("Unsupported version")] pub struct UnsupportedVersion; +pub struct AcpConnection { + server_name: &'static str, + connection: Rc, + sessions: Rc>>, + auth_methods: Vec, + prompt_capabilities: acp::PromptCapabilities, + _io_task: Task>, +} + +pub struct AcpSession { + thread: WeakEntity, + suppress_abort_err: bool, +} + 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) + let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?; + Ok(Rc::new(conn) as _) +} + +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::piped()) + .kill_on_drop(true) + .spawn()?; + + let stdout = child.stdout.take().context("Failed to take stdout")?; + let stdin = child.stdin.take().context("Failed to take stdin")?; + let stderr = child.stderr.take().context("Failed to take stderr")?; + log::trace!("Spawned (pid: {})", child.id()); + + 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); + + cx.background_spawn(async move { + let mut stderr = BufReader::new(stderr); + let mut line = String::new(); + while let Ok(n) = stderr.read_line(&mut line).await + && n > 0 + { + log::warn!("agent stderr: {}", &line); + line.clear(); + } + }) + .detach(); + + cx.spawn({ + let sessions = sessions.clone(); + async move |cx| { + let status = child.status().await?; + + for session in sessions.borrow().values() { + session + .thread + .update(cx, |thread, cx| { + thread.emit_load_error(LoadError::Exited { status }, cx) + }) + .ok(); + } + + anyhow::Ok(()) + } + }) + .detach(); + + let connection = Rc::new(connection); + + cx.update(|cx| { + AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { + registry.set_active_connection(server_name, &connection, cx) + }); + })?; + + 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, + server_name, + sessions, + prompt_capabilities: response.agent_capabilities.prompt_capabilities, + _io_task: io_task, + }) + } +} + +impl AgentConnection for AcpConnection { + fn new_thread( + self: Rc, + project: Entity, + cwd: &Path, + cx: &mut App, + ) -> 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 + .map_err(|err| { + if err.code == acp::ErrorCode::AUTH_REQUIRED.code { + let mut error = AuthRequired::new(); + + if err.message != acp::ErrorCode::AUTH_REQUIRED.message { + error = error.with_description(err.message); + } + + anyhow!(error) + } else { + anyhow!(err) + } + })?; + + let session_id = response.session_id; + let action_log = cx.new(|_| ActionLog::new(project.clone()))?; + let thread = cx.new(|_cx| { + AcpThread::new( + self.server_name, + self.clone(), + project, + action_log, + session_id.clone(), + ) + })?; + + let session = AcpSession { + thread: thread.downgrade(), + suppress_abort_err: false, + }; + 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, + _id: Option, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { + let conn = self.connection.clone(); + let sessions = self.sessions.clone(); + let session_id = params.session_id.clone(); + cx.foreground_executor().spawn(async move { + let result = conn.prompt(params).await; + + let mut suppress_abort_err = false; + + if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { + suppress_abort_err = session.suppress_abort_err; + session.suppress_abort_err = false; + } + + match result { + Ok(response) => Ok(response), + Err(err) => { + if err.code != ErrorCode::INTERNAL_ERROR.code { + anyhow::bail!(err) + } + + let Some(data) = &err.data else { + anyhow::bail!(err) + }; + + // Temporary workaround until the following PR is generally available: + // https://github.com/google-gemini/gemini-cli/pull/6656 + + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct ErrorDetails { + details: Box, + } + + match serde_json::from_value(data.clone()) { + Ok(ErrorDetails { details }) => { + if suppress_abort_err && details.contains("This operation was aborted") + { + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Cancelled, + }) + } else { + Err(anyhow!(details)) + } + } + Err(_) => Err(anyhow!(err)), + } + } + } + }) + } + + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + self.prompt_capabilities + } + + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { + session.suppress_abort_err = true; } - Err(err) => Err(err), + let conn = self.connection.clone(); + let params = acp::CancelNotification { + session_id: session_id.clone(), + }; + cx.foreground_executor() + .spawn(async move { conn.cancel(params).await }) + .detach(); + } + + fn into_any(self: Rc) -> Rc { + self + } +} + +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_authorization(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/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index bf44fc195e0cdcd8a9440306fb2b1053db8a596a..2f9a963abc8b09d2255a5229dd2e44e06b2e8c9f 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -109,7 +109,6 @@ rustc-hash = { version = "1" } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] } rustls = { version = "0.23", features = ["ring"] } rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] } -schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] } sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] } sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] } semver = { version = "1", features = ["serde"] } @@ -244,7 +243,6 @@ rustc-hash = { version = "1" } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] } rustls = { version = "0.23", features = ["ring"] } rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] } -schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] } sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] } sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] } semver = { version = "1", features = ["serde"] } From ea42013746f1533a49c32c0a6a5d6b84920f85b2 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sat, 23 Aug 2025 01:21:20 -0400 Subject: [PATCH 294/744] acp: Eagerly load all kinds of mentions (#36741) This PR makes it so that all kinds of @-mentions start loading their context as soon as they are confirmed. Previously, we were waiting to load the context for file, symbol, selection, and rule mentions until the user's message was sent. By kicking off loading immediately for these kinds of context, we can support adding selections from unsaved buffers, and we make the semantics of @-mentions more consistent. Loading all kinds of context eagerly also makes it possible to simplify the structure of the MentionSet and the code around it. Now MentionSet is just a single hash map, all the management of creases happens in a uniform way in `MessageEditor::confirm_completion`, and the helper methods for loading different kinds of context are much more focused and orthogonal. Release Notes: - N/A --------- Co-authored-by: Conrad --- crates/acp_thread/src/mention.rs | 154 +- crates/agent/src/thread_store.rs | 13 +- crates/agent2/src/db.rs | 13 +- crates/agent2/src/thread.rs | 54 +- .../agent_ui/src/acp/completion_provider.rs | 4 +- crates/agent_ui/src/acp/message_editor.rs | 1250 +++++++---------- crates/agent_ui/src/acp/thread_view.rs | 46 +- 7 files changed, 698 insertions(+), 836 deletions(-) diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index a1e713cffa051a0eef58a45fceafc8876cab3311..6fa0887e2278467dae9887516d882da90a78d0df 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -5,7 +5,7 @@ use prompt_store::{PromptId, UserPromptId}; use serde::{Deserialize, Serialize}; use std::{ fmt, - ops::Range, + ops::RangeInclusive, path::{Path, PathBuf}, str::FromStr, }; @@ -17,13 +17,14 @@ pub enum MentionUri { File { abs_path: PathBuf, }, + PastedImage, Directory { abs_path: PathBuf, }, Symbol { - path: PathBuf, + abs_path: PathBuf, name: String, - line_range: Range, + line_range: RangeInclusive, }, Thread { id: acp::SessionId, @@ -38,8 +39,9 @@ pub enum MentionUri { name: String, }, Selection { - path: PathBuf, - line_range: Range, + #[serde(default, skip_serializing_if = "Option::is_none")] + abs_path: Option, + line_range: RangeInclusive, }, Fetch { url: Url, @@ -48,36 +50,44 @@ pub enum MentionUri { impl MentionUri { pub fn parse(input: &str) -> Result { + fn parse_line_range(fragment: &str) -> Result> { + let range = fragment + .strip_prefix("L") + .context("Line range must start with \"L\"")?; + let (start, end) = range + .split_once(":") + .context("Line range must use colon as separator")?; + let range = start + .parse::() + .context("Parsing line range start")? + .checked_sub(1) + .context("Line numbers should be 1-based")? + ..=end + .parse::() + .context("Parsing line range end")? + .checked_sub(1) + .context("Line numbers should be 1-based")?; + Ok(range) + } + let url = url::Url::parse(input)?; let path = url.path(); match url.scheme() { "file" => { let path = url.to_file_path().ok().context("Extracting file path")?; if let Some(fragment) = url.fragment() { - let range = fragment - .strip_prefix("L") - .context("Line range must start with \"L\"")?; - let (start, end) = range - .split_once(":") - .context("Line range must use colon as separator")?; - let line_range = start - .parse::() - .context("Parsing line range start")? - .checked_sub(1) - .context("Line numbers should be 1-based")? - ..end - .parse::() - .context("Parsing line range end")? - .checked_sub(1) - .context("Line numbers should be 1-based")?; + let line_range = parse_line_range(fragment)?; if let Some(name) = single_query_param(&url, "symbol")? { Ok(Self::Symbol { name, - path, + abs_path: path, line_range, }) } else { - Ok(Self::Selection { path, line_range }) + Ok(Self::Selection { + abs_path: Some(path), + line_range, + }) } } else if input.ends_with("/") { Ok(Self::Directory { abs_path: path }) @@ -105,6 +115,17 @@ impl MentionUri { id: rule_id.into(), name, }) + } else if path.starts_with("/agent/pasted-image") { + Ok(Self::PastedImage) + } else if path.starts_with("/agent/untitled-buffer") { + let fragment = url + .fragment() + .context("Missing fragment for untitled buffer selection")?; + let line_range = parse_line_range(fragment)?; + Ok(Self::Selection { + abs_path: None, + line_range, + }) } else { bail!("invalid zed url: {:?}", input); } @@ -121,13 +142,16 @@ impl MentionUri { .unwrap_or_default() .to_string_lossy() .into_owned(), + MentionUri::PastedImage => "Image".to_string(), MentionUri::Symbol { name, .. } => name.clone(), MentionUri::Thread { name, .. } => name.clone(), MentionUri::TextThread { name, .. } => name.clone(), MentionUri::Rule { name, .. } => name.clone(), MentionUri::Selection { - path, line_range, .. - } => selection_name(path, line_range), + abs_path: path, + line_range, + .. + } => selection_name(path.as_deref(), line_range), MentionUri::Fetch { url } => url.to_string(), } } @@ -137,6 +161,7 @@ impl MentionUri { MentionUri::File { abs_path } => { FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into()) } + MentionUri::PastedImage => IconName::Image.path().into(), MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx) .unwrap_or_else(|| IconName::Folder.path().into()), MentionUri::Symbol { .. } => IconName::Code.path().into(), @@ -157,29 +182,40 @@ impl MentionUri { MentionUri::File { abs_path } => { Url::from_file_path(abs_path).expect("mention path should be absolute") } + MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(), MentionUri::Directory { abs_path } => { Url::from_directory_path(abs_path).expect("mention path should be absolute") } MentionUri::Symbol { - path, + abs_path, name, line_range, } => { - let mut url = Url::from_file_path(path).expect("mention path should be absolute"); + let mut url = + Url::from_file_path(abs_path).expect("mention path should be absolute"); url.query_pairs_mut().append_pair("symbol", name); url.set_fragment(Some(&format!( "L{}:{}", - line_range.start + 1, - line_range.end + 1 + line_range.start() + 1, + line_range.end() + 1 ))); url } - MentionUri::Selection { path, line_range } => { - let mut url = Url::from_file_path(path).expect("mention path should be absolute"); + MentionUri::Selection { + abs_path: path, + line_range, + } => { + let mut url = if let Some(path) = path { + Url::from_file_path(path).expect("mention path should be absolute") + } else { + let mut url = Url::parse("zed:///").unwrap(); + url.set_path("/agent/untitled-buffer"); + url + }; url.set_fragment(Some(&format!( "L{}:{}", - line_range.start + 1, - line_range.end + 1 + line_range.start() + 1, + line_range.end() + 1 ))); url } @@ -191,7 +227,10 @@ impl MentionUri { } MentionUri::TextThread { path, name } => { let mut url = Url::parse("zed:///").unwrap(); - url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy())); + url.set_path(&format!( + "/agent/text-thread/{}", + path.to_string_lossy().trim_start_matches('/') + )); url.query_pairs_mut().append_pair("name", name); url } @@ -237,12 +276,14 @@ fn single_query_param(url: &Url, name: &'static str) -> Result> { } } -pub fn selection_name(path: &Path, line_range: &Range) -> String { +pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive) -> String { format!( "{} ({}:{})", - path.file_name().unwrap_or_default().display(), - line_range.start + 1, - line_range.end + 1 + path.and_then(|path| path.file_name()) + .unwrap_or("Untitled".as_ref()) + .display(), + *line_range.start() + 1, + *line_range.end() + 1 ) } @@ -302,14 +343,14 @@ mod tests { let parsed = MentionUri::parse(symbol_uri).unwrap(); match &parsed { MentionUri::Symbol { - path, + abs_path: path, name, line_range, } => { assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs")); assert_eq!(name, "MySymbol"); - assert_eq!(line_range.start, 9); - assert_eq!(line_range.end, 19); + assert_eq!(line_range.start(), &9); + assert_eq!(line_range.end(), &19); } _ => panic!("Expected Symbol variant"), } @@ -321,16 +362,39 @@ mod tests { let selection_uri = uri!("file:///path/to/file.rs#L5:15"); let parsed = MentionUri::parse(selection_uri).unwrap(); match &parsed { - MentionUri::Selection { path, line_range } => { - assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs")); - assert_eq!(line_range.start, 4); - assert_eq!(line_range.end, 14); + MentionUri::Selection { + abs_path: path, + line_range, + } => { + assert_eq!( + path.as_ref().unwrap().to_str().unwrap(), + path!("/path/to/file.rs") + ); + assert_eq!(line_range.start(), &4); + assert_eq!(line_range.end(), &14); } _ => panic!("Expected Selection variant"), } assert_eq!(parsed.to_uri().to_string(), selection_uri); } + #[test] + fn test_parse_untitled_selection_uri() { + let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10"); + let parsed = MentionUri::parse(selection_uri).unwrap(); + match &parsed { + MentionUri::Selection { + abs_path: None, + line_range, + } => { + assert_eq!(line_range.start(), &0); + assert_eq!(line_range.end(), &9); + } + _ => panic!("Expected Selection variant without path"), + } + assert_eq!(parsed.to_uri().to_string(), selection_uri); + } + #[test] fn test_parse_thread_uri() { let thread_uri = "zed:///agent/thread/session123?name=Thread+name"; diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 45e551dbdf01425f8ecd454e2808d1ea1cf94c51..cba2457566709d5664270a8239495aaac3fec6fb 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -893,8 +893,19 @@ impl ThreadsDatabase { let needs_migration_from_heed = mdb_path.exists(); - let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { + let connection = if *ZED_STATELESS { Connection::open_memory(Some("THREAD_FALLBACK_DB")) + } else if cfg!(any(feature = "test-support", test)) { + // rust stores the name of the test on the current thread. + // We use this to automatically create a database that will + // be shared within the test (for the test_retrieve_old_thread) + // but not with concurrent tests. + let thread = std::thread::current(); + let test_name = thread.name(); + Connection::open_memory(Some(&format!( + "THREAD_FALLBACK_{}", + test_name.unwrap_or_default() + ))) } else { Connection::open_file(&sqlite_path.to_string_lossy()) }; diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index 1b88955a241945996be911a337035a970c8b4026..e7d31c0c7ac4dd2327931e2d888ec29f6ca96e73 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent2/src/db.rs @@ -266,8 +266,19 @@ impl ThreadsDatabase { } pub fn new(executor: BackgroundExecutor) -> Result { - let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { + let connection = if *ZED_STATELESS { Connection::open_memory(Some("THREAD_FALLBACK_DB")) + } else if cfg!(any(feature = "test-support", test)) { + // rust stores the name of the test on the current thread. + // We use this to automatically create a database that will + // be shared within the test (for the test_retrieve_old_thread) + // but not with concurrent tests. + let thread = std::thread::current(); + let test_name = thread.name(); + Connection::open_memory(Some(&format!( + "THREAD_FALLBACK_{}", + test_name.unwrap_or_default() + ))) } else { let threads_dir = paths::data_dir().join("threads"); std::fs::create_dir_all(&threads_dir)?; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index c89e5875f98534b7b13b0d9e5da905b481c82c9b..6d616f73fc4a7a961c30dd317fe1c01e48ec90e6 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -45,14 +45,15 @@ use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; use settings::{Settings, update_settings_file}; use smol::stream::StreamExt; +use std::fmt::Write; use std::{ collections::BTreeMap, + ops::RangeInclusive, path::Path, sync::Arc, time::{Duration, Instant}, }; -use std::{fmt::Write, ops::Range}; -use util::{ResultExt, markdown::MarkdownCodeBlock}; +use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock}; use uuid::Uuid; const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user"; @@ -187,6 +188,7 @@ impl UserMessage { const OPEN_FILES_TAG: &str = ""; const OPEN_DIRECTORIES_TAG: &str = ""; const OPEN_SYMBOLS_TAG: &str = ""; + const OPEN_SELECTIONS_TAG: &str = ""; const OPEN_THREADS_TAG: &str = ""; const OPEN_FETCH_TAG: &str = ""; const OPEN_RULES_TAG: &str = @@ -195,6 +197,7 @@ impl UserMessage { let mut file_context = OPEN_FILES_TAG.to_string(); let mut directory_context = OPEN_DIRECTORIES_TAG.to_string(); let mut symbol_context = OPEN_SYMBOLS_TAG.to_string(); + let mut selection_context = OPEN_SELECTIONS_TAG.to_string(); let mut thread_context = OPEN_THREADS_TAG.to_string(); let mut fetch_context = OPEN_FETCH_TAG.to_string(); let mut rules_context = OPEN_RULES_TAG.to_string(); @@ -211,7 +214,7 @@ impl UserMessage { match uri { MentionUri::File { abs_path } => { write!( - &mut symbol_context, + &mut file_context, "\n{}", MarkdownCodeBlock { tag: &codeblock_tag(abs_path, None), @@ -220,17 +223,19 @@ impl UserMessage { ) .ok(); } + MentionUri::PastedImage => { + debug_panic!("pasted image URI should not be used in mention content") + } MentionUri::Directory { .. } => { write!(&mut directory_context, "\n{}\n", content).ok(); } MentionUri::Symbol { - path, line_range, .. - } - | MentionUri::Selection { - path, line_range, .. + abs_path: path, + line_range, + .. } => { write!( - &mut rules_context, + &mut symbol_context, "\n{}", MarkdownCodeBlock { tag: &codeblock_tag(path, Some(line_range)), @@ -239,6 +244,24 @@ impl UserMessage { ) .ok(); } + MentionUri::Selection { + abs_path: path, + line_range, + .. + } => { + write!( + &mut selection_context, + "\n{}", + MarkdownCodeBlock { + tag: &codeblock_tag( + path.as_deref().unwrap_or("Untitled".as_ref()), + Some(line_range) + ), + text: content + } + ) + .ok(); + } MentionUri::Thread { .. } => { write!(&mut thread_context, "\n{}\n", content).ok(); } @@ -291,6 +314,13 @@ impl UserMessage { .push(language_model::MessageContent::Text(symbol_context)); } + if selection_context.len() > OPEN_SELECTIONS_TAG.len() { + selection_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(selection_context)); + } + if thread_context.len() > OPEN_THREADS_TAG.len() { thread_context.push_str("\n"); message @@ -326,7 +356,7 @@ impl UserMessage { } } -fn codeblock_tag(full_path: &Path, line_range: Option<&Range>) -> String { +fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive>) -> String { let mut result = String::new(); if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) { @@ -336,10 +366,10 @@ fn codeblock_tag(full_path: &Path, line_range: Option<&Range>) -> String { let _ = write!(result, "{}", full_path.display()); if let Some(range) = line_range { - if range.start == range.end { - let _ = write!(result, ":{}", range.start + 1); + if range.start() == range.end() { + let _ = write!(result, ":{}", range.start() + 1); } else { - let _ = write!(result, ":{}-{}", range.start + 1, range.end + 1); + let _ = write!(result, ":{}-{}", range.start() + 1, range.end() + 1); } } diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 22a9ea677334a63999be2c258f691cef23d34ac1..5b4096706981f11e8340e1017a7955676865eb90 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -247,9 +247,9 @@ impl ContextPickerCompletionProvider { let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?; let uri = MentionUri::Symbol { - path: abs_path, + abs_path, name: symbol.name.clone(), - line_range: symbol.range.start.0.row..symbol.range.end.0.row, + line_range: symbol.range.start.0.row..=symbol.range.end.0.row, }; let new_text = format!("{} ", uri.as_link()); let new_text_len = new_text.len(); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 7d73ebeb197605b5a0c078fa3bc5b7a283d18641..115008cf5298963895306a3dc0fd6e0a7345f1b8 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -6,7 +6,7 @@ use acp_thread::{MentionUri, selection_name}; use agent_client_protocol as acp; use agent_servers::AgentServer; use agent2::HistoryStore; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Result, anyhow}; use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ @@ -17,8 +17,8 @@ use editor::{ display_map::{Crease, CreaseId, FoldId}, }; use futures::{ - FutureExt as _, TryFutureExt as _, - future::{Shared, join_all, try_join_all}, + FutureExt as _, + future::{Shared, join_all}, }; use gpui::{ AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, @@ -28,14 +28,14 @@ use gpui::{ use language::{Buffer, Language}; use language_model::LanguageModelImage; use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree}; -use prompt_store::PromptStore; +use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::Settings; use std::{ cell::Cell, ffi::OsStr, fmt::Write, - ops::Range, + ops::{Range, RangeInclusive}, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -49,12 +49,8 @@ use ui::{ Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div, h_flex, px, }; -use url::Url; -use util::ResultExt; -use workspace::{ - Toast, Workspace, - notifications::{NotificationId, NotifyResultExt as _}, -}; +use util::{ResultExt, debug_panic}; +use workspace::{Workspace, notifications::NotifyResultExt as _}; use zed_actions::agent::Chat; const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50); @@ -219,9 +215,9 @@ impl MessageEditor { pub fn mentions(&self) -> HashSet { self.mention_set - .uri_by_crease_id + .mentions .values() - .cloned() + .map(|(uri, _)| uri.clone()) .collect() } @@ -246,132 +242,168 @@ impl MessageEditor { else { return Task::ready(()); }; + let end_anchor = snapshot + .buffer_snapshot + .anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1); - if let MentionUri::File { abs_path, .. } = &mention_uri { - let extension = abs_path - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default(); - - if Img::extensions().contains(&extension) && !extension.contains("svg") { - if !self.prompt_capabilities.get().image { - struct ImagesNotAllowed; + let crease_id = if let MentionUri::File { abs_path } = &mention_uri + && let Some(extension) = abs_path.extension() + && let Some(extension) = extension.to_str() + && Img::extensions().contains(&extension) + && !extension.contains("svg") + { + let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + log::error!("project path not found"); + return Task::ready(()); + }; + let image = self + .project + .update(cx, |project, cx| project.open_image(project_path, cx)); + let image = cx + .spawn(async move |_, cx| { + let image = image.await.map_err(|e| e.to_string())?; + let image = image + .update(cx, |image, _| image.image.clone()) + .map_err(|e| e.to_string())?; + Ok(image) + }) + .shared(); + insert_crease_for_image( + *excerpt_id, + start, + content_len, + Some(abs_path.as_path().into()), + image, + self.editor.clone(), + window, + cx, + ) + } else { + crate::context_picker::insert_crease_for_mention( + *excerpt_id, + start, + content_len, + crease_text, + mention_uri.icon_path(cx), + self.editor.clone(), + window, + cx, + ) + }; + let Some(crease_id) = crease_id else { + return Task::ready(()); + }; - let end_anchor = snapshot.buffer_snapshot.anchor_before( - start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1, - ); + let task = match mention_uri.clone() { + MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx), + MentionUri::Directory { abs_path } => self.confirm_mention_for_directory(abs_path, cx), + MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx), + MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx), + MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx), + MentionUri::Symbol { + abs_path, + line_range, + .. + } => self.confirm_mention_for_symbol(abs_path, line_range, cx), + MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx), + MentionUri::PastedImage => { + debug_panic!("pasted image URI should not be included in completions"); + Task::ready(Err(anyhow!( + "pasted imaged URI should not be included in completions" + ))) + } + MentionUri::Selection { .. } => { + // Handled elsewhere + debug_panic!("unexpected selection URI"); + Task::ready(Err(anyhow!("unexpected selection URI"))) + } + }; + let task = cx + .spawn(async move |_, _| task.await.map_err(|e| e.to_string())) + .shared(); + self.mention_set + .mentions + .insert(crease_id, (mention_uri, task.clone())); - self.editor.update(cx, |editor, cx| { + // Notify the user if we failed to load the mentioned context + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_none() { + this.update(cx, |this, cx| { + this.editor.update(cx, |editor, cx| { // Remove mention - editor.edit([((start_anchor..end_anchor), "")], cx); + editor.edit([(start_anchor..end_anchor, "")], cx); }); - - self.workspace - .update(cx, |workspace, cx| { - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - "This agent does not support images yet", - ) - .autohide(), - cx, - ); - }) - .ok(); - return Task::ready(()); - } - - let project = self.project.clone(); - let Some(project_path) = project - .read(cx) - .project_path_for_absolute_path(abs_path, cx) - else { - return Task::ready(()); - }; - let image = cx - .spawn(async move |_, cx| { - let image = project - .update(cx, |project, cx| project.open_image(project_path, cx)) - .map_err(|e| e.to_string())? - .await - .map_err(|e| e.to_string())?; - image - .read_with(cx, |image, _cx| image.image.clone()) - .map_err(|e| e.to_string()) - }) - .shared(); - let Some(crease_id) = insert_crease_for_image( - *excerpt_id, - start, - content_len, - Some(abs_path.as_path().into()), - image.clone(), - self.editor.clone(), - window, - cx, - ) else { - return Task::ready(()); - }; - return self.confirm_mention_for_image( - crease_id, - start_anchor, - Some(abs_path.clone()), - image, - window, - cx, - ); + this.mention_set.mentions.remove(&crease_id); + }) + .ok(); } - } + }) + } - let Some(crease_id) = crate::context_picker::insert_crease_for_mention( - *excerpt_id, - start, - content_len, - crease_text, - mention_uri.icon_path(cx), - self.editor.clone(), - window, - cx, - ) else { - return Task::ready(()); + fn confirm_mention_for_file( + &mut self, + abs_path: PathBuf, + cx: &mut Context, + ) -> Task> { + let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return Task::ready(Err(anyhow!("project path not found"))); }; + let extension = abs_path + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); - match mention_uri { - MentionUri::Fetch { url } => { - self.confirm_mention_for_fetch(crease_id, start_anchor, url, window, cx) - } - MentionUri::Directory { abs_path } => { - self.confirm_mention_for_directory(crease_id, start_anchor, abs_path, window, cx) - } - MentionUri::Thread { id, name } => { - self.confirm_mention_for_thread(crease_id, start_anchor, id, name, window, cx) - } - MentionUri::TextThread { path, name } => self.confirm_mention_for_text_thread( - crease_id, - start_anchor, - path, - name, - window, - cx, - ), - MentionUri::File { .. } - | MentionUri::Symbol { .. } - | MentionUri::Rule { .. } - | MentionUri::Selection { .. } => { - self.mention_set.insert_uri(crease_id, mention_uri.clone()); - Task::ready(()) + if Img::extensions().contains(&extension) && !extension.contains("svg") { + if !self.prompt_capabilities.get().image { + return Task::ready(Err(anyhow!("This agent does not support images yet"))); } + let task = self + .project + .update(cx, |project, cx| project.open_image(project_path, cx)); + return cx.spawn(async move |_, cx| { + let image = task.await?; + let image = image.update(cx, |image, _| image.image.clone())?; + let format = image.format; + let image = cx + .update(|cx| LanguageModelImage::from_image(image, cx))? + .await; + if let Some(image) = image { + Ok(Mention::Image(MentionImage { + data: image.source, + format, + })) + } else { + Err(anyhow!("Failed to convert image")) + } + }); } + + let buffer = self + .project + .update(cx, |project, cx| project.open_buffer(project_path, cx)); + cx.spawn(async move |_, cx| { + let buffer = buffer.await?; + let mention = buffer.update(cx, |buffer, cx| Mention::Text { + content: buffer.text(), + tracked_buffers: vec![cx.entity()], + })?; + anyhow::Ok(mention) + }) } fn confirm_mention_for_directory( &mut self, - crease_id: CreaseId, - anchor: Anchor, abs_path: PathBuf, - window: &mut Window, cx: &mut Context, - ) -> Task<()> { + ) -> Task> { fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc, PathBuf)> { let mut files = Vec::new(); @@ -386,24 +418,21 @@ impl MessageEditor { files } - let uri = MentionUri::Directory { - abs_path: abs_path.clone(), - }; let Some(project_path) = self .project .read(cx) .project_path_for_absolute_path(&abs_path, cx) else { - return Task::ready(()); + return Task::ready(Err(anyhow!("project path not found"))); }; let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else { - return Task::ready(()); + return Task::ready(Err(anyhow!("project entry not found"))); }; let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else { - return Task::ready(()); + return Task::ready(Err(anyhow!("worktree not found"))); }; let project = self.project.clone(); - let task = cx.spawn(async move |_, cx| { + cx.spawn(async move |_, cx| { let directory_path = entry.path.clone(); let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?; @@ -453,89 +482,83 @@ impl MessageEditor { ((rel_path, full_path, rope), buffer) }) .unzip(); - (render_directory_contents(contents), tracked_buffers) + Mention::Text { + content: render_directory_contents(contents), + tracked_buffers, + } }) .await; anyhow::Ok(contents) - }); - let task = cx - .spawn(async move |_, _| task.await.map_err(|e| e.to_string())) - .shared(); - - self.mention_set - .directories - .insert(abs_path.clone(), task.clone()); - - let editor = self.editor.clone(); - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - this.update(cx, |this, _| { - this.mention_set.insert_uri(crease_id, uri); - }) - .ok(); - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - this.update(cx, |this, _cx| { - this.mention_set.directories.remove(&abs_path); - }) - .ok(); - } }) } fn confirm_mention_for_fetch( &mut self, - crease_id: CreaseId, - anchor: Anchor, url: url::Url, - window: &mut Window, cx: &mut Context, - ) -> Task<()> { - let Some(http_client) = self + ) -> Task> { + let http_client = match self .workspace - .update(cx, |workspace, _cx| workspace.client().http_client()) - .ok() - else { - return Task::ready(()); + .update(cx, |workspace, _| workspace.client().http_client()) + { + Ok(http_client) => http_client, + Err(e) => return Task::ready(Err(e)), }; - - let url_string = url.to_string(); - let fetch = cx - .background_executor() - .spawn(async move { - fetch_url_content(http_client, url_string) - .map_err(|e| e.to_string()) - .await + cx.background_executor().spawn(async move { + let content = fetch_url_content(http_client, url.to_string()).await?; + Ok(Mention::Text { + content, + tracked_buffers: Vec::new(), }) - .shared(); - self.mention_set - .add_fetch_result(url.clone(), fetch.clone()); + }) + } - cx.spawn_in(window, async move |this, cx| { - let fetch = fetch.await.notify_async_err(cx); - this.update(cx, |this, cx| { - if fetch.is_some() { - this.mention_set - .insert_uri(crease_id, MentionUri::Fetch { url }); - } else { - // Remove crease if we failed to fetch - this.editor.update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }); - this.mention_set.fetch_results.remove(&url); + fn confirm_mention_for_symbol( + &mut self, + abs_path: PathBuf, + line_range: RangeInclusive, + cx: &mut Context, + ) -> Task> { + let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return Task::ready(Err(anyhow!("project path not found"))); + }; + let buffer = self + .project + .update(cx, |project, cx| project.open_buffer(project_path, cx)); + cx.spawn(async move |_, cx| { + let buffer = buffer.await?; + let mention = buffer.update(cx, |buffer, cx| { + let start = Point::new(*line_range.start(), 0).min(buffer.max_point()); + let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point()); + let content = buffer.text_for_range(start..end).collect(); + Mention::Text { + content, + tracked_buffers: vec![cx.entity()], } + })?; + anyhow::Ok(mention) + }) + } + + fn confirm_mention_for_rule( + &mut self, + id: PromptId, + cx: &mut Context, + ) -> Task> { + let Some(prompt_store) = self.prompt_store.clone() else { + return Task::ready(Err(anyhow!("missing prompt store"))); + }; + let prompt = prompt_store.read(cx).load(id, cx); + cx.spawn(async move |_, _| { + let prompt = prompt.await?; + Ok(Mention::Text { + content: prompt, + tracked_buffers: Vec::new(), }) - .ok(); }) } @@ -560,24 +583,24 @@ impl MessageEditor { let range = snapshot.anchor_after(offset + range_to_fold.start) ..snapshot.anchor_after(offset + range_to_fold.end); - // TODO support selections from buffers with no path - let Some(project_path) = buffer.read(cx).project_path(cx) else { - continue; - }; - let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { - continue; - }; + let abs_path = buffer + .read(cx) + .project_path(cx) + .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx)); let snapshot = buffer.read(cx).snapshot(); + let text = snapshot + .text_for_range(selection_range.clone()) + .collect::(); let point_range = selection_range.to_point(&snapshot); - let line_range = point_range.start.row..point_range.end.row; + let line_range = point_range.start.row..=point_range.end.row; let uri = MentionUri::Selection { - path: abs_path.clone(), + abs_path: abs_path.clone(), line_range: line_range.clone(), }; let crease = crate::context_picker::crease_for_mention( - selection_name(&abs_path, &line_range).into(), + selection_name(abs_path.as_deref(), &line_range).into(), uri.icon_path(cx), range, self.editor.downgrade(), @@ -589,132 +612,69 @@ impl MessageEditor { crease_ids.first().copied().unwrap() }); - self.mention_set.insert_uri(crease_id, uri); + self.mention_set.mentions.insert( + crease_id, + ( + uri, + Task::ready(Ok(Mention::Text { + content: text, + tracked_buffers: vec![buffer], + })) + .shared(), + ), + ); } } fn confirm_mention_for_thread( &mut self, - crease_id: CreaseId, - anchor: Anchor, id: acp::SessionId, - name: String, - window: &mut Window, cx: &mut Context, - ) -> Task<()> { - let uri = MentionUri::Thread { - id: id.clone(), - name, - }; + ) -> Task> { let server = Rc::new(agent2::NativeAgentServer::new( self.project.read(cx).fs().clone(), self.history_store.clone(), )); let connection = server.connect(Path::new(""), &self.project, cx); - let load_summary = cx.spawn({ - let id = id.clone(); - async move |_, cx| { - let agent = connection.await?; - let agent = agent.downcast::().unwrap(); - let summary = agent - .0 - .update(cx, |agent, cx| agent.thread_summary(id, cx))? - .await?; - anyhow::Ok(summary) - } - }); - let task = cx - .spawn(async move |_, _| load_summary.await.map_err(|e| format!("{e}"))) - .shared(); - - self.mention_set.insert_thread(id.clone(), task.clone()); - self.mention_set.insert_uri(crease_id, uri); - - let editor = self.editor.clone(); - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_none() { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - this.update(cx, |this, _| { - this.mention_set.thread_summaries.remove(&id); - this.mention_set.uri_by_crease_id.remove(&crease_id); - }) - .ok(); - } + cx.spawn(async move |_, cx| { + let agent = connection.await?; + let agent = agent.downcast::().unwrap(); + let summary = agent + .0 + .update(cx, |agent, cx| agent.thread_summary(id, cx))? + .await?; + anyhow::Ok(Mention::Text { + content: summary.to_string(), + tracked_buffers: Vec::new(), + }) }) } fn confirm_mention_for_text_thread( &mut self, - crease_id: CreaseId, - anchor: Anchor, path: PathBuf, - name: String, - window: &mut Window, cx: &mut Context, - ) -> Task<()> { - let uri = MentionUri::TextThread { - path: path.clone(), - name, - }; + ) -> Task> { let context = self.history_store.update(cx, |text_thread_store, cx| { text_thread_store.load_text_thread(path.as_path().into(), cx) }); - let task = cx - .spawn(async move |_, cx| { - let context = context.await.map_err(|e| e.to_string())?; - let xml = context - .update(cx, |context, cx| context.to_xml(cx)) - .map_err(|e| e.to_string())?; - Ok(xml) + cx.spawn(async move |_, cx| { + let context = context.await?; + let xml = context.update(cx, |context, cx| context.to_xml(cx))?; + Ok(Mention::Text { + content: xml, + tracked_buffers: Vec::new(), }) - .shared(); - - self.mention_set - .insert_text_thread(path.clone(), task.clone()); - - let editor = self.editor.clone(); - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - this.update(cx, |this, _| { - this.mention_set.insert_uri(crease_id, uri); - }) - .ok(); - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - this.update(cx, |this, _| { - this.mention_set.text_thread_summaries.remove(&path); - }) - .ok(); - } }) } pub fn contents( &self, - window: &mut Window, cx: &mut Context, ) -> Task, Vec>)>> { - let contents = self.mention_set.contents( - &self.project, - self.prompt_store.as_ref(), - &self.prompt_capabilities.get(), - window, - cx, - ); + let contents = self + .mention_set + .contents(&self.prompt_capabilities.get(), cx); let editor = self.editor.clone(); let prevent_slash_commands = self.prevent_slash_commands; @@ -729,7 +689,7 @@ impl MessageEditor { editor.display_map.update(cx, |map, cx| { let snapshot = map.snapshot(cx); for (crease_id, crease) in snapshot.crease_snapshot.creases() { - let Some(mention) = contents.get(&crease_id) else { + let Some((uri, mention)) = contents.get(&crease_id) else { continue; }; @@ -747,7 +707,6 @@ impl MessageEditor { } let chunk = match mention { Mention::Text { - uri, content, tracked_buffers, } => { @@ -764,17 +723,25 @@ impl MessageEditor { }) } Mention::Image(mention_image) => { + let uri = match uri { + MentionUri::File { .. } => Some(uri.to_uri().to_string()), + MentionUri::PastedImage => None, + other => { + debug_panic!( + "unexpected mention uri for image: {:?}", + other + ); + None + } + }; acp::ContentBlock::Image(acp::ImageContent { annotations: None, data: mention_image.data.to_string(), mime_type: mention_image.format.mime_type().into(), - uri: mention_image - .abs_path - .as_ref() - .map(|path| format!("file://{}", path.display())), + uri, }) } - Mention::UriOnly(uri) => { + Mention::UriOnly => { acp::ContentBlock::ResourceLink(acp::ResourceLink { name: uri.name(), uri: uri.to_uri().to_string(), @@ -813,7 +780,13 @@ impl MessageEditor { pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.clear(window, cx); - editor.remove_creases(self.mention_set.drain(), cx) + editor.remove_creases( + self.mention_set + .mentions + .drain() + .map(|(crease_id, _)| crease_id), + cx, + ) }); } @@ -853,7 +826,7 @@ impl MessageEditor { } cx.stop_propagation(); - let replacement_text = "image"; + let replacement_text = MentionUri::PastedImage.as_link().to_string(); for image in images { let (excerpt_id, text_anchor, multibuffer_anchor) = self.editor.update(cx, |message_editor, cx| { @@ -876,24 +849,62 @@ impl MessageEditor { }); let content_len = replacement_text.len(); - let Some(anchor) = multibuffer_anchor else { - return; + let Some(start_anchor) = multibuffer_anchor else { + continue; }; - let task = Task::ready(Ok(Arc::new(image))).shared(); + let end_anchor = self.editor.update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len) + }); + let image = Arc::new(image); let Some(crease_id) = insert_crease_for_image( excerpt_id, text_anchor, content_len, None.clone(), - task.clone(), + Task::ready(Ok(image.clone())).shared(), self.editor.clone(), window, cx, ) else { - return; + continue; }; - self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx) - .detach(); + let task = cx + .spawn_in(window, { + async move |_, cx| { + let format = image.format; + let image = cx + .update(|_, cx| LanguageModelImage::from_image(image, cx)) + .map_err(|e| e.to_string())? + .await; + if let Some(image) = image { + Ok(Mention::Image(MentionImage { + data: image.source, + format, + })) + } else { + Err("Failed to convert image".into()) + } + } + }) + .shared(); + + self.mention_set + .mentions + .insert(crease_id, (MentionUri::PastedImage, task.clone())); + + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_none() { + this.update(cx, |this, cx| { + this.editor.update(cx, |editor, cx| { + editor.edit([(start_anchor..end_anchor, "")], cx); + }); + this.mention_set.mentions.remove(&crease_id); + }) + .ok(); + } + }) + .detach(); } } @@ -995,67 +1006,6 @@ impl MessageEditor { }) } - fn confirm_mention_for_image( - &mut self, - crease_id: CreaseId, - anchor: Anchor, - abs_path: Option, - image: Shared, String>>>, - window: &mut Window, - cx: &mut Context, - ) -> Task<()> { - let editor = self.editor.clone(); - let task = cx - .spawn_in(window, { - let abs_path = abs_path.clone(); - async move |_, cx| { - let image = image.await?; - let format = image.format; - let image = cx - .update(|_, cx| LanguageModelImage::from_image(image, cx)) - .map_err(|e| e.to_string())? - .await; - if let Some(image) = image { - Ok(MentionImage { - abs_path, - data: image.source, - format, - }) - } else { - Err("Failed to convert image".into()) - } - } - }) - .shared(); - - self.mention_set.insert_image(crease_id, task.clone()); - - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - if let Some(abs_path) = abs_path.clone() { - this.update(cx, |this, _cx| { - this.mention_set - .insert_uri(crease_id, MentionUri::File { abs_path }); - }) - .ok(); - } - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - this.update(cx, |this, _cx| { - this.mention_set.images.remove(&crease_id); - }) - .ok(); - } - }) - } - pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.set_mode(mode); @@ -1073,7 +1023,6 @@ impl MessageEditor { let mut text = String::new(); let mut mentions = Vec::new(); - let mut images = Vec::new(); for chunk in message { match chunk { @@ -1084,26 +1033,58 @@ impl MessageEditor { resource: acp::EmbeddedResourceResource::TextResourceContents(resource), .. }) => { - if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() { - let start = text.len(); - write!(&mut text, "{}", mention_uri.as_link()).ok(); - let end = text.len(); - mentions.push((start..end, mention_uri, resource.text)); - } + let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() else { + continue; + }; + let start = text.len(); + write!(&mut text, "{}", mention_uri.as_link()).ok(); + let end = text.len(); + mentions.push(( + start..end, + mention_uri, + Mention::Text { + content: resource.text, + tracked_buffers: Vec::new(), + }, + )); } acp::ContentBlock::ResourceLink(resource) => { if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() { let start = text.len(); write!(&mut text, "{}", mention_uri.as_link()).ok(); let end = text.len(); - mentions.push((start..end, mention_uri, resource.uri)); + mentions.push((start..end, mention_uri, Mention::UriOnly)); } } - acp::ContentBlock::Image(content) => { + acp::ContentBlock::Image(acp::ImageContent { + uri, + data, + mime_type, + annotations: _, + }) => { + let mention_uri = if let Some(uri) = uri { + MentionUri::parse(&uri) + } else { + Ok(MentionUri::PastedImage) + }; + let Some(mention_uri) = mention_uri.log_err() else { + continue; + }; + let Some(format) = ImageFormat::from_mime_type(&mime_type) else { + log::error!("failed to parse MIME type for image: {mime_type:?}"); + continue; + }; let start = text.len(); - text.push_str("image"); + write!(&mut text, "{}", mention_uri.as_link()).ok(); let end = text.len(); - images.push((start..end, content)); + mentions.push(( + start..end, + mention_uri, + Mention::Image(MentionImage { + data: data.into(), + format, + }), + )); } acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {} } @@ -1114,9 +1095,9 @@ impl MessageEditor { editor.buffer().read(cx).snapshot(cx) }); - for (range, mention_uri, text) in mentions { + for (range, mention_uri, mention) in mentions { let anchor = snapshot.anchor_before(range.start); - let crease_id = crate::context_picker::insert_crease_for_mention( + let Some(crease_id) = crate::context_picker::insert_crease_for_mention( anchor.excerpt_id, anchor.text_anchor, range.end - range.start, @@ -1125,77 +1106,14 @@ impl MessageEditor { self.editor.clone(), window, cx, - ); - - if let Some(crease_id) = crease_id { - self.mention_set.insert_uri(crease_id, mention_uri.clone()); - } - - match mention_uri { - MentionUri::Thread { id, .. } => { - self.mention_set - .insert_thread(id, Task::ready(Ok(text.into())).shared()); - } - MentionUri::TextThread { path, .. } => { - self.mention_set - .insert_text_thread(path, Task::ready(Ok(text)).shared()); - } - MentionUri::Fetch { url } => { - self.mention_set - .add_fetch_result(url, Task::ready(Ok(text)).shared()); - } - MentionUri::Directory { abs_path } => { - let task = Task::ready(Ok((text, Vec::new()))).shared(); - self.mention_set.directories.insert(abs_path, task); - } - MentionUri::File { .. } - | MentionUri::Symbol { .. } - | MentionUri::Rule { .. } - | MentionUri::Selection { .. } => {} - } - } - for (range, content) in images { - let Some(format) = ImageFormat::from_mime_type(&content.mime_type) else { + ) else { continue; }; - let anchor = snapshot.anchor_before(range.start); - let abs_path = content - .uri - .as_ref() - .and_then(|uri| uri.strip_prefix("file://").map(|s| Path::new(s).into())); - - let name = content - .uri - .as_ref() - .and_then(|uri| { - uri.strip_prefix("file://") - .and_then(|path| Path::new(path).file_name()) - }) - .map(|name| name.to_string_lossy().to_string()) - .unwrap_or("Image".to_owned()); - let crease_id = crate::context_picker::insert_crease_for_mention( - anchor.excerpt_id, - anchor.text_anchor, - range.end - range.start, - name.into(), - IconName::Image.path().into(), - self.editor.clone(), - window, - cx, - ); - let data: SharedString = content.data.to_string().into(); - if let Some(crease_id) = crease_id { - self.mention_set.insert_image( - crease_id, - Task::ready(Ok(MentionImage { - abs_path, - data, - format, - })) - .shared(), - ); - } + self.mention_set.mentions.insert( + crease_id, + (mention_uri.clone(), Task::ready(Ok(mention)).shared()), + ); } cx.notify(); } @@ -1425,289 +1343,60 @@ impl Render for ImageHover { } } -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub enum Mention { Text { - uri: MentionUri, content: String, tracked_buffers: Vec>, }, Image(MentionImage), - UriOnly(MentionUri), + UriOnly, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct MentionImage { - pub abs_path: Option, pub data: SharedString, pub format: ImageFormat, } #[derive(Default)] pub struct MentionSet { - uri_by_crease_id: HashMap, - fetch_results: HashMap>>>, - images: HashMap>>>, - thread_summaries: HashMap>>>, - text_thread_summaries: HashMap>>>, - directories: HashMap>), String>>>>, + mentions: HashMap>>)>, } impl MentionSet { - pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) { - self.uri_by_crease_id.insert(crease_id, uri); - } - - pub fn add_fetch_result(&mut self, url: Url, content: Shared>>) { - self.fetch_results.insert(url, content); - } - - pub fn insert_image( - &mut self, - crease_id: CreaseId, - task: Shared>>, - ) { - self.images.insert(crease_id, task); - } - - fn insert_thread( - &mut self, - id: acp::SessionId, - task: Shared>>, - ) { - self.thread_summaries.insert(id, task); - } - - fn insert_text_thread(&mut self, path: PathBuf, task: Shared>>) { - self.text_thread_summaries.insert(path, task); - } - - pub fn contents( + fn contents( &self, - project: &Entity, - prompt_store: Option<&Entity>, prompt_capabilities: &acp::PromptCapabilities, - _window: &mut Window, cx: &mut App, - ) -> Task>> { + ) -> Task>> { if !prompt_capabilities.embedded_context { let mentions = self - .uri_by_crease_id + .mentions .iter() - .map(|(crease_id, uri)| (*crease_id, Mention::UriOnly(uri.clone()))) + .map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly))) .collect(); return Task::ready(Ok(mentions)); } - let mut processed_image_creases = HashSet::default(); - - let mut contents = self - .uri_by_crease_id - .iter() - .map(|(&crease_id, uri)| { - match uri { - MentionUri::File { abs_path, .. } => { - let uri = uri.clone(); - let abs_path = abs_path.to_path_buf(); - - if let Some(task) = self.images.get(&crease_id).cloned() { - processed_image_creases.insert(crease_id); - return cx.spawn(async move |_| { - let image = task.await.map_err(|e| anyhow!("{e}"))?; - anyhow::Ok((crease_id, Mention::Image(image))) - }); - } - - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(abs_path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; - - anyhow::Ok(( - crease_id, - Mention::Text { - uri, - content, - tracked_buffers: vec![buffer], - }, - )) - }) - } - MentionUri::Directory { abs_path } => { - let Some(content) = self.directories.get(abs_path).cloned() else { - return Task::ready(Err(anyhow!("missing directory load task"))); - }; - let uri = uri.clone(); - cx.spawn(async move |_| { - let (content, tracked_buffers) = - content.await.map_err(|e| anyhow::anyhow!("{e}"))?; - Ok(( - crease_id, - Mention::Text { - uri, - content, - tracked_buffers, - }, - )) - }) - } - MentionUri::Symbol { - path, line_range, .. - } - | MentionUri::Selection { - path, line_range, .. - } => { - let uri = uri.clone(); - let path_buf = path.clone(); - let line_range = line_range.clone(); - - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(&path_buf, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| { - buffer - .text_for_range( - Point::new(line_range.start, 0) - ..Point::new( - line_range.end, - buffer.line_len(line_range.end), - ), - ) - .collect() - })?; - - anyhow::Ok(( - crease_id, - Mention::Text { - uri, - content, - tracked_buffers: vec![buffer], - }, - )) - }) - } - MentionUri::Thread { id, .. } => { - let Some(content) = self.thread_summaries.get(id).cloned() else { - return Task::ready(Err(anyhow!("missing thread summary"))); - }; - let uri = uri.clone(); - cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Text { - uri, - content: content - .await - .map_err(|e| anyhow::anyhow!("{e}"))? - .to_string(), - tracked_buffers: Vec::new(), - }, - )) - }) - } - MentionUri::TextThread { path, .. } => { - let Some(content) = self.text_thread_summaries.get(path).cloned() else { - return Task::ready(Err(anyhow!("missing text thread summary"))); - }; - let uri = uri.clone(); - cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Text { - uri, - content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, - tracked_buffers: Vec::new(), - }, - )) - }) - } - MentionUri::Rule { id: prompt_id, .. } => { - let Some(prompt_store) = prompt_store else { - return Task::ready(Err(anyhow!("missing prompt store"))); - }; - let text_task = prompt_store.read(cx).load(*prompt_id, cx); - let uri = uri.clone(); - cx.spawn(async move |_| { - // TODO: report load errors instead of just logging - let text = text_task.await?; - anyhow::Ok(( - crease_id, - Mention::Text { - uri, - content: text, - tracked_buffers: Vec::new(), - }, - )) - }) - } - MentionUri::Fetch { url } => { - let Some(content) = self.fetch_results.get(url).cloned() else { - return Task::ready(Err(anyhow!("missing fetch result"))); - }; - let uri = uri.clone(); - cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Text { - uri, - content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, - tracked_buffers: Vec::new(), - }, - )) - }) - } - } - }) - .collect::>(); - - // Handle images that didn't have a mention URI (because they were added by the paste handler). - contents.extend(self.images.iter().filter_map(|(crease_id, image)| { - if processed_image_creases.contains(crease_id) { - return None; - } - let crease_id = *crease_id; - let image = image.clone(); - Some(cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?), - )) - })) - })); - + let mentions = self.mentions.clone(); cx.spawn(async move |_cx| { - let contents = try_join_all(contents).await?.into_iter().collect(); - anyhow::Ok(contents) + let mut contents = HashMap::default(); + for (crease_id, (mention_uri, task)) in mentions { + contents.insert( + crease_id, + (mention_uri, task.await.map_err(|e| anyhow!("{e}"))?), + ); + } + Ok(contents) }) } - pub fn drain(&mut self) -> impl Iterator { - self.fetch_results.clear(); - self.thread_summaries.clear(); - self.text_thread_summaries.clear(); - self.directories.clear(); - self.uri_by_crease_id - .drain() - .map(|(id, _)| id) - .chain(self.images.drain().map(|(id, _)| id)) - } - - pub fn remove_invalid(&mut self, snapshot: EditorSnapshot) { + fn remove_invalid(&mut self, snapshot: EditorSnapshot) { for (crease_id, crease) in snapshot.crease_snapshot.creases() { if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { - self.uri_by_crease_id.remove(&crease_id); + self.mentions.remove(&crease_id); } } } @@ -1969,9 +1658,7 @@ mod tests { }); let (content, _) = message_editor - .update_in(cx, |message_editor, window, cx| { - message_editor.contents(window, cx) - }) + .update(cx, |message_editor, cx| message_editor.contents(cx)) .await .unwrap(); @@ -2038,7 +1725,8 @@ mod tests { "six.txt": "6", "seven.txt": "7", "eight.txt": "8", - } + }, + "x.png": "", }), ) .await; @@ -2222,14 +1910,10 @@ mod tests { }; let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - &project, - None, - &all_prompt_capabilities, - window, - cx, - ) + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&all_prompt_capabilities, cx) }) .await .unwrap() @@ -2237,7 +1921,7 @@ mod tests { .collect::>(); { - let [Mention::Text { content, uri, .. }] = contents.as_slice() else { + let [(uri, Mention::Text { content, .. })] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(content, "1"); @@ -2245,14 +1929,10 @@ mod tests { } let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - &project, - None, - &acp::PromptCapabilities::default(), - window, - cx, - ) + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&acp::PromptCapabilities::default(), cx) }) .await .unwrap() @@ -2260,7 +1940,7 @@ mod tests { .collect::>(); { - let [Mention::UriOnly(uri)] = contents.as_slice() else { + let [(uri, Mention::UriOnly)] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(uri, &url_one.parse::().unwrap()); @@ -2300,14 +1980,10 @@ mod tests { cx.run_until_parked(); let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - &project, - None, - &all_prompt_capabilities, - window, - cx, - ) + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&all_prompt_capabilities, cx) }) .await .unwrap() @@ -2317,7 +1993,7 @@ mod tests { let url_eight = uri!("file:///dir/b/eight.txt"); { - let [_, Mention::Text { content, uri, .. }] = contents.as_slice() else { + let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(content, "8"); @@ -2414,14 +2090,10 @@ mod tests { }); let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - &project, - None, - &all_prompt_capabilities, - window, - cx, - ) + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&all_prompt_capabilities, cx) }) .await .unwrap() @@ -2429,7 +2101,7 @@ mod tests { .collect::>(); { - let [_, _, Mention::Text { content, uri, .. }] = contents.as_slice() else { + let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(content, "1"); @@ -2444,11 +2116,85 @@ mod tests { cx.run_until_parked(); editor.read_with(&cx, |editor, cx| { - assert_eq!( - editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") - ); - }); + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") + ); + }); + + // Try to mention an "image" file that will fail to load + cx.simulate_input("@file x.png"); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png") + ); + assert!(editor.has_visible_completions_menu()); + assert_eq!(current_completion_labels(editor), &["x.png dir/"]); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + // Getting the message contents fails + message_editor + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&all_prompt_capabilities, cx) + }) + .await + .expect_err("Should fail to load x.png"); + + cx.run_until_parked(); + + // Mention was removed + editor.read_with(&cx, |editor, cx| { + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") + ); + }); + + // Once more + cx.simulate_input("@file x.png"); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png") + ); + assert!(editor.has_visible_completions_menu()); + assert_eq!(current_completion_labels(editor), &["x.png dir/"]); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + // This time don't immediately get the contents, just let the confirmed completion settle + cx.run_until_parked(); + + // Mention was removed + editor.read_with(&cx, |editor, cx| { + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") + ); + }); + + // Now getting the contents succeeds, because the invalid mention was removed + let contents = message_editor + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&all_prompt_capabilities, cx) + }) + .await + .unwrap(); + assert_eq!(contents.len(), 3); } fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec> { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 0e1d4123b9ffc01e386804e85cb4bd75e1e49180..3ad1234e226e0a165eb1f7bd4c8c8f665f4d22e0 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -274,6 +274,7 @@ pub struct AcpThreadView { edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, + terminal_expanded: bool, editing_message: Option, prompt_capabilities: Rc>, _cancel_task: Option>, @@ -384,6 +385,7 @@ impl AcpThreadView { edits_expanded: false, plan_expanded: false, editor_expanded: false, + terminal_expanded: true, history_store, hovered_recent_history_item: None, prompt_capabilities, @@ -835,7 +837,7 @@ impl AcpThreadView { let contents = self .message_editor - .update(cx, |message_editor, cx| message_editor.contents(window, cx)); + .update(cx, |message_editor, cx| message_editor.contents(cx)); self.send_impl(contents, window, cx) } @@ -848,7 +850,7 @@ impl AcpThreadView { let contents = self .message_editor - .update(cx, |message_editor, cx| message_editor.contents(window, cx)); + .update(cx, |message_editor, cx| message_editor.contents(cx)); cx.spawn_in(window, async move |this, cx| { cancelled.await; @@ -956,8 +958,7 @@ impl AcpThreadView { return; }; - let contents = - message_editor.update(cx, |message_editor, cx| message_editor.contents(window, cx)); + let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx)); let task = cx.foreground_executor().spawn(async move { rewind.await?; @@ -1690,9 +1691,10 @@ impl AcpThreadView { matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some(); let use_card_layout = needs_confirmation || is_edit; - let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; + let is_collapsible = !tool_call.content.is_empty() && !use_card_layout; - let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); + let is_open = + needs_confirmation || is_edit || self.expanded_tool_calls.contains(&tool_call.id); let gradient_overlay = |color: Hsla| { div() @@ -2162,8 +2164,6 @@ impl AcpThreadView { .map(|path| format!("{}", path.display())) .unwrap_or_else(|| "current directory".to_string()); - let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); - let header = h_flex() .id(SharedString::from(format!( "terminal-tool-header-{}", @@ -2297,19 +2297,12 @@ impl AcpThreadView { "terminal-tool-disclosure-{}", terminal.entity_id() )), - is_expanded, + self.terminal_expanded, ) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) - .on_click(cx.listener({ - let id = tool_call.id.clone(); - move |this, _event, _window, _cx| { - if is_expanded { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - } + .on_click(cx.listener(move |this, _event, _window, _cx| { + this.terminal_expanded = !this.terminal_expanded; })), ); @@ -2318,7 +2311,7 @@ impl AcpThreadView { .read(cx) .entry(entry_ix) .and_then(|entry| entry.terminal(terminal)); - let show_output = is_expanded && terminal_view.is_some(); + let show_output = self.terminal_expanded && terminal_view.is_some(); v_flex() .mb_2() @@ -3655,6 +3648,7 @@ impl AcpThreadView { .open_path(path, None, true, window, cx) .detach_and_log_err(cx); } + MentionUri::PastedImage => {} MentionUri::Directory { abs_path } => { let project = workspace.project(); let Some(entry) = project.update(cx, |project, cx| { @@ -3669,9 +3663,14 @@ impl AcpThreadView { }); } MentionUri::Symbol { - path, line_range, .. + abs_path: path, + line_range, + .. } - | MentionUri::Selection { path, line_range } => { + | MentionUri::Selection { + abs_path: Some(path), + line_range, + } => { let project = workspace.project(); let Some((path, _)) = project.update(cx, |project, cx| { let path = project.find_project_path(path, cx)?; @@ -3687,8 +3686,8 @@ impl AcpThreadView { let Some(editor) = item.await?.downcast::() else { return Ok(()); }; - let range = - Point::new(line_range.start, 0)..Point::new(line_range.start, 0); + let range = Point::new(*line_range.start(), 0) + ..Point::new(*line_range.start(), 0); editor .update_in(cx, |editor, window, cx| { editor.change_selections( @@ -3703,6 +3702,7 @@ impl AcpThreadView { }) .detach_and_log_err(cx); } + MentionUri::Selection { abs_path: None, .. } => {} MentionUri::Thread { id, name } => { if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, cx| { From 70575d1115133988df19d3d2d6c8cc1f35a19a6b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 23 Aug 2025 10:03:36 +0300 Subject: [PATCH 295/744] Remove redundant Cargo diagnostics settings (#36795) Removes `diagnostics.cargo.fetch_cargo_diagnostics` settings as those are not needed for the flycheck diagnostics to run. This setting disabled `checkOnSave` in rust-analyzer and allowed to update diagnostics via flycheck in the project diagnostics editor with the "refresh" button. Instead, `"checkOnSave": false,` can be set manually as https://zed.dev/docs/languages/rust#more-server-configuration example shows and flycheck commands can be called manually from anywhere, including the diagnostics panel, to refresh the diagnostics. Release Notes: - Removed redundant `diagnostics.cargo.fetch_cargo_diagnostics` settings --- Cargo.lock | 1 - assets/settings/default.json | 5 - crates/diagnostics/Cargo.toml | 1 - crates/diagnostics/src/diagnostics.rs | 127 +-------------------- crates/diagnostics/src/toolbar_controls.rs | 38 ++---- crates/languages/src/rust.rs | 14 --- crates/project/src/project_settings.rs | 22 ---- docs/src/languages/rust.md | 17 +-- 8 files changed, 14 insertions(+), 211 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa3a910390f4d74d135de602c8ec8c1f51f76e4a..6964ed4890ee21ec3c3aa022bc28dea7d64454ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4685,7 +4685,6 @@ dependencies = [ "component", "ctor", "editor", - "futures 0.3.31", "gpui", "indoc", "language", diff --git a/assets/settings/default.json b/assets/settings/default.json index 014b4832505887046d70db7cad2c21d308422643..ac26952c7f634223b10abdfc107ad8c27a5b17ac 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1133,11 +1133,6 @@ // The minimum severity of the diagnostics to show inline. // Inherits editor's diagnostics' max severity settings when `null`. "max_severity": null - }, - "cargo": { - // When enabled, Zed disables rust-analyzer's check on save and starts to query - // Cargo diagnostics separately. - "fetch_cargo_diagnostics": false } }, // Files or globs of files that will be excluded by Zed entirely. They will be skipped during file diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 53b5792e10e73d1629a104e345965547b6f2b25e..fd678078e8668b8a569c2d0f1627c786987a3cb4 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -18,7 +18,6 @@ collections.workspace = true component.workspace = true ctor.workspace = true editor.workspace = true -futures.workspace = true gpui.workspace = true indoc.workspace = true language.workspace = true diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 037e4fc0fd7d89c12daa26569387eddebf6c0fd1..1c27e820a0d8afb64c5c67e66e125caf8720593d 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -13,7 +13,6 @@ use editor::{ DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey, display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, }; -use futures::future::join_all; use gpui::{ AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, @@ -24,7 +23,6 @@ use language::{ }; use project::{ DiagnosticSummary, Project, ProjectPath, - lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck}, project_settings::{DiagnosticSeverity, ProjectSettings}, }; use settings::Settings; @@ -79,17 +77,10 @@ pub(crate) struct ProjectDiagnosticsEditor { paths_to_update: BTreeSet, include_warnings: bool, update_excerpts_task: Option>>, - cargo_diagnostics_fetch: CargoDiagnosticsFetchState, diagnostic_summary_update: Task<()>, _subscription: Subscription, } -struct CargoDiagnosticsFetchState { - fetch_task: Option>, - cancel_task: Option>, - diagnostic_sources: Arc>, -} - impl EventEmitter for ProjectDiagnosticsEditor {} const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50); @@ -260,11 +251,7 @@ impl ProjectDiagnosticsEditor { ) }); this.diagnostics.clear(); - this.update_all_diagnostics(false, window, cx); - }) - .detach(); - cx.observe_release(&cx.entity(), |editor, _, cx| { - editor.stop_cargo_diagnostics_fetch(cx); + this.update_all_excerpts(window, cx); }) .detach(); @@ -281,15 +268,10 @@ impl ProjectDiagnosticsEditor { editor, paths_to_update: Default::default(), update_excerpts_task: None, - cargo_diagnostics_fetch: CargoDiagnosticsFetchState { - fetch_task: None, - cancel_task: None, - diagnostic_sources: Arc::new(Vec::new()), - }, diagnostic_summary_update: Task::ready(()), _subscription: project_event_subscription, }; - this.update_all_diagnostics(true, window, cx); + this.update_all_excerpts(window, cx); this } @@ -373,20 +355,10 @@ impl ProjectDiagnosticsEditor { window: &mut Window, cx: &mut Context, ) { - let fetch_cargo_diagnostics = ProjectSettings::get_global(cx) - .diagnostics - .fetch_cargo_diagnostics(); - - if fetch_cargo_diagnostics { - if self.cargo_diagnostics_fetch.fetch_task.is_some() { - self.stop_cargo_diagnostics_fetch(cx); - } else { - self.update_all_diagnostics(false, window, cx); - } - } else if self.update_excerpts_task.is_some() { + if self.update_excerpts_task.is_some() { self.update_excerpts_task = None; } else { - self.update_all_diagnostics(false, window, cx); + self.update_all_excerpts(window, cx); } cx.notify(); } @@ -404,73 +376,6 @@ impl ProjectDiagnosticsEditor { } } - fn update_all_diagnostics( - &mut self, - first_launch: bool, - window: &mut Window, - cx: &mut Context, - ) { - let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx); - if cargo_diagnostics_sources.is_empty() { - self.update_all_excerpts(window, cx); - } else if first_launch && !self.summary.is_empty() { - self.update_all_excerpts(window, cx); - } else { - self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx); - } - } - - fn fetch_cargo_diagnostics( - &mut self, - diagnostics_sources: Arc>, - cx: &mut Context, - ) { - let project = self.project.clone(); - self.cargo_diagnostics_fetch.cancel_task = None; - self.cargo_diagnostics_fetch.fetch_task = None; - self.cargo_diagnostics_fetch.diagnostic_sources = diagnostics_sources.clone(); - if self.cargo_diagnostics_fetch.diagnostic_sources.is_empty() { - return; - } - - self.cargo_diagnostics_fetch.fetch_task = Some(cx.spawn(async move |editor, cx| { - let mut fetch_tasks = Vec::new(); - for buffer_path in diagnostics_sources.iter().cloned() { - if cx - .update(|cx| { - fetch_tasks.push(run_flycheck(project.clone(), Some(buffer_path), cx)); - }) - .is_err() - { - break; - } - } - - let _ = join_all(fetch_tasks).await; - editor - .update(cx, |editor, _| { - editor.cargo_diagnostics_fetch.fetch_task = None; - }) - .ok(); - })); - } - - fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) { - self.cargo_diagnostics_fetch.fetch_task = None; - let mut cancel_gasks = Vec::new(); - for buffer_path in std::mem::take(&mut self.cargo_diagnostics_fetch.diagnostic_sources) - .iter() - .cloned() - { - cancel_gasks.push(cancel_flycheck(self.project.clone(), Some(buffer_path), cx)); - } - - self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move { - let _ = join_all(cancel_gasks).await; - log::info!("Finished fetching cargo diagnostics"); - })); - } - /// Enqueue an update of all excerpts. Updates all paths that either /// currently have diagnostics or are currently present in this view. fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context) { @@ -695,30 +600,6 @@ impl ProjectDiagnosticsEditor { }) }) } - - pub fn cargo_diagnostics_sources(&self, cx: &App) -> Vec { - let fetch_cargo_diagnostics = ProjectSettings::get_global(cx) - .diagnostics - .fetch_cargo_diagnostics(); - if !fetch_cargo_diagnostics { - return Vec::new(); - } - self.project - .read(cx) - .worktrees(cx) - .filter_map(|worktree| { - let _cargo_toml_entry = worktree.read(cx).entry_for_path("Cargo.toml")?; - let rust_file_entry = worktree.read(cx).entries(false, 0).find(|entry| { - entry - .path - .extension() - .and_then(|extension| extension.to_str()) - == Some("rs") - })?; - self.project.read(cx).path_for_entry(rust_file_entry.id, cx) - }) - .collect() - } } impl Focusable for ProjectDiagnosticsEditor { diff --git a/crates/diagnostics/src/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index e77b80115f2ffe6de512743d3eb00311052d7937..404db391648f4af0092c52572ee2e6d3a57a34ef 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh}; use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window}; use ui::prelude::*; @@ -15,26 +13,18 @@ impl Render for ToolbarControls { let mut include_warnings = false; let mut has_stale_excerpts = false; let mut is_updating = false; - let cargo_diagnostics_sources = Arc::new(self.diagnostics().map_or(Vec::new(), |editor| { - editor.read(cx).cargo_diagnostics_sources(cx) - })); - let fetch_cargo_diagnostics = !cargo_diagnostics_sources.is_empty(); if let Some(editor) = self.diagnostics() { let diagnostics = editor.read(cx); include_warnings = diagnostics.include_warnings; has_stale_excerpts = !diagnostics.paths_to_update.is_empty(); - is_updating = if fetch_cargo_diagnostics { - diagnostics.cargo_diagnostics_fetch.fetch_task.is_some() - } else { - diagnostics.update_excerpts_task.is_some() - || diagnostics - .project - .read(cx) - .language_servers_running_disk_based_diagnostics(cx) - .next() - .is_some() - }; + is_updating = diagnostics.update_excerpts_task.is_some() + || diagnostics + .project + .read(cx) + .language_servers_running_disk_based_diagnostics(cx) + .next() + .is_some(); } let tooltip = if include_warnings { @@ -64,7 +54,6 @@ impl Render for ToolbarControls { .on_click(cx.listener(move |toolbar_controls, _, _, cx| { if let Some(diagnostics) = toolbar_controls.diagnostics() { diagnostics.update(cx, |diagnostics, cx| { - diagnostics.stop_cargo_diagnostics_fetch(cx); diagnostics.update_excerpts_task = None; cx.notify(); }); @@ -76,7 +65,7 @@ impl Render for ToolbarControls { IconButton::new("refresh-diagnostics", IconName::ArrowCircle) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .disabled(!has_stale_excerpts && !fetch_cargo_diagnostics) + .disabled(!has_stale_excerpts) .tooltip(Tooltip::for_action_title( "Refresh diagnostics", &ToggleDiagnosticsRefresh, @@ -84,17 +73,8 @@ impl Render for ToolbarControls { .on_click(cx.listener({ move |toolbar_controls, _, window, cx| { if let Some(diagnostics) = toolbar_controls.diagnostics() { - let cargo_diagnostics_sources = - Arc::clone(&cargo_diagnostics_sources); diagnostics.update(cx, move |diagnostics, cx| { - if fetch_cargo_diagnostics { - diagnostics.fetch_cargo_diagnostics( - cargo_diagnostics_sources, - cx, - ); - } else { - diagnostics.update_all_excerpts(window, cx); - } + diagnostics.update_all_excerpts(window, cx); }); } } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index c6c735714854da15194cad6d15c2e3bd124fe308..3e8dce756be42ca59d88d86404518e65cf54ff7e 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -510,20 +510,6 @@ impl LspAdapter for RustLspAdapter { } } - let cargo_diagnostics_fetched_separately = ProjectSettings::get_global(cx) - .diagnostics - .fetch_cargo_diagnostics(); - if cargo_diagnostics_fetched_separately { - let disable_check_on_save = json!({ - "checkOnSave": false, - }); - if let Some(initialization_options) = &mut original.initialization_options { - merge_json_value_into(disable_check_on_save, initialization_options); - } else { - original.initialization_options = Some(disable_check_on_save); - } - } - Ok(original) } } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index a6fea4059c2f20b3ef1a06fe47b17cb7275886e0..4447c2512943257b27a91fb1ac051bccde6e3f7f 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -181,17 +181,6 @@ pub struct DiagnosticsSettings { /// Settings for showing inline diagnostics. pub inline: InlineDiagnosticsSettings, - - /// Configuration, related to Rust language diagnostics. - pub cargo: Option, -} - -impl DiagnosticsSettings { - pub fn fetch_cargo_diagnostics(&self) -> bool { - self.cargo - .as_ref() - .is_some_and(|cargo_diagnostics| cargo_diagnostics.fetch_cargo_diagnostics) - } } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] @@ -258,7 +247,6 @@ impl Default for DiagnosticsSettings { include_warnings: true, lsp_pull_diagnostics: LspPullDiagnosticsSettings::default(), inline: InlineDiagnosticsSettings::default(), - cargo: None, } } } @@ -292,16 +280,6 @@ impl Default for GlobalLspSettings { } } -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct CargoDiagnosticsSettings { - /// When enabled, Zed disables rust-analyzer's check on save and starts to query - /// Cargo diagnostics separately. - /// - /// Default: false - #[serde(default)] - pub fetch_cargo_diagnostics: bool, -} - #[derive( Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema, )] diff --git a/docs/src/languages/rust.md b/docs/src/languages/rust.md index 769528027526dea8a3e88841da6b3173ae997062..0bfa3ecac75f1371b38e1609090315841ea97a4c 100644 --- a/docs/src/languages/rust.md +++ b/docs/src/languages/rust.md @@ -136,22 +136,7 @@ This is enabled by default and can be configured as ## Manual Cargo Diagnostics fetch By default, rust-analyzer has `checkOnSave: true` enabled, which causes every buffer save to trigger a `cargo check --workspace --all-targets` command. -For lager projects this might introduce excessive wait times, so a more fine-grained triggering could be enabled by altering the - -```json -"diagnostics": { - "cargo": { - // When enabled, Zed disables rust-analyzer's check on save and starts to query - // Cargo diagnostics separately. - "fetch_cargo_diagnostics": false - } -} -``` - -default settings. - -This will stop rust-analyzer from running `cargo check ...` on save, yet still allow to run -`editor: run/clear/cancel flycheck` commands in Rust files to refresh cargo diagnostics; the project diagnostics editor will also refresh cargo diagnostics with `editor: run flycheck` command when the setting is enabled. +If disabled with `checkOnSave: false` (see the example of the server configuration json above), it's still possible to fetch the diagnostics manually, with the `editor: run/clear/cancel flycheck` commands in Rust files to refresh cargo diagnostics; the project diagnostics editor will also refresh cargo diagnostics with `editor: run flycheck` command when the setting is enabled. ## More server configuration From 61bc1cc44172d61c2fb69d8265bc5d809512f342 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 23 Aug 2025 16:30:54 +0200 Subject: [PATCH 296/744] acp: Support launching custom agent servers (#36805) It's enough to add this to your settings: ```json { "agent_servers": { "Name Of Your Agent": { "command": "/path/to/custom/agent", "args": ["arguments", "that", "you", "want"], } } } ``` Release Notes: - N/A --- crates/acp_tools/src/acp_tools.rs | 12 +- crates/agent2/src/native_agent_server.rs | 12 +- crates/agent_servers/src/acp.rs | 12 +- crates/agent_servers/src/agent_servers.rs | 8 +- crates/agent_servers/src/claude.rs | 12 +- crates/agent_servers/src/custom.rs | 59 ++++++++++ crates/agent_servers/src/e2e_tests.rs | 13 +-- crates/agent_servers/src/gemini.rs | 13 +-- crates/agent_servers/src/settings.rs | 24 +++- crates/agent_ui/src/acp/thread_view.rs | 20 ++-- crates/agent_ui/src/agent_panel.rs | 105 ++++++++++++++---- crates/agent_ui/src/agent_ui.rs | 19 +++- crates/language_model/src/language_model.rs | 4 +- .../language_models/src/provider/anthropic.rs | 6 +- crates/language_models/src/provider/google.rs | 6 +- 15 files changed, 236 insertions(+), 89 deletions(-) create mode 100644 crates/agent_servers/src/custom.rs diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs index ca5e57e85aa8e96dde5362fe0608e60f4fa12a81..ee12b04cdec06809b93f8f991f0c32788772a5b7 100644 --- a/crates/acp_tools/src/acp_tools.rs +++ b/crates/acp_tools/src/acp_tools.rs @@ -46,7 +46,7 @@ pub struct AcpConnectionRegistry { } struct ActiveConnection { - server_name: &'static str, + server_name: SharedString, connection: Weak, } @@ -63,12 +63,12 @@ impl AcpConnectionRegistry { pub fn set_active_connection( &self, - server_name: &'static str, + server_name: impl Into, connection: &Rc, cx: &mut Context, ) { self.active_connection.replace(Some(ActiveConnection { - server_name, + server_name: server_name.into(), connection: Rc::downgrade(connection), })); cx.notify(); @@ -85,7 +85,7 @@ struct AcpTools { } struct WatchedConnection { - server_name: &'static str, + server_name: SharedString, messages: Vec, list_state: ListState, connection: Weak, @@ -142,7 +142,7 @@ impl AcpTools { }); self.watched_connection = Some(WatchedConnection { - server_name: active_connection.server_name, + server_name: active_connection.server_name.clone(), messages: vec![], list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)), connection: active_connection.connection.clone(), @@ -442,7 +442,7 @@ impl Item for AcpTools { "ACP: {}", self.watched_connection .as_ref() - .map_or("Disconnected", |connection| connection.server_name) + .map_or("Disconnected", |connection| &connection.server_name) ) .into() } diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 4ce467d6fdec953397a38ed2c5133d89a2e6adc1..12d3c79d1bf1b046dfd7703ffa089684039039c4 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -3,7 +3,7 @@ use std::{any::Any, path::Path, rc::Rc, sync::Arc}; use agent_servers::AgentServer; use anyhow::Result; use fs::Fs; -use gpui::{App, Entity, Task}; +use gpui::{App, Entity, SharedString, Task}; use project::Project; use prompt_store::PromptStore; @@ -22,16 +22,16 @@ impl NativeAgentServer { } impl AgentServer for NativeAgentServer { - fn name(&self) -> &'static str { - "Zed Agent" + fn name(&self) -> SharedString { + "Zed Agent".into() } - fn empty_state_headline(&self) -> &'static str { + fn empty_state_headline(&self) -> SharedString { self.name() } - fn empty_state_message(&self) -> &'static str { - "" + fn empty_state_message(&self) -> SharedString { + "".into() } fn logo(&self) -> ui::IconName { diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index a99a4014316c8fce6404289c8125840674be6b91..c9c938c6c0bae74ba0dc23c4b2e37c03c18d2e59 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -15,7 +15,7 @@ use std::{path::Path, rc::Rc}; use thiserror::Error; use anyhow::{Context as _, Result}; -use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; +use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity}; use acp_thread::{AcpThread, AuthRequired, LoadError}; @@ -24,7 +24,7 @@ use acp_thread::{AcpThread, AuthRequired, LoadError}; pub struct UnsupportedVersion; pub struct AcpConnection { - server_name: &'static str, + server_name: SharedString, connection: Rc, sessions: Rc>>, auth_methods: Vec, @@ -38,7 +38,7 @@ pub struct AcpSession { } pub async fn connect( - server_name: &'static str, + server_name: SharedString, command: AgentServerCommand, root_dir: &Path, cx: &mut AsyncApp, @@ -51,7 +51,7 @@ const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; impl AcpConnection { pub async fn stdio( - server_name: &'static str, + server_name: SharedString, command: AgentServerCommand, root_dir: &Path, cx: &mut AsyncApp, @@ -121,7 +121,7 @@ impl AcpConnection { cx.update(|cx| { AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { - registry.set_active_connection(server_name, &connection, cx) + registry.set_active_connection(server_name.clone(), &connection, cx) }); })?; @@ -187,7 +187,7 @@ impl AgentConnection for AcpConnection { let action_log = cx.new(|_| ActionLog::new(project.clone()))?; let thread = cx.new(|_cx| { AcpThread::new( - self.server_name, + self.server_name.clone(), self.clone(), project, action_log, diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 2f5ec478ae8288f4e4db3db84c622d2c03c615be..fa5920133857c5f4f0743278ce01b12de2e7bd1d 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -1,5 +1,6 @@ mod acp; mod claude; +mod custom; mod gemini; mod settings; @@ -7,6 +8,7 @@ mod settings; pub mod e2e_tests; pub use claude::*; +pub use custom::*; pub use gemini::*; pub use settings::*; @@ -31,9 +33,9 @@ pub fn init(cx: &mut App) { pub trait AgentServer: Send { fn logo(&self) -> ui::IconName; - fn name(&self) -> &'static str; - fn empty_state_headline(&self) -> &'static str; - fn empty_state_message(&self) -> &'static str; + fn name(&self) -> SharedString; + fn empty_state_headline(&self) -> SharedString; + fn empty_state_message(&self) -> SharedString; fn connect( &self, diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index ef666974f1e6a29345e469d8cf19fe13a3fd02eb..048563103f658d1290657a04cbe71742056ae69c 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -30,7 +30,7 @@ use futures::{ io::BufReader, select_biased, }; -use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; +use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity}; use serde::{Deserialize, Serialize}; use util::{ResultExt, debug_panic}; @@ -43,16 +43,16 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri pub struct ClaudeCode; impl AgentServer for ClaudeCode { - fn name(&self) -> &'static str { - "Claude Code" + fn name(&self) -> SharedString { + "Claude Code".into() } - fn empty_state_headline(&self) -> &'static str { + fn empty_state_headline(&self) -> SharedString { self.name() } - fn empty_state_message(&self) -> &'static str { - "How can I help you today?" + fn empty_state_message(&self) -> SharedString { + "How can I help you today?".into() } fn logo(&self) -> ui::IconName { diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs new file mode 100644 index 0000000000000000000000000000000000000000..e544c4f21f68bd4b797c29feacca0dc41cd997f8 --- /dev/null +++ b/crates/agent_servers/src/custom.rs @@ -0,0 +1,59 @@ +use crate::{AgentServerCommand, AgentServerSettings}; +use acp_thread::AgentConnection; +use anyhow::Result; +use gpui::{App, Entity, SharedString, Task}; +use project::Project; +use std::{path::Path, rc::Rc}; +use ui::IconName; + +/// A generic agent server implementation for custom user-defined agents +pub struct CustomAgentServer { + name: SharedString, + command: AgentServerCommand, +} + +impl CustomAgentServer { + pub fn new(name: SharedString, settings: &AgentServerSettings) -> Self { + Self { + name, + command: settings.command.clone(), + } + } +} + +impl crate::AgentServer for CustomAgentServer { + fn name(&self) -> SharedString { + self.name.clone() + } + + fn logo(&self) -> IconName { + IconName::Terminal + } + + fn empty_state_headline(&self) -> SharedString { + "No conversations yet".into() + } + + fn empty_state_message(&self) -> SharedString { + format!("Start a conversation with {}", self.name).into() + } + + fn connect( + &self, + root_dir: &Path, + _project: &Entity, + cx: &mut App, + ) -> Task>> { + let server_name = self.name(); + let command = self.command.clone(); + let root_dir = root_dir.to_path_buf(); + + cx.spawn(async move |mut cx| { + crate::acp::connect(server_name, command, &root_dir, &mut cx).await + }) + } + + fn into_any(self: Rc) -> Rc { + self + } +} diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index c2710790719251980bc39bb9367a62fdc3cee4a3..42264b4b4f747e11cab11a21f7cde2ad0c43fee3 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -1,17 +1,15 @@ -use std::{ - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; - use crate::AgentServer; use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus}; use agent_client_protocol as acp; - use futures::{FutureExt, StreamExt, channel::mpsc, select}; use gpui::{AppContext, Entity, TestAppContext}; use indoc::indoc; use project::{FakeFs, Project}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; use util::path; pub async fn test_basic(server: F, cx: &mut TestAppContext) @@ -479,6 +477,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc { gemini: Some(crate::AgentServerSettings { command: crate::gemini::tests::local_command(), }), + custom: collections::HashMap::default(), }, cx, ); diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 29120fff6eec542855e376122e74097c4cad56b4..9ebcee745c99436cafdeab4e239a84fe5f7ef127 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -4,11 +4,10 @@ use std::{any::Any, path::Path}; use crate::{AgentServer, AgentServerCommand}; use acp_thread::{AgentConnection, LoadError}; use anyhow::Result; -use gpui::{Entity, Task}; +use gpui::{App, Entity, SharedString, Task}; use language_models::provider::google::GoogleLanguageModelProvider; use project::Project; use settings::SettingsStore; -use ui::App; use crate::AllAgentServersSettings; @@ -18,16 +17,16 @@ pub struct Gemini; const ACP_ARG: &str = "--experimental-acp"; impl AgentServer for Gemini { - fn name(&self) -> &'static str { - "Gemini CLI" + fn name(&self) -> SharedString { + "Gemini CLI".into() } - fn empty_state_headline(&self) -> &'static str { + fn empty_state_headline(&self) -> SharedString { self.name() } - fn empty_state_message(&self) -> &'static str { - "Ask questions, edit files, run commands" + fn empty_state_message(&self) -> SharedString { + "Ask questions, edit files, run commands".into() } fn logo(&self) -> ui::IconName { diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 645674b5f15087250c2364fb9a8a846e163ad54c..96ac6e3cbe7dcd8a03aef5c6ec79c884bf99ae67 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -1,6 +1,7 @@ use crate::AgentServerCommand; use anyhow::Result; -use gpui::App; +use collections::HashMap; +use gpui::{App, SharedString}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; @@ -13,9 +14,13 @@ pub fn init(cx: &mut App) { pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, + + /// Custom agent servers configured by the user + #[serde(flatten)] + pub custom: HashMap, } -#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)] +#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] pub struct AgentServerSettings { #[serde(flatten)] pub command: AgentServerCommand, @@ -29,13 +34,26 @@ impl settings::Settings for AllAgentServersSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { let mut settings = AllAgentServersSettings::default(); - for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() { + for AllAgentServersSettings { + gemini, + claude, + custom, + } in sources.defaults_and_customizations() + { if gemini.is_some() { settings.gemini = gemini.clone(); } if claude.is_some() { settings.claude = claude.clone(); } + + // Merge custom agents + for (name, config) in custom { + // Skip built-in agent names to avoid conflicts + if name != "gemini" && name != "claude" { + settings.custom.insert(name.clone(), config.clone()); + } + } } Ok(settings) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 3ad1234e226e0a165eb1f7bd4c8c8f665f4d22e0..87928767c686d1ec54585bb0ea7b5c8f2a673a28 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -600,7 +600,7 @@ impl AcpThreadView { let view = registry.read(cx).provider(&provider_id).map(|provider| { provider.configuration_view( - language_model::ConfigurationViewTargetAgent::Other(agent_name), + language_model::ConfigurationViewTargetAgent::Other(agent_name.clone()), window, cx, ) @@ -1372,7 +1372,7 @@ impl AcpThreadView { .icon_color(Color::Muted) .style(ButtonStyle::Transparent) .tooltip(move |_window, cx| { - cx.new(|_| UnavailableEditingTooltip::new(agent_name.into())) + cx.new(|_| UnavailableEditingTooltip::new(agent_name.clone())) .into() }) ) @@ -3911,13 +3911,13 @@ impl AcpThreadView { match AgentSettings::get_global(cx).notify_when_agent_waiting { NotifyWhenAgentWaiting::PrimaryScreen => { if let Some(primary) = cx.primary_display() { - self.pop_up(icon, caption.into(), title.into(), window, primary, cx); + self.pop_up(icon, caption.into(), title, window, primary, cx); } } NotifyWhenAgentWaiting::AllScreens => { let caption = caption.into(); for screen in cx.displays() { - self.pop_up(icon, caption.clone(), title.into(), window, screen, cx); + self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx); } } NotifyWhenAgentWaiting::Never => { @@ -5153,16 +5153,16 @@ pub(crate) mod tests { ui::IconName::Ai } - fn name(&self) -> &'static str { - "Test" + fn name(&self) -> SharedString { + "Test".into() } - fn empty_state_headline(&self) -> &'static str { - "Test" + fn empty_state_headline(&self) -> SharedString { + "Test".into() } - fn empty_state_message(&self) -> &'static str { - "Test" + fn empty_state_message(&self) -> SharedString { + "Test".into() } fn connect( diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 0e611d0db95a0dcb319be79f82086d762e9c2fb1..50f9fc6a457d5aa060dfe6cf98e09e838a35a549 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use std::time::Duration; use acp_thread::AcpThread; +use agent_servers::AgentServerSettings; use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; @@ -128,7 +129,7 @@ pub fn init(cx: &mut App) { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| { - panel.external_thread(action.agent, None, None, window, cx) + panel.external_thread(action.agent.clone(), None, None, window, cx) }); } }) @@ -239,7 +240,7 @@ enum WhichFontSize { None, } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub enum AgentType { #[default] Zed, @@ -247,23 +248,29 @@ pub enum AgentType { Gemini, ClaudeCode, NativeAgent, + Custom { + name: SharedString, + settings: AgentServerSettings, + }, } impl AgentType { - fn label(self) -> impl Into { + fn label(&self) -> SharedString { match self { - Self::Zed | Self::TextThread => "Zed Agent", - Self::NativeAgent => "Agent 2", - Self::Gemini => "Gemini CLI", - Self::ClaudeCode => "Claude Code", + Self::Zed | Self::TextThread => "Zed Agent".into(), + Self::NativeAgent => "Agent 2".into(), + Self::Gemini => "Gemini CLI".into(), + Self::ClaudeCode => "Claude Code".into(), + Self::Custom { name, .. } => name.into(), } } - fn icon(self) -> Option { + fn icon(&self) -> Option { match self { Self::Zed | Self::NativeAgent | Self::TextThread => None, Self::Gemini => Some(IconName::AiGemini), Self::ClaudeCode => Some(IconName::AiClaude), + Self::Custom { .. } => Some(IconName::Terminal), } } } @@ -517,7 +524,7 @@ pub struct AgentPanel { impl AgentPanel { fn serialize(&mut self, cx: &mut Context) { let width = self.width; - let selected_agent = self.selected_agent; + let selected_agent = self.selected_agent.clone(); self.pending_serialization = Some(cx.background_spawn(async move { KEY_VALUE_STORE .write_kvp( @@ -607,7 +614,7 @@ impl AgentPanel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|w| w.round()); if let Some(selected_agent) = serialized_panel.selected_agent { - panel.selected_agent = selected_agent; + panel.selected_agent = selected_agent.clone(); panel.new_agent_thread(selected_agent, window, cx); } cx.notify(); @@ -1077,14 +1084,17 @@ impl AgentPanel { cx.spawn_in(window, async move |this, cx| { let ext_agent = match agent_choice { Some(agent) => { - cx.background_spawn(async move { - if let Some(serialized) = - serde_json::to_string(&LastUsedExternalAgent { agent }).log_err() - { - KEY_VALUE_STORE - .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized) - .await - .log_err(); + cx.background_spawn({ + let agent = agent.clone(); + async move { + if let Some(serialized) = + serde_json::to_string(&LastUsedExternalAgent { agent }).log_err() + { + KEY_VALUE_STORE + .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized) + .await + .log_err(); + } } }) .detach(); @@ -1110,7 +1120,9 @@ impl AgentPanel { this.update_in(cx, |this, window, cx| { match ext_agent { - crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => { + crate::ExternalAgent::Gemini + | crate::ExternalAgent::NativeAgent + | crate::ExternalAgent::Custom { .. } => { if !cx.has_flag::() { return; } @@ -1839,14 +1851,14 @@ impl AgentPanel { cx: &mut Context, ) { if self.selected_agent != agent { - self.selected_agent = agent; + self.selected_agent = agent.clone(); self.serialize(cx); } self.new_agent_thread(agent, window, cx); } pub fn selected_agent(&self) -> AgentType { - self.selected_agent + self.selected_agent.clone() } pub fn new_agent_thread( @@ -1885,6 +1897,13 @@ impl AgentPanel { window, cx, ), + AgentType::Custom { name, settings } => self.external_thread( + Some(crate::ExternalAgent::Custom { name, settings }), + None, + None, + window, + cx, + ), } } @@ -2610,13 +2629,55 @@ impl AgentPanel { } }), ) + }) + .when(cx.has_flag::(), |mut menu| { + // Add custom agents from settings + let settings = + agent_servers::AllAgentServersSettings::get_global(cx); + for (agent_name, agent_settings) in &settings.custom { + menu = menu.item( + ContextMenuEntry::new(format!("New {} Thread", agent_name)) + .icon(IconName::Terminal) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + let agent_name = agent_name.clone(); + let agent_settings = agent_settings.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::Custom { + name: agent_name + .clone(), + settings: + agent_settings + .clone(), + }, + window, + cx, + ); + }); + } + }); + } + } + }), + ); + } + + menu }); menu })) } }); - let selected_agent_label = self.selected_agent.label().into(); + let selected_agent_label = self.selected_agent.label(); let selected_agent = div() .id("selected_agent_icon") .when_some(self.selected_agent.icon(), |this, icon| { diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 6084fd64235b4c276c841889cb0516661c14b1f7..40f6c6a2bbedfe4543042b85a7ecbf2e0d7f9e70 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -28,13 +28,14 @@ use std::rc::Rc; use std::sync::Arc; use agent::{Thread, ThreadId}; +use agent_servers::AgentServerSettings; use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection}; use assistant_slash_command::SlashCommandRegistry; use client::Client; use command_palette_hooks::CommandPaletteFilter; use feature_flags::FeatureFlagAppExt as _; use fs::Fs; -use gpui::{Action, App, Entity, actions}; +use gpui::{Action, App, Entity, SharedString, actions}; use language::LanguageRegistry; use language_model::{ ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, @@ -159,13 +160,17 @@ pub struct NewNativeAgentThreadFromSummary { from_session_id: agent_client_protocol::SessionId, } -#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] enum ExternalAgent { #[default] Gemini, ClaudeCode, NativeAgent, + Custom { + name: SharedString, + settings: AgentServerSettings, + }, } impl ExternalAgent { @@ -175,9 +180,13 @@ impl ExternalAgent { history: Entity, ) -> Rc { match self { - ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), - ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), - ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), + Self::Gemini => Rc::new(agent_servers::Gemini), + Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode), + Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), + Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new( + name.clone(), + settings, + )), } } } diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index e0a3866443eee342ed17b8bf517eaa81604757c1..d5313b6a3aa5b38e6adb40b26edb827e66fb7dae 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -643,11 +643,11 @@ pub trait LanguageModelProvider: 'static { fn reset_credentials(&self, cx: &mut App) -> Task>; } -#[derive(Default, Clone, Copy)] +#[derive(Default, Clone)] pub enum ConfigurationViewTargetAgent { #[default] ZedAgent, - Other(&'static str), + Other(SharedString), } #[derive(PartialEq, Eq)] diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 0d061c058765cd507c3785b82da3d055d0bc12be..c492edeaf569fe5eeedadd840bd6338c073b48dd 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -1041,9 +1041,9 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent { - ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic", - ConfigurationViewTargetAgent::Other(agent) => agent, + .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent { + ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic".into(), + ConfigurationViewTargetAgent::Other(agent) => agent.clone(), }))) .child( List::new() diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index 566620675ee759516d767804bff837ec953d8369..f252ab7aa3ed723b24d7883371c48f7ceb7f4dff 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -921,9 +921,9 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent { - ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI", - ConfigurationViewTargetAgent::Other(agent) => agent, + .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent { + ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI".into(), + ConfigurationViewTargetAgent::Other(agent) => agent.clone(), }))) .child( List::new() From 60ea4754b29ea5292539496abf40f1a361dced4a Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Sat, 23 Aug 2025 20:30:16 +0530 Subject: [PATCH 297/744] project: Fix dynamic registration for textDocument/documentColor (#36807) From: https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/protocol/src/common/protocol.colorProvider.ts#L50 Release Notes: - N/A --- crates/project/src/lsp_store.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index fb1fae373635ab30dc737709c26cef0e0393a649..d2958dce01cd06b1cf74b007d3f7520102706544 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11913,7 +11913,7 @@ impl LspStore { notify_server_capabilities_updated(&server, cx); } } - "textDocument/colorProvider" => { + "textDocument/documentColor" => { if let Some(caps) = reg .register_options .map(serde_json::from_value) @@ -12064,7 +12064,7 @@ impl LspStore { }); notify_server_capabilities_updated(&server, cx); } - "textDocument/colorProvider" => { + "textDocument/documentColor" => { server.update_capabilities(|capabilities| { capabilities.color_provider = None; }); From d49409caba7f4b39409c38299d61511b6a3bb406 Mon Sep 17 00:00:00 2001 From: itsaphel Date: Sat, 23 Aug 2025 17:11:27 +0100 Subject: [PATCH 298/744] docs: Update settings in diagnostics.md (#36806) For project_panel, the diagnostics key seems to be `show_diagnostics` not `diagnostics` ([source](https://github.com/zed-industries/zed/blob/main/crates/project_panel/src/project_panel_settings.rs#L149-L152)). Updating the docs accordingly Release Notes: - N/A --- docs/src/diagnostics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/diagnostics.md b/docs/src/diagnostics.md index a015fbebf88b64ebb75941133d3ab21279182685..9603c8197cf7ef473da027a51fa0db64d0b9b8e9 100644 --- a/docs/src/diagnostics.md +++ b/docs/src/diagnostics.md @@ -51,7 +51,7 @@ To configure, use ```json5 "project_panel": { - "diagnostics": "all", + "show_diagnostics": "all", } ``` From 19764794b77c08e828fd7170c7539a9ca9c2b3de Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sat, 23 Aug 2025 16:39:14 -0400 Subject: [PATCH 299/744] acp: Animate loading context creases (#36814) - Add pulsating animation for context creases while they're loading - Add spinner in message editors (replacing send button) during the window where sending has been requested, but we haven't finished loading the message contents to send to the model - During the same window, ignore further send requests, so we don't end up sending the same message twice if you mash enter while loading is in progress - Wait for context to load before rewinding the thread when sending an edited past message, avoiding an empty-looking state during the same window Release Notes: - N/A --- Cargo.lock | 1 + crates/agent_ui/Cargo.toml | 1 + crates/agent_ui/src/acp/message_editor.rs | 224 ++++++++++++++-------- crates/agent_ui/src/acp/thread_view.rs | 92 +++++++-- 4 files changed, 217 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6964ed4890ee21ec3c3aa022bc28dea7d64454ef..0575796034e1e0725122418c61ff718afa2f09ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,6 +403,7 @@ dependencies = [ "parking_lot", "paths", "picker", + "postage", "pretty_assertions", "project", "prompt_store", diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 43e3b251245af5d014c952afe2fec1f30abafe53..6b0979ee696571841a7ec620ca48de2880f66492 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -67,6 +67,7 @@ ordered-float.workspace = true parking_lot.workspace = true paths.workspace = true picker.workspace = true +postage.workspace = true project.workspace = true prompt_store.workspace = true proto.workspace = true diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 115008cf5298963895306a3dc0fd6e0a7345f1b8..bab42e3da286692119574eb8b8f40fe05babb9b6 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -21,12 +21,13 @@ use futures::{ future::{Shared, join_all}, }; use gpui::{ - AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, - HighlightStyle, Image, ImageFormat, Img, KeyContext, Subscription, Task, TextStyle, - UnderlineStyle, WeakEntity, + Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId, + EventEmitter, FocusHandle, Focusable, HighlightStyle, Image, ImageFormat, Img, KeyContext, + Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, pulsating_between, }; use language::{Buffer, Language}; use language_model::LanguageModelImage; +use postage::stream::Stream as _; use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree}; use prompt_store::{PromptId, PromptStore}; use rope::Point; @@ -44,10 +45,10 @@ use std::{ use text::{OffsetRangeExt, ToOffset as _}; use theme::ThemeSettings; use ui::{ - ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName, - IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement, - Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div, - h_flex, px, + ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Element as _, + FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, + LabelCommon, LabelSize, ParentElement, Render, SelectableButton, SharedString, Styled, + TextSize, TintColor, Toggleable, Window, div, h_flex, px, }; use util::{ResultExt, debug_panic}; use workspace::{Workspace, notifications::NotifyResultExt as _}; @@ -246,7 +247,7 @@ impl MessageEditor { .buffer_snapshot .anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1); - let crease_id = if let MentionUri::File { abs_path } = &mention_uri + let crease = if let MentionUri::File { abs_path } = &mention_uri && let Some(extension) = abs_path.extension() && let Some(extension) = extension.to_str() && Img::extensions().contains(&extension) @@ -272,29 +273,31 @@ impl MessageEditor { Ok(image) }) .shared(); - insert_crease_for_image( + insert_crease_for_mention( *excerpt_id, start, content_len, - Some(abs_path.as_path().into()), - image, + mention_uri.name().into(), + IconName::Image.path().into(), + Some(image), self.editor.clone(), window, cx, ) } else { - crate::context_picker::insert_crease_for_mention( + insert_crease_for_mention( *excerpt_id, start, content_len, crease_text, mention_uri.icon_path(cx), + None, self.editor.clone(), window, cx, ) }; - let Some(crease_id) = crease_id else { + let Some((crease_id, tx)) = crease else { return Task::ready(()); }; @@ -331,7 +334,9 @@ impl MessageEditor { // Notify the user if we failed to load the mentioned context cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_none() { + let result = task.await.notify_async_err(cx); + drop(tx); + if result.is_none() { this.update(cx, |this, cx| { this.editor.update(cx, |editor, cx| { // Remove mention @@ -857,12 +862,13 @@ impl MessageEditor { snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len) }); let image = Arc::new(image); - let Some(crease_id) = insert_crease_for_image( + let Some((crease_id, tx)) = insert_crease_for_mention( excerpt_id, text_anchor, content_len, - None.clone(), - Task::ready(Ok(image.clone())).shared(), + MentionUri::PastedImage.name().into(), + IconName::Image.path().into(), + Some(Task::ready(Ok(image.clone())).shared()), self.editor.clone(), window, cx, @@ -877,6 +883,7 @@ impl MessageEditor { .update(|_, cx| LanguageModelImage::from_image(image, cx)) .map_err(|e| e.to_string())? .await; + drop(tx); if let Some(image) = image { Ok(Mention::Image(MentionImage { data: image.source, @@ -1097,18 +1104,20 @@ impl MessageEditor { for (range, mention_uri, mention) in mentions { let anchor = snapshot.anchor_before(range.start); - let Some(crease_id) = crate::context_picker::insert_crease_for_mention( + let Some((crease_id, tx)) = insert_crease_for_mention( anchor.excerpt_id, anchor.text_anchor, range.end - range.start, mention_uri.name().into(), mention_uri.icon_path(cx), + None, self.editor.clone(), window, cx, ) else { continue; }; + drop(tx); self.mention_set.mentions.insert( crease_id, @@ -1227,23 +1236,21 @@ impl Render for MessageEditor { } } -pub(crate) fn insert_crease_for_image( +pub(crate) fn insert_crease_for_mention( excerpt_id: ExcerptId, anchor: text::Anchor, content_len: usize, - abs_path: Option>, - image: Shared, String>>>, + crease_label: SharedString, + crease_icon: SharedString, + // abs_path: Option>, + image: Option, String>>>>, editor: Entity, window: &mut Window, cx: &mut App, -) -> Option { - let crease_label = abs_path - .as_ref() - .and_then(|path| path.file_name()) - .map(|name| name.to_string_lossy().to_string().into()) - .unwrap_or(SharedString::from("Image")); - - editor.update(cx, |editor, cx| { +) -> Option<(CreaseId, postage::barrier::Sender)> { + let (tx, rx) = postage::barrier::channel(); + + let crease_id = editor.update(cx, |editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?; @@ -1252,7 +1259,15 @@ pub(crate) fn insert_crease_for_image( let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len); let placeholder = FoldPlaceholder { - render: render_image_fold_icon_button(crease_label, image, cx.weak_entity()), + render: render_fold_icon_button( + crease_label, + crease_icon, + start..end, + rx, + image, + cx.weak_entity(), + cx, + ), merge_adjacent: false, ..Default::default() }; @@ -1269,63 +1284,112 @@ pub(crate) fn insert_crease_for_image( editor.fold_creases(vec![crease], false, window, cx); Some(ids[0]) - }) + })?; + + Some((crease_id, tx)) } -fn render_image_fold_icon_button( +fn render_fold_icon_button( label: SharedString, - image_task: Shared, String>>>, + icon: SharedString, + range: Range, + mut loading_finished: postage::barrier::Receiver, + image_task: Option, String>>>>, editor: WeakEntity, + cx: &mut App, ) -> Arc, &mut App) -> AnyElement> { - Arc::new({ - move |fold_id, fold_range, cx| { - let is_in_text_selection = editor - .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) - .unwrap_or_default(); - - ButtonLike::new(fold_id) - .style(ButtonStyle::Filled) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .toggle_state(is_in_text_selection) - .child( - h_flex() - .gap_1() - .child( - Icon::new(IconName::Image) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child( - Label::new(label.clone()) - .size(LabelSize::Small) - .buffer_font(cx) - .single_line(), - ), - ) - .hoverable_tooltip({ + let loading = cx.new(|cx| { + let loading = cx.spawn(async move |this, cx| { + loading_finished.recv().await; + this.update(cx, |this: &mut LoadingContext, cx| { + this.loading = None; + cx.notify(); + }) + .ok(); + }); + LoadingContext { + id: cx.entity_id(), + label, + icon, + range, + editor, + loading: Some(loading), + image: image_task.clone(), + } + }); + Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element()) +} + +struct LoadingContext { + id: EntityId, + label: SharedString, + icon: SharedString, + range: Range, + editor: WeakEntity, + loading: Option>, + image: Option, String>>>>, +} + +impl Render for LoadingContext { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_in_text_selection = self + .editor + .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx)) + .unwrap_or_default(); + ButtonLike::new(("loading-context", self.id)) + .style(ButtonStyle::Filled) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .toggle_state(is_in_text_selection) + .when_some(self.image.clone(), |el, image_task| { + el.hoverable_tooltip(move |_, cx| { + let image = image_task.peek().cloned().transpose().ok().flatten(); let image_task = image_task.clone(); - move |_, cx| { - let image = image_task.peek().cloned().transpose().ok().flatten(); - let image_task = image_task.clone(); - cx.new::(|cx| ImageHover { - image, - _task: cx.spawn(async move |this, cx| { - if let Ok(image) = image_task.clone().await { - this.update(cx, |this, cx| { - if this.image.replace(image).is_none() { - cx.notify(); - } - }) - .ok(); - } - }), - }) - .into() - } + cx.new::(|cx| ImageHover { + image, + _task: cx.spawn(async move |this, cx| { + if let Ok(image) = image_task.clone().await { + this.update(cx, |this, cx| { + if this.image.replace(image).is_none() { + cx.notify(); + } + }) + .ok(); + } + }), + }) + .into() }) - .into_any_element() - } - }) + }) + .child( + h_flex() + .gap_1() + .child( + Icon::from_path(self.icon.clone()) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child( + Label::new(self.label.clone()) + .size(LabelSize::Small) + .buffer_font(cx) + .single_line(), + ) + .map(|el| { + if self.loading.is_some() { + el.with_animation( + "loading-context-crease", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 0.8)), + |label, delta| label.opacity(delta), + ) + .into_any() + } else { + el.into_any() + } + }), + ) + } } struct ImageHover { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 87928767c686d1ec54585bb0ea7b5c8f2a673a28..3ad3ecbf618810f29256b531f38dee99e5ae54c2 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -277,6 +277,7 @@ pub struct AcpThreadView { terminal_expanded: bool, editing_message: Option, prompt_capabilities: Rc>, + is_loading_contents: bool, _cancel_task: Option>, _subscriptions: [Subscription; 3], } @@ -389,6 +390,7 @@ impl AcpThreadView { history_store, hovered_recent_history_item: None, prompt_capabilities, + is_loading_contents: false, _subscriptions: subscriptions, _cancel_task: None, focus_handle: cx.focus_handle(), @@ -823,6 +825,11 @@ impl AcpThreadView { fn send(&mut self, window: &mut Window, cx: &mut Context) { let Some(thread) = self.thread() else { return }; + + if self.is_loading_contents { + return; + } + self.history_store.update(cx, |history, cx| { history.push_recently_opened_entry( HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()), @@ -876,6 +883,15 @@ impl AcpThreadView { let Some(thread) = self.thread().cloned() else { return; }; + + self.is_loading_contents = true; + let guard = cx.new(|_| ()); + cx.observe_release(&guard, |this, _guard, cx| { + this.is_loading_contents = false; + cx.notify(); + }) + .detach(); + let task = cx.spawn_in(window, async move |this, cx| { let (contents, tracked_buffers) = contents.await?; @@ -896,6 +912,7 @@ impl AcpThreadView { action_log.buffer_read(buffer, cx) } }); + drop(guard); thread.send(contents, cx) })?; send.await @@ -950,19 +967,24 @@ impl AcpThreadView { let Some(thread) = self.thread().cloned() else { return; }; + if self.is_loading_contents { + return; + } - let Some(rewind) = thread.update(cx, |thread, cx| { - let user_message_id = thread.entries().get(entry_ix)?.user_message()?.id.clone()?; - Some(thread.rewind(user_message_id, cx)) + let Some(user_message_id) = thread.update(cx, |thread, _| { + thread.entries().get(entry_ix)?.user_message()?.id.clone() }) else { return; }; let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx)); - let task = cx.foreground_executor().spawn(async move { - rewind.await?; - contents.await + let task = cx.spawn(async move |_, cx| { + let contents = contents.await?; + thread + .update(cx, |thread, cx| thread.rewind(user_message_id, cx))? + .await?; + Ok(contents) }); self.send_impl(task, window, cx); } @@ -1341,25 +1363,34 @@ impl AcpThreadView { base_container .child( IconButton::new("cancel", IconName::Close) + .disabled(self.is_loading_contents) .icon_color(Color::Error) .icon_size(IconSize::XSmall) .on_click(cx.listener(Self::cancel_editing)) ) .child( - IconButton::new("regenerate", IconName::Return) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - .tooltip(Tooltip::text( - "Editing will restart the thread from this point." - )) - .on_click(cx.listener({ - let editor = editor.clone(); - move |this, _, window, cx| { - this.regenerate( - entry_ix, &editor, window, cx, - ); - } - })), + if self.is_loading_contents { + div() + .id("loading-edited-message-content") + .tooltip(Tooltip::text("Loading Added Context…")) + .child(loading_contents_spinner(IconSize::XSmall)) + .into_any_element() + } else { + IconButton::new("regenerate", IconName::Return) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .tooltip(Tooltip::text( + "Editing will restart the thread from this point." + )) + .on_click(cx.listener({ + let editor = editor.clone(); + move |this, _, window, cx| { + this.regenerate( + entry_ix, &editor, window, cx, + ); + } + })).into_any_element() + } ) ) } else { @@ -3542,7 +3573,14 @@ impl AcpThreadView { .thread() .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); - if is_generating && is_editor_empty { + if self.is_loading_contents { + div() + .id("loading-message-content") + .px_1() + .tooltip(Tooltip::text("Loading Added Context…")) + .child(loading_contents_spinner(IconSize::default())) + .into_any_element() + } else if is_generating && is_editor_empty { IconButton::new("stop-generation", IconName::Stop) .icon_color(Color::Error) .style(ButtonStyle::Tinted(ui::TintColor::Error)) @@ -4643,6 +4681,18 @@ impl AcpThreadView { } } +fn loading_contents_spinner(size: IconSize) -> AnyElement { + Icon::new(IconName::LoadCircle) + .size(size) + .color(Color::Accent) + .with_animation( + "load_context_circle", + Animation::new(Duration::from_secs(3)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + ) + .into_any_element() +} + impl Focusable for AcpThreadView { fn focus_handle(&self, cx: &App) -> FocusHandle { match self.thread_state { From 1b91f3de41bf86c1792d9bd4a8677a222ca4d903 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sat, 23 Aug 2025 20:02:23 -0400 Subject: [PATCH 300/744] acp: Fix accidentally reverted thread view changes (#36825) Merge conflict resolution for #36741 accidentally reverted the changes in #36670 to allow expanding terminals individually and in #36675 to allow collapsing edit cards. This PR re-applies those changes, fixing the regression. Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 27 +++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 3ad3ecbf618810f29256b531f38dee99e5ae54c2..d62ccf4cef9edfef09f19f75cf86169732742df4 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -274,7 +274,6 @@ pub struct AcpThreadView { edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, - terminal_expanded: bool, editing_message: Option, prompt_capabilities: Rc>, is_loading_contents: bool, @@ -386,7 +385,6 @@ impl AcpThreadView { edits_expanded: false, plan_expanded: false, editor_expanded: false, - terminal_expanded: true, history_store, hovered_recent_history_item: None, prompt_capabilities, @@ -1722,10 +1720,9 @@ impl AcpThreadView { matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some(); let use_card_layout = needs_confirmation || is_edit; - let is_collapsible = !tool_call.content.is_empty() && !use_card_layout; + let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; - let is_open = - needs_confirmation || is_edit || self.expanded_tool_calls.contains(&tool_call.id); + let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); let gradient_overlay = |color: Hsla| { div() @@ -2195,6 +2192,8 @@ impl AcpThreadView { .map(|path| format!("{}", path.display())) .unwrap_or_else(|| "current directory".to_string()); + let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); + let header = h_flex() .id(SharedString::from(format!( "terminal-tool-header-{}", @@ -2328,21 +2327,27 @@ impl AcpThreadView { "terminal-tool-disclosure-{}", terminal.entity_id() )), - self.terminal_expanded, + is_expanded, ) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) - .on_click(cx.listener(move |this, _event, _window, _cx| { - this.terminal_expanded = !this.terminal_expanded; - })), - ); + .on_click(cx.listener({ + let id = tool_call.id.clone(); + move |this, _event, _window, _cx| { + if is_expanded { + this.expanded_tool_calls.remove(&id); + } else { + this.expanded_tool_calls.insert(id.clone()); + } + }})), + ); let terminal_view = self .entry_view_state .read(cx) .entry(entry_ix) .and_then(|entry| entry.terminal(terminal)); - let show_output = self.terminal_expanded && terminal_view.is_some(); + let show_output = is_expanded && terminal_view.is_some(); v_flex() .mb_2() From de5f87e8f24eea848baa07fa733134b76a96dbce Mon Sep 17 00:00:00 2001 From: versecafe <147033096+versecafe@users.noreply.github.com> Date: Sat, 23 Aug 2025 23:54:47 -0700 Subject: [PATCH 301/744] languages: Add `module` to TS/JS keywords (#36830) image Release Notes: - Improved syntax highlights for `module` keyword in TS/JS --- crates/languages/src/javascript/highlights.scm | 3 ++- crates/languages/src/tsx/highlights.scm | 3 ++- crates/languages/src/typescript/highlights.scm | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/languages/src/javascript/highlights.scm b/crates/languages/src/javascript/highlights.scm index 9d5ebbaf711f931491ddb57f8a78e5fc38d36896..ebeac7efffb8770616fbc94ee4bbf3c25275a198 100644 --- a/crates/languages/src/javascript/highlights.scm +++ b/crates/languages/src/javascript/highlights.scm @@ -231,6 +231,7 @@ "implements" "interface" "keyof" + "module" "namespace" "private" "protected" @@ -250,4 +251,4 @@ (jsx_closing_element ([""]) @punctuation.bracket.jsx) (jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx) (jsx_attribute "=" @punctuation.delimiter.jsx) -(jsx_text) @text.jsx \ No newline at end of file +(jsx_text) @text.jsx diff --git a/crates/languages/src/tsx/highlights.scm b/crates/languages/src/tsx/highlights.scm index 5e2fbbf63ac9bce667599955c90bb5416dc29ec5..f7cb987831578f1d3e78decbf89f71c91d3a3b7e 100644 --- a/crates/languages/src/tsx/highlights.scm +++ b/crates/languages/src/tsx/highlights.scm @@ -237,6 +237,7 @@ "implements" "interface" "keyof" + "module" "namespace" "private" "protected" @@ -256,4 +257,4 @@ (jsx_closing_element ([""]) @punctuation.bracket.jsx) (jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx) (jsx_attribute "=" @punctuation.delimiter.jsx) -(jsx_text) @text.jsx \ No newline at end of file +(jsx_text) @text.jsx diff --git a/crates/languages/src/typescript/highlights.scm b/crates/languages/src/typescript/highlights.scm index af37ef6415ba501c1623977c04b7a7b7d110eeb5..84cbbae77d43c96e62578c444ee913055604e11a 100644 --- a/crates/languages/src/typescript/highlights.scm +++ b/crates/languages/src/typescript/highlights.scm @@ -248,6 +248,7 @@ "is" "keyof" "let" + "module" "namespace" "new" "of" @@ -272,4 +273,4 @@ "while" "with" "yield" -] @keyword \ No newline at end of file +] @keyword From dd6fce6d4eafdc6b2463e28af029a3e92b41e39f Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Sun, 24 Aug 2025 09:59:32 +0300 Subject: [PATCH 302/744] multi_buffer: Pre-allocate IDs when editing (#36819) Something I came across when looking at `edit_internal`. Potentially saves multiple re-allocations on an edit Release Notes: - N/A --- crates/multi_buffer/src/multi_buffer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index a54d38163da211883985d8eee798e29730da1938..e27cbf868a67eec98c0538a4410f4b7ff11b7d0c 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -835,7 +835,7 @@ impl MultiBuffer { this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns); drop(snapshot); - let mut buffer_ids = Vec::new(); + let mut buffer_ids = Vec::with_capacity(buffer_edits.len()); for (buffer_id, mut edits) in buffer_edits { buffer_ids.push(buffer_id); edits.sort_by_key(|edit| edit.range.start); From 54c7d9dc5fc915cf979d2df8f514ebd4b8f3fb2d Mon Sep 17 00:00:00 2001 From: Chuqiao Feng Date: Sun, 24 Aug 2025 19:01:42 +0800 Subject: [PATCH 303/744] Fix crash when opening inspector on Windows debug build (#36829) --- Cargo.lock | 1 + crates/inspector_ui/Cargo.toml | 1 + crates/inspector_ui/src/div_inspector.rs | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 0575796034e1e0725122418c61ff718afa2f09ea..c835b503ad214edea10225da8c3828d7ea32e616 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8468,6 +8468,7 @@ dependencies = [ "theme", "ui", "util", + "util_macros", "workspace", "workspace-hack", "zed_actions", diff --git a/crates/inspector_ui/Cargo.toml b/crates/inspector_ui/Cargo.toml index 8e55a8a477e5346bd12ec594b36ac04e197dfc8e..cefe888974da2c9d164ad97079441ddec2d7fdff 100644 --- a/crates/inspector_ui/Cargo.toml +++ b/crates/inspector_ui/Cargo.toml @@ -24,6 +24,7 @@ serde_json_lenient.workspace = true theme.workspace = true ui.workspace = true util.workspace = true +util_macros.workspace = true workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs index 0c2b16b9f49019bff1f7860ae5cd658ae20f12b5..c3d687e57adbb0b1883d81f6e7ba726d0d60974d 100644 --- a/crates/inspector_ui/src/div_inspector.rs +++ b/crates/inspector_ui/src/div_inspector.rs @@ -25,7 +25,7 @@ use util::split_str_with_ranges; /// Path used for unsaved buffer that contains style json. To support the json language server, this /// matches the name used in the generated schemas. -const ZED_INSPECTOR_STYLE_JSON: &str = "/zed-inspector-style.json"; +const ZED_INSPECTOR_STYLE_JSON: &str = util_macros::path!("/zed-inspector-style.json"); pub(crate) struct DivInspector { state: State, From d8bffd7ef298ccf6017e25299ce8d9d0bc1ba4aa Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Sun, 24 Aug 2025 13:05:39 +0200 Subject: [PATCH 304/744] acp: Cancel editing when focus is lost and message was not changed (#36822) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 2 +- crates/agent_ui/src/acp/message_editor.rs | 16 ++++++++++------ crates/agent_ui/src/acp/thread_view.rs | 13 +++++++++++++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index c748f2227577d37db674bd84748c8a5398da1f42..029d17505446cb3ef3d18f4c5ecb0ab6516fcad3 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -509,7 +509,7 @@ impl ContentBlock { "`Image`".into() } - fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { + pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { match self { ContentBlock::Empty => "", ContentBlock::Markdown { markdown } => markdown.read(cx).source(), diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index bab42e3da286692119574eb8b8f40fe05babb9b6..70faa0ed27af454a309738f4418a86831f423652 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -74,6 +74,7 @@ pub enum MessageEditorEvent { Send, Cancel, Focus, + LostFocus, } impl EventEmitter for MessageEditor {} @@ -131,10 +132,14 @@ impl MessageEditor { editor }); - cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| { + cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| { cx.emit(MessageEditorEvent::Focus) }) .detach(); + cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| { + cx.emit(MessageEditorEvent::LostFocus) + }) + .detach(); let mut subscriptions = Vec::new(); subscriptions.push(cx.subscribe_in(&editor, window, { @@ -1169,17 +1174,16 @@ impl MessageEditor { }) } + pub fn text(&self, cx: &App) -> String { + self.editor.read(cx).text(cx) + } + #[cfg(test)] pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.set_text(text, window, cx); }); } - - #[cfg(test)] - pub fn text(&self, cx: &App) -> String { - self.editor.read(cx).text(cx) - } } fn render_directory_contents(entries: Vec<(Arc, PathBuf, String)>) -> String { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index d62ccf4cef9edfef09f19f75cf86169732742df4..9caa4bad8c4432011ab6f1f6de6b8d8c348e7fc1 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -762,6 +762,7 @@ impl AcpThreadView { MessageEditorEvent::Focus => { self.cancel_editing(&Default::default(), window, cx); } + MessageEditorEvent::LostFocus => {} } } @@ -793,6 +794,18 @@ impl AcpThreadView { cx.notify(); } } + ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => { + if let Some(thread) = self.thread() + && let Some(AgentThreadEntry::UserMessage(user_message)) = + thread.read(cx).entries().get(event.entry_index) + && user_message.id.is_some() + { + if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) { + self.editing_message = None; + cx.notify(); + } + } + } ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => { self.regenerate(event.entry_index, editor, window, cx); } From a79aef7bdd38668215f1e916ef866f029ba8d9cb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 24 Aug 2025 18:30:34 +0200 Subject: [PATCH 305/744] acp: Never build a request with a tool use without its corresponding result (#36847) Release Notes: - N/A --- crates/agent2/src/tests/mod.rs | 76 ++++++++++++++++++++++++++++++++++ crates/agent2/src/thread.rs | 74 ++++++++++++++++----------------- 2 files changed, 113 insertions(+), 37 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 60b31980812a0d4fa3580d85128b0f39509326ce..5b935dae4cfc184177332547f24c45fe7197670e 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -4,6 +4,7 @@ use agent_client_protocol::{self as acp}; use agent_settings::AgentProfileId; use anyhow::Result; use client::{Client, UserStore}; +use cloud_llm_client::CompletionIntent; use context_server::{ContextServer, ContextServerCommand, ContextServerId}; use fs::{FakeFs, Fs}; use futures::{ @@ -1737,6 +1738,81 @@ async fn test_title_generation(cx: &mut TestAppContext) { thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); } +#[gpui::test] +async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let _events = thread + .update(cx, |thread, cx| { + thread.add_tool(ToolRequiringPermission); + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["Hey!"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + let permission_tool_use = LanguageModelToolUse { + id: "tool_id_1".into(), + name: ToolRequiringPermission::name().into(), + raw_input: "{}".into(), + input: json!({}), + is_input_complete: true, + }; + let echo_tool_use = LanguageModelToolUse { + id: "tool_id_2".into(), + name: EchoTool::name().into(), + raw_input: json!({"text": "test"}).to_string(), + input: json!({"text": "test"}), + is_input_complete: true, + }; + fake_model.send_last_completion_stream_text_chunk("Hi!"); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + permission_tool_use, + )); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + echo_tool_use.clone(), + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + // Ensure pending tools are skipped when building a request. + let request = thread + .read_with(cx, |thread, cx| { + thread.build_completion_request(CompletionIntent::EditFile, cx) + }) + .unwrap(); + assert_eq!( + request.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Hey!".into()], + cache: true + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec![ + MessageContent::Text("Hi!".into()), + MessageContent::ToolUse(echo_tool_use.clone()) + ], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec![MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id: echo_tool_use.id.clone(), + tool_name: echo_tool_use.name, + is_error: false, + content: "test".into(), + output: Some("test".into()) + })], + cache: false + }, + ], + ); +} + #[gpui::test] async fn test_agent_connection(cx: &mut TestAppContext) { cx.update(settings::init); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 6d616f73fc4a7a961c30dd317fe1c01e48ec90e6..c000027368e40a5a9d10eeff896d12e15fffacf0 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -448,24 +448,33 @@ impl AgentMessage { cache: false, }; for chunk in &self.content { - let chunk = match chunk { + match chunk { AgentMessageContent::Text(text) => { - language_model::MessageContent::Text(text.clone()) + assistant_message + .content + .push(language_model::MessageContent::Text(text.clone())); } AgentMessageContent::Thinking { text, signature } => { - language_model::MessageContent::Thinking { - text: text.clone(), - signature: signature.clone(), - } + assistant_message + .content + .push(language_model::MessageContent::Thinking { + text: text.clone(), + signature: signature.clone(), + }); } AgentMessageContent::RedactedThinking(value) => { - language_model::MessageContent::RedactedThinking(value.clone()) + assistant_message.content.push( + language_model::MessageContent::RedactedThinking(value.clone()), + ); } - AgentMessageContent::ToolUse(value) => { - language_model::MessageContent::ToolUse(value.clone()) + AgentMessageContent::ToolUse(tool_use) => { + if self.tool_results.contains_key(&tool_use.id) { + assistant_message + .content + .push(language_model::MessageContent::ToolUse(tool_use.clone())); + } } }; - assistant_message.content.push(chunk); } let mut user_message = LanguageModelRequestMessage { @@ -1315,23 +1324,6 @@ impl Thread { } } - pub fn build_system_message(&self, cx: &App) -> LanguageModelRequestMessage { - log::debug!("Building system message"); - let prompt = SystemPromptTemplate { - project: self.project_context.read(cx), - available_tools: self.tools.keys().cloned().collect(), - } - .render(&self.templates) - .context("failed to build system prompt") - .expect("Invalid template"); - log::debug!("System message built"); - LanguageModelRequestMessage { - role: Role::System, - content: vec![prompt.into()], - cache: true, - } - } - /// A helper method that's called on every streamed completion event. /// Returns an optional tool result task, which the main agentic loop will /// send back to the model when it resolves. @@ -1773,7 +1765,7 @@ impl Thread { pub(crate) fn build_completion_request( &self, completion_intent: CompletionIntent, - cx: &mut App, + cx: &App, ) -> Result { let model = self.model().context("No language model configured")?; let tools = if let Some(turn) = self.running_turn.as_ref() { @@ -1894,21 +1886,29 @@ impl Thread { "Building request messages from {} thread messages", self.messages.len() ); - let mut messages = vec![self.build_system_message(cx)]; + + let system_prompt = SystemPromptTemplate { + project: self.project_context.read(cx), + available_tools: self.tools.keys().cloned().collect(), + } + .render(&self.templates) + .context("failed to build system prompt") + .expect("Invalid template"); + let mut messages = vec![LanguageModelRequestMessage { + role: Role::System, + content: vec![system_prompt.into()], + cache: false, + }]; for message in &self.messages { messages.extend(message.to_request()); } - if let Some(message) = self.pending_message.as_ref() { - messages.extend(message.to_request()); + if let Some(last_message) = messages.last_mut() { + last_message.cache = true; } - if let Some(last_user_message) = messages - .iter_mut() - .rev() - .find(|message| message.role == Role::User) - { - last_user_message.cache = true; + if let Some(message) = self.pending_message.as_ref() { + messages.extend(message.to_request()); } messages From 11545c669e100392a8ca60063476037ab52c7cb5 Mon Sep 17 00:00:00 2001 From: Aleksei Gusev Date: Sun, 24 Aug 2025 19:57:12 +0300 Subject: [PATCH 306/744] Add file icons to multibuffer view (#36836) multi-buffer-icons-git-diff Unfortunately, `cargo format` decided to reformat everything. Probably, because of hitting the right margin, no idea. The essence of this change is the following: ```rust .map(|path_header| { let filename = filename .map(SharedString::from) .unwrap_or_else(|| "untitled".into()); let path = path::Path::new(filename.as_str()); let icon = FileIcons::get_icon(path, cx).unwrap_or_default(); let icon = Icon::from_path(icon).color(Color::Muted); let label = Label::new(filename).single_line().when_some( file_status, |el, status| { el.color(if status.is_conflicted() { Color::Conflict } else if status.is_modified() { Color::Modified } else if status.is_deleted() { Color::Disabled } else { Color::Created }) .when(status.is_deleted(), |el| el.strikethrough()) }, ); path_header.child(icon).child(label) }) ``` Release Notes: - Added file icons to multi buffer view --- crates/editor/src/element.rs | 339 ++++++++++++++++++----------------- 1 file changed, 175 insertions(+), 164 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 32582ba9411411230c9ff3e7339a951ff1fd33ff..4f3580da07750db5241fed9a4f313c5a191b36e6 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -74,7 +74,7 @@ use std::{ fmt::{self, Write}, iter, mem, ops::{Deref, Range}, - path::Path, + path::{self, Path}, rc::Rc, sync::Arc, time::{Duration, Instant}, @@ -90,8 +90,8 @@ use unicode_segmentation::UnicodeSegmentation; use util::post_inc; use util::{RangeExt, ResultExt, debug_panic}; use workspace::{ - CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item, - notifications::NotifyTaskExt, + CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, + item::Item, notifications::NotifyTaskExt, }; /// Determines what kinds of highlights should be applied to a lines background. @@ -3603,176 +3603,187 @@ impl EditorElement { let focus_handle = editor.focus_handle(cx); let colors = cx.theme().colors(); - let header = - div() - .p_1() - .w_full() - .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) - .child( - h_flex() - .size_full() - .gap_2() - .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) - .pl_0p5() - .pr_5() - .rounded_sm() - .when(is_sticky, |el| el.shadow_md()) - .border_1() - .map(|div| { - let border_color = if is_selected - && is_folded - && focus_handle.contains_focused(window, cx) - { - colors.border_focused - } else { - colors.border - }; - div.border_color(border_color) - }) - .bg(colors.editor_subheader_background) - .hover(|style| style.bg(colors.element_hover)) - .map(|header| { - let editor = self.editor.clone(); - let buffer_id = for_excerpt.buffer_id; - let toggle_chevron_icon = - FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); - header.child( - div() - .hover(|style| style.bg(colors.element_selected)) - .rounded_xs() - .child( - ButtonLike::new("toggle-buffer-fold") - .style(ui::ButtonStyle::Transparent) - .height(px(28.).into()) - .width(px(28.)) - .children(toggle_chevron_icon) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::with_meta_in( - "Toggle Excerpt Fold", - Some(&ToggleFold), - "Alt+click to toggle all", - &focus_handle, + let header = div() + .p_1() + .w_full() + .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) + .child( + h_flex() + .size_full() + .gap_2() + .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) + .pl_0p5() + .pr_5() + .rounded_sm() + .when(is_sticky, |el| el.shadow_md()) + .border_1() + .map(|div| { + let border_color = if is_selected + && is_folded + && focus_handle.contains_focused(window, cx) + { + colors.border_focused + } else { + colors.border + }; + div.border_color(border_color) + }) + .bg(colors.editor_subheader_background) + .hover(|style| style.bg(colors.element_hover)) + .map(|header| { + let editor = self.editor.clone(); + let buffer_id = for_excerpt.buffer_id; + let toggle_chevron_icon = + FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); + header.child( + div() + .hover(|style| style.bg(colors.element_selected)) + .rounded_xs() + .child( + ButtonLike::new("toggle-buffer-fold") + .style(ui::ButtonStyle::Transparent) + .height(px(28.).into()) + .width(px(28.)) + .children(toggle_chevron_icon) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::with_meta_in( + "Toggle Excerpt Fold", + Some(&ToggleFold), + "Alt+click to toggle all", + &focus_handle, + window, + cx, + ) + } + }) + .on_click(move |event, window, cx| { + if event.modifiers().alt { + // Alt+click toggles all buffers + editor.update(cx, |editor, cx| { + editor.toggle_fold_all( + &ToggleFoldAll, window, cx, - ) - } - }) - .on_click(move |event, window, cx| { - if event.modifiers().alt { - // Alt+click toggles all buffers + ); + }); + } else { + // Regular click toggles single buffer + if is_folded { editor.update(cx, |editor, cx| { - editor.toggle_fold_all( - &ToggleFoldAll, - window, - cx, - ); + editor.unfold_buffer(buffer_id, cx); }); } else { - // Regular click toggles single buffer - if is_folded { - editor.update(cx, |editor, cx| { - editor.unfold_buffer(buffer_id, cx); - }); - } else { - editor.update(cx, |editor, cx| { - editor.fold_buffer(buffer_id, cx); - }); - } + editor.update(cx, |editor, cx| { + editor.fold_buffer(buffer_id, cx); + }); } - }), - ), - ) - }) - .children( - editor - .addons - .values() - .filter_map(|addon| { - addon.render_buffer_header_controls(for_excerpt, window, cx) - }) - .take(1), - ) - .child( - h_flex() - .size(Pixels(12.0)) - .justify_center() - .children(indicator), + } + }), + ), ) - .child( - h_flex() - .cursor_pointer() - .id("path header block") - .size_full() - .justify_between() - .overflow_hidden() - .child( - h_flex() - .gap_2() - .child( - Label::new( - filename - .map(SharedString::from) - .unwrap_or_else(|| "untitled".into()), - ) - .single_line() - .when_some(file_status, |el, status| { - el.color(if status.is_conflicted() { - Color::Conflict - } else if status.is_modified() { - Color::Modified - } else if status.is_deleted() { - Color::Disabled - } else { - Color::Created - }) - .when(status.is_deleted(), |el| el.strikethrough()) - }), - ) - .when_some(parent_path, |then, path| { - then.child(div().child(path).text_color( - if file_status.is_some_and(FileStatus::is_deleted) { - colors.text_disabled - } else { - colors.text_muted + }) + .children( + editor + .addons + .values() + .filter_map(|addon| { + addon.render_buffer_header_controls(for_excerpt, window, cx) + }) + .take(1), + ) + .child( + h_flex() + .size(Pixels(12.0)) + .justify_center() + .children(indicator), + ) + .child( + h_flex() + .cursor_pointer() + .id("path header block") + .size_full() + .justify_between() + .overflow_hidden() + .child( + h_flex() + .gap_2() + .map(|path_header| { + let filename = filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()); + + path_header + .when(ItemSettings::get_global(cx).file_icons, |el| { + let path = path::Path::new(filename.as_str()); + let icon = FileIcons::get_icon(path, cx) + .unwrap_or_default(); + let icon = + Icon::from_path(icon).color(Color::Muted); + el.child(icon) + }) + .child(Label::new(filename).single_line().when_some( + file_status, + |el, status| { + el.color(if status.is_conflicted() { + Color::Conflict + } else if status.is_modified() { + Color::Modified + } else if status.is_deleted() { + Color::Disabled + } else { + Color::Created + }) + .when(status.is_deleted(), |el| { + el.strikethrough() + }) }, )) - }), - ) - .when( - can_open_excerpts && is_selected && relative_path.is_some(), - |el| { - el.child( - h_flex() - .id("jump-to-file-button") - .gap_2p5() - .child(Label::new("Jump To File")) - .children( - KeyBinding::for_action_in( - &OpenExcerpts, - &focus_handle, - window, - cx, - ) - .map(|binding| binding.into_any_element()), - ), - ) - }, - ) - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .on_click(window.listener_for(&self.editor, { - move |editor, e: &ClickEvent, window, cx| { - editor.open_excerpts_common( - Some(jump_data.clone()), - e.modifiers().secondary(), - window, - cx, - ); - } - })), - ), - ); + }) + .when_some(parent_path, |then, path| { + then.child(div().child(path).text_color( + if file_status.is_some_and(FileStatus::is_deleted) { + colors.text_disabled + } else { + colors.text_muted + }, + )) + }), + ) + .when( + can_open_excerpts && is_selected && relative_path.is_some(), + |el| { + el.child( + h_flex() + .id("jump-to-file-button") + .gap_2p5() + .child(Label::new("Jump To File")) + .children( + KeyBinding::for_action_in( + &OpenExcerpts, + &focus_handle, + window, + cx, + ) + .map(|binding| binding.into_any_element()), + ), + ) + }, + ) + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_click(window.listener_for(&self.editor, { + move |editor, e: &ClickEvent, window, cx| { + editor.open_excerpts_common( + Some(jump_data.clone()), + e.modifiers().secondary(), + window, + cx, + ); + } + })), + ), + ); let file = for_excerpt.buffer.file().cloned(); let editor = self.editor.clone(); From c48197b2804a17ecf8ec46781985d4f9cff35e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hendrik=20M=C3=BCller?= Date: Mon, 25 Aug 2025 11:28:33 +0200 Subject: [PATCH 307/744] util: Fix edge case when parsing paths (#36025) Searching for files broke a couple releases ago. It used to be possible to start typing part of a file name, then select a file (not confirm it yet) and then type in `:` and a line number to navigate directly to that line. The current behavior can be seen in the following screenshots. When the `:` is typed, the selection is lost, since no files match any more. Screenshot From 2025-08-12 10-36-08 Screenshot From 2025-08-12 10-36-25 Screenshot From 2025-08-12 10-36-47 --- With this PR, the previous behavior is restored and can be seen in these screenshots: Screenshot From 2025-08-12 10-36-08 Screenshot From 2025-08-12 10-47-07 Screenshot From 2025-08-12 10-47-21 --- Release Notes: - Adjusted the file finder to show matching file paths when adding the `:row:column` to the query --- crates/file_finder/src/file_finder.rs | 9 ++-- crates/file_finder/src/file_finder_tests.rs | 48 +++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 8aaaa047292065a9db0c47f980e559ca61c04546..75121523245cc9233fa4c66c9021d8a958524f6e 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1401,13 +1401,16 @@ impl PickerDelegate for FileFinderDelegate { #[cfg(windows)] let raw_query = raw_query.trim().to_owned().replace("/", "\\"); #[cfg(not(windows))] - let raw_query = raw_query.trim().to_owned(); + let raw_query = raw_query.trim(); - let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query { + let raw_query = raw_query.trim_end_matches(':').to_owned(); + let path = path_position.path.to_str(); + let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':'); + let file_query_end = if path_trimmed == raw_query { None } else { // Safe to unwrap as we won't get here when the unwrap in if fails - Some(path_position.path.to_str().unwrap().len()) + Some(path.unwrap().len()) }; let query = FileSearchQuery { diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index 8203d1b1fdf684c3dcbd1fb6c058a7b7e6bab9cb..cd0f203d6a300b4039df74a646bf0a9d56818347 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -218,6 +218,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) { " ndan ", " band ", "a bandana", + "bandana:", ] { picker .update_in(cx, |picker, window, cx| { @@ -252,6 +253,53 @@ async fn test_matching_paths(cx: &mut TestAppContext) { } } +#[gpui::test] +async fn test_matching_paths_with_colon(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + path!("/root"), + json!({ + "a": { + "foo:bar.rs": "", + "foo.rs": "", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; + + let (picker, _, cx) = build_find_picker(project, cx); + + // 'foo:' matches both files + cx.simulate_input("foo:"); + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 3); + assert_match_at_position(picker, 0, "foo.rs"); + assert_match_at_position(picker, 1, "foo:bar.rs"); + }); + + // 'foo:b' matches one of the files + cx.simulate_input("b"); + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 2); + assert_match_at_position(picker, 0, "foo:bar.rs"); + }); + + cx.dispatch_action(editor::actions::Backspace); + + // 'foo:1' matches both files, specifying which row to jump to + cx.simulate_input("1"); + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 3); + assert_match_at_position(picker, 0, "foo.rs"); + assert_match_at_position(picker, 1, "foo:bar.rs"); + }); +} + #[gpui::test] async fn test_unicode_paths(cx: &mut TestAppContext) { let app_state = init_test(cx); From fe5e81203f03e86ada3397b1738b9f0f79801368 Mon Sep 17 00:00:00 2001 From: versecafe <147033096+versecafe@users.noreply.github.com> Date: Mon, 25 Aug 2025 03:55:56 -0700 Subject: [PATCH 308/744] Fix macOS arch reporting from `arch_ios` to `arch_arm` (#36217) ```xml arch_kind arch_arm ``` Closes #36037 Release Notes: - N/A --- crates/zed/resources/info/SupportedPlatforms.plist | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 crates/zed/resources/info/SupportedPlatforms.plist diff --git a/crates/zed/resources/info/SupportedPlatforms.plist b/crates/zed/resources/info/SupportedPlatforms.plist new file mode 100644 index 0000000000000000000000000000000000000000..fd2a4101d8cfaf937fb5eca8eb24044ceb5164b6 --- /dev/null +++ b/crates/zed/resources/info/SupportedPlatforms.plist @@ -0,0 +1,4 @@ +CFBundleSupportedPlatforms + + MacOSX + From dfc99de7b8c3796ded1c5ec73b585f85ef7bc783 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 25 Aug 2025 08:18:23 -0300 Subject: [PATCH 309/744] thread view: Add a few UI tweaks (#36845) Release Notes: - N/A --- assets/icons/copy.svg | 5 +- crates/agent_ui/src/acp/thread_view.rs | 76 +++++++++++--------------- crates/markdown/src/markdown.rs | 11 ++-- 3 files changed, 43 insertions(+), 49 deletions(-) diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg index bca13f8d56a1b644051c5be2f17c0e4cc1cdb43b..aba193930bd1e93062b1e7eef3e4a0de2e7f4ab6 100644 --- a/assets/icons/copy.svg +++ b/assets/icons/copy.svg @@ -1 +1,4 @@ - + + + + diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 9caa4bad8c4432011ab6f1f6de6b8d8c348e7fc1..0b987e25b6b4c8a8662e1ac4fa74468830fcf458 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1306,7 +1306,11 @@ impl AcpThreadView { v_flex() .id(("user_message", entry_ix)) - .pt_2() + .map(|this| if rules_item.is_some() { + this.pt_3() + } else { + this.pt_2() + }) .pb_4() .px_2() .gap_1p5() @@ -1315,6 +1319,7 @@ impl AcpThreadView { .children(message.id.clone().and_then(|message_id| { message.checkpoint.as_ref()?.show.then(|| { h_flex() + .px_3() .gap_2() .child(Divider::horizontal()) .child( @@ -1492,9 +1497,7 @@ impl AcpThreadView { .child(self.render_thread_controls(cx)) .when_some( self.thread_feedback.comments_editor.clone(), - |this, editor| { - this.child(Self::render_feedback_feedback_editor(editor, window, cx)) - }, + |this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)), ) .into_any_element() } else { @@ -1725,6 +1728,7 @@ impl AcpThreadView { tool_call.status, ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed ); + let needs_confirmation = matches!( tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } @@ -1742,7 +1746,7 @@ impl AcpThreadView { .absolute() .top_0() .right_0() - .w_16() + .w_12() .h_full() .bg(linear_gradient( 90., @@ -1902,7 +1906,7 @@ impl AcpThreadView { .into_any() }), ) - .when(in_progress && use_card_layout, |this| { + .when(in_progress && use_card_layout && !is_open, |this| { this.child( div().absolute().right_2().child( Icon::new(IconName::ArrowCircle) @@ -2460,7 +2464,6 @@ impl AcpThreadView { Some( h_flex() .px_2p5() - .pb_1() .child( Icon::new(IconName::Attach) .size(IconSize::XSmall) @@ -2476,8 +2479,7 @@ impl AcpThreadView { Label::new(user_rules_text) .size(LabelSize::XSmall) .color(Color::Muted) - .truncate() - .buffer_font(cx), + .truncate(), ) .hover(|s| s.bg(cx.theme().colors().element_hover)) .tooltip(Tooltip::text("View User Rules")) @@ -2491,7 +2493,13 @@ impl AcpThreadView { }), ) }) - .when(has_both, |this| this.child(Divider::vertical())) + .when(has_both, |this| { + this.child( + Label::new("•") + .size(LabelSize::XSmall) + .color(Color::Disabled), + ) + }) .when_some(rules_file_text, |parent, rules_file_text| { parent.child( h_flex() @@ -2500,8 +2508,7 @@ impl AcpThreadView { .child( Label::new(rules_file_text) .size(LabelSize::XSmall) - .color(Color::Muted) - .buffer_font(cx), + .color(Color::Muted), ) .hover(|s| s.bg(cx.theme().colors().element_hover)) .tooltip(Tooltip::text("View Project Rules")) @@ -3078,13 +3085,13 @@ impl AcpThreadView { h_flex() .p_1() .justify_between() + .flex_wrap() .when(expanded, |this| { this.border_b_1().border_color(cx.theme().colors().border) }) .child( h_flex() .id("edits-container") - .w_full() .gap_1() .child(Disclosure::new("edits-disclosure", expanded)) .map(|this| { @@ -4177,13 +4184,8 @@ impl AcpThreadView { container.child(open_as_markdown).child(scroll_to_top) } - fn render_feedback_feedback_editor( - editor: Entity, - window: &mut Window, - cx: &Context, - ) -> Div { - let focus_handle = editor.focus_handle(cx); - v_flex() + fn render_feedback_feedback_editor(editor: Entity, cx: &Context) -> Div { + h_flex() .key_context("AgentFeedbackMessageEditor") .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| { this.thread_feedback.dismiss_comments(); @@ -4192,43 +4194,31 @@ impl AcpThreadView { .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| { this.submit_feedback_message(cx); })) - .mb_2() - .mx_4() .p_2() + .mb_2() + .mx_5() + .gap_1() .rounded_md() .border_1() .border_color(cx.theme().colors().border) .bg(cx.theme().colors().editor_background) - .child(editor) + .child(div().w_full().child(editor)) .child( h_flex() - .gap_1() - .justify_end() .child( - Button::new("dismiss-feedback-message", "Cancel") - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx) - .map(|kb| kb.size(rems_from_px(10.))), - ) + IconButton::new("dismiss-feedback-message", IconName::Close) + .icon_color(Color::Error) + .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) .on_click(cx.listener(move |this, _, _window, cx| { this.thread_feedback.dismiss_comments(); cx.notify(); })), ) .child( - Button::new("submit-feedback-message", "Share Feedback") - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &menu::Confirm, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(10.))), - ) + IconButton::new("submit-feedback-message", IconName::Return) + .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) .on_click(cx.listener(move |this, _, _window, cx| { this.submit_feedback_message(cx); })), diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 39a438c512163766cbda3b89df8421c4b79db9eb..f16da45d799fc9d3b988e76d51b26f89223ef596 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1085,10 +1085,10 @@ impl Element for MarkdownElement { ); el.child( h_flex() - .w_5() + .w_4() .absolute() - .top_1() - .right_1() + .top_1p5() + .right_1p5() .justify_end() .child(codeblock), ) @@ -1115,11 +1115,12 @@ impl Element for MarkdownElement { cx, ); el.child( - div() + h_flex() + .w_4() .absolute() .top_0() .right_0() - .w_5() + .justify_end() .visible_on_hover("code_block") .child(codeblock), ) From 8c83281399d013aab4415b4e425063be5a02b89e Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 25 Aug 2025 08:23:36 -0400 Subject: [PATCH 310/744] acp: Fix read_file tool flickering (#36854) We were rendering a Markdown link like `[Read file x.rs (lines Y-Z)](@selection)` while the tool ran, but then switching to just `x.rs` as soon as we got the file location from the tool call (due to an if/else in the UI code that applies to all tools). This caused a flicker, which is fixed by having `initial_title` return just the filename from the input as it arrives instead of a link that we're going to stop rendering almost immediately anyway. Release Notes: - N/A --- crates/agent2/src/tools/read_file_tool.rs | 29 ++++++----------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index 903e1582ac4dec2b8060b7070368991c865c716c..fea9732093cf3b12865e40adacf75ecf2916c0b1 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -10,7 +10,7 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; -use std::sync::Arc; +use std::{path::Path, sync::Arc}; use crate::{AgentTool, ToolCallEventStream}; @@ -68,27 +68,12 @@ impl AgentTool for ReadFileTool { } fn initial_title(&self, input: Result) -> SharedString { - if let Ok(input) = input { - let path = &input.path; - match (input.start_line, input.end_line) { - (Some(start), Some(end)) => { - format!( - "[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))", - path, start, end, path, start, end - ) - } - (Some(start), None) => { - format!( - "[Read file `{}` (from line {})](@selection:{}:({}-{}))", - path, start, path, start, start - ) - } - _ => format!("[Read file `{}`](@file:{})", path, path), - } - .into() - } else { - "Read file".into() - } + input + .ok() + .as_ref() + .and_then(|input| Path::new(&input.path).file_name()) + .map(|file_name| file_name.to_string_lossy().to_string().into()) + .unwrap_or_default() } fn run( From 4c0ad95acc50c8bc509e5845991c811dbcf1a513 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 25 Aug 2025 14:52:25 +0200 Subject: [PATCH 311/744] acp: Show retry button for errors (#36862) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/acp_thread/src/acp_thread.rs | 6 +- crates/acp_thread/src/connection.rs | 8 +- crates/agent2/src/agent.rs | 8 +- crates/agent2/src/tests/mod.rs | 98 ++++++++++++++++--- crates/agent2/src/thread.rs | 124 +++++++++++++------------ crates/agent_ui/src/acp/thread_view.rs | 46 ++++++++- 6 files changed, 212 insertions(+), 78 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 029d17505446cb3ef3d18f4c5ecb0ab6516fcad3..d9a7a2582a65d7037fe2868a55f631a18a22ec11 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1373,6 +1373,10 @@ impl AcpThread { }) } + pub fn can_resume(&self, cx: &App) -> bool { + self.connection.resume(&self.session_id, cx).is_some() + } + pub fn resume(&mut self, cx: &mut Context) -> BoxFuture<'static, Result<()>> { self.run_turn(cx, async move |this, cx| { this.update(cx, |this, cx| { @@ -2659,7 +2663,7 @@ mod tests { fn truncate( &self, session_id: &acp::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { Some(Rc::new(FakeAgentSessionEditor { _session_id: session_id.clone(), diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 91e46dbac164b754b07be7597765d13d116673d2..5f5032e588f44b09e2559ead45fff93ae6c97687 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -43,7 +43,7 @@ pub trait AgentConnection { fn resume( &self, _session_id: &acp::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { None } @@ -53,7 +53,7 @@ pub trait AgentConnection { fn truncate( &self, _session_id: &acp::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { None } @@ -61,7 +61,7 @@ pub trait AgentConnection { fn set_title( &self, _session_id: &acp::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { None } @@ -439,7 +439,7 @@ mod test_support { fn truncate( &self, _session_id: &agent_client_protocol::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { Some(Rc::new(StubAgentSessionEditor)) } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 4eaf87e218e52500470e3cece86f8f946dee2dea..415933b7d1b8d8ecf29489ebf6c209e528693c29 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -936,7 +936,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { fn resume( &self, session_id: &acp::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { Some(Rc::new(NativeAgentSessionResume { connection: self.clone(), @@ -956,9 +956,9 @@ impl acp_thread::AgentConnection for NativeAgentConnection { fn truncate( &self, session_id: &agent_client_protocol::SessionId, - cx: &mut App, + cx: &App, ) -> Option> { - self.0.update(cx, |agent, _cx| { + self.0.read_with(cx, |agent, _cx| { agent.sessions.get(session_id).map(|session| { Rc::new(NativeAgentSessionEditor { thread: session.thread.clone(), @@ -971,7 +971,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { fn set_title( &self, session_id: &acp::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { Some(Rc::new(NativeAgentSessionSetTitle { connection: self.clone(), diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 5b935dae4cfc184177332547f24c45fe7197670e..87ecc1037cb6df9d06af37c0479606b0e97dd0d0 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -5,6 +5,7 @@ use agent_settings::AgentProfileId; use anyhow::Result; use client::{Client, UserStore}; use cloud_llm_client::CompletionIntent; +use collections::IndexMap; use context_server::{ContextServer, ContextServerCommand, ContextServerId}; use fs::{FakeFs, Fs}; use futures::{ @@ -673,15 +674,6 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { "} ) }); - - // Ensure we error if calling resume when tool use limit was *not* reached. - let error = thread - .update(cx, |thread, cx| thread.resume(cx)) - .unwrap_err(); - assert_eq!( - error.to_string(), - "can only resume after tool use limit is reached" - ) } #[gpui::test] @@ -2105,6 +2097,7 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { .unwrap(); cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hey,"); fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded { provider: LanguageModelProviderName::new("Anthropic"), retry_after: Some(Duration::from_secs(3)), @@ -2114,8 +2107,9 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { cx.executor().advance_clock(Duration::from_secs(3)); cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Hey!"); + fake_model.send_last_completion_stream_text_chunk("there!"); fake_model.end_last_completion_stream(); + cx.run_until_parked(); let mut retry_events = Vec::new(); while let Some(Ok(event)) = events.next().await { @@ -2143,12 +2137,94 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { ## Assistant - Hey! + Hey, + + [resume] + + ## Assistant + + there! "} ) }); } +#[gpui::test] +async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) { + let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let events = thread + .update(cx, |thread, cx| { + thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["Call the echo tool!"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + let tool_use_1 = LanguageModelToolUse { + id: "tool_1".into(), + name: EchoTool::name().into(), + raw_input: json!({"text": "test"}).to_string(), + input: json!({"text": "test"}), + is_input_complete: true, + }; + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + tool_use_1.clone(), + )); + fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded { + provider: LanguageModelProviderName::new("Anthropic"), + retry_after: Some(Duration::from_secs(3)), + }); + fake_model.end_last_completion_stream(); + + cx.executor().advance_clock(Duration::from_secs(3)); + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + completion.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Call the echo tool!".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec![language_model::MessageContent::ToolResult( + LanguageModelToolResult { + tool_use_id: tool_use_1.id.clone(), + tool_name: tool_use_1.name.clone(), + is_error: false, + content: "test".into(), + output: Some("test".into()) + } + )], + cache: true + }, + ] + ); + + fake_model.send_last_completion_stream_text_chunk("Done"); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + events.collect::>().await; + thread.read_with(cx, |thread, _cx| { + assert_eq!( + thread.last_message(), + Some(Message::Agent(AgentMessage { + content: vec![AgentMessageContent::Text("Done".into())], + tool_results: IndexMap::default() + })) + ); + }) +} + #[gpui::test] async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index c000027368e40a5a9d10eeff896d12e15fffacf0..43f391ca64aeae018dcd3650ba5a8571a3d0005c 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -123,7 +123,7 @@ impl Message { match self { Message::User(message) => message.to_markdown(), Message::Agent(message) => message.to_markdown(), - Message::Resume => "[resumed after tool use limit was reached]".into(), + Message::Resume => "[resume]\n".into(), } } @@ -1085,11 +1085,6 @@ impl Thread { &mut self, cx: &mut Context, ) -> Result>> { - anyhow::ensure!( - self.tool_use_limit_reached, - "can only resume after tool use limit is reached" - ); - self.messages.push(Message::Resume); cx.notify(); @@ -1216,12 +1211,13 @@ impl Thread { cx: &mut AsyncApp, ) -> Result<()> { log::debug!("Stream completion started successfully"); - let request = this.update(cx, |this, cx| { - this.build_completion_request(completion_intent, cx) - })??; let mut attempt = None; - 'retry: loop { + loop { + let request = this.update(cx, |this, cx| { + this.build_completion_request(completion_intent, cx) + })??; + telemetry::event!( "Agent Thread Completion", thread_id = this.read_with(cx, |this, _| this.id.to_string())?, @@ -1236,10 +1232,11 @@ impl Thread { attempt.unwrap_or(0) ); let mut events = model - .stream_completion(request.clone(), cx) + .stream_completion(request, cx) .await .map_err(|error| anyhow!(error))?; let mut tool_results = FuturesUnordered::new(); + let mut error = None; while let Some(event) = events.next().await { match event { @@ -1249,51 +1246,9 @@ impl Thread { this.handle_streamed_completion_event(event, event_stream, cx) })??); } - Err(error) => { - let completion_mode = - this.read_with(cx, |thread, _cx| thread.completion_mode())?; - if completion_mode == CompletionMode::Normal { - return Err(anyhow!(error))?; - } - - let Some(strategy) = Self::retry_strategy_for(&error) else { - return Err(anyhow!(error))?; - }; - - let max_attempts = match &strategy { - RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, - RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, - }; - - let attempt = attempt.get_or_insert(0u8); - - *attempt += 1; - - let attempt = *attempt; - if attempt > max_attempts { - return Err(anyhow!(error))?; - } - - let delay = match &strategy { - RetryStrategy::ExponentialBackoff { initial_delay, .. } => { - let delay_secs = - initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); - Duration::from_secs(delay_secs) - } - RetryStrategy::Fixed { delay, .. } => *delay, - }; - log::debug!("Retry attempt {attempt} with delay {delay:?}"); - - event_stream.send_retry(acp_thread::RetryStatus { - last_error: error.to_string().into(), - attempt: attempt as usize, - max_attempts: max_attempts as usize, - started_at: Instant::now(), - duration: delay, - }); - - cx.background_executor().timer(delay).await; - continue 'retry; + Err(err) => { + error = Some(err); + break; } } } @@ -1320,7 +1275,58 @@ impl Thread { })?; } - return Ok(()); + if let Some(error) = error { + let completion_mode = this.read_with(cx, |thread, _cx| thread.completion_mode())?; + if completion_mode == CompletionMode::Normal { + return Err(anyhow!(error))?; + } + + let Some(strategy) = Self::retry_strategy_for(&error) else { + return Err(anyhow!(error))?; + }; + + let max_attempts = match &strategy { + RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, + RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, + }; + + let attempt = attempt.get_or_insert(0u8); + + *attempt += 1; + + let attempt = *attempt; + if attempt > max_attempts { + return Err(anyhow!(error))?; + } + + let delay = match &strategy { + RetryStrategy::ExponentialBackoff { initial_delay, .. } => { + let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); + Duration::from_secs(delay_secs) + } + RetryStrategy::Fixed { delay, .. } => *delay, + }; + log::debug!("Retry attempt {attempt} with delay {delay:?}"); + + event_stream.send_retry(acp_thread::RetryStatus { + last_error: error.to_string().into(), + attempt: attempt as usize, + max_attempts: max_attempts as usize, + started_at: Instant::now(), + duration: delay, + }); + cx.background_executor().timer(delay).await; + this.update(cx, |this, cx| { + this.flush_pending_message(cx); + if let Some(Message::Agent(message)) = this.messages.last() { + if message.tool_results.is_empty() { + this.messages.push(Message::Resume); + } + } + })?; + } else { + return Ok(()); + } } } @@ -1737,6 +1743,10 @@ impl Thread { return; }; + if message.content.is_empty() { + return; + } + for content in &message.content { let AgentMessageContent::ToolUse(tool_use) = content else { continue; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 0b987e25b6b4c8a8662e1ac4fa74468830fcf458..5674b15c98c8f5eb13a684758638cafeba1678f0 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -820,6 +820,9 @@ impl AcpThreadView { let Some(thread) = self.thread() else { return; }; + if !thread.read(cx).can_resume(cx) { + return; + } let task = thread.update(cx, |thread, cx| thread.resume(cx)); cx.spawn(async move |this, cx| { @@ -4459,12 +4462,53 @@ impl AcpThreadView { } fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout { + let can_resume = self + .thread() + .map_or(false, |thread| thread.read(cx).can_resume(cx)); + + let can_enable_burn_mode = self.as_native_thread(cx).map_or(false, |thread| { + let thread = thread.read(cx); + let supports_burn_mode = thread + .model() + .map_or(false, |model| model.supports_burn_mode()); + supports_burn_mode && thread.completion_mode() == CompletionMode::Normal + }); + Callout::new() .severity(Severity::Error) .title("Error") .icon(IconName::XCircle) .description(error.clone()) - .actions_slot(self.create_copy_button(error.to_string())) + .actions_slot( + h_flex() + .gap_0p5() + .when(can_resume && can_enable_burn_mode, |this| { + this.child( + Button::new("enable-burn-mode-and-retry", "Enable Burn Mode and Retry") + .icon(IconName::ZedBurnMode) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + this.toggle_burn_mode(&ToggleBurnMode, window, cx); + this.resume_chat(cx); + })), + ) + }) + .when(can_resume, |this| { + this.child( + Button::new("retry", "Retry") + .icon(IconName::RotateCw) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.resume_chat(cx); + })), + ) + }) + .child(self.create_copy_button(error.to_string())), + ) .dismiss_action(self.dismiss_error_button(cx)) } From 2b5a3029727f6aa031f6771add3cc4a8cc85515a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:08:48 -0300 Subject: [PATCH 312/744] thread view: Prevent user message controls to be cut-off (#36865) In the thread view, when focusing on the user message, we display the editing control container absolutely-positioned in the top right. However, if there are no rules items and no restore checkpoint button _and_ it is the very first message, the editing controls container would be cut-off. This PR fixes that by giving it a bit more top padding. Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 5674b15c98c8f5eb13a684758638cafeba1678f0..25f2745f75607d2e07b1f0f825d39bd5f5137cd9 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1305,14 +1305,23 @@ impl AcpThreadView { None }; + let has_checkpoint_button = message + .checkpoint + .as_ref() + .is_some_and(|checkpoint| checkpoint.show); + let agent_name = self.agent.name(); v_flex() .id(("user_message", entry_ix)) - .map(|this| if rules_item.is_some() { - this.pt_3() - } else { - this.pt_2() + .map(|this| { + if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none() { + this.pt_4() + } else if rules_item.is_some() { + this.pt_3() + } else { + this.pt_2() + } }) .pb_4() .px_2() From db949546cf477818da206f12f5e0dfa45f2e038a Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 25 Aug 2025 15:14:48 +0200 Subject: [PATCH 313/744] agent2: Less noisy logs (#36863) Release Notes: - N/A --- crates/agent2/src/agent.rs | 10 ++++----- crates/agent2/src/native_agent_server.rs | 4 ++-- crates/agent2/src/thread.rs | 26 ++++++++++++------------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 415933b7d1b8d8ecf29489ebf6c209e528693c29..1576c3cf96c56540de615ae94999c95ba08d7492 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -180,7 +180,7 @@ impl NativeAgent { fs: Arc, cx: &mut AsyncApp, ) -> Result> { - log::info!("Creating new NativeAgent"); + log::debug!("Creating new NativeAgent"); let project_context = cx .update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))? @@ -756,7 +756,7 @@ impl NativeAgentConnection { } } - log::info!("Response stream completed"); + log::debug!("Response stream completed"); anyhow::Ok(acp::PromptResponse { stop_reason: acp::StopReason::EndTurn, }) @@ -781,7 +781,7 @@ impl AgentModelSelector for NativeAgentConnection { model_id: acp_thread::AgentModelId, cx: &mut App, ) -> Task> { - log::info!("Setting model for session {}: {}", session_id, model_id); + log::debug!("Setting model for session {}: {}", session_id, model_id); let Some(thread) = self .0 .read(cx) @@ -852,7 +852,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { cx: &mut App, ) -> Task>> { let agent = self.0.clone(); - log::info!("Creating new thread for project at: {:?}", cwd); + log::debug!("Creating new thread for project at: {:?}", cwd); cx.spawn(async move |cx| { log::debug!("Starting thread creation in async context"); @@ -917,7 +917,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .into_iter() .map(Into::into) .collect::>(); - log::info!("Converted prompt to message: {} chars", content.len()); + log::debug!("Converted prompt to message: {} chars", content.len()); log::debug!("Message id: {:?}", id); log::debug!("Message content: {:?}", content); diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 12d3c79d1bf1b046dfd7703ffa089684039039c4..33ee44c9a36c14976f3213e627043532daf5fa47 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -44,7 +44,7 @@ impl AgentServer for NativeAgentServer { project: &Entity, cx: &mut App, ) -> Task>> { - log::info!( + log::debug!( "NativeAgentServer::connect called for path: {:?}", _root_dir ); @@ -63,7 +63,7 @@ impl AgentServer for NativeAgentServer { // Create the connection wrapper let connection = NativeAgentConnection(agent); - log::info!("NativeAgentServer connection established successfully"); + log::debug!("NativeAgentServer connection established successfully"); Ok(Rc::new(connection) as Rc) }) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 43f391ca64aeae018dcd3650ba5a8571a3d0005c..4bbbdbdec7517c73cce47794d8ccbae7885a2245 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1088,7 +1088,7 @@ impl Thread { self.messages.push(Message::Resume); cx.notify(); - log::info!("Total messages in thread: {}", self.messages.len()); + log::debug!("Total messages in thread: {}", self.messages.len()); self.run_turn(cx) } @@ -1106,7 +1106,7 @@ impl Thread { { let model = self.model().context("No language model configured")?; - log::info!("Thread::send called with model: {:?}", model.name()); + log::info!("Thread::send called with model: {}", model.name().0); self.advance_prompt_id(); let content = content.into_iter().map(Into::into).collect::>(); @@ -1116,7 +1116,7 @@ impl Thread { .push(Message::User(UserMessage { id, content })); cx.notify(); - log::info!("Total messages in thread: {}", self.messages.len()); + log::debug!("Total messages in thread: {}", self.messages.len()); self.run_turn(cx) } @@ -1140,7 +1140,7 @@ impl Thread { event_stream: event_stream.clone(), tools: self.enabled_tools(profile, &model, cx), _task: cx.spawn(async move |this, cx| { - log::info!("Starting agent turn execution"); + log::debug!("Starting agent turn execution"); let turn_result: Result<()> = async { let mut intent = CompletionIntent::UserPrompt; @@ -1165,7 +1165,7 @@ impl Thread { log::info!("Tool use limit reached, completing turn"); return Err(language_model::ToolUseLimitReachedError.into()); } else if end_turn { - log::info!("No tool uses found, completing turn"); + log::debug!("No tool uses found, completing turn"); return Ok(()); } else { intent = CompletionIntent::ToolResults; @@ -1177,7 +1177,7 @@ impl Thread { match turn_result { Ok(()) => { - log::info!("Turn execution completed"); + log::debug!("Turn execution completed"); event_stream.send_stop(acp::StopReason::EndTurn); } Err(error) => { @@ -1227,7 +1227,7 @@ impl Thread { attempt ); - log::info!( + log::debug!( "Calling model.stream_completion, attempt {}", attempt.unwrap_or(0) ); @@ -1254,7 +1254,7 @@ impl Thread { } while let Some(tool_result) = tool_results.next().await { - log::info!("Tool finished {:?}", tool_result); + log::debug!("Tool finished {:?}", tool_result); event_stream.update_tool_call_fields( &tool_result.tool_use_id, @@ -1528,7 +1528,7 @@ impl Thread { }); let supports_images = self.model().is_some_and(|model| model.supports_images()); let tool_result = tool.run(tool_use.input, tool_event_stream, cx); - log::info!("Running tool {}", tool_use.name); + log::debug!("Running tool {}", tool_use.name); Some(cx.foreground_executor().spawn(async move { let tool_result = tool_result.await.and_then(|output| { if let LanguageModelToolResultContent::Image(_) = &output.llm_output @@ -1640,7 +1640,7 @@ impl Thread { summary.extend(lines.next()); } - log::info!("Setting summary: {}", summary); + log::debug!("Setting summary: {}", summary); let summary = SharedString::from(summary); this.update(cx, |this, cx| { @@ -1657,7 +1657,7 @@ impl Thread { return; }; - log::info!( + log::debug!( "Generating title with model: {:?}", self.summarization_model.as_ref().map(|model| model.name()) ); @@ -1799,8 +1799,8 @@ impl Thread { log::debug!("Completion mode: {:?}", self.completion_mode); let messages = self.build_request_messages(cx); - log::info!("Request will include {} messages", messages.len()); - log::info!("Request includes {} tools", tools.len()); + log::debug!("Request will include {} messages", messages.len()); + log::debug!("Request includes {} tools", tools.len()); let request = LanguageModelRequest { thread_id: Some(self.id.to_string()), From 69127d2beaf69900505c420adef9310a5aeb9694 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 25 Aug 2025 15:38:19 +0200 Subject: [PATCH 314/744] acp: Simplify control flow for native agent loop (#36868) Release Notes: - N/A Co-authored-by: Bennet Bo Fenner --- crates/agent2/src/thread.rs | 162 ++++++++++++++++-------------------- 1 file changed, 72 insertions(+), 90 deletions(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 4bbbdbdec7517c73cce47794d8ccbae7885a2245..2d1e608297b4e3045765446a4263378dedfbacaf 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1142,37 +1142,7 @@ impl Thread { _task: cx.spawn(async move |this, cx| { log::debug!("Starting agent turn execution"); - let turn_result: Result<()> = async { - let mut intent = CompletionIntent::UserPrompt; - loop { - Self::stream_completion(&this, &model, intent, &event_stream, cx).await?; - - let mut end_turn = true; - this.update(cx, |this, cx| { - // Generate title if needed. - if this.title.is_none() && this.pending_title_generation.is_none() { - this.generate_title(cx); - } - - // End the turn if the model didn't use tools. - let message = this.pending_message.as_ref(); - end_turn = - message.map_or(true, |message| message.tool_results.is_empty()); - this.flush_pending_message(cx); - })?; - - if this.read_with(cx, |this, _| this.tool_use_limit_reached)? { - log::info!("Tool use limit reached, completing turn"); - return Err(language_model::ToolUseLimitReachedError.into()); - } else if end_turn { - log::debug!("No tool uses found, completing turn"); - return Ok(()); - } else { - intent = CompletionIntent::ToolResults; - } - } - } - .await; + let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await; _ = this.update(cx, |this, cx| this.flush_pending_message(cx)); match turn_result { @@ -1203,20 +1173,17 @@ impl Thread { Ok(events_rx) } - async fn stream_completion( + async fn run_turn_internal( this: &WeakEntity, - model: &Arc, - completion_intent: CompletionIntent, + model: Arc, event_stream: &ThreadEventStream, cx: &mut AsyncApp, ) -> Result<()> { - log::debug!("Stream completion started successfully"); - - let mut attempt = None; + let mut attempt = 0; + let mut intent = CompletionIntent::UserPrompt; loop { - let request = this.update(cx, |this, cx| { - this.build_completion_request(completion_intent, cx) - })??; + let request = + this.update(cx, |this, cx| this.build_completion_request(intent, cx))??; telemetry::event!( "Agent Thread Completion", @@ -1227,23 +1194,19 @@ impl Thread { attempt ); - log::debug!( - "Calling model.stream_completion, attempt {}", - attempt.unwrap_or(0) - ); + log::debug!("Calling model.stream_completion, attempt {}", attempt); let mut events = model .stream_completion(request, cx) .await .map_err(|error| anyhow!(error))?; let mut tool_results = FuturesUnordered::new(); let mut error = None; - while let Some(event) = events.next().await { + log::trace!("Received completion event: {:?}", event); match event { Ok(event) => { - log::trace!("Received completion event: {:?}", event); tool_results.extend(this.update(cx, |this, cx| { - this.handle_streamed_completion_event(event, event_stream, cx) + this.handle_completion_event(event, event_stream, cx) })??); } Err(err) => { @@ -1253,6 +1216,7 @@ impl Thread { } } + let end_turn = tool_results.is_empty(); while let Some(tool_result) = tool_results.next().await { log::debug!("Tool finished {:?}", tool_result); @@ -1275,65 +1239,83 @@ impl Thread { })?; } - if let Some(error) = error { - let completion_mode = this.read_with(cx, |thread, _cx| thread.completion_mode())?; - if completion_mode == CompletionMode::Normal { - return Err(anyhow!(error))?; - } - - let Some(strategy) = Self::retry_strategy_for(&error) else { - return Err(anyhow!(error))?; - }; - - let max_attempts = match &strategy { - RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, - RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, - }; - - let attempt = attempt.get_or_insert(0u8); - - *attempt += 1; - - let attempt = *attempt; - if attempt > max_attempts { - return Err(anyhow!(error))?; + this.update(cx, |this, cx| { + this.flush_pending_message(cx); + if this.title.is_none() && this.pending_title_generation.is_none() { + this.generate_title(cx); } + })?; - let delay = match &strategy { - RetryStrategy::ExponentialBackoff { initial_delay, .. } => { - let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); - Duration::from_secs(delay_secs) - } - RetryStrategy::Fixed { delay, .. } => *delay, - }; - log::debug!("Retry attempt {attempt} with delay {delay:?}"); - - event_stream.send_retry(acp_thread::RetryStatus { - last_error: error.to_string().into(), - attempt: attempt as usize, - max_attempts: max_attempts as usize, - started_at: Instant::now(), - duration: delay, - }); - cx.background_executor().timer(delay).await; - this.update(cx, |this, cx| { - this.flush_pending_message(cx); + if let Some(error) = error { + attempt += 1; + let retry = + this.update(cx, |this, _| this.handle_completion_error(error, attempt))??; + let timer = cx.background_executor().timer(retry.duration); + event_stream.send_retry(retry); + timer.await; + this.update(cx, |this, _cx| { if let Some(Message::Agent(message)) = this.messages.last() { if message.tool_results.is_empty() { + intent = CompletionIntent::UserPrompt; this.messages.push(Message::Resume); } } })?; - } else { + } else if this.read_with(cx, |this, _| this.tool_use_limit_reached)? { + return Err(language_model::ToolUseLimitReachedError.into()); + } else if end_turn { return Ok(()); + } else { + intent = CompletionIntent::ToolResults; + attempt = 0; } } } + fn handle_completion_error( + &mut self, + error: LanguageModelCompletionError, + attempt: u8, + ) -> Result { + if self.completion_mode == CompletionMode::Normal { + return Err(anyhow!(error)); + } + + let Some(strategy) = Self::retry_strategy_for(&error) else { + return Err(anyhow!(error)); + }; + + let max_attempts = match &strategy { + RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, + RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, + }; + + if attempt > max_attempts { + return Err(anyhow!(error)); + } + + let delay = match &strategy { + RetryStrategy::ExponentialBackoff { initial_delay, .. } => { + let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); + Duration::from_secs(delay_secs) + } + RetryStrategy::Fixed { delay, .. } => *delay, + }; + log::debug!("Retry attempt {attempt} with delay {delay:?}"); + + Ok(acp_thread::RetryStatus { + last_error: error.to_string().into(), + attempt: attempt as usize, + max_attempts: max_attempts as usize, + started_at: Instant::now(), + duration: delay, + }) + } + /// A helper method that's called on every streamed completion event. /// Returns an optional tool result task, which the main agentic loop will /// send back to the model when it resolves. - fn handle_streamed_completion_event( + fn handle_completion_event( &mut self, event: LanguageModelCompletionEvent, event_stream: &ThreadEventStream, From fda5111dc0239e3003d3c0d26346270c356cbc9f Mon Sep 17 00:00:00 2001 From: Zach Riegel Date: Mon, 25 Aug 2025 08:30:09 -0700 Subject: [PATCH 315/744] Add CSS language injections for calls to `styled` (#33966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …emotion). Closes: https://github.com/zed-industries/zed/issues/17026 Release Notes: - Added CSS language injection support for styled-components and emotion in JavaScript, TypeScript, and TSX files. --- crates/languages/src/javascript/injections.scm | 15 +++++++++++++++ crates/languages/src/tsx/injections.scm | 15 +++++++++++++++ crates/languages/src/typescript/injections.scm | 15 +++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/crates/languages/src/javascript/injections.scm b/crates/languages/src/javascript/injections.scm index 7baba5f227eb0df31cd753029296e165dfff0180..dbec1937b12a24d336d69051d70e45d0eee5b3de 100644 --- a/crates/languages/src/javascript/injections.scm +++ b/crates/languages/src/javascript/injections.scm @@ -11,6 +11,21 @@ (#set! injection.language "css")) ) +(call_expression + function: (member_expression + object: (identifier) @_obj (#eq? @_obj "styled") + property: (property_identifier)) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + +(call_expression + function: (call_expression + function: (identifier) @_name (#eq? @_name "styled")) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string) @injection.content diff --git a/crates/languages/src/tsx/injections.scm b/crates/languages/src/tsx/injections.scm index 48da80995bba86765e3dc78748eea6b4d5811bed..9eec01cc8962b6c807db77a5f8bd2ff1707b4a0d 100644 --- a/crates/languages/src/tsx/injections.scm +++ b/crates/languages/src/tsx/injections.scm @@ -11,6 +11,21 @@ (#set! injection.language "css")) ) +(call_expression + function: (member_expression + object: (identifier) @_obj (#eq? @_obj "styled") + property: (property_identifier)) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + +(call_expression + function: (call_expression + function: (identifier) @_name (#eq? @_name "styled")) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string (string_fragment) @injection.content diff --git a/crates/languages/src/typescript/injections.scm b/crates/languages/src/typescript/injections.scm index 7affdc5b758deb5ff717476f0de934a1786469aa..1ca1e9ad59176cc1df9461d6fe8630179162e45c 100644 --- a/crates/languages/src/typescript/injections.scm +++ b/crates/languages/src/typescript/injections.scm @@ -15,6 +15,21 @@ (#set! injection.language "css")) ) +(call_expression + function: (member_expression + object: (identifier) @_obj (#eq? @_obj "styled") + property: (property_identifier)) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + +(call_expression + function: (call_expression + function: (identifier) @_name (#eq? @_name "styled")) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string) @injection.content From 2fe3dbed31147cc869bdb01aea4b7fae57f5fdc8 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 25 Aug 2025 21:00:53 +0530 Subject: [PATCH 316/744] project: Remove redundant Option from parse_register_capabilities (#36874) Release Notes: - N/A --- crates/project/src/lsp_store.rs | 83 +++++++++++++++------------------ 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index d2958dce01cd06b1cf74b007d3f7520102706544..853490ddac127304bed3afa6a70e2a6e5aa9eb6c 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11706,12 +11706,11 @@ impl LspStore { // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. } "workspace/symbol" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.workspace_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.workspace_symbol_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "workspace/fileOperations" => { if let Some(options) = reg.register_options { @@ -11735,12 +11734,11 @@ impl LspStore { } } "textDocument/rangeFormatting" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.document_range_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.document_range_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/onTypeFormatting" => { if let Some(options) = reg @@ -11755,36 +11753,32 @@ impl LspStore { } } "textDocument/formatting" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.document_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.document_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/rename" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.rename_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.rename_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/inlayHint" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.inlay_hint_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.inlay_hint_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/documentSymbol" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.document_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.document_symbol_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/codeAction" => { if let Some(options) = reg @@ -11800,12 +11794,11 @@ impl LspStore { } } "textDocument/definition" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.definition_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.definition_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/completion" => { if let Some(caps) = reg @@ -12184,10 +12177,10 @@ impl LspStore { // https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133 fn parse_register_capabilities( reg: lsp::Registration, -) -> anyhow::Result>> { +) -> Result> { Ok(match reg.register_options { - Some(options) => Some(OneOf::Right(serde_json::from_value::(options)?)), - None => Some(OneOf::Left(true)), + Some(options) => OneOf::Right(serde_json::from_value::(options)?), + None => OneOf::Left(true), }) } From 65fb17e2c9f817e7d7776cc406e3c7f3291fd24d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Aug 2025 09:34:30 -0600 Subject: [PATCH 317/744] acp: Remember following state (#36793) A beta user reported that following was "lost" when asking for confirmation, I suspect they moved their cursor in the agent file while reviewing the change. Now we will resume following when the agent starts up again. Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 73 ++++++++++++++++++++------ 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index d9a7a2582a65d7037fe2868a55f631a18a22ec11..cc338795865b8bbb21510cb0b1bee9de17754b07 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -774,7 +774,7 @@ pub enum AcpThreadEvent { impl EventEmitter for AcpThread {} -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Debug)] pub enum ThreadStatus { Idle, WaitingForToolConfirmation, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 25f2745f75607d2e07b1f0f825d39bd5f5137cd9..609777e2d18cea7dc2346ed1602066cb8db3821c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -274,6 +274,7 @@ pub struct AcpThreadView { edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, + should_be_following: bool, editing_message: Option, prompt_capabilities: Rc>, is_loading_contents: bool, @@ -385,6 +386,7 @@ impl AcpThreadView { edits_expanded: false, plan_expanded: false, editor_expanded: false, + should_be_following: false, history_store, hovered_recent_history_item: None, prompt_capabilities, @@ -897,6 +899,13 @@ impl AcpThreadView { let Some(thread) = self.thread().cloned() else { return; }; + if self.should_be_following { + self.workspace + .update(cx, |workspace, cx| { + workspace.follow(CollaboratorId::Agent, window, cx); + }) + .ok(); + } self.is_loading_contents = true; let guard = cx.new(|_| ()); @@ -938,6 +947,16 @@ impl AcpThreadView { this.handle_thread_error(err, cx); }) .ok(); + } else { + this.update(cx, |this, cx| { + this.should_be_following = this + .workspace + .update(cx, |workspace, _| { + workspace.is_being_followed(CollaboratorId::Agent) + }) + .unwrap_or_default(); + }) + .ok(); } }) .detach(); @@ -1254,6 +1273,7 @@ impl AcpThreadView { tool_call_id: acp::ToolCallId, option_id: acp::PermissionOptionId, option_kind: acp::PermissionOptionKind, + window: &mut Window, cx: &mut Context, ) { let Some(thread) = self.thread() else { @@ -1262,6 +1282,13 @@ impl AcpThreadView { thread.update(cx, |thread, cx| { thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx); }); + if self.should_be_following { + self.workspace + .update(cx, |workspace, cx| { + workspace.follow(CollaboratorId::Agent, window, cx); + }) + .ok(); + } cx.notify(); } @@ -2095,11 +2122,12 @@ impl AcpThreadView { let tool_call_id = tool_call_id.clone(); let option_id = option.id.clone(); let option_kind = option.kind; - move |this, _, _, cx| { + move |this, _, window, cx| { this.authorize_tool_call( tool_call_id.clone(), option_id.clone(), option_kind, + window, cx, ); } @@ -3652,13 +3680,34 @@ impl AcpThreadView { } } - fn render_follow_toggle(&self, cx: &mut Context) -> impl IntoElement { - let following = self - .workspace - .read_with(cx, |workspace, _| { - workspace.is_being_followed(CollaboratorId::Agent) + fn is_following(&self, cx: &App) -> bool { + match self.thread().map(|thread| thread.read(cx).status()) { + Some(ThreadStatus::Generating) => self + .workspace + .read_with(cx, |workspace, _| { + workspace.is_being_followed(CollaboratorId::Agent) + }) + .unwrap_or(false), + _ => self.should_be_following, + } + } + + fn toggle_following(&mut self, window: &mut Window, cx: &mut Context) { + let following = self.is_following(cx); + self.should_be_following = !following; + self.workspace + .update(cx, |workspace, cx| { + if following { + workspace.unfollow(CollaboratorId::Agent, window, cx); + } else { + workspace.follow(CollaboratorId::Agent, window, cx); + } }) - .unwrap_or(false); + .ok(); + } + + fn render_follow_toggle(&self, cx: &mut Context) -> impl IntoElement { + let following = self.is_following(cx); IconButton::new("follow-agent", IconName::Crosshair) .icon_size(IconSize::Small) @@ -3679,15 +3728,7 @@ impl AcpThreadView { } }) .on_click(cx.listener(move |this, _, window, cx| { - this.workspace - .update(cx, |workspace, cx| { - if following { - workspace.unfollow(CollaboratorId::Agent, window, cx); - } else { - workspace.follow(CollaboratorId::Agent, window, cx); - } - }) - .ok(); + this.toggle_following(window, cx); })) } From 557753d092e167422ae28df5d4399612dc59e893 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 25 Aug 2025 17:46:07 +0200 Subject: [PATCH 318/744] acp: Add Reauthenticate to dropdown (#36878) Release Notes: - N/A Co-authored-by: Conrad Irwin --- crates/agent_ui/src/acp/thread_view.rs | 18 ++++++++++++++++++ crates/agent_ui/src/agent_panel.rs | 13 +++++++++++++ crates/zed_actions/src/lib.rs | 4 +++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 609777e2d18cea7dc2346ed1602066cb8db3821c..18a65ec634cc5e8abc6c0aecb601afb7cb076b1c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4746,6 +4746,24 @@ impl AcpThreadView { })) } + pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context) { + let agent = self.agent.clone(); + let ThreadState::Ready { thread, .. } = &self.thread_state else { + return; + }; + + let connection = thread.read(cx).connection().clone(); + let err = AuthRequired { + description: None, + provider_id: None, + }; + self.clear_thread_error(cx); + let this = cx.weak_entity(); + window.defer(cx, |window, cx| { + Self::handle_auth_required(this, err, agent, connection, window, cx); + }) + } + fn upgrade_button(&self, cx: &mut Context) -> impl IntoElement { Button::new("upgrade", "Upgrade") .label_size(LabelSize::Small) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 50f9fc6a457d5aa060dfe6cf98e09e838a35a549..f1a8a744ee63af2ef5121d636bd6aee6f3f1d61b 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -9,6 +9,7 @@ use agent_servers::AgentServerSettings; use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; +use zed_actions::agent::ReauthenticateAgent; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::agent_diff::AgentDiffThread; @@ -2204,6 +2205,8 @@ impl AgentPanel { "Enable Full Screen" }; + let selected_agent = self.selected_agent.clone(); + PopoverMenu::new("agent-options-menu") .trigger_with_tooltip( IconButton::new("agent-options-menu", IconName::Ellipsis) @@ -2283,6 +2286,11 @@ impl AgentPanel { .action("Settings", Box::new(OpenSettings)) .separator() .action(full_screen_label, Box::new(ToggleZoom)); + + if selected_agent == AgentType::Gemini { + menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent)) + } + menu })) } @@ -3751,6 +3759,11 @@ impl Render for AgentPanel { } })) .on_action(cx.listener(Self::toggle_burn_mode)) + .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| { + if let Some(thread_view) = this.active_thread_view() { + thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx)) + } + })) .child(self.render_toolbar(window, cx)) .children(self.render_onboarding(window, cx)) .map(|parent| match &self.active_view { diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 069abc0a12736564bb72849ab54cc2bfbce11a2b..a5223a2cdf6d7a7799050be9e8b5e77df003cd9c 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -290,7 +290,9 @@ pub mod agent { Chat, /// Toggles the language model selector dropdown. #[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])] - ToggleModelSelector + ToggleModelSelector, + /// Triggers re-authentication on Gemini + ReauthenticateAgent ] ); } From 2dc4f156b387ccd4698fbf1a5e54ea7050b738ca Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Aug 2025 11:51:31 -0400 Subject: [PATCH 319/744] Revert "Capture `shorthand_field_initializer` and modules in Rust highlights (#35842)" (#36880) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR reverts https://github.com/zed-industries/zed/pull/35842, as it broke the syntax highlighting for `crate`: ### Before Revert Screenshot 2025-08-25 at 11 29 50 AM ### After Revert Screenshot 2025-08-25 at 11 32 17 AM This reverts commit 896a35f7befce468427a30489adf88c851b9507d. Release Notes: - Reverted https://github.com/zed-industries/zed/pull/35842. --- crates/languages/src/rust/highlights.scm | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/crates/languages/src/rust/highlights.scm b/crates/languages/src/rust/highlights.scm index 9c02fbedaa6bc9013fe889daae156cc130eda4f3..1c46061827cd504df669aadacd0a489172d1ce5a 100644 --- a/crates/languages/src/rust/highlights.scm +++ b/crates/languages/src/rust/highlights.scm @@ -6,9 +6,6 @@ (self) @variable.special (field_identifier) @property -(shorthand_field_initializer - (identifier) @property) - (trait_item name: (type_identifier) @type.interface) (impl_item trait: (type_identifier) @type.interface) (abstract_type trait: (type_identifier) @type.interface) @@ -41,20 +38,11 @@ (identifier) @function.special (scoped_identifier name: (identifier) @function.special) - ] - "!" @function.special) + ]) (macro_definition name: (identifier) @function.special.definition) -(mod_item - name: (identifier) @module) - -(visibility_modifier [ - (crate) @keyword - (super) @keyword -]) - ; Identifier conventions ; Assume uppercase names are types/enum-constructors @@ -127,7 +115,9 @@ "where" "while" "yield" + (crate) (mutable_specifier) + (super) ] @keyword [ @@ -199,7 +189,6 @@ operator: "/" @operator (lifetime) @lifetime -(lifetime (identifier) @lifetime) (parameter (identifier) @variable.parameter) From a102b087438c0424ce32a47177b7e16132aa24df Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Aug 2025 10:03:07 -0600 Subject: [PATCH 320/744] Require confirmation for fetch tool (#36881) Using prompt injection, the agent may be tricked into making a fetch request that includes unexpected data from the conversation in the URL. As agent conversations may contain sensitive information (like private code, or potentially even API keys), this seems bad. The easiest way to prevent this is to require the user to look at the URL before the model is allowed to fetch it. Thanks to @ant4g0nist for bringing this to our attention. Release Notes: - agent panel: The fetch tool now requires confirmation. --- crates/agent2/src/tools/fetch_tool.rs | 9 +++++++-- crates/assistant_tools/src/fetch_tool.rs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/agent2/src/tools/fetch_tool.rs b/crates/agent2/src/tools/fetch_tool.rs index 0313c4e4c2f03a2ec40afdf9325ca2c84c45cf3c..dd97271a799d11daf09e95147c18ab07d55e1caf 100644 --- a/crates/agent2/src/tools/fetch_tool.rs +++ b/crates/agent2/src/tools/fetch_tool.rs @@ -136,12 +136,17 @@ impl AgentTool for FetchTool { fn run( self: Arc, input: Self::Input, - _event_stream: ToolCallEventStream, + event_stream: ToolCallEventStream, cx: &mut App, ) -> Task> { + let authorize = event_stream.authorize(input.url.clone(), cx); + let text = cx.background_spawn({ let http_client = self.http_client.clone(); - async move { Self::build_message(http_client, &input.url).await } + async move { + authorize.await?; + Self::build_message(http_client, &input.url).await + } }); cx.foreground_executor().spawn(async move { diff --git a/crates/assistant_tools/src/fetch_tool.rs b/crates/assistant_tools/src/fetch_tool.rs index 79e205f205d02ba2a3f977163d2296423f71d9da..cc22c9fc09f73914720c4b639f8d273207d7ca53 100644 --- a/crates/assistant_tools/src/fetch_tool.rs +++ b/crates/assistant_tools/src/fetch_tool.rs @@ -118,7 +118,7 @@ impl Tool for FetchTool { } fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false + true } fn may_perform_edits(&self) -> bool { From 5c346a4ccf3642e8e804db70c59616ac3cb0f86a Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 25 Aug 2025 19:12:33 +0200 Subject: [PATCH 321/744] kotlin: Specify default language server (#36871) As of https://github.com/zed-extensions/kotlin/commit/db52fc3655df8594a89b3a6b539274f23dfa2f28, the Kotlin extension has two language servers. However, following that change, no default language server for Kotlin was configured within this repo, which led to two language servers being activated for Kotlin by default. This PR makes `kotlin-language-server` the default language server for the extension. This also ensures that the [documentation within the repository](https://github.com/zed-extensions/kotlin?tab=readme-ov-file#kotlin-lsp) matches what is actually the case. Release Notes: - kotlin: Made `kotlin-language-server` the default language server. --- assets/settings/default.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/settings/default.json b/assets/settings/default.json index ac26952c7f634223b10abdfc107ad8c27a5b17ac..59450dcc156c32638b881e141825bcc4ac3ac2fc 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1629,6 +1629,9 @@ "allowed": true } }, + "Kotlin": { + "language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."] + }, "LaTeX": { "formatter": "language_server", "language_servers": ["texlab", "..."], From 2e1ca472414792eea4f9a8ae4eabb469b76f1cf3 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Aug 2025 13:21:20 -0400 Subject: [PATCH 322/744] Make fields of `AiUpsellCard` private (#36888) This PR makes the fields of the `AiUpsellCard` private, for better encapsulation. Release Notes: - N/A --- crates/ai_onboarding/src/ai_upsell_card.rs | 15 ++++++++++----- crates/onboarding/src/ai_setup_page.rs | 18 +++++++----------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index e9639ca075d1190ef6ab13f1bb01dd7333010d86..106dcb0aef0ee35836b2c7c576d7c68799ea988a 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -12,11 +12,11 @@ use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions} #[derive(IntoElement, RegisterComponent)] pub struct AiUpsellCard { - pub sign_in_status: SignInStatus, - pub sign_in: Arc, - pub account_too_young: bool, - pub user_plan: Option, - pub tab_index: Option, + sign_in_status: SignInStatus, + sign_in: Arc, + account_too_young: bool, + user_plan: Option, + tab_index: Option, } impl AiUpsellCard { @@ -43,6 +43,11 @@ impl AiUpsellCard { tab_index: None, } } + + pub fn tab_index(mut self, tab_index: Option) -> Self { + self.tab_index = tab_index; + self + } } impl RenderOnce for AiUpsellCard { diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 672bcf1cd992ee128f885825d60d805bbb011c40..54c49bc72a49309002421c4f8ac3544c86e4dc69 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -283,17 +283,13 @@ pub(crate) fn render_ai_setup_page( v_flex() .mt_2() .gap_6() - .child({ - let mut ai_upsell_card = - AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx); - - ai_upsell_card.tab_index = Some({ - tab_index += 1; - tab_index - 1 - }); - - ai_upsell_card - }) + .child( + AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx) + .tab_index(Some({ + tab_index += 1; + tab_index - 1 + })), + ) .child(render_llm_provider_section( &mut tab_index, workspace, From f1204dfc333ceb83b5769c0f3ab876fc96e39252 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 25 Aug 2025 10:46:36 -0700 Subject: [PATCH 323/744] Revert "workspace: Disable padding on zoomed panels" (#36884) Reverts zed-industries/zed#36012 We thought we didn't need this UI, but it turns out it was load bearing :) Release Notes: - Restored the zoomed panel padding --- crates/workspace/src/workspace.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index bf58786d677f424eb4c3ea39a9b7bcd79ef46083..3654df09beaf74e88e3214ceed9b0700f56a6470 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6622,15 +6622,25 @@ impl Render for Workspace { } }) .children(self.zoomed.as_ref().and_then(|view| { - Some(div() + let zoomed_view = view.upgrade()?; + let div = div() .occlude() .absolute() .overflow_hidden() .border_color(colors.border) .bg(colors.background) - .child(view.upgrade()?) + .child(zoomed_view) .inset_0() - .shadow_lg()) + .shadow_lg(); + + Some(match self.zoomed_position { + Some(DockPosition::Left) => div.right_2().border_r_1(), + Some(DockPosition::Right) => div.left_2().border_l_1(), + Some(DockPosition::Bottom) => div.top_2().border_t_1(), + None => { + div.top_2().bottom_2().left_2().right_2().border_1() + } + }) })) .children(self.render_notifications(window, cx)), ) From 5fd29d37a63539c991ebae477bd4a78c849e0a78 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 25 Aug 2025 14:28:11 -0400 Subject: [PATCH 324/744] acp: Model-specific prompt capabilities for 1PA (#36879) Adds support for per-session prompt capabilities and capability changes on the Zed side (ACP itself still only has per-connection static capabilities for now), and uses it to reflect image support accurately in 1PA threads based on the currently-selected model. Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 38 +++++++++++++++++------ crates/acp_thread/src/connection.rs | 18 +++++------ crates/agent2/src/agent.rs | 13 +++----- crates/agent2/src/thread.rs | 21 +++++++++++++ crates/agent_servers/src/acp.rs | 9 +++--- crates/agent_servers/src/claude.rs | 16 +++++----- crates/agent_ui/src/acp/message_editor.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 20 ++++++------ crates/agent_ui/src/agent_diff.rs | 1 + crates/watch/src/watch.rs | 13 ++++++++ 10 files changed, 98 insertions(+), 53 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index cc338795865b8bbb21510cb0b1bee9de17754b07..779f9964da1d3197a4290c94972e7707ba814250 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -756,6 +756,8 @@ pub struct AcpThread { connection: Rc, session_id: acp::SessionId, token_usage: Option, + prompt_capabilities: acp::PromptCapabilities, + _observe_prompt_capabilities: Task>, } #[derive(Debug)] @@ -770,6 +772,7 @@ pub enum AcpThreadEvent { Stopped, Error, LoadError(LoadError), + PromptCapabilitiesUpdated, } impl EventEmitter for AcpThread {} @@ -821,7 +824,20 @@ impl AcpThread { project: Entity, action_log: Entity, session_id: acp::SessionId, + mut prompt_capabilities_rx: watch::Receiver, + cx: &mut Context, ) -> Self { + let prompt_capabilities = *prompt_capabilities_rx.borrow(); + let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| { + loop { + let caps = prompt_capabilities_rx.recv().await?; + this.update(cx, |this, cx| { + this.prompt_capabilities = caps; + cx.emit(AcpThreadEvent::PromptCapabilitiesUpdated); + })?; + } + }); + Self { action_log, shared_buffers: Default::default(), @@ -833,9 +849,15 @@ impl AcpThread { connection, session_id, token_usage: None, + prompt_capabilities, + _observe_prompt_capabilities: task, } } + pub fn prompt_capabilities(&self) -> acp::PromptCapabilities { + self.prompt_capabilities + } + pub fn connection(&self) -> &Rc { &self.connection } @@ -2599,13 +2621,19 @@ mod tests { .into(), ); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let thread = cx.new(|_cx| { + let thread = cx.new(|cx| { AcpThread::new( "Test", self.clone(), project, action_log, session_id.clone(), + watch::Receiver::constant(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }), + cx, ) }); self.sessions.lock().insert(session_id, thread.downgrade()); @@ -2639,14 +2667,6 @@ mod tests { } } - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - acp::PromptCapabilities { - image: true, - audio: true, - embedded_context: true, - } - } - fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { let sessions = self.sessions.lock(); let thread = sessions.get(session_id).unwrap().clone(); diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 5f5032e588f44b09e2559ead45fff93ae6c97687..af229b7545651c2f19f361afc7ea0abadcb5cc76 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -38,8 +38,6 @@ pub trait AgentConnection { cx: &mut App, ) -> Task>; - fn prompt_capabilities(&self) -> acp::PromptCapabilities; - fn resume( &self, _session_id: &acp::SessionId, @@ -329,13 +327,19 @@ mod test_support { ) -> Task>> { let session_id = acp::SessionId(self.sessions.lock().len().to_string().into()); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let thread = cx.new(|_cx| { + let thread = cx.new(|cx| { AcpThread::new( "Test", self.clone(), project, action_log, session_id.clone(), + watch::Receiver::constant(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }), + cx, ) }); self.sessions.lock().insert( @@ -348,14 +352,6 @@ mod test_support { Task::ready(Ok(thread)) } - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - acp::PromptCapabilities { - image: true, - audio: true, - embedded_context: true, - } - } - fn authenticate( &self, _method_id: acp::AuthMethodId, diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 1576c3cf96c56540de615ae94999c95ba08d7492..ecfaea4b4967a019da83edb3416cef66464c229e 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -240,13 +240,16 @@ impl NativeAgent { let title = thread.title(); let project = thread.project.clone(); let action_log = thread.action_log.clone(); - let acp_thread = cx.new(|_cx| { + let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone(); + let acp_thread = cx.new(|cx| { acp_thread::AcpThread::new( title, connection, project.clone(), action_log.clone(), session_id.clone(), + prompt_capabilities_rx, + cx, ) }); let subscriptions = vec![ @@ -925,14 +928,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }) } - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - acp::PromptCapabilities { - image: true, - audio: false, - embedded_context: true, - } - } - fn resume( &self, session_id: &acp::SessionId, diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 2d1e608297b4e3045765446a4263378dedfbacaf..1b1c014b7930091271b541e9d7cd12281274ec14 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -575,11 +575,22 @@ pub struct Thread { templates: Arc, model: Option>, summarization_model: Option>, + prompt_capabilities_tx: watch::Sender, + pub(crate) prompt_capabilities_rx: watch::Receiver, pub(crate) project: Entity, pub(crate) action_log: Entity, } impl Thread { + fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities { + let image = model.map_or(true, |model| model.supports_images()); + acp::PromptCapabilities { + image, + audio: false, + embedded_context: true, + } + } + pub fn new( project: Entity, project_context: Entity, @@ -590,6 +601,8 @@ impl Thread { ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); let action_log = cx.new(|_cx| ActionLog::new(project.clone())); + let (prompt_capabilities_tx, prompt_capabilities_rx) = + watch::channel(Self::prompt_capabilities(model.as_deref())); Self { id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()), prompt_id: PromptId::new(), @@ -617,6 +630,8 @@ impl Thread { templates, model, summarization_model: None, + prompt_capabilities_tx, + prompt_capabilities_rx, project, action_log, } @@ -750,6 +765,8 @@ impl Thread { .or_else(|| registry.default_model()) .map(|model| model.model) }); + let (prompt_capabilities_tx, prompt_capabilities_rx) = + watch::channel(Self::prompt_capabilities(model.as_deref())); Self { id, @@ -779,6 +796,8 @@ impl Thread { project, action_log, updated_at: db_thread.updated_at, + prompt_capabilities_tx, + prompt_capabilities_rx, } } @@ -946,10 +965,12 @@ impl Thread { pub fn set_model(&mut self, model: Arc, cx: &mut Context) { let old_usage = self.latest_token_usage(); self.model = Some(model); + let new_caps = Self::prompt_capabilities(self.model.as_deref()); let new_usage = self.latest_token_usage(); if old_usage != new_usage { cx.emit(TokenUsageUpdated(new_usage)); } + self.prompt_capabilities_tx.send(new_caps).log_err(); cx.notify() } diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index c9c938c6c0bae74ba0dc23c4b2e37c03c18d2e59..5a4efe12e50572041109fc4e5540cbcb2fb2ca7d 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -185,13 +185,16 @@ impl AgentConnection for AcpConnection { let session_id = response.session_id; let action_log = cx.new(|_| ActionLog::new(project.clone()))?; - let thread = cx.new(|_cx| { + let thread = cx.new(|cx| { AcpThread::new( self.server_name.clone(), self.clone(), project, action_log, session_id.clone(), + // ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically. + watch::Receiver::constant(self.prompt_capabilities), + cx, ) })?; @@ -279,10 +282,6 @@ impl AgentConnection for AcpConnection { }) } - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - self.prompt_capabilities - } - fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { session.suppress_abort_err = true; diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 048563103f658d1290657a04cbe71742056ae69c..6006bf3edbee05408a45d502f243c396bcaf05fe 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -249,13 +249,19 @@ impl AgentConnection for ClaudeAgentConnection { }); let action_log = cx.new(|_| ActionLog::new(project.clone()))?; - let thread = cx.new(|_cx| { + let thread = cx.new(|cx| { AcpThread::new( "Claude Code", self.clone(), project, action_log, session_id.clone(), + watch::Receiver::constant(acp::PromptCapabilities { + image: true, + audio: false, + embedded_context: true, + }), + cx, ) })?; @@ -319,14 +325,6 @@ impl AgentConnection for ClaudeAgentConnection { cx.foreground_executor().spawn(async move { end_rx.await? }) } - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - acp::PromptCapabilities { - image: true, - audio: false, - embedded_context: true, - } - } - fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { let sessions = self.sessions.borrow(); let Some(session) = sessions.get(session_id) else { diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 70faa0ed27af454a309738f4418a86831f423652..12ae893c317b9a5518b78fdc4c4d7ab7c315eba7 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -373,7 +373,7 @@ impl MessageEditor { if Img::extensions().contains(&extension) && !extension.contains("svg") { if !self.prompt_capabilities.get().image { - return Task::ready(Err(anyhow!("This agent does not support images yet"))); + return Task::ready(Err(anyhow!("This model does not support images yet"))); } let task = self .project diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 18a65ec634cc5e8abc6c0aecb601afb7cb076b1c..faba18acb1ffd4fe7b15c97c49f81eff227bc7be 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -474,7 +474,7 @@ impl AcpThreadView { let action_log = thread.read(cx).action_log().clone(); this.prompt_capabilities - .set(connection.prompt_capabilities()); + .set(thread.read(cx).prompt_capabilities()); let count = thread.read(cx).entries().len(); this.list_state.splice(0..0, count); @@ -1163,6 +1163,10 @@ impl AcpThreadView { }); } } + AcpThreadEvent::PromptCapabilitiesUpdated => { + self.prompt_capabilities + .set(thread.read(cx).prompt_capabilities()); + } AcpThreadEvent::TokenUsageUpdated => {} } cx.notify(); @@ -5367,6 +5371,12 @@ pub(crate) mod tests { project, action_log, SessionId("test".into()), + watch::Receiver::constant(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }), + cx, ) }))) } @@ -5375,14 +5385,6 @@ pub(crate) mod tests { &[] } - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - acp::PromptCapabilities { - image: true, - audio: true, - embedded_context: true, - } - } - fn authenticate( &self, _method_id: acp::AuthMethodId, diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index e07424987c48ef917b9470adbdfdbc6812957967..1e1ff95178308e20988019305b0546a169acba8f 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1529,6 +1529,7 @@ impl AgentDiff { | AcpThreadEvent::TokenUsageUpdated | AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::ToolAuthorizationRequired + | AcpThreadEvent::PromptCapabilitiesUpdated | AcpThreadEvent::Retry(_) => {} } } diff --git a/crates/watch/src/watch.rs b/crates/watch/src/watch.rs index f0ed5b4a186b8b524e5d2038b14ff92372374c4e..71dab748200e6ba1e3ecf0a5baaf4290bef68b59 100644 --- a/crates/watch/src/watch.rs +++ b/crates/watch/src/watch.rs @@ -162,6 +162,19 @@ impl Receiver { pending_waker_id: None, } } + + /// Creates a new [`Receiver`] holding an initial value that will never change. + pub fn constant(value: T) -> Self { + let state = Arc::new(RwLock::new(State { + value, + wakers: BTreeMap::new(), + next_waker_id: WakerId::default(), + version: 0, + closed: false, + })); + + Self { state, version: 0 } + } } impl Receiver { From c786c0150f6b315ba4074117241b90bddd8f00fc Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:45:24 -0300 Subject: [PATCH 325/744] agent: Add section for agent servers in settings view (#35206) Release Notes: - N/A --------- Co-authored-by: Cole Miller --- crates/agent_servers/src/agent_servers.rs | 2 +- crates/agent_servers/src/gemini.rs | 18 +- crates/agent_ui/src/acp/thread_view.rs | 4 +- crates/agent_ui/src/agent_configuration.rs | 249 +++++++++++++++++++-- crates/agent_ui/src/agent_panel.rs | 2 + crates/agent_ui/src/agent_ui.rs | 1 + 6 files changed, 254 insertions(+), 22 deletions(-) diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index fa5920133857c5f4f0743278ce01b12de2e7bd1d..0439934094a841adf9602f5d83408d5cd32ebbf5 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -97,7 +97,7 @@ pub struct AgentServerCommand { } impl AgentServerCommand { - pub(crate) async fn resolve( + pub async fn resolve( path_bin_name: &'static str, extra_args: &[&'static str], fallback_path: Option<&Path>, diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 9ebcee745c99436cafdeab4e239a84fe5f7ef127..d09829fe6542be9df598e17369dbd85108d79426 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -53,7 +53,7 @@ impl AgentServer for Gemini { return Err(LoadError::NotInstalled { error_message: "Failed to find Gemini CLI binary".into(), install_message: "Install Gemini CLI".into(), - install_command: "npm install -g @google/gemini-cli@preview".into() + install_command: Self::install_command().into(), }.into()); }; @@ -88,7 +88,7 @@ impl AgentServer for Gemini { current_version ).into(), upgrade_message: "Upgrade Gemini CLI to latest".into(), - upgrade_command: "npm install -g @google/gemini-cli@preview".into(), + upgrade_command: Self::upgrade_command().into(), }.into()) } } @@ -101,6 +101,20 @@ impl AgentServer for Gemini { } } +impl Gemini { + pub fn binary_name() -> &'static str { + "gemini" + } + + pub fn install_command() -> &'static str { + "npm install -g @google/gemini-cli@preview" + } + + pub fn upgrade_command() -> &'static str { + "npm install -g @google/gemini-cli@preview" + } +} + #[cfg(test)] pub(crate) mod tests { use super::*; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index faba18acb1ffd4fe7b15c97c49f81eff227bc7be..97af249ae58e4674d3d806ae7541f8d427c54b04 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2811,7 +2811,7 @@ impl AcpThreadView { let cwd = project.first_project_directory(cx); let shell = project.terminal_settings(&cwd, cx).shell.clone(); let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId("install".to_string()), + id: task::TaskId(install_command.clone()), full_label: install_command.clone(), label: install_command.clone(), command: Some(install_command.clone()), @@ -2868,7 +2868,7 @@ impl AcpThreadView { let cwd = project.first_project_directory(cx); let shell = project.terminal_settings(&cwd, cx).shell.clone(); let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId("upgrade".to_string()), + id: task::TaskId(upgrade_command.to_string()), full_label: upgrade_command.clone(), label: upgrade_command.clone(), command: Some(upgrade_command.clone()), diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index f33f0ba0321b9928b92974e835cd8d34553fc447..52fb7eed4b96e2ff92097f415276ffeceb4fa11d 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -5,6 +5,7 @@ mod tool_picker; use std::{sync::Arc, time::Duration}; +use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini}; use agent_settings::AgentSettings; use assistant_tool::{ToolSource, ToolWorkingSet}; use cloud_llm_client::Plan; @@ -15,7 +16,7 @@ use extension_host::ExtensionStore; use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle, - Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, + Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, }; use language::LanguageRegistry; use language_model::{ @@ -23,10 +24,11 @@ use language_model::{ }; use notifications::status_toast::{StatusToast, ToastIcon}; use project::{ + Project, context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, project_settings::{ContextServerSettings, ProjectSettings}, }; -use settings::{Settings, update_settings_file}; +use settings::{Settings, SettingsStore, update_settings_file}; use ui::{ Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, @@ -39,7 +41,7 @@ pub(crate) use configure_context_server_modal::ConfigureContextServerModal; pub(crate) use manage_profiles_modal::ManageProfilesModal; use crate::{ - AddContextServer, + AddContextServer, ExternalAgent, NewExternalAgentThread, agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider}, }; @@ -47,6 +49,7 @@ pub struct AgentConfiguration { fs: Arc, language_registry: Arc, workspace: WeakEntity, + project: WeakEntity, focus_handle: FocusHandle, configuration_views_by_provider: HashMap, context_server_store: Entity, @@ -56,6 +59,8 @@ pub struct AgentConfiguration { _registry_subscription: Subscription, scroll_handle: ScrollHandle, scrollbar_state: ScrollbarState, + gemini_is_installed: bool, + _check_for_gemini: Task<()>, } impl AgentConfiguration { @@ -65,6 +70,7 @@ impl AgentConfiguration { tools: Entity, language_registry: Arc, workspace: WeakEntity, + project: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -89,6 +95,11 @@ impl AgentConfiguration { cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify()) .detach(); + cx.observe_global_in::(window, |this, _, cx| { + this.check_for_gemini(cx); + cx.notify(); + }) + .detach(); let scroll_handle = ScrollHandle::new(); let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); @@ -97,6 +108,7 @@ impl AgentConfiguration { fs, language_registry, workspace, + project, focus_handle, configuration_views_by_provider: HashMap::default(), context_server_store, @@ -106,8 +118,11 @@ impl AgentConfiguration { _registry_subscription: registry_subscription, scroll_handle, scrollbar_state, + gemini_is_installed: false, + _check_for_gemini: Task::ready(()), }; this.build_provider_configuration_views(window, cx); + this.check_for_gemini(cx); this } @@ -137,6 +152,34 @@ impl AgentConfiguration { self.configuration_views_by_provider .insert(provider.id(), configuration_view); } + + fn check_for_gemini(&mut self, cx: &mut Context) { + let project = self.project.clone(); + let settings = AllAgentServersSettings::get_global(cx).clone(); + self._check_for_gemini = cx.spawn({ + async move |this, cx| { + let Some(project) = project.upgrade() else { + return; + }; + let gemini_is_installed = AgentServerCommand::resolve( + Gemini::binary_name(), + &[], + // TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here + None, + settings.gemini, + &project, + cx, + ) + .await + .is_some(); + this.update(cx, |this, cx| { + this.gemini_is_installed = gemini_is_installed; + cx.notify(); + }) + .ok(); + } + }); + } } impl Focusable for AgentConfiguration { @@ -211,7 +254,6 @@ impl AgentConfiguration { .child( h_flex() .id(provider_id_string.clone()) - .cursor_pointer() .px_2() .py_0p5() .w_full() @@ -231,10 +273,7 @@ impl AgentConfiguration { h_flex() .w_full() .gap_1() - .child( - Label::new(provider_name.clone()) - .size(LabelSize::Large), - ) + .child(Label::new(provider_name.clone())) .map(|this| { if is_zed_provider && is_signed_in { this.child( @@ -279,7 +318,7 @@ impl AgentConfiguration { "Start New Thread", ) .icon_position(IconPosition::Start) - .icon(IconName::Plus) + .icon(IconName::Thread) .icon_size(IconSize::Small) .icon_color(Color::Muted) .label_size(LabelSize::Small) @@ -378,7 +417,7 @@ impl AgentConfiguration { ), ) .child( - Label::new("Add at least one provider to use AI-powered features.") + Label::new("Add at least one provider to use AI-powered features with Zed's native agent.") .color(Color::Muted), ), ), @@ -519,6 +558,14 @@ impl AgentConfiguration { } } + fn card_item_bg_color(&self, cx: &mut Context) -> Hsla { + cx.theme().colors().background.opacity(0.25) + } + + fn card_item_border_color(&self, cx: &mut Context) -> Hsla { + cx.theme().colors().border.opacity(0.6) + } + fn render_context_servers_section( &mut self, window: &mut Window, @@ -536,7 +583,12 @@ impl AgentConfiguration { v_flex() .gap_0p5() .child(Headline::new("Model Context Protocol (MCP) Servers")) - .child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)), + .child( + Label::new( + "All context servers connected through the Model Context Protocol.", + ) + .color(Color::Muted), + ), ) .children( context_server_ids.into_iter().map(|context_server_id| { @@ -546,7 +598,7 @@ impl AgentConfiguration { .child( h_flex() .justify_between() - .gap_2() + .gap_1p5() .child( h_flex().w_full().child( Button::new("add-context-server", "Add Custom Server") @@ -637,8 +689,6 @@ impl AgentConfiguration { .map_or([].as_slice(), |tools| tools.as_slice()); let tool_count = tools.len(); - let border_color = cx.theme().colors().border.opacity(0.6); - let (source_icon, source_tooltip) = if is_from_extension { ( IconName::ZedMcpExtension, @@ -781,8 +831,8 @@ impl AgentConfiguration { .id(item_id.clone()) .border_1() .rounded_md() - .border_color(border_color) - .bg(cx.theme().colors().background.opacity(0.2)) + .border_color(self.card_item_border_color(cx)) + .bg(self.card_item_bg_color(cx)) .overflow_hidden() .child( h_flex() @@ -790,7 +840,11 @@ impl AgentConfiguration { .justify_between() .when( error.is_some() || are_tools_expanded && tool_count >= 1, - |element| element.border_b_1().border_color(border_color), + |element| { + element + .border_b_1() + .border_color(self.card_item_border_color(cx)) + }, ) .child( h_flex() @@ -972,6 +1026,166 @@ impl AgentConfiguration { )) }) } + + fn render_agent_servers_section(&mut self, cx: &mut Context) -> impl IntoElement { + let settings = AllAgentServersSettings::get_global(cx).clone(); + let user_defined_agents = settings + .custom + .iter() + .map(|(name, settings)| { + self.render_agent_server( + IconName::Ai, + name.clone(), + ExternalAgent::Custom { + name: name.clone(), + settings: settings.clone(), + }, + None, + cx, + ) + .into_any_element() + }) + .collect::>(); + + v_flex() + .border_b_1() + .border_color(cx.theme().colors().border) + .child( + v_flex() + .p(DynamicSpacing::Base16.rems(cx)) + .pr(DynamicSpacing::Base20.rems(cx)) + .gap_2() + .child( + v_flex() + .gap_0p5() + .child(Headline::new("External Agents")) + .child( + Label::new( + "Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.", + ) + .color(Color::Muted), + ), + ) + .child(self.render_agent_server( + IconName::AiGemini, + "Gemini CLI", + ExternalAgent::Gemini, + (!self.gemini_is_installed).then_some(Gemini::install_command().into()), + cx, + )) + // TODO add CC + .children(user_defined_agents), + ) + } + + fn render_agent_server( + &self, + icon: IconName, + name: impl Into, + agent: ExternalAgent, + install_command: Option, + cx: &mut Context, + ) -> impl IntoElement { + let name = name.into(); + h_flex() + .p_1() + .pl_2() + .gap_1p5() + .justify_between() + .border_1() + .rounded_md() + .border_color(self.card_item_border_color(cx)) + .bg(self.card_item_bg_color(cx)) + .overflow_hidden() + .child( + h_flex() + .gap_1p5() + .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) + .child(Label::new(name.clone())), + ) + .map(|this| { + if let Some(install_command) = install_command { + this.child( + Button::new( + SharedString::from(format!("install_external_agent-{name}")), + "Install Agent", + ) + .label_size(LabelSize::Small) + .icon(IconName::Plus) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .tooltip(Tooltip::text(install_command.clone())) + .on_click(cx.listener( + move |this, _, window, cx| { + let Some(project) = this.project.upgrade() else { + return; + }; + let Some(workspace) = this.workspace.upgrade() else { + return; + }; + let cwd = project.read(cx).first_project_directory(cx); + let shell = + project.read(cx).terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId(install_command.to_string()), + full_label: install_command.to_string(), + label: install_command.to_string(), + command: Some(install_command.to_string()), + args: Vec::new(), + command_label: install_command.to_string(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + let task = workspace.update(cx, |workspace, cx| { + workspace.spawn_in_terminal(spawn_in_terminal, window, cx) + }); + cx.spawn(async move |this, cx| { + task.await; + this.update(cx, |this, cx| { + this.check_for_gemini(cx); + }) + .ok(); + }) + .detach(); + }, + )), + ) + } else { + this.child( + h_flex().gap_1().child( + Button::new( + SharedString::from(format!("start_acp_thread-{name}")), + "Start New Thread", + ) + .label_size(LabelSize::Small) + .icon(IconName::Thread) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .on_click(move |_, window, cx| { + window.dispatch_action( + NewExternalAgentThread { + agent: Some(agent.clone()), + } + .boxed_clone(), + cx, + ); + }), + ), + ) + } + }) + } } impl Render for AgentConfiguration { @@ -991,6 +1205,7 @@ impl Render for AgentConfiguration { .size_full() .overflow_y_scroll() .child(self.render_general_settings_section(cx)) + .child(self.render_agent_servers_section(cx)) .child(self.render_context_servers_section(window, cx)) .child(self.render_provider_configuration_section(cx)), ) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index f1a8a744ee63af2ef5121d636bd6aee6f3f1d61b..c825785755f3c0836c8303b86ef735ccabb914ca 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -241,6 +241,7 @@ enum WhichFontSize { None, } +// TODO unify this with ExternalAgent #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub enum AgentType { #[default] @@ -1474,6 +1475,7 @@ impl AgentPanel { tools, self.language_registry.clone(), self.workspace.clone(), + self.project.downgrade(), window, cx, ) diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 40f6c6a2bbedfe4543042b85a7ecbf2e0d7f9e70..d159f375b56a88cc5582594849975cc6e2a00019 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -160,6 +160,7 @@ pub struct NewNativeAgentThreadFromSummary { from_session_id: agent_client_protocol::SessionId, } +// TODO unify this with AgentType #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] enum ExternalAgent { From 59af2a7d1f513d9f58fc07d4429d118c6a944069 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 25 Aug 2025 20:51:23 +0200 Subject: [PATCH 326/744] acp: Add telemetry (#36894) Release Notes: - N/A --------- Co-authored-by: Conrad Irwin --- crates/agent2/src/native_agent_server.rs | 4 ++ crates/agent2/src/tests/mod.rs | 1 + crates/agent_servers/src/agent_servers.rs | 1 + crates/agent_servers/src/claude.rs | 4 ++ crates/agent_servers/src/custom.rs | 4 ++ crates/agent_servers/src/gemini.rs | 4 ++ crates/agent_ui/src/acp/thread_view.rs | 79 ++++++++++++++++------- crates/agent_ui/src/agent_panel.rs | 8 +++ crates/agent_ui/src/agent_ui.rs | 9 +++ crates/agent_ui/src/text_thread_editor.rs | 1 + 10 files changed, 93 insertions(+), 22 deletions(-) diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 33ee44c9a36c14976f3213e627043532daf5fa47..9ff98ccd18dec4d9a17a1a7161cd3622dacf0d3f 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -22,6 +22,10 @@ impl NativeAgentServer { } impl AgentServer for NativeAgentServer { + fn telemetry_id(&self) -> &'static str { + "zed" + } + fn name(&self) -> SharedString { "Zed Agent".into() } diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 87ecc1037cb6df9d06af37c0479606b0e97dd0d0..864fbf8b104b1190a778b3d157853f2d38134976 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1685,6 +1685,7 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) { } #[gpui::test] +#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_title_generation(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 0439934094a841adf9602f5d83408d5cd32ebbf5..7c7e124ca71b684cdda7a24e02c82d1b6117a0cc 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -36,6 +36,7 @@ pub trait AgentServer: Send { fn name(&self) -> SharedString; fn empty_state_headline(&self) -> SharedString; fn empty_state_message(&self) -> SharedString; + fn telemetry_id(&self) -> &'static str; fn connect( &self, diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 6006bf3edbee05408a45d502f243c396bcaf05fe..250e564526d5360be7a84f5cfd9511e5f73a2c1f 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -43,6 +43,10 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri pub struct ClaudeCode; impl AgentServer for ClaudeCode { + fn telemetry_id(&self) -> &'static str { + "claude-code" + } + fn name(&self) -> SharedString { "Claude Code".into() } diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index e544c4f21f68bd4b797c29feacca0dc41cd997f8..72823026d7ce353e76485fbe76783b3cc2bbeb56 100644 --- a/crates/agent_servers/src/custom.rs +++ b/crates/agent_servers/src/custom.rs @@ -22,6 +22,10 @@ impl CustomAgentServer { } impl crate::AgentServer for CustomAgentServer { + fn telemetry_id(&self) -> &'static str { + "custom" + } + fn name(&self) -> SharedString { self.name.clone() } diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index d09829fe6542be9df598e17369dbd85108d79426..5d6a70fa64d981b2f25aee13c9d1d3ac7f94468f 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -17,6 +17,10 @@ pub struct Gemini; const ACP_ARG: &str = "--experimental-acp"; impl AgentServer for Gemini { + fn telemetry_id(&self) -> &'static str { + "gemini-cli" + } + fn name(&self) -> SharedString { "Gemini CLI".into() } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 97af249ae58e4674d3d806ae7541f8d427c54b04..d80f4eabce236e224968f1fdc872adda54ad1160 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -892,6 +892,8 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) { + let agent_telemetry_id = self.agent.telemetry_id(); + self.thread_error.take(); self.editing_message.take(); self.thread_feedback.clear(); @@ -936,6 +938,9 @@ impl AcpThreadView { } }); drop(guard); + + telemetry::event!("Agent Message Sent", agent = agent_telemetry_id); + thread.send(contents, cx) })?; send.await @@ -1246,30 +1251,44 @@ impl AcpThreadView { pending_auth_method.replace(method.clone()); let authenticate = connection.authenticate(method, cx); cx.notify(); - self.auth_task = Some(cx.spawn_in(window, { - let project = self.project.clone(); - let agent = self.agent.clone(); - async move |this, cx| { - let result = authenticate.await; + self.auth_task = + Some(cx.spawn_in(window, { + let project = self.project.clone(); + let agent = self.agent.clone(); + async move |this, cx| { + let result = authenticate.await; - this.update_in(cx, |this, window, cx| { - if let Err(err) = result { - this.handle_thread_error(err, cx); - } else { - this.thread_state = Self::initial_state( - agent, - None, - this.workspace.clone(), - project.clone(), - window, - cx, - ) + match &result { + Ok(_) => telemetry::event!( + "Authenticate Agent Succeeded", + agent = agent.telemetry_id() + ), + Err(_) => { + telemetry::event!( + "Authenticate Agent Failed", + agent = agent.telemetry_id(), + ) + } } - this.auth_task.take() - }) - .ok(); - } - })); + + this.update_in(cx, |this, window, cx| { + if let Err(err) = result { + this.handle_thread_error(err, cx); + } else { + this.thread_state = Self::initial_state( + agent, + None, + this.workspace.clone(), + project.clone(), + window, + cx, + ) + } + this.auth_task.take() + }) + .ok(); + } + })); } fn authorize_tool_call( @@ -2776,6 +2795,12 @@ impl AcpThreadView { .on_click({ let method_id = method.id.clone(); cx.listener(move |this, _, window, cx| { + telemetry::event!( + "Authenticate Agent Started", + agent = this.agent.telemetry_id(), + method = method_id + ); + this.authenticate(method_id.clone(), window, cx) }) }) @@ -2804,6 +2829,8 @@ impl AcpThreadView { .icon_color(Color::Muted) .icon_position(IconPosition::Start) .on_click(cx.listener(move |this, _, window, cx| { + telemetry::event!("Agent Install CLI", agent = this.agent.telemetry_id()); + let task = this .workspace .update(cx, |workspace, cx| { @@ -2861,6 +2888,8 @@ impl AcpThreadView { .icon_color(Color::Muted) .icon_position(IconPosition::Start) .on_click(cx.listener(move |this, _, window, cx| { + telemetry::event!("Agent Upgrade CLI", agent = this.agent.telemetry_id()); + let task = this .workspace .update(cx, |workspace, cx| { @@ -3708,6 +3737,8 @@ impl AcpThreadView { } }) .ok(); + + telemetry::event!("Follow Agent Selected", following = !following); } fn render_follow_toggle(&self, cx: &mut Context) -> impl IntoElement { @@ -5323,6 +5354,10 @@ pub(crate) mod tests { where C: 'static + AgentConnection + Send + Clone, { + fn telemetry_id(&self) -> &'static str { + "test" + } + fn logo(&self) -> ui::IconName { ui::IconName::Ai } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index c825785755f3c0836c8303b86ef735ccabb914ca..1eafb8dd4d08246b006e522922a7ef65fc105e85 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1026,6 +1026,8 @@ impl AgentPanel { } fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context) { + telemetry::event!("Agent Thread Started", agent = "zed-text"); + let context = self .context_store .update(cx, |context_store, cx| context_store.create(cx)); @@ -1118,6 +1120,8 @@ impl AgentPanel { } }; + telemetry::event!("Agent Thread Started", agent = ext_agent.name()); + let server = ext_agent.server(fs, history); this.update_in(cx, |this, window, cx| { @@ -2327,6 +2331,8 @@ impl AgentPanel { .menu({ let menu = self.assistant_navigation_menu.clone(); move |window, cx| { + telemetry::event!("View Thread History Clicked"); + if let Some(menu) = menu.as_ref() { menu.update(cx, |_, cx| { cx.defer_in(window, |menu, window, cx| { @@ -2505,6 +2511,8 @@ impl AgentPanel { let workspace = self.workspace.clone(); move |window, cx| { + telemetry::event!("New Thread Clicked"); + let active_thread = active_thread.clone(); Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { menu = menu diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index d159f375b56a88cc5582594849975cc6e2a00019..110c432df3932f902b6ffcdac505a86e88550b28 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -175,6 +175,15 @@ enum ExternalAgent { } impl ExternalAgent { + fn name(&self) -> &'static str { + match self { + Self::NativeAgent => "zed", + Self::Gemini => "gemini-cli", + Self::ClaudeCode => "claude-code", + Self::Custom { .. } => "custom", + } + } + pub fn server( &self, fs: Arc, diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index edb672a872f99f1f996aa799111fc520fd623c4a..e9e7eba4b668fd09eb98a45b43bea6eb72b15277 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -361,6 +361,7 @@ impl TextThreadEditor { if self.sending_disabled(cx) { return; } + telemetry::event!("Agent Message Sent", agent = "zed-text"); self.send_to_model(window, cx); } From 79e74b880bafaf32778eead2e336cb0f7d68cde7 Mon Sep 17 00:00:00 2001 From: Cretezy Date: Mon, 25 Aug 2025 15:02:19 -0400 Subject: [PATCH 327/744] workspace: Allow disabling of padding on zoomed panels (#31913) Screenshot: | Before | After | | -------|------| | ![image](https://github.com/user-attachments/assets/629e7da2-6070-4abb-b469-3b0824524ca4) | ![image](https://github.com/user-attachments/assets/99e54412-2e0b-4df9-9c40-a89b0411f6d8) | | ![image](https://github.com/user-attachments/assets/e99da846-f39b-47b5-808e-65c22a1af47b) | ![image](https://github.com/user-attachments/assets/ccd4408f-8cce-44ec-a69a-81794125ec99) | Release Notes: - Added `zoomed_padding` to allow disabling of padding around zoomed panels Co-authored-by: Mikayla Maki --- assets/settings/default.json | 6 ++++++ crates/workspace/src/workspace.rs | 4 ++++ crates/workspace/src/workspace_settings.rs | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/assets/settings/default.json b/assets/settings/default.json index 59450dcc156c32638b881e141825bcc4ac3ac2fc..f0b9e11e57f074ac3c50b4e830343f7a5290f965 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -162,6 +162,12 @@ // 2. Always quit the application // "on_last_window_closed": "quit_app", "on_last_window_closed": "platform_default", + // Whether to show padding for zoomed panels. + // When enabled, zoomed center panels (e.g. code editor) will have padding all around, + // while zoomed bottom/left/right panels will have padding to the top/right/left (respectively). + // + // Default: true + "zoomed_padding": true, // Whether to use the system provided dialogs for Open and Save As. // When set to false, Zed will use the built-in keyboard-first pickers. "use_system_path_prompts": true, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3654df09beaf74e88e3214ceed9b0700f56a6470..0b4694601ed5b79d281248edf981d46ab15ae7cf 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6633,6 +6633,10 @@ impl Render for Workspace { .inset_0() .shadow_lg(); + if !WorkspaceSettings::get_global(cx).zoomed_padding { + return Some(div); + } + Some(match self.zoomed_position { Some(DockPosition::Left) => div.right_2().border_r_1(), Some(DockPosition::Right) => div.left_2().border_l_1(), diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 56353475148b5606d306201075fdfc6d59ffcf6f..3b6bc1ea970d0e7502e36f75630c9e8dd05906b5 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -29,6 +29,7 @@ pub struct WorkspaceSettings { pub on_last_window_closed: OnLastWindowClosed, pub resize_all_panels_in_dock: Vec, pub close_on_file_delete: bool, + pub zoomed_padding: bool, } #[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)] @@ -202,6 +203,12 @@ pub struct WorkspaceSettingsContent { /// /// Default: false pub close_on_file_delete: Option, + /// Whether to show padding for zoomed panels. + /// When enabled, zoomed bottom panels will have some top padding, + /// while zoomed left/right panels will have padding to the right/left (respectively). + /// + /// Default: true + pub zoomed_padding: Option, } #[derive(Deserialize)] From 949398cb93b6f68986cab45dc4ef91e6b54fb2b6 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:07:30 -0300 Subject: [PATCH 328/744] thread view: Fix some design papercuts (#36893) Release Notes: - N/A --------- Co-authored-by: Conrad Irwin Co-authored-by: Ben Brandt Co-authored-by: Matt Miller --- crates/agent2/src/tools/find_path_tool.rs | 27 +- crates/agent_ui/src/acp/thread_history.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 296 +++++++++---------- crates/assistant_tools/src/find_path_tool.rs | 8 +- crates/assistant_tools/src/read_file_tool.rs | 2 +- 5 files changed, 152 insertions(+), 183 deletions(-) diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index 5b35c40f859099f8fc49d2664fb8fec63dbf36ee..384bd56e776d8814e668d6fe3104a394c63b639d 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -165,16 +165,17 @@ fn search_paths(glob: &str, project: Entity, cx: &mut App) -> Task(if hovered || selected { + .end_slot::(if hovered { Some( IconButton::new("delete", IconName::Trash) .shape(IconButtonShape::Square) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index d80f4eabce236e224968f1fdc872adda54ad1160..837ce6f90ad7f2b445d91abeadc6658c3d348a0a 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -35,6 +35,7 @@ use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::{Settings as _, SettingsStore}; use std::cell::Cell; +use std::path::Path; use std::sync::Arc; use std::time::Instant; use std::{collections::BTreeMap, rc::Rc, time::Duration}; @@ -1551,12 +1552,11 @@ impl AcpThreadView { return primary; }; - let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); - let primary = if entry_ix == total_entries - 1 && !is_generating { + let primary = if entry_ix == total_entries - 1 { v_flex() .w_full() .child(primary) - .child(self.render_thread_controls(cx)) + .child(self.render_thread_controls(&thread, cx)) .when_some( self.thread_feedback.comments_editor.clone(), |this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)), @@ -1698,15 +1698,16 @@ impl AcpThreadView { .into_any_element() } - fn render_tool_call_icon( + fn render_tool_call( &self, - group_name: SharedString, entry_ix: usize, - is_collapsible: bool, - is_open: bool, tool_call: &ToolCall, + window: &Window, cx: &Context, ) -> Div { + let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix)); + let card_header_id = SharedString::from("inner-tool-call-header"); + let tool_icon = if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 { FileIcons::get_icon(&tool_call.locations[0].path, cx) @@ -1714,7 +1715,7 @@ impl AcpThreadView { .unwrap_or(Icon::new(IconName::ToolPencil)) } else { Icon::new(match tool_call.kind { - acp::ToolKind::Read => IconName::ToolRead, + acp::ToolKind::Read => IconName::ToolSearch, acp::ToolKind::Edit => IconName::ToolPencil, acp::ToolKind::Delete => IconName::ToolDeleteFile, acp::ToolKind::Move => IconName::ArrowRightLeft, @@ -1728,59 +1729,6 @@ impl AcpThreadView { .size(IconSize::Small) .color(Color::Muted); - let base_container = h_flex().flex_shrink_0().size_4().justify_center(); - - if is_collapsible { - base_container - .child( - div() - .group_hover(&group_name, |s| s.invisible().w_0()) - .child(tool_icon), - ) - .child( - h_flex() - .absolute() - .inset_0() - .invisible() - .justify_center() - .group_hover(&group_name, |s| s.visible()) - .child( - Disclosure::new(("expand", entry_ix), is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronRight) - .on_click(cx.listener({ - let id = tool_call.id.clone(); - move |this: &mut Self, _, _, cx: &mut Context| { - if is_open { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - cx.notify(); - } - })), - ), - ) - } else { - base_container.child(tool_icon) - } - } - - fn render_tool_call( - &self, - entry_ix: usize, - tool_call: &ToolCall, - window: &Window, - cx: &Context, - ) -> Div { - let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix)); - let card_header_id = SharedString::from("inner-tool-call-header"); - - let in_progress = match &tool_call.status { - ToolCallStatus::InProgress => true, - _ => false, - }; - let failed_or_canceled = match &tool_call.status { ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true, _ => false, @@ -1880,6 +1828,7 @@ impl AcpThreadView { .child( h_flex() .id(header_id) + .group(&card_header_id) .relative() .w_full() .max_w_full() @@ -1897,19 +1846,11 @@ impl AcpThreadView { }) .child( h_flex() - .group(&card_header_id) .relative() .w_full() .h(window.line_height() - px(2.)) .text_size(self.tool_name_font_size()) - .child(self.render_tool_call_icon( - card_header_id, - entry_ix, - is_collapsible, - is_open, - tool_call, - cx, - )) + .child(tool_icon) .child(if tool_call.locations.len() == 1 { let name = tool_call.locations[0] .path @@ -1937,13 +1878,13 @@ impl AcpThreadView { }) .child(name) .tooltip(Tooltip::text("Jump to File")) + .cursor(gpui::CursorStyle::PointingHand) .on_click(cx.listener(move |this, _, window, cx| { this.open_tool_call_location(entry_ix, 0, window, cx); })) .into_any_element() } else { h_flex() - .id("non-card-label-container") .relative() .w_full() .max_w_full() @@ -1954,47 +1895,39 @@ impl AcpThreadView { default_markdown_style(false, true, window, cx), ))) .child(gradient_overlay(gradient_color)) - .on_click(cx.listener({ - let id = tool_call.id.clone(); - move |this: &mut Self, _, _, cx: &mut Context| { - if is_open { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - cx.notify(); - } - })) .into_any() }), ) - .when(in_progress && use_card_layout && !is_open, |this| { - this.child( - div().absolute().right_2().child( - Icon::new(IconName::ArrowCircle) - .color(Color::Muted) - .size(IconSize::Small) - .with_animation( - "running", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage( - delta, - ))) - }, - ), - ), - ) - }) - .when(failed_or_canceled, |this| { - this.child( - div().absolute().right_2().child( - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small), - ), - ) - }), + .child( + h_flex() + .gap_px() + .when(is_collapsible, |this| { + this.child( + Disclosure::new(("expand", entry_ix), is_open) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronDown) + .visible_on_hover(&card_header_id) + .on_click(cx.listener({ + let id = tool_call.id.clone(); + move |this: &mut Self, _, _, cx: &mut Context| { + if is_open { + this.expanded_tool_calls.remove(&id); + } else { + this.expanded_tool_calls.insert(id.clone()); + } + cx.notify(); + } + })), + ) + }) + .when(failed_or_canceled, |this| { + this.child( + Icon::new(IconName::Close) + .color(Color::Error) + .size(IconSize::Small), + ) + }), + ), ) .children(tool_output_display) } @@ -2064,9 +1997,27 @@ impl AcpThreadView { cx: &Context, ) -> AnyElement { let uri: SharedString = resource_link.uri.clone().into(); + let is_file = resource_link.uri.strip_prefix("file://"); - let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") { - path.to_string().into() + let label: SharedString = if let Some(abs_path) = is_file { + if let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&Path::new(abs_path), cx) + && let Some(worktree) = self + .project + .read(cx) + .worktree_for_id(project_path.worktree_id, cx) + { + worktree + .read(cx) + .full_path(&project_path.path) + .to_string_lossy() + .to_string() + .into() + } else { + abs_path.to_string().into() + } } else { uri.clone() }; @@ -2083,10 +2034,12 @@ impl AcpThreadView { Button::new(button_id, label) .label_size(LabelSize::Small) .color(Color::Muted) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) .truncate(true) + .when(is_file.is_none(), |this| { + this.icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + }) .on_click(cx.listener({ let workspace = self.workspace.clone(); move |_, _, window, cx: &mut Context| { @@ -3727,16 +3680,19 @@ impl AcpThreadView { fn toggle_following(&mut self, window: &mut Window, cx: &mut Context) { let following = self.is_following(cx); + self.should_be_following = !following; - self.workspace - .update(cx, |workspace, cx| { - if following { - workspace.unfollow(CollaboratorId::Agent, window, cx); - } else { - workspace.follow(CollaboratorId::Agent, window, cx); - } - }) - .ok(); + if self.thread().map(|thread| thread.read(cx).status()) == Some(ThreadStatus::Generating) { + self.workspace + .update(cx, |workspace, cx| { + if following { + workspace.unfollow(CollaboratorId::Agent, window, cx); + } else { + workspace.follow(CollaboratorId::Agent, window, cx); + } + }) + .ok(); + } telemetry::event!("Follow Agent Selected", following = !following); } @@ -3744,6 +3700,20 @@ impl AcpThreadView { fn render_follow_toggle(&self, cx: &mut Context) -> impl IntoElement { let following = self.is_following(cx); + let tooltip_label = if following { + if self.agent.name() == "Zed Agent" { + format!("Stop Following the {}", self.agent.name()) + } else { + format!("Stop Following {}", self.agent.name()) + } + } else { + if self.agent.name() == "Zed Agent" { + format!("Follow the {}", self.agent.name()) + } else { + format!("Follow {}", self.agent.name()) + } + }; + IconButton::new("follow-agent", IconName::Crosshair) .icon_size(IconSize::Small) .icon_color(Color::Muted) @@ -3751,10 +3721,10 @@ impl AcpThreadView { .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor))) .tooltip(move |window, cx| { if following { - Tooltip::for_action("Stop Following Agent", &Follow, window, cx) + Tooltip::for_action(tooltip_label.clone(), &Follow, window, cx) } else { Tooltip::with_meta( - "Follow Agent", + tooltip_label.clone(), Some(&Follow), "Track the agent's location as it reads and edits files.", window, @@ -4175,7 +4145,20 @@ impl AcpThreadView { } } - fn render_thread_controls(&self, cx: &Context) -> impl IntoElement { + fn render_thread_controls( + &self, + thread: &Entity, + cx: &Context, + ) -> impl IntoElement { + let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); + if is_generating { + return h_flex().id("thread-controls-container").ml_1().child( + div() + .py_2() + .px(rems_from_px(22.)) + .child(SpinnerLabel::new().size(LabelSize::Small)), + ); + } let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) @@ -4899,45 +4882,30 @@ impl Render for AcpThreadView { .items_center() .justify_end() .child(self.render_load_error(e, cx)), - ThreadState::Ready { thread, .. } => { - let thread_clone = thread.clone(); - - v_flex().flex_1().map(|this| { - if has_messages { - this.child( - list( - self.list_state.clone(), - cx.processor(|this, index: usize, window, cx| { - let Some((entry, len)) = this.thread().and_then(|thread| { - let entries = &thread.read(cx).entries(); - Some((entries.get(index)?, entries.len())) - }) else { - return Empty.into_any(); - }; - this.render_entry(index, len, entry, window, cx) - }), - ) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .flex_grow() - .into_any(), - ) - .child(self.render_vertical_scrollbar(cx)) - .children( - match thread_clone.read(cx).status() { - ThreadStatus::Idle - | ThreadStatus::WaitingForToolConfirmation => None, - ThreadStatus::Generating => div() - .py_2() - .px(rems_from_px(22.)) - .child(SpinnerLabel::new().size(LabelSize::Small)) - .into(), - }, + ThreadState::Ready { .. } => v_flex().flex_1().map(|this| { + if has_messages { + this.child( + list( + self.list_state.clone(), + cx.processor(|this, index: usize, window, cx| { + let Some((entry, len)) = this.thread().and_then(|thread| { + let entries = &thread.read(cx).entries(); + Some((entries.get(index)?, entries.len())) + }) else { + return Empty.into_any(); + }; + this.render_entry(index, len, entry, window, cx) + }), ) - } else { - this.child(self.render_recent_history(window, cx)) - } - }) - } + .with_sizing_behavior(gpui::ListSizingBehavior::Auto) + .flex_grow() + .into_any(), + ) + .child(self.render_vertical_scrollbar(cx)) + } else { + this.child(self.render_recent_history(window, cx)) + } + }), }) // The activity bar is intentionally rendered outside of the ThreadState::Ready match // above so that the scrollbar doesn't render behind it. The current setup allows diff --git a/crates/assistant_tools/src/find_path_tool.rs b/crates/assistant_tools/src/find_path_tool.rs index ac2c7a32abc0a3768caee85cfd25ab109f03aab3..d1451132aeb066a5d4ff9e05f81db3855c1d513a 100644 --- a/crates/assistant_tools/src/find_path_tool.rs +++ b/crates/assistant_tools/src/find_path_tool.rs @@ -435,8 +435,8 @@ mod test { assert_eq!( matches, &[ - PathBuf::from("root/apple/banana/carrot"), - PathBuf::from("root/apple/bandana/carbonara") + PathBuf::from(path!("root/apple/banana/carrot")), + PathBuf::from(path!("root/apple/bandana/carbonara")) ] ); @@ -447,8 +447,8 @@ mod test { assert_eq!( matches, &[ - PathBuf::from("root/apple/banana/carrot"), - PathBuf::from("root/apple/bandana/carbonara") + PathBuf::from(path!("root/apple/banana/carrot")), + PathBuf::from(path!("root/apple/bandana/carbonara")) ] ); } diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index 766ee3b1611ccc9093fbe9299644413877afd991..a6e984fca6f2704a6dbe4c16d5e659f0c8bfe141 100644 --- a/crates/assistant_tools/src/read_file_tool.rs +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -68,7 +68,7 @@ impl Tool for ReadFileTool { } fn icon(&self) -> IconName { - IconName::ToolRead + IconName::ToolSearch } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { From 4605b9663059df38176db1c19c9edb1d52ac0ece Mon Sep 17 00:00:00 2001 From: John Tur Date: Mon, 25 Aug 2025 15:45:28 -0400 Subject: [PATCH 329/744] Fix constant thread creation on Windows (#36779) See https://github.com/zed-industries/zed/issues/36057#issuecomment-3215808649 Fixes https://github.com/zed-industries/zed/issues/36057 Release Notes: - N/A --- crates/gpui/src/platform/windows/dispatcher.rs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/crates/gpui/src/platform/windows/dispatcher.rs b/crates/gpui/src/platform/windows/dispatcher.rs index e5b9c020d511b478779dc1affb3927018f8f7b3f..f554dea1284c5e3293cd0705d1754d07e4b6395a 100644 --- a/crates/gpui/src/platform/windows/dispatcher.rs +++ b/crates/gpui/src/platform/windows/dispatcher.rs @@ -9,10 +9,8 @@ use parking::Parker; use parking_lot::Mutex; use util::ResultExt; use windows::{ - Foundation::TimeSpan, System::Threading::{ - ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions, - WorkItemPriority, + ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority, }, Win32::{ Foundation::{LPARAM, WPARAM}, @@ -56,12 +54,7 @@ impl WindowsDispatcher { Ok(()) }) }; - ThreadPool::RunWithPriorityAndOptionsAsync( - &handler, - WorkItemPriority::High, - WorkItemOptions::TimeSliced, - ) - .log_err(); + ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err(); } fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) { @@ -72,12 +65,7 @@ impl WindowsDispatcher { Ok(()) }) }; - let delay = TimeSpan { - // A time period expressed in 100-nanosecond units. - // 10,000,000 ticks per second - Duration: (duration.as_nanos() / 100) as i64, - }; - ThreadPoolTimer::CreateTimer(&handler, delay).log_err(); + ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err(); } } From 0470baca50a557491d0a193ec125500c5bf22770 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 25 Aug 2025 13:50:08 -0600 Subject: [PATCH 330/744] open_ai: Remove `model` field from ResponseStreamEvent (#36902) Closes #36901 Release Notes: - Fixed use of Open WebUI as an LLM provider. --- crates/open_ai/src/open_ai.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index acf6ec434a013c711e0c77be3658913f9c6db7cb..08be82b8303291f9fa7795b2f04cbe8392d6d581 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -446,7 +446,6 @@ pub enum ResponseStreamResult { #[derive(Serialize, Deserialize, Debug)] pub struct ResponseStreamEvent { - pub model: String, pub choices: Vec, pub usage: Option, } From 9cc006ff7473afbfa999c3424b221326ade4ccf1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Aug 2025 14:07:10 -0600 Subject: [PATCH 331/744] acp: Update error matching (#36898) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 21 ++++++++++++--------- crates/agent_servers/src/acp.rs | 4 +++- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 779f9964da1d3197a4290c94972e7707ba814250..4ded647a746f18e030529e4c14a00c1ffd2335e3 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -183,16 +183,15 @@ impl ToolCall { language_registry: Arc, cx: &mut App, ) -> Self { + let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") { + first_line.to_owned() + "…" + } else { + tool_call.title + }; Self { id: tool_call.id, - label: cx.new(|cx| { - Markdown::new( - tool_call.title.into(), - Some(language_registry.clone()), - None, - cx, - ) - }), + label: cx + .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)), kind: tool_call.kind, content: tool_call .content @@ -233,7 +232,11 @@ impl ToolCall { if let Some(title) = title { self.label.update(cx, |label, cx| { - label.replace(title, cx); + if let Some((first_line, _)) = title.split_once("\n") { + label.replace(first_line.to_owned() + "…", cx) + } else { + label.replace(title, cx); + } }); } diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 5a4efe12e50572041109fc4e5540cbcb2fb2ca7d..9080fc1ab07d91e13708ccf0348d5df01d49e3c0 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -266,7 +266,9 @@ impl AgentConnection for AcpConnection { match serde_json::from_value(data.clone()) { Ok(ErrorDetails { details }) => { - if suppress_abort_err && details.contains("This operation was aborted") + if suppress_abort_err + && (details.contains("This operation was aborted") + || details.contains("The user aborted a request")) { Ok(acp::PromptResponse { stop_reason: acp::StopReason::Cancelled, From 823a0018e5a5f758c63ab68622db23e1dfa45fba Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 25 Aug 2025 16:10:17 -0400 Subject: [PATCH 332/744] acp: Show output for read_file tool in a code block (#36900) Release Notes: - N/A --- crates/agent2/src/tools/read_file_tool.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index fea9732093cf3b12865e40adacf75ecf2916c0b1..e771c26eca6e453a8f3d4150079b31a839227a4d 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -11,6 +11,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; use std::{path::Path, sync::Arc}; +use util::markdown::MarkdownCodeBlock; use crate::{AgentTool, ToolCallEventStream}; @@ -243,6 +244,19 @@ impl AgentTool for ReadFileTool { }]), ..Default::default() }); + if let Ok(LanguageModelToolResultContent::Text(text)) = &result { + let markdown = MarkdownCodeBlock { + tag: &input.path, + text, + } + .to_string(); + event_stream.update_fields(ToolCallUpdateFields { + content: Some(vec![acp::ToolCallContent::Content { + content: markdown.into(), + }]), + ..Default::default() + }) + } } })?; From 99cee8778cc7c6ee9ddd405f5f00caa713299d68 Mon Sep 17 00:00:00 2001 From: Alvaro Parker <64918109+AlvaroParker@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:18:03 -0400 Subject: [PATCH 333/744] tab_switcher: Add support for diagnostics (#34547) Support to show diagnostics on the tab switcher in the same way they are displayed on the tab bar. This follows the setting `tabs.show_diagnostics`. This will improve user experience when disabling the tab bar and still being able to see the diagnostics when switching tabs Preview: Screenshot From 2025-07-16 11-02-42 Release Notes: - Added diagnostics indicators to the tab switcher --------- Co-authored-by: Kirill Bulatov --- crates/language/src/buffer.rs | 20 +++-- crates/project/src/lsp_store.rs | 23 +++-- crates/tab_switcher/src/tab_switcher.rs | 114 +++++++++++++++++------- 3 files changed, 108 insertions(+), 49 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index b106110c33d12a54cb7c4731598306c5da14abe9..4ddc2b3018614f592beeb55aaa2cc9ed46b5522c 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1569,11 +1569,21 @@ impl Buffer { self.send_operation(op, true, cx); } - pub fn get_diagnostics(&self, server_id: LanguageServerId) -> Option<&DiagnosticSet> { - let Ok(idx) = self.diagnostics.binary_search_by_key(&server_id, |v| v.0) else { - return None; - }; - Some(&self.diagnostics[idx].1) + pub fn buffer_diagnostics( + &self, + for_server: Option, + ) -> Vec<&DiagnosticEntry> { + match for_server { + Some(server_id) => match self.diagnostics.binary_search_by_key(&server_id, |v| v.0) { + Ok(idx) => self.diagnostics[idx].1.iter().collect(), + Err(_) => Vec::new(), + }, + None => self + .diagnostics + .iter() + .flat_map(|(_, diagnostic_set)| diagnostic_set.iter()) + .collect(), + } } fn request_autoindent(&mut self, cx: &mut Context) { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 853490ddac127304bed3afa6a70e2a6e5aa9eb6c..deebaedd74a9a56bba27632a640443e03d5f5517 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -7588,19 +7588,16 @@ impl LspStore { let snapshot = buffer_handle.read(cx).snapshot(); let buffer = buffer_handle.read(cx); let reused_diagnostics = buffer - .get_diagnostics(server_id) - .into_iter() - .flat_map(|diag| { - diag.iter() - .filter(|v| merge(buffer, &v.diagnostic, cx)) - .map(|v| { - let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); - let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); - DiagnosticEntry { - range: start..end, - diagnostic: v.diagnostic.clone(), - } - }) + .buffer_diagnostics(Some(server_id)) + .iter() + .filter(|v| merge(buffer, &v.diagnostic, cx)) + .map(|v| { + let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); + let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); + DiagnosticEntry { + range: start..end, + diagnostic: v.diagnostic.clone(), + } }) .collect::>(); diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 7c70bcd5b55c8138053ade315dec3113f37a09f8..bf3ce7b568f9388fee387caa654cbb9072df97b3 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -2,12 +2,14 @@ mod tab_switcher_tests; use collections::HashMap; -use editor::items::entry_git_aware_label_color; +use editor::items::{ + entry_diagnostic_aware_icon_decoration_and_color, entry_git_aware_label_color, +}; use fuzzy::StringMatchCandidate; use gpui::{ Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle, - Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Render, - Styled, Task, WeakEntity, Window, actions, rems, + Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Point, + Render, Styled, Task, WeakEntity, Window, actions, rems, }; use picker::{Picker, PickerDelegate}; use project::Project; @@ -15,11 +17,14 @@ use schemars::JsonSchema; use serde::Deserialize; use settings::Settings; use std::{cmp::Reverse, sync::Arc}; -use ui::{ListItem, ListItemSpacing, Tooltip, prelude::*}; +use ui::{ + DecoratedIcon, IconDecoration, IconDecorationKind, ListItem, ListItemSpacing, Tooltip, + prelude::*, +}; use util::ResultExt; use workspace::{ ModalView, Pane, SaveIntent, Workspace, - item::{ItemHandle, ItemSettings, TabContentParams}, + item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams}, pane::{Event as PaneEvent, render_item_indicator, tab_details}, }; @@ -233,6 +238,77 @@ pub struct TabSwitcherDelegate { restored_items: bool, } +impl TabMatch { + fn icon( + &self, + project: &Entity, + selected: bool, + window: &Window, + cx: &App, + ) -> Option { + let icon = self.item.tab_icon(window, cx)?; + let item_settings = ItemSettings::get_global(cx); + let show_diagnostics = item_settings.show_diagnostics; + let git_status_color = item_settings + .git_status + .then(|| { + let path = self.item.project_path(cx)?; + let project = project.read(cx); + let entry = project.entry_for_path(&path, cx)?; + let git_status = project + .project_path_git_status(&path, cx) + .map(|status| status.summary()) + .unwrap_or_default(); + Some(entry_git_aware_label_color( + git_status, + entry.is_ignored, + selected, + )) + }) + .flatten(); + let colored_icon = icon.color(git_status_color.unwrap_or_default()); + + let most_sever_diagostic_level = if show_diagnostics == ShowDiagnostics::Off { + None + } else { + let buffer_store = project.read(cx).buffer_store().read(cx); + let buffer = self + .item + .project_path(cx) + .and_then(|path| buffer_store.get_by_path(&path)) + .map(|buffer| buffer.read(cx)); + buffer.and_then(|buffer| { + buffer + .buffer_diagnostics(None) + .iter() + .map(|diagnostic_entry| diagnostic_entry.diagnostic.severity) + .min() + }) + }; + + let decorations = + entry_diagnostic_aware_icon_decoration_and_color(most_sever_diagostic_level) + .filter(|(d, _)| { + *d != IconDecorationKind::Triangle + || show_diagnostics != ShowDiagnostics::Errors + }) + .map(|(icon, color)| { + let knockout_item_color = if selected { + cx.theme().colors().element_selected + } else { + cx.theme().colors().element_background + }; + IconDecoration::new(icon, knockout_item_color, cx) + .color(color.color(cx)) + .position(Point { + x: px(-2.), + y: px(-2.), + }) + }); + Some(DecoratedIcon::new(colored_icon, decorations)) + } +} + impl TabSwitcherDelegate { #[allow(clippy::complexity)] fn new( @@ -574,31 +650,7 @@ impl PickerDelegate for TabSwitcherDelegate { }; let label = tab_match.item.tab_content(params, window, cx); - let icon = tab_match.item.tab_icon(window, cx).map(|icon| { - let git_status_color = ItemSettings::get_global(cx) - .git_status - .then(|| { - tab_match - .item - .project_path(cx) - .as_ref() - .and_then(|path| { - let project = self.project.read(cx); - let entry = project.entry_for_path(path, cx)?; - let git_status = project - .project_path_git_status(path, cx) - .map(|status| status.summary()) - .unwrap_or_default(); - Some((entry, git_status)) - }) - .map(|(entry, git_status)| { - entry_git_aware_label_color(git_status, entry.is_ignored, selected) - }) - }) - .flatten(); - - icon.color(git_status_color.unwrap_or_default()) - }); + let icon = tab_match.icon(&self.project, selected, window, cx); let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx); let indicator_color = if let Some(ref indicator) = indicator { @@ -640,7 +692,7 @@ impl PickerDelegate for TabSwitcherDelegate { .inset(true) .toggle_state(selected) .child(h_flex().w_full().child(label)) - .start_slot::(icon) + .start_slot::(icon) .map(|el| { if self.selected_index == ix { el.end_slot::(close_button) From ad25aba990cc26b41903a91cdbff9bfec07ff95c Mon Sep 17 00:00:00 2001 From: Gwen Lg <105106246+gwen-lg@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:23:29 +0200 Subject: [PATCH 334/744] remote_server: Improve error reporting (#33770) Closes #33736 Use `thiserror` to implement error stack and `anyhow` to report is to user. Also move some code from main to remote_server to have better crate isolation. Release Notes: - N/A --------- Co-authored-by: Kirill Bulatov --- Cargo.lock | 1 + crates/remote_server/Cargo.toml | 1 + crates/remote_server/src/main.rs | 90 ++----------- crates/remote_server/src/remote_server.rs | 74 +++++++++++ crates/remote_server/src/unix.rs | 155 ++++++++++++++++++---- 5 files changed, 216 insertions(+), 105 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c835b503ad214edea10225da8c3828d7ea32e616..42649b137f35cff3bcc9622229d1b396dd9d1d87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13521,6 +13521,7 @@ dependencies = [ "smol", "sysinfo", "telemetry_events", + "thiserror 2.0.12", "toml 0.8.20", "unindent", "util", diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index dcec9f6fe003734ff9382f2146c9e232f0e59f96..5dbb9a2771c4e3fda04ed014f993f843b44dd976 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -65,6 +65,7 @@ telemetry_events.workspace = true util.workspace = true watch.workspace = true worktree.workspace = true +thiserror.workspace = true [target.'cfg(not(windows))'.dependencies] crashes.workspace = true diff --git a/crates/remote_server/src/main.rs b/crates/remote_server/src/main.rs index 03b0c3eda3ca4556e9e9fa0f588b68effd84a5f9..368c7cb639b332ec4f605ca1b507d90847d5d974 100644 --- a/crates/remote_server/src/main.rs +++ b/crates/remote_server/src/main.rs @@ -1,6 +1,7 @@ #![cfg_attr(target_os = "windows", allow(unused, dead_code))] -use clap::{Parser, Subcommand}; +use clap::Parser; +use remote_server::Commands; use std::path::PathBuf; #[derive(Parser)] @@ -21,105 +22,34 @@ struct Cli { printenv: bool, } -#[derive(Subcommand)] -enum Commands { - Run { - #[arg(long)] - log_file: PathBuf, - #[arg(long)] - pid_file: PathBuf, - #[arg(long)] - stdin_socket: PathBuf, - #[arg(long)] - stdout_socket: PathBuf, - #[arg(long)] - stderr_socket: PathBuf, - }, - Proxy { - #[arg(long)] - reconnect: bool, - #[arg(long)] - identifier: String, - }, - Version, -} - #[cfg(windows)] fn main() { unimplemented!() } #[cfg(not(windows))] -fn main() { - use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; - use remote::proxy::ProxyLaunchError; - use remote_server::unix::{execute_proxy, execute_run}; - +fn main() -> anyhow::Result<()> { let cli = Cli::parse(); if let Some(socket_path) = &cli.askpass { askpass::main(socket_path); - return; + return Ok(()); } if let Some(socket) = &cli.crash_handler { crashes::crash_server(socket.as_path()); - return; + return Ok(()); } if cli.printenv { util::shell_env::print_env(); - return; + return Ok(()); } - let result = match cli.command { - Some(Commands::Run { - log_file, - pid_file, - stdin_socket, - stdout_socket, - stderr_socket, - }) => execute_run( - log_file, - pid_file, - stdin_socket, - stdout_socket, - stderr_socket, - ), - Some(Commands::Proxy { - identifier, - reconnect, - }) => match execute_proxy(identifier, reconnect) { - Ok(_) => Ok(()), - Err(err) => { - if let Some(err) = err.downcast_ref::() { - std::process::exit(err.to_exit_code()); - } - Err(err) - } - }, - Some(Commands::Version) => { - let release_channel = *RELEASE_CHANNEL; - match release_channel { - ReleaseChannel::Stable | ReleaseChannel::Preview => { - println!("{}", env!("ZED_PKG_VERSION")) - } - ReleaseChannel::Nightly | ReleaseChannel::Dev => { - println!( - "{}", - option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name()) - ) - } - }; - std::process::exit(0); - } - None => { - eprintln!("usage: remote "); - std::process::exit(1); - } - }; - if let Err(error) = result { - log::error!("exiting due to error: {}", error); + if let Some(command) = cli.command { + remote_server::run(command) + } else { + eprintln!("usage: remote "); std::process::exit(1); } } diff --git a/crates/remote_server/src/remote_server.rs b/crates/remote_server/src/remote_server.rs index 52003969af4dfbed6d96863289b5bca506c9315b..c14a4828ac28890ad1711882045b83cee02104a9 100644 --- a/crates/remote_server/src/remote_server.rs +++ b/crates/remote_server/src/remote_server.rs @@ -6,4 +6,78 @@ pub mod unix; #[cfg(test)] mod remote_editing_tests; +use clap::Subcommand; +use std::path::PathBuf; + pub use headless_project::{HeadlessAppState, HeadlessProject}; + +#[derive(Subcommand)] +pub enum Commands { + Run { + #[arg(long)] + log_file: PathBuf, + #[arg(long)] + pid_file: PathBuf, + #[arg(long)] + stdin_socket: PathBuf, + #[arg(long)] + stdout_socket: PathBuf, + #[arg(long)] + stderr_socket: PathBuf, + }, + Proxy { + #[arg(long)] + reconnect: bool, + #[arg(long)] + identifier: String, + }, + Version, +} + +#[cfg(not(windows))] +pub fn run(command: Commands) -> anyhow::Result<()> { + use anyhow::Context; + use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; + use unix::{ExecuteProxyError, execute_proxy, execute_run}; + + match command { + Commands::Run { + log_file, + pid_file, + stdin_socket, + stdout_socket, + stderr_socket, + } => execute_run( + log_file, + pid_file, + stdin_socket, + stdout_socket, + stderr_socket, + ), + Commands::Proxy { + identifier, + reconnect, + } => execute_proxy(identifier, reconnect) + .inspect_err(|err| { + if let ExecuteProxyError::ServerNotRunning(err) = err { + std::process::exit(err.to_exit_code()); + } + }) + .context("running proxy on the remote server"), + Commands::Version => { + let release_channel = *RELEASE_CHANNEL; + match release_channel { + ReleaseChannel::Stable | ReleaseChannel::Preview => { + println!("{}", env!("ZED_PKG_VERSION")) + } + ReleaseChannel::Nightly | ReleaseChannel::Dev => { + println!( + "{}", + option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name()) + ) + } + }; + Ok(()) + } + } +} diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index b8a735155208097872fc4e8eac71840ad064dfcf..c6d1566d6038bb1b908c0b28eb8d24b85e5cc86d 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -36,6 +36,7 @@ use smol::Async; use smol::{net::unix::UnixListener, stream::StreamExt as _}; use std::ffi::OsStr; use std::ops::ControlFlow; +use std::process::ExitStatus; use std::str::FromStr; use std::sync::LazyLock; use std::{env, thread}; @@ -46,6 +47,7 @@ use std::{ sync::Arc, }; use telemetry_events::LocationData; +use thiserror::Error; use util::ResultExt; pub static VERSION: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL { @@ -526,7 +528,23 @@ pub fn execute_run( Ok(()) } -#[derive(Clone)] +#[derive(Debug, Error)] +pub(crate) enum ServerPathError { + #[error("Failed to create server_dir `{path}`")] + CreateServerDir { + #[source] + source: std::io::Error, + path: PathBuf, + }, + #[error("Failed to create logs_dir `{path}`")] + CreateLogsDir { + #[source] + source: std::io::Error, + path: PathBuf, + }, +} + +#[derive(Clone, Debug)] struct ServerPaths { log_file: PathBuf, pid_file: PathBuf, @@ -536,10 +554,19 @@ struct ServerPaths { } impl ServerPaths { - fn new(identifier: &str) -> Result { + fn new(identifier: &str) -> Result { let server_dir = paths::remote_server_state_dir().join(identifier); - std::fs::create_dir_all(&server_dir)?; - std::fs::create_dir_all(&logs_dir())?; + std::fs::create_dir_all(&server_dir).map_err(|source| { + ServerPathError::CreateServerDir { + source, + path: server_dir.clone(), + } + })?; + let log_dir = logs_dir(); + std::fs::create_dir_all(log_dir).map_err(|source| ServerPathError::CreateLogsDir { + source: source, + path: log_dir.clone(), + })?; let pid_file = server_dir.join("server.pid"); let stdin_socket = server_dir.join("stdin.sock"); @@ -557,7 +584,43 @@ impl ServerPaths { } } -pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { +#[derive(Debug, Error)] +pub(crate) enum ExecuteProxyError { + #[error("Failed to init server paths")] + ServerPath(#[from] ServerPathError), + + #[error(transparent)] + ServerNotRunning(#[from] ProxyLaunchError), + + #[error("Failed to check PidFile '{path}'")] + CheckPidFile { + #[source] + source: CheckPidError, + path: PathBuf, + }, + + #[error("Failed to kill existing server with pid '{pid}'")] + KillRunningServer { + #[source] + source: std::io::Error, + pid: u32, + }, + + #[error("failed to spawn server")] + SpawnServer(#[source] SpawnServerError), + + #[error("stdin_task failed")] + StdinTask(#[source] anyhow::Error), + #[error("stdout_task failed")] + StdoutTask(#[source] anyhow::Error), + #[error("stderr_task failed")] + StderrTask(#[source] anyhow::Error), +} + +pub(crate) fn execute_proxy( + identifier: String, + is_reconnecting: bool, +) -> Result<(), ExecuteProxyError> { init_logging_proxy(); let server_paths = ServerPaths::new(&identifier)?; @@ -574,12 +637,19 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { log::info!("starting proxy process. PID: {}", std::process::id()); - let server_pid = check_pid_file(&server_paths.pid_file)?; + let server_pid = check_pid_file(&server_paths.pid_file).map_err(|source| { + ExecuteProxyError::CheckPidFile { + source, + path: server_paths.pid_file.clone(), + } + })?; let server_running = server_pid.is_some(); if is_reconnecting { if !server_running { log::error!("attempted to reconnect, but no server running"); - anyhow::bail!(ProxyLaunchError::ServerNotRunning); + return Err(ExecuteProxyError::ServerNotRunning( + ProxyLaunchError::ServerNotRunning, + )); } } else { if let Some(pid) = server_pid { @@ -590,7 +660,7 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { kill_running_server(pid, &server_paths)?; } - spawn_server(&server_paths)?; + spawn_server(&server_paths).map_err(ExecuteProxyError::SpawnServer)?; }; let stdin_task = smol::spawn(async move { @@ -630,9 +700,9 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { if let Err(forwarding_result) = smol::block_on(async move { futures::select! { - result = stdin_task.fuse() => result.context("stdin_task failed"), - result = stdout_task.fuse() => result.context("stdout_task failed"), - result = stderr_task.fuse() => result.context("stderr_task failed"), + result = stdin_task.fuse() => result.map_err(ExecuteProxyError::StdinTask), + result = stdout_task.fuse() => result.map_err(ExecuteProxyError::StdoutTask), + result = stderr_task.fuse() => result.map_err(ExecuteProxyError::StderrTask), } }) { log::error!( @@ -645,12 +715,12 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { Ok(()) } -fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> { +fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxyError> { log::info!("killing existing server with PID {}", pid); std::process::Command::new("kill") .arg(pid.to_string()) .output() - .context("failed to kill existing server")?; + .map_err(|source| ExecuteProxyError::KillRunningServer { source, pid })?; for file in [ &paths.pid_file, @@ -664,18 +734,39 @@ fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> { Ok(()) } -fn spawn_server(paths: &ServerPaths) -> Result<()> { +#[derive(Debug, Error)] +pub(crate) enum SpawnServerError { + #[error("failed to remove stdin socket")] + RemoveStdinSocket(#[source] std::io::Error), + + #[error("failed to remove stdout socket")] + RemoveStdoutSocket(#[source] std::io::Error), + + #[error("failed to remove stderr socket")] + RemoveStderrSocket(#[source] std::io::Error), + + #[error("failed to get current_exe")] + CurrentExe(#[source] std::io::Error), + + #[error("failed to launch server process")] + ProcessStatus(#[source] std::io::Error), + + #[error("failed to launch and detach server process: {status}\n{paths}")] + LaunchStatus { status: ExitStatus, paths: String }, +} + +fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> { if paths.stdin_socket.exists() { - std::fs::remove_file(&paths.stdin_socket)?; + std::fs::remove_file(&paths.stdin_socket).map_err(SpawnServerError::RemoveStdinSocket)?; } if paths.stdout_socket.exists() { - std::fs::remove_file(&paths.stdout_socket)?; + std::fs::remove_file(&paths.stdout_socket).map_err(SpawnServerError::RemoveStdoutSocket)?; } if paths.stderr_socket.exists() { - std::fs::remove_file(&paths.stderr_socket)?; + std::fs::remove_file(&paths.stderr_socket).map_err(SpawnServerError::RemoveStderrSocket)?; } - let binary_name = std::env::current_exe()?; + let binary_name = std::env::current_exe().map_err(SpawnServerError::CurrentExe)?; let mut server_process = std::process::Command::new(binary_name); server_process .arg("run") @@ -692,11 +783,17 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> { let status = server_process .status() - .context("failed to launch server process")?; - anyhow::ensure!( - status.success(), - "failed to launch and detach server process" - ); + .map_err(SpawnServerError::ProcessStatus)?; + + if !status.success() { + return Err(SpawnServerError::LaunchStatus { + status, + paths: format!( + "log file: {:?}, pid file: {:?}", + paths.log_file, paths.pid_file, + ), + }); + } let mut total_time_waited = std::time::Duration::from_secs(0); let wait_duration = std::time::Duration::from_millis(20); @@ -717,7 +814,15 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> { Ok(()) } -fn check_pid_file(path: &Path) -> Result> { +#[derive(Debug, Error)] +#[error("Failed to remove PID file for missing process (pid `{pid}`")] +pub(crate) struct CheckPidError { + #[source] + source: std::io::Error, + pid: u32, +} + +fn check_pid_file(path: &Path) -> Result, CheckPidError> { let Some(pid) = std::fs::read_to_string(&path) .ok() .and_then(|contents| contents.parse::().ok()) @@ -742,7 +847,7 @@ fn check_pid_file(path: &Path) -> Result> { log::debug!( "Found PID file, but process with that PID does not exist. Removing PID file." ); - std::fs::remove_file(&path).context("Failed to remove PID file")?; + std::fs::remove_file(&path).map_err(|source| CheckPidError { source, pid })?; Ok(None) } } From 628a9cd8eab0c41aee0011bfab1462c7bc54adf5 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:34:55 -0300 Subject: [PATCH 335/744] thread view: Add link to docs in the toolbar plus menu (#36883) Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 10 ++++++++++ crates/ui/src/components/context_menu.rs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 1eafb8dd4d08246b006e522922a7ef65fc105e85..269aec33659a5c39c87137b79f4c32d9da930d4a 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -9,6 +9,7 @@ use agent_servers::AgentServerSettings; use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; +use zed_actions::OpenBrowser; use zed_actions::agent::ReauthenticateAgent; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; @@ -2689,6 +2690,15 @@ impl AgentPanel { } menu + }) + .when(cx.has_flag::(), |menu| { + menu.separator().link( + "Add Your Own Agent", + OpenBrowser { + url: "https://agentclientprotocol.com/".into(), + } + .boxed_clone(), + ) }); menu })) diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 25575c4f1e1b16092d5305577099240dd58ecc5a..21ab283d883eb631e1fa2fd5ec3ca571ba0f898c 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -561,7 +561,7 @@ impl ContextMenu { action: Some(action.boxed_clone()), handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), icon: Some(IconName::ArrowUpRight), - icon_size: IconSize::Small, + icon_size: IconSize::XSmall, icon_position: IconPosition::End, icon_color: None, disabled: false, From 65de969cc858fe2d309895643754d5a0ad3d7880 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 26 Aug 2025 00:16:37 +0300 Subject: [PATCH 336/744] Do not show directories in the `InvalidBufferView` (#36906) Follow-up of https://github.com/zed-industries/zed/pull/36764 Release Notes: - N/A --- crates/editor/src/items.rs | 2 +- crates/language_tools/src/lsp_log.rs | 1 - crates/workspace/src/invalid_buffer_view.rs | 10 +- crates/workspace/src/item.rs | 4 +- crates/workspace/src/workspace.rs | 119 +++++++------------- 5 files changed, 50 insertions(+), 86 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 641e8a97ed0ef45cab42e33fb40677dd7cb21fb4..b7110190fd8931ed9c1b4ee075b47f89d7f1e992 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1404,7 +1404,7 @@ impl ProjectItem for Editor { } fn for_broken_project_item( - abs_path: PathBuf, + abs_path: &Path, is_local: bool, e: &anyhow::Error, window: &mut Window, diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 43c0365291c349a8e456c9172e46d121a6f23fe7..d5206c1f264b3b49f504090154f2df6e4ddf55be 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1743,6 +1743,5 @@ pub enum Event { } impl EventEmitter for LogStore {} -impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} diff --git a/crates/workspace/src/invalid_buffer_view.rs b/crates/workspace/src/invalid_buffer_view.rs index b017373474c041929c09c7bdaec70ec2f1b5b6fb..b8c0db29d3ab95497fc5e850b0738b762f42b28b 100644 --- a/crates/workspace/src/invalid_buffer_view.rs +++ b/crates/workspace/src/invalid_buffer_view.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, sync::Arc}; +use std::{path::Path, sync::Arc}; use gpui::{EventEmitter, FocusHandle, Focusable}; use ui::{ @@ -12,7 +12,7 @@ use crate::Item; /// A view to display when a certain buffer fails to open. pub struct InvalidBufferView { /// Which path was attempted to open. - pub abs_path: Arc, + pub abs_path: Arc, /// An error message, happened when opening the buffer. pub error: SharedString, is_local: bool, @@ -21,7 +21,7 @@ pub struct InvalidBufferView { impl InvalidBufferView { pub fn new( - abs_path: PathBuf, + abs_path: &Path, is_local: bool, e: &anyhow::Error, _: &mut Window, @@ -29,7 +29,7 @@ impl InvalidBufferView { ) -> Self { Self { is_local, - abs_path: Arc::new(abs_path), + abs_path: Arc::from(abs_path), error: format!("{e}").into(), focus_handle: cx.focus_handle(), } @@ -43,7 +43,7 @@ impl Item for InvalidBufferView { // Ensure we always render at least the filename. detail += 1; - let path = self.abs_path.as_path(); + let path = self.abs_path.as_ref(); let mut prefix = path; while detail > 0 { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 3485fcca439912a2db86b25a25295c42c198253a..db91bd82b904b40d0eaf2466689156f03d3723f3 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -23,7 +23,7 @@ use std::{ any::{Any, TypeId}, cell::RefCell, ops::Range, - path::PathBuf, + path::Path, rc::Rc, sync::Arc, time::Duration, @@ -1168,7 +1168,7 @@ pub trait ProjectItem: Item { /// with the error from that failure as an argument. /// Allows to open an item that can gracefully display and handle errors. fn for_broken_project_item( - _abs_path: PathBuf, + _abs_path: &Path, _is_local: bool, _e: &anyhow::Error, _window: &mut Window, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0b4694601ed5b79d281248edf981d46ab15ae7cf..044601df97840bc95a821cd4f2ccfc2f8b0abbbf 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -613,48 +613,59 @@ impl ProjectItemRegistry { self.build_project_item_for_path_fns .push(|project, project_path, window, cx| { let project_path = project_path.clone(); - let abs_path = project.read(cx).absolute_path(&project_path, cx); + let is_file = project + .read(cx) + .entry_for_path(&project_path, cx) + .is_some_and(|entry| entry.is_file()); + let entry_abs_path = project.read(cx).absolute_path(&project_path, cx); let is_local = project.read(cx).is_local(); let project_item = ::try_open(project, &project_path, cx)?; let project = project.clone(); - Some(window.spawn(cx, async move |cx| match project_item.await { - Ok(project_item) => { - let project_item = project_item; - let project_entry_id: Option = - project_item.read_with(cx, project::ProjectItem::entry_id)?; - let build_workspace_item = Box::new( - |pane: &mut Pane, window: &mut Window, cx: &mut Context| { - Box::new(cx.new(|cx| { - T::for_project_item( - project, - Some(pane), - project_item, - window, - cx, - ) - })) as Box - }, - ) as Box<_>; - Ok((project_entry_id, build_workspace_item)) - } - Err(e) => match abs_path { - Some(abs_path) => match cx.update(|window, cx| { - T::for_broken_project_item(abs_path, is_local, &e, window, cx) - })? { - Some(broken_project_item_view) => { - let build_workspace_item = Box::new( + Some(window.spawn(cx, async move |cx| { + match project_item.await.with_context(|| { + format!( + "opening project path {:?}", + entry_abs_path.as_deref().unwrap_or(&project_path.path) + ) + }) { + Ok(project_item) => { + let project_item = project_item; + let project_entry_id: Option = + project_item.read_with(cx, project::ProjectItem::entry_id)?; + let build_workspace_item = Box::new( + |pane: &mut Pane, window: &mut Window, cx: &mut Context| { + Box::new(cx.new(|cx| { + T::for_project_item( + project, + Some(pane), + project_item, + window, + cx, + ) + })) as Box + }, + ) as Box<_>; + Ok((project_entry_id, build_workspace_item)) + } + Err(e) => match entry_abs_path.as_deref().filter(|_| is_file) { + Some(abs_path) => match cx.update(|window, cx| { + T::for_broken_project_item(abs_path, is_local, &e, window, cx) + })? { + Some(broken_project_item_view) => { + let build_workspace_item = Box::new( move |_: &mut Pane, _: &mut Window, cx: &mut Context| { cx.new(|_| broken_project_item_view).boxed_clone() }, ) as Box<_>; - Ok((None, build_workspace_item)) - } + Ok((None, build_workspace_item)) + } + None => Err(e)?, + }, None => Err(e)?, }, - None => Err(e)?, - }, + } })) }); } @@ -4011,52 +4022,6 @@ impl Workspace { maybe_pane_handle } - pub fn split_pane_with_item( - &mut self, - pane_to_split: WeakEntity, - split_direction: SplitDirection, - from: WeakEntity, - item_id_to_move: EntityId, - window: &mut Window, - cx: &mut Context, - ) { - let Some(pane_to_split) = pane_to_split.upgrade() else { - return; - }; - let Some(from) = from.upgrade() else { - return; - }; - - let new_pane = self.add_pane(window, cx); - move_item(&from, &new_pane, item_id_to_move, 0, true, window, cx); - self.center - .split(&pane_to_split, &new_pane, split_direction) - .unwrap(); - cx.notify(); - } - - pub fn split_pane_with_project_entry( - &mut self, - pane_to_split: WeakEntity, - split_direction: SplitDirection, - project_entry: ProjectEntryId, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - let pane_to_split = pane_to_split.upgrade()?; - let new_pane = self.add_pane(window, cx); - self.center - .split(&pane_to_split, &new_pane, split_direction) - .unwrap(); - - let path = self.project.read(cx).path_for_entry(project_entry, cx)?; - let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx); - Some(cx.foreground_executor().spawn(async move { - task.await?; - Ok(()) - })) - } - pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context) { let active_item = self.active_pane.read(cx).active_item(); for pane in &self.panes { From 1460573dd4397e193764e80f2854ba33d94495ce Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Aug 2025 16:04:44 -0600 Subject: [PATCH 337/744] acp: Rename dev command (#36908) Release Notes: - N/A --- crates/acp_tools/src/acp_tools.rs | 4 ++-- crates/zed/src/zed.rs | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs index ee12b04cdec06809b93f8f991f0c32788772a5b7..e20a040e9da70a40066f3e5534171818de34a936 100644 --- a/crates/acp_tools/src/acp_tools.rs +++ b/crates/acp_tools/src/acp_tools.rs @@ -21,12 +21,12 @@ use ui::prelude::*; use util::ResultExt as _; use workspace::{Item, Workspace}; -actions!(acp, [OpenDebugTools]); +actions!(dev, [OpenAcpLogs]); pub fn init(cx: &mut App) { cx.observe_new( |workspace: &mut Workspace, _window, _cx: &mut Context| { - workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| { + workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| { let acp_tools = Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx))); workspace.add_item_to_active_pane(acp_tools, None, true, window, cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1b9657dcc69592aff2a5633b674bd40cf31be468..638e1dca0e261dcb7d66c7ac2b8df9ed9ac78ff9 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4434,7 +4434,6 @@ mod tests { assert_eq!(actions_without_namespace, Vec::<&str>::new()); let expected_namespaces = vec![ - "acp", "activity_indicator", "agent", #[cfg(not(target_os = "macos"))] From f8667a837949597200e2ae8e490d947c6cda75aa Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Aug 2025 16:23:58 -0600 Subject: [PATCH 338/744] Remove unused files (#36909) Closes #ISSUE Release Notes: - N/A --- crates/agent2/src/tests/mod.rs | 1 + crates/agent_servers/src/acp/v0.rs | 524 ----------------------------- crates/agent_servers/src/acp/v1.rs | 376 --------------------- 3 files changed, 1 insertion(+), 900 deletions(-) delete mode 100644 crates/agent_servers/src/acp/v0.rs delete mode 100644 crates/agent_servers/src/acp/v1.rs diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 864fbf8b104b1190a778b3d157853f2d38134976..093b8ba971ae5509478122f93180c124bd16eab5 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1347,6 +1347,7 @@ async fn test_cancellation(cx: &mut TestAppContext) { } #[gpui::test] +#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs deleted file mode 100644 index be960489293f6db935d5d4cba0160ed807079c64..0000000000000000000000000000000000000000 --- a/crates/agent_servers/src/acp/v0.rs +++ /dev/null @@ -1,524 +0,0 @@ -// Translates old acp agents into the new schema -use action_log::ActionLog; -use agent_client_protocol as acp; -use agentic_coding_protocol::{self as acp_old, AgentRequest as _}; -use anyhow::{Context as _, Result, anyhow}; -use futures::channel::oneshot; -use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; -use project::Project; -use std::{any::Any, cell::RefCell, path::Path, rc::Rc}; -use ui::App; -use util::ResultExt as _; - -use crate::AgentServerCommand; -use acp_thread::{AcpThread, AgentConnection, AuthRequired}; - -#[derive(Clone)] -struct OldAcpClientDelegate { - thread: Rc>>, - cx: AsyncApp, - next_tool_call_id: Rc>, - // sent_buffer_versions: HashMap, HashMap>, -} - -impl OldAcpClientDelegate { - fn new(thread: Rc>>, cx: AsyncApp) -> Self { - Self { - thread, - cx, - next_tool_call_id: Rc::new(RefCell::new(0)), - } - } -} - -impl acp_old::Client for OldAcpClientDelegate { - async fn stream_assistant_message_chunk( - &self, - params: acp_old::StreamAssistantMessageChunkParams, - ) -> Result<(), acp_old::Error> { - let cx = &mut self.cx.clone(); - - cx.update(|cx| { - self.thread - .borrow() - .update(cx, |thread, cx| match params.chunk { - acp_old::AssistantMessageChunk::Text { text } => { - thread.push_assistant_content_block(text.into(), false, cx) - } - acp_old::AssistantMessageChunk::Thought { thought } => { - thread.push_assistant_content_block(thought.into(), true, cx) - } - }) - .log_err(); - })?; - - Ok(()) - } - - async fn request_tool_call_confirmation( - &self, - request: acp_old::RequestToolCallConfirmationParams, - ) -> Result { - let cx = &mut self.cx.clone(); - - let old_acp_id = *self.next_tool_call_id.borrow() + 1; - self.next_tool_call_id.replace(old_acp_id); - - let tool_call = into_new_tool_call( - acp::ToolCallId(old_acp_id.to_string().into()), - request.tool_call, - ); - - let mut options = match request.confirmation { - acp_old::ToolCallConfirmation::Edit { .. } => vec![( - acp_old::ToolCallConfirmationOutcome::AlwaysAllow, - acp::PermissionOptionKind::AllowAlways, - "Always Allow Edits".to_string(), - )], - acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![( - acp_old::ToolCallConfirmationOutcome::AlwaysAllow, - acp::PermissionOptionKind::AllowAlways, - format!("Always Allow {}", root_command), - )], - acp_old::ToolCallConfirmation::Mcp { - server_name, - tool_name, - .. - } => vec![ - ( - acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer, - acp::PermissionOptionKind::AllowAlways, - format!("Always Allow {}", server_name), - ), - ( - acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool, - acp::PermissionOptionKind::AllowAlways, - format!("Always Allow {}", tool_name), - ), - ], - acp_old::ToolCallConfirmation::Fetch { .. } => vec![( - acp_old::ToolCallConfirmationOutcome::AlwaysAllow, - acp::PermissionOptionKind::AllowAlways, - "Always Allow".to_string(), - )], - acp_old::ToolCallConfirmation::Other { .. } => vec![( - acp_old::ToolCallConfirmationOutcome::AlwaysAllow, - acp::PermissionOptionKind::AllowAlways, - "Always Allow".to_string(), - )], - }; - - options.extend([ - ( - acp_old::ToolCallConfirmationOutcome::Allow, - acp::PermissionOptionKind::AllowOnce, - "Allow".to_string(), - ), - ( - acp_old::ToolCallConfirmationOutcome::Reject, - acp::PermissionOptionKind::RejectOnce, - "Reject".to_string(), - ), - ]); - - let mut outcomes = Vec::with_capacity(options.len()); - let mut acp_options = Vec::with_capacity(options.len()); - - for (index, (outcome, kind, label)) in options.into_iter().enumerate() { - outcomes.push(outcome); - acp_options.push(acp::PermissionOption { - id: acp::PermissionOptionId(index.to_string().into()), - name: label, - kind, - }) - } - - let response = cx - .update(|cx| { - self.thread.borrow().update(cx, |thread, cx| { - thread.request_tool_call_authorization(tool_call.into(), acp_options, cx) - }) - })?? - .context("Failed to update thread")? - .await; - - let outcome = match response { - Ok(option_id) => outcomes[option_id.0.parse::().unwrap_or(0)], - Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel, - }; - - Ok(acp_old::RequestToolCallConfirmationResponse { - id: acp_old::ToolCallId(old_acp_id), - outcome, - }) - } - - async fn push_tool_call( - &self, - request: acp_old::PushToolCallParams, - ) -> Result { - let cx = &mut self.cx.clone(); - - let old_acp_id = *self.next_tool_call_id.borrow() + 1; - self.next_tool_call_id.replace(old_acp_id); - - cx.update(|cx| { - self.thread.borrow().update(cx, |thread, cx| { - thread.upsert_tool_call( - into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request), - cx, - ) - }) - })?? - .context("Failed to update thread")?; - - Ok(acp_old::PushToolCallResponse { - id: acp_old::ToolCallId(old_acp_id), - }) - } - - async fn update_tool_call( - &self, - request: acp_old::UpdateToolCallParams, - ) -> Result<(), acp_old::Error> { - let cx = &mut self.cx.clone(); - - cx.update(|cx| { - self.thread.borrow().update(cx, |thread, cx| { - thread.update_tool_call( - acp::ToolCallUpdate { - id: acp::ToolCallId(request.tool_call_id.0.to_string().into()), - fields: acp::ToolCallUpdateFields { - status: Some(into_new_tool_call_status(request.status)), - content: Some( - request - .content - .into_iter() - .map(into_new_tool_call_content) - .collect::>(), - ), - ..Default::default() - }, - }, - cx, - ) - }) - })? - .context("Failed to update thread")??; - - Ok(()) - } - - async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> { - let cx = &mut self.cx.clone(); - - cx.update(|cx| { - self.thread.borrow().update(cx, |thread, cx| { - thread.update_plan( - acp::Plan { - entries: request - .entries - .into_iter() - .map(into_new_plan_entry) - .collect(), - }, - cx, - ) - }) - })? - .context("Failed to update thread")?; - - Ok(()) - } - - async fn read_text_file( - &self, - acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams, - ) -> Result { - let content = self - .cx - .update(|cx| { - self.thread.borrow().update(cx, |thread, cx| { - thread.read_text_file(path, line, limit, false, cx) - }) - })? - .context("Failed to update thread")? - .await?; - Ok(acp_old::ReadTextFileResponse { content }) - } - - async fn write_text_file( - &self, - acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams, - ) -> Result<(), acp_old::Error> { - self.cx - .update(|cx| { - self.thread - .borrow() - .update(cx, |thread, cx| thread.write_text_file(path, content, cx)) - })? - .context("Failed to update thread")? - .await?; - - Ok(()) - } -} - -fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall { - acp::ToolCall { - id, - title: request.label, - kind: acp_kind_from_old_icon(request.icon), - status: acp::ToolCallStatus::InProgress, - content: request - .content - .into_iter() - .map(into_new_tool_call_content) - .collect(), - locations: request - .locations - .into_iter() - .map(into_new_tool_call_location) - .collect(), - raw_input: None, - raw_output: None, - } -} - -fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind { - match icon { - acp_old::Icon::FileSearch => acp::ToolKind::Search, - acp_old::Icon::Folder => acp::ToolKind::Search, - acp_old::Icon::Globe => acp::ToolKind::Search, - acp_old::Icon::Hammer => acp::ToolKind::Other, - acp_old::Icon::LightBulb => acp::ToolKind::Think, - acp_old::Icon::Pencil => acp::ToolKind::Edit, - acp_old::Icon::Regex => acp::ToolKind::Search, - acp_old::Icon::Terminal => acp::ToolKind::Execute, - } -} - -fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus { - match status { - acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress, - acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed, - acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed, - } -} - -fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent { - match content { - acp_old::ToolCallContent::Markdown { markdown } => markdown.into(), - acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff { - diff: into_new_diff(diff), - }, - } -} - -fn into_new_diff(diff: acp_old::Diff) -> acp::Diff { - acp::Diff { - path: diff.path, - old_text: diff.old_text, - new_text: diff.new_text, - } -} - -fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation { - acp::ToolCallLocation { - path: location.path, - line: location.line, - } -} - -fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry { - acp::PlanEntry { - content: entry.content, - priority: into_new_plan_priority(entry.priority), - status: into_new_plan_status(entry.status), - } -} - -fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority { - match priority { - acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low, - acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium, - acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High, - } -} - -fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus { - match status { - acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending, - acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress, - acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed, - } -} - -pub struct AcpConnection { - pub name: &'static str, - pub connection: acp_old::AgentConnection, - pub _child_status: Task>, - pub current_thread: Rc>>, -} - -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(); - log::trace!("Spawned (pid: {})", child.id()); - - 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, - _cwd: &Path, - cx: &mut App, - ) -> Task>> { - let task = self.connection.request_any( - acp_old::InitializeParams { - protocol_version: acp_old::ProtocolVersion::latest(), - } - .into_any(), - ); - let current_thread = self.current_thread.clone(); - cx.spawn(async move |cx| { - let result = task.await?; - let result = acp_old::InitializeParams::response_from_any(result)?; - - if !result.is_authenticated { - anyhow::bail!(AuthRequired::new()) - } - - cx.update(|cx| { - let thread = cx.new(|cx| { - let session_id = acp::SessionId("acp-old-no-id".into()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); - AcpThread::new(self.name, self.clone(), project, action_log, session_id) - }); - current_thread.replace(thread.downgrade()); - thread - }) - }) - } - - 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()); - cx.foreground_executor().spawn(async move { - task.await?; - Ok(()) - }) - } - - fn prompt( - &self, - _id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { - let chunks = params - .prompt - .into_iter() - .filter_map(|block| match block { - acp::ContentBlock::Text(text) => { - Some(acp_old::UserMessageChunk::Text { text: text.text }) - } - acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path { - path: link.uri.into(), - }), - _ => None, - }) - .collect(); - - let task = self - .connection - .request_any(acp_old::SendUserMessageParams { chunks }.into_any()); - cx.foreground_executor().spawn(async move { - task.await?; - anyhow::Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - }) - }) - } - - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - acp::PromptCapabilities { - image: false, - audio: false, - embedded_context: false, - } - } - - fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) { - let task = self - .connection - .request_any(acp_old::CancelSendMessageParams.into_any()); - cx.foreground_executor() - .spawn(async move { - task.await?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } - - fn into_any(self: Rc) -> Rc { - self - } -} diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs deleted file mode 100644 index 1945ad24834f8d175bebf9b49f856403edcb2622..0000000000000000000000000000000000000000 --- a/crates/agent_servers/src/acp/v1.rs +++ /dev/null @@ -1,376 +0,0 @@ -use acp_tools::AcpConnectionRegistry; -use action_log::ActionLog; -use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; -use anyhow::anyhow; -use collections::HashMap; -use futures::AsyncBufReadExt as _; -use futures::channel::oneshot; -use futures::io::BufReader; -use project::Project; -use serde::Deserialize; -use std::path::Path; -use std::rc::Rc; -use std::{any::Any, cell::RefCell}; - -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, LoadError}; - -pub struct AcpConnection { - server_name: &'static str, - connection: Rc, - sessions: Rc>>, - auth_methods: Vec, - prompt_capabilities: acp::PromptCapabilities, - _io_task: Task>, -} - -pub struct AcpSession { - thread: WeakEntity, - suppress_abort_err: bool, -} - -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::piped()) - .kill_on_drop(true) - .spawn()?; - - let stdout = child.stdout.take().context("Failed to take stdout")?; - let stdin = child.stdin.take().context("Failed to take stdin")?; - let stderr = child.stderr.take().context("Failed to take stderr")?; - log::trace!("Spawned (pid: {})", child.id()); - - 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); - - cx.background_spawn(async move { - let mut stderr = BufReader::new(stderr); - let mut line = String::new(); - while let Ok(n) = stderr.read_line(&mut line).await - && n > 0 - { - log::warn!("agent stderr: {}", &line); - line.clear(); - } - }) - .detach(); - - cx.spawn({ - let sessions = sessions.clone(); - async move |cx| { - let status = child.status().await?; - - for session in sessions.borrow().values() { - session - .thread - .update(cx, |thread, cx| { - thread.emit_load_error(LoadError::Exited { status }, cx) - }) - .ok(); - } - - anyhow::Ok(()) - } - }) - .detach(); - - let connection = Rc::new(connection); - - cx.update(|cx| { - AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { - registry.set_active_connection(server_name, &connection, cx) - }); - })?; - - 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, - server_name, - sessions, - prompt_capabilities: response.agent_capabilities.prompt_capabilities, - _io_task: io_task, - }) - } -} - -impl AgentConnection for AcpConnection { - fn new_thread( - self: Rc, - project: Entity, - cwd: &Path, - cx: &mut App, - ) -> 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 - .map_err(|err| { - if err.code == acp::ErrorCode::AUTH_REQUIRED.code { - let mut error = AuthRequired::new(); - - if err.message != acp::ErrorCode::AUTH_REQUIRED.message { - error = error.with_description(err.message); - } - - anyhow!(error) - } else { - anyhow!(err) - } - })?; - - let session_id = response.session_id; - let action_log = cx.new(|_| ActionLog::new(project.clone()))?; - let thread = cx.new(|_cx| { - AcpThread::new( - self.server_name, - self.clone(), - project, - action_log, - session_id.clone(), - ) - })?; - - let session = AcpSession { - thread: thread.downgrade(), - suppress_abort_err: false, - }; - 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, - _id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { - let conn = self.connection.clone(); - let sessions = self.sessions.clone(); - let session_id = params.session_id.clone(); - cx.foreground_executor().spawn(async move { - let result = conn.prompt(params).await; - - let mut suppress_abort_err = false; - - if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { - suppress_abort_err = session.suppress_abort_err; - session.suppress_abort_err = false; - } - - match result { - Ok(response) => Ok(response), - Err(err) => { - if err.code != ErrorCode::INTERNAL_ERROR.code { - anyhow::bail!(err) - } - - let Some(data) = &err.data else { - anyhow::bail!(err) - }; - - // Temporary workaround until the following PR is generally available: - // https://github.com/google-gemini/gemini-cli/pull/6656 - - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] - struct ErrorDetails { - details: Box, - } - - match serde_json::from_value(data.clone()) { - Ok(ErrorDetails { details }) => { - if suppress_abort_err && details.contains("This operation was aborted") - { - Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Cancelled, - }) - } else { - Err(anyhow!(details)) - } - } - Err(_) => Err(anyhow!(err)), - } - } - } - }) - } - - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - self.prompt_capabilities - } - - fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { - if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { - session.suppress_abort_err = true; - } - let conn = self.connection.clone(); - let params = acp::CancelNotification { - session_id: session_id.clone(), - }; - cx.foreground_executor() - .spawn(async move { conn.cancel(params).await }) - .detach(); - } - - fn into_any(self: Rc) -> Rc { - self - } -} - -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_authorization(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(()) - } -} From d43df9e841bce3af1df219690c5c796f8bbff99a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 25 Aug 2025 17:27:52 -0700 Subject: [PATCH 339/744] Fix workspace migration failure (#36911) This fixes a regression on nightly introduced in https://github.com/zed-industries/zed/pull/36714 Release Notes: - N/A --- crates/command_palette/src/persistence.rs | 18 +- crates/db/src/db.rs | 118 +--- crates/db/src/kvp.rs | 30 +- crates/editor/src/persistence.rs | 27 +- crates/image_viewer/src/image_viewer.rs | 19 +- crates/onboarding/src/onboarding.rs | 21 +- crates/onboarding/src/welcome.rs | 21 +- crates/settings_ui/src/keybindings.rs | 15 +- crates/sqlez/src/domain.rs | 14 +- crates/sqlez/src/migrations.rs | 64 +- crates/sqlez/src/thread_safe_connection.rs | 18 +- crates/terminal_view/src/persistence.rs | 18 +- crates/vim/src/state.rs | 18 +- crates/workspace/src/path_list.rs | 14 +- crates/workspace/src/persistence.rs | 655 +++++++++--------- .../src/zed/component_preview/persistence.rs | 19 +- 16 files changed, 588 insertions(+), 501 deletions(-) diff --git a/crates/command_palette/src/persistence.rs b/crates/command_palette/src/persistence.rs index 5be97c36bc57cea59b51272270fd39ae1a9ab70d..01cf403083b2de4ed7919801ab33e4aae947007e 100644 --- a/crates/command_palette/src/persistence.rs +++ b/crates/command_palette/src/persistence.rs @@ -1,7 +1,10 @@ use anyhow::Result; use db::{ - define_connection, query, - sqlez::{bindable::Column, statement::Statement}, + query, + sqlez::{ + bindable::Column, domain::Domain, statement::Statement, + thread_safe_connection::ThreadSafeConnection, + }, sqlez_macros::sql, }; use serde::{Deserialize, Serialize}; @@ -50,8 +53,11 @@ impl Column for SerializedCommandInvocation { } } -define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> = - &[sql!( +pub struct CommandPaletteDB(ThreadSafeConnection); + +impl Domain for CommandPaletteDB { + const NAME: &str = stringify!(CommandPaletteDB); + const MIGRATIONS: &[&str] = &[sql!( CREATE TABLE IF NOT EXISTS command_invocations( id INTEGER PRIMARY KEY AUTOINCREMENT, command_name TEXT NOT NULL, @@ -59,7 +65,9 @@ define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL ) STRICT; )]; -); +} + +db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []); impl CommandPaletteDB { pub async fn write_command_invocation( diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 8b790cbec8498c1c3f83d55b25c14042b04b9424..0802bd8bb7ec738b948d0dbf14c24863833e3ba1 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -110,11 +110,14 @@ pub async fn open_test_db(db_name: &str) -> ThreadSafeConnection { } /// Implements a basic DB wrapper for a given domain +/// +/// Arguments: +/// - static variable name for connection +/// - type of connection wrapper +/// - dependencies, whose migrations should be run prior to this domain's migrations #[macro_export] -macro_rules! define_connection { - (pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => { - pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection); - +macro_rules! static_connection { + ($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => { impl ::std::ops::Deref for $t { type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection; @@ -123,16 +126,6 @@ macro_rules! define_connection { } } - impl $crate::sqlez::domain::Domain for $t { - fn name() -> &'static str { - stringify!($t) - } - - fn migrations() -> &'static [&'static str] { - $migrations - } - } - impl $t { #[cfg(any(test, feature = "test-support"))] pub async fn open_test_db(name: &'static str) -> Self { @@ -142,44 +135,8 @@ macro_rules! define_connection { #[cfg(any(test, feature = "test-support"))] pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { - $t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id)))) - }); - - #[cfg(not(any(test, feature = "test-support")))] - pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { - let db_dir = $crate::database_dir(); - let scope = if false $(|| stringify!($global) == "global")? { - "global" - } else { - $crate::RELEASE_CHANNEL.dev_name() - }; - $t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope))) - }); - }; - (pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => { - pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection); - - impl ::std::ops::Deref for $t { - type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl $crate::sqlez::domain::Domain for $t { - fn name() -> &'static str { - stringify!($t) - } - - fn migrations() -> &'static [&'static str] { - $migrations - } - } - - #[cfg(any(test, feature = "test-support"))] - pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { - $t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id)))) + #[allow(unused_parens)] + $t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id)))) }); #[cfg(not(any(test, feature = "test-support")))] @@ -190,9 +147,10 @@ macro_rules! define_connection { } else { $crate::RELEASE_CHANNEL.dev_name() }; - $t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope))) + #[allow(unused_parens)] + $t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope))) }); - }; + } } pub fn write_and_log(cx: &App, db_write: impl FnOnce() -> F + Send + 'static) @@ -219,17 +177,12 @@ mod tests { enum BadDB {} impl Domain for BadDB { - fn name() -> &'static str { - "db_tests" - } - - fn migrations() -> &'static [&'static str] { - &[ - sql!(CREATE TABLE test(value);), - // failure because test already exists - sql!(CREATE TABLE test(value);), - ] - } + const NAME: &str = "db_tests"; + const MIGRATIONS: &[&str] = &[ + sql!(CREATE TABLE test(value);), + // failure because test already exists + sql!(CREATE TABLE test(value);), + ]; } let tempdir = tempfile::Builder::new() @@ -251,25 +204,15 @@ mod tests { enum CorruptedDB {} impl Domain for CorruptedDB { - fn name() -> &'static str { - "db_tests" - } - - fn migrations() -> &'static [&'static str] { - &[sql!(CREATE TABLE test(value);)] - } + const NAME: &str = "db_tests"; + const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)]; } enum GoodDB {} impl Domain for GoodDB { - fn name() -> &'static str { - "db_tests" //Notice same name - } - - fn migrations() -> &'static [&'static str] { - &[sql!(CREATE TABLE test2(value);)] //But different migration - } + const NAME: &str = "db_tests"; //Notice same name + const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; } let tempdir = tempfile::Builder::new() @@ -305,25 +248,16 @@ mod tests { enum CorruptedDB {} impl Domain for CorruptedDB { - fn name() -> &'static str { - "db_tests" - } + const NAME: &str = "db_tests"; - fn migrations() -> &'static [&'static str] { - &[sql!(CREATE TABLE test(value);)] - } + const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)]; } enum GoodDB {} impl Domain for GoodDB { - fn name() -> &'static str { - "db_tests" //Notice same name - } - - fn migrations() -> &'static [&'static str] { - &[sql!(CREATE TABLE test2(value);)] //But different migration - } + const NAME: &str = "db_tests"; //Notice same name + const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration } let tempdir = tempfile::Builder::new() diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index 256b789c9b2f2909ec5b12f6dc9dd60c04555e51..8ea877b35bfaf57bb258e7e179fa5b71f2b518ea 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -2,16 +2,26 @@ use gpui::App; use sqlez_macros::sql; use util::ResultExt as _; -use crate::{define_connection, query, write_and_log}; +use crate::{ + query, + sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, + write_and_log, +}; -define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> = - &[sql!( +pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection); + +impl Domain for KeyValueStore { + const NAME: &str = stringify!(KeyValueStore); + + const MIGRATIONS: &[&str] = &[sql!( CREATE TABLE IF NOT EXISTS kv_store( key TEXT PRIMARY KEY, value TEXT NOT NULL ) STRICT; )]; -); +} + +crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []); pub trait Dismissable { const KEY: &'static str; @@ -91,15 +101,19 @@ mod tests { } } -define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> = - &[sql!( +pub struct GlobalKeyValueStore(ThreadSafeConnection); + +impl Domain for GlobalKeyValueStore { + const NAME: &str = stringify!(GlobalKeyValueStore); + const MIGRATIONS: &[&str] = &[sql!( CREATE TABLE IF NOT EXISTS kv_store( key TEXT PRIMARY KEY, value TEXT NOT NULL ) STRICT; )]; - global -); +} + +crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global); impl GlobalKeyValueStore { query! { diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index 88fde539479b3159a2fbcb7e3b0473d4b4b91e76..ec7c149b4e107600c35e70ef3dffcdb2e8f8bcb7 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,13 +1,17 @@ use anyhow::Result; -use db::sqlez::bindable::{Bind, Column, StaticColumnCount}; -use db::sqlez::statement::Statement; +use db::{ + query, + sqlez::{ + bindable::{Bind, Column, StaticColumnCount}, + domain::Domain, + statement::Statement, + }, + sqlez_macros::sql, +}; use fs::MTime; use itertools::Itertools as _; use std::path::PathBuf; -use db::sqlez_macros::sql; -use db::{define_connection, query}; - use workspace::{ItemId, WorkspaceDb, WorkspaceId}; #[derive(Clone, Debug, PartialEq, Default)] @@ -83,7 +87,11 @@ impl Column for SerializedEditor { } } -define_connection!( +pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); + +impl Domain for EditorDb { + const NAME: &str = stringify!(EditorDb); + // Current schema shape using pseudo-rust syntax: // editors( // item_id: usize, @@ -113,7 +121,8 @@ define_connection!( // start: usize, // end: usize, // ) - pub static ref DB: EditorDb = &[ + + const MIGRATIONS: &[&str] = &[ sql! ( CREATE TABLE editors( item_id INTEGER NOT NULL, @@ -189,7 +198,9 @@ define_connection!( ) STRICT; ), ]; -); +} + +db::static_connection!(DB, EditorDb, [WorkspaceDb]); // https://www.sqlite.org/limits.html // > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index b96557b391f5941283b67b7b798ee177ab383cb2..2dca57424b86e2221acc271efac19cdf39a3f79f 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -401,12 +401,19 @@ pub fn init(cx: &mut App) { mod persistence { use std::path::PathBuf; - use db::{define_connection, query, sqlez_macros::sql}; + use db::{ + query, + sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, + sqlez_macros::sql, + }; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; - define_connection! { - pub static ref IMAGE_VIEWER: ImageViewerDb = - &[sql!( + pub struct ImageViewerDb(ThreadSafeConnection); + + impl Domain for ImageViewerDb { + const NAME: &str = stringify!(ImageViewerDb); + + const MIGRATIONS: &[&str] = &[sql!( CREATE TABLE image_viewers ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -417,9 +424,11 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } + db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]); + impl ImageViewerDb { query! { pub async fn save_image_path( diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 884374a72fe8b71bc55803b800c9429c19a96d5e..873dd63201423bba8995136e2fde82551966b3dd 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -850,13 +850,19 @@ impl workspace::SerializableItem for Onboarding { } mod persistence { - use db::{define_connection, query, sqlez_macros::sql}; + use db::{ + query, + sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, + sqlez_macros::sql, + }; use workspace::WorkspaceDb; - define_connection! { - pub static ref ONBOARDING_PAGES: OnboardingPagesDb = - &[ - sql!( + pub struct OnboardingPagesDb(ThreadSafeConnection); + + impl Domain for OnboardingPagesDb { + const NAME: &str = stringify!(OnboardingPagesDb); + + const MIGRATIONS: &[&str] = &[sql!( CREATE TABLE onboarding_pages ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -866,10 +872,11 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - ), - ]; + )]; } + db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]); + impl OnboardingPagesDb { query! { pub async fn save_onboarding_page( diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 3fe9c32a48c4f6ee9cb3756e08b1eb9a836657dc..8ff55d812b007d1b210781ec747b30cd1f505f35 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -414,13 +414,19 @@ impl workspace::SerializableItem for WelcomePage { } mod persistence { - use db::{define_connection, query, sqlez_macros::sql}; + use db::{ + query, + sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, + sqlez_macros::sql, + }; use workspace::WorkspaceDb; - define_connection! { - pub static ref WELCOME_PAGES: WelcomePagesDb = - &[ - sql!( + pub struct WelcomePagesDb(ThreadSafeConnection); + + impl Domain for WelcomePagesDb { + const NAME: &str = stringify!(WelcomePagesDb); + + const MIGRATIONS: &[&str] = (&[sql!( CREATE TABLE welcome_pages ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -430,10 +436,11 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - ), - ]; + )]); } + db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]); + impl WelcomePagesDb { query! { pub async fn save_welcome_page( diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 9c76725972cfeab2751cb05e968f9cf3e7211418..288f59c8e045becabfe24271deac66e3bc4ebe98 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -3348,12 +3348,15 @@ impl SerializableItem for KeymapEditor { } mod persistence { - use db::{define_connection, query, sqlez_macros::sql}; + use db::{query, sqlez::domain::Domain, sqlez_macros::sql}; use workspace::WorkspaceDb; - define_connection! { - pub static ref KEYBINDING_EDITORS: KeybindingEditorDb = - &[sql!( + pub struct KeybindingEditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); + + impl Domain for KeybindingEditorDb { + const NAME: &str = stringify!(KeybindingEditorDb); + + const MIGRATIONS: &[&str] = &[sql!( CREATE TABLE keybinding_editors ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -3362,9 +3365,11 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } + db::static_connection!(KEYBINDING_EDITORS, KeybindingEditorDb, [WorkspaceDb]); + impl KeybindingEditorDb { query! { pub async fn save_keybinding_editor( diff --git a/crates/sqlez/src/domain.rs b/crates/sqlez/src/domain.rs index a83f4e18d6600ce4ac1cc3373b4b235695785522..5744a67da20a8e5091a6071dcc2a0c63d5fd7448 100644 --- a/crates/sqlez/src/domain.rs +++ b/crates/sqlez/src/domain.rs @@ -1,8 +1,12 @@ use crate::connection::Connection; pub trait Domain: 'static { - fn name() -> &'static str; - fn migrations() -> &'static [&'static str]; + const NAME: &str; + const MIGRATIONS: &[&str]; + + fn should_allow_migration_change(_index: usize, _old: &str, _new: &str) -> bool { + false + } } pub trait Migrator: 'static { @@ -17,7 +21,11 @@ impl Migrator for () { impl Migrator for D { fn migrate(connection: &Connection) -> anyhow::Result<()> { - connection.migrate(Self::name(), Self::migrations()) + connection.migrate( + Self::NAME, + Self::MIGRATIONS, + Self::should_allow_migration_change, + ) } } diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index 7c59ffe65800128568b13d96cbdf457428d2b218..2429ddeb4127591b56fb74a9c84884d9dc5f378f 100644 --- a/crates/sqlez/src/migrations.rs +++ b/crates/sqlez/src/migrations.rs @@ -34,7 +34,12 @@ impl Connection { /// Note: Unlike everything else in SQLez, migrations are run eagerly, without first /// preparing the SQL statements. This makes it possible to do multi-statement schema /// updates in a single string without running into prepare errors. - pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> { + pub fn migrate( + &self, + domain: &'static str, + migrations: &[&'static str], + mut should_allow_migration_change: impl FnMut(usize, &str, &str) -> bool, + ) -> Result<()> { self.with_savepoint("migrating", || { // Setup the migrations table unconditionally self.exec(indoc! {" @@ -65,9 +70,14 @@ impl Connection { &sqlformat::QueryParams::None, Default::default(), ); - if completed_migration == migration { + if completed_migration == migration + || migration.trim().starts_with("-- ALLOW_MIGRATION_CHANGE") + { // Migration already run. Continue continue; + } else if should_allow_migration_change(index, &completed_migration, &migration) + { + continue; } else { anyhow::bail!(formatdoc! {" Migration changed for {domain} at step {index} @@ -108,6 +118,7 @@ mod test { a TEXT, b TEXT )"}], + disallow_migration_change, ) .unwrap(); @@ -136,6 +147,7 @@ mod test { d TEXT )"}, ], + disallow_migration_change, ) .unwrap(); @@ -214,7 +226,11 @@ mod test { // Run the migration verifying that the row got dropped connection - .migrate("test", &["DELETE FROM test_table"]) + .migrate( + "test", + &["DELETE FROM test_table"], + disallow_migration_change, + ) .unwrap(); assert_eq!( connection @@ -232,7 +248,11 @@ mod test { // Run the same migration again and verify that the table was left unchanged connection - .migrate("test", &["DELETE FROM test_table"]) + .migrate( + "test", + &["DELETE FROM test_table"], + disallow_migration_change, + ) .unwrap(); assert_eq!( connection @@ -252,27 +272,28 @@ mod test { .migrate( "test migration", &[ - indoc! {" - CREATE TABLE test ( - col INTEGER - )"}, - indoc! {" - INSERT INTO test (col) VALUES (1)"}, + "CREATE TABLE test (col INTEGER)", + "INSERT INTO test (col) VALUES (1)", ], + disallow_migration_change, ) .unwrap(); + let mut migration_changed = false; + // Create another migration with the same domain but different steps let second_migration_result = connection.migrate( "test migration", &[ - indoc! {" - CREATE TABLE test ( - color INTEGER - )"}, - indoc! {" - INSERT INTO test (color) VALUES (1)"}, + "CREATE TABLE test (color INTEGER )", + "INSERT INTO test (color) VALUES (1)", ], + |_, old, new| { + assert_eq!(old, "CREATE TABLE test (col INTEGER)"); + assert_eq!(new, "CREATE TABLE test (color INTEGER)"); + migration_changed = true; + false + }, ); // Verify new migration returns error when run @@ -284,7 +305,11 @@ mod test { let connection = Connection::open_memory(Some("test_create_alter_drop")); connection - .migrate("first_migration", &["CREATE TABLE table1(a TEXT) STRICT;"]) + .migrate( + "first_migration", + &["CREATE TABLE table1(a TEXT) STRICT;"], + disallow_migration_change, + ) .unwrap(); connection @@ -305,6 +330,7 @@ mod test { ALTER TABLE table2 RENAME TO table1; "}], + disallow_migration_change, ) .unwrap(); @@ -312,4 +338,8 @@ mod test { assert_eq!(res, "test text"); } + + fn disallow_migration_change(_: usize, _: &str, _: &str) -> bool { + false + } } diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index afdc96586efdf4298cb2b1b814e77920af95d53d..58d3afe78fb4d8b211c48c0ae1f9f72af74ad5c1 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -278,12 +278,8 @@ mod test { enum TestDomain {} impl Domain for TestDomain { - fn name() -> &'static str { - "test" - } - fn migrations() -> &'static [&'static str] { - &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"] - } + const NAME: &str = "test"; + const MIGRATIONS: &[&str] = &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"]; } for _ in 0..100 { @@ -312,12 +308,9 @@ mod test { fn wild_zed_lost_failure() { enum TestWorkspace {} impl Domain for TestWorkspace { - fn name() -> &'static str { - "workspace" - } + const NAME: &str = "workspace"; - fn migrations() -> &'static [&'static str] { - &[" + const MIGRATIONS: &[&str] = &[" CREATE TABLE workspaces( workspace_id INTEGER PRIMARY KEY, dock_visible INTEGER, -- Boolean @@ -336,8 +329,7 @@ mod test { ON DELETE CASCADE ON UPDATE CASCADE ) STRICT; - "] - } + "]; } let builder = diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index b93b267f585814c2faafe75a9906021f3ad15932..c7ebd314e4618300058b1a2e083d97d3bb569df3 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -9,7 +9,11 @@ use std::path::{Path, PathBuf}; use ui::{App, Context, Pixels, Window}; use util::ResultExt as _; -use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; +use db::{ + query, + sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, + sqlez_macros::sql, +}; use workspace::{ ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace, WorkspaceDb, WorkspaceId, @@ -375,9 +379,13 @@ impl<'de> Deserialize<'de> for SerializedAxis { } } -define_connection! { - pub static ref TERMINAL_DB: TerminalDb = - &[sql!( +pub struct TerminalDb(ThreadSafeConnection); + +impl Domain for TerminalDb { + const NAME: &str = stringify!(TerminalDb); + + const MIGRATIONS: &[&str] = &[ + sql!( CREATE TABLE terminals ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -414,6 +422,8 @@ define_connection! { ]; } +db::static_connection!(TERMINAL_DB, TerminalDb, [WorkspaceDb]); + impl TerminalDb { query! { pub async fn update_workspace_id( diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index c0176cb12c34ac0d58504edde1508bbfd04c6be8..fe4bc7433d57f882b9935cfd547fab6e2eb736c1 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -7,8 +7,10 @@ use crate::{motion::Motion, object::Object}; use anyhow::Result; use collections::HashMap; use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor}; -use db::define_connection; -use db::sqlez_macros::sql; +use db::{ + sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, + sqlez_macros::sql, +}; use editor::display_map::{is_invisible, replacement}; use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint}; use gpui::{ @@ -1668,8 +1670,12 @@ impl MarksView { } } -define_connection! ( - pub static ref DB: VimDb = &[ +pub struct VimDb(ThreadSafeConnection); + +impl Domain for VimDb { + const NAME: &str = stringify!(VimDb); + + const MIGRATIONS: &[&str] = &[ sql! ( CREATE TABLE vim_marks ( workspace_id INTEGER, @@ -1689,7 +1695,9 @@ define_connection! ( ON vim_global_marks_paths(workspace_id, mark_name); ), ]; -); +} + +db::static_connection!(DB, VimDb, [WorkspaceDb]); struct SerializedMark { path: Arc, diff --git a/crates/workspace/src/path_list.rs b/crates/workspace/src/path_list.rs index 4f9ed4231289516a5434cc429adc7242731d9a27..cf463e6b2295f78a8492c0f4aafd6ca6b8ed1788 100644 --- a/crates/workspace/src/path_list.rs +++ b/crates/workspace/src/path_list.rs @@ -58,11 +58,7 @@ impl PathList { let mut paths: Vec = if serialized.paths.is_empty() { Vec::new() } else { - serde_json::from_str::>(&serialized.paths) - .unwrap_or(Vec::new()) - .into_iter() - .map(|s| SanitizedPath::from(s).into()) - .collect() + serialized.paths.split('\n').map(PathBuf::from).collect() }; let mut order: Vec = serialized @@ -85,7 +81,13 @@ impl PathList { pub fn serialize(&self) -> SerializedPathList { use std::fmt::Write as _; - let paths = serde_json::to_string(&self.paths).unwrap_or_default(); + let mut paths = String::new(); + for path in self.paths.iter() { + if !paths.is_empty() { + paths.push('\n'); + } + paths.push_str(&path.to_string_lossy()); + } let mut order = String::new(); for ix in self.order.iter() { diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 39a1e08c9315e08c2f243ac5b97c1db32bcd639f..89e1147d8affaadc241af3d619fec5e1759ec654 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -10,7 +10,11 @@ use std::{ use anyhow::{Context as _, Result, bail}; use collections::HashMap; -use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; +use db::{ + query, + sqlez::{connection::Connection, domain::Domain}, + sqlez_macros::sql, +}; use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; @@ -275,186 +279,189 @@ impl sqlez::bindable::Bind for SerializedPixels { } } -define_connection! { - pub static ref DB: WorkspaceDb<()> = - &[ - sql!( - CREATE TABLE workspaces( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. - dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. - dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. - left_sidebar_open INTEGER, // Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) - ) STRICT; - - CREATE TABLE pane_groups( - group_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - parent_group_id INTEGER, // NULL indicates that this is a root node - position INTEGER, // NULL indicates that this is a root node - axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; - - CREATE TABLE panes( - pane_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - active INTEGER NOT NULL, // Boolean - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE - ) STRICT; - - CREATE TABLE center_panes( - pane_id INTEGER PRIMARY KEY, - parent_group_id INTEGER, // NULL means that this is a root pane - position INTEGER, // NULL means that this is a root pane - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; - - CREATE TABLE items( - item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique - workspace_id INTEGER NOT NULL, - pane_id INTEGER NOT NULL, - kind TEXT NOT NULL, - position INTEGER NOT NULL, - active INTEGER NOT NULL, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - PRIMARY KEY(item_id, workspace_id) - ) STRICT; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN window_state TEXT; - ALTER TABLE workspaces ADD COLUMN window_x REAL; - ALTER TABLE workspaces ADD COLUMN window_y REAL; - ALTER TABLE workspaces ADD COLUMN window_width REAL; - ALTER TABLE workspaces ADD COLUMN window_height REAL; - ALTER TABLE workspaces ADD COLUMN display BLOB; - ), - // Drop foreign key constraint from workspaces.dock_pane to panes table. - sql!( - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. - dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. - dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. - left_sidebar_open INTEGER, // Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB - ) STRICT; - INSERT INTO workspaces_2 SELECT * FROM workspaces; - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - ), - // Add panels related information - sql!( - ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; - ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; - ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; - ), - // Add panel zoom persistence - sql!( - ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool - ), - // Add pane group flex data - sql!( - ALTER TABLE pane_groups ADD COLUMN flexes TEXT; - ), - // Add fullscreen field to workspace - // Deprecated, `WindowBounds` holds the fullscreen state now. - // Preserving so users can downgrade Zed. - sql!( - ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool - ), - // Add preview field to items - sql!( - ALTER TABLE items ADD COLUMN preview INTEGER; //bool - ), - // Add centered_layout field to workspace - sql!( - ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool - ), - sql!( - CREATE TABLE remote_projects ( - remote_project_id INTEGER NOT NULL UNIQUE, - path TEXT, - dev_server_name TEXT - ); - ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER; - ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths; - ), - sql!( - DROP TABLE remote_projects; - CREATE TABLE dev_server_projects ( - id INTEGER NOT NULL UNIQUE, - path TEXT, - dev_server_name TEXT - ); - ALTER TABLE workspaces DROP COLUMN remote_project_id; - ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL; - ), - sql!( - ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0; - ), - sql!( - CREATE TABLE ssh_projects ( - id INTEGER PRIMARY KEY, - host TEXT NOT NULL, - port INTEGER, - path TEXT NOT NULL, - user TEXT - ); - ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE; - ), - sql!( - ALTER TABLE ssh_projects RENAME COLUMN path TO paths; - ), - sql!( - CREATE TABLE toolchains ( - workspace_id INTEGER, - worktree_id INTEGER, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - PRIMARY KEY (workspace_id, worktree_id, language_name) - ); - ), - sql!( - ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; - ), - sql!( +pub struct WorkspaceDb(ThreadSafeConnection); + +impl Domain for WorkspaceDb { + const NAME: &str = stringify!(WorkspaceDb); + + const MIGRATIONS: &[&str] = &[ + sql!( + CREATE TABLE workspaces( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) + ) STRICT; + + CREATE TABLE pane_groups( + group_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + parent_group_id INTEGER, // NULL indicates that this is a root node + position INTEGER, // NULL indicates that this is a root node + axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; + + CREATE TABLE panes( + pane_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + active INTEGER NOT NULL, // Boolean + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; + + CREATE TABLE center_panes( + pane_id INTEGER PRIMARY KEY, + parent_group_id INTEGER, // NULL means that this is a root pane + position INTEGER, // NULL means that this is a root pane + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; + + CREATE TABLE items( + item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique + workspace_id INTEGER NOT NULL, + pane_id INTEGER NOT NULL, + kind TEXT NOT NULL, + position INTEGER NOT NULL, + active INTEGER NOT NULL, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + PRIMARY KEY(item_id, workspace_id) + ) STRICT; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_state TEXT; + ALTER TABLE workspaces ADD COLUMN window_x REAL; + ALTER TABLE workspaces ADD COLUMN window_y REAL; + ALTER TABLE workspaces ADD COLUMN window_width REAL; + ALTER TABLE workspaces ADD COLUMN window_height REAL; + ALTER TABLE workspaces ADD COLUMN display BLOB; + ), + // Drop foreign key constraint from workspaces.dock_pane to panes table. + sql!( + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB + ) STRICT; + INSERT INTO workspaces_2 SELECT * FROM workspaces; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + ), + // Add panels related information + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; + ), + // Add panel zoom persistence + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool + ), + // Add pane group flex data + sql!( + ALTER TABLE pane_groups ADD COLUMN flexes TEXT; + ), + // Add fullscreen field to workspace + // Deprecated, `WindowBounds` holds the fullscreen state now. + // Preserving so users can downgrade Zed. + sql!( + ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool + ), + // Add preview field to items + sql!( + ALTER TABLE items ADD COLUMN preview INTEGER; //bool + ), + // Add centered_layout field to workspace + sql!( + ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool + ), + sql!( + CREATE TABLE remote_projects ( + remote_project_id INTEGER NOT NULL UNIQUE, + path TEXT, + dev_server_name TEXT + ); + ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER; + ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths; + ), + sql!( + DROP TABLE remote_projects; + CREATE TABLE dev_server_projects ( + id INTEGER NOT NULL UNIQUE, + path TEXT, + dev_server_name TEXT + ); + ALTER TABLE workspaces DROP COLUMN remote_project_id; + ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL; + ), + sql!( + ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0; + ), + sql!( + CREATE TABLE ssh_projects ( + id INTEGER PRIMARY KEY, + host TEXT NOT NULL, + port INTEGER, + path TEXT NOT NULL, + user TEXT + ); + ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE; + ), + sql!( + ALTER TABLE ssh_projects RENAME COLUMN path TO paths; + ), + sql!( + CREATE TABLE toolchains ( + workspace_id INTEGER, + worktree_id INTEGER, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + PRIMARY KEY (workspace_id, worktree_id, language_name) + ); + ), + sql!( + ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; + ), + sql!( CREATE TABLE breakpoints ( workspace_id INTEGER NOT NULL, path TEXT NOT NULL, @@ -466,141 +473,165 @@ define_connection! { ON UPDATE CASCADE ); ), - sql!( - ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT; - CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); - ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT; - ), - sql!( - ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL - ), - sql!( - ALTER TABLE breakpoints DROP COLUMN kind - ), - sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL), - sql!( - ALTER TABLE breakpoints ADD COLUMN condition TEXT; - ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; - ), - sql!(CREATE TABLE toolchains2 ( - workspace_id INTEGER, - worktree_id INTEGER, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - raw_json TEXT NOT NULL, - relative_worktree_path TEXT NOT NULL, - PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT; - INSERT INTO toolchains2 - SELECT * FROM toolchains; - DROP TABLE toolchains; - ALTER TABLE toolchains2 RENAME TO toolchains; - ), - sql!( - CREATE TABLE ssh_connections ( - id INTEGER PRIMARY KEY, - host TEXT NOT NULL, - port INTEGER, - user TEXT - ); + sql!( + ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT; + CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); + ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT; + ), + sql!( + ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL + ), + sql!( + ALTER TABLE breakpoints DROP COLUMN kind + ), + sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL), + sql!( + ALTER TABLE breakpoints ADD COLUMN condition TEXT; + ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; + ), + sql!(CREATE TABLE toolchains2 ( + workspace_id INTEGER, + worktree_id INTEGER, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + raw_json TEXT NOT NULL, + relative_worktree_path TEXT NOT NULL, + PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT; + INSERT INTO toolchains2 + SELECT * FROM toolchains; + DROP TABLE toolchains; + ALTER TABLE toolchains2 RENAME TO toolchains; + ), + sql!( + CREATE TABLE ssh_connections ( + id INTEGER PRIMARY KEY, + host TEXT NOT NULL, + port INTEGER, + user TEXT + ); - INSERT INTO ssh_connections (host, port, user) - SELECT DISTINCT host, port, user - FROM ssh_projects; - - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - paths TEXT, - paths_order TEXT, - ssh_connection_id INTEGER REFERENCES ssh_connections(id), - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB, - left_dock_visible INTEGER, - left_dock_active_panel TEXT, - right_dock_visible INTEGER, - right_dock_active_panel TEXT, - bottom_dock_visible INTEGER, - bottom_dock_active_panel TEXT, - left_dock_zoom INTEGER, - right_dock_zoom INTEGER, - bottom_dock_zoom INTEGER, - fullscreen INTEGER, - centered_layout INTEGER, - session_id TEXT, - window_id INTEGER - ) STRICT; - - INSERT - INTO workspaces_2 - SELECT - workspaces.workspace_id, - CASE - WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths + INSERT INTO ssh_connections (host, port, user) + SELECT DISTINCT host, port, user + FROM ssh_projects; + + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + paths TEXT, + paths_order TEXT, + ssh_connection_id INTEGER REFERENCES ssh_connections(id), + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB, + left_dock_visible INTEGER, + left_dock_active_panel TEXT, + right_dock_visible INTEGER, + right_dock_active_panel TEXT, + bottom_dock_visible INTEGER, + bottom_dock_active_panel TEXT, + left_dock_zoom INTEGER, + right_dock_zoom INTEGER, + bottom_dock_zoom INTEGER, + fullscreen INTEGER, + centered_layout INTEGER, + session_id TEXT, + window_id INTEGER + ) STRICT; + + INSERT + INTO workspaces_2 + SELECT + workspaces.workspace_id, + CASE + WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths + ELSE + CASE + WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN + NULL + ELSE + replace(workspaces.local_paths_array, ',', "\n") + END + END as paths, + + CASE + WHEN ssh_projects.id IS NOT NULL THEN "" + ELSE workspaces.local_paths_order_array + END as paths_order, + + CASE + WHEN ssh_projects.id IS NOT NULL THEN ( + SELECT ssh_connections.id + FROM ssh_connections + WHERE + ssh_connections.host IS ssh_projects.host AND + ssh_connections.port IS ssh_projects.port AND + ssh_connections.user IS ssh_projects.user + ) + ELSE NULL + END as ssh_connection_id, + + workspaces.timestamp, + workspaces.window_state, + workspaces.window_x, + workspaces.window_y, + workspaces.window_width, + workspaces.window_height, + workspaces.display, + workspaces.left_dock_visible, + workspaces.left_dock_active_panel, + workspaces.right_dock_visible, + workspaces.right_dock_active_panel, + workspaces.bottom_dock_visible, + workspaces.bottom_dock_active_panel, + workspaces.left_dock_zoom, + workspaces.right_dock_zoom, + workspaces.bottom_dock_zoom, + workspaces.fullscreen, + workspaces.centered_layout, + workspaces.session_id, + workspaces.window_id + FROM + workspaces LEFT JOIN + ssh_projects ON + workspaces.ssh_project_id = ssh_projects.id; + + DROP TABLE ssh_projects; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + + CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths); + ), + // Fix any data from when workspaces.paths were briefly encoded as JSON arrays + sql!( + UPDATE workspaces + SET paths = CASE + WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN + replace( + substr(paths, 3, length(paths) - 4), + '"' || ',' || '"', + CHAR(10) + ) ELSE - CASE - WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN - NULL - ELSE - json('[' || '"' || replace(workspaces.local_paths_array, ',', '"' || "," || '"') || '"' || ']') - END - END as paths, - - CASE - WHEN ssh_projects.id IS NOT NULL THEN "" - ELSE workspaces.local_paths_order_array - END as paths_order, - - CASE - WHEN ssh_projects.id IS NOT NULL THEN ( - SELECT ssh_connections.id - FROM ssh_connections - WHERE - ssh_connections.host IS ssh_projects.host AND - ssh_connections.port IS ssh_projects.port AND - ssh_connections.user IS ssh_projects.user - ) - ELSE NULL - END as ssh_connection_id, - - workspaces.timestamp, - workspaces.window_state, - workspaces.window_x, - workspaces.window_y, - workspaces.window_width, - workspaces.window_height, - workspaces.display, - workspaces.left_dock_visible, - workspaces.left_dock_active_panel, - workspaces.right_dock_visible, - workspaces.right_dock_active_panel, - workspaces.bottom_dock_visible, - workspaces.bottom_dock_active_panel, - workspaces.left_dock_zoom, - workspaces.right_dock_zoom, - workspaces.bottom_dock_zoom, - workspaces.fullscreen, - workspaces.centered_layout, - workspaces.session_id, - workspaces.window_id - FROM - workspaces LEFT JOIN - ssh_projects ON - workspaces.ssh_project_id = ssh_projects.id; - - DROP TABLE ssh_projects; - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - - CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths); - ), + replace(paths, ',', CHAR(10)) + END + WHERE paths IS NOT NULL + ), ]; + + // Allow recovering from bad migration that was initially shipped to nightly + // when introducing the ssh_connections table. + fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool { + old.starts_with("CREATE TABLE ssh_connections") + && new.starts_with("CREATE TABLE ssh_connections") + } } +db::static_connection!(DB, WorkspaceDb, []); + impl WorkspaceDb { /// Returns a serialized workspace for the given worktree_roots. If the passed array /// is empty, the most recent workspace is returned instead. If no workspace for the @@ -1803,6 +1834,7 @@ mod tests { ON DELETE CASCADE ) STRICT; )], + |_, _, _| false, ) .unwrap(); }) @@ -1851,6 +1883,7 @@ mod tests { REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT;)], + |_, _, _| false, ) }) .await diff --git a/crates/zed/src/zed/component_preview/persistence.rs b/crates/zed/src/zed/component_preview/persistence.rs index 780f7f76264e9e132d0a932e213bb1743156106d..c37a4cc3899fd2da4834070f0a987650079ad515 100644 --- a/crates/zed/src/zed/component_preview/persistence.rs +++ b/crates/zed/src/zed/component_preview/persistence.rs @@ -1,10 +1,17 @@ use anyhow::Result; -use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; +use db::{ + query, + sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, + sqlez_macros::sql, +}; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; -define_connection! { - pub static ref COMPONENT_PREVIEW_DB: ComponentPreviewDb = - &[sql!( +pub struct ComponentPreviewDb(ThreadSafeConnection); + +impl Domain for ComponentPreviewDb { + const NAME: &str = stringify!(ComponentPreviewDb); + + const MIGRATIONS: &[&str] = &[sql!( CREATE TABLE component_previews ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -13,9 +20,11 @@ define_connection! { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } +db::static_connection!(COMPONENT_PREVIEW_DB, ComponentPreviewDb, [WorkspaceDb]); + impl ComponentPreviewDb { pub async fn save_active_page( &self, From 633ce23ae974211de452683e6d5b2e1a0bf21431 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Aug 2025 18:55:24 -0600 Subject: [PATCH 340/744] acp: Send user-configured MCP tools (#36910) Release Notes: - N/A --- crates/agent2/src/tests/mod.rs | 1 + crates/agent_servers/src/acp.rs | 30 ++++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 093b8ba971ae5509478122f93180c124bd16eab5..78e5c88280c0a8a3768892c5fa3b629b875433ad 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -72,6 +72,7 @@ async fn test_echo(cx: &mut TestAppContext) { } #[gpui::test] +#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_thinking(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 9080fc1ab07d91e13708ccf0348d5df01d49e3c0..b4e897374ad079265a85f2f504109abefcbc075f 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -162,12 +162,34 @@ impl AgentConnection for AcpConnection { let conn = self.connection.clone(); let sessions = self.sessions.clone(); let cwd = cwd.to_path_buf(); + let context_server_store = project.read(cx).context_server_store().read(cx); + let mcp_servers = context_server_store + .configured_server_ids() + .iter() + .filter_map(|id| { + let configuration = context_server_store.configuration_for_server(id)?; + let command = configuration.command(); + Some(acp::McpServer { + name: id.0.to_string(), + command: command.path.clone(), + args: command.args.clone(), + env: if let Some(env) = command.env.as_ref() { + env.iter() + .map(|(name, value)| acp::EnvVariable { + name: name.clone(), + value: value.clone(), + }) + .collect() + } else { + vec![] + }, + }) + }) + .collect(); + cx.spawn(async move |cx| { let response = conn - .new_session(acp::NewSessionRequest { - mcp_servers: vec![], - cwd, - }) + .new_session(acp::NewSessionRequest { mcp_servers, cwd }) .await .map_err(|err| { if err.code == acp::ErrorCode::AUTH_REQUIRED.code { From bb5cfe118f588336d54b0499be998cd7744fb8a2 Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Tue, 26 Aug 2025 04:37:29 +0100 Subject: [PATCH 341/744] Add "shift-r" and "g ." support for helix mode (#35468) Related #4642 Compatible with #34136 Release Notes: - Helix: `Shift+R` works as Paste instead of taking you to ReplaceMode - Helix: `g .` goes to last modification place (similar to `. in vim) --- assets/keymaps/vim.json | 2 + crates/vim/src/helix.rs | 87 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 62e50b3c8c21a39f6185e7e1c293c949ce6c6af8..67add61bd35845c2e46c31c74d8ef4baf422aaf3 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -428,11 +428,13 @@ "g h": "vim::StartOfLine", "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s" "g e": "vim::EndOfDocument", + "g .": "vim::HelixGotoLastModification", // go to last modification "g r": "editor::FindAllReferences", // zed specific "g t": "vim::WindowTop", "g c": "vim::WindowMiddle", "g b": "vim::WindowBottom", + "shift-r": "editor::Paste", "x": "editor::SelectLine", "shift-x": "editor::SelectLine", "%": "editor::SelectAll", diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 2bc531268d4f909c11c29d6b001d8a34e887c927..726022021d8d834f31c0c5e6a0fcc24f329d13b9 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -23,6 +23,8 @@ actions!( HelixInsert, /// Appends at the end of the selection. HelixAppend, + /// Goes to the location of the last modification. + HelixGotoLastModification, ] ); @@ -31,6 +33,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::helix_insert); Vim::action(editor, cx, Vim::helix_append); Vim::action(editor, cx, Vim::helix_yank); + Vim::action(editor, cx, Vim::helix_goto_last_modification); } impl Vim { @@ -430,6 +433,15 @@ impl Vim { }); self.switch_mode(Mode::HelixNormal, true, window, cx); } + + pub fn helix_goto_last_modification( + &mut self, + _: &HelixGotoLastModification, + window: &mut Window, + cx: &mut Context, + ) { + self.jump(".".into(), false, false, window, cx); + } } #[cfg(test)] @@ -441,6 +453,7 @@ mod test { #[gpui::test] async fn test_word_motions(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); // « // ˇ // » @@ -502,6 +515,7 @@ mod test { #[gpui::test] async fn test_delete(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); // test delete a selection cx.set_state( @@ -582,6 +596,7 @@ mod test { #[gpui::test] async fn test_f_and_t(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); cx.set_state( indoc! {" @@ -635,6 +650,7 @@ mod test { #[gpui::test] async fn test_newline_char(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal); @@ -652,6 +668,7 @@ mod test { #[gpui::test] async fn test_insert_selected(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); cx.set_state( indoc! {" «The ˇ»quick brown @@ -674,6 +691,7 @@ mod test { #[gpui::test] async fn test_append(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); // test from the end of the selection cx.set_state( indoc! {" @@ -716,6 +734,7 @@ mod test { #[gpui::test] async fn test_replace(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); // No selection (single character) cx.set_state("ˇaa", Mode::HelixNormal); @@ -763,4 +782,72 @@ mod test { cx.shared_clipboard().assert_eq("worl"); cx.assert_state("hello «worlˇ»d", Mode::HelixNormal); } + #[gpui::test] + async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + + // First copy some text to clipboard + cx.set_state("«hello worldˇ»", Mode::HelixNormal); + cx.simulate_keystrokes("y"); + + // Test paste with shift-r on single cursor + cx.set_state("foo ˇbar", Mode::HelixNormal); + cx.simulate_keystrokes("shift-r"); + + cx.assert_state("foo hello worldˇbar", Mode::HelixNormal); + + // Test paste with shift-r on selection + cx.set_state("foo «barˇ» baz", Mode::HelixNormal); + cx.simulate_keystrokes("shift-r"); + + cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal); + } + + #[gpui::test] + async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + + // Make a modification at a specific location + cx.set_state("ˇhello", Mode::HelixNormal); + assert_eq!(cx.mode(), Mode::HelixNormal); + cx.simulate_keystrokes("i"); + assert_eq!(cx.mode(), Mode::Insert); + cx.simulate_keystrokes("escape"); + assert_eq!(cx.mode(), Mode::HelixNormal); + } + + #[gpui::test] + async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + + // Make a modification at a specific location + cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); + cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); + cx.simulate_keystrokes("i"); + cx.simulate_keystrokes("escape"); + cx.simulate_keystrokes("i"); + cx.simulate_keystrokes("m o d i f i e d space"); + cx.simulate_keystrokes("escape"); + + // TODO: this fails, because state is no longer helix + cx.assert_state( + "line one\nline modified ˇtwo\nline three", + Mode::HelixNormal, + ); + + // Move cursor away from the modification + cx.simulate_keystrokes("up"); + + // Use "g ." to go back to last modification + cx.simulate_keystrokes("g ."); + + // Verify we're back at the modification location and still in HelixNormal mode + cx.assert_state( + "line one\nline modifiedˇ two\nline three", + Mode::HelixNormal, + ); + } } From bf5ed6d1c9795369310b5b9d6c752d9dc54991b5 Mon Sep 17 00:00:00 2001 From: Rui Ning <107875822+iryanin@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:40:53 +0800 Subject: [PATCH 342/744] Remote: Change "sh -c" to "sh -lc" to make config in $HOME/.profile effective (#36760) Closes #ISSUE Release Notes: - The environment of original remote dev cannot be changed without sudo because of the behavior of "sh -c". This PR changes "sh -c" to "sh -lc" to let the shell source $HOME/.profile and support customized environment like customized $PATH variable. --- crates/remote/src/ssh_session.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index b9af5286439e9f757d53062b8c003eb85e69fbee..67940184705f9fc91aba29997a16e1df592099a1 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -445,7 +445,7 @@ impl SshSocket { } async fn platform(&self) -> Result { - let uname = self.run_command("sh", &["-c", "uname -sm"]).await?; + let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?; let Some((os, arch)) = uname.split_once(" ") else { anyhow::bail!("unknown uname: {uname:?}") }; @@ -476,7 +476,7 @@ impl SshSocket { } async fn shell(&self) -> String { - match self.run_command("sh", &["-c", "echo $SHELL"]).await { + match self.run_command("sh", &["-lc", "echo $SHELL"]).await { Ok(shell) => shell.trim().to_owned(), Err(e) => { log::error!("Failed to get shell: {e}"); @@ -1533,7 +1533,7 @@ impl RemoteConnection for SshRemoteConnection { let ssh_proxy_process = match self .socket - .ssh_command("sh", &["-c", &start_proxy_command]) + .ssh_command("sh", &["-lc", &start_proxy_command]) // IMPORTANT: we kill this process when we drop the task that uses it. .kill_on_drop(true) .spawn() @@ -1910,7 +1910,7 @@ impl SshRemoteConnection { .run_command( "sh", &[ - "-c", + "-lc", &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) @@ -1988,7 +1988,7 @@ impl SshRemoteConnection { .run_command( "sh", &[ - "-c", + "-lc", &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) @@ -2036,7 +2036,7 @@ impl SshRemoteConnection { dst_path = &dst_path.to_string() ) }; - self.socket.run_command("sh", &["-c", &script]).await?; + self.socket.run_command("sh", &["-lc", &script]).await?; Ok(()) } From 64b14ef84859de5d03c5959faedc1216415a2b52 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 25 Aug 2025 22:21:05 -0700 Subject: [PATCH 343/744] Fix Sqlite newline syntax in workspace migration (#36916) Fixes one more case where I incorrectly tried to use a `\n` escape sequence for a newline in sqlite. Release Notes: - N/A --- crates/workspace/src/persistence.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 89e1147d8affaadc241af3d619fec5e1759ec654..12e719cfd911c05b803810ee087e933c5b575ec6 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -553,7 +553,7 @@ impl Domain for WorkspaceDb { WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN NULL ELSE - replace(workspaces.local_paths_array, ',', "\n") + replace(workspaces.local_paths_array, ',', CHAR(10)) END END as paths, From 428fc6d483b785227dfd56e4e493ee7ccc3c384d Mon Sep 17 00:00:00 2001 From: Dan Dascalescu Date: Tue, 26 Aug 2025 12:05:40 +0300 Subject: [PATCH 344/744] chore: Fix typo in `10_bug_report.yml` (#36922) Release Notes: - N/A --- .github/ISSUE_TEMPLATE/10_bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/10_bug_report.yml b/.github/ISSUE_TEMPLATE/10_bug_report.yml index e132eca1e52bc617f35fc2ec6e4e34fe3c796b11..1bf6c80e4073dafa90e736f995053c570f0ba2da 100644 --- a/.github/ISSUE_TEMPLATE/10_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/10_bug_report.yml @@ -14,7 +14,7 @@ body: ### Description From c14d84cfdb61a4d6fbaeabe14c3b5ca0909163af Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 26 Aug 2025 11:20:33 +0200 Subject: [PATCH 345/744] acp: Add button to configure custom agent in the configuration view (#36923) Release Notes: - N/A --- crates/agent_ui/src/agent_configuration.rs | 107 ++++++++++++++++++++- 1 file changed, 102 insertions(+), 5 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 52fb7eed4b96e2ff92097f415276ffeceb4fa11d..aa9b2ca94f570f639f72af9311d1e02bf8af431e 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -5,18 +5,21 @@ mod tool_picker; use std::{sync::Arc, time::Duration}; -use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini}; +use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini}; use agent_settings::AgentSettings; +use anyhow::Result; use assistant_tool::{ToolSource, ToolWorkingSet}; use cloud_llm_client::Plan; use collections::HashMap; use context_server::ContextServerId; +use editor::{Editor, SelectionEffects, scroll::Autoscroll}; use extension::ExtensionManifest; use extension_host::ExtensionStore; use fs::Fs; use gpui::{ - Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle, - Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, + Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity, + EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, + WeakEntity, percentage, }; use language::LanguageRegistry; use language_model::{ @@ -34,7 +37,7 @@ use ui::{ Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, }; use util::ResultExt as _; -use workspace::Workspace; +use workspace::{Workspace, create_and_open_local_file}; use zed_actions::ExtensionCategoryFilter; pub(crate) use configure_context_server_modal::ConfigureContextServerModal; @@ -1058,7 +1061,36 @@ impl AgentConfiguration { .child( v_flex() .gap_0p5() - .child(Headline::new("External Agents")) + .child( + h_flex() + .w_full() + .gap_2() + .justify_between() + .child(Headline::new("External Agents")) + .child( + Button::new("add-agent", "Add Agent") + .icon_position(IconPosition::Start) + .icon(IconName::Plus) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .label_size(LabelSize::Small) + .on_click( + move |_, window, cx| { + if let Some(workspace) = window.root().flatten() { + let workspace = workspace.downgrade(); + window + .spawn(cx, async |cx| { + open_new_agent_servers_entry_in_settings_editor( + workspace, + cx, + ).await + }) + .detach_and_log_err(cx); + } + } + ), + ) + ) .child( Label::new( "Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.", @@ -1324,3 +1356,68 @@ fn show_unable_to_uninstall_extension_with_context_server( workspace.toggle_status_toast(status_toast, cx); } + +async fn open_new_agent_servers_entry_in_settings_editor( + workspace: WeakEntity, + cx: &mut AsyncWindowContext, +) -> Result<()> { + let settings_editor = workspace + .update_in(cx, |_, window, cx| { + create_and_open_local_file(paths::settings_file(), window, cx, || { + settings::initial_user_settings_content().as_ref().into() + }) + })? + .await? + .downcast::() + .unwrap(); + + settings_editor + .downgrade() + .update_in(cx, |item, window, cx| { + let text = item.buffer().read(cx).snapshot(cx).text(); + + let settings = cx.global::(); + + let edits = settings.edits_for_update::(&text, |file| { + let unique_server_name = (0..u8::MAX) + .map(|i| { + if i == 0 { + "your_agent".into() + } else { + format!("your_agent_{}", i).into() + } + }) + .find(|name| !file.custom.contains_key(name)); + if let Some(server_name) = unique_server_name { + file.custom.insert( + server_name, + AgentServerSettings { + command: AgentServerCommand { + path: "path_to_executable".into(), + args: vec![], + env: Some(HashMap::default()), + }, + }, + ); + } + }); + + if !edits.is_empty() { + let ranges = edits + .iter() + .map(|(range, _)| range.clone()) + .collect::>(); + + item.edit(edits, cx); + + item.change_selections( + SelectionEffects::scroll(Autoscroll::newest()), + window, + cx, + |selections| { + selections.select_ranges(ranges); + }, + ); + } + }) +} From b249593abee31e420fe447f6b551b5e2130b1bc8 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Tue, 26 Aug 2025 02:46:29 -0700 Subject: [PATCH 346/744] agent2: Always finalize diffs from the edit tool (#36918) Previously, we wouldn't finalize the diff if an error occurred during editing or the tool call was canceled. Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/agent2/src/thread.rs | 24 +++++ crates/agent2/src/tools/edit_file_tool.rs | 103 ++++++++++++++++++++- crates/language_model/src/fake_provider.rs | 31 ++++++- 3 files changed, 152 insertions(+), 6 deletions(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 1b1c014b7930091271b541e9d7cd12281274ec14..4acd72f2750afe7979972ef1b27088f120561dcf 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -2459,6 +2459,30 @@ impl ToolCallEventStreamReceiver { } } + pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields { + let event = self.0.next().await; + if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( + update, + )))) = event + { + update.fields + } else { + panic!("Expected update fields but got: {:?}", event); + } + } + + pub async fn expect_diff(&mut self) -> Entity { + let event = self.0.next().await; + if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff( + update, + )))) = event + { + update.diff + } else { + panic!("Expected diff but got: {:?}", event); + } + } + pub async fn expect_terminal(&mut self) -> Entity { let event = self.0.next().await; if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal( diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 5a68d0c70a04aea7367b8264c34d139d3602cc44..f86bfd25f74556118c827050837ec5beda37d471 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -273,6 +273,13 @@ impl AgentTool for EditFileTool { let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?; event_stream.update_diff(diff.clone()); + let _finalize_diff = util::defer({ + let diff = diff.downgrade(); + let mut cx = cx.clone(); + move || { + diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok(); + } + }); let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; let old_text = cx @@ -389,8 +396,6 @@ impl AgentTool for EditFileTool { }) .await; - diff.update(cx, |diff, cx| diff.finalize(cx)).ok(); - let input_path = input.path.display(); if unified_diff.is_empty() { anyhow::ensure!( @@ -1545,6 +1550,100 @@ mod tests { ); } + #[gpui::test] + async fn test_diff_finalization(cx: &mut TestAppContext) { + init_test(cx); + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree("/", json!({"main.rs": ""})).await; + + let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await; + let languages = project.read_with(cx, |project, _cx| project.languages().clone()); + let context_server_registry = + cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); + let model = Arc::new(FakeLanguageModel::default()); + let thread = cx.new(|cx| { + Thread::new( + project.clone(), + cx.new(|_cx| ProjectContext::default()), + context_server_registry.clone(), + Templates::new(), + Some(model.clone()), + cx, + ) + }); + + // Ensure the diff is finalized after the edit completes. + { + let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let edit = cx.update(|cx| { + tool.run( + EditFileToolInput { + display_description: "Edit file".into(), + path: path!("/main.rs").into(), + mode: EditFileMode::Edit, + }, + stream_tx, + cx, + ) + }); + stream_rx.expect_update_fields().await; + let diff = stream_rx.expect_diff().await; + diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); + cx.run_until_parked(); + model.end_last_completion_stream(); + edit.await.unwrap(); + diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); + } + + // Ensure the diff is finalized if an error occurs while editing. + { + model.forbid_requests(); + let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let edit = cx.update(|cx| { + tool.run( + EditFileToolInput { + display_description: "Edit file".into(), + path: path!("/main.rs").into(), + mode: EditFileMode::Edit, + }, + stream_tx, + cx, + ) + }); + stream_rx.expect_update_fields().await; + let diff = stream_rx.expect_diff().await; + diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); + edit.await.unwrap_err(); + diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); + model.allow_requests(); + } + + // Ensure the diff is finalized if the tool call gets dropped. + { + let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let edit = cx.update(|cx| { + tool.run( + EditFileToolInput { + display_description: "Edit file".into(), + path: path!("/main.rs").into(), + mode: EditFileMode::Edit, + }, + stream_tx, + cx, + ) + }); + stream_rx.expect_update_fields().await; + let diff = stream_rx.expect_diff().await; + diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); + drop(edit); + cx.run_until_parked(); + diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); + } + } + fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); diff --git a/crates/language_model/src/fake_provider.rs b/crates/language_model/src/fake_provider.rs index ebfd37d16cf622a622047b7f5babedebd541ad57..b06a475f9385012e5b88466c80fbb14e0ed744ac 100644 --- a/crates/language_model/src/fake_provider.rs +++ b/crates/language_model/src/fake_provider.rs @@ -4,12 +4,16 @@ use crate::{ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, }; +use anyhow::anyhow; use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream}; use gpui::{AnyView, App, AsyncApp, Entity, Task, Window}; use http_client::Result; use parking_lot::Mutex; use smol::stream::StreamExt; -use std::sync::Arc; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering::SeqCst}, +}; #[derive(Clone)] pub struct FakeLanguageModelProvider { @@ -106,6 +110,7 @@ pub struct FakeLanguageModel { >, )>, >, + forbid_requests: AtomicBool, } impl Default for FakeLanguageModel { @@ -114,11 +119,20 @@ impl Default for FakeLanguageModel { provider_id: LanguageModelProviderId::from("fake".to_string()), provider_name: LanguageModelProviderName::from("Fake".to_string()), current_completion_txs: Mutex::new(Vec::new()), + forbid_requests: AtomicBool::new(false), } } } impl FakeLanguageModel { + pub fn allow_requests(&self) { + self.forbid_requests.store(false, SeqCst); + } + + pub fn forbid_requests(&self) { + self.forbid_requests.store(true, SeqCst); + } + pub fn pending_completions(&self) -> Vec { self.current_completion_txs .lock() @@ -251,9 +265,18 @@ impl LanguageModel for FakeLanguageModel { LanguageModelCompletionError, >, > { - let (tx, rx) = mpsc::unbounded(); - self.current_completion_txs.lock().push((request, tx)); - async move { Ok(rx.boxed()) }.boxed() + if self.forbid_requests.load(SeqCst) { + async move { + Err(LanguageModelCompletionError::Other(anyhow!( + "requests are forbidden" + ))) + } + .boxed() + } else { + let (tx, rx) = mpsc::unbounded(); + self.current_completion_txs.lock().push((request, tx)); + async move { Ok(rx.boxed()) }.boxed() + } } fn as_fake(&self) -> &Self { From e96b68bc1599b92b6404f77326d79e198f4a8efb Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 26 Aug 2025 12:55:45 +0200 Subject: [PATCH 347/744] acp: Polish UI (#36927) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/agent2/src/tests/mod.rs | 2 +- crates/agent2/src/thread.rs | 14 ++++++++++++-- crates/agent_ui/src/acp/thread_view.rs | 7 ++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 78e5c88280c0a8a3768892c5fa3b629b875433ad..a55eaacee36708b6cbe75136b8edeaa2cd45552f 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -472,7 +472,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { tool_name: ToolRequiringPermission::name().into(), is_error: true, content: "Permission to run tool denied by user".into(), - output: None + output: Some("Permission to run tool denied by user".into()) }) ] ); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 4acd72f2750afe7979972ef1b27088f120561dcf..97ea1caf1d766be0314a16cc0f518ad701564569 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -732,7 +732,17 @@ impl Thread { stream.update_tool_call_fields( &tool_use.id, acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::Completed), + status: Some( + tool_result + .as_ref() + .map_or(acp::ToolCallStatus::Failed, |result| { + if result.is_error { + acp::ToolCallStatus::Failed + } else { + acp::ToolCallStatus::Completed + } + }), + ), raw_output: output, ..Default::default() }, @@ -1557,7 +1567,7 @@ impl Thread { tool_name: tool_use.name, is_error: true, content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())), - output: None, + output: Some(error.to_string().into()), }, } })) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 837ce6f90ad7f2b445d91abeadc6658c3d348a0a..6d8f8fb82efb1d6acf9728b04879e197b1f06bd4 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1684,7 +1684,7 @@ impl AcpThreadView { div() .relative() .mt_1p5() - .ml(px(7.)) + .ml(rems(0.4)) .pl_4() .border_l_1() .border_color(self.tool_card_border_color(cx)) @@ -1850,6 +1850,7 @@ impl AcpThreadView { .w_full() .h(window.line_height() - px(2.)) .text_size(self.tool_name_font_size()) + .gap_0p5() .child(tool_icon) .child(if tool_call.locations.len() == 1 { let name = tool_call.locations[0] @@ -1968,7 +1969,7 @@ impl AcpThreadView { v_flex() .mt_1p5() - .ml(px(7.)) + .ml(rems(0.4)) .px_3p5() .gap_2() .border_l_1() @@ -2025,7 +2026,7 @@ impl AcpThreadView { let button_id = SharedString::from(format!("item-{}", uri)); div() - .ml(px(7.)) + .ml(rems(0.4)) .pl_2p5() .border_l_1() .border_color(self.tool_card_border_color(cx)) From 10a1140d49fc0af2c1adf301433a3c9a34374417 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 26 Aug 2025 13:18:50 +0200 Subject: [PATCH 348/744] acp: Improve matching logic when adding new entry to agent_servers (#36926) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/agent_ui/src/agent_configuration.rs | 77 +++++++++++++++++----- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index aa9b2ca94f570f639f72af9311d1e02bf8af431e..c279115880e8fd1f6083fd9ada80c9946164fd31 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -3,7 +3,7 @@ mod configure_context_server_modal; mod manage_profiles_modal; mod tool_picker; -use std::{sync::Arc, time::Duration}; +use std::{ops::Range, sync::Arc, time::Duration}; use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini}; use agent_settings::AgentSettings; @@ -1378,8 +1378,9 @@ async fn open_new_agent_servers_entry_in_settings_editor( let settings = cx.global::(); + let mut unique_server_name = None; let edits = settings.edits_for_update::(&text, |file| { - let unique_server_name = (0..u8::MAX) + let server_name: Option = (0..u8::MAX) .map(|i| { if i == 0 { "your_agent".into() @@ -1388,7 +1389,8 @@ async fn open_new_agent_servers_entry_in_settings_editor( } }) .find(|name| !file.custom.contains_key(name)); - if let Some(server_name) = unique_server_name { + if let Some(server_name) = server_name { + unique_server_name = Some(server_name.clone()); file.custom.insert( server_name, AgentServerSettings { @@ -1402,22 +1404,61 @@ async fn open_new_agent_servers_entry_in_settings_editor( } }); - if !edits.is_empty() { - let ranges = edits - .iter() - .map(|(range, _)| range.clone()) - .collect::>(); - - item.edit(edits, cx); + if edits.is_empty() { + return; + } - item.change_selections( - SelectionEffects::scroll(Autoscroll::newest()), - window, - cx, - |selections| { - selections.select_ranges(ranges); - }, - ); + let ranges = edits + .iter() + .map(|(range, _)| range.clone()) + .collect::>(); + + item.edit(edits, cx); + if let Some((unique_server_name, buffer)) = + unique_server_name.zip(item.buffer().read(cx).as_singleton()) + { + let snapshot = buffer.read(cx).snapshot(); + if let Some(range) = + find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot) + { + item.change_selections( + SelectionEffects::scroll(Autoscroll::newest()), + window, + cx, + |selections| { + selections.select_ranges(vec![range]); + }, + ); + } } }) } + +fn find_text_in_buffer( + text: &str, + start: usize, + snapshot: &language::BufferSnapshot, +) -> Option> { + let chars = text.chars().collect::>(); + + let mut offset = start; + let mut char_offset = 0; + for c in snapshot.chars_at(start) { + if char_offset >= chars.len() { + break; + } + offset += 1; + + if c == chars[char_offset] { + char_offset += 1; + } else { + char_offset = 0; + } + } + + if char_offset == chars.len() { + Some(offset.saturating_sub(chars.len())..offset) + } else { + None + } +} From 372b3c7af632caffbc4e73d5b84bc804d375904a Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 26 Aug 2025 15:30:26 +0200 Subject: [PATCH 349/744] acp: Enable feature flag for everyone (#36928) Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 11 ----------- crates/feature_flags/src/feature_flags.rs | 6 +++++- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 269aec33659a5c39c87137b79f4c32d9da930d4a..267c76d73fac4c0d5a624799b6e8938688335fd4 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -591,17 +591,6 @@ impl AgentPanel { None }; - // Wait for the Gemini/Native feature flag to be available. - let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?; - if !client.status().borrow().is_signed_out() { - cx.update(|_, cx| { - cx.wait_for_flag_or_timeout::( - Duration::from_secs(2), - ) - })? - .await; - } - let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| { Self::new( diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 422979c4297cc72fdf2bf1d14cfba433d155c80e..f5f7fc42b35eba2ccd437c1e4cc4add0b4091773 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -98,6 +98,10 @@ impl FeatureFlag for GeminiAndNativeFeatureFlag { // integration too, and we'd like to turn Gemini/Native on in new builds // without enabling Claude Code in old builds. const NAME: &'static str = "gemini-and-native"; + + fn enabled_for_all() -> bool { + true + } } pub struct ClaudeCodeFeatureFlag; @@ -201,7 +205,7 @@ impl FeatureFlagAppExt for App { fn has_flag(&self) -> bool { self.try_global::() .map(|flags| flags.has_flag::()) - .unwrap_or(false) + .unwrap_or(T::enabled_for_all()) } fn is_staff(&self) -> bool { From aa0f7a2d09c06331dbb176a8b0e45235f1ad8516 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 26 Aug 2025 09:33:42 -0400 Subject: [PATCH 350/744] Fix conflicts in Linux default keymap (#36519) Closes https://github.com/zed-industries/zed/issues/29746 | Action | New Key | Old Key | Former Conflict | | - | - | - | - | | `edit_prediction::ToggleMenu` | `ctrl-alt-shift-i` | `ctrl-shift-i` | `editor::Format` | | `editor::ToggleEditPrediction` | `ctrl-alt-shift-e` | `ctrl-shift-e` | `project_panel::ToggleFocus` | These aren't great keys and I'm open to alternate suggestions, but the will work out of the box without conflict. Release Notes: - N/A --- assets/keymaps/default-linux.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index e84f4834af58ffcac596a73264260d7d1f922d89..3cca560c0088a5be19ea42aeb4753db4d158bf4d 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -40,7 +40,7 @@ "shift-f11": "debugger::StepOut", "f11": "zed::ToggleFullScreen", "ctrl-alt-z": "edit_prediction::RateCompletions", - "ctrl-shift-i": "edit_prediction::ToggleMenu", + "ctrl-alt-shift-i": "edit_prediction::ToggleMenu", "ctrl-alt-l": "lsp_tool::ToggleMenu" } }, @@ -120,7 +120,7 @@ "alt-g m": "git::OpenModifiedFiles", "menu": "editor::OpenContextMenu", "shift-f10": "editor::OpenContextMenu", - "ctrl-shift-e": "editor::ToggleEditPrediction", + "ctrl-alt-shift-e": "editor::ToggleEditPrediction", "f9": "editor::ToggleBreakpoint", "shift-f9": "editor::EditLogBreakpoint" } From 76dbcde62836445d146c3918edce26dbaec25314 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 26 Aug 2025 09:35:45 -0400 Subject: [PATCH 351/744] Support disabling drag-and-drop in Project Panel (#36719) Release Notes: - Added setting for disabling drag and drop in project panel. `{ "project_panel": {"drag_and_drop": false } }` --- assets/settings/default.json | 2 + crates/project_panel/src/project_panel.rs | 74 ++++++++++--------- .../src/project_panel_settings.rs | 5 ++ docs/src/configuring-zed.md | 1 + docs/src/visual-customization.md | 1 + 5 files changed, 50 insertions(+), 33 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index f0b9e11e57f074ac3c50b4e830343f7a5290f965..804198090fb4f5649160f6534bd3ed54b1368bce 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -653,6 +653,8 @@ // "never" "show": "always" }, + // Whether to enable drag-and-drop operations in the project panel. + "drag_and_drop": true, // Whether to hide the root entry when only one folder is open in the window. "hide_root": false }, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index c99f5f8172b0fe51c49d23e7cbc5c6f9e714d7f0..5a30a3e9bcabc3c2c16f942cf864daa6b28f6b98 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4089,6 +4089,7 @@ impl ProjectPanel { .when(!is_sticky, |this| { this .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over)) + .when(settings.drag_and_drop, |this| this .on_drag_move::(cx.listener( move |this, event: &DragMoveEvent, _, cx| { let is_current_target = this.drag_target_entry.as_ref() @@ -4222,7 +4223,7 @@ impl ProjectPanel { } this.drag_onto(selections, entry_id, kind.is_file(), window, cx); }), - ) + )) }) .on_mouse_down( MouseButton::Left, @@ -4433,6 +4434,7 @@ impl ProjectPanel { div() .when(!is_sticky, |div| { div + .when(settings.drag_and_drop, |div| div .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| { this.hover_scroll_task.take(); this.drag_target_entry = None; @@ -4464,7 +4466,7 @@ impl ProjectPanel { } }, - )) + ))) }) .child( Label::new(DELIMITER.clone()) @@ -4484,6 +4486,7 @@ impl ProjectPanel { .when(index != components_len - 1, |div|{ let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned(); div + .when(settings.drag_and_drop, |div| div .on_drag_move(cx.listener( move |this, event: &DragMoveEvent, _, _| { if event.bounds.contains(&event.event.position) { @@ -4521,7 +4524,7 @@ impl ProjectPanel { target.index == index ), |this| { this.bg(item_colors.drag_over) - }) + })) }) }) .on_click(cx.listener(move |this, _, _, cx| { @@ -5029,7 +5032,8 @@ impl ProjectPanel { sticky_parents.reverse(); - let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status; + let panel_settings = ProjectPanelSettings::get_global(cx); + let git_status_enabled = panel_settings.git_status; let root_name = OsStr::new(worktree.root_name()); let git_summaries_by_id = if git_status_enabled { @@ -5113,11 +5117,11 @@ impl Render for ProjectPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let has_worktree = !self.visible_entries.is_empty(); let project = self.project.read(cx); - let indent_size = ProjectPanelSettings::get_global(cx).indent_size; - let show_indent_guides = - ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always; + let panel_settings = ProjectPanelSettings::get_global(cx); + let indent_size = panel_settings.indent_size; + let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always; let show_sticky_entries = { - if ProjectPanelSettings::get_global(cx).sticky_scroll { + if panel_settings.sticky_scroll { let is_scrollable = self.scroll_handle.is_scrollable(); let is_scrolled = self.scroll_handle.offset().y < px(0.); is_scrollable && is_scrolled @@ -5205,8 +5209,10 @@ impl Render for ProjectPanel { h_flex() .id("project-panel") .group("project-panel") - .on_drag_move(cx.listener(handle_drag_move::)) - .on_drag_move(cx.listener(handle_drag_move::)) + .when(panel_settings.drag_and_drop, |this| { + this.on_drag_move(cx.listener(handle_drag_move::)) + .on_drag_move(cx.listener(handle_drag_move::)) + }) .size_full() .relative() .on_modifiers_changed(cx.listener( @@ -5544,30 +5550,32 @@ impl Render for ProjectPanel { })), ) .when(is_local, |div| { - div.drag_over::(|style, _, _, cx| { - style.bg(cx.theme().colors().drop_target_background) + div.when(panel_settings.drag_and_drop, |div| { + div.drag_over::(|style, _, _, cx| { + style.bg(cx.theme().colors().drop_target_background) + }) + .on_drop(cx.listener( + move |this, external_paths: &ExternalPaths, window, cx| { + this.drag_target_entry = None; + this.hover_scroll_task.take(); + if let Some(task) = this + .workspace + .update(cx, |workspace, cx| { + workspace.open_workspace_for_paths( + true, + external_paths.paths().to_owned(), + window, + cx, + ) + }) + .log_err() + { + task.detach_and_log_err(cx); + } + cx.stop_propagation(); + }, + )) }) - .on_drop(cx.listener( - move |this, external_paths: &ExternalPaths, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - if let Some(task) = this - .workspace - .update(cx, |workspace, cx| { - workspace.open_workspace_for_paths( - true, - external_paths.paths().to_owned(), - window, - cx, - ) - }) - .log_err() - { - task.detach_and_log_err(cx); - } - cx.stop_propagation(); - }, - )) }) } } diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index 8a243589ed76f6a3518fc7c0722f5fb5e8604c73..fc399d66a7b78e75a9e43a3e7bf0404624123685 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -47,6 +47,7 @@ pub struct ProjectPanelSettings { pub scrollbar: ScrollbarSettings, pub show_diagnostics: ShowDiagnostics, pub hide_root: bool, + pub drag_and_drop: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -160,6 +161,10 @@ pub struct ProjectPanelSettingsContent { /// /// Default: true pub sticky_scroll: Option, + /// Whether to enable drag-and-drop operations in the project panel. + /// + /// Default: true + pub drag_and_drop: Option, } impl Settings for ProjectPanelSettings { diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index fb139db6e404a22a25c63967ea2c46f94a9ca648..a8a46896893d4b19827f90720e2076250e5a4be3 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -3243,6 +3243,7 @@ Run the `theme selector: toggle` action in the command palette to see a current "indent_size": 20, "auto_reveal_entries": true, "auto_fold_dirs": true, + "drag_and_drop": true, "scrollbar": { "show": null }, diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 24b2a9d769764215c0868d455ffe6bfd615be158..4fc5a9ba8864bc3a721d4d7d101977d729082e59 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -431,6 +431,7 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k "auto_reveal_entries": true, // Show file in panel when activating its buffer "auto_fold_dirs": true, // Fold dirs with single subdir "sticky_scroll": true, // Stick parent directories at top of the project panel. + "drag_and_drop": true, // Whether drag and drop is enabled "scrollbar": { // Project panel scrollbar settings "show": null // Show/hide: (auto, system, always, never) }, From b7dad2cf7199e4e31ce149d707dc87683981bc5d Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 26 Aug 2025 09:41:57 -0400 Subject: [PATCH 352/744] Fix initial_tasks.json triggering diagnostic warning (#36523) `zed::OpenProjectTasks` without an existing tasks.json will recreate it from the template. This file will immediately show a warning. Screenshot 2025-08-19 at 17 16 07 Release Notes: - N/A --- assets/settings/initial_tasks.json | 4 ++-- docs/src/tasks.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json index a79c550671f85d7b107db5e85883caa28fe41411..5cead67b6d5bb89e878e3bfb8d250dcbbd2ce447 100644 --- a/assets/settings/initial_tasks.json +++ b/assets/settings/initial_tasks.json @@ -43,8 +43,8 @@ // "args": ["--login"] // } // } - "shell": "system", + "shell": "system" // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. - "tags": [] + // "tags": [] } ] diff --git a/docs/src/tasks.md b/docs/src/tasks.md index 95505634327c297b795846cceea63d70aa068008..bff3eac86048752be50f8fd605bc5b76677ca0c0 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -45,9 +45,9 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to // Whether to show the task line in the output of the spawned task, defaults to `true`. "show_summary": true, // Whether to show the command line in the output of the spawned task, defaults to `true`. - "show_output": true, + "show_output": true // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. - "tags": [] + // "tags": [] } ] ``` From 2c64b05ea44c4b8be85d298c01c3c8984b433398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=ADn?= Date: Tue, 26 Aug 2025 15:43:58 +0200 Subject: [PATCH 353/744] emacs: Add editor::FindAllReferences keybinding (#36840) This commit maps `editor::FindAllReferences` to Alt+? in the Emacs keymap. Release Notes: - N/A --- assets/keymaps/linux/emacs.json | 1 + assets/keymaps/macos/emacs.json | 1 + 2 files changed, 2 insertions(+) diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index 0ff3796f03d85affdae88d009e88e73516ba385a..62910e297bb18f52917477806ceea1b79dcb5d86 100755 --- a/assets/keymaps/linux/emacs.json +++ b/assets/keymaps/linux/emacs.json @@ -38,6 +38,7 @@ "alt-;": ["editor::ToggleComments", { "advance_downwards": false }], "ctrl-x ctrl-;": "editor::ToggleComments", "alt-.": "editor::GoToDefinition", // xref-find-definitions + "alt-?": "editor::FindAllReferences", // xref-find-references "alt-,": "pane::GoBack", // xref-pop-marker-stack "ctrl-x h": "editor::SelectAll", // mark-whole-buffer "ctrl-d": "editor::Delete", // delete-char diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index 0ff3796f03d85affdae88d009e88e73516ba385a..62910e297bb18f52917477806ceea1b79dcb5d86 100755 --- a/assets/keymaps/macos/emacs.json +++ b/assets/keymaps/macos/emacs.json @@ -38,6 +38,7 @@ "alt-;": ["editor::ToggleComments", { "advance_downwards": false }], "ctrl-x ctrl-;": "editor::ToggleComments", "alt-.": "editor::GoToDefinition", // xref-find-definitions + "alt-?": "editor::FindAllReferences", // xref-find-references "alt-,": "pane::GoBack", // xref-pop-marker-stack "ctrl-x h": "editor::SelectAll", // mark-whole-buffer "ctrl-d": "editor::Delete", // delete-char From 858ab9cc2358a4f7571c3a0a46cdfa9ac5124714 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 26 Aug 2025 15:55:09 +0200 Subject: [PATCH 354/744] Revert "ai: Auto select user model when there's no default" (#36932) Reverts zed-industries/zed#36722 Release Notes: - N/A --- crates/agent/src/thread.rs | 17 +-- crates/agent2/src/agent.rs | 4 +- crates/agent2/src/tests/mod.rs | 4 +- .../agent_ui/src/language_model_selector.rs | 55 ++++++++- crates/git_ui/src/git_panel.rs | 2 +- crates/language_model/src/registry.rs | 114 ++++++++---------- crates/language_models/Cargo.toml | 1 - crates/language_models/src/language_models.rs | 103 +--------------- crates/language_models/src/provider/cloud.rs | 6 +- 9 files changed, 122 insertions(+), 184 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 899e360ab01226cdb6b47b37713f99e45c0ef6b7..7b70fde56ab1e7acb6705aeace82f142dc28a9f3 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -664,7 +664,7 @@ impl Thread { } pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option { - if self.configured_model.is_none() || self.messages.is_empty() { + if self.configured_model.is_none() { self.configured_model = LanguageModelRegistry::read_global(cx).default_model(); } self.configured_model.clone() @@ -2097,7 +2097,7 @@ impl Thread { } pub fn summarize(&mut self, cx: &mut Context) { - let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else { + let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else { println!("No thread summary model"); return; }; @@ -2416,7 +2416,7 @@ impl Thread { } let Some(ConfiguredModel { model, provider }) = - LanguageModelRegistry::read_global(cx).thread_summary_model(cx) + LanguageModelRegistry::read_global(cx).thread_summary_model() else { return; }; @@ -5410,10 +5410,13 @@ fn main() {{ }), cx, ); - registry.set_thread_summary_model(Some(ConfiguredModel { - provider, - model: model.clone(), - })); + registry.set_thread_summary_model( + Some(ConfiguredModel { + provider, + model: model.clone(), + }), + cx, + ); }) }); diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index ecfaea4b4967a019da83edb3416cef66464c229e..6fa36d33d50515c49f6d64c89146072316de7689 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -228,7 +228,7 @@ impl NativeAgent { ) -> Entity { let connection = Rc::new(NativeAgentConnection(cx.entity())); let registry = LanguageModelRegistry::read_global(cx); - let summarization_model = registry.thread_summary_model(cx).map(|c| c.model); + let summarization_model = registry.thread_summary_model().map(|c| c.model); thread_handle.update(cx, |thread, cx| { thread.set_summarization_model(summarization_model, cx); @@ -524,7 +524,7 @@ impl NativeAgent { let registry = LanguageModelRegistry::read_global(cx); let default_model = registry.default_model().map(|m| m.model); - let summarization_model = registry.thread_summary_model(cx).map(|m| m.model); + let summarization_model = registry.thread_summary_model().map(|m| m.model); for session in self.sessions.values_mut() { session.thread.update(cx, |thread, cx| { diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index a55eaacee36708b6cbe75136b8edeaa2cd45552f..fbeee46a484a71742dd4ce52b537bebb5da91924 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1822,11 +1822,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) { let clock = Arc::new(clock::FakeSystemClock::new()); let client = Client::new(clock, http_client, cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - Project::init_settings(cx); - agent_settings::init(cx); language_model::init(client.clone(), cx); language_models::init(user_store, client.clone(), cx); + Project::init_settings(cx); LanguageModelRegistry::test(cx); + agent_settings::init(cx); }); cx.executor().forbid_parking(); diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index aceca79dbf95cd64bbf68b89907d0903f4aba9ff..3633e533da97b2b80e5c8d62c271da7121d3582b 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -6,7 +6,8 @@ use feature_flags::ZedProFeatureFlag; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task}; use language_model::{ - ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry, + AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId, + LanguageModelRegistry, }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; @@ -76,6 +77,7 @@ pub struct LanguageModelPickerDelegate { all_models: Arc, filtered_entries: Vec, selected_index: usize, + _authenticate_all_providers_task: Task<()>, _subscriptions: Vec, } @@ -96,6 +98,7 @@ impl LanguageModelPickerDelegate { selected_index: Self::get_active_model_index(&entries, get_active_model(cx)), filtered_entries: entries, get_active_model: Arc::new(get_active_model), + _authenticate_all_providers_task: Self::authenticate_all_providers(cx), _subscriptions: vec![cx.subscribe_in( &LanguageModelRegistry::global(cx), window, @@ -139,6 +142,56 @@ impl LanguageModelPickerDelegate { .unwrap_or(0) } + /// Authenticates all providers in the [`LanguageModelRegistry`]. + /// + /// We do this so that we can populate the language selector with all of the + /// models from the configured providers. + fn authenticate_all_providers(cx: &mut App) -> Task<()> { + let authenticate_all_providers = LanguageModelRegistry::global(cx) + .read(cx) + .providers() + .iter() + .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) + .collect::>(); + + cx.spawn(async move |_cx| { + for (provider_id, provider_name, authenticate_task) in authenticate_all_providers { + if let Err(err) = authenticate_task.await { + if matches!(err, AuthenticateError::CredentialsNotFound) { + // Since we're authenticating these providers in the + // background for the purposes of populating the + // language selector, we don't care about providers + // where the credentials are not found. + } else { + // Some providers have noisy failure states that we + // don't want to spam the logs with every time the + // language model selector is initialized. + // + // Ideally these should have more clear failure modes + // that we know are safe to ignore here, like what we do + // with `CredentialsNotFound` above. + match provider_id.0.as_ref() { + "lmstudio" | "ollama" => { + // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". + // + // These fail noisily, so we don't log them. + } + "copilot_chat" => { + // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. + } + _ => { + log::error!( + "Failed to authenticate provider: {}: {err}", + provider_name.0 + ); + } + } + } + } + } + }) + } + pub fn active_model(&self, cx: &App) -> Option { (self.get_active_model)(cx) } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 958a609a096173eef379d4788d9e0dc64bbfbe5a..4ecb4a8829659ca9a25152db8d1eff529cfff2b1 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -4466,7 +4466,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option, - /// This model is automatically configured by a user's environment after - /// authenticating all providers. It's only used when default_model is not available. - environment_fallback_model: Option, + default_fast_model: Option, inline_assistant_model: Option, commit_message_model: Option, thread_summary_model: Option, @@ -99,6 +98,9 @@ impl ConfiguredModel { pub enum Event { DefaultModelChanged, + InlineAssistantModelChanged, + CommitMessageModelChanged, + ThreadSummaryModelChanged, ProviderStateChanged(LanguageModelProviderId), AddedProvider(LanguageModelProviderId), RemovedProvider(LanguageModelProviderId), @@ -224,7 +226,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_inline_assistant_model(configured_model); + self.set_inline_assistant_model(configured_model, cx); } pub fn select_commit_message_model( @@ -233,7 +235,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_commit_message_model(configured_model); + self.set_commit_message_model(configured_model, cx); } pub fn select_thread_summary_model( @@ -242,7 +244,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_thread_summary_model(configured_model); + self.set_thread_summary_model(configured_model, cx); } /// Selects and sets the inline alternatives for language models based on @@ -276,60 +278,68 @@ impl LanguageModelRegistry { } pub fn set_default_model(&mut self, model: Option, cx: &mut Context) { - match (self.default_model(), model.as_ref()) { + match (self.default_model.as_ref(), model.as_ref()) { (Some(old), Some(new)) if old.is_same_as(new) => {} (None, None) => {} _ => cx.emit(Event::DefaultModelChanged), } + self.default_fast_model = maybe!({ + let provider = &model.as_ref()?.provider; + let fast_model = provider.default_fast_model(cx)?; + Some(ConfiguredModel { + provider: provider.clone(), + model: fast_model, + }) + }); self.default_model = model; } - pub fn set_environment_fallback_model( + pub fn set_inline_assistant_model( &mut self, model: Option, cx: &mut Context, ) { - if self.default_model.is_none() { - match (self.environment_fallback_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::DefaultModelChanged), - } + match (self.inline_assistant_model.as_ref(), model.as_ref()) { + (Some(old), Some(new)) if old.is_same_as(new) => {} + (None, None) => {} + _ => cx.emit(Event::InlineAssistantModelChanged), } - self.environment_fallback_model = model; - } - - pub fn set_inline_assistant_model(&mut self, model: Option) { self.inline_assistant_model = model; } - pub fn set_commit_message_model(&mut self, model: Option) { + pub fn set_commit_message_model( + &mut self, + model: Option, + cx: &mut Context, + ) { + match (self.commit_message_model.as_ref(), model.as_ref()) { + (Some(old), Some(new)) if old.is_same_as(new) => {} + (None, None) => {} + _ => cx.emit(Event::CommitMessageModelChanged), + } self.commit_message_model = model; } - pub fn set_thread_summary_model(&mut self, model: Option) { + pub fn set_thread_summary_model( + &mut self, + model: Option, + cx: &mut Context, + ) { + match (self.thread_summary_model.as_ref(), model.as_ref()) { + (Some(old), Some(new)) if old.is_same_as(new) => {} + (None, None) => {} + _ => cx.emit(Event::ThreadSummaryModelChanged), + } self.thread_summary_model = model; } - #[track_caller] pub fn default_model(&self) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; } - self.default_model - .clone() - .or_else(|| self.environment_fallback_model.clone()) - } - - pub fn default_fast_model(&self, cx: &App) -> Option { - let provider = self.default_model()?.provider; - let fast_model = provider.default_fast_model(cx)?; - Some(ConfiguredModel { - provider, - model: fast_model, - }) + self.default_model.clone() } pub fn inline_assistant_model(&self) -> Option { @@ -343,7 +353,7 @@ impl LanguageModelRegistry { .or_else(|| self.default_model.clone()) } - pub fn commit_message_model(&self, cx: &App) -> Option { + pub fn commit_message_model(&self) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; @@ -351,11 +361,11 @@ impl LanguageModelRegistry { self.commit_message_model .clone() - .or_else(|| self.default_fast_model(cx)) + .or_else(|| self.default_fast_model.clone()) .or_else(|| self.default_model.clone()) } - pub fn thread_summary_model(&self, cx: &App) -> Option { + pub fn thread_summary_model(&self) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; @@ -363,7 +373,7 @@ impl LanguageModelRegistry { self.thread_summary_model .clone() - .or_else(|| self.default_fast_model(cx)) + .or_else(|| self.default_fast_model.clone()) .or_else(|| self.default_model.clone()) } @@ -400,34 +410,4 @@ mod tests { let providers = registry.read(cx).providers(); assert!(providers.is_empty()); } - - #[gpui::test] - async fn test_configure_environment_fallback_model(cx: &mut gpui::TestAppContext) { - let registry = cx.new(|_| LanguageModelRegistry::default()); - - let provider = FakeLanguageModelProvider::default(); - registry.update(cx, |registry, cx| { - registry.register_provider(provider.clone(), cx); - }); - - cx.update(|cx| provider.authenticate(cx)).await.unwrap(); - - registry.update(cx, |registry, cx| { - let provider = registry.provider(&provider.id()).unwrap(); - - registry.set_environment_fallback_model( - Some(ConfiguredModel { - provider: provider.clone(), - model: provider.default_model(cx).unwrap(), - }), - cx, - ); - - let default_model = registry.default_model().unwrap(); - let fallback_model = registry.environment_fallback_model.clone().unwrap(); - - assert_eq!(default_model.model.id(), fallback_model.model.id()); - assert_eq!(default_model.provider.id(), fallback_model.provider.id()); - }); - } } diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index cd41478668b17e1c680127625bf47a03604eec60..b5bfb870f643452bd5be248c9910d99f16a8101e 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -44,7 +44,6 @@ ollama = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] } open_router = { workspace = true, features = ["schemars"] } partial-json-fixer.workspace = true -project.workspace = true release_channel.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index beed306e740bfdc8170776e187b9ea59d3a59fbb..738b72b0c9a6dbb7c9606cc72707b27e66abf09c 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -3,12 +3,8 @@ use std::sync::Arc; use ::settings::{Settings, SettingsStore}; use client::{Client, UserStore}; use collections::HashSet; -use futures::future; -use gpui::{App, AppContext as _, Context, Entity}; -use language_model::{ - AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry, -}; -use project::DisableAiSettings; +use gpui::{App, Context, Entity}; +use language_model::{LanguageModelProviderId, LanguageModelRegistry}; use provider::deepseek::DeepSeekLanguageModelProvider; pub mod provider; @@ -17,7 +13,7 @@ pub mod ui; use crate::provider::anthropic::AnthropicLanguageModelProvider; use crate::provider::bedrock::BedrockLanguageModelProvider; -use crate::provider::cloud::{self, CloudLanguageModelProvider}; +use crate::provider::cloud::CloudLanguageModelProvider; use crate::provider::copilot_chat::CopilotChatLanguageModelProvider; use crate::provider::google::GoogleLanguageModelProvider; use crate::provider::lmstudio::LmStudioLanguageModelProvider; @@ -52,13 +48,6 @@ pub fn init(user_store: Entity, client: Arc, cx: &mut App) { cx, ); }); - - let mut already_authenticated = false; - if !DisableAiSettings::get_global(cx).disable_ai { - authenticate_all_providers(registry.clone(), cx); - already_authenticated = true; - } - cx.observe_global::(move |cx| { let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx) .openai_compatible @@ -76,12 +65,6 @@ pub fn init(user_store: Entity, client: Arc, cx: &mut App) { ); }); openai_compatible_providers = openai_compatible_providers_new; - already_authenticated = false; - } - - if !DisableAiSettings::get_global(cx).disable_ai && !already_authenticated { - authenticate_all_providers(registry.clone(), cx); - already_authenticated = true; } }) .detach(); @@ -168,83 +151,3 @@ fn register_language_model_providers( registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx); registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx); } - -/// Authenticates all providers in the [`LanguageModelRegistry`]. -/// -/// We do this so that we can populate the language selector with all of the -/// models from the configured providers. -/// -/// This function won't do anything if AI is disabled. -fn authenticate_all_providers(registry: Entity, cx: &mut App) { - let providers_to_authenticate = registry - .read(cx) - .providers() - .iter() - .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) - .collect::>(); - - let mut tasks = Vec::with_capacity(providers_to_authenticate.len()); - - for (provider_id, provider_name, authenticate_task) in providers_to_authenticate { - tasks.push(cx.background_spawn(async move { - if let Err(err) = authenticate_task.await { - if matches!(err, AuthenticateError::CredentialsNotFound) { - // Since we're authenticating these providers in the - // background for the purposes of populating the - // language selector, we don't care about providers - // where the credentials are not found. - } else { - // Some providers have noisy failure states that we - // don't want to spam the logs with every time the - // language model selector is initialized. - // - // Ideally these should have more clear failure modes - // that we know are safe to ignore here, like what we do - // with `CredentialsNotFound` above. - match provider_id.0.as_ref() { - "lmstudio" | "ollama" => { - // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". - // - // These fail noisily, so we don't log them. - } - "copilot_chat" => { - // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. - } - _ => { - log::error!( - "Failed to authenticate provider: {}: {err}", - provider_name.0 - ); - } - } - } - } - })); - } - - let all_authenticated_future = future::join_all(tasks); - - cx.spawn(async move |cx| { - all_authenticated_future.await; - - registry - .update(cx, |registry, cx| { - let cloud_provider = registry.provider(&cloud::PROVIDER_ID); - let fallback_model = cloud_provider - .iter() - .chain(registry.providers().iter()) - .find(|provider| provider.is_authenticated(cx)) - .and_then(|provider| { - Some(ConfiguredModel { - provider: provider.clone(), - model: provider - .default_model(cx) - .or_else(|| provider.recommended_models(cx).first().cloned())?, - }) - }); - registry.set_environment_fallback_model(fallback_model, cx); - }) - .ok(); - }) - .detach(); -} diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index fb6e2fb1e463ffa6d06591e90d11674b1cb091a8..b473d06357a7ac4a60749ef796642bee41a5a511 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -44,8 +44,8 @@ use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, i use crate::provider::google::{GoogleEventMapper, into_google}; use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai}; -pub const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID; -pub const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; +const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID; +const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; #[derive(Default, Clone, Debug, PartialEq)] pub struct ZedDotDevSettings { @@ -146,7 +146,7 @@ impl State { default_fast_model: None, recommended_models: Vec::new(), _fetch_models_task: cx.spawn(async move |this, cx| { - maybe!(async { + maybe!(async move { let (client, llm_api_token) = this .read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?; From 65c6c709fdf606cbba9aea8aa8c3818702b110d7 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:55:40 -0300 Subject: [PATCH 355/744] thread view: Refine tool call UI (#36937) Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner --- crates/agent_ui/src/acp/entry_view_state.rs | 58 ++- crates/agent_ui/src/acp/thread_view.rs | 467 +++++++++++--------- crates/markdown/src/markdown.rs | 2 +- 3 files changed, 315 insertions(+), 212 deletions(-) diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 0e4080d689bd4ae4ff67bdd7c6a9beb3f220f2b9..becf6953fd8e63bd6e208c74234c91beb94c4b44 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -6,7 +6,7 @@ use agent2::HistoryStore; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; use gpui::{ - AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, + AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle, TextStyleRefinement, WeakEntity, Window, }; use language::language_settings::SoftWrap; @@ -154,10 +154,22 @@ impl EntryViewState { }); } } - AgentThreadEntry::AssistantMessage(_) => { - if index == self.entries.len() { - self.entries.push(Entry::empty()) - } + AgentThreadEntry::AssistantMessage(message) => { + let entry = if let Some(Entry::AssistantMessage(entry)) = + self.entries.get_mut(index) + { + entry + } else { + self.set_entry( + index, + Entry::AssistantMessage(AssistantMessageEntry::default()), + ); + let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else { + unreachable!() + }; + entry + }; + entry.sync(message); } }; } @@ -177,7 +189,7 @@ impl EntryViewState { pub fn settings_changed(&mut self, cx: &mut App) { for entry in self.entries.iter() { match entry { - Entry::UserMessage { .. } => {} + Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {} Entry::Content(response_views) => { for view in response_views.values() { if let Ok(diff_editor) = view.clone().downcast::() { @@ -208,9 +220,29 @@ pub enum ViewEvent { MessageEditorEvent(Entity, MessageEditorEvent), } +#[derive(Default, Debug)] +pub struct AssistantMessageEntry { + scroll_handles_by_chunk_index: HashMap, +} + +impl AssistantMessageEntry { + pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option { + self.scroll_handles_by_chunk_index.get(&ix).cloned() + } + + pub fn sync(&mut self, message: &acp_thread::AssistantMessage) { + if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() { + let ix = message.chunks.len() - 1; + let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default(); + handle.scroll_to_bottom(); + } + } +} + #[derive(Debug)] pub enum Entry { UserMessage(Entity), + AssistantMessage(AssistantMessageEntry), Content(HashMap), } @@ -218,7 +250,7 @@ impl Entry { pub fn message_editor(&self) -> Option<&Entity> { match self { Self::UserMessage(editor) => Some(editor), - Entry::Content(_) => None, + Self::AssistantMessage(_) | Self::Content(_) => None, } } @@ -239,6 +271,16 @@ impl Entry { .map(|entity| entity.downcast::().unwrap()) } + pub fn scroll_handle_for_assistant_message_chunk( + &self, + chunk_ix: usize, + ) -> Option { + match self { + Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix), + Self::UserMessage(_) | Self::Content(_) => None, + } + } + fn content_map(&self) -> Option<&HashMap> { match self { Self::Content(map) => Some(map), @@ -254,7 +296,7 @@ impl Entry { pub fn has_content(&self) -> bool { match self { Self::Content(map) => !map.is_empty(), - Self::UserMessage(_) => false, + Self::UserMessage(_) | Self::AssistantMessage(_) => false, } } } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 6d8f8fb82efb1d6acf9728b04879e197b1f06bd4..f3b1e6ce3b5a349cc767b82a5e4a799d0671c0a9 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -20,11 +20,11 @@ use file_icons::FileIcons; use fs::Fs; use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, - EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, - ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, - Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, - WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point, - prelude::*, pulsating_between, + CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, + ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, + Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, + Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, + point, prelude::*, pulsating_between, }; use language::Buffer; @@ -66,7 +66,6 @@ use crate::{ KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector, }; -const RESPONSE_PADDING_X: Pixels = px(19.); pub const MIN_EDITOR_LINES: usize = 4; pub const MAX_EDITOR_LINES: usize = 8; @@ -1334,6 +1333,10 @@ impl AcpThreadView { window: &mut Window, cx: &Context, ) -> AnyElement { + let is_generating = self + .thread() + .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); + let primary = match &entry { AgentThreadEntry::UserMessage(message) => { let Some(editor) = self @@ -1493,6 +1496,20 @@ impl AcpThreadView { .into_any() } AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { + let is_last = entry_ix + 1 == total_entries; + let pending_thinking_chunk_ix = if is_generating && is_last { + chunks + .iter() + .enumerate() + .next_back() + .filter(|(_, segment)| { + matches!(segment, AssistantMessageChunk::Thought { .. }) + }) + .map(|(index, _)| index) + } else { + None + }; + let style = default_markdown_style(false, false, window, cx); let message_body = v_flex() .w_full() @@ -1511,6 +1528,7 @@ impl AcpThreadView { entry_ix, chunk_ix, md.clone(), + Some(chunk_ix) == pending_thinking_chunk_ix, window, cx, ) @@ -1524,7 +1542,7 @@ impl AcpThreadView { v_flex() .px_5() .py_1() - .when(entry_ix + 1 == total_entries, |this| this.pb_4()) + .when(is_last, |this| this.pb_4()) .w_full() .text_ui(cx) .child(message_body) @@ -1533,7 +1551,7 @@ impl AcpThreadView { AgentThreadEntry::ToolCall(tool_call) => { let has_terminals = tool_call.terminals().next().is_some(); - div().w_full().py_1().px_5().map(|this| { + div().w_full().map(|this| { if has_terminals { this.children(tool_call.terminals().map(|terminal| { self.render_terminal_tool_call( @@ -1609,64 +1627,90 @@ impl AcpThreadView { entry_ix: usize, chunk_ix: usize, chunk: Entity, + pending: bool, window: &Window, cx: &Context, ) -> AnyElement { let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix)); let card_header_id = SharedString::from("inner-card-header"); + let key = (entry_ix, chunk_ix); + let is_open = self.expanded_thinking_blocks.contains(&key); + let editor_bg = cx.theme().colors().editor_background; + let gradient_overlay = div() + .rounded_b_lg() + .h_full() + .absolute() + .w_full() + .bottom_0() + .left_0() + .bg(linear_gradient( + 180., + linear_color_stop(editor_bg, 1.), + linear_color_stop(editor_bg.opacity(0.2), 0.), + )); + + let scroll_handle = self + .entry_view_state + .read(cx) + .entry(entry_ix) + .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix)); v_flex() + .rounded_md() + .border_1() + .border_color(self.tool_card_border_color(cx)) .child( h_flex() .id(header_id) .group(&card_header_id) .relative() .w_full() - .gap_1p5() + .py_0p5() + .px_1p5() + .rounded_t_md() + .bg(self.tool_card_header_bg(cx)) + .justify_between() + .border_b_1() + .border_color(self.tool_card_border_color(cx)) .child( h_flex() - .size_4() - .justify_center() + .h(window.line_height()) + .gap_1p5() .child( - div() - .group_hover(&card_header_id, |s| s.invisible().w_0()) - .child( - Icon::new(IconName::ToolThink) - .size(IconSize::Small) - .color(Color::Muted), - ), + Icon::new(IconName::ToolThink) + .size(IconSize::Small) + .color(Color::Muted), ) .child( - h_flex() - .absolute() - .inset_0() - .invisible() - .justify_center() - .group_hover(&card_header_id, |s| s.visible()) - .child( - Disclosure::new(("expand", entry_ix), is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronRight) - .on_click(cx.listener({ - move |this, _event, _window, cx| { - if is_open { - this.expanded_thinking_blocks.remove(&key); - } else { - this.expanded_thinking_blocks.insert(key); - } - cx.notify(); - } - })), - ), + div() + .text_size(self.tool_name_font_size()) + .text_color(cx.theme().colors().text_muted) + .map(|this| { + if pending { + this.child("Thinking") + } else { + this.child("Thought Process") + } + }), ), ) .child( - div() - .text_size(self.tool_name_font_size()) - .text_color(cx.theme().colors().text_muted) - .child("Thinking"), + Disclosure::new(("expand", entry_ix), is_open) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronDown) + .visible_on_hover(&card_header_id) + .on_click(cx.listener({ + move |this, _event, _window, cx| { + if is_open { + this.expanded_thinking_blocks.remove(&key); + } else { + this.expanded_thinking_blocks.insert(key); + } + cx.notify(); + } + })), ) .on_click(cx.listener({ move |this, _event, _window, cx| { @@ -1679,22 +1723,28 @@ impl AcpThreadView { } })), ) - .when(is_open, |this| { - this.child( - div() - .relative() - .mt_1p5() - .ml(rems(0.4)) - .pl_4() - .border_l_1() - .border_color(self.tool_card_border_color(cx)) - .text_ui_sm(cx) - .child(self.render_markdown( - chunk, - default_markdown_style(false, false, window, cx), - )), - ) - }) + .child( + div() + .relative() + .bg(editor_bg) + .rounded_b_lg() + .child( + div() + .id(("thinking-content", chunk_ix)) + .when_some(scroll_handle, |this, scroll_handle| { + this.track_scroll(&scroll_handle) + }) + .p_2() + .when(!is_open, |this| this.max_h_20()) + .text_ui_sm(cx) + .overflow_hidden() + .child(self.render_markdown( + chunk, + default_markdown_style(false, false, window, cx), + )), + ) + .when(!is_open && pending, |this| this.child(gradient_overlay)), + ) .into_any_element() } @@ -1705,7 +1755,6 @@ impl AcpThreadView { window: &Window, cx: &Context, ) -> Div { - let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix)); let card_header_id = SharedString::from("inner-tool-call-header"); let tool_icon = @@ -1734,11 +1783,7 @@ impl AcpThreadView { _ => false, }; - let failed_tool_call = matches!( - tool_call.status, - ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed - ); - + let has_location = tool_call.locations.len() == 1; let needs_confirmation = matches!( tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } @@ -1751,23 +1796,31 @@ impl AcpThreadView { let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); - let gradient_overlay = |color: Hsla| { + let gradient_overlay = { div() .absolute() .top_0() .right_0() .w_12() .h_full() - .bg(linear_gradient( - 90., - linear_color_stop(color, 1.), - linear_color_stop(color.opacity(0.2), 0.), - )) - }; - let gradient_color = if use_card_layout { - self.tool_card_header_bg(cx) - } else { - cx.theme().colors().panel_background + .map(|this| { + if use_card_layout { + this.bg(linear_gradient( + 90., + linear_color_stop(self.tool_card_header_bg(cx), 1.), + linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.), + )) + } else { + this.bg(linear_gradient( + 90., + linear_color_stop(cx.theme().colors().panel_background, 1.), + linear_color_stop( + cx.theme().colors().panel_background.opacity(0.2), + 0., + ), + )) + } + }) }; let tool_output_display = if is_open { @@ -1818,41 +1871,58 @@ impl AcpThreadView { }; v_flex() - .when(use_card_layout, |this| { - this.rounded_md() - .border_1() - .border_color(self.tool_card_border_color(cx)) - .bg(cx.theme().colors().editor_background) - .overflow_hidden() + .map(|this| { + if use_card_layout { + this.my_2() + .rounded_md() + .border_1() + .border_color(self.tool_card_border_color(cx)) + .bg(cx.theme().colors().editor_background) + .overflow_hidden() + } else { + this.my_1() + } + }) + .map(|this| { + if has_location && !use_card_layout { + this.ml_4() + } else { + this.ml_5() + } }) + .mr_5() .child( h_flex() - .id(header_id) .group(&card_header_id) .relative() .w_full() - .max_w_full() .gap_1() + .justify_between() .when(use_card_layout, |this| { - this.pl_1p5() - .pr_1() - .py_0p5() + this.p_0p5() .rounded_t_md() - .when(is_open && !failed_tool_call, |this| { + .bg(self.tool_card_header_bg(cx)) + .when(is_open && !failed_or_canceled, |this| { this.border_b_1() .border_color(self.tool_card_border_color(cx)) }) - .bg(self.tool_card_header_bg(cx)) }) .child( h_flex() .relative() .w_full() - .h(window.line_height() - px(2.)) + .h(window.line_height()) .text_size(self.tool_name_font_size()) - .gap_0p5() + .gap_1p5() + .when(has_location || use_card_layout, |this| this.px_1()) + .when(has_location, |this| { + this.cursor(CursorStyle::PointingHand) + .rounded_sm() + .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5))) + }) + .overflow_hidden() .child(tool_icon) - .child(if tool_call.locations.len() == 1 { + .child(if has_location { let name = tool_call.locations[0] .path .file_name() @@ -1863,13 +1933,6 @@ impl AcpThreadView { h_flex() .id(("open-tool-call-location", entry_ix)) .w_full() - .max_w_full() - .px_1p5() - .rounded_sm() - .overflow_x_scroll() - .hover(|label| { - label.bg(cx.theme().colors().element_hover.opacity(0.5)) - }) .map(|this| { if use_card_layout { this.text_color(cx.theme().colors().text) @@ -1879,31 +1942,28 @@ impl AcpThreadView { }) .child(name) .tooltip(Tooltip::text("Jump to File")) - .cursor(gpui::CursorStyle::PointingHand) .on_click(cx.listener(move |this, _, window, cx| { this.open_tool_call_location(entry_ix, 0, window, cx); })) .into_any_element() } else { h_flex() - .relative() .w_full() - .max_w_full() - .ml_1p5() - .overflow_hidden() - .child(h_flex().pr_8().child(self.render_markdown( + .child(self.render_markdown( tool_call.label.clone(), default_markdown_style(false, true, window, cx), - ))) - .child(gradient_overlay(gradient_color)) + )) .into_any() - }), + }) + .when(!has_location, |this| this.child(gradient_overlay)), ) - .child( - h_flex() - .gap_px() - .when(is_collapsible, |this| { - this.child( + .when(is_collapsible || failed_or_canceled, |this| { + this.child( + h_flex() + .px_1() + .gap_px() + .when(is_collapsible, |this| { + this.child( Disclosure::new(("expand", entry_ix), is_open) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) @@ -1920,15 +1980,16 @@ impl AcpThreadView { } })), ) - }) - .when(failed_or_canceled, |this| { - this.child( - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small), - ) - }), - ), + }) + .when(failed_or_canceled, |this| { + this.child( + Icon::new(IconName::Close) + .color(Color::Error) + .size(IconSize::Small), + ) + }), + ) + }), ) .children(tool_output_display) } @@ -2214,6 +2275,12 @@ impl AcpThreadView { started_at.elapsed() }; + let header_id = + SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id())); + let header_group = SharedString::from(format!( + "terminal-tool-header-group-{}", + terminal.entity_id() + )); let header_bg = cx .theme() .colors() @@ -2229,10 +2296,7 @@ impl AcpThreadView { let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); let header = h_flex() - .id(SharedString::from(format!( - "terminal-tool-header-{}", - terminal.entity_id() - ))) + .id(header_id) .flex_none() .gap_1() .justify_between() @@ -2296,23 +2360,6 @@ impl AcpThreadView { ), ) }) - .when(tool_failed || command_failed, |header| { - header.child( - div() - .id(("terminal-tool-error-code-indicator", terminal.entity_id())) - .child( - Icon::new(IconName::Close) - .size(IconSize::Small) - .color(Color::Error), - ) - .when_some(output.and_then(|o| o.exit_status), |this, status| { - this.tooltip(Tooltip::text(format!( - "Exited with code {}", - status.code().unwrap_or(-1), - ))) - }), - ) - }) .when(truncated_output, |header| { let tooltip = if let Some(output) = output { if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { @@ -2365,6 +2412,7 @@ impl AcpThreadView { ) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) + .visible_on_hover(&header_group) .on_click(cx.listener({ let id = tool_call.id.clone(); move |this, _event, _window, _cx| { @@ -2373,8 +2421,26 @@ impl AcpThreadView { } else { this.expanded_tool_calls.insert(id.clone()); } - }})), - ); + } + })), + ) + .when(tool_failed || command_failed, |header| { + header.child( + div() + .id(("terminal-tool-error-code-indicator", terminal.entity_id())) + .child( + Icon::new(IconName::Close) + .size(IconSize::Small) + .color(Color::Error), + ) + .when_some(output.and_then(|o| o.exit_status), |this, status| { + this.tooltip(Tooltip::text(format!( + "Exited with code {}", + status.code().unwrap_or(-1), + ))) + }), + ) + }); let terminal_view = self .entry_view_state @@ -2384,7 +2450,8 @@ impl AcpThreadView { let show_output = is_expanded && terminal_view.is_some(); v_flex() - .mb_2() + .my_2() + .mx_5() .border_1() .when(tool_failed || command_failed, |card| card.border_dashed()) .border_color(border_color) @@ -2392,9 +2459,10 @@ impl AcpThreadView { .overflow_hidden() .child( v_flex() + .group(&header_group) .py_1p5() - .pl_2() .pr_1p5() + .pl_2() .gap_0p5() .bg(header_bg) .text_xs() @@ -4153,13 +4221,14 @@ impl AcpThreadView { ) -> impl IntoElement { let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); if is_generating { - return h_flex().id("thread-controls-container").ml_1().child( + return h_flex().id("thread-controls-container").child( div() .py_2() - .px(rems_from_px(22.)) + .px_5() .child(SpinnerLabel::new().size(LabelSize::Small)), ); } + let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) @@ -4185,12 +4254,10 @@ impl AcpThreadView { .id("thread-controls-container") .group("thread-controls-container") .w_full() - .mr_1() - .pt_1() - .pb_2() - .px(RESPONSE_PADDING_X) + .py_2() + .px_5() .gap_px() - .opacity(0.4) + .opacity(0.6) .hover(|style| style.opacity(1.)) .flex_wrap() .justify_end(); @@ -4201,56 +4268,50 @@ impl AcpThreadView { .is_some_and(|thread| thread.read(cx).connection().telemetry().is_some()) { let feedback = self.thread_feedback.feedback; - container = container.child( - div().visible_on_hover("thread-controls-container").child( - Label::new( - match feedback { + + container = container + .child( + div().visible_on_hover("thread-controls-container").child( + Label::new(match feedback { Some(ThreadFeedback::Positive) => "Thanks for your feedback!", - Some(ThreadFeedback::Negative) => "We appreciate your feedback and will use it to improve.", - None => "Rating the thread sends all of your current conversation to the Zed team.", - } - ) - .color(Color::Muted) - .size(LabelSize::XSmall) - .truncate(), - ), - ).child( - h_flex() - .child( - IconButton::new("feedback-thumbs-up", IconName::ThumbsUp) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(match feedback { - Some(ThreadFeedback::Positive) => Color::Accent, - _ => Color::Ignored, - }) - .tooltip(Tooltip::text("Helpful Response")) - .on_click(cx.listener(move |this, _, window, cx| { - this.handle_feedback_click( - ThreadFeedback::Positive, - window, - cx, - ); - })), - ) - .child( - IconButton::new("feedback-thumbs-down", IconName::ThumbsDown) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(match feedback { - Some(ThreadFeedback::Negative) => Color::Accent, - _ => Color::Ignored, - }) - .tooltip(Tooltip::text("Not Helpful")) - .on_click(cx.listener(move |this, _, window, cx| { - this.handle_feedback_click( - ThreadFeedback::Negative, - window, - cx, - ); - })), - ) - ) + Some(ThreadFeedback::Negative) => { + "We appreciate your feedback and will use it to improve." + } + None => { + "Rating the thread sends all of your current conversation to the Zed team." + } + }) + .color(Color::Muted) + .size(LabelSize::XSmall) + .truncate(), + ), + ) + .child( + IconButton::new("feedback-thumbs-up", IconName::ThumbsUp) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(match feedback { + Some(ThreadFeedback::Positive) => Color::Accent, + _ => Color::Ignored, + }) + .tooltip(Tooltip::text("Helpful Response")) + .on_click(cx.listener(move |this, _, window, cx| { + this.handle_feedback_click(ThreadFeedback::Positive, window, cx); + })), + ) + .child( + IconButton::new("feedback-thumbs-down", IconName::ThumbsDown) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(match feedback { + Some(ThreadFeedback::Negative) => Color::Accent, + _ => Color::Ignored, + }) + .tooltip(Tooltip::text("Not Helpful")) + .on_click(cx.listener(move |this, _, window, cx| { + this.handle_feedback_click(ThreadFeedback::Negative, window, cx); + })), + ); } container.child(open_as_markdown).child(scroll_to_top) diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index f16da45d799fc9d3b988e76d51b26f89223ef596..1f607a033ae08b67f1c2cb66d5ed9d9efd316971 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1323,7 +1323,7 @@ fn render_copy_code_block_button( .icon_size(IconSize::Small) .style(ButtonStyle::Filled) .shape(ui::IconButtonShape::Square) - .tooltip(Tooltip::text("Copy Code")) + .tooltip(Tooltip::text("Copy")) .on_click({ let markdown = markdown; move |_event, _window, cx| { From 0e575b280997b346e58497725fa0609ac4f6f47c Mon Sep 17 00:00:00 2001 From: Adam Mulvany Date: Wed, 27 Aug 2025 02:38:53 +1000 Subject: [PATCH 356/744] helix: Fix `buffer search: deploy` reset to normal mode (#36917) ## Fix: Preserve Helix mode when using search ### Problem When using `buffer search: deploy` in Helix mode, pressing Enter to dismiss the search incorrectly returned to Vim NORMAL mode instead of Helix NORMAL mode. ### Root Cause The `search_deploy` function was resetting the entire `SearchState` to default values when buffer search: deploy was activated. Since the default `Mode` is `Normal`, this caused `prior_mode` to be set to Vim's Normal mode regardless of the actual mode before search. ### Solution Modified `search_deploy` to preserve the current mode when resetting search state: - Store the current mode before resetting - Reset search state to default - Restore the saved mode to `prior_mode` This ensures the editor returns to the correct mode (Helix NORMAL or Vim NORMAL) after dismissing buffer search. ### Settings I was able to reproduce and then test the fix was successful with the following config and have also tested with vim: default_mode commented out to ensure that's not influencing the mode selection flow: ``` "helix_mode": true, "vim_mode": true, "vim": { "default_mode": "helix_normal" }, ``` This is on Kubuntu 24.04. The following test combinations pass locally: - `cargo test -p search` - `cargo test -p vim` - `cargo test -p editor` - `cargo test -p workspace` - `cargo test -p gpui -- vim` - `cargo test -p gpui -- helix` Release Notes: - Fixed Helix mode switching to Vim normal mode after using `buffer search: deploy` to search Closes #36872 --- crates/vim/src/normal/search.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 4fbeec72365c1613acd4d1d740c518a4676a48a5..dba003ec5fce9b74375b1791dba690a2267ff0ba 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -203,7 +203,10 @@ impl Vim { // hook into the existing to clear out any vim search state on cmd+f or edit -> find. fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context) { + // Preserve the current mode when resetting search state + let current_mode = self.mode; self.search = Default::default(); + self.search.prior_mode = current_mode; cx.propagate(); } From b1b60bb7fe64f6b1ee6c1758f2792a68822bcc8a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Aug 2025 10:54:39 -0700 Subject: [PATCH 357/744] Work around duplicate ssh projects in workspace migration (#36946) Fixes another case where the sqlite migration could fail, reported by @SomeoneToIgnore. Release Notes: - N/A --- crates/workspace/src/persistence.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 12e719cfd911c05b803810ee087e933c5b575ec6..c4ba93bcec60f158d22904674996ba63202a64a6 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -599,6 +599,13 @@ impl Domain for WorkspaceDb { ssh_projects ON workspaces.ssh_project_id = ssh_projects.id; + DELETE FROM workspaces_2 + WHERE workspace_id NOT IN ( + SELECT MAX(workspace_id) + FROM workspaces_2 + GROUP BY ssh_connection_id, paths + ); + DROP TABLE ssh_projects; DROP TABLE workspaces; ALTER TABLE workspaces_2 RENAME TO workspaces; From fff0ecead17c601721c35fcacfbb4804ad22a956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Wed, 27 Aug 2025 03:24:50 +0800 Subject: [PATCH 358/744] windows: Fix keystroke & keymap (#36572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #36300 This PR follows Windows conventions by introducing `KeybindingKeystroke`, so shortcuts now show up as `ctrl-shift-4` instead of `ctrl-$`. It also fixes issues with keyboard layouts: when `use_key_equivalents` is set to true, keys are remapped based on their virtual key codes. For example, `ctrl-\` on a standard English layout will be mapped to `ctrl-ё` on a Russian layout. Release Notes: - N/A --------- Co-authored-by: Kate --- assets/keymaps/default-windows.json | 1260 ++++++++++++++ crates/docs_preprocessor/src/main.rs | 5 + crates/editor/src/editor.rs | 26 +- crates/editor/src/element.rs | 10 +- crates/gpui/src/app.rs | 17 +- crates/gpui/src/keymap.rs | 7 +- crates/gpui/src/keymap/binding.rs | 49 +- crates/gpui/src/platform.rs | 6 +- crates/gpui/src/platform/keyboard.rs | 34 + crates/gpui/src/platform/keystroke.rs | 252 ++- crates/gpui/src/platform/linux/platform.rs | 8 +- crates/gpui/src/platform/mac/keyboard.rs | 1453 ++++++++++++++++- crates/gpui/src/platform/mac/platform.rs | 28 +- crates/gpui/src/platform/test/platform.rs | 11 +- crates/gpui/src/platform/windows/keyboard.rs | 244 ++- crates/gpui/src/platform/windows/platform.rs | 4 + crates/language_tools/src/key_context_view.rs | 4 +- crates/settings/src/key_equivalents.rs | 1424 ---------------- crates/settings/src/keymap_file.rs | 66 +- crates/settings/src/settings.rs | 7 +- crates/settings_ui/src/keybindings.rs | 101 +- .../src/ui_components/keystroke_input.rs | 74 +- crates/ui/src/components/keybinding.rs | 131 +- crates/zed/src/zed.rs | 10 +- .../zed/src/zed/quick_action_bar/preview.rs | 5 +- 25 files changed, 3515 insertions(+), 1721 deletions(-) create mode 100644 assets/keymaps/default-windows.json delete mode 100644 crates/settings/src/key_equivalents.rs diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json new file mode 100644 index 0000000000000000000000000000000000000000..c7a6c3149ccab86d3efcf8f5db94e0b40be6a3c0 --- /dev/null +++ b/assets/keymaps/default-windows.json @@ -0,0 +1,1260 @@ +[ + // Standard Windows bindings + { + "use_key_equivalents": true, + "bindings": { + "home": "menu::SelectFirst", + "shift-pageup": "menu::SelectFirst", + "pageup": "menu::SelectFirst", + "end": "menu::SelectLast", + "shift-pagedown": "menu::SelectLast", + "pagedown": "menu::SelectLast", + "ctrl-n": "menu::SelectNext", + "tab": "menu::SelectNext", + "down": "menu::SelectNext", + "ctrl-p": "menu::SelectPrevious", + "shift-tab": "menu::SelectPrevious", + "up": "menu::SelectPrevious", + "enter": "menu::Confirm", + "ctrl-enter": "menu::SecondaryConfirm", + "ctrl-escape": "menu::Cancel", + "ctrl-c": "menu::Cancel", + "escape": "menu::Cancel", + "shift-alt-enter": "menu::Restart", + "alt-enter": ["picker::ConfirmInput", { "secondary": false }], + "ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }], + "ctrl-shift-w": "workspace::CloseWindow", + "shift-escape": "workspace::ToggleZoom", + "open": "workspace::Open", + "ctrl-o": "workspace::Open", + "ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }], + "ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }], + "ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }], + "ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }], + "ctrl-,": "zed::OpenSettings", + "ctrl-q": "zed::Quit", + "f4": "debugger::Start", + "shift-f5": "debugger::Stop", + "ctrl-shift-f5": "debugger::RerunSession", + "f6": "debugger::Pause", + "f7": "debugger::StepOver", + "ctrl-f11": "debugger::StepInto", + "shift-f11": "debugger::StepOut", + "f11": "zed::ToggleFullScreen", + "ctrl-shift-i": "edit_prediction::ToggleMenu", + "shift-alt-l": "lsp_tool::ToggleMenu" + } + }, + { + "context": "Picker || menu", + "use_key_equivalents": true, + "bindings": { + "up": "menu::SelectPrevious", + "down": "menu::SelectNext" + } + }, + { + "context": "Editor", + "use_key_equivalents": true, + "bindings": { + "escape": "editor::Cancel", + "shift-backspace": "editor::Backspace", + "backspace": "editor::Backspace", + "delete": "editor::Delete", + "tab": "editor::Tab", + "shift-tab": "editor::Backtab", + "ctrl-k": "editor::CutToEndOfLine", + "ctrl-k ctrl-q": "editor::Rewrap", + "ctrl-k q": "editor::Rewrap", + "ctrl-backspace": "editor::DeleteToPreviousWordStart", + "ctrl-delete": "editor::DeleteToNextWordEnd", + "cut": "editor::Cut", + "shift-delete": "editor::Cut", + "ctrl-x": "editor::Cut", + "copy": "editor::Copy", + "ctrl-insert": "editor::Copy", + "ctrl-c": "editor::Copy", + "paste": "editor::Paste", + "shift-insert": "editor::Paste", + "ctrl-v": "editor::Paste", + "undo": "editor::Undo", + "ctrl-z": "editor::Undo", + "redo": "editor::Redo", + "ctrl-y": "editor::Redo", + "ctrl-shift-z": "editor::Redo", + "up": "editor::MoveUp", + "ctrl-up": "editor::LineUp", + "ctrl-down": "editor::LineDown", + "pageup": "editor::MovePageUp", + "alt-pageup": "editor::PageUp", + "shift-pageup": "editor::SelectPageUp", + "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], + "down": "editor::MoveDown", + "pagedown": "editor::MovePageDown", + "alt-pagedown": "editor::PageDown", + "shift-pagedown": "editor::SelectPageDown", + "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }], + "left": "editor::MoveLeft", + "right": "editor::MoveRight", + "ctrl-left": "editor::MoveToPreviousWordStart", + "ctrl-right": "editor::MoveToNextWordEnd", + "ctrl-home": "editor::MoveToBeginning", + "ctrl-end": "editor::MoveToEnd", + "shift-up": "editor::SelectUp", + "shift-down": "editor::SelectDown", + "shift-left": "editor::SelectLeft", + "shift-right": "editor::SelectRight", + "ctrl-shift-left": "editor::SelectToPreviousWordStart", + "ctrl-shift-right": "editor::SelectToNextWordEnd", + "ctrl-shift-home": "editor::SelectToBeginning", + "ctrl-shift-end": "editor::SelectToEnd", + "ctrl-a": "editor::SelectAll", + "ctrl-l": "editor::SelectLine", + "shift-alt-f": "editor::Format", + "shift-alt-o": "editor::OrganizeImports", + "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], + "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], + "ctrl-alt-space": "editor::ShowCharacterPalette", + "ctrl-;": "editor::ToggleLineNumbers", + "ctrl-'": "editor::ToggleSelectedDiffHunks", + "ctrl-\"": "editor::ExpandAllDiffHunks", + "ctrl-i": "editor::ShowSignatureHelp", + "alt-g b": "git::Blame", + "alt-g m": "git::OpenModifiedFiles", + "menu": "editor::OpenContextMenu", + "shift-f10": "editor::OpenContextMenu", + "ctrl-shift-e": "editor::ToggleEditPrediction", + "f9": "editor::ToggleBreakpoint", + "shift-f9": "editor::EditLogBreakpoint" + } + }, + { + "context": "Editor && mode == full", + "use_key_equivalents": true, + "bindings": { + "shift-enter": "editor::Newline", + "enter": "editor::Newline", + "ctrl-enter": "editor::NewlineAbove", + "ctrl-shift-enter": "editor::NewlineBelow", + "ctrl-k ctrl-z": "editor::ToggleSoftWrap", + "ctrl-k z": "editor::ToggleSoftWrap", + "find": "buffer_search::Deploy", + "ctrl-f": "buffer_search::Deploy", + "ctrl-h": "buffer_search::DeployReplace", + "ctrl-shift-.": "assistant::QuoteSelection", + "ctrl-shift-,": "assistant::InsertIntoEditor", + "shift-alt-e": "editor::SelectEnclosingSymbol", + "ctrl-shift-backspace": "editor::GoToPreviousChange", + "ctrl-shift-alt-backspace": "editor::GoToNextChange", + "alt-enter": "editor::OpenSelectionsInMultibuffer" + } + }, + { + "context": "Editor && mode == full && edit_prediction", + "use_key_equivalents": true, + "bindings": { + "alt-]": "editor::NextEditPrediction", + "alt-[": "editor::PreviousEditPrediction" + } + }, + { + "context": "Editor && !edit_prediction", + "use_key_equivalents": true, + "bindings": { + "alt-\\": "editor::ShowEditPrediction" + } + }, + { + "context": "Editor && mode == auto_height", + "use_key_equivalents": true, + "bindings": { + "ctrl-enter": "editor::Newline", + "shift-enter": "editor::Newline", + "ctrl-shift-enter": "editor::NewlineBelow" + } + }, + { + "context": "Markdown", + "use_key_equivalents": true, + "bindings": { + "copy": "markdown::Copy", + "ctrl-c": "markdown::Copy" + } + }, + { + "context": "Editor && jupyter && !ContextEditor", + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-enter": "repl::Run", + "ctrl-alt-enter": "repl::RunInPlace" + } + }, + { + "context": "Editor && !agent_diff", + "use_key_equivalents": true, + "bindings": { + "ctrl-k ctrl-r": "git::Restore", + "alt-y": "git::StageAndNext", + "shift-alt-y": "git::UnstageAndNext" + } + }, + { + "context": "Editor && editor_agent_diff", + "use_key_equivalents": true, + "bindings": { + "ctrl-y": "agent::Keep", + "ctrl-n": "agent::Reject", + "ctrl-shift-y": "agent::KeepAll", + "ctrl-shift-n": "agent::RejectAll", + "ctrl-shift-r": "agent::OpenAgentDiff" + } + }, + { + "context": "AgentDiff", + "use_key_equivalents": true, + "bindings": { + "ctrl-y": "agent::Keep", + "ctrl-n": "agent::Reject", + "ctrl-shift-y": "agent::KeepAll", + "ctrl-shift-n": "agent::RejectAll" + } + }, + { + "context": "ContextEditor > Editor", + "use_key_equivalents": true, + "bindings": { + "ctrl-enter": "assistant::Assist", + "ctrl-s": "workspace::Save", + "save": "workspace::Save", + "ctrl-shift-,": "assistant::InsertIntoEditor", + "shift-enter": "assistant::Split", + "ctrl-r": "assistant::CycleMessageRole", + "enter": "assistant::ConfirmCommand", + "alt-enter": "editor::Newline", + "ctrl-k c": "assistant::CopyCode", + "ctrl-g": "search::SelectNextMatch", + "ctrl-shift-g": "search::SelectPreviousMatch", + "ctrl-k l": "agent::OpenRulesLibrary" + } + }, + { + "context": "AgentPanel", + "use_key_equivalents": true, + "bindings": { + "ctrl-n": "agent::NewThread", + "shift-alt-n": "agent::NewTextThread", + "ctrl-shift-h": "agent::OpenHistory", + "shift-alt-c": "agent::OpenSettings", + "shift-alt-p": "agent::OpenRulesLibrary", + "ctrl-i": "agent::ToggleProfileSelector", + "shift-alt-/": "agent::ToggleModelSelector", + "ctrl-shift-a": "agent::ToggleContextPicker", + "ctrl-shift-j": "agent::ToggleNavigationMenu", + "ctrl-shift-i": "agent::ToggleOptionsMenu", + // "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu", + "shift-alt-escape": "agent::ExpandMessageEditor", + "ctrl-shift-.": "assistant::QuoteSelection", + "shift-alt-e": "agent::RemoveAllContext", + "ctrl-shift-e": "project_panel::ToggleFocus", + "ctrl-shift-enter": "agent::ContinueThread", + "super-ctrl-b": "agent::ToggleBurnMode", + "alt-enter": "agent::ContinueWithBurnMode" + } + }, + { + "context": "AgentPanel > NavigationMenu", + "use_key_equivalents": true, + "bindings": { + "shift-backspace": "agent::DeleteRecentlyOpenThread" + } + }, + { + "context": "AgentPanel > Markdown", + "use_key_equivalents": true, + "bindings": { + "copy": "markdown::CopyAsMarkdown", + "ctrl-c": "markdown::CopyAsMarkdown" + } + }, + { + "context": "AgentPanel && prompt_editor", + "use_key_equivalents": true, + "bindings": { + "ctrl-n": "agent::NewTextThread", + "ctrl-alt-t": "agent::NewThread" + } + }, + { + "context": "AgentPanel && external_agent_thread", + "use_key_equivalents": true, + "bindings": { + "ctrl-n": "agent::NewExternalAgentThread", + "ctrl-alt-t": "agent::NewThread" + } + }, + { + "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send", + "use_key_equivalents": true, + "bindings": { + "enter": "agent::Chat", + "ctrl-enter": "agent::ChatWithFollow", + "ctrl-i": "agent::ToggleProfileSelector", + "ctrl-shift-r": "agent::OpenAgentDiff", + "ctrl-shift-y": "agent::KeepAll", + "ctrl-shift-n": "agent::RejectAll" + } + }, + { + "context": "MessageEditor && !Picker > Editor && use_modifier_to_send", + "use_key_equivalents": true, + "bindings": { + "ctrl-enter": "agent::Chat", + "enter": "editor::Newline", + "ctrl-i": "agent::ToggleProfileSelector", + "ctrl-shift-r": "agent::OpenAgentDiff", + "ctrl-shift-y": "agent::KeepAll", + "ctrl-shift-n": "agent::RejectAll" + } + }, + { + "context": "EditMessageEditor > Editor", + "use_key_equivalents": true, + "bindings": { + "escape": "menu::Cancel", + "enter": "menu::Confirm", + "alt-enter": "editor::Newline" + } + }, + { + "context": "AgentFeedbackMessageEditor > Editor", + "use_key_equivalents": true, + "bindings": { + "escape": "menu::Cancel", + "enter": "menu::Confirm", + "alt-enter": "editor::Newline" + } + }, + { + "context": "ContextStrip", + "use_key_equivalents": true, + "bindings": { + "up": "agent::FocusUp", + "right": "agent::FocusRight", + "left": "agent::FocusLeft", + "down": "agent::FocusDown", + "backspace": "agent::RemoveFocusedContext", + "enter": "agent::AcceptSuggestedContext" + } + }, + { + "context": "AcpThread > Editor", + "use_key_equivalents": true, + "bindings": { + "enter": "agent::Chat", + "ctrl-shift-r": "agent::OpenAgentDiff", + "ctrl-shift-y": "agent::KeepAll", + "ctrl-shift-n": "agent::RejectAll" + } + }, + { + "context": "ThreadHistory", + "use_key_equivalents": true, + "bindings": { + "backspace": "agent::RemoveSelectedThread" + } + }, + { + "context": "PromptLibrary", + "use_key_equivalents": true, + "bindings": { + "new": "rules_library::NewRule", + "ctrl-n": "rules_library::NewRule", + "ctrl-shift-s": "rules_library::ToggleDefaultRule" + } + }, + { + "context": "BufferSearchBar", + "use_key_equivalents": true, + "bindings": { + "escape": "buffer_search::Dismiss", + "tab": "buffer_search::FocusEditor", + "enter": "search::SelectNextMatch", + "shift-enter": "search::SelectPreviousMatch", + "alt-enter": "search::SelectAllMatches", + "find": "search::FocusSearch", + "ctrl-f": "search::FocusSearch", + "ctrl-h": "search::ToggleReplace", + "ctrl-l": "search::ToggleSelection" + } + }, + { + "context": "BufferSearchBar && in_replace > Editor", + "use_key_equivalents": true, + "bindings": { + "enter": "search::ReplaceNext", + "ctrl-enter": "search::ReplaceAll" + } + }, + { + "context": "BufferSearchBar && !in_replace > Editor", + "use_key_equivalents": true, + "bindings": { + "up": "search::PreviousHistoryQuery", + "down": "search::NextHistoryQuery" + } + }, + { + "context": "ProjectSearchBar", + "use_key_equivalents": true, + "bindings": { + "escape": "project_search::ToggleFocus", + "shift-find": "search::FocusSearch", + "ctrl-shift-f": "search::FocusSearch", + "ctrl-shift-h": "search::ToggleReplace", + "alt-r": "search::ToggleRegex" // vscode + } + }, + { + "context": "ProjectSearchBar > Editor", + "use_key_equivalents": true, + "bindings": { + "up": "search::PreviousHistoryQuery", + "down": "search::NextHistoryQuery" + } + }, + { + "context": "ProjectSearchBar && in_replace > Editor", + "use_key_equivalents": true, + "bindings": { + "enter": "search::ReplaceNext", + "ctrl-alt-enter": "search::ReplaceAll" + } + }, + { + "context": "ProjectSearchView", + "use_key_equivalents": true, + "bindings": { + "escape": "project_search::ToggleFocus", + "ctrl-shift-h": "search::ToggleReplace", + "alt-r": "search::ToggleRegex" // vscode + } + }, + { + "context": "Pane", + "use_key_equivalents": true, + "bindings": { + "alt-1": ["pane::ActivateItem", 0], + "alt-2": ["pane::ActivateItem", 1], + "alt-3": ["pane::ActivateItem", 2], + "alt-4": ["pane::ActivateItem", 3], + "alt-5": ["pane::ActivateItem", 4], + "alt-6": ["pane::ActivateItem", 5], + "alt-7": ["pane::ActivateItem", 6], + "alt-8": ["pane::ActivateItem", 7], + "alt-9": ["pane::ActivateItem", 8], + "alt-0": "pane::ActivateLastItem", + "ctrl-pageup": "pane::ActivatePreviousItem", + "ctrl-pagedown": "pane::ActivateNextItem", + "ctrl-shift-pageup": "pane::SwapItemLeft", + "ctrl-shift-pagedown": "pane::SwapItemRight", + "ctrl-f4": ["pane::CloseActiveItem", { "close_pinned": false }], + "ctrl-w": ["pane::CloseActiveItem", { "close_pinned": false }], + "ctrl-shift-alt-t": ["pane::CloseOtherItems", { "close_pinned": false }], + "ctrl-shift-alt-w": "workspace::CloseInactiveTabsAndPanes", + "ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }], + "ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }], + "ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }], + "ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }], + "ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes", + "back": "pane::GoBack", + "alt--": "pane::GoBack", + "alt-=": "pane::GoForward", + "forward": "pane::GoForward", + "f3": "search::SelectNextMatch", + "shift-f3": "search::SelectPreviousMatch", + "shift-find": "project_search::ToggleFocus", + "ctrl-shift-f": "project_search::ToggleFocus", + "shift-alt-h": "search::ToggleReplace", + "alt-l": "search::ToggleSelection", + "alt-enter": "search::SelectAllMatches", + "alt-c": "search::ToggleCaseSensitive", + "alt-w": "search::ToggleWholeWord", + "alt-find": "project_search::ToggleFilters", + "alt-f": "project_search::ToggleFilters", + "alt-r": "search::ToggleRegex", + // "ctrl-shift-alt-x": "search::ToggleRegex", + "ctrl-k shift-enter": "pane::TogglePinTab" + } + }, + // Bindings from VS Code + { + "context": "Editor", + "use_key_equivalents": true, + "bindings": { + "ctrl-[": "editor::Outdent", + "ctrl-]": "editor::Indent", + "ctrl-shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above + "ctrl-shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below + "ctrl-shift-k": "editor::DeleteLine", + "alt-up": "editor::MoveLineUp", + "alt-down": "editor::MoveLineDown", + "shift-alt-up": "editor::DuplicateLineUp", + "shift-alt-down": "editor::DuplicateLineDown", + "shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand Selection + "shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection + "ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection + "ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word + "ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand + "ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch + "ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch + "ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip + "ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch + "ctrl-k ctrl-i": "editor::Hover", + "ctrl-k ctrl-b": "editor::BlameHover", + "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }], + "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }], + "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }], + "f2": "editor::Rename", + "f12": "editor::GoToDefinition", + "alt-f12": "editor::GoToDefinitionSplit", + "ctrl-shift-f10": "editor::GoToDefinitionSplit", + "ctrl-f12": "editor::GoToImplementation", + "shift-f12": "editor::GoToTypeDefinition", + "ctrl-alt-f12": "editor::GoToTypeDefinitionSplit", + "shift-alt-f12": "editor::FindAllReferences", + "ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains + "ctrl-shift-\\": "editor::MoveToEnclosingBracket", + "ctrl-shift-[": "editor::Fold", + "ctrl-shift-]": "editor::UnfoldLines", + "ctrl-k ctrl-l": "editor::ToggleFold", + "ctrl-k ctrl-[": "editor::FoldRecursive", + "ctrl-k ctrl-]": "editor::UnfoldRecursive", + "ctrl-k ctrl-1": ["editor::FoldAtLevel", 1], + "ctrl-k ctrl-2": ["editor::FoldAtLevel", 2], + "ctrl-k ctrl-3": ["editor::FoldAtLevel", 3], + "ctrl-k ctrl-4": ["editor::FoldAtLevel", 4], + "ctrl-k ctrl-5": ["editor::FoldAtLevel", 5], + "ctrl-k ctrl-6": ["editor::FoldAtLevel", 6], + "ctrl-k ctrl-7": ["editor::FoldAtLevel", 7], + "ctrl-k ctrl-8": ["editor::FoldAtLevel", 8], + "ctrl-k ctrl-9": ["editor::FoldAtLevel", 9], + "ctrl-k ctrl-0": "editor::FoldAll", + "ctrl-k ctrl-j": "editor::UnfoldAll", + "ctrl-space": "editor::ShowCompletions", + "ctrl-shift-space": "editor::ShowWordCompletions", + "ctrl-.": "editor::ToggleCodeActions", + "ctrl-k r": "editor::RevealInFileManager", + "ctrl-k p": "editor::CopyPath", + "ctrl-\\": "pane::SplitRight", + "ctrl-shift-alt-c": "editor::DisplayCursorNames", + "alt-.": "editor::GoToHunk", + "alt-,": "editor::GoToPreviousHunk" + } + }, + { + "context": "Editor && extension == md", + "use_key_equivalents": true, + "bindings": { + "ctrl-k v": "markdown::OpenPreviewToTheSide", + "ctrl-shift-v": "markdown::OpenPreview" + } + }, + { + "context": "Editor && extension == svg", + "use_key_equivalents": true, + "bindings": { + "ctrl-k v": "svg::OpenPreviewToTheSide", + "ctrl-shift-v": "svg::OpenPreview" + } + }, + { + "context": "Editor && mode == full", + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-o": "outline::Toggle", + "ctrl-g": "go_to_line::Toggle" + } + }, + { + "context": "Workspace", + "use_key_equivalents": true, + "bindings": { + "alt-open": ["projects::OpenRecent", { "create_new_window": false }], + // Change the default action on `menu::Confirm` by setting the parameter + // "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }], + "ctrl-r": ["projects::OpenRecent", { "create_new_window": false }], + "shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], + // Change to open path modal for existing remote connection by setting the parameter + // "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]", + "ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], + "shift-alt-b": "branches::OpenRecent", + "shift-alt-enter": "toast::RunAction", + "ctrl-shift-`": "workspace::NewTerminal", + "save": "workspace::Save", + "ctrl-s": "workspace::Save", + "ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat", + "shift-save": "workspace::SaveAs", + "ctrl-shift-s": "workspace::SaveAs", + "new": "workspace::NewFile", + "ctrl-n": "workspace::NewFile", + "shift-new": "workspace::NewWindow", + "ctrl-shift-n": "workspace::NewWindow", + "ctrl-`": "terminal_panel::ToggleFocus", + "f10": ["app_menu::OpenApplicationMenu", "Zed"], + "alt-1": ["workspace::ActivatePane", 0], + "alt-2": ["workspace::ActivatePane", 1], + "alt-3": ["workspace::ActivatePane", 2], + "alt-4": ["workspace::ActivatePane", 3], + "alt-5": ["workspace::ActivatePane", 4], + "alt-6": ["workspace::ActivatePane", 5], + "alt-7": ["workspace::ActivatePane", 6], + "alt-8": ["workspace::ActivatePane", 7], + "alt-9": ["workspace::ActivatePane", 8], + "ctrl-alt-b": "workspace::ToggleRightDock", + "ctrl-b": "workspace::ToggleLeftDock", + "ctrl-j": "workspace::ToggleBottomDock", + "ctrl-shift-y": "workspace::CloseAllDocks", + "alt-r": "workspace::ResetActiveDockSize", + // For 0px parameter, uses UI font size value. + "shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }], + "shift-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }], + "shift-alt-0": "workspace::ResetOpenDocksSize", + "ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }], + "ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }], + "shift-find": "pane::DeploySearch", + "ctrl-shift-f": "pane::DeploySearch", + "ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }], + "ctrl-shift-t": "pane::ReopenClosedItem", + "ctrl-k ctrl-s": "zed::OpenKeymapEditor", + "ctrl-k ctrl-t": "theme_selector::Toggle", + "ctrl-alt-super-p": "settings_profile_selector::Toggle", + "ctrl-t": "project_symbols::Toggle", + "ctrl-p": "file_finder::Toggle", + "ctrl-tab": "tab_switcher::Toggle", + "ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }], + "ctrl-e": "file_finder::Toggle", + "f1": "command_palette::Toggle", + "ctrl-shift-p": "command_palette::Toggle", + "ctrl-shift-m": "diagnostics::Deploy", + "ctrl-shift-e": "project_panel::ToggleFocus", + "ctrl-shift-b": "outline_panel::ToggleFocus", + "ctrl-shift-g": "git_panel::ToggleFocus", + "ctrl-shift-d": "debug_panel::ToggleFocus", + "ctrl-shift-/": "agent::ToggleFocus", + "alt-save": "workspace::SaveAll", + "ctrl-k s": "workspace::SaveAll", + "ctrl-k m": "language_selector::Toggle", + "escape": "workspace::Unfollow", + "ctrl-k ctrl-left": "workspace::ActivatePaneLeft", + "ctrl-k ctrl-right": "workspace::ActivatePaneRight", + "ctrl-k ctrl-up": "workspace::ActivatePaneUp", + "ctrl-k ctrl-down": "workspace::ActivatePaneDown", + "ctrl-k shift-left": "workspace::SwapPaneLeft", + "ctrl-k shift-right": "workspace::SwapPaneRight", + "ctrl-k shift-up": "workspace::SwapPaneUp", + "ctrl-k shift-down": "workspace::SwapPaneDown", + "ctrl-shift-x": "zed::Extensions", + "ctrl-shift-r": "task::Rerun", + "alt-t": "task::Rerun", + "shift-alt-t": "task::Spawn", + "shift-alt-r": ["task::Spawn", { "reveal_target": "center" }], + // also possible to spawn tasks by name: + // "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }] + // or by tag: + // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }], + "f5": "debugger::Rerun", + "ctrl-f4": "workspace::CloseActiveDock", + "ctrl-w": "workspace::CloseActiveDock" + } + }, + { + "context": "Workspace && debugger_running", + "use_key_equivalents": true, + "bindings": { + "f5": "zed::NoAction" + } + }, + { + "context": "Workspace && debugger_stopped", + "use_key_equivalents": true, + "bindings": { + "f5": "debugger::Continue" + } + }, + { + "context": "ApplicationMenu", + "use_key_equivalents": true, + "bindings": { + "f10": "menu::Cancel", + "left": "app_menu::ActivateMenuLeft", + "right": "app_menu::ActivateMenuRight" + } + }, + // Bindings from Sublime Text + { + "context": "Editor", + "use_key_equivalents": true, + "bindings": { + "ctrl-u": "editor::UndoSelection", + "ctrl-shift-u": "editor::RedoSelection", + "ctrl-shift-j": "editor::JoinLines", + "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", + "shift-alt-h": "editor::DeleteToPreviousSubwordStart", + "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd", + "shift-alt-d": "editor::DeleteToNextSubwordEnd", + "ctrl-alt-left": "editor::MoveToPreviousSubwordStart", + "ctrl-alt-right": "editor::MoveToNextSubwordEnd", + "ctrl-shift-alt-left": "editor::SelectToPreviousSubwordStart", + "ctrl-shift-alt-right": "editor::SelectToNextSubwordEnd" + } + }, + // Bindings from Atom + { + "context": "Pane", + "use_key_equivalents": true, + "bindings": { + "ctrl-k up": "pane::SplitUp", + "ctrl-k down": "pane::SplitDown", + "ctrl-k left": "pane::SplitLeft", + "ctrl-k right": "pane::SplitRight" + } + }, + // Bindings that should be unified with bindings for more general actions + { + "context": "Editor && renaming", + "use_key_equivalents": true, + "bindings": { + "enter": "editor::ConfirmRename" + } + }, + { + "context": "Editor && showing_completions", + "use_key_equivalents": true, + "bindings": { + "enter": "editor::ConfirmCompletion", + "shift-enter": "editor::ConfirmCompletionReplace", + "tab": "editor::ComposeCompletion" + } + }, + // Bindings for accepting edit predictions + // + // alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is + // because alt-tab may not be available, as it is often used for window switching. + { + "context": "Editor && edit_prediction", + "use_key_equivalents": true, + "bindings": { + "alt-tab": "editor::AcceptEditPrediction", + "alt-l": "editor::AcceptEditPrediction", + "tab": "editor::AcceptEditPrediction", + "alt-right": "editor::AcceptPartialEditPrediction" + } + }, + { + "context": "Editor && edit_prediction_conflict", + "use_key_equivalents": true, + "bindings": { + "alt-tab": "editor::AcceptEditPrediction", + "alt-l": "editor::AcceptEditPrediction", + "alt-right": "editor::AcceptPartialEditPrediction" + } + }, + { + "context": "Editor && showing_code_actions", + "use_key_equivalents": true, + "bindings": { + "enter": "editor::ConfirmCodeAction" + } + }, + { + "context": "Editor && (showing_code_actions || showing_completions)", + "use_key_equivalents": true, + "bindings": { + "ctrl-p": "editor::ContextMenuPrevious", + "up": "editor::ContextMenuPrevious", + "ctrl-n": "editor::ContextMenuNext", + "down": "editor::ContextMenuNext", + "pageup": "editor::ContextMenuFirst", + "pagedown": "editor::ContextMenuLast" + } + }, + { + "context": "Editor && showing_signature_help && !showing_completions", + "use_key_equivalents": true, + "bindings": { + "up": "editor::SignatureHelpPrevious", + "down": "editor::SignatureHelpNext" + } + }, + // Custom bindings + { + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-alt-f": "workspace::FollowNextCollaborator", + // Only available in debug builds: opens an element inspector for development. + "shift-alt-i": "dev::ToggleInspector" + } + }, + { + "context": "!Terminal", + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-c": "collab_panel::ToggleFocus" + } + }, + { + "context": "!ContextEditor > Editor && mode == full", + "use_key_equivalents": true, + "bindings": { + "alt-enter": "editor::OpenExcerpts", + "shift-enter": "editor::ExpandExcerpts", + "ctrl-alt-enter": "editor::OpenExcerptsSplit", + "ctrl-shift-e": "pane::RevealInProjectPanel", + "ctrl-f8": "editor::GoToHunk", + "ctrl-shift-f8": "editor::GoToPreviousHunk", + "ctrl-enter": "assistant::InlineAssist", + "ctrl-shift-;": "editor::ToggleInlayHints" + } + }, + { + "context": "PromptEditor", + "use_key_equivalents": true, + "bindings": { + "ctrl-[": "agent::CyclePreviousInlineAssist", + "ctrl-]": "agent::CycleNextInlineAssist", + "shift-alt-e": "agent::RemoveAllContext" + } + }, + { + "context": "Prompt", + "use_key_equivalents": true, + "bindings": { + "left": "menu::SelectPrevious", + "right": "menu::SelectNext", + "h": "menu::SelectPrevious", + "l": "menu::SelectNext" + } + }, + { + "context": "ProjectSearchBar && !in_replace", + "use_key_equivalents": true, + "bindings": { + "ctrl-enter": "project_search::SearchInNew" + } + }, + { + "context": "OutlinePanel && not_editing", + "use_key_equivalents": true, + "bindings": { + "left": "outline_panel::CollapseSelectedEntry", + "right": "outline_panel::ExpandSelectedEntry", + "alt-copy": "outline_panel::CopyPath", + "shift-alt-c": "outline_panel::CopyPath", + "shift-alt-copy": "workspace::CopyRelativePath", + "ctrl-shift-alt-c": "workspace::CopyRelativePath", + "ctrl-alt-r": "outline_panel::RevealInFileManager", + "space": "outline_panel::OpenSelectedEntry", + "shift-down": "menu::SelectNext", + "shift-up": "menu::SelectPrevious", + "alt-enter": "editor::OpenExcerpts", + "ctrl-alt-enter": "editor::OpenExcerptsSplit" + } + }, + { + "context": "ProjectPanel", + "use_key_equivalents": true, + "bindings": { + "left": "project_panel::CollapseSelectedEntry", + "right": "project_panel::ExpandSelectedEntry", + "new": "project_panel::NewFile", + "ctrl-n": "project_panel::NewFile", + "alt-new": "project_panel::NewDirectory", + "alt-n": "project_panel::NewDirectory", + "cut": "project_panel::Cut", + "ctrl-x": "project_panel::Cut", + "copy": "project_panel::Copy", + "ctrl-insert": "project_panel::Copy", + "ctrl-c": "project_panel::Copy", + "paste": "project_panel::Paste", + "shift-insert": "project_panel::Paste", + "ctrl-v": "project_panel::Paste", + "alt-copy": "project_panel::CopyPath", + "shift-alt-c": "project_panel::CopyPath", + "shift-alt-copy": "workspace::CopyRelativePath", + "ctrl-k ctrl-shift-c": "workspace::CopyRelativePath", + "enter": "project_panel::Rename", + "f2": "project_panel::Rename", + "backspace": ["project_panel::Trash", { "skip_prompt": false }], + "delete": ["project_panel::Trash", { "skip_prompt": false }], + "shift-delete": ["project_panel::Delete", { "skip_prompt": false }], + "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], + "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], + "ctrl-alt-r": "project_panel::RevealInFileManager", + "ctrl-shift-enter": "project_panel::OpenWithSystem", + "alt-d": "project_panel::CompareMarkedFiles", + "shift-find": "project_panel::NewSearchInDirectory", + "ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory", + "shift-down": "menu::SelectNext", + "shift-up": "menu::SelectPrevious", + "escape": "menu::Cancel" + } + }, + { + "context": "ProjectPanel && not_editing", + "use_key_equivalents": true, + "bindings": { + "space": "project_panel::Open" + } + }, + { + "context": "GitPanel && ChangesList", + "use_key_equivalents": true, + "bindings": { + "up": "menu::SelectPrevious", + "down": "menu::SelectNext", + "enter": "menu::Confirm", + "alt-y": "git::StageFile", + "shift-alt-y": "git::UnstageFile", + "space": "git::ToggleStaged", + "shift-space": "git::StageRange", + "tab": "git_panel::FocusEditor", + "shift-tab": "git_panel::FocusEditor", + "escape": "git_panel::ToggleFocus", + "alt-enter": "menu::SecondaryConfirm", + "delete": ["git::RestoreFile", { "skip_prompt": false }], + "backspace": ["git::RestoreFile", { "skip_prompt": false }], + "shift-delete": ["git::RestoreFile", { "skip_prompt": false }], + "ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }], + "ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }] + } + }, + { + "context": "GitPanel && CommitEditor", + "use_key_equivalents": true, + "bindings": { + "escape": "git::Cancel" + } + }, + { + "context": "GitCommit > Editor", + "use_key_equivalents": true, + "bindings": { + "escape": "menu::Cancel", + "enter": "editor::Newline", + "ctrl-enter": "git::Commit", + "ctrl-shift-enter": "git::Amend", + "alt-l": "git::GenerateCommitMessage" + } + }, + { + "context": "GitPanel", + "use_key_equivalents": true, + "bindings": { + "ctrl-g ctrl-g": "git::Fetch", + "ctrl-g up": "git::Push", + "ctrl-g down": "git::Pull", + "ctrl-g shift-up": "git::ForcePush", + "ctrl-g d": "git::Diff", + "ctrl-g backspace": "git::RestoreTrackedFiles", + "ctrl-g shift-backspace": "git::TrashUntrackedFiles", + "ctrl-space": "git::StageAll", + "ctrl-shift-space": "git::UnstageAll", + "ctrl-enter": "git::Commit", + "ctrl-shift-enter": "git::Amend" + } + }, + { + "context": "GitDiff > Editor", + "use_key_equivalents": true, + "bindings": { + "ctrl-enter": "git::Commit", + "ctrl-shift-enter": "git::Amend", + "ctrl-space": "git::StageAll", + "ctrl-shift-space": "git::UnstageAll" + } + }, + { + "context": "AskPass > Editor", + "use_key_equivalents": true, + "bindings": { + "enter": "menu::Confirm" + } + }, + { + "context": "CommitEditor > Editor", + "use_key_equivalents": true, + "bindings": { + "escape": "git_panel::FocusChanges", + "tab": "git_panel::FocusChanges", + "shift-tab": "git_panel::FocusChanges", + "enter": "editor::Newline", + "ctrl-enter": "git::Commit", + "ctrl-shift-enter": "git::Amend", + "alt-up": "git_panel::FocusChanges", + "alt-l": "git::GenerateCommitMessage" + } + }, + { + "context": "DebugPanel", + "use_key_equivalents": true, + "bindings": { + "ctrl-t": "debugger::ToggleThreadPicker", + "ctrl-i": "debugger::ToggleSessionPicker", + "shift-alt-escape": "debugger::ToggleExpandItem" + } + }, + { + "context": "VariableList", + "use_key_equivalents": true, + "bindings": { + "left": "variable_list::CollapseSelectedEntry", + "right": "variable_list::ExpandSelectedEntry", + "enter": "variable_list::EditVariable", + "ctrl-c": "variable_list::CopyVariableValue", + "ctrl-alt-c": "variable_list::CopyVariableName", + "delete": "variable_list::RemoveWatch", + "backspace": "variable_list::RemoveWatch", + "alt-enter": "variable_list::AddWatch" + } + }, + { + "context": "BreakpointList", + "use_key_equivalents": true, + "bindings": { + "space": "debugger::ToggleEnableBreakpoint", + "backspace": "debugger::UnsetBreakpoint", + "left": "debugger::PreviousBreakpointProperty", + "right": "debugger::NextBreakpointProperty" + } + }, + { + "context": "CollabPanel && not_editing", + "use_key_equivalents": true, + "bindings": { + "ctrl-backspace": "collab_panel::Remove", + "space": "menu::Confirm" + } + }, + { + "context": "CollabPanel", + "use_key_equivalents": true, + "bindings": { + "alt-up": "collab_panel::MoveChannelUp", + "alt-down": "collab_panel::MoveChannelDown" + } + }, + { + "context": "(CollabPanel && editing) > Editor", + "use_key_equivalents": true, + "bindings": { + "space": "collab_panel::InsertSpace" + } + }, + { + "context": "ChannelModal", + "use_key_equivalents": true, + "bindings": { + "tab": "channel_modal::ToggleMode" + } + }, + { + "context": "Picker > Editor", + "use_key_equivalents": true, + "bindings": { + "escape": "menu::Cancel", + "up": "menu::SelectPrevious", + "down": "menu::SelectNext", + "tab": "picker::ConfirmCompletion", + "alt-enter": ["picker::ConfirmInput", { "secondary": false }] + } + }, + { + "context": "ChannelModal > Picker > Editor", + "use_key_equivalents": true, + "bindings": { + "tab": "channel_modal::ToggleMode" + } + }, + { + "context": "FileFinder || (FileFinder > Picker > Editor)", + "use_key_equivalents": true, + "bindings": { + "ctrl-p": "file_finder::Toggle", + "ctrl-shift-a": "file_finder::ToggleSplitMenu", + "ctrl-shift-i": "file_finder::ToggleFilterMenu" + } + }, + { + "context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)", + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-p": "file_finder::SelectPrevious", + "ctrl-j": "pane::SplitDown", + "ctrl-k": "pane::SplitUp", + "ctrl-h": "pane::SplitLeft", + "ctrl-l": "pane::SplitRight" + } + }, + { + "context": "TabSwitcher", + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-tab": "menu::SelectPrevious", + "ctrl-up": "menu::SelectPrevious", + "ctrl-down": "menu::SelectNext", + "ctrl-backspace": "tab_switcher::CloseSelectedItem" + } + }, + { + "context": "Terminal", + "use_key_equivalents": true, + "bindings": { + "ctrl-alt-space": "terminal::ShowCharacterPalette", + "copy": "terminal::Copy", + "ctrl-insert": "terminal::Copy", + "ctrl-shift-c": "terminal::Copy", + "paste": "terminal::Paste", + "shift-insert": "terminal::Paste", + "ctrl-shift-v": "terminal::Paste", + "ctrl-enter": "assistant::InlineAssist", + "alt-b": ["terminal::SendText", "\u001bb"], + "alt-f": ["terminal::SendText", "\u001bf"], + "alt-.": ["terminal::SendText", "\u001b."], + "ctrl-delete": ["terminal::SendText", "\u001bd"], + // Overrides for conflicting keybindings + "ctrl-b": ["terminal::SendKeystroke", "ctrl-b"], + "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"], + "ctrl-e": ["terminal::SendKeystroke", "ctrl-e"], + "ctrl-o": ["terminal::SendKeystroke", "ctrl-o"], + "ctrl-w": ["terminal::SendKeystroke", "ctrl-w"], + "ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"], + "ctrl-shift-a": "editor::SelectAll", + "find": "buffer_search::Deploy", + "ctrl-shift-f": "buffer_search::Deploy", + "ctrl-shift-l": "terminal::Clear", + "ctrl-shift-w": "pane::CloseActiveItem", + "up": ["terminal::SendKeystroke", "up"], + "pageup": ["terminal::SendKeystroke", "pageup"], + "down": ["terminal::SendKeystroke", "down"], + "pagedown": ["terminal::SendKeystroke", "pagedown"], + "escape": ["terminal::SendKeystroke", "escape"], + "enter": ["terminal::SendKeystroke", "enter"], + "shift-pageup": "terminal::ScrollPageUp", + "shift-pagedown": "terminal::ScrollPageDown", + "shift-up": "terminal::ScrollLineUp", + "shift-down": "terminal::ScrollLineDown", + "shift-home": "terminal::ScrollToTop", + "shift-end": "terminal::ScrollToBottom", + "ctrl-shift-space": "terminal::ToggleViMode", + "ctrl-shift-r": "terminal::RerunTask", + "ctrl-alt-r": "terminal::RerunTask", + "alt-t": "terminal::RerunTask" + } + }, + { + "context": "ZedPredictModal", + "use_key_equivalents": true, + "bindings": { + "escape": "menu::Cancel" + } + }, + { + "context": "ConfigureContextServerModal > Editor", + "use_key_equivalents": true, + "bindings": { + "escape": "menu::Cancel", + "enter": "editor::Newline", + "ctrl-enter": "menu::Confirm" + } + }, + { + "context": "OnboardingAiConfigurationModal", + "use_key_equivalents": true, + "bindings": { + "escape": "menu::Cancel" + } + }, + { + "context": "Diagnostics", + "use_key_equivalents": true, + "bindings": { + "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh" + } + }, + { + "context": "DebugConsole > Editor", + "use_key_equivalents": true, + "bindings": { + "enter": "menu::Confirm", + "alt-enter": "console::WatchExpression" + } + }, + { + "context": "RunModal", + "use_key_equivalents": true, + "bindings": { + "ctrl-tab": "pane::ActivateNextItem", + "ctrl-shift-tab": "pane::ActivatePreviousItem" + } + }, + { + "context": "MarkdownPreview", + "use_key_equivalents": true, + "bindings": { + "pageup": "markdown::MovePageUp", + "pagedown": "markdown::MovePageDown" + } + }, + { + "context": "KeymapEditor", + "use_key_equivalents": true, + "bindings": { + "ctrl-f": "search::FocusSearch", + "alt-find": "keymap_editor::ToggleKeystrokeSearch", + "alt-f": "keymap_editor::ToggleKeystrokeSearch", + "alt-c": "keymap_editor::ToggleConflictFilter", + "enter": "keymap_editor::EditBinding", + "alt-enter": "keymap_editor::CreateBinding", + "ctrl-c": "keymap_editor::CopyAction", + "ctrl-shift-c": "keymap_editor::CopyContext", + "ctrl-t": "keymap_editor::ShowMatchingKeybinds" + } + }, + { + "context": "KeystrokeInput", + "use_key_equivalents": true, + "bindings": { + "enter": "keystroke_input::StartRecording", + "escape escape escape": "keystroke_input::StopRecording", + "delete": "keystroke_input::ClearKeystrokes" + } + }, + { + "context": "KeybindEditorModal", + "use_key_equivalents": true, + "bindings": { + "ctrl-enter": "menu::Confirm", + "escape": "menu::Cancel" + } + }, + { + "context": "KeybindEditorModal > Editor", + "use_key_equivalents": true, + "bindings": { + "up": "menu::SelectPrevious", + "down": "menu::SelectNext" + } + }, + { + "context": "Onboarding", + "use_key_equivalents": true, + "bindings": { + "ctrl-1": "onboarding::ActivateBasicsPage", + "ctrl-2": "onboarding::ActivateEditingPage", + "ctrl-3": "onboarding::ActivateAISetupPage", + "ctrl-escape": "onboarding::Finish", + "alt-tab": "onboarding::SignIn", + "shift-alt-a": "onboarding::OpenAccount" + } + } +] diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index c900eb692aee34b13f13f4fb67061b577b28be1d..c8c3dc54b76085707c0491eab683ff954a483bf9 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -19,6 +19,10 @@ static KEYMAP_LINUX: LazyLock = LazyLock::new(|| { load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap") }); +static KEYMAP_WINDOWS: LazyLock = LazyLock::new(|| { + load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap") +}); + static ALL_ACTIONS: LazyLock> = LazyLock::new(dump_all_gpui_actions); const FRONT_MATTER_COMMENT: &str = ""; @@ -216,6 +220,7 @@ fn find_binding(os: &str, action: &str) -> Option { let keymap = match os { "macos" => &KEYMAP_MACOS, "linux" | "freebsd" => &KEYMAP_LINUX, + "windows" => &KEYMAP_WINDOWS, _ => unreachable!("Not a valid OS: {}", os), }; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 29e009fdf8d5a8c06d12e36253db59886dd0b9be..80680ae9c00999bd91eb1ad66971259f587752ac 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2588,7 +2588,7 @@ impl Editor { || binding .keystrokes() .first() - .is_some_and(|keystroke| keystroke.modifiers.modified()) + .is_some_and(|keystroke| keystroke.display_modifiers.modified()) })) } @@ -7686,16 +7686,16 @@ impl Editor { .keystroke() { modifiers_held = modifiers_held - || (&accept_keystroke.modifiers == modifiers - && accept_keystroke.modifiers.modified()); + || (&accept_keystroke.display_modifiers == modifiers + && accept_keystroke.display_modifiers.modified()); }; if let Some(accept_partial_keystroke) = self .accept_edit_prediction_keybind(true, window, cx) .keystroke() { modifiers_held = modifiers_held - || (&accept_partial_keystroke.modifiers == modifiers - && accept_partial_keystroke.modifiers.modified()); + || (&accept_partial_keystroke.display_modifiers == modifiers + && accept_partial_keystroke.display_modifiers.modified()); } if modifiers_held { @@ -9044,7 +9044,7 @@ impl Editor { let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; - let modifiers_color = if accept_keystroke.modifiers == window.modifiers() { + let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() { Color::Accent } else { Color::Muted @@ -9056,19 +9056,19 @@ impl Editor { .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .text_size(TextSize::XSmall.rems(cx)) .child(h_flex().children(ui::render_modifiers( - &accept_keystroke.modifiers, + &accept_keystroke.display_modifiers, PlatformStyle::platform(), Some(modifiers_color), Some(IconSize::XSmall.rems().into()), true, ))) .when(is_platform_style_mac, |parent| { - parent.child(accept_keystroke.key.clone()) + parent.child(accept_keystroke.display_key.clone()) }) .when(!is_platform_style_mac, |parent| { parent.child( Key::new( - util::capitalize(&accept_keystroke.key), + util::capitalize(&accept_keystroke.display_key), Some(Color::Default), ) .size(Some(IconSize::XSmall.rems().into())), @@ -9171,7 +9171,7 @@ impl Editor { max_width: Pixels, cursor_point: Point, style: &EditorStyle, - accept_keystroke: Option<&gpui::Keystroke>, + accept_keystroke: Option<&gpui::KeybindingKeystroke>, _window: &Window, cx: &mut Context, ) -> Option { @@ -9249,7 +9249,7 @@ impl Editor { accept_keystroke.as_ref(), |el, accept_keystroke| { el.child(h_flex().children(ui::render_modifiers( - &accept_keystroke.modifiers, + &accept_keystroke.display_modifiers, PlatformStyle::platform(), Some(Color::Default), Some(IconSize::XSmall.rems().into()), @@ -9319,7 +9319,7 @@ impl Editor { .child(completion), ) .when_some(accept_keystroke, |el, accept_keystroke| { - if !accept_keystroke.modifiers.modified() { + if !accept_keystroke.display_modifiers.modified() { return el; } @@ -9338,7 +9338,7 @@ impl Editor { .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .when(is_platform_style_mac, |parent| parent.gap_1()) .child(h_flex().children(ui::render_modifiers( - &accept_keystroke.modifiers, + &accept_keystroke.display_modifiers, PlatformStyle::platform(), Some(if !has_completion { Color::Muted diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4f3580da07750db5241fed9a4f313c5a191b36e6..91034829f7896600e690d8438bf7de23d4d19983 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -43,10 +43,10 @@ use gpui::{ Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, - Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, - ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, - TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, + KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, + ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, + Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black, }; @@ -7150,7 +7150,7 @@ fn header_jump_data( pub struct AcceptEditPredictionBinding(pub(crate) Option); impl AcceptEditPredictionBinding { - pub fn keystroke(&self) -> Option<&Keystroke> { + pub fn keystroke(&self) -> Option<&KeybindingKeystroke> { if let Some(binding) = self.0.as_ref() { match &binding.keystrokes() { [keystroke, ..] => Some(keystroke), diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index bbd59fa7bc1276bedac8a17e9fe947a7211172eb..b59d7e717ad9dadc222e36fb54f2cc0d01466b75 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -37,10 +37,10 @@ use crate::{ AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId, EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext, Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, - PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle, - PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, - SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, - WindowHandle, WindowId, WindowInvalidator, + PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder, + PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle, + Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, + Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator, colors::{Colors, GlobalColors}, current_platform, hash, init_app_menus, }; @@ -263,6 +263,7 @@ pub struct App { pub(crate) focus_handles: Arc, pub(crate) keymap: Rc>, pub(crate) keyboard_layout: Box, + pub(crate) keyboard_mapper: Rc, pub(crate) global_action_listeners: FxHashMap>>, pending_effects: VecDeque, @@ -312,6 +313,7 @@ impl App { let text_system = Arc::new(TextSystem::new(platform.text_system())); let entities = EntityMap::new(); let keyboard_layout = platform.keyboard_layout(); + let keyboard_mapper = platform.keyboard_mapper(); let app = Rc::new_cyclic(|this| AppCell { app: RefCell::new(App { @@ -337,6 +339,7 @@ impl App { focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), keymap: Rc::new(RefCell::new(Keymap::default())), keyboard_layout, + keyboard_mapper, global_action_listeners: FxHashMap::default(), pending_effects: VecDeque::new(), pending_notifications: FxHashSet::default(), @@ -376,6 +379,7 @@ impl App { if let Some(app) = app.upgrade() { let cx = &mut app.borrow_mut(); cx.keyboard_layout = cx.platform.keyboard_layout(); + cx.keyboard_mapper = cx.platform.keyboard_mapper(); cx.keyboard_layout_observers .clone() .retain(&(), move |callback| (callback)(cx)); @@ -424,6 +428,11 @@ impl App { self.keyboard_layout.as_ref() } + /// Get the current keyboard mapper. + pub fn keyboard_mapper(&self) -> &Rc { + &self.keyboard_mapper + } + /// Invokes a handler when the current keyboard layout changes pub fn on_keyboard_layout_change(&self, mut callback: F) -> Subscription where diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 757205fcc3d2a886744582769951b94abf754352..b3db09d8214d18cae77de4fbeff1ad7f722d83fa 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -4,7 +4,7 @@ mod context; pub use binding::*; pub use context::*; -use crate::{Action, Keystroke, is_no_action}; +use crate::{Action, AsKeystroke, Keystroke, is_no_action}; use collections::{HashMap, HashSet}; use smallvec::SmallVec; use std::any::TypeId; @@ -141,7 +141,7 @@ impl Keymap { /// only. pub fn bindings_for_input( &self, - input: &[Keystroke], + input: &[impl AsKeystroke], context_stack: &[KeyContext], ) -> (SmallVec<[KeyBinding; 1]>, bool) { let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new(); @@ -192,7 +192,6 @@ impl Keymap { (bindings, !pending.is_empty()) } - /// Check if the given binding is enabled, given a certain key context. /// Returns the deepest depth at which the binding matches, or None if it doesn't match. fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option { @@ -639,7 +638,7 @@ mod tests { fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) { let actual = keymap .bindings_for_action(action) - .map(|binding| binding.keystrokes[0].unparse()) + .map(|binding| binding.keystrokes[0].inner.unparse()) .collect::>(); assert_eq!(actual, expected, "{:?}", action); } diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index 729498d153b62b3e250421c82b4bdc05e6c0030f..a7cf9d5c540c74119bfb5c634a086a6259c2e852 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -1,14 +1,15 @@ use std::rc::Rc; -use collections::HashMap; - -use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString}; +use crate::{ + Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate, + KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString, +}; use smallvec::SmallVec; /// A keybinding and its associated metadata, from the keymap. pub struct KeyBinding { pub(crate) action: Box, - pub(crate) keystrokes: SmallVec<[Keystroke; 2]>, + pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>, pub(crate) context_predicate: Option>, pub(crate) meta: Option, /// The json input string used when building the keybinding, if any @@ -32,7 +33,15 @@ impl KeyBinding { pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self { let context_predicate = context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); - Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap() + Self::load( + keystrokes, + Box::new(action), + context_predicate, + false, + None, + &DummyKeyboardMapper, + ) + .unwrap() } /// Load a keybinding from the given raw data. @@ -40,24 +49,22 @@ impl KeyBinding { keystrokes: &str, action: Box, context_predicate: Option>, - key_equivalents: Option<&HashMap>, + use_key_equivalents: bool, action_input: Option, + keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> std::result::Result { - let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes + let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes .split_whitespace() - .map(Keystroke::parse) + .map(|source| { + let keystroke = Keystroke::parse(source)?; + Ok(KeybindingKeystroke::new( + keystroke, + use_key_equivalents, + keyboard_mapper, + )) + }) .collect::>()?; - if let Some(equivalents) = key_equivalents { - for keystroke in keystrokes.iter_mut() { - if keystroke.key.chars().count() == 1 - && let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) - { - keystroke.key = key.to_string(); - } - } - } - Ok(Self { keystrokes, action, @@ -79,13 +86,13 @@ impl KeyBinding { } /// Check if the given keystrokes match this binding. - pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option { + pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option { if self.keystrokes.len() < typed.len() { return None; } for (target, typed) in self.keystrokes.iter().zip(typed.iter()) { - if !typed.should_match(target) { + if !typed.as_keystroke().should_match(target) { return None; } } @@ -94,7 +101,7 @@ impl KeyBinding { } /// Get the keystrokes associated with this binding - pub fn keystrokes(&self) -> &[Keystroke] { + pub fn keystrokes(&self) -> &[KeybindingKeystroke] { self.keystrokes.as_slice() } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 4d2feeaf1d041245b110bf25674fd18145a9a7ee..f64710bc562146f5372f0f26f7e732714350434d 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -231,7 +231,6 @@ pub(crate) trait Platform: 'static { fn on_quit(&self, callback: Box); fn on_reopen(&self, callback: Box); - fn on_keyboard_layout_change(&self, callback: Box); fn set_menus(&self, menus: Vec, keymap: &Keymap); fn get_menus(&self) -> Option> { @@ -251,7 +250,6 @@ pub(crate) trait Platform: 'static { fn on_app_menu_action(&self, callback: Box); fn on_will_open_app_menu(&self, callback: Box); fn on_validate_app_menu_command(&self, callback: Box bool>); - fn keyboard_layout(&self) -> Box; fn compositor_name(&self) -> &'static str { "" @@ -272,6 +270,10 @@ pub(crate) trait Platform: 'static { fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task>; fn read_credentials(&self, url: &str) -> Task)>>>; fn delete_credentials(&self, url: &str) -> Task>; + + fn keyboard_layout(&self) -> Box; + fn keyboard_mapper(&self) -> Rc; + fn on_keyboard_layout_change(&self, callback: Box); } /// A handle to a platform's display, e.g. a monitor or laptop screen. diff --git a/crates/gpui/src/platform/keyboard.rs b/crates/gpui/src/platform/keyboard.rs index e28d7815200800b7e3950c6819e6ef3fc42f0306..10b8620258ecffd41e8018fc539c47812df0fe05 100644 --- a/crates/gpui/src/platform/keyboard.rs +++ b/crates/gpui/src/platform/keyboard.rs @@ -1,3 +1,7 @@ +use collections::HashMap; + +use crate::{KeybindingKeystroke, Keystroke}; + /// A trait for platform-specific keyboard layouts pub trait PlatformKeyboardLayout { /// Get the keyboard layout ID, which should be unique to the layout @@ -5,3 +9,33 @@ pub trait PlatformKeyboardLayout { /// Get the keyboard layout display name fn name(&self) -> &str; } + +/// A trait for platform-specific keyboard mappings +pub trait PlatformKeyboardMapper { + /// Map a key equivalent to its platform-specific representation + fn map_key_equivalent( + &self, + keystroke: Keystroke, + use_key_equivalents: bool, + ) -> KeybindingKeystroke; + /// Get the key equivalents for the current keyboard layout, + /// only used on macOS + fn get_key_equivalents(&self) -> Option<&HashMap>; +} + +/// A dummy implementation of the platform keyboard mapper +pub struct DummyKeyboardMapper; + +impl PlatformKeyboardMapper for DummyKeyboardMapper { + fn map_key_equivalent( + &self, + keystroke: Keystroke, + _use_key_equivalents: bool, + ) -> KeybindingKeystroke { + KeybindingKeystroke::from_keystroke(keystroke) + } + + fn get_key_equivalents(&self) -> Option<&HashMap> { + None + } +} diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 24601eefd6de450622247caaca5ff680c60a3257..6ce17c3a01cd1eeef1d49e44c80b9e81f045b71b 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -5,6 +5,14 @@ use std::{ fmt::{Display, Write}, }; +use crate::PlatformKeyboardMapper; + +/// This is a helper trait so that we can simplify the implementation of some functions +pub trait AsKeystroke { + /// Returns the GPUI representation of the keystroke. + fn as_keystroke(&self) -> &Keystroke; +} + /// A keystroke and associated metadata generated by the platform #[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] pub struct Keystroke { @@ -24,6 +32,17 @@ pub struct Keystroke { pub key_char: Option, } +/// Represents a keystroke that can be used in keybindings and displayed to the user. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeybindingKeystroke { + /// The GPUI representation of the keystroke. + pub inner: Keystroke, + /// The modifiers to display. + pub display_modifiers: Modifiers, + /// The key to display. + pub display_key: String, +} + /// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use /// markdown to display it. #[derive(Debug)] @@ -58,7 +77,7 @@ impl Keystroke { /// /// This method assumes that `self` was typed and `target' is in the keymap, and checks /// both possibilities for self against the target. - pub fn should_match(&self, target: &Keystroke) -> bool { + pub fn should_match(&self, target: &KeybindingKeystroke) -> bool { #[cfg(not(target_os = "windows"))] if let Some(key_char) = self .key_char @@ -71,7 +90,7 @@ impl Keystroke { ..Default::default() }; - if &target.key == key_char && target.modifiers == ime_modifiers { + if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers { return true; } } @@ -83,12 +102,12 @@ impl Keystroke { .filter(|key_char| key_char != &&self.key) { // On Windows, if key_char is set, then the typed keystroke produced the key_char - if &target.key == key_char && target.modifiers == Modifiers::none() { + if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() { return true; } } - target.modifiers == self.modifiers && target.key == self.key + target.inner.modifiers == self.modifiers && target.inner.key == self.key } /// key syntax is: @@ -200,31 +219,7 @@ impl Keystroke { /// Produces a representation of this key that Parse can understand. pub fn unparse(&self) -> String { - let mut str = String::new(); - if self.modifiers.function { - str.push_str("fn-"); - } - if self.modifiers.control { - str.push_str("ctrl-"); - } - if self.modifiers.alt { - str.push_str("alt-"); - } - if self.modifiers.platform { - #[cfg(target_os = "macos")] - str.push_str("cmd-"); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - str.push_str("super-"); - - #[cfg(target_os = "windows")] - str.push_str("win-"); - } - if self.modifiers.shift { - str.push_str("shift-"); - } - str.push_str(&self.key); - str + unparse(&self.modifiers, &self.key) } /// Returns true if this keystroke left @@ -266,6 +261,32 @@ impl Keystroke { } } +impl KeybindingKeystroke { + /// Create a new keybinding keystroke from the given keystroke + pub fn new( + inner: Keystroke, + use_key_equivalents: bool, + keyboard_mapper: &dyn PlatformKeyboardMapper, + ) -> Self { + keyboard_mapper.map_key_equivalent(inner, use_key_equivalents) + } + + pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self { + let key = keystroke.key.clone(); + let modifiers = keystroke.modifiers; + KeybindingKeystroke { + inner: keystroke, + display_modifiers: modifiers, + display_key: key, + } + } + + /// Produces a representation of this key that Parse can understand. + pub fn unparse(&self) -> String { + unparse(&self.display_modifiers, &self.display_key) + } +} + fn is_printable_key(key: &str) -> bool { !matches!( key, @@ -322,65 +343,15 @@ fn is_printable_key(key: &str) -> bool { impl std::fmt::Display for Keystroke { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.modifiers.control { - #[cfg(target_os = "macos")] - f.write_char('^')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "ctrl-")?; - } - if self.modifiers.alt { - #[cfg(target_os = "macos")] - f.write_char('⌥')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "alt-")?; - } - if self.modifiers.platform { - #[cfg(target_os = "macos")] - f.write_char('⌘')?; - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - f.write_char('❖')?; - - #[cfg(target_os = "windows")] - f.write_char('⊞')?; - } - if self.modifiers.shift { - #[cfg(target_os = "macos")] - f.write_char('⇧')?; + display_modifiers(&self.modifiers, f)?; + display_key(&self.key, f) + } +} - #[cfg(not(target_os = "macos"))] - write!(f, "shift-")?; - } - let key = match self.key.as_str() { - #[cfg(target_os = "macos")] - "backspace" => '⌫', - #[cfg(target_os = "macos")] - "up" => '↑', - #[cfg(target_os = "macos")] - "down" => '↓', - #[cfg(target_os = "macos")] - "left" => '←', - #[cfg(target_os = "macos")] - "right" => '→', - #[cfg(target_os = "macos")] - "tab" => '⇥', - #[cfg(target_os = "macos")] - "escape" => '⎋', - #[cfg(target_os = "macos")] - "shift" => '⇧', - #[cfg(target_os = "macos")] - "control" => '⌃', - #[cfg(target_os = "macos")] - "alt" => '⌥', - #[cfg(target_os = "macos")] - "platform" => '⌘', - - key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(), - key => return f.write_str(key), - }; - f.write_char(key) +impl std::fmt::Display for KeybindingKeystroke { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + display_modifiers(&self.display_modifiers, f)?; + display_key(&self.display_key, f) } } @@ -600,3 +571,110 @@ pub struct Capslock { #[serde(default)] pub on: bool, } + +impl AsKeystroke for Keystroke { + fn as_keystroke(&self) -> &Keystroke { + self + } +} + +impl AsKeystroke for KeybindingKeystroke { + fn as_keystroke(&self) -> &Keystroke { + &self.inner + } +} + +fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if modifiers.control { + #[cfg(target_os = "macos")] + f.write_char('^')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "ctrl-")?; + } + if modifiers.alt { + #[cfg(target_os = "macos")] + f.write_char('⌥')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "alt-")?; + } + if modifiers.platform { + #[cfg(target_os = "macos")] + f.write_char('⌘')?; + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + f.write_char('❖')?; + + #[cfg(target_os = "windows")] + f.write_char('⊞')?; + } + if modifiers.shift { + #[cfg(target_os = "macos")] + f.write_char('⇧')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "shift-")?; + } + Ok(()) +} + +fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let key = match key { + #[cfg(target_os = "macos")] + "backspace" => '⌫', + #[cfg(target_os = "macos")] + "up" => '↑', + #[cfg(target_os = "macos")] + "down" => '↓', + #[cfg(target_os = "macos")] + "left" => '←', + #[cfg(target_os = "macos")] + "right" => '→', + #[cfg(target_os = "macos")] + "tab" => '⇥', + #[cfg(target_os = "macos")] + "escape" => '⎋', + #[cfg(target_os = "macos")] + "shift" => '⇧', + #[cfg(target_os = "macos")] + "control" => '⌃', + #[cfg(target_os = "macos")] + "alt" => '⌥', + #[cfg(target_os = "macos")] + "platform" => '⌘', + + key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(), + key => return f.write_str(key), + }; + f.write_char(key) +} + +#[inline] +fn unparse(modifiers: &Modifiers, key: &str) -> String { + let mut result = String::new(); + if modifiers.function { + result.push_str("fn-"); + } + if modifiers.control { + result.push_str("ctrl-"); + } + if modifiers.alt { + result.push_str("alt-"); + } + if modifiers.platform { + #[cfg(target_os = "macos")] + result.push_str("cmd-"); + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + result.push_str("super-"); + + #[cfg(target_os = "windows")] + result.push_str("win-"); + } + if modifiers.shift { + result.push_str("shift-"); + } + result.push_str(&key); + result +} diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 3fb1ef45729e1e79f339aa19ad11554e7fce3772..8bd89fc399cb8215748467090b973f3f4ee00759 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State}; use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions, - Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, - Point, Result, Task, WindowAppearance, WindowParams, px, + Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, + PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px, }; #[cfg(any(feature = "wayland", feature = "x11"))] @@ -144,6 +144,10 @@ impl Platform for P { self.keyboard_layout() } + fn keyboard_mapper(&self) -> Rc { + Rc::new(crate::DummyKeyboardMapper) + } + fn on_keyboard_layout_change(&self, callback: Box) { self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback)); } diff --git a/crates/gpui/src/platform/mac/keyboard.rs b/crates/gpui/src/platform/mac/keyboard.rs index a9f6af3edb584157b72b0df25f6389472410883b..14097312468cbb732b46f004dbb0970c26f6e821 100644 --- a/crates/gpui/src/platform/mac/keyboard.rs +++ b/crates/gpui/src/platform/mac/keyboard.rs @@ -1,8 +1,9 @@ +use collections::HashMap; use std::ffi::{CStr, c_void}; use objc::{msg_send, runtime::Object, sel, sel_impl}; -use crate::PlatformKeyboardLayout; +use crate::{KeybindingKeystroke, Keystroke, PlatformKeyboardLayout, PlatformKeyboardMapper}; use super::{ TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID, @@ -14,6 +15,10 @@ pub(crate) struct MacKeyboardLayout { name: String, } +pub(crate) struct MacKeyboardMapper { + key_equivalents: Option>, +} + impl PlatformKeyboardLayout for MacKeyboardLayout { fn id(&self) -> &str { &self.id @@ -24,6 +29,27 @@ impl PlatformKeyboardLayout for MacKeyboardLayout { } } +impl PlatformKeyboardMapper for MacKeyboardMapper { + fn map_key_equivalent( + &self, + mut keystroke: Keystroke, + use_key_equivalents: bool, + ) -> KeybindingKeystroke { + if use_key_equivalents && let Some(key_equivalents) = &self.key_equivalents { + if keystroke.key.chars().count() == 1 + && let Some(key) = key_equivalents.get(&keystroke.key.chars().next().unwrap()) + { + keystroke.key = key.to_string(); + } + } + KeybindingKeystroke::from_keystroke(keystroke) + } + + fn get_key_equivalents(&self) -> Option<&HashMap> { + self.key_equivalents.as_ref() + } +} + impl MacKeyboardLayout { pub(crate) fn new() -> Self { unsafe { @@ -47,3 +73,1428 @@ impl MacKeyboardLayout { } } } + +impl MacKeyboardMapper { + pub(crate) fn new(layout_id: &str) -> Self { + let key_equivalents = get_key_equivalents(layout_id); + + Self { key_equivalents } + } +} + +// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range +// without using option. This means that some of our built in keyboard shortcuts do not work +// for those users. +// +// The way macOS solves this problem is to move shortcuts around so that they are all reachable, +// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct +// +// For example, cmd-> is the "switch window" shortcut because the > key is right above tab. +// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves +// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position +// as cmd-> on a QWERTY layout. +// +// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö +// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard +// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the +// specific key moves) +// +// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every +// possible key combination, and inspecting the UI to see what it rendered. So that's what we did... +// +// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the +// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with: +// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add' +// From there I used multi-cursor to produce this match statement. +fn get_key_equivalents(layout_id: &str) -> Option> { + let mappings: &[(char, char)] = match layout_id { + "com.apple.keylayout.ABC-AZERTY" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.ABC-QWERTZ" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Albanian" => &[ + ('"', '\''), + (':', 'Ç'), + (';', 'ç'), + ('<', ';'), + ('>', ':'), + ('@', '"'), + ('\'', '@'), + ('\\', 'ë'), + ('`', '<'), + ('|', 'Ë'), + ('~', '>'), + ], + "com.apple.keylayout.Austrian" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Azeri" => &[ + ('"', 'Ə'), + (',', 'ç'), + ('.', 'ş'), + ('/', '.'), + (':', 'I'), + (';', 'ı'), + ('<', 'Ç'), + ('>', 'Ş'), + ('?', ','), + ('W', 'Ü'), + ('[', 'ö'), + ('\'', 'ə'), + (']', 'ğ'), + ('w', 'ü'), + ('{', 'Ö'), + ('|', '/'), + ('}', 'Ğ'), + ], + "com.apple.keylayout.Belgian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Brazilian-ABNT2" => &[ + ('"', '`'), + ('/', 'ç'), + ('?', 'Ç'), + ('\'', '´'), + ('\\', '~'), + ('^', '¨'), + ('`', '\''), + ('|', '^'), + ('~', '"'), + ], + "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.British" => &[('#', '£')], + "com.apple.keylayout.Canadian-CSA" => &[ + ('"', 'È'), + ('/', 'é'), + ('<', '\''), + ('>', '"'), + ('?', 'É'), + ('[', '^'), + ('\'', 'è'), + ('\\', 'à'), + (']', 'ç'), + ('`', 'ù'), + ('{', '¨'), + ('|', 'À'), + ('}', 'Ç'), + ('~', 'Ù'), + ], + "com.apple.keylayout.Croatian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Croatian-PC" => &[ + ('"', 'Ć'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Czech" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Czech-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Danish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ø'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', '*'), + ('}', 'Ø'), + ('~', '>'), + ], + "com.apple.keylayout.Faroese" => &[ + ('"', 'Ø'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Æ'), + (';', 'æ'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'å'), + ('\'', 'ø'), + ('\\', '\''), + (']', 'ð'), + ('^', '&'), + ('`', '<'), + ('{', 'Å'), + ('|', '*'), + ('}', 'Ð'), + ('~', '>'), + ], + "com.apple.keylayout.Finnish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishExtended" => &[ + ('"', 'ˆ'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.French" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.French-PC" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('-', ')'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '-'), + ('7', 'è'), + ('8', '_'), + ('9', 'ç'), + (':', '§'), + (';', '!'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '*'), + (']', '$'), + ('^', '6'), + ('_', '°'), + ('`', '<'), + ('{', '¨'), + ('|', 'μ'), + ('}', '£'), + ('~', '>'), + ], + "com.apple.keylayout.French-numerical" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.German" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.German-DIN-2137" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], + "com.apple.keylayout.Hungarian" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Hungarian-QWERTY" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Icelandic" => &[ + ('"', 'Ö'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ð'), + (';', 'ð'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', 'ö'), + ('\\', 'þ'), + (']', '´'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', 'Þ'), + ('}', '´'), + ('~', '>'), + ], + "com.apple.keylayout.Irish" => &[('#', '£')], + "com.apple.keylayout.IrishExtended" => &[('#', '£')], + "com.apple.keylayout.Italian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + (',', ';'), + ('.', ':'), + ('/', ','), + ('0', 'é'), + ('1', '&'), + ('2', '"'), + ('3', '\''), + ('4', '('), + ('5', 'ç'), + ('6', 'è'), + ('7', ')'), + ('8', '£'), + ('9', 'à'), + (':', '!'), + (';', 'ò'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', 'ì'), + ('\'', 'ù'), + ('\\', '§'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '^'), + ('|', '°'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Italian-Pro" => &[ + ('"', '^'), + ('#', '£'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'é'), + (';', 'è'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ò'), + ('\'', 'ì'), + ('\\', 'ù'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ç'), + ('|', '§'), + ('}', '°'), + ('~', '>'), + ], + "com.apple.keylayout.LatinAmerican" => &[ + ('"', '¨'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ñ'), + (';', 'ñ'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', '{'), + ('\'', '´'), + ('\\', '¿'), + (']', '}'), + ('^', '&'), + ('`', '<'), + ('{', '['), + ('|', '¡'), + ('}', ']'), + ('~', '>'), + ], + "com.apple.keylayout.Lithuanian" => &[ + ('!', 'Ą'), + ('#', 'Ę'), + ('$', 'Ė'), + ('%', 'Į'), + ('&', 'Ų'), + ('*', 'Ū'), + ('+', 'Ž'), + ('1', 'ą'), + ('2', 'č'), + ('3', 'ę'), + ('4', 'ė'), + ('5', 'į'), + ('6', 'š'), + ('7', 'ų'), + ('8', 'ū'), + ('=', 'ž'), + ('@', 'Č'), + ('^', 'Š'), + ], + "com.apple.keylayout.Maltese" => &[ + ('#', '£'), + ('[', 'ġ'), + (']', 'ħ'), + ('`', 'ż'), + ('{', 'Ġ'), + ('}', 'Ħ'), + ('~', 'Ż'), + ], + "com.apple.keylayout.NorthernSami" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Norwegian" => &[ + ('"', '^'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianExtended" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\\', '@'), + (']', 'æ'), + ('`', '<'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.Polish" => &[ + ('!', '§'), + ('"', 'ę'), + ('#', '!'), + ('$', '?'), + ('%', '+'), + ('&', ':'), + ('(', '/'), + (')', '"'), + ('*', '_'), + ('+', ']'), + (',', '.'), + ('.', ','), + ('/', 'ż'), + (':', 'Ł'), + (';', 'ł'), + ('<', 'ś'), + ('=', '['), + ('>', 'ń'), + ('?', 'Ż'), + ('@', '%'), + ('[', 'ó'), + ('\'', 'ą'), + ('\\', ';'), + (']', '('), + ('^', '='), + ('_', 'ć'), + ('`', '<'), + ('{', 'ź'), + ('|', '$'), + ('}', ')'), + ('~', '>'), + ], + "com.apple.keylayout.Portuguese" => &[ + ('"', '`'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'ª'), + (';', 'º'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ç'), + ('\'', '´'), + (']', '~'), + ('^', '&'), + ('`', '<'), + ('{', 'Ç'), + ('}', '^'), + ('~', '>'), + ], + "com.apple.keylayout.Sami-PC" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Serbian-Latin" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Slovak" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovak-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovenian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish" => &[ + ('!', '¡'), + ('"', '¨'), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '!'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '/'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', ':'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish-ISO" => &[ + ('"', '¨'), + ('#', '·'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '"'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '&'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', '`'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish-Pro" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwedishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissFrench" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'ü'), + (';', 'è'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'é'), + ('\'', '^'), + ('\\', '$'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ö'), + ('|', '£'), + ('}', 'ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissGerman" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'è'), + (';', 'ü'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '^'), + ('\\', '$'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'é'), + ('|', '£'), + ('}', 'à'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish" => &[ + ('"', '-'), + ('#', '"'), + ('$', '\''), + ('%', '('), + ('&', ')'), + ('(', '%'), + (')', ':'), + ('*', '_'), + (',', 'ö'), + ('-', 'ş'), + ('.', 'ç'), + ('/', '.'), + (':', '$'), + ('<', 'Ö'), + ('>', 'Ç'), + ('@', '*'), + ('[', 'ğ'), + ('\'', ','), + ('\\', 'ü'), + (']', 'ı'), + ('^', '/'), + ('_', 'Ş'), + ('`', '<'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-QWERTY-PC" => &[ + ('"', 'I'), + ('#', '^'), + ('$', '+'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', ':'), + (',', 'ö'), + ('.', 'ç'), + ('/', '*'), + (':', 'Ş'), + (';', 'ş'), + ('<', 'Ö'), + ('=', '.'), + ('>', 'Ç'), + ('@', '\''), + ('[', 'ğ'), + ('\'', 'ı'), + ('\\', ','), + (']', 'ü'), + ('^', '&'), + ('`', '<'), + ('{', 'Ğ'), + ('|', ';'), + ('}', 'Ü'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-Standard" => &[ + ('"', 'Ş'), + ('#', '^'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (',', '.'), + ('.', ','), + (':', 'Ç'), + (';', 'ç'), + ('<', ':'), + ('=', '*'), + ('>', ';'), + ('@', '"'), + ('[', 'ğ'), + ('\'', 'ş'), + ('\\', 'ü'), + (']', 'ı'), + ('^', '&'), + ('`', 'ö'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', 'Ö'), + ], + "com.apple.keylayout.Turkmen" => &[ + ('C', 'Ç'), + ('Q', 'Ä'), + ('V', 'Ý'), + ('X', 'Ü'), + ('[', 'ň'), + ('\\', 'ş'), + (']', 'ö'), + ('^', '№'), + ('`', 'ž'), + ('c', 'ç'), + ('q', 'ä'), + ('v', 'ý'), + ('x', 'ü'), + ('{', 'Ň'), + ('|', 'Ş'), + ('}', 'Ö'), + ('~', 'Ž'), + ], + "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.Welsh" => &[('#', '£')], + + _ => return None, + }; + + Some(HashMap::from_iter(mappings.iter().cloned())) +} diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 832550dc46281c56749aa8f6bc4d59a041c9a00d..30453def00bbacf7e8cc820020bf8ec831afc514 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1,5 +1,5 @@ use super::{ - BoolExt, MacKeyboardLayout, + BoolExt, MacKeyboardLayout, MacKeyboardMapper, attributed_string::{NSAttributedString, NSMutableAttributedString}, events::key_to_native, renderer, @@ -8,8 +8,9 @@ use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString, CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher, MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform, - PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, - SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash, + PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, + PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, + hash, }; use anyhow::{Context as _, anyhow}; use block::ConcreteBlock; @@ -171,6 +172,7 @@ pub(crate) struct MacPlatformState { finish_launching: Option>, dock_menu: Option, menus: Option>, + keyboard_mapper: Rc, } impl Default for MacPlatform { @@ -189,6 +191,9 @@ impl MacPlatform { #[cfg(not(feature = "font-kit"))] let text_system = Arc::new(crate::NoopTextSystem::new()); + let keyboard_layout = MacKeyboardLayout::new(); + let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); + Self(Mutex::new(MacPlatformState { headless, text_system, @@ -209,6 +214,7 @@ impl MacPlatform { dock_menu: None, on_keyboard_layout_change: None, menus: None, + keyboard_mapper, })) } @@ -348,19 +354,19 @@ impl MacPlatform { let mut mask = NSEventModifierFlags::empty(); for (modifier, flag) in &[ ( - keystroke.modifiers.platform, + keystroke.display_modifiers.platform, NSEventModifierFlags::NSCommandKeyMask, ), ( - keystroke.modifiers.control, + keystroke.display_modifiers.control, NSEventModifierFlags::NSControlKeyMask, ), ( - keystroke.modifiers.alt, + keystroke.display_modifiers.alt, NSEventModifierFlags::NSAlternateKeyMask, ), ( - keystroke.modifiers.shift, + keystroke.display_modifiers.shift, NSEventModifierFlags::NSShiftKeyMask, ), ] { @@ -373,7 +379,7 @@ impl MacPlatform { .initWithTitle_action_keyEquivalent_( ns_string(name), selector, - ns_string(key_to_native(&keystroke.key).as_ref()), + ns_string(key_to_native(&keystroke.display_key).as_ref()), ) .autorelease(); if Self::os_version() >= SemanticVersion::new(12, 0, 0) { @@ -882,6 +888,10 @@ impl Platform for MacPlatform { Box::new(MacKeyboardLayout::new()) } + fn keyboard_mapper(&self) -> Rc { + self.0.lock().keyboard_mapper.clone() + } + fn app_path(&self) -> Result { unsafe { let bundle: id = NSBundle::mainBundle(); @@ -1393,6 +1403,8 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) { extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) { let platform = unsafe { get_mac_platform(this) }; let mut lock = platform.0.lock(); + let keyboard_layout = MacKeyboardLayout::new(); + lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); if let Some(mut callback) = lock.on_keyboard_layout_change.take() { drop(lock); callback(); diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 00afcd81b599cc53f12005062e4b87abd9c30e38..15b909199fbd53b974e6a140f3223641dc0ac6ae 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -1,8 +1,9 @@ use crate::{ AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, - ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout, - PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, - SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size, + DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, + PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton, + ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task, + TestDisplay, TestWindow, WindowAppearance, WindowParams, size, }; use anyhow::Result; use collections::VecDeque; @@ -237,6 +238,10 @@ impl Platform for TestPlatform { Box::new(TestKeyboardLayout) } + fn keyboard_mapper(&self) -> Rc { + Rc::new(DummyKeyboardMapper) + } + fn on_keyboard_layout_change(&self, _: Box) {} fn run(&self, _on_finish_launching: Box) { diff --git a/crates/gpui/src/platform/windows/keyboard.rs b/crates/gpui/src/platform/windows/keyboard.rs index 371feb70c25ab593ce612c7a90381a4cffdeff7d..0eb97fbb0c500d6481b6add7d6e716c795e69fa2 100644 --- a/crates/gpui/src/platform/windows/keyboard.rs +++ b/crates/gpui/src/platform/windows/keyboard.rs @@ -1,22 +1,31 @@ use anyhow::Result; +use collections::HashMap; use windows::Win32::UI::{ Input::KeyboardAndMouse::{ - GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0, - VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU, - VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102, - VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT, + GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode, + VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, + VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, + VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT, }, WindowsAndMessaging::KL_NAMELENGTH, }; use windows_core::HSTRING; -use crate::{Modifiers, PlatformKeyboardLayout}; +use crate::{ + KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, +}; pub(crate) struct WindowsKeyboardLayout { id: String, name: String, } +pub(crate) struct WindowsKeyboardMapper { + key_to_vkey: HashMap, + vkey_to_key: HashMap, + vkey_to_shifted: HashMap, +} + impl PlatformKeyboardLayout for WindowsKeyboardLayout { fn id(&self) -> &str { &self.id @@ -27,6 +36,65 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout { } } +impl PlatformKeyboardMapper for WindowsKeyboardMapper { + fn map_key_equivalent( + &self, + mut keystroke: Keystroke, + use_key_equivalents: bool, + ) -> KeybindingKeystroke { + let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents) + else { + return KeybindingKeystroke::from_keystroke(keystroke); + }; + if shifted_key && keystroke.modifiers.shift { + log::warn!( + "Keystroke '{}' has both shift and a shifted key, this is likely a bug", + keystroke.key + ); + } + + let shift = shifted_key || keystroke.modifiers.shift; + keystroke.modifiers.shift = false; + + let Some(key) = self.vkey_to_key.get(&vkey).cloned() else { + log::error!( + "Failed to map key equivalent '{:?}' to a valid key", + keystroke + ); + return KeybindingKeystroke::from_keystroke(keystroke); + }; + + keystroke.key = if shift { + let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else { + log::error!( + "Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key", + keystroke, + vkey + ); + return KeybindingKeystroke::from_keystroke(keystroke); + }; + shifted_key + } else { + key.clone() + }; + + let modifiers = Modifiers { + shift, + ..keystroke.modifiers + }; + + KeybindingKeystroke { + inner: keystroke, + display_modifiers: modifiers, + display_key: key, + } + } + + fn get_key_equivalents(&self) -> Option<&HashMap> { + None + } +} + impl WindowsKeyboardLayout { pub(crate) fn new() -> Result { let mut buffer = [0u16; KL_NAMELENGTH as usize]; @@ -48,6 +116,41 @@ impl WindowsKeyboardLayout { } } +impl WindowsKeyboardMapper { + pub(crate) fn new() -> Self { + let mut key_to_vkey = HashMap::default(); + let mut vkey_to_key = HashMap::default(); + let mut vkey_to_shifted = HashMap::default(); + for vkey in CANDIDATE_VKEYS { + if let Some(key) = get_key_from_vkey(*vkey) { + key_to_vkey.insert(key.clone(), (vkey.0, false)); + vkey_to_key.insert(vkey.0, key); + } + let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) }; + if scan_code == 0 { + continue; + } + if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) { + key_to_vkey.insert(shifted_key.clone(), (vkey.0, true)); + vkey_to_shifted.insert(vkey.0, shifted_key); + } + } + Self { + key_to_vkey, + vkey_to_key, + vkey_to_shifted, + } + } + + fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> { + if use_key_equivalents { + get_vkey_from_key_with_us_layout(key) + } else { + self.key_to_vkey.get(key).cloned() + } + } +} + pub(crate) fn get_keystroke_key( vkey: VIRTUAL_KEY, scan_code: u32, @@ -140,3 +243,134 @@ pub(crate) fn generate_key_char( _ => None, } } + +fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> { + match key { + // ` => VK_OEM_3 + "`" => Some((VK_OEM_3.0, false)), + "~" => Some((VK_OEM_3.0, true)), + "1" => Some((VK_1.0, false)), + "!" => Some((VK_1.0, true)), + "2" => Some((VK_2.0, false)), + "@" => Some((VK_2.0, true)), + "3" => Some((VK_3.0, false)), + "#" => Some((VK_3.0, true)), + "4" => Some((VK_4.0, false)), + "$" => Some((VK_4.0, true)), + "5" => Some((VK_5.0, false)), + "%" => Some((VK_5.0, true)), + "6" => Some((VK_6.0, false)), + "^" => Some((VK_6.0, true)), + "7" => Some((VK_7.0, false)), + "&" => Some((VK_7.0, true)), + "8" => Some((VK_8.0, false)), + "*" => Some((VK_8.0, true)), + "9" => Some((VK_9.0, false)), + "(" => Some((VK_9.0, true)), + "0" => Some((VK_0.0, false)), + ")" => Some((VK_0.0, true)), + "-" => Some((VK_OEM_MINUS.0, false)), + "_" => Some((VK_OEM_MINUS.0, true)), + "=" => Some((VK_OEM_PLUS.0, false)), + "+" => Some((VK_OEM_PLUS.0, true)), + "[" => Some((VK_OEM_4.0, false)), + "{" => Some((VK_OEM_4.0, true)), + "]" => Some((VK_OEM_6.0, false)), + "}" => Some((VK_OEM_6.0, true)), + "\\" => Some((VK_OEM_5.0, false)), + "|" => Some((VK_OEM_5.0, true)), + ";" => Some((VK_OEM_1.0, false)), + ":" => Some((VK_OEM_1.0, true)), + "'" => Some((VK_OEM_7.0, false)), + "\"" => Some((VK_OEM_7.0, true)), + "," => Some((VK_OEM_COMMA.0, false)), + "<" => Some((VK_OEM_COMMA.0, true)), + "." => Some((VK_OEM_PERIOD.0, false)), + ">" => Some((VK_OEM_PERIOD.0, true)), + "/" => Some((VK_OEM_2.0, false)), + "?" => Some((VK_OEM_2.0, true)), + _ => None, + } +} + +const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[ + VK_OEM_3, + VK_OEM_MINUS, + VK_OEM_PLUS, + VK_OEM_4, + VK_OEM_5, + VK_OEM_6, + VK_OEM_1, + VK_OEM_7, + VK_OEM_COMMA, + VK_OEM_PERIOD, + VK_OEM_2, + VK_OEM_102, + VK_OEM_8, + VK_ABNT_C1, + VK_0, + VK_1, + VK_2, + VK_3, + VK_4, + VK_5, + VK_6, + VK_7, + VK_8, + VK_9, +]; + +#[cfg(test)] +mod tests { + use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper}; + + #[test] + fn test_keyboard_mapper() { + let mapper = WindowsKeyboardMapper::new(); + + // Normal case + let keystroke = Keystroke { + modifiers: Modifiers::control(), + key: "a".to_string(), + key_char: None, + }; + let mapped = mapper.map_key_equivalent(keystroke.clone(), true); + assert_eq!(mapped.inner, keystroke); + assert_eq!(mapped.display_key, "a"); + assert_eq!(mapped.display_modifiers, Modifiers::control()); + + // Shifted case, ctrl-$ + let keystroke = Keystroke { + modifiers: Modifiers::control(), + key: "$".to_string(), + key_char: None, + }; + let mapped = mapper.map_key_equivalent(keystroke.clone(), true); + assert_eq!(mapped.inner, keystroke); + assert_eq!(mapped.display_key, "4"); + assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); + + // Shifted case, but shift is true + let keystroke = Keystroke { + modifiers: Modifiers::control_shift(), + key: "$".to_string(), + key_char: None, + }; + let mapped = mapper.map_key_equivalent(keystroke, true); + assert_eq!(mapped.inner.modifiers, Modifiers::control()); + assert_eq!(mapped.display_key, "4"); + assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); + + // Windows style + let keystroke = Keystroke { + modifiers: Modifiers::control_shift(), + key: "4".to_string(), + key_char: None, + }; + let mapped = mapper.map_key_equivalent(keystroke, true); + assert_eq!(mapped.inner.modifiers, Modifiers::control()); + assert_eq!(mapped.inner.key, "$"); + assert_eq!(mapped.display_key, "4"); + assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); + } +} diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 6202e05fb3b26f10ba8fdf365a185dccbc6ae2ed..5ac2be2f238e9c6986a5496af5e69a6ef658c0f7 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -351,6 +351,10 @@ impl Platform for WindowsPlatform { ) } + fn keyboard_mapper(&self) -> Rc { + Rc::new(WindowsKeyboardMapper::new()) + } + fn on_keyboard_layout_change(&self, callback: Box) { self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback); } diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 057259d114f88f785c1a016d82f443b2ee2be644..4140713544ed2b22413f909ac45989de8df4e706 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -4,7 +4,6 @@ use gpui::{ }; use itertools::Itertools; use serde_json::json; -use settings::get_key_equivalents; use ui::{Button, ButtonStyle}; use ui::{ ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon, @@ -169,7 +168,8 @@ impl Item for KeyContextView { impl Render for KeyContextView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { use itertools::Itertools; - let key_equivalents = get_key_equivalents(cx.keyboard_layout().id()); + + let key_equivalents = cx.keyboard_mapper().get_key_equivalents(); v_flex() .id("key-context-view") .overflow_scroll() diff --git a/crates/settings/src/key_equivalents.rs b/crates/settings/src/key_equivalents.rs deleted file mode 100644 index 65801375356289f41fa1688cbb32dff4249333d9..0000000000000000000000000000000000000000 --- a/crates/settings/src/key_equivalents.rs +++ /dev/null @@ -1,1424 +0,0 @@ -use collections::HashMap; - -// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range -// without using option. This means that some of our built in keyboard shortcuts do not work -// for those users. -// -// The way macOS solves this problem is to move shortcuts around so that they are all reachable, -// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct -// -// For example, cmd-> is the "switch window" shortcut because the > key is right above tab. -// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves -// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position -// as cmd-> on a QWERTY layout. -// -// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö -// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard -// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the -// specific key moves) -// -// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every -// possible key combination, and inspecting the UI to see what it rendered. So that's what we did... -// -// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the -// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with: -// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add' -// From there I used multi-cursor to produce this match statement. -#[cfg(target_os = "macos")] -pub fn get_key_equivalents(layout: &str) -> Option> { - let mappings: &[(char, char)] = match layout { - "com.apple.keylayout.ABC-AZERTY" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.ABC-QWERTZ" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Albanian" => &[ - ('"', '\''), - (':', 'Ç'), - (';', 'ç'), - ('<', ';'), - ('>', ':'), - ('@', '"'), - ('\'', '@'), - ('\\', 'ë'), - ('`', '<'), - ('|', 'Ë'), - ('~', '>'), - ], - "com.apple.keylayout.Austrian" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Azeri" => &[ - ('"', 'Ə'), - (',', 'ç'), - ('.', 'ş'), - ('/', '.'), - (':', 'I'), - (';', 'ı'), - ('<', 'Ç'), - ('>', 'Ş'), - ('?', ','), - ('W', 'Ü'), - ('[', 'ö'), - ('\'', 'ə'), - (']', 'ğ'), - ('w', 'ü'), - ('{', 'Ö'), - ('|', '/'), - ('}', 'Ğ'), - ], - "com.apple.keylayout.Belgian" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.Brazilian-ABNT2" => &[ - ('"', '`'), - ('/', 'ç'), - ('?', 'Ç'), - ('\'', '´'), - ('\\', '~'), - ('^', '¨'), - ('`', '\''), - ('|', '^'), - ('~', '"'), - ], - "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], - "com.apple.keylayout.British" => &[('#', '£')], - "com.apple.keylayout.Canadian-CSA" => &[ - ('"', 'È'), - ('/', 'é'), - ('<', '\''), - ('>', '"'), - ('?', 'É'), - ('[', '^'), - ('\'', 'è'), - ('\\', 'à'), - (']', 'ç'), - ('`', 'ù'), - ('{', '¨'), - ('|', 'À'), - ('}', 'Ç'), - ('~', 'Ù'), - ], - "com.apple.keylayout.Croatian" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Croatian-PC" => &[ - ('"', 'Ć'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Czech" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ě'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ř'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ů'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', ')'), - ('^', '6'), - ('`', '¨'), - ('{', 'Ú'), - ('}', '('), - ('~', '`'), - ], - "com.apple.keylayout.Czech-QWERTY" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ě'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ř'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ů'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', ')'), - ('^', '6'), - ('`', '¨'), - ('{', 'Ú'), - ('}', '('), - ('~', '`'), - ], - "com.apple.keylayout.Danish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'æ'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ø'), - ('^', '&'), - ('`', '<'), - ('{', 'Æ'), - ('|', '*'), - ('}', 'Ø'), - ('~', '>'), - ], - "com.apple.keylayout.Faroese" => &[ - ('"', 'Ø'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Æ'), - (';', 'æ'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'å'), - ('\'', 'ø'), - ('\\', '\''), - (']', 'ð'), - ('^', '&'), - ('`', '<'), - ('{', 'Å'), - ('|', '*'), - ('}', 'Ð'), - ('~', '>'), - ], - "com.apple.keylayout.Finnish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.FinnishExtended" => &[ - ('"', 'ˆ'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.FinnishSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '@'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.French" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.French-PC" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('-', ')'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '-'), - ('7', 'è'), - ('8', '_'), - ('9', 'ç'), - (':', '§'), - (';', '!'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '*'), - (']', '$'), - ('^', '6'), - ('_', '°'), - ('`', '<'), - ('{', '¨'), - ('|', 'μ'), - ('}', '£'), - ('~', '>'), - ], - "com.apple.keylayout.French-numerical" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.German" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.German-DIN-2137" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], - "com.apple.keylayout.Hungarian" => &[ - ('!', '\''), - ('"', 'Á'), - ('#', '+'), - ('$', '!'), - ('&', '='), - ('(', ')'), - (')', 'Ö'), - ('*', '('), - ('+', 'Ó'), - ('/', 'ü'), - ('0', 'ö'), - (':', 'É'), - (';', 'é'), - ('<', 'Ü'), - ('=', 'ó'), - ('>', ':'), - ('@', '"'), - ('[', 'ő'), - ('\'', 'á'), - ('\\', 'ű'), - (']', 'ú'), - ('^', '/'), - ('`', 'í'), - ('{', 'Ő'), - ('|', 'Ű'), - ('}', 'Ú'), - ('~', 'Í'), - ], - "com.apple.keylayout.Hungarian-QWERTY" => &[ - ('!', '\''), - ('"', 'Á'), - ('#', '+'), - ('$', '!'), - ('&', '='), - ('(', ')'), - (')', 'Ö'), - ('*', '('), - ('+', 'Ó'), - ('/', 'ü'), - ('0', 'ö'), - (':', 'É'), - (';', 'é'), - ('<', 'Ü'), - ('=', 'ó'), - ('>', ':'), - ('@', '"'), - ('[', 'ő'), - ('\'', 'á'), - ('\\', 'ű'), - (']', 'ú'), - ('^', '/'), - ('`', 'í'), - ('{', 'Ő'), - ('|', 'Ű'), - ('}', 'Ú'), - ('~', 'Í'), - ], - "com.apple.keylayout.Icelandic" => &[ - ('"', 'Ö'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Ð'), - (';', 'ð'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'æ'), - ('\'', 'ö'), - ('\\', 'þ'), - (']', '´'), - ('^', '&'), - ('`', '<'), - ('{', 'Æ'), - ('|', 'Þ'), - ('}', '´'), - ('~', '>'), - ], - "com.apple.keylayout.Irish" => &[('#', '£')], - "com.apple.keylayout.IrishExtended" => &[('#', '£')], - "com.apple.keylayout.Italian" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - (',', ';'), - ('.', ':'), - ('/', ','), - ('0', 'é'), - ('1', '&'), - ('2', '"'), - ('3', '\''), - ('4', '('), - ('5', 'ç'), - ('6', 'è'), - ('7', ')'), - ('8', '£'), - ('9', 'à'), - (':', '!'), - (';', 'ò'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', 'ì'), - ('\'', 'ù'), - ('\\', '§'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '^'), - ('|', '°'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.Italian-Pro" => &[ - ('"', '^'), - ('#', '£'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'é'), - (';', 'è'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ò'), - ('\'', 'ì'), - ('\\', 'ù'), - (']', 'à'), - ('^', '&'), - ('`', '<'), - ('{', 'ç'), - ('|', '§'), - ('}', '°'), - ('~', '>'), - ], - "com.apple.keylayout.LatinAmerican" => &[ - ('"', '¨'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Ñ'), - (';', 'ñ'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', '{'), - ('\'', '´'), - ('\\', '¿'), - (']', '}'), - ('^', '&'), - ('`', '<'), - ('{', '['), - ('|', '¡'), - ('}', ']'), - ('~', '>'), - ], - "com.apple.keylayout.Lithuanian" => &[ - ('!', 'Ą'), - ('#', 'Ę'), - ('$', 'Ė'), - ('%', 'Į'), - ('&', 'Ų'), - ('*', 'Ū'), - ('+', 'Ž'), - ('1', 'ą'), - ('2', 'č'), - ('3', 'ę'), - ('4', 'ė'), - ('5', 'į'), - ('6', 'š'), - ('7', 'ų'), - ('8', 'ū'), - ('=', 'ž'), - ('@', 'Č'), - ('^', 'Š'), - ], - "com.apple.keylayout.Maltese" => &[ - ('#', '£'), - ('[', 'ġ'), - (']', 'ħ'), - ('`', 'ż'), - ('{', 'Ġ'), - ('}', 'Ħ'), - ('~', 'Ż'), - ], - "com.apple.keylayout.NorthernSami" => &[ - ('"', 'Ŋ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('Q', 'Á'), - ('W', 'Š'), - ('X', 'Č'), - ('[', 'ø'), - ('\'', 'ŋ'), - ('\\', 'đ'), - (']', 'æ'), - ('^', '&'), - ('`', 'ž'), - ('q', 'á'), - ('w', 'š'), - ('x', 'č'), - ('{', 'Ø'), - ('|', 'Đ'), - ('}', 'Æ'), - ('~', 'Ž'), - ], - "com.apple.keylayout.Norwegian" => &[ - ('"', '^'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\'', '¨'), - ('\\', '@'), - (']', 'æ'), - ('^', '&'), - ('`', '<'), - ('{', 'Ø'), - ('|', '*'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.NorwegianExtended" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\\', '@'), - (']', 'æ'), - ('`', '<'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.NorwegianSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\'', '¨'), - ('\\', '@'), - (']', 'æ'), - ('^', '&'), - ('`', '<'), - ('{', 'Ø'), - ('|', '*'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.Polish" => &[ - ('!', '§'), - ('"', 'ę'), - ('#', '!'), - ('$', '?'), - ('%', '+'), - ('&', ':'), - ('(', '/'), - (')', '"'), - ('*', '_'), - ('+', ']'), - (',', '.'), - ('.', ','), - ('/', 'ż'), - (':', 'Ł'), - (';', 'ł'), - ('<', 'ś'), - ('=', '['), - ('>', 'ń'), - ('?', 'Ż'), - ('@', '%'), - ('[', 'ó'), - ('\'', 'ą'), - ('\\', ';'), - (']', '('), - ('^', '='), - ('_', 'ć'), - ('`', '<'), - ('{', 'ź'), - ('|', '$'), - ('}', ')'), - ('~', '>'), - ], - "com.apple.keylayout.Portuguese" => &[ - ('"', '`'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'ª'), - (';', 'º'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ç'), - ('\'', '´'), - (']', '~'), - ('^', '&'), - ('`', '<'), - ('{', 'Ç'), - ('}', '^'), - ('~', '>'), - ], - "com.apple.keylayout.Sami-PC" => &[ - ('"', 'Ŋ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('Q', 'Á'), - ('W', 'Š'), - ('X', 'Č'), - ('[', 'ø'), - ('\'', 'ŋ'), - ('\\', 'đ'), - (']', 'æ'), - ('^', '&'), - ('`', 'ž'), - ('q', 'á'), - ('w', 'š'), - ('x', 'č'), - ('{', 'Ø'), - ('|', 'Đ'), - ('}', 'Æ'), - ('~', 'Ž'), - ], - "com.apple.keylayout.Serbian-Latin" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Slovak" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ľ'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ť'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ô'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', 'ä'), - ('^', '6'), - ('`', 'ň'), - ('{', 'Ú'), - ('}', 'Ä'), - ('~', 'Ň'), - ], - "com.apple.keylayout.Slovak-QWERTY" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ľ'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ť'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ô'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', 'ä'), - ('^', '6'), - ('`', 'ň'), - ('{', 'Ú'), - ('}', 'Ä'), - ('~', 'Ň'), - ], - "com.apple.keylayout.Slovenian" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Spanish" => &[ - ('!', '¡'), - ('"', '¨'), - ('.', 'ç'), - ('/', '.'), - (':', 'º'), - (';', '´'), - ('<', '¿'), - ('>', 'Ç'), - ('@', '!'), - ('[', 'ñ'), - ('\'', '`'), - ('\\', '\''), - (']', ';'), - ('^', '/'), - ('`', '<'), - ('{', 'Ñ'), - ('|', '"'), - ('}', ':'), - ('~', '>'), - ], - "com.apple.keylayout.Spanish-ISO" => &[ - ('"', '¨'), - ('#', '·'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('.', 'ç'), - ('/', '.'), - (':', 'º'), - (';', '´'), - ('<', '¿'), - ('>', 'Ç'), - ('@', '"'), - ('[', 'ñ'), - ('\'', '`'), - ('\\', '\''), - (']', ';'), - ('^', '&'), - ('`', '<'), - ('{', 'Ñ'), - ('|', '"'), - ('}', '`'), - ('~', '>'), - ], - "com.apple.keylayout.Swedish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Swedish-Pro" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwedishSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '@'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwissFrench" => &[ - ('!', '+'), - ('"', '`'), - ('#', '*'), - ('$', 'ç'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', '!'), - ('/', '\''), - (':', 'ü'), - (';', 'è'), - ('<', ';'), - ('=', '¨'), - ('>', ':'), - ('@', '"'), - ('[', 'é'), - ('\'', '^'), - ('\\', '$'), - (']', 'à'), - ('^', '&'), - ('`', '<'), - ('{', 'ö'), - ('|', '£'), - ('}', 'ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwissGerman" => &[ - ('!', '+'), - ('"', '`'), - ('#', '*'), - ('$', 'ç'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', '!'), - ('/', '\''), - (':', 'è'), - (';', 'ü'), - ('<', ';'), - ('=', '¨'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '^'), - ('\\', '$'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'é'), - ('|', '£'), - ('}', 'à'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish" => &[ - ('"', '-'), - ('#', '"'), - ('$', '\''), - ('%', '('), - ('&', ')'), - ('(', '%'), - (')', ':'), - ('*', '_'), - (',', 'ö'), - ('-', 'ş'), - ('.', 'ç'), - ('/', '.'), - (':', '$'), - ('<', 'Ö'), - ('>', 'Ç'), - ('@', '*'), - ('[', 'ğ'), - ('\'', ','), - ('\\', 'ü'), - (']', 'ı'), - ('^', '/'), - ('_', 'Ş'), - ('`', '<'), - ('{', 'Ğ'), - ('|', 'Ü'), - ('}', 'I'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish-QWERTY-PC" => &[ - ('"', 'I'), - ('#', '^'), - ('$', '+'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', ':'), - (',', 'ö'), - ('.', 'ç'), - ('/', '*'), - (':', 'Ş'), - (';', 'ş'), - ('<', 'Ö'), - ('=', '.'), - ('>', 'Ç'), - ('@', '\''), - ('[', 'ğ'), - ('\'', 'ı'), - ('\\', ','), - (']', 'ü'), - ('^', '&'), - ('`', '<'), - ('{', 'Ğ'), - ('|', ';'), - ('}', 'Ü'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish-Standard" => &[ - ('"', 'Ş'), - ('#', '^'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (',', '.'), - ('.', ','), - (':', 'Ç'), - (';', 'ç'), - ('<', ':'), - ('=', '*'), - ('>', ';'), - ('@', '"'), - ('[', 'ğ'), - ('\'', 'ş'), - ('\\', 'ü'), - (']', 'ı'), - ('^', '&'), - ('`', 'ö'), - ('{', 'Ğ'), - ('|', 'Ü'), - ('}', 'I'), - ('~', 'Ö'), - ], - "com.apple.keylayout.Turkmen" => &[ - ('C', 'Ç'), - ('Q', 'Ä'), - ('V', 'Ý'), - ('X', 'Ü'), - ('[', 'ň'), - ('\\', 'ş'), - (']', 'ö'), - ('^', '№'), - ('`', 'ž'), - ('c', 'ç'), - ('q', 'ä'), - ('v', 'ý'), - ('x', 'ü'), - ('{', 'Ň'), - ('|', 'Ş'), - ('}', 'Ö'), - ('~', 'Ž'), - ], - "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], - "com.apple.keylayout.Welsh" => &[('#', '£')], - - _ => return None, - }; - - Some(HashMap::from_iter(mappings.iter().cloned())) -} - -#[cfg(not(target_os = "macos"))] -pub fn get_key_equivalents(_layout: &str) -> Option> { - None -} diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index ae3f42853ac11af0c1e4510c7ac51bd7379cb657..0e8303c4c17d0b13774b721b7f7bb3565a40cb97 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -3,7 +3,8 @@ use collections::{BTreeMap, HashMap, IndexMap}; use fs::Fs; use gpui::{ Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE, - KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, Keystroke, NoAction, SharedString, + KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke, + NoAction, SharedString, }; use schemars::{JsonSchema, json_schema}; use serde::Deserialize; @@ -211,9 +212,6 @@ impl KeymapFile { } pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult { - let key_equivalents = - crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id()); - if content.is_empty() { return KeymapFileLoadResult::Success { key_bindings: Vec::new(), @@ -255,12 +253,6 @@ impl KeymapFile { } }; - let key_equivalents = if *use_key_equivalents { - key_equivalents.as_ref() - } else { - None - }; - let mut section_errors = String::new(); if !unrecognized_fields.is_empty() { @@ -278,7 +270,7 @@ impl KeymapFile { keystrokes, action, context_predicate.clone(), - key_equivalents, + *use_key_equivalents, cx, ); match result { @@ -336,7 +328,7 @@ impl KeymapFile { keystrokes: &str, action: &KeymapAction, context: Option>, - key_equivalents: Option<&HashMap>, + use_key_equivalents: bool, cx: &App, ) -> std::result::Result { let (build_result, action_input_string) = match &action.0 { @@ -404,8 +396,9 @@ impl KeymapFile { keystrokes, action, context, - key_equivalents, + use_key_equivalents, action_input_string.map(SharedString::from), + cx.keyboard_mapper().as_ref(), ) { Ok(key_binding) => key_binding, Err(InvalidKeystrokeError { keystroke }) => { @@ -607,6 +600,7 @@ impl KeymapFile { mut operation: KeybindUpdateOperation<'a>, mut keymap_contents: String, tab_size: usize, + keyboard_mapper: &dyn gpui::PlatformKeyboardMapper, ) -> Result { match operation { // if trying to replace a keybinding that is not user-defined, treat it as an add operation @@ -646,7 +640,7 @@ impl KeymapFile { .action_value() .context("Failed to generate target action JSON value")?; let Some((index, keystrokes_str)) = - find_binding(&keymap, &target, &target_action_value) + find_binding(&keymap, &target, &target_action_value, keyboard_mapper) else { anyhow::bail!("Failed to find keybinding to remove"); }; @@ -681,7 +675,7 @@ impl KeymapFile { .context("Failed to generate source action JSON value")?; if let Some((index, keystrokes_str)) = - find_binding(&keymap, &target, &target_action_value) + find_binding(&keymap, &target, &target_action_value, keyboard_mapper) { if target.context == source.context { // if we are only changing the keybinding (common case) @@ -781,7 +775,7 @@ impl KeymapFile { } let use_key_equivalents = from.and_then(|from| { let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?; - let (index, _) = find_binding(&keymap, &from, &action_value)?; + let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?; Some(keymap.0[index].use_key_equivalents) }).unwrap_or(false); if use_key_equivalents { @@ -808,6 +802,7 @@ impl KeymapFile { keymap: &'b KeymapFile, target: &KeybindUpdateTarget<'a>, target_action_value: &Value, + keyboard_mapper: &dyn gpui::PlatformKeyboardMapper, ) -> Option<(usize, &'b str)> { let target_context_parsed = KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok(); @@ -823,8 +818,11 @@ impl KeymapFile { for (keystrokes_str, action) in bindings { let Ok(keystrokes) = keystrokes_str .split_whitespace() - .map(Keystroke::parse) - .collect::, _>>() + .map(|source| { + let keystroke = Keystroke::parse(source)?; + Ok(KeybindingKeystroke::new(keystroke, false, keyboard_mapper)) + }) + .collect::, InvalidKeystrokeError>>() else { continue; }; @@ -832,7 +830,7 @@ impl KeymapFile { || !keystrokes .iter() .zip(target.keystrokes) - .all(|(a, b)| a.should_match(b)) + .all(|(a, b)| a.inner.should_match(b)) { continue; } @@ -847,7 +845,7 @@ impl KeymapFile { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum KeybindUpdateOperation<'a> { Replace { /// Describes the keybind to create @@ -916,7 +914,7 @@ impl<'a> KeybindUpdateOperation<'a> { #[derive(Debug, Clone)] pub struct KeybindUpdateTarget<'a> { pub context: Option<&'a str>, - pub keystrokes: &'a [Keystroke], + pub keystrokes: &'a [KeybindingKeystroke], pub action_name: &'a str, pub action_arguments: Option<&'a str>, } @@ -941,6 +939,9 @@ impl<'a> KeybindUpdateTarget<'a> { fn keystrokes_unparsed(&self) -> String { let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8); for keystroke in self.keystrokes { + // The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()` + // here is that, we want the user to use `ctrl-shift-4` instead of `ctrl-$` + // by default on Windows. keystrokes.push_str(&keystroke.unparse()); keystrokes.push(' '); } @@ -959,7 +960,7 @@ impl<'a> KeybindUpdateTarget<'a> { } } -#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum KeybindSource { User, Vim, @@ -1020,7 +1021,7 @@ impl From for KeyBindingMetaIndex { #[cfg(test)] mod tests { - use gpui::Keystroke; + use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke}; use unindent::Unindent; use crate::{ @@ -1049,16 +1050,27 @@ mod tests { operation: KeybindUpdateOperation, expected: impl ToString, ) { - let result = KeymapFile::update_keybinding(operation, input.to_string(), 4) - .expect("Update succeeded"); + let result = KeymapFile::update_keybinding( + operation, + input.to_string(), + 4, + &gpui::DummyKeyboardMapper, + ) + .expect("Update succeeded"); pretty_assertions::assert_eq!(expected.to_string(), result); } #[track_caller] - fn parse_keystrokes(keystrokes: &str) -> Vec { + fn parse_keystrokes(keystrokes: &str) -> Vec { keystrokes .split(' ') - .map(|s| Keystroke::parse(s).expect("Keystrokes valid")) + .map(|s| { + KeybindingKeystroke::new( + Keystroke::parse(s).expect("Keystrokes valid"), + false, + &DummyKeyboardMapper, + ) + }) .collect() } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index b73ab9ae95ae75b3ac9b7a58b663a79235261b5b..1966755d626af6e155440379982af180e9ccbc95 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,6 +1,5 @@ mod base_keymap_setting; mod editable_setting_control; -mod key_equivalents; mod keymap_file; mod settings_file; mod settings_json; @@ -14,7 +13,6 @@ use util::asset_str; pub use base_keymap_setting::*; pub use editable_setting_control::*; -pub use key_equivalents::*; pub use keymap_file::{ KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation, KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult, @@ -89,7 +87,10 @@ pub fn default_settings() -> Cow<'static, str> { #[cfg(target_os = "macos")] pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json"; -#[cfg(not(target_os = "macos"))] +#[cfg(target_os = "windows")] +pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-windows.json"; + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json"; pub fn default_keymap() -> Cow<'static, str> { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 288f59c8e045becabfe24271deac66e3bc4ebe98..76c716600768bb318e2983267b961de440cbcf8f 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -14,9 +14,9 @@ use gpui::{ Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or}, - KeyContext, Keystroke, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful, - StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred, - div, + KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point, + ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, + TextStyleRefinement, WeakEntity, actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; use notifications::status_toast::{StatusToast, ToastIcon}; @@ -174,7 +174,7 @@ impl FilterState { #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] struct ActionMapping { - keystrokes: Vec, + keystrokes: Vec, context: Option, } @@ -236,7 +236,7 @@ struct ConflictState { } type ConflictKeybindMapping = HashMap< - Vec, + Vec, Vec<( Option, Vec, @@ -414,12 +414,14 @@ impl Focusable for KeymapEditor { } } /// Helper function to check if two keystroke sequences match exactly -fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool { +fn keystrokes_match_exactly( + keystrokes1: &[KeybindingKeystroke], + keystrokes2: &[KeybindingKeystroke], +) -> bool { keystrokes1.len() == keystrokes2.len() - && keystrokes1 - .iter() - .zip(keystrokes2) - .all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers) + && keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| { + k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers + }) } impl KeymapEditor { @@ -509,7 +511,7 @@ impl KeymapEditor { self.filter_editor.read(cx).text(cx) } - fn current_keystroke_query(&self, cx: &App) -> Vec { + fn current_keystroke_query(&self, cx: &App) -> Vec { match self.search_mode { SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(), SearchMode::Normal => Default::default(), @@ -530,7 +532,7 @@ impl KeymapEditor { let keystroke_query = keystroke_query .into_iter() - .map(|keystroke| keystroke.unparse()) + .map(|keystroke| keystroke.inner.unparse()) .collect::>() .join(" "); @@ -554,7 +556,7 @@ impl KeymapEditor { async fn update_matches( this: WeakEntity, action_query: String, - keystroke_query: Vec, + keystroke_query: Vec, cx: &mut AsyncApp, ) -> anyhow::Result<()> { let action_query = command_palette::normalize_action_query(&action_query); @@ -603,13 +605,15 @@ impl KeymapEditor { { let query = &keystroke_query[query_cursor]; let keystroke = &keystrokes[keystroke_cursor]; - let matches = - query.modifiers.is_subset_of(&keystroke.modifiers) - && ((query.key.is_empty() - || query.key == keystroke.key) - && query.key_char.as_ref().is_none_or( - |q_kc| q_kc == &keystroke.key, - )); + let matches = query + .inner + .modifiers + .is_subset_of(&keystroke.inner.modifiers) + && ((query.inner.key.is_empty() + || query.inner.key == keystroke.inner.key) + && query.inner.key_char.as_ref().is_none_or( + |q_kc| q_kc == &keystroke.inner.key, + )); if matches { found_count += 1; query_cursor += 1; @@ -678,7 +682,7 @@ impl KeymapEditor { .map(KeybindSource::from_meta) .unwrap_or(KeybindSource::Unknown); - let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx); + let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx); let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx) .vim_mode(source == KeybindSource::Vim); @@ -1202,8 +1206,11 @@ impl KeymapEditor { .read(cx) .get_scrollbar_offset(Axis::Vertical), )); - cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await) - .detach_and_notify_err(window, cx); + let keyboard_mapper = cx.keyboard_mapper().clone(); + cx.spawn(async move |_, _| { + remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await + }) + .detach_and_notify_err(window, cx); } fn copy_context_to_clipboard( @@ -1422,7 +1429,7 @@ impl ProcessedBinding { .map(|keybind| keybind.get_action_mapping()) } - fn keystrokes(&self) -> Option<&[Keystroke]> { + fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> { self.ui_key_binding() .map(|binding| binding.keystrokes.as_slice()) } @@ -2220,7 +2227,7 @@ impl KeybindingEditorModal { Ok(action_arguments) } - fn validate_keystrokes(&self, cx: &App) -> anyhow::Result> { + fn validate_keystrokes(&self, cx: &App) -> anyhow::Result> { let new_keystrokes = self .keybind_editor .read_with(cx, |editor, _| editor.keystrokes().to_vec()); @@ -2316,6 +2323,7 @@ impl KeybindingEditorModal { }).unwrap_or(Ok(()))?; let create = self.creating; + let keyboard_mapper = cx.keyboard_mapper().clone(); cx.spawn(async move |this, cx| { let action_name = existing_keybind.action().name; @@ -2328,6 +2336,7 @@ impl KeybindingEditorModal { new_action_args.as_deref(), &fs, tab_size, + keyboard_mapper.as_ref(), ) .await { @@ -2445,11 +2454,21 @@ impl KeybindingEditorModal { } } -fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke { - Keystroke { - modifiers, - key, - ..Default::default() +fn remove_key_char( + KeybindingKeystroke { + inner, + display_modifiers, + display_key, + }: KeybindingKeystroke, +) -> KeybindingKeystroke { + KeybindingKeystroke { + inner: Keystroke { + modifiers: inner.modifiers, + key: inner.key, + key_char: None, + }, + display_modifiers, + display_key, } } @@ -2992,6 +3011,7 @@ async fn save_keybinding_update( new_args: Option<&str>, fs: &Arc, tab_size: usize, + keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> anyhow::Result<()> { let keymap_contents = settings::KeymapFile::load_keymap_file(fs) .await @@ -3034,9 +3054,13 @@ async fn save_keybinding_update( let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); - let updated_keymap_contents = - settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) - .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; + let updated_keymap_contents = settings::KeymapFile::update_keybinding( + operation, + keymap_contents, + tab_size, + keyboard_mapper, + ) + .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; fs.write( paths::keymap_file().as_path(), updated_keymap_contents.as_bytes(), @@ -3057,6 +3081,7 @@ async fn remove_keybinding( existing: ProcessedBinding, fs: &Arc, tab_size: usize, + keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> anyhow::Result<()> { let Some(keystrokes) = existing.keystrokes() else { anyhow::bail!("Cannot remove a keybinding that does not exist"); @@ -3080,9 +3105,13 @@ async fn remove_keybinding( }; let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); - let updated_keymap_contents = - settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) - .context("Failed to update keybinding")?; + let updated_keymap_contents = settings::KeymapFile::update_keybinding( + operation, + keymap_contents, + tab_size, + keyboard_mapper, + ) + .context("Failed to update keybinding")?; fs.write( paths::keymap_file().as_path(), updated_keymap_contents.as_bytes(), diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index 1b8010853ecabc7f4198172a4364f7a1f88fbe67..ca50d5c03dcde33f4fd1b83cfeb56eb1b341d818 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -1,6 +1,6 @@ use gpui::{ Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, - Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions, + KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions, }; use ui::{ ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize, @@ -42,8 +42,8 @@ impl PartialEq for CloseKeystrokeResult { } pub struct KeystrokeInput { - keystrokes: Vec, - placeholder_keystrokes: Option>, + keystrokes: Vec, + placeholder_keystrokes: Option>, outer_focus_handle: FocusHandle, inner_focus_handle: FocusHandle, intercept_subscription: Option, @@ -70,7 +70,7 @@ impl KeystrokeInput { const KEYSTROKE_COUNT_MAX: usize = 3; pub fn new( - placeholder_keystrokes: Option>, + placeholder_keystrokes: Option>, window: &mut Window, cx: &mut Context, ) -> Self { @@ -97,7 +97,7 @@ impl KeystrokeInput { } } - pub fn set_keystrokes(&mut self, keystrokes: Vec, cx: &mut Context) { + pub fn set_keystrokes(&mut self, keystrokes: Vec, cx: &mut Context) { self.keystrokes = keystrokes; self.keystrokes_changed(cx); } @@ -106,7 +106,7 @@ impl KeystrokeInput { self.search = search; } - pub fn keystrokes(&self) -> &[Keystroke] { + pub fn keystrokes(&self) -> &[KeybindingKeystroke] { if let Some(placeholders) = self.placeholder_keystrokes.as_ref() && self.keystrokes.is_empty() { @@ -116,18 +116,22 @@ impl KeystrokeInput { && self .keystrokes .last() - .is_some_and(|last| last.key.is_empty()) + .is_some_and(|last| last.display_key.is_empty()) { return &self.keystrokes[..self.keystrokes.len() - 1]; } &self.keystrokes } - fn dummy(modifiers: Modifiers) -> Keystroke { - Keystroke { - modifiers, - key: "".to_string(), - key_char: None, + fn dummy(modifiers: Modifiers) -> KeybindingKeystroke { + KeybindingKeystroke { + inner: Keystroke { + modifiers, + key: "".to_string(), + key_char: None, + }, + display_modifiers: modifiers, + display_key: "".to_string(), } } @@ -254,7 +258,7 @@ impl KeystrokeInput { self.keystrokes_changed(cx); if let Some(last) = self.keystrokes.last_mut() - && last.key.is_empty() + && last.display_key.is_empty() && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX { if !self.search && !event.modifiers.modified() { @@ -263,13 +267,15 @@ impl KeystrokeInput { } if self.search { if self.previous_modifiers.modified() { - last.modifiers |= event.modifiers; + last.display_modifiers |= event.modifiers; + last.inner.modifiers |= event.modifiers; } else { self.keystrokes.push(Self::dummy(event.modifiers)); } self.previous_modifiers |= event.modifiers; } else { - last.modifiers = event.modifiers; + last.display_modifiers = event.modifiers; + last.inner.modifiers = event.modifiers; return; } } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX { @@ -297,14 +303,17 @@ impl KeystrokeInput { return; } - let mut keystroke = keystroke.clone(); + let mut keystroke = + KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref()); if let Some(last) = self.keystrokes.last() - && last.key.is_empty() + && last.display_key.is_empty() && (!self.search || self.previous_modifiers.modified()) { - let key = keystroke.key.clone(); + let display_key = keystroke.display_key.clone(); + let inner_key = keystroke.inner.key.clone(); keystroke = last.clone(); - keystroke.key = key; + keystroke.display_key = display_key; + keystroke.inner.key = inner_key; self.keystrokes.pop(); } @@ -324,11 +333,14 @@ impl KeystrokeInput { self.keystrokes_changed(cx); if self.search { - self.previous_modifiers = keystroke.modifiers; + self.previous_modifiers = keystroke.display_modifiers; return; } - if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && keystroke.modifiers.modified() { - self.keystrokes.push(Self::dummy(keystroke.modifiers)); + if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX + && keystroke.display_modifiers.modified() + { + self.keystrokes + .push(Self::dummy(keystroke.display_modifiers)); } } @@ -364,7 +376,7 @@ impl KeystrokeInput { &self.keystrokes }; keystrokes.iter().map(move |keystroke| { - h_flex().children(ui::render_keystroke( + h_flex().children(ui::render_keybinding_keystroke( keystroke, Some(Color::Default), Some(rems(0.875).into()), @@ -809,9 +821,13 @@ mod tests { /// Verifies that the keystrokes match the expected strings #[track_caller] pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self { - let actual = self - .input - .read_with(&self.cx, |input, _| input.keystrokes.clone()); + let actual: Vec = self.input.read_with(&self.cx, |input, _| { + input + .keystrokes + .iter() + .map(|keystroke| keystroke.inner.clone()) + .collect() + }); Self::expect_keystrokes_equal(&actual, expected); self } @@ -939,7 +955,7 @@ mod tests { } struct KeystrokeUpdateTracker { - initial_keystrokes: Vec, + initial_keystrokes: Vec, _subscription: Subscription, input: Entity, received_keystrokes_updated: bool, @@ -983,8 +999,8 @@ mod tests { ); } - fn keystrokes_str(ks: &[Keystroke]) -> String { - ks.iter().map(|ks| ks.unparse()).join(" ") + fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String { + ks.iter().map(|ks| ks.inner.unparse()).join(" ") } } } diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 1e7bb40c400e053788862544287474dfe075758f..81817045dc6ce07d4f40a12e3850fef3e1b234f9 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -1,8 +1,8 @@ use crate::PlatformStyle; use crate::{Icon, IconName, IconSize, h_flex, prelude::*}; use gpui::{ - Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers, Window, - relative, + Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke, + Modifiers, Window, relative, }; use itertools::Itertools; @@ -13,7 +13,7 @@ pub struct KeyBinding { /// More than one keystroke produces a chord. /// /// This should always contain at least one keystroke. - pub keystrokes: Vec, + pub keystrokes: Vec, /// The [`PlatformStyle`] to use when displaying this keybinding. platform_style: PlatformStyle, @@ -59,7 +59,7 @@ impl KeyBinding { cx.try_global::().is_some_and(|g| g.0) } - pub fn new(keystrokes: Vec, cx: &App) -> Self { + pub fn new(keystrokes: Vec, cx: &App) -> Self { Self { keystrokes, platform_style: PlatformStyle::platform(), @@ -99,16 +99,16 @@ impl KeyBinding { } fn render_key( - keystroke: &Keystroke, + key: &str, color: Option, platform_style: PlatformStyle, size: impl Into>, ) -> AnyElement { - let key_icon = icon_for_key(keystroke, platform_style); + let key_icon = icon_for_key(key, platform_style); match key_icon { Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(), None => { - let key = util::capitalize(&keystroke.key); + let key = util::capitalize(key); Key::new(&key, color).size(size).into_any_element() } } @@ -124,7 +124,7 @@ impl RenderOnce for KeyBinding { "KEY_BINDING-{}", self.keystrokes .iter() - .map(|k| k.key.to_string()) + .map(|k| k.display_key.to_string()) .collect::>() .join(" ") ) @@ -137,7 +137,7 @@ impl RenderOnce for KeyBinding { .py_0p5() .rounded_xs() .text_color(cx.theme().colors().text_muted) - .children(render_keystroke( + .children(render_keybinding_keystroke( keystroke, color, self.size, @@ -148,8 +148,8 @@ impl RenderOnce for KeyBinding { } } -pub fn render_keystroke( - keystroke: &Keystroke, +pub fn render_keybinding_keystroke( + keystroke: &KeybindingKeystroke, color: Option, size: impl Into>, platform_style: PlatformStyle, @@ -163,26 +163,39 @@ pub fn render_keystroke( let size = size.into(); if use_text { - let element = Key::new(keystroke_text(keystroke, platform_style, vim_mode), color) - .size(size) - .into_any_element(); + let element = Key::new( + keystroke_text( + &keystroke.display_modifiers, + &keystroke.display_key, + platform_style, + vim_mode, + ), + color, + ) + .size(size) + .into_any_element(); vec![element] } else { let mut elements = Vec::new(); elements.extend(render_modifiers( - &keystroke.modifiers, + &keystroke.display_modifiers, platform_style, color, size, true, )); - elements.push(render_key(keystroke, color, platform_style, size)); + elements.push(render_key( + &keystroke.display_key, + color, + platform_style, + size, + )); elements } } -fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option { - match keystroke.key.as_str() { +fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option { + match key { "left" => Some(IconName::ArrowLeft), "right" => Some(IconName::ArrowRight), "up" => Some(IconName::ArrowUp), @@ -379,7 +392,7 @@ impl KeyIcon { /// Returns a textual representation of the key binding for the given [`Action`]. pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option { let key_binding = window.highest_precedence_binding_for_action(action)?; - Some(text_for_keystrokes(key_binding.keystrokes(), cx)) + Some(text_for_keybinding_keystrokes(key_binding.keystrokes(), cx)) } pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String { @@ -387,22 +400,50 @@ pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String { let vim_enabled = cx.try_global::().is_some(); keystrokes .iter() - .map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled)) + .map(|keystroke| { + keystroke_text( + &keystroke.modifiers, + &keystroke.key, + platform_style, + vim_enabled, + ) + }) + .join(" ") +} + +pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String { + let platform_style = PlatformStyle::platform(); + let vim_enabled = cx.try_global::().is_some(); + keystrokes + .iter() + .map(|keystroke| { + keystroke_text( + &keystroke.display_modifiers, + &keystroke.display_key, + platform_style, + vim_enabled, + ) + }) .join(" ") } -pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String { +pub fn text_for_keystroke(modifiers: &Modifiers, key: &str, cx: &App) -> String { let platform_style = PlatformStyle::platform(); let vim_enabled = cx.try_global::().is_some(); - keystroke_text(keystroke, platform_style, vim_enabled) + keystroke_text(modifiers, key, platform_style, vim_enabled) } /// Returns a textual representation of the given [`Keystroke`]. -fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode: bool) -> String { +fn keystroke_text( + modifiers: &Modifiers, + key: &str, + platform_style: PlatformStyle, + vim_mode: bool, +) -> String { let mut text = String::new(); let delimiter = '-'; - if keystroke.modifiers.function { + if modifiers.function { match vim_mode { false => text.push_str("Fn"), true => text.push_str("fn"), @@ -411,7 +452,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode text.push(delimiter); } - if keystroke.modifiers.control { + if modifiers.control { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Control"), (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"), @@ -421,7 +462,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode text.push(delimiter); } - if keystroke.modifiers.platform { + if modifiers.platform { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Command"), (PlatformStyle::Mac, true) => text.push_str("cmd"), @@ -434,7 +475,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode text.push(delimiter); } - if keystroke.modifiers.alt { + if modifiers.alt { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Option"), (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"), @@ -444,7 +485,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode text.push(delimiter); } - if keystroke.modifiers.shift { + if modifiers.shift { match (platform_style, vim_mode) { (_, false) => text.push_str("Shift"), (_, true) => text.push_str("shift"), @@ -453,9 +494,9 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode } if vim_mode { - text.push_str(&keystroke.key) + text.push_str(key) } else { - let key = match keystroke.key.as_str() { + let key = match key { "pageup" => "PageUp", "pagedown" => "PageDown", key => &util::capitalize(key), @@ -562,9 +603,11 @@ mod tests { #[test] fn test_text_for_keystroke() { + let keystroke = Keystroke::parse("cmd-c").unwrap(); assert_eq!( keystroke_text( - &Keystroke::parse("cmd-c").unwrap(), + &keystroke.modifiers, + &keystroke.key, PlatformStyle::Mac, false ), @@ -572,7 +615,8 @@ mod tests { ); assert_eq!( keystroke_text( - &Keystroke::parse("cmd-c").unwrap(), + &keystroke.modifiers, + &keystroke.key, PlatformStyle::Linux, false ), @@ -580,16 +624,19 @@ mod tests { ); assert_eq!( keystroke_text( - &Keystroke::parse("cmd-c").unwrap(), + &keystroke.modifiers, + &keystroke.key, PlatformStyle::Windows, false ), "Win-C".to_string() ); + let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap(); assert_eq!( keystroke_text( - &Keystroke::parse("ctrl-alt-delete").unwrap(), + &keystroke.modifiers, + &keystroke.key, PlatformStyle::Mac, false ), @@ -597,7 +644,8 @@ mod tests { ); assert_eq!( keystroke_text( - &Keystroke::parse("ctrl-alt-delete").unwrap(), + &keystroke.modifiers, + &keystroke.key, PlatformStyle::Linux, false ), @@ -605,16 +653,19 @@ mod tests { ); assert_eq!( keystroke_text( - &Keystroke::parse("ctrl-alt-delete").unwrap(), + &keystroke.modifiers, + &keystroke.key, PlatformStyle::Windows, false ), "Ctrl-Alt-Delete".to_string() ); + let keystroke = Keystroke::parse("shift-pageup").unwrap(); assert_eq!( keystroke_text( - &Keystroke::parse("shift-pageup").unwrap(), + &keystroke.modifiers, + &keystroke.key, PlatformStyle::Mac, false ), @@ -622,7 +673,8 @@ mod tests { ); assert_eq!( keystroke_text( - &Keystroke::parse("shift-pageup").unwrap(), + &keystroke.modifiers, + &keystroke.key, PlatformStyle::Linux, false, ), @@ -630,7 +682,8 @@ mod tests { ); assert_eq!( keystroke_text( - &Keystroke::parse("shift-pageup").unwrap(), + &keystroke.modifiers, + &keystroke.key, PlatformStyle::Windows, false ), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 638e1dca0e261dcb7d66c7ac2b8df9ed9ac78ff9..553444ebdbc2fe018f1cad39d0776c4c113eba6f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1308,11 +1308,11 @@ pub fn handle_keymap_file_changes( }) .detach(); - let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id()); + let mut current_layout_id = cx.keyboard_layout().id().to_string(); cx.on_keyboard_layout_change(move |cx| { - let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id()); - if next_mapping != current_mapping { - current_mapping = next_mapping; + let next_layout_id = cx.keyboard_layout().id(); + if next_layout_id != current_layout_id { + current_layout_id = next_layout_id.to_string(); keyboard_layout_tx.unbounded_send(()).ok(); } }) @@ -4729,7 +4729,7 @@ mod tests { // and key strokes contain the given key bindings .into_iter() - .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)), + .any(|binding| binding.keystrokes().iter().any(|k| k.display_key == key)), "On {} Failed to find {} with key binding {}", line, action.name(), diff --git a/crates/zed/src/zed/quick_action_bar/preview.rs b/crates/zed/src/zed/quick_action_bar/preview.rs index 3772104f39050c53ced37031e2c2f3e052dcb12d..fb5a75f78d834ab3943e9dfd87cc7744fc453fcd 100644 --- a/crates/zed/src/zed/quick_action_bar/preview.rs +++ b/crates/zed/src/zed/quick_action_bar/preview.rs @@ -72,7 +72,10 @@ impl QuickActionBar { Tooltip::with_meta( tooltip_text, Some(open_action_for_tooltip), - format!("{} to open in a split", text_for_keystroke(&alt_click, cx)), + format!( + "{} to open in a split", + text_for_keystroke(&alt_click.modifiers, &alt_click.key, cx) + ), window, cx, ) From c5d3c7d790cdfda178aed768f00d488c45805e60 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:58:23 -0300 Subject: [PATCH 359/744] thread view: Improve agent installation UI (#36957) Release Notes: - N/A --------- Co-authored-by: Conrad Irwin --- assets/icons/terminal_ghost.svg | 4 + crates/agent_ui/src/acp/thread_view.rs | 253 ++++++++++++++----------- crates/icons/src/icons.rs | 1 + 3 files changed, 143 insertions(+), 115 deletions(-) create mode 100644 assets/icons/terminal_ghost.svg diff --git a/assets/icons/terminal_ghost.svg b/assets/icons/terminal_ghost.svg new file mode 100644 index 0000000000000000000000000000000000000000..7d0d0e068e8a6f01837e860e8223690a95541769 --- /dev/null +++ b/assets/icons/terminal_ghost.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f3b1e6ce3b5a349cc767b82a5e4a799d0671c0a9..c68c3a3e937f4461c6d28f59198ddf04b467d9de 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -43,7 +43,7 @@ use text::Anchor; use theme::ThemeSettings; use ui::{ Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, - Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*, + Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -278,6 +278,7 @@ pub struct AcpThreadView { editing_message: Option, prompt_capabilities: Rc>, is_loading_contents: bool, + install_command_markdown: Entity, _cancel_task: Option>, _subscriptions: [Subscription; 3], } @@ -391,6 +392,7 @@ impl AcpThreadView { hovered_recent_history_item: None, prompt_capabilities, is_loading_contents: false, + install_command_markdown: cx.new(|cx| Markdown::new("".into(), None, None, cx)), _subscriptions: subscriptions, _cancel_task: None, focus_handle: cx.focus_handle(), @@ -666,7 +668,12 @@ impl AcpThreadView { match &self.thread_state { ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(), ThreadState::Loading { .. } => "Loading…".into(), - ThreadState::LoadError(_) => "Failed to load".into(), + ThreadState::LoadError(error) => match error { + LoadError::NotInstalled { .. } => format!("Install {}", self.agent.name()).into(), + LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(), + LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(), + LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(), + }, } } @@ -2834,125 +2841,26 @@ impl AcpThreadView { ) } - fn render_load_error(&self, e: &LoadError, cx: &Context) -> AnyElement { - let (message, action_slot) = match e { + fn render_load_error( + &self, + e: &LoadError, + window: &mut Window, + cx: &mut Context, + ) -> AnyElement { + let (message, action_slot): (SharedString, _) = match e { LoadError::NotInstalled { - error_message, - install_message, + error_message: _, + install_message: _, install_command, } => { - let install_command = install_command.clone(); - let button = Button::new("install", install_message) - .tooltip(Tooltip::text(install_command.clone())) - .style(ButtonStyle::Outlined) - .label_size(LabelSize::Small) - .icon(IconName::Download) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(move |this, _, window, cx| { - telemetry::event!("Agent Install CLI", agent = this.agent.telemetry_id()); - - let task = this - .workspace - .update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - let cwd = project.first_project_directory(cx); - let shell = project.terminal_settings(&cwd, cx).shell.clone(); - let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId(install_command.clone()), - full_label: install_command.clone(), - label: install_command.clone(), - command: Some(install_command.clone()), - args: Vec::new(), - command_label: install_command.clone(), - cwd, - env: Default::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: Default::default(), - reveal_target: Default::default(), - hide: Default::default(), - shell, - show_summary: true, - show_command: true, - show_rerun: false, - }; - workspace.spawn_in_terminal(spawn_in_terminal, window, cx) - }) - .ok(); - let Some(task) = task else { return }; - cx.spawn_in(window, async move |this, cx| { - if let Some(Ok(_)) = task.await { - this.update_in(cx, |this, window, cx| { - this.reset(window, cx); - }) - .ok(); - } - }) - .detach() - })); - - (error_message.clone(), Some(button.into_any_element())) + return self.render_not_installed(install_command.clone(), false, window, cx); } LoadError::Unsupported { - error_message, - upgrade_message, + error_message: _, + upgrade_message: _, upgrade_command, } => { - let upgrade_command = upgrade_command.clone(); - let button = Button::new("upgrade", upgrade_message) - .tooltip(Tooltip::text(upgrade_command.clone())) - .style(ButtonStyle::Outlined) - .label_size(LabelSize::Small) - .icon(IconName::Download) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(move |this, _, window, cx| { - telemetry::event!("Agent Upgrade CLI", agent = this.agent.telemetry_id()); - - let task = this - .workspace - .update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - let cwd = project.first_project_directory(cx); - let shell = project.terminal_settings(&cwd, cx).shell.clone(); - let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId(upgrade_command.to_string()), - full_label: upgrade_command.clone(), - label: upgrade_command.clone(), - command: Some(upgrade_command.clone()), - args: Vec::new(), - command_label: upgrade_command.clone(), - cwd, - env: Default::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: Default::default(), - reveal_target: Default::default(), - hide: Default::default(), - shell, - show_summary: true, - show_command: true, - show_rerun: false, - }; - workspace.spawn_in_terminal(spawn_in_terminal, window, cx) - }) - .ok(); - let Some(task) = task else { return }; - cx.spawn_in(window, async move |this, cx| { - if let Some(Ok(_)) = task.await { - this.update_in(cx, |this, window, cx| { - this.reset(window, cx); - }) - .ok(); - } - }) - .detach() - })); - - (error_message.clone(), Some(button.into_any_element())) + return self.render_not_installed(upgrade_command.clone(), true, window, cx); } LoadError::Exited { .. } => ("Server exited with status {status}".into(), None), LoadError::Other(msg) => ( @@ -2970,6 +2878,121 @@ impl AcpThreadView { .into_any_element() } + fn install_agent(&self, install_command: String, window: &mut Window, cx: &mut Context) { + telemetry::event!("Agent Install CLI", agent = self.agent.telemetry_id()); + let task = self + .workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + let cwd = project.first_project_directory(cx); + let shell = project.terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId(install_command.clone()), + full_label: install_command.clone(), + label: install_command.clone(), + command: Some(install_command.clone()), + args: Vec::new(), + command_label: install_command.clone(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + workspace.spawn_in_terminal(spawn_in_terminal, window, cx) + }) + .ok(); + let Some(task) = task else { return }; + cx.spawn_in(window, async move |this, cx| { + if let Some(Ok(_)) = task.await { + this.update_in(cx, |this, window, cx| { + this.reset(window, cx); + }) + .ok(); + } + }) + .detach() + } + + fn render_not_installed( + &self, + install_command: String, + is_upgrade: bool, + window: &mut Window, + cx: &mut Context, + ) -> AnyElement { + self.install_command_markdown.update(cx, |markdown, cx| { + if !markdown.source().contains(&install_command) { + markdown.replace(format!("```\n{}\n```", install_command), cx); + } + }); + + let (heading_label, description_label, button_label, or_label) = if is_upgrade { + ( + "Upgrade Gemini CLI in Zed", + "Get access to the latest version with support for Zed.", + "Upgrade Gemini CLI", + "Or, to upgrade it manually:", + ) + } else { + ( + "Get Started with Gemini CLI in Zed", + "Use Google's new coding agent directly in Zed.", + "Install Gemini CLI", + "Or, to install it manually:", + ) + }; + + v_flex() + .w_full() + .p_3p5() + .gap_2p5() + .border_t_1() + .border_color(cx.theme().colors().border) + .bg(linear_gradient( + 180., + linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.), + linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.), + )) + .child( + v_flex().gap_0p5().child(Label::new(heading_label)).child( + Label::new(description_label) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + .child( + Button::new("install_gemini", button_label) + .full_width() + .size(ButtonSize::Medium) + .style(ButtonStyle::Tinted(TintColor::Accent)) + .label_size(LabelSize::Small) + .icon(IconName::TerminalGhost) + .icon_color(Color::Muted) + .icon_size(IconSize::Small) + .icon_position(IconPosition::Start) + .on_click(cx.listener(move |this, _, window, cx| { + this.install_agent(install_command.clone(), window, cx) + })), + ) + .child( + Label::new(or_label) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child(MarkdownElement::new( + self.install_command_markdown.clone(), + default_markdown_style(false, false, window, cx), + )) + .into_any_element() + } + fn render_activity_bar( &self, thread_entity: &Entity, @@ -4943,7 +4966,7 @@ impl Render for AcpThreadView { .size_full() .items_center() .justify_end() - .child(self.render_load_error(e, cx)), + .child(self.render_load_error(e, window, cx)), ThreadState::Ready { .. } => v_flex().flex_1().map(|this| { if has_messages { this.child( diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 4fc6039fd76e753b5a515d8917c22b27c97737fc..f7363395ae76fd99f0642d7d0f4117371e575b5d 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -215,6 +215,7 @@ pub enum IconName { Tab, Terminal, TerminalAlt, + TerminalGhost, TextSnippet, TextThread, Thread, From bd4e943597d4b6c3ac52cd2edae2ae2e2abbec81 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:59:12 -0300 Subject: [PATCH 360/744] acp: Add onboarding modal & title bar banner (#36784) Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner --- assets/images/acp_grid.svg | 1257 +++++++++++++++++ assets/images/acp_logo.svg | 1 + assets/images/acp_logo_serif.svg | 2 + crates/agent_ui/src/agent_configuration.rs | 2 +- crates/agent_ui/src/agent_panel.rs | 41 +- crates/agent_ui/src/ui.rs | 2 + .../agent_ui/src/ui/acp_onboarding_modal.rs | 254 ++++ crates/client/src/zed_urls.rs | 8 + crates/title_bar/src/onboarding_banner.rs | 2 +- crates/title_bar/src/title_bar.rs | 10 +- crates/ui/src/components/image.rs | 3 + crates/zed_actions/src/lib.rs | 2 + 12 files changed, 1556 insertions(+), 28 deletions(-) create mode 100644 assets/images/acp_grid.svg create mode 100644 assets/images/acp_logo.svg create mode 100644 assets/images/acp_logo_serif.svg create mode 100644 crates/agent_ui/src/ui/acp_onboarding_modal.rs diff --git a/assets/images/acp_grid.svg b/assets/images/acp_grid.svg new file mode 100644 index 0000000000000000000000000000000000000000..8ebff8e1bc87b17e536c7f97dfa2118130233258 --- /dev/null +++ b/assets/images/acp_grid.svg @@ -0,0 +1,1257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/acp_logo.svg b/assets/images/acp_logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..efaa46707be0a893917c3fc072a14b9c7b6b0c9b --- /dev/null +++ b/assets/images/acp_logo.svg @@ -0,0 +1 @@ + diff --git a/assets/images/acp_logo_serif.svg b/assets/images/acp_logo_serif.svg new file mode 100644 index 0000000000000000000000000000000000000000..6bc359cf82dde8060a66c051c8727f0e0624b938 --- /dev/null +++ b/assets/images/acp_logo_serif.svg @@ -0,0 +1,2 @@ + + diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index c279115880e8fd1f6083fd9ada80c9946164fd31..224f49cc3e11f71a208a3f8f5b9f777b14478d23 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -1093,7 +1093,7 @@ impl AgentConfiguration { ) .child( Label::new( - "Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.", + "Bring the agent of your choice to Zed via our new Agent Client Protocol.", ) .color(Color::Muted), ), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 267c76d73fac4c0d5a624799b6e8938688335fd4..d1cf748733127b4c5466d96d7507e5bf7c8a16ce 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -14,6 +14,7 @@ use zed_actions::agent::ReauthenticateAgent; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::agent_diff::AgentDiffThread; +use crate::ui::AcpOnboardingModal; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, @@ -77,7 +78,10 @@ use workspace::{ }; use zed_actions::{ DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize, - agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector}, + agent::{ + OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding, + ToggleModelSelector, + }, assistant::{OpenRulesLibrary, ToggleFocus}, }; @@ -201,6 +205,9 @@ pub fn init(cx: &mut App) { .register_action(|workspace, _: &OpenOnboardingModal, window, cx| { AgentOnboardingModal::toggle(workspace, window, cx) }) + .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| { + AcpOnboardingModal::toggle(workspace, window, cx) + }) .register_action(|_workspace, _: &ResetOnboarding, window, cx| { window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx); window.refresh(); @@ -1841,19 +1848,6 @@ impl AgentPanel { menu } - pub fn set_selected_agent( - &mut self, - agent: AgentType, - window: &mut Window, - cx: &mut Context, - ) { - if self.selected_agent != agent { - self.selected_agent = agent.clone(); - self.serialize(cx); - } - self.new_agent_thread(agent, window, cx); - } - pub fn selected_agent(&self) -> AgentType { self.selected_agent.clone() } @@ -1864,6 +1858,11 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) { + if self.selected_agent != agent { + self.selected_agent = agent.clone(); + self.serialize(cx); + } + match agent { AgentType::Zed => { window.dispatch_action( @@ -2544,7 +2543,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.set_selected_agent( + panel.new_agent_thread( AgentType::NativeAgent, window, cx, @@ -2570,7 +2569,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.set_selected_agent( + panel.new_agent_thread( AgentType::TextThread, window, cx, @@ -2598,7 +2597,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.set_selected_agent( + panel.new_agent_thread( AgentType::Gemini, window, cx, @@ -2625,7 +2624,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.set_selected_agent( + panel.new_agent_thread( AgentType::ClaudeCode, window, cx, @@ -2658,7 +2657,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.set_selected_agent( + panel.new_agent_thread( AgentType::Custom { name: agent_name .clone(), @@ -2682,9 +2681,9 @@ impl AgentPanel { }) .when(cx.has_flag::(), |menu| { menu.separator().link( - "Add Your Own Agent", + "Add Other Agents", OpenBrowser { - url: "https://agentclientprotocol.com/".into(), + url: zed_urls::external_agents_docs(cx), } .boxed_clone(), ) diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index ada973cddfc847c67b805ee053fb50e6d9cd99d7..600698b07e1e2bf43d78c5c225838476f04a5c76 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -1,3 +1,4 @@ +mod acp_onboarding_modal; mod agent_notification; mod burn_mode_tooltip; mod context_pill; @@ -6,6 +7,7 @@ mod onboarding_modal; pub mod preview; mod unavailable_editing_tooltip; +pub use acp_onboarding_modal::*; pub use agent_notification::*; pub use burn_mode_tooltip::*; pub use context_pill::*; diff --git a/crates/agent_ui/src/ui/acp_onboarding_modal.rs b/crates/agent_ui/src/ui/acp_onboarding_modal.rs new file mode 100644 index 0000000000000000000000000000000000000000..0ed9de7221014476f21c0406e6be8ac3592fca7c --- /dev/null +++ b/crates/agent_ui/src/ui/acp_onboarding_modal.rs @@ -0,0 +1,254 @@ +use client::zed_urls; +use gpui::{ + ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render, + linear_color_stop, linear_gradient, +}; +use ui::{TintColor, Vector, VectorName, prelude::*}; +use workspace::{ModalView, Workspace}; + +use crate::agent_panel::{AgentPanel, AgentType}; + +macro_rules! acp_onboarding_event { + ($name:expr) => { + telemetry::event!($name, source = "ACP Onboarding"); + }; + ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => { + telemetry::event!($name, source = "ACP Onboarding", $($key $(= $value)?),+); + }; +} + +pub struct AcpOnboardingModal { + focus_handle: FocusHandle, + workspace: Entity, +} + +impl AcpOnboardingModal { + pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { + let workspace_entity = cx.entity(); + workspace.toggle_modal(window, cx, |_window, cx| Self { + workspace: workspace_entity, + focus_handle: cx.focus_handle(), + }); + } + + fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { + self.workspace.update(cx, |workspace, cx| { + workspace.focus_panel::(window, cx); + + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel.new_agent_thread(AgentType::Gemini, window, cx); + }); + } + }); + + cx.emit(DismissEvent); + + acp_onboarding_event!("Open Panel Clicked"); + } + + fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { + cx.open_url(&zed_urls::external_agents_docs(cx)); + cx.notify(); + + acp_onboarding_event!("Documentation Link Clicked"); + } + + fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { + cx.emit(DismissEvent); + } +} + +impl EventEmitter for AcpOnboardingModal {} + +impl Focusable for AcpOnboardingModal { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl ModalView for AcpOnboardingModal {} + +impl Render for AcpOnboardingModal { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let illustration_element = |label: bool, opacity: f32| { + h_flex() + .px_1() + .py_0p5() + .gap_1() + .rounded_sm() + .bg(cx.theme().colors().element_active.opacity(0.05)) + .border_1() + .border_color(cx.theme().colors().border) + .border_dashed() + .child( + Icon::new(IconName::Stop) + .size(IconSize::Small) + .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))), + ) + .map(|this| { + if label { + this.child( + Label::new("Your Agent Here") + .size(LabelSize::Small) + .color(Color::Muted), + ) + } else { + this.child( + div().w_16().h_1().rounded_full().bg(cx + .theme() + .colors() + .element_active + .opacity(0.6)), + ) + } + }) + .opacity(opacity) + }; + + let illustration = h_flex() + .relative() + .h(rems_from_px(126.)) + .bg(cx.theme().colors().editor_background) + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .justify_center() + .gap_8() + .rounded_t_md() + .overflow_hidden() + .child( + div().absolute().inset_0().w(px(515.)).h(px(126.)).child( + Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.)) + .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))), + ), + ) + .child(div().absolute().inset_0().size_full().bg(linear_gradient( + 0., + linear_color_stop( + cx.theme().colors().elevated_surface_background.opacity(0.1), + 0.9, + ), + linear_color_stop( + cx.theme().colors().elevated_surface_background.opacity(0.), + 0., + ), + ))) + .child( + div() + .absolute() + .inset_0() + .size_full() + .bg(gpui::black().opacity(0.15)), + ) + .child( + h_flex() + .gap_4() + .child( + Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.)) + .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), + ) + .child( + Vector::new( + VectorName::AcpLogoSerif, + rems_from_px(111.), + rems_from_px(41.), + ) + .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), + ), + ) + .child( + v_flex() + .gap_1p5() + .child(illustration_element(false, 0.15)) + .child(illustration_element(true, 0.3)) + .child( + h_flex() + .pl_1() + .pr_2() + .py_0p5() + .gap_1() + .rounded_sm() + .bg(cx.theme().colors().element_active.opacity(0.2)) + .border_1() + .border_color(cx.theme().colors().border) + .child( + Icon::new(IconName::AiGemini) + .size(IconSize::Small) + .color(Color::Muted), + ) + .child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)), + ) + .child(illustration_element(true, 0.3)) + .child(illustration_element(false, 0.15)), + ); + + let heading = v_flex() + .w_full() + .gap_1() + .child( + Label::new("Now Available") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large)); + + let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration."; + + let open_panel_button = Button::new("open-panel", "Start with Gemini CLI") + .icon_size(IconSize::Indicator) + .style(ButtonStyle::Tinted(TintColor::Accent)) + .full_width() + .on_click(cx.listener(Self::open_panel)); + + let docs_button = Button::new("add-other-agents", "Add Other Agents") + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::Indicator) + .icon_color(Color::Muted) + .full_width() + .on_click(cx.listener(Self::view_docs)); + + let close_button = h_flex().absolute().top_2().right_2().child( + IconButton::new("cancel", IconName::Close).on_click(cx.listener( + |_, _: &ClickEvent, _window, cx| { + acp_onboarding_event!("Canceled", trigger = "X click"); + cx.emit(DismissEvent); + }, + )), + ); + + v_flex() + .id("acp-onboarding") + .key_context("AcpOnboardingModal") + .relative() + .w(rems(34.)) + .h_full() + .elevation_3(cx) + .track_focus(&self.focus_handle(cx)) + .overflow_hidden() + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| { + acp_onboarding_event!("Canceled", trigger = "Action"); + cx.emit(DismissEvent); + })) + .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| { + this.focus_handle.focus(window); + })) + .child(illustration) + .child( + v_flex() + .p_4() + .gap_2() + .child(heading) + .child(Label::new(copy).color(Color::Muted)) + .child( + v_flex() + .w_full() + .mt_2() + .gap_1() + .child(open_panel_button) + .child(docs_button), + ), + ) + .child(close_button) + } +} diff --git a/crates/client/src/zed_urls.rs b/crates/client/src/zed_urls.rs index 9df41906d79b4d43234a28dde19bd6862469de8c..7193c099473c95794796c2fc4d3eaaf2f06eb1ac 100644 --- a/crates/client/src/zed_urls.rs +++ b/crates/client/src/zed_urls.rs @@ -43,3 +43,11 @@ pub fn ai_privacy_and_security(cx: &App) -> String { server_url = server_url(cx) ) } + +/// Returns the URL to Zed AI's external agents documentation. +pub fn external_agents_docs(cx: &App) -> String { + format!( + "{server_url}/docs/ai/external-agents", + server_url = server_url(cx) + ) +} diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs index ed43c5277a51d660738f2b0b3efee77ccbafd381..1c2894249000861f6de14f4960205e5deffab47b 100644 --- a/crates/title_bar/src/onboarding_banner.rs +++ b/crates/title_bar/src/onboarding_banner.rs @@ -119,7 +119,7 @@ impl Render for OnboardingBanner { h_flex() .h_full() .gap_1() - .child(Icon::new(self.details.icon_name).size(IconSize::Small)) + .child(Icon::new(self.details.icon_name).size(IconSize::XSmall)) .child( h_flex() .gap_0p5() diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index b84a2800b65f5a2c280256a4765101ae125f7ec4..ad64dac9c69863222506bbca83d3d7379fc097ab 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -275,11 +275,11 @@ impl TitleBar { let banner = cx.new(|cx| { OnboardingBanner::new( - "Debugger Onboarding", - IconName::Debug, - "The Debugger", - None, - zed_actions::debugger::OpenOnboardingModal.boxed_clone(), + "ACP Onboarding", + IconName::Sparkle, + "Bring Your Own Agent", + Some("Introducing:".into()), + zed_actions::agent::OpenAcpOnboardingModal.boxed_clone(), cx, ) }); diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs index 09c3bbeb943ca11a00d42621f0bdd73613efaee3..6e552ddcee83e20d3812f78c67270c0291c2c0e7 100644 --- a/crates/ui/src/components/image.rs +++ b/crates/ui/src/components/image.rs @@ -13,6 +13,9 @@ use crate::prelude::*; )] #[strum(serialize_all = "snake_case")] pub enum VectorName { + AcpGrid, + AcpLogo, + AcpLogoSerif, AiGrid, DebuggerGrid, Grid, diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index a5223a2cdf6d7a7799050be9e8b5e77df003cd9c..8f4c42ca496e26d23765eb006d7eb0fe9db197ee 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -284,6 +284,8 @@ pub mod agent { OpenSettings, /// Opens the agent onboarding modal. OpenOnboardingModal, + /// Opens the ACP onboarding modal. + OpenAcpOnboardingModal, /// Resets the agent onboarding state. ResetOnboarding, /// Starts a chat conversation with the agent. From d8847192c80f24bf5b279f49e1a6d0937fad541c Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 26 Aug 2025 17:35:56 -0300 Subject: [PATCH 361/744] thread view: Adjust thinking block UI (#36958) Release Notes: - N/A Co-authored-by: Conrad Irwin --- crates/agent_ui/src/acp/thread_view.rs | 77 ++++++++++---------------- 1 file changed, 30 insertions(+), 47 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c68c3a3e937f4461c6d28f59198ddf04b467d9de..cd02191d4bbab5f208ebda9e6f92fc8a1aa61972 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1644,19 +1644,6 @@ impl AcpThreadView { let key = (entry_ix, chunk_ix); let is_open = self.expanded_thinking_blocks.contains(&key); - let editor_bg = cx.theme().colors().editor_background; - let gradient_overlay = div() - .rounded_b_lg() - .h_full() - .absolute() - .w_full() - .bottom_0() - .left_0() - .bg(linear_gradient( - 180., - linear_color_stop(editor_bg, 1.), - linear_color_stop(editor_bg.opacity(0.2), 0.), - )); let scroll_handle = self .entry_view_state @@ -1664,27 +1651,34 @@ impl AcpThreadView { .entry(entry_ix) .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix)); + let thinking_content = { + div() + .id(("thinking-content", chunk_ix)) + .when_some(scroll_handle, |this, scroll_handle| { + this.track_scroll(&scroll_handle) + }) + .when(!is_open, |this| this.max_h_12().opacity(0.6)) + .text_ui_sm(cx) + .overflow_hidden() + .child( + self.render_markdown(chunk, default_markdown_style(false, false, window, cx)), + ) + }; + v_flex() - .rounded_md() - .border_1() - .border_color(self.tool_card_border_color(cx)) + .gap_1() .child( h_flex() .id(header_id) .group(&card_header_id) .relative() .w_full() - .py_0p5() - .px_1p5() - .rounded_t_md() - .bg(self.tool_card_header_bg(cx)) .justify_between() - .border_b_1() - .border_color(self.tool_card_border_color(cx)) .child( h_flex() .h(window.line_height()) .gap_1p5() + .overflow_hidden() .child( Icon::new(IconName::ToolThink) .size(IconSize::Small) @@ -1698,7 +1692,7 @@ impl AcpThreadView { if pending { this.child("Thinking") } else { - this.child("Thought Process") + this.child("Thought") } }), ), @@ -1730,28 +1724,17 @@ impl AcpThreadView { } })), ) - .child( - div() - .relative() - .bg(editor_bg) - .rounded_b_lg() - .child( - div() - .id(("thinking-content", chunk_ix)) - .when_some(scroll_handle, |this, scroll_handle| { - this.track_scroll(&scroll_handle) - }) - .p_2() - .when(!is_open, |this| this.max_h_20()) - .text_ui_sm(cx) - .overflow_hidden() - .child(self.render_markdown( - chunk, - default_markdown_style(false, false, window, cx), - )), - ) - .when(!is_open && pending, |this| this.child(gradient_overlay)), - ) + .when(is_open, |this| { + this.child( + div() + .relative() + .ml_1p5() + .pl_3p5() + .border_l_1() + .border_color(self.tool_card_border_color(cx)) + .child(thinking_content), + ) + }) .into_any_element() } @@ -1924,7 +1907,7 @@ impl AcpThreadView { .when(has_location || use_card_layout, |this| this.px_1()) .when(has_location, |this| { this.cursor(CursorStyle::PointingHand) - .rounded_sm() + .rounded(rems_from_px(3.)) // Concentric border radius .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5))) }) .overflow_hidden() @@ -4247,7 +4230,7 @@ impl AcpThreadView { return h_flex().id("thread-controls-container").child( div() .py_2() - .px_5() + .px(rems_from_px(22.)) .child(SpinnerLabel::new().size(LabelSize::Small)), ); } From d7c735959e9a7e194c7c1669d9287f182234926b Mon Sep 17 00:00:00 2001 From: Daniel Dye Date: Tue, 26 Aug 2025 22:08:45 +0100 Subject: [PATCH 362/744] Add xAI's Grok Code Fast 1 model (#36959) Release Notes: - Add the `grok-code-fast-1` model to xAI's list of available models. --- crates/language_models/src/provider/x_ai.rs | 2 +- crates/x_ai/src/x_ai.rs | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index b37a55e19f389bcf7e5ccd09b52e2b3f6b7ff094..bb17f22c7f3fdbb0296b1e0bb290fbce9a979ddf 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -319,7 +319,7 @@ impl LanguageModel for XAiLanguageModel { } fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { let model_id = self.model.id().trim().to_lowercase(); - if model_id.eq(x_ai::Model::Grok4.id()) { + if model_id.eq(x_ai::Model::Grok4.id()) || model_id.eq(x_ai::Model::GrokCodeFast1.id()) { LanguageModelToolSchemaFormat::JsonSchemaSubset } else { LanguageModelToolSchemaFormat::JsonSchema diff --git a/crates/x_ai/src/x_ai.rs b/crates/x_ai/src/x_ai.rs index 569503784c68d2676de24d369cb36774ee48f054..50f8681c31b5c95d2fc74351416512cbb539252f 100644 --- a/crates/x_ai/src/x_ai.rs +++ b/crates/x_ai/src/x_ai.rs @@ -20,6 +20,8 @@ pub enum Model { Grok3MiniFast, #[serde(rename = "grok-4-latest")] Grok4, + #[serde(rename = "grok-code-fast-1")] + GrokCodeFast1, #[serde(rename = "custom")] Custom { name: String, @@ -43,6 +45,7 @@ impl Model { "grok-3-mini" => Ok(Self::Grok3Mini), "grok-3-fast" => Ok(Self::Grok3Fast), "grok-3-mini-fast" => Ok(Self::Grok3MiniFast), + "grok-code-fast-1" => Ok(Self::GrokCodeFast1), _ => anyhow::bail!("invalid model id '{id}'"), } } @@ -55,6 +58,7 @@ impl Model { Self::Grok3Fast => "grok-3-fast", Self::Grok3MiniFast => "grok-3-mini-fast", Self::Grok4 => "grok-4", + Self::GrokCodeFast1 => "grok-code-fast-1", Self::Custom { name, .. } => name, } } @@ -67,6 +71,7 @@ impl Model { Self::Grok3Fast => "Grok 3 Fast", Self::Grok3MiniFast => "Grok 3 Mini Fast", Self::Grok4 => "Grok 4", + Self::GrokCodeFast1 => "Grok Code Fast 1", Self::Custom { name, display_name, .. } => display_name.as_ref().unwrap_or(name), @@ -76,7 +81,7 @@ impl Model { pub fn max_token_count(&self) -> u64 { match self { Self::Grok3 | Self::Grok3Mini | Self::Grok3Fast | Self::Grok3MiniFast => 131_072, - Self::Grok4 => 256_000, + Self::Grok4 | Self::GrokCodeFast1 => 256_000, Self::Grok2Vision => 8_192, Self::Custom { max_tokens, .. } => *max_tokens, } @@ -85,7 +90,7 @@ impl Model { pub fn max_output_tokens(&self) -> Option { match self { Self::Grok3 | Self::Grok3Mini | Self::Grok3Fast | Self::Grok3MiniFast => Some(8_192), - Self::Grok4 => Some(64_000), + Self::Grok4 | Self::GrokCodeFast1 => Some(64_000), Self::Grok2Vision => Some(4_096), Self::Custom { max_output_tokens, .. @@ -101,7 +106,7 @@ impl Model { | Self::Grok3Fast | Self::Grok3MiniFast | Self::Grok4 => true, - Model::Custom { .. } => false, + Self::GrokCodeFast1 | Model::Custom { .. } => false, } } @@ -116,7 +121,8 @@ impl Model { | Self::Grok3Mini | Self::Grok3Fast | Self::Grok3MiniFast - | Self::Grok4 => true, + | Self::Grok4 + | Self::GrokCodeFast1 => true, Model::Custom { .. } => false, } } From 9614b72b06870ac36e8f2ff33ed55d2a6cbe2825 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 26 Aug 2025 19:43:07 -0300 Subject: [PATCH 363/744] thread view: Add one more UI clean up pass (#36965) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 204 ++++++++++++------------- 1 file changed, 99 insertions(+), 105 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index cd02191d4bbab5f208ebda9e6f92fc8a1aa61972..30941f9e76d7615bc6ea6c076c9eb97a0ff239cf 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1340,10 +1340,6 @@ impl AcpThreadView { window: &mut Window, cx: &Context, ) -> AnyElement { - let is_generating = self - .thread() - .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); - let primary = match &entry { AgentThreadEntry::UserMessage(message) => { let Some(editor) = self @@ -1377,14 +1373,14 @@ impl AcpThreadView { .id(("user_message", entry_ix)) .map(|this| { if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none() { - this.pt_4() + this.pt(rems_from_px(18.)) } else if rules_item.is_some() { this.pt_3() } else { this.pt_2() } }) - .pb_4() + .pb_3() .px_2() .gap_1p5() .w_full() @@ -1504,18 +1500,6 @@ impl AcpThreadView { } AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { let is_last = entry_ix + 1 == total_entries; - let pending_thinking_chunk_ix = if is_generating && is_last { - chunks - .iter() - .enumerate() - .next_back() - .filter(|(_, segment)| { - matches!(segment, AssistantMessageChunk::Thought { .. }) - }) - .map(|(index, _)| index) - } else { - None - }; let style = default_markdown_style(false, false, window, cx); let message_body = v_flex() @@ -1535,7 +1519,6 @@ impl AcpThreadView { entry_ix, chunk_ix, md.clone(), - Some(chunk_ix) == pending_thinking_chunk_ix, window, cx, ) @@ -1548,7 +1531,7 @@ impl AcpThreadView { v_flex() .px_5() - .py_1() + .py_1p5() .when(is_last, |this| this.pb_4()) .w_full() .text_ui(cx) @@ -1634,7 +1617,6 @@ impl AcpThreadView { entry_ix: usize, chunk_ix: usize, chunk: Entity, - pending: bool, window: &Window, cx: &Context, ) -> AnyElement { @@ -1657,7 +1639,6 @@ impl AcpThreadView { .when_some(scroll_handle, |this, scroll_handle| { this.track_scroll(&scroll_handle) }) - .when(!is_open, |this| this.max_h_12().opacity(0.6)) .text_ui_sm(cx) .overflow_hidden() .child( @@ -1673,10 +1654,11 @@ impl AcpThreadView { .group(&card_header_id) .relative() .w_full() + .pr_1() .justify_between() .child( h_flex() - .h(window.line_height()) + .h(window.line_height() - px(2.)) .gap_1p5() .overflow_hidden() .child( @@ -1688,13 +1670,7 @@ impl AcpThreadView { div() .text_size(self.tool_name_font_size()) .text_color(cx.theme().colors().text_muted) - .map(|this| { - if pending { - this.child("Thinking") - } else { - this.child("Thought") - } - }), + .child("Thinking"), ), ) .child( @@ -1727,7 +1703,6 @@ impl AcpThreadView { .when(is_open, |this| { this.child( div() - .relative() .ml_1p5() .pl_3p5() .border_l_1() @@ -1815,25 +1790,27 @@ impl AcpThreadView { let tool_output_display = if is_open { match &tool_call.status { - ToolCallStatus::WaitingForConfirmation { options, .. } => { - v_flex() - .w_full() - .children(tool_call.content.iter().map(|content| { - div() - .child(self.render_tool_call_content( - entry_ix, content, tool_call, window, cx, - )) - .into_any_element() - })) - .child(self.render_permission_buttons( - options, - entry_ix, - tool_call.id.clone(), - tool_call.content.is_empty(), - cx, - )) - .into_any() - } + ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex() + .w_full() + .children(tool_call.content.iter().map(|content| { + div() + .child(self.render_tool_call_content( + entry_ix, + content, + tool_call, + use_card_layout, + window, + cx, + )) + .into_any_element() + })) + .child(self.render_permission_buttons( + options, + entry_ix, + tool_call.id.clone(), + cx, + )) + .into_any(), ToolCallStatus::Pending | ToolCallStatus::InProgress if is_edit && tool_call.content.is_empty() @@ -1848,9 +1825,14 @@ impl AcpThreadView { | ToolCallStatus::Canceled => v_flex() .w_full() .children(tool_call.content.iter().map(|content| { - div().child( - self.render_tool_call_content(entry_ix, content, tool_call, window, cx), - ) + div().child(self.render_tool_call_content( + entry_ix, + content, + tool_call, + use_card_layout, + window, + cx, + )) })) .into_any(), ToolCallStatus::Rejected => Empty.into_any(), @@ -1863,7 +1845,7 @@ impl AcpThreadView { v_flex() .map(|this| { if use_card_layout { - this.my_2() + this.my_1p5() .rounded_md() .border_1() .border_color(self.tool_card_border_color(cx)) @@ -1890,18 +1872,14 @@ impl AcpThreadView { .justify_between() .when(use_card_layout, |this| { this.p_0p5() - .rounded_t_md() + .rounded_t(rems_from_px(5.)) .bg(self.tool_card_header_bg(cx)) - .when(is_open && !failed_or_canceled, |this| { - this.border_b_1() - .border_color(self.tool_card_border_color(cx)) - }) }) .child( h_flex() .relative() .w_full() - .h(window.line_height()) + .h(window.line_height() - px(2.)) .text_size(self.tool_name_font_size()) .gap_1p5() .when(has_location || use_card_layout, |this| this.px_1()) @@ -1989,6 +1967,7 @@ impl AcpThreadView { entry_ix: usize, content: &ToolCallContent, tool_call: &ToolCall, + card_layout: bool, window: &Window, cx: &Context, ) -> AnyElement { @@ -1997,7 +1976,13 @@ impl AcpThreadView { if let Some(resource_link) = content.resource_link() { self.render_resource_link(resource_link, cx) } else if let Some(markdown) = content.markdown() { - self.render_markdown_output(markdown.clone(), tool_call.id.clone(), window, cx) + self.render_markdown_output( + markdown.clone(), + tool_call.id.clone(), + card_layout, + window, + cx, + ) } else { Empty.into_any_element() } @@ -2013,6 +1998,7 @@ impl AcpThreadView { &self, markdown: Entity, tool_call_id: acp::ToolCallId, + card_layout: bool, window: &Window, cx: &Context, ) -> AnyElement { @@ -2020,26 +2006,35 @@ impl AcpThreadView { v_flex() .mt_1p5() - .ml(rems(0.4)) - .px_3p5() .gap_2() - .border_l_1() - .border_color(self.tool_card_border_color(cx)) + .when(!card_layout, |this| { + this.ml(rems(0.4)) + .px_3p5() + .border_l_1() + .border_color(self.tool_card_border_color(cx)) + }) + .when(card_layout, |this| { + this.p_2() + .border_t_1() + .border_color(self.tool_card_border_color(cx)) + }) .text_sm() .text_color(cx.theme().colors().text_muted) .child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx))) - .child( - IconButton::new(button_id, IconName::ChevronUp) - .full_width() - .style(ButtonStyle::Outlined) - .icon_color(Color::Muted) - .on_click(cx.listener({ - move |this: &mut Self, _, _, cx: &mut Context| { - this.expanded_tool_calls.remove(&tool_call_id); - cx.notify(); - } - })), - ) + .when(!card_layout, |this| { + this.child( + IconButton::new(button_id, IconName::ChevronUp) + .full_width() + .style(ButtonStyle::Outlined) + .icon_color(Color::Muted) + .on_click(cx.listener({ + move |this: &mut Self, _, _, cx: &mut Context| { + this.expanded_tool_calls.remove(&tool_call_id); + cx.notify(); + } + })), + ) + }) .into_any_element() } @@ -2107,7 +2102,6 @@ impl AcpThreadView { options: &[acp::PermissionOption], entry_ix: usize, tool_call_id: acp::ToolCallId, - empty_content: bool, cx: &Context, ) -> Div { h_flex() @@ -2117,10 +2111,8 @@ impl AcpThreadView { .gap_1() .justify_between() .flex_wrap() - .when(!empty_content, |this| { - this.border_t_1() - .border_color(self.tool_card_border_color(cx)) - }) + .border_t_1() + .border_color(self.tool_card_border_color(cx)) .child( div() .min_w(rems_from_px(145.)) @@ -2218,6 +2210,8 @@ impl AcpThreadView { v_flex() .h_full() + .border_t_1() + .border_color(self.tool_card_border_color(cx)) .child( if let Some(entry) = self.entry_view_state.read(cx).entry(entry_ix) && let Some(editor) = entry.editor_for_diff(diff) @@ -2350,6 +2344,28 @@ impl AcpThreadView { ), ) }) + .child( + Disclosure::new( + SharedString::from(format!( + "terminal-tool-disclosure-{}", + terminal.entity_id() + )), + is_expanded, + ) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronDown) + .visible_on_hover(&header_group) + .on_click(cx.listener({ + let id = tool_call.id.clone(); + move |this, _event, _window, _cx| { + if is_expanded { + this.expanded_tool_calls.remove(&id); + } else { + this.expanded_tool_calls.insert(id.clone()); + } + } + })), + ) .when(truncated_output, |header| { let tooltip = if let Some(output) = output { if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { @@ -2392,28 +2408,6 @@ impl AcpThreadView { .size(LabelSize::XSmall), ) }) - .child( - Disclosure::new( - SharedString::from(format!( - "terminal-tool-disclosure-{}", - terminal.entity_id() - )), - is_expanded, - ) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .visible_on_hover(&header_group) - .on_click(cx.listener({ - let id = tool_call.id.clone(); - move |this, _event, _window, _cx| { - if is_expanded { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - } - })), - ) .when(tool_failed || command_failed, |header| { header.child( div() @@ -2440,7 +2434,7 @@ impl AcpThreadView { let show_output = is_expanded && terminal_view.is_some(); v_flex() - .my_2() + .my_1p5() .mx_5() .border_1() .when(tool_failed || command_failed, |card| card.border_dashed()) From d713390366f6d9730baacb7b077b1546a8662f4f Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 26 Aug 2025 19:35:29 -0400 Subject: [PATCH 364/744] Add get stable channel release notes script (#36969) Release Notes: - N/A --- script/get-preview-channel-changes | 41 +++------- script/get-stable-channel-release-notes | 101 ++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 29 deletions(-) create mode 100755 script/get-stable-channel-release-notes diff --git a/script/get-preview-channel-changes b/script/get-preview-channel-changes index 8e7dcf9b1e1172d846b1204a1f8ff9e0a34b5c8a..14cd80f1550aec9784d3a24f89f577adc275632f 100755 --- a/script/get-preview-channel-changes +++ b/script/get-preview-channel-changes @@ -1,12 +1,11 @@ #!/usr/bin/env node --redirect-warnings=/dev/null const { execFileSync } = require("child_process"); -let { GITHUB_ACCESS_TOKEN } = process.env; +const { GITHUB_ACCESS_TOKEN } = process.env; const GITHUB_URL = "https://github.com"; const SKIPPABLE_NOTE_REGEX = /^\s*-?\s*n\/?a\s*/ims; const PULL_REQUEST_WEB_URL = "https://github.com/zed-industries/zed/pull"; -const PULL_REQUEST_API_URL = - "https://api.github.com/repos/zed-industries/zed/pulls"; +const PULL_REQUEST_API_URL = "https://api.github.com/repos/zed-industries/zed/pulls"; const DIVIDER = "-".repeat(80); main(); @@ -25,15 +24,12 @@ async function main() { const STAFF_MEMBERS = new Set( ( await ( - await fetch( - "https://api.github.com/orgs/zed-industries/teams/staff/members", - { - headers: { - Authorization: `token ${GITHUB_ACCESS_TOKEN}`, - Accept: "application/vnd.github+json", - }, + await fetch("https://api.github.com/orgs/zed-industries/teams/staff/members", { + headers: { + Authorization: `token ${GITHUB_ACCESS_TOKEN}`, + Accept: "application/vnd.github+json", }, - ) + }) ).json() ).map(({ login }) => login.toLowerCase()), ); @@ -44,11 +40,7 @@ async function main() { }; // Get the last two preview tags - const [newTag, oldTag] = execFileSync( - "git", - ["tag", "--sort", "-committerdate"], - { encoding: "utf8" }, - ) + const [newTag, oldTag] = execFileSync("git", ["tag", "--sort", "-committerdate"], { encoding: "utf8" }) .split("\n") .filter((t) => t.startsWith("v") && t.endsWith("-pre")); @@ -59,14 +51,10 @@ async function main() { const pullRequestNumbers = getPullRequestNumbers(oldTag, newTag); // Get the PRs that were cherry-picked between main and the old tag. - const existingPullRequestNumbers = new Set( - getPullRequestNumbers("main", oldTag), - ); + const existingPullRequestNumbers = new Set(getPullRequestNumbers("main", oldTag)); // Filter out those existing PRs from the set of new PRs. - const newPullRequestNumbers = pullRequestNumbers.filter( - (number) => !existingPullRequestNumbers.has(number), - ); + const newPullRequestNumbers = pullRequestNumbers.filter((number) => !existingPullRequestNumbers.has(number)); // Fetch the pull requests from the GitHub API. console.log("Merged Pull requests:"); @@ -84,8 +72,7 @@ async function main() { const releaseNotesHeader = /^\s*Release Notes:(.+)/ims; const releaseNotes = pullRequest.body || ""; - let contributor = - pullRequest.user?.login ?? "Unable to identify contributor"; + let contributor = pullRequest.user?.login ?? "Unable to identify contributor"; const captures = releaseNotesHeader.exec(releaseNotes); let notes = captures ? captures[1] : "MISSING"; notes = notes.trim(); @@ -127,11 +114,7 @@ function getCreditString(pullRequestNumber, contributor, isStaff) { } function getPullRequestNumbers(oldTag, newTag) { - const pullRequestNumbers = execFileSync( - "git", - ["log", `${oldTag}..${newTag}`, "--oneline"], - { encoding: "utf8" }, - ) + const pullRequestNumbers = execFileSync("git", ["log", `${oldTag}..${newTag}`, "--oneline"], { encoding: "utf8" }) .split("\n") .filter((line) => line.length > 0) .map((line) => { diff --git a/script/get-stable-channel-release-notes b/script/get-stable-channel-release-notes new file mode 100755 index 0000000000000000000000000000000000000000..b16bc9e41f3111821180ce7844e3a804e5d0a9d7 --- /dev/null +++ b/script/get-stable-channel-release-notes @@ -0,0 +1,101 @@ +#!/usr/bin/env node --redirect-warnings=/dev/null + +// This script should be ran before `bump-zed-minor-versions` + +// Prints the changelogs for all preview releases associated with the most +// recent preview minor version. + +// Future TODO: Have the script perform deduplication of lines that were +// included in both past stable and preview patches that shouldn't be mentioned +// again in this week's stable minor release. + +// Future TODO: Get changelogs for latest cherry-picked commits on preview and +// stable that didn't make it into a release, as they were cherry picked + +const { execFileSync } = require("child_process"); +const { GITHUB_ACCESS_TOKEN } = process.env; +const GITHUB_TAGS_API_URL = "https://api.github.com/repos/zed-industries/zed/releases/tags"; +const DIVIDER = "-".repeat(80); + +main(); + +async function main() { + if (!GITHUB_ACCESS_TOKEN) { + try { + GITHUB_ACCESS_TOKEN = execFileSync("gh", ["auth", "token"]).toString(); + } catch (error) { + console.log(error); + console.log("No GITHUB_ACCESS_TOKEN and no `gh auth token`"); + process.exit(1); + } + } + + const allTags = execFileSync("git", ["tag", "--sort", "-committerdate"], { encoding: "utf8" }) + .split("\n") + .filter((t) => t.length > 0); + const latestPreviewTag = allTags.filter((t) => t.startsWith("v") && t.endsWith("-pre"))[0]; + const latestPreviewMinorVersion = latestPreviewTag.split(".")[1]; + const latestPreviewTagRegex = new RegExp(`^v(\\d+)\\.(${latestPreviewMinorVersion})\\.(\\d+)-pre$`); + + const parsedPreviewTags = allTags + .map((tag) => { + const match = tag.match(latestPreviewTagRegex); + if (match) { + return { + tag, + version: { + major: parseInt(match[1]), + minor: parseInt(match[2]), + patch: parseInt(match[3]), + }, + }; + } + return null; + }) + .filter((item) => item !== null) + .sort((a, b) => a.version.patch - b.version.patch); + + const matchingPreviewTags = parsedPreviewTags.map((item) => item.tag); + + console.log("Fetching release information for preview tags:"); + console.log(DIVIDER); + + for (const tag of matchingPreviewTags) { + const releaseApiUrl = `${GITHUB_TAGS_API_URL}/${tag}`; + + try { + const response = await fetch(releaseApiUrl, { + headers: { + Authorization: `token ${GITHUB_ACCESS_TOKEN}`, + }, + }); + + if (!response.ok) { + console.log(`Failed to fetch release for ${tag}: ${response.status}`); + continue; + } + + const release = await response.json(); + + console.log(`\nRelease: ${release.name || tag}`); + console.log(`Tag: ${tag}`); + console.log(`Published: ${release.published_at}`); + console.log(`URL: ${release.html_url}`); + console.log("\nRelease Notes:"); + console.log(release.body || "No release notes"); + console.log(DIVIDER); + } catch (error) { + console.log(`Error fetching release for ${tag}:`, error.message); + } + } + + const patchUpdateTags = parsedPreviewTags.filter((tag) => tag.version.patch != 0).map((tag) => tag.tag); + + console.log(); + console.log("Please review the release notes associated with the following patch versions:"); + for (const tag of patchUpdateTags) { + console.log(`- ${tag}`); + } + console.log("Remove items that have already been mentioned in the current published stable versions."); + console.log("https://github.com/zed-industries/zed/releases?q=prerelease%3Afalse&expanded=true"); +} From 1eae76e85638fbf6da433d008fda6a29ae954ed5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Aug 2025 17:15:39 -0700 Subject: [PATCH 365/744] Restructure remote client crate, consolidate SSH logic (#36967) This is a pure refactor that consolidates all SSH remoting logic such that it should be straightforward to add another transport to the remoting system. Release Notes: - N/A --------- Co-authored-by: Mikayla Maki --- crates/agent_ui/src/inline_prompt_editor.rs | 2 +- crates/call/src/call_impl/room.rs | 2 +- .../remote_editing_collaboration_tests.rs | 22 +- crates/collab/src/tests/test_server.rs | 6 +- crates/debugger_ui/src/session/running.rs | 9 +- crates/editor/src/editor.rs | 6 +- crates/extension_host/src/extension_host.rs | 18 +- crates/file_finder/src/file_finder.rs | 2 +- crates/language_tools/src/lsp_log.rs | 6 +- crates/outline_panel/src/outline_panel.rs | 6 +- crates/project/src/debugger/dap_store.rs | 108 +- crates/project/src/git_store.rs | 68 +- crates/project/src/project.rs | 311 +- crates/project/src/terminals.rs | 355 +-- crates/project/src/worktree_store.rs | 6 +- crates/project_panel/src/project_panel.rs | 4 +- crates/proto/src/proto.rs | 4 +- .../src/disconnected_overlay.rs | 4 +- crates/recent_projects/src/remote_servers.rs | 8 +- crates/recent_projects/src/ssh_connections.rs | 19 +- crates/remote/src/protocol.rs | 10 + crates/remote/src/remote.rs | 10 +- crates/remote/src/remote_client.rs | 1478 +++++++++ crates/remote/src/ssh_session.rs | 2749 ----------------- crates/remote/src/transport.rs | 1 + crates/remote/src/transport/ssh.rs | 1358 ++++++++ crates/remote_server/src/headless_project.rs | 52 +- .../remote_server/src/remote_editing_tests.rs | 12 +- crates/remote_server/src/unix.rs | 25 +- crates/task/src/shell_builder.rs | 15 +- crates/terminal_view/src/terminal_element.rs | 2 +- crates/terminal_view/src/terminal_panel.rs | 29 +- crates/title_bar/src/collab.rs | 2 +- crates/title_bar/src/title_bar.rs | 12 +- crates/vim/src/command.rs | 4 +- crates/workspace/src/tasks.rs | 2 +- crates/workspace/src/workspace.rs | 14 +- crates/zed/src/reliability.rs | 4 +- crates/zed/src/zed.rs | 4 +- 39 files changed, 3334 insertions(+), 3415 deletions(-) create mode 100644 crates/remote/src/remote_client.rs delete mode 100644 crates/remote/src/ssh_session.rs create mode 100644 crates/remote/src/transport.rs create mode 100644 crates/remote/src/transport/ssh.rs diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index a626122769f656cf6627d104d00f4fa3a368e7db..3abefac8e8964ffdddc1397132541d0056f33ea8 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -334,7 +334,7 @@ impl PromptEditor { EditorEvent::Edited { .. } => { if let Some(workspace) = window.root::().flatten() { workspace.update(cx, |workspace, cx| { - let is_via_ssh = workspace.project().read(cx).is_via_ssh(); + let is_via_ssh = workspace.project().read(cx).is_via_remote_server(); workspace .client() diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index ffe4c6c25191dc8f9087ccfcc77252b8e5a25a13..c31a458c64124c266c56a7004746d7b6a0a4adc6 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -1161,7 +1161,7 @@ impl Room { let request = self.client.request(proto::ShareProject { room_id: self.id(), worktrees: project.read(cx).worktree_metadata_protos(cx), - is_ssh_project: project.read(cx).is_via_ssh(), + is_ssh_project: project.read(cx).is_via_remote_server(), }); cx.spawn(async move |this, cx| { diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 8ab6e6910c88880bc8b6451d972e39b5c2315812..6b46459a59b16717d965b42c4e19820f6d1dc062 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -26,7 +26,7 @@ use project::{ debugger::session::ThreadId, lsp_store::{FormatTrigger, LspFormatTarget}, }; -use remote::SshRemoteClient; +use remote::RemoteClient; use remote_server::{HeadlessAppState, HeadlessProject}; use rpc::proto; use serde_json::json; @@ -59,7 +59,7 @@ async fn test_sharing_an_ssh_remote_project( .await; // Set up project on remote FS - let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); remote_fs .insert_tree( @@ -101,7 +101,7 @@ async fn test_sharing_an_ssh_remote_project( ) }); - let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::fake_client(opts, cx_a).await; let (project_a, worktree_id) = client_a .build_ssh_project(path!("/code/project1"), client_ssh, cx_a) .await; @@ -235,7 +235,7 @@ async fn test_ssh_collaboration_git_branches( .await; // Set up project on remote FS - let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); remote_fs .insert_tree("/project", serde_json::json!({ ".git":{} })) @@ -268,7 +268,7 @@ async fn test_ssh_collaboration_git_branches( ) }); - let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::fake_client(opts, cx_a).await; let (project_a, _) = client_a .build_ssh_project("/project", client_ssh, cx_a) .await; @@ -420,7 +420,7 @@ async fn test_ssh_collaboration_formatting_with_prettier( .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; - let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); let buffer_text = "let one = \"two\""; let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; @@ -473,7 +473,7 @@ async fn test_ssh_collaboration_formatting_with_prettier( ) }); - let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::fake_client(opts, cx_a).await; let (project_a, worktree_id) = client_a .build_ssh_project(path!("/project"), client_ssh, cx_a) .await; @@ -602,7 +602,7 @@ async fn test_remote_server_debugger( release_channel::init(SemanticVersion::default(), cx); dap_adapters::init(cx); }); - let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); remote_fs .insert_tree( @@ -633,7 +633,7 @@ async fn test_remote_server_debugger( ) }); - let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::fake_client(opts, cx_a).await; let mut server = TestServer::start(server_cx.executor()).await; let client_a = server.create_client(cx_a, "user_a").await; cx_a.update(|cx| { @@ -711,7 +711,7 @@ async fn test_slow_adapter_startup_retries( release_channel::init(SemanticVersion::default(), cx); dap_adapters::init(cx); }); - let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx); + let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx); let remote_fs = FakeFs::new(server_cx.executor()); remote_fs .insert_tree( @@ -742,7 +742,7 @@ async fn test_slow_adapter_startup_retries( ) }); - let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await; + let client_ssh = RemoteClient::fake_client(opts, cx_a).await; let mut server = TestServer::start(server_cx.executor()).await; let client_a = server.create_client(cx_a, "user_a").await; cx_a.update(|cx| { diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index fd5e3eefc158034e8b15dc3fd7e401b1041fe08e..eb7df28478158a10a0c2d52c3560cad391937383 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -26,7 +26,7 @@ use node_runtime::NodeRuntime; use notifications::NotificationStore; use parking_lot::Mutex; use project::{Project, WorktreeId}; -use remote::SshRemoteClient; +use remote::RemoteClient; use rpc::{ RECEIVE_TIMEOUT, proto::{self, ChannelRole}, @@ -765,11 +765,11 @@ impl TestClient { pub async fn build_ssh_project( &self, root_path: impl AsRef, - ssh: Entity, + ssh: Entity, cx: &mut TestAppContext, ) -> (Entity, WorktreeId) { let project = cx.update(|cx| { - Project::ssh( + Project::remote( ssh, self.client().clone(), self.app_state.node_runtime.clone(), diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 9991395f351dd3cd6d6a7f6d95ded11024ba6a4e..a0e7c9a10173c31edc193fd0309913b60c6f8a95 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -916,10 +916,11 @@ impl RunningState { let task_store = project.read(cx).task_store().downgrade(); let weak_project = project.downgrade(); let weak_workspace = workspace.downgrade(); - let ssh_info = project + let remote_shell = project .read(cx) - .ssh_client() - .and_then(|it| it.read(cx).ssh_info()); + .remote_client() + .as_ref() + .and_then(|remote| remote.read(cx).shell()); cx.spawn_in(window, async move |this, cx| { let DebugScenario { @@ -1003,7 +1004,7 @@ impl RunningState { None }; - let builder = ShellBuilder::new(ssh_info.as_ref().map(|info| &*info.shell), &task.resolved.shell); + let builder = ShellBuilder::new(remote_shell.as_deref(), &task.resolved.shell); let command_label = builder.command_label(&task.resolved.command_label); let (command, args) = builder.build(task.resolved.command.clone(), &task.resolved.args); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 80680ae9c00999bd91eb1ad66971259f587752ac..52549902dd603ee8ffdc7c50dd331c87c95828cb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -20074,7 +20074,7 @@ impl Editor { let (telemetry, is_via_ssh) = { let project = project.read(cx); let telemetry = project.client().telemetry().clone(); - let is_via_ssh = project.is_via_ssh(); + let is_via_ssh = project.is_via_remote_server(); (telemetry, is_via_ssh) }; refresh_linked_ranges(self, window, cx); @@ -20642,7 +20642,7 @@ impl Editor { copilot_enabled, copilot_enabled_for_language, edit_predictions_provider, - is_via_ssh = project.is_via_ssh(), + is_via_ssh = project.is_via_remote_server(), ); } else { telemetry::event!( @@ -20652,7 +20652,7 @@ impl Editor { copilot_enabled, copilot_enabled_for_language, edit_predictions_provider, - is_via_ssh = project.is_via_ssh(), + is_via_ssh = project.is_via_remote_server(), ); }; } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index fde0aeac9405d114f9cee89ca054d4503a35d482..b8189c36511a03f136e5e215549453947e888bb1 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -43,7 +43,7 @@ use language::{ use node_runtime::NodeRuntime; use project::ContextProviderWithTasks; use release_channel::ReleaseChannel; -use remote::SshRemoteClient; +use remote::RemoteClient; use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -117,7 +117,7 @@ pub struct ExtensionStore { pub wasm_host: Arc, pub wasm_extensions: Vec<(Arc, WasmExtension)>, pub tasks: Vec>, - pub ssh_clients: HashMap>, + pub remote_clients: HashMap>, pub ssh_registered_tx: UnboundedSender<()>, } @@ -270,7 +270,7 @@ impl ExtensionStore { reload_tx, tasks: Vec::new(), - ssh_clients: HashMap::default(), + remote_clients: HashMap::default(), ssh_registered_tx: connection_registered_tx, }; @@ -1693,7 +1693,7 @@ impl ExtensionStore { async fn sync_extensions_over_ssh( this: &WeakEntity, - client: WeakEntity, + client: WeakEntity, cx: &mut AsyncApp, ) -> Result<()> { let extensions = this.update(cx, |this, _cx| { @@ -1765,8 +1765,8 @@ impl ExtensionStore { pub async fn update_ssh_clients(this: &WeakEntity, cx: &mut AsyncApp) -> Result<()> { let clients = this.update(cx, |this, _cx| { - this.ssh_clients.retain(|_k, v| v.upgrade().is_some()); - this.ssh_clients.values().cloned().collect::>() + this.remote_clients.retain(|_k, v| v.upgrade().is_some()); + this.remote_clients.values().cloned().collect::>() })?; for client in clients { @@ -1778,17 +1778,17 @@ impl ExtensionStore { anyhow::Ok(()) } - pub fn register_ssh_client(&mut self, client: Entity, cx: &mut Context) { + pub fn register_remote_client(&mut self, client: Entity, cx: &mut Context) { let connection_options = client.read(cx).connection_options(); let ssh_url = connection_options.ssh_url(); - if let Some(existing_client) = self.ssh_clients.get(&ssh_url) + if let Some(existing_client) = self.remote_clients.get(&ssh_url) && existing_client.upgrade().is_some() { return; } - self.ssh_clients.insert(ssh_url, client.downgrade()); + self.remote_clients.insert(ssh_url, client.downgrade()); self.ssh_registered_tx.unbounded_send(()).ok(); } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 75121523245cc9233fa4c66c9021d8a958524f6e..e2f8d55cf2a0c8f21a1ed6b3eab870d267173bba 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1381,7 +1381,7 @@ impl PickerDelegate for FileFinderDelegate { project .worktree_for_id(history_item.project.worktree_id, cx) .is_some() - || ((project.is_local() || project.is_via_ssh()) + || ((project.is_local() || project.is_via_remote_server()) && history_item.absolute.is_some()) }), self.currently_opened_path.as_ref(), diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index d5206c1f264b3b49f504090154f2df6e4ddf55be..a71e434e5274392add0463830519834202b7ba58 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -222,7 +222,7 @@ pub fn init(cx: &mut App) { cx.observe_new(move |workspace: &mut Workspace, _, cx| { let project = workspace.project(); - if project.read(cx).is_local() || project.read(cx).is_via_ssh() { + if project.read(cx).is_local() || project.read(cx).is_via_remote_server() { log_store.update(cx, |store, cx| { store.add_project(project, cx); }); @@ -231,7 +231,7 @@ pub fn init(cx: &mut App) { let log_store = log_store.clone(); workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| { let project = workspace.project().read(cx); - if project.is_local() || project.is_via_ssh() { + if project.is_local() || project.is_via_remote_server() { let project = workspace.project().clone(); let log_store = log_store.clone(); get_or_create_tool( @@ -321,7 +321,7 @@ impl LogStore { .retain(|_, state| state.kind.project() != Some(&weak_project)); }), cx.subscribe(project, |this, project, event, cx| { - let server_kind = if project.read(cx).is_via_ssh() { + let server_kind = if project.read(cx).is_via_remote_server() { LanguageServerKind::Remote { project: project.downgrade(), } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 10698cead8656885d0ea2d2f98ebd235e29fec1b..1521d012955050c932d2d2f797a25ab8f3c99d48 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -5102,9 +5102,9 @@ impl EventEmitter for OutlinePanel {} impl Render for OutlinePanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let (is_local, is_via_ssh) = self - .project - .read_with(cx, |project, _| (project.is_local(), project.is_via_ssh())); + let (is_local, is_via_ssh) = self.project.read_with(cx, |project, _| { + (project.is_local(), project.is_via_remote_server()) + }); let query = self.query(cx); let pinned = self.pinned; let settings = OutlinePanelSettings::get_global(cx); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 2906c32ff4f67ba733d4f0faf8f511d0c433ec91..d8c6d3acc1116e9a97b2f6ca3fc54ec098029cbe 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -5,11 +5,8 @@ use super::{ session::{self, Session, SessionStateEvent}, }; use crate::{ - InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, - debugger::session::SessionQuirks, - project_settings::ProjectSettings, - terminals::{SshCommand, wrap_for_ssh}, - worktree_store::WorktreeStore, + InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, debugger::session::SessionQuirks, + project_settings::ProjectSettings, worktree_store::WorktreeStore, }; use anyhow::{Context as _, Result, anyhow}; use async_trait::async_trait; @@ -34,7 +31,7 @@ use http_client::HttpClient; use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind}; use node_runtime::NodeRuntime; -use remote::{SshInfo, SshRemoteClient, ssh_session::SshArgs}; +use remote::RemoteClient; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self}, @@ -68,7 +65,7 @@ pub enum DapStoreEvent { enum DapStoreMode { Local(LocalDapStore), - Ssh(SshDapStore), + Remote(RemoteDapStore), Collab, } @@ -80,8 +77,8 @@ pub struct LocalDapStore { toolchain_store: Arc, } -pub struct SshDapStore { - ssh_client: Entity, +pub struct RemoteDapStore { + remote_client: Entity, upstream_client: AnyProtoClient, upstream_project_id: u64, } @@ -147,16 +144,16 @@ impl DapStore { Self::new(mode, breakpoint_store, worktree_store, cx) } - pub fn new_ssh( + pub fn new_remote( project_id: u64, - ssh_client: Entity, + remote_client: Entity, breakpoint_store: Entity, worktree_store: Entity, cx: &mut Context, ) -> Self { - let mode = DapStoreMode::Ssh(SshDapStore { - upstream_client: ssh_client.read(cx).proto_client(), - ssh_client, + let mode = DapStoreMode::Remote(RemoteDapStore { + upstream_client: remote_client.read(cx).proto_client(), + remote_client, upstream_project_id: project_id, }); @@ -242,64 +239,51 @@ impl DapStore { Ok(binary) }) } - DapStoreMode::Ssh(ssh) => { - let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary { - session_id: session_id.to_proto(), - project_id: ssh.upstream_project_id, - worktree_id: worktree.read(cx).id().to_proto(), - definition: Some(definition.to_proto()), - }); - let ssh_client = ssh.ssh_client.clone(); + DapStoreMode::Remote(remote) => { + let request = remote + .upstream_client + .request(proto::GetDebugAdapterBinary { + session_id: session_id.to_proto(), + project_id: remote.upstream_project_id, + worktree_id: worktree.read(cx).id().to_proto(), + definition: Some(definition.to_proto()), + }); + let remote = remote.remote_client.clone(); cx.spawn(async move |_, cx| { let response = request.await?; let binary = DebugAdapterBinary::from_proto(response)?; - let (mut ssh_command, envs, path_style, ssh_shell) = - ssh_client.read_with(cx, |ssh, _| { - let SshInfo { - args: SshArgs { arguments, envs }, - path_style, - shell, - } = ssh.ssh_info().context("SSH arguments not found")?; - anyhow::Ok(( - SshCommand { arguments }, - envs.unwrap_or_default(), - path_style, - shell, - )) - })??; - - let mut connection = None; - if let Some(c) = binary.connection { - let local_bind_addr = Ipv4Addr::LOCALHOST; - let port = - dap::transport::TcpTransport::unused_port(local_bind_addr).await?; - ssh_command.add_port_forwarding(port, c.host.to_string(), c.port); + let port_forwarding; + let connection; + if let Some(c) = binary.connection { + let host = Ipv4Addr::LOCALHOST; + let port = dap::transport::TcpTransport::unused_port(host).await?; + port_forwarding = Some((port, c.host.to_string(), c.port)); connection = Some(TcpArguments { port, - host: local_bind_addr, + host, timeout: c.timeout, }) + } else { + port_forwarding = None; + connection = None; } - let (program, args) = wrap_for_ssh( - &ssh_shell, - &ssh_command, - binary - .command - .as_ref() - .map(|command| (command, &binary.arguments)), - binary.cwd.as_deref(), - binary.envs, - None, - path_style, - ); + let command = remote.read_with(cx, |remote, _cx| { + remote.build_command( + binary.command, + &binary.arguments, + &binary.envs, + binary.cwd.map(|path| path.display().to_string()), + port_forwarding, + ) + })??; Ok(DebugAdapterBinary { - command: Some(program), - arguments: args, - envs, + command: Some(command.program), + arguments: command.args, + envs: command.env, cwd: None, connection, request_args: binary.request_args, @@ -365,9 +349,9 @@ impl DapStore { ))) } } - DapStoreMode::Ssh(ssh) => { - let request = ssh.upstream_client.request(proto::RunDebugLocators { - project_id: ssh.upstream_project_id, + DapStoreMode::Remote(remote) => { + let request = remote.upstream_client.request(proto::RunDebugLocators { + project_id: remote.upstream_project_id, build_command: Some(build_command.to_proto()), locator: locator_name.to_owned(), }); diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 5cf298a8bff1dbecbe5899020e4463ba658ed719..a1c0508c3ed5355685828487229967c83b59cbd3 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -44,7 +44,7 @@ use parking_lot::Mutex; use postage::stream::Stream as _; use rpc::{ AnyProtoClient, TypedEnvelope, - proto::{self, FromProto, SSH_PROJECT_ID, ToProto, git_reset, split_repository_update}, + proto::{self, FromProto, ToProto, git_reset, split_repository_update}, }; use serde::Deserialize; use std::{ @@ -141,14 +141,10 @@ enum GitStoreState { project_environment: Entity, fs: Arc, }, - Ssh { - upstream_client: AnyProtoClient, - upstream_project_id: ProjectId, - downstream: Option<(AnyProtoClient, ProjectId)>, - }, Remote { upstream_client: AnyProtoClient, - upstream_project_id: ProjectId, + upstream_project_id: u64, + downstream: Option<(AnyProtoClient, ProjectId)>, }, } @@ -355,7 +351,7 @@ impl GitStore { worktree_store: &Entity, buffer_store: Entity, upstream_client: AnyProtoClient, - project_id: ProjectId, + project_id: u64, cx: &mut Context, ) -> Self { Self::new( @@ -364,23 +360,6 @@ impl GitStore { GitStoreState::Remote { upstream_client, upstream_project_id: project_id, - }, - cx, - ) - } - - pub fn ssh( - worktree_store: &Entity, - buffer_store: Entity, - upstream_client: AnyProtoClient, - cx: &mut Context, - ) -> Self { - Self::new( - worktree_store.clone(), - buffer_store, - GitStoreState::Ssh { - upstream_client, - upstream_project_id: ProjectId(SSH_PROJECT_ID), downstream: None, }, cx, @@ -451,7 +430,7 @@ impl GitStore { pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context) { match &mut self.state { - GitStoreState::Ssh { + GitStoreState::Remote { downstream: downstream_client, .. } => { @@ -527,9 +506,6 @@ impl GitStore { }), }); } - GitStoreState::Remote { .. } => { - debug_panic!("shared called on remote store"); - } } } @@ -541,15 +517,12 @@ impl GitStore { } => { downstream_client.take(); } - GitStoreState::Ssh { + GitStoreState::Remote { downstream: downstream_client, .. } => { downstream_client.take(); } - GitStoreState::Remote { .. } => { - debug_panic!("unshared called on remote store"); - } } self.shared_diffs.clear(); } @@ -1047,21 +1020,17 @@ impl GitStore { } => downstream_client .as_ref() .map(|state| (state.client.clone(), state.project_id)), - GitStoreState::Ssh { + GitStoreState::Remote { downstream: downstream_client, .. } => downstream_client.clone(), - GitStoreState::Remote { .. } => None, } } fn upstream_client(&self) -> Option { match &self.state { GitStoreState::Local { .. } => None, - GitStoreState::Ssh { - upstream_client, .. - } - | GitStoreState::Remote { + GitStoreState::Remote { upstream_client, .. } => Some(upstream_client.clone()), } @@ -1432,12 +1401,7 @@ impl GitStore { cx.background_executor() .spawn(async move { fs.git_init(&path, fallback_branch_name) }) } - GitStoreState::Ssh { - upstream_client, - upstream_project_id: project_id, - .. - } - | GitStoreState::Remote { + GitStoreState::Remote { upstream_client, upstream_project_id: project_id, .. @@ -1447,7 +1411,7 @@ impl GitStore { cx.background_executor().spawn(async move { client .request(proto::GitInit { - project_id: project_id.0, + project_id: project_id, abs_path: path.to_string_lossy().to_string(), fallback_branch_name, }) @@ -1471,13 +1435,18 @@ impl GitStore { cx.background_executor() .spawn(async move { fs.git_clone(&repo, &path).await }) } - GitStoreState::Ssh { + GitStoreState::Remote { upstream_client, upstream_project_id, .. } => { + if upstream_client.is_via_collab() { + return Task::ready(Err(anyhow!( + "Git Clone isn't supported for project guests" + ))); + } let request = upstream_client.request(proto::GitClone { - project_id: upstream_project_id.0, + project_id: *upstream_project_id, abs_path: path.to_string_lossy().to_string(), remote_repo: repo, }); @@ -1491,9 +1460,6 @@ impl GitStore { } }) } - GitStoreState::Remote { .. } => { - Task::ready(Err(anyhow!("Git Clone isn't supported for remote users"))) - } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9fd4eed641a17bf317c7d69ef0afa0802c8ae276..9e3900198cbc9aa4845428235196763511c0751c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -42,9 +42,7 @@ pub use manifest_tree::ManifestTree; use anyhow::{Context as _, Result, anyhow}; use buffer_store::{BufferStore, BufferStoreEvent}; -use client::{ - Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore, proto, -}; +use client::{Client, Collaborator, PendingEntitySubscription, TypedEnvelope, UserStore, proto}; use clock::ReplicaId; use dap::client::DebugAdapterClient; @@ -89,10 +87,10 @@ use node_runtime::NodeRuntime; use parking_lot::Mutex; pub use prettier_store::PrettierStore; use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent}; -use remote::{SshConnectionOptions, SshRemoteClient}; +use remote::{RemoteClient, SshConnectionOptions}; use rpc::{ AnyProtoClient, ErrorCode, - proto::{FromProto, LanguageServerPromptResponse, SSH_PROJECT_ID, ToProto}, + proto::{FromProto, LanguageServerPromptResponse, REMOTE_SERVER_PROJECT_ID, ToProto}, }; use search::{SearchInputKind, SearchQuery, SearchResult}; use search_history::SearchHistory; @@ -177,12 +175,12 @@ pub struct Project { dap_store: Entity, breakpoint_store: Entity, - client: Arc, + collab_client: Arc, join_project_response_message_id: u32, task_store: Entity, user_store: Entity, fs: Arc, - ssh_client: Option>, + remote_client: Option>, client_state: ProjectClientState, git_store: Entity, collaborators: HashMap, @@ -1154,12 +1152,12 @@ impl Project { active_entry: None, snippets, languages, - client, + collab_client: client, task_store, user_store, settings_observer, fs, - ssh_client: None, + remote_client: None, breakpoint_store, dap_store, @@ -1183,8 +1181,8 @@ impl Project { }) } - pub fn ssh( - ssh: Entity, + pub fn remote( + remote: Entity, client: Arc, node: NodeRuntime, user_store: Entity, @@ -1200,10 +1198,15 @@ impl Project { let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx); - let (ssh_proto, path_style) = - ssh.read_with(cx, |ssh, _| (ssh.proto_client(), ssh.path_style())); + let (remote_proto, path_style) = + remote.read_with(cx, |remote, _| (remote.proto_client(), remote.path_style())); let worktree_store = cx.new(|_| { - WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID, path_style) + WorktreeStore::remote( + false, + remote_proto.clone(), + REMOTE_SERVER_PROJECT_ID, + path_style, + ) }); cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); @@ -1215,31 +1218,32 @@ impl Project { let buffer_store = cx.new(|cx| { BufferStore::remote( worktree_store.clone(), - ssh.read(cx).proto_client(), - SSH_PROJECT_ID, + remote.read(cx).proto_client(), + REMOTE_SERVER_PROJECT_ID, cx, ) }); let image_store = cx.new(|cx| { ImageStore::remote( worktree_store.clone(), - ssh.read(cx).proto_client(), - SSH_PROJECT_ID, + remote.read(cx).proto_client(), + REMOTE_SERVER_PROJECT_ID, cx, ) }); cx.subscribe(&buffer_store, Self::on_buffer_store_event) .detach(); - let toolchain_store = cx - .new(|cx| ToolchainStore::remote(SSH_PROJECT_ID, ssh.read(cx).proto_client(), cx)); + let toolchain_store = cx.new(|cx| { + ToolchainStore::remote(REMOTE_SERVER_PROJECT_ID, remote.read(cx).proto_client(), cx) + }); let task_store = cx.new(|cx| { TaskStore::remote( fs.clone(), buffer_store.downgrade(), worktree_store.clone(), toolchain_store.read(cx).as_language_toolchain_store(), - ssh.read(cx).proto_client(), - SSH_PROJECT_ID, + remote.read(cx).proto_client(), + REMOTE_SERVER_PROJECT_ID, cx, ) }); @@ -1262,8 +1266,8 @@ impl Project { buffer_store.clone(), worktree_store.clone(), languages.clone(), - ssh_proto.clone(), - SSH_PROJECT_ID, + remote_proto.clone(), + REMOTE_SERVER_PROJECT_ID, fs.clone(), cx, ) @@ -1271,12 +1275,12 @@ impl Project { cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); let breakpoint_store = - cx.new(|_| BreakpointStore::remote(SSH_PROJECT_ID, ssh_proto.clone())); + cx.new(|_| BreakpointStore::remote(REMOTE_SERVER_PROJECT_ID, remote_proto.clone())); let dap_store = cx.new(|cx| { - DapStore::new_ssh( - SSH_PROJECT_ID, - ssh.clone(), + DapStore::new_remote( + REMOTE_SERVER_PROJECT_ID, + remote.clone(), breakpoint_store.clone(), worktree_store.clone(), cx, @@ -1284,10 +1288,16 @@ impl Project { }); let git_store = cx.new(|cx| { - GitStore::ssh(&worktree_store, buffer_store.clone(), ssh_proto.clone(), cx) + GitStore::remote( + &worktree_store, + buffer_store.clone(), + remote_proto.clone(), + REMOTE_SERVER_PROJECT_ID, + cx, + ) }); - cx.subscribe(&ssh, Self::on_ssh_event).detach(); + cx.subscribe(&remote, Self::on_remote_client_event).detach(); let this = Self { buffer_ordered_messages_tx: tx, @@ -1306,11 +1316,13 @@ impl Project { _subscriptions: vec![ cx.on_release(Self::release), cx.on_app_quit(|this, cx| { - let shutdown = this.ssh_client.take().and_then(|client| { - client.read(cx).shutdown_processes( - Some(proto::ShutdownRemoteServer {}), - cx.background_executor().clone(), - ) + let shutdown = this.remote_client.take().and_then(|client| { + client.update(cx, |client, cx| { + client.shutdown_processes( + Some(proto::ShutdownRemoteServer {}), + cx.background_executor().clone(), + ) + }) }); cx.background_executor().spawn(async move { @@ -1323,12 +1335,12 @@ impl Project { active_entry: None, snippets, languages, - client, + collab_client: client, task_store, user_store, settings_observer, fs, - ssh_client: Some(ssh.clone()), + remote_client: Some(remote.clone()), buffers_needing_diff: Default::default(), git_diff_debouncer: DebouncedDelay::new(), terminals: Terminals { @@ -1346,52 +1358,34 @@ impl Project { agent_location: None, }; - // ssh -> local machine handlers - ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &cx.entity()); - ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store); - ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store); - ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store); - ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.dap_store); - ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer); - ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.git_store); - - ssh_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer); - ssh_proto.add_entity_message_handler(Self::handle_update_worktree); - ssh_proto.add_entity_message_handler(Self::handle_update_project); - ssh_proto.add_entity_message_handler(Self::handle_toast); - ssh_proto.add_entity_request_handler(Self::handle_language_server_prompt_request); - ssh_proto.add_entity_message_handler(Self::handle_hide_toast); - ssh_proto.add_entity_request_handler(Self::handle_update_buffer_from_ssh); - BufferStore::init(&ssh_proto); - LspStore::init(&ssh_proto); - SettingsObserver::init(&ssh_proto); - TaskStore::init(Some(&ssh_proto)); - ToolchainStore::init(&ssh_proto); - DapStore::init(&ssh_proto, cx); - GitStore::init(&ssh_proto); + // remote server -> local machine handlers + remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &cx.entity()); + remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.buffer_store); + remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.worktree_store); + remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.lsp_store); + remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.dap_store); + remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.settings_observer); + remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.git_store); + + remote_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer); + remote_proto.add_entity_message_handler(Self::handle_update_worktree); + remote_proto.add_entity_message_handler(Self::handle_update_project); + remote_proto.add_entity_message_handler(Self::handle_toast); + remote_proto.add_entity_request_handler(Self::handle_language_server_prompt_request); + remote_proto.add_entity_message_handler(Self::handle_hide_toast); + remote_proto.add_entity_request_handler(Self::handle_update_buffer_from_remote_server); + BufferStore::init(&remote_proto); + LspStore::init(&remote_proto); + SettingsObserver::init(&remote_proto); + TaskStore::init(Some(&remote_proto)); + ToolchainStore::init(&remote_proto); + DapStore::init(&remote_proto, cx); + GitStore::init(&remote_proto); this }) } - pub async fn remote( - remote_id: u64, - client: Arc, - user_store: Entity, - languages: Arc, - fs: Arc, - cx: AsyncApp, - ) -> Result> { - let project = - Self::in_room(remote_id, client, user_store, languages, fs, cx.clone()).await?; - cx.update(|cx| { - connection_manager::Manager::global(cx).update(cx, |manager, cx| { - manager.maintain_project_connection(&project, cx) - }) - })?; - Ok(project) - } - pub async fn in_room( remote_id: u64, client: Arc, @@ -1523,7 +1517,7 @@ impl Project { &worktree_store, buffer_store.clone(), client.clone().into(), - ProjectId(remote_id), + remote_id, cx, ) })?; @@ -1574,11 +1568,11 @@ impl Project { task_store, snippets, fs, - ssh_client: None, + remote_client: None, settings_observer: settings_observer.clone(), client_subscriptions: Default::default(), _subscriptions: vec![cx.on_release(Self::release)], - client: client.clone(), + collab_client: client.clone(), client_state: ProjectClientState::Remote { sharing_has_stopped: false, capability: Capability::ReadWrite, @@ -1661,11 +1655,13 @@ impl Project { } fn release(&mut self, cx: &mut App) { - if let Some(client) = self.ssh_client.take() { - let shutdown = client.read(cx).shutdown_processes( - Some(proto::ShutdownRemoteServer {}), - cx.background_executor().clone(), - ); + if let Some(client) = self.remote_client.take() { + let shutdown = client.update(cx, |client, cx| { + client.shutdown_processes( + Some(proto::ShutdownRemoteServer {}), + cx.background_executor().clone(), + ) + }); cx.background_spawn(async move { if let Some(shutdown) = shutdown { @@ -1681,7 +1677,7 @@ impl Project { let _ = self.unshare_internal(cx); } ProjectClientState::Remote { remote_id, .. } => { - let _ = self.client.send(proto::LeaveProject { + let _ = self.collab_client.send(proto::LeaveProject { project_id: *remote_id, }); self.disconnected_from_host_internal(cx); @@ -1808,11 +1804,11 @@ impl Project { } pub fn client(&self) -> Arc { - self.client.clone() + self.collab_client.clone() } - pub fn ssh_client(&self) -> Option> { - self.ssh_client.clone() + pub fn remote_client(&self) -> Option> { + self.remote_client.clone() } pub fn user_store(&self) -> Entity { @@ -1893,30 +1889,30 @@ impl Project { if self.is_local() { return true; } - if self.is_via_ssh() { + if self.is_via_remote_server() { return true; } false } - pub fn ssh_connection_state(&self, cx: &App) -> Option { - self.ssh_client + pub fn remote_connection_state(&self, cx: &App) -> Option { + self.remote_client .as_ref() - .map(|ssh| ssh.read(cx).connection_state()) + .map(|remote| remote.read(cx).connection_state()) } - pub fn ssh_connection_options(&self, cx: &App) -> Option { - self.ssh_client + pub fn remote_connection_options(&self, cx: &App) -> Option { + self.remote_client .as_ref() - .map(|ssh| ssh.read(cx).connection_options()) + .map(|remote| remote.read(cx).connection_options()) } pub fn replica_id(&self) -> ReplicaId { match self.client_state { ProjectClientState::Remote { replica_id, .. } => replica_id, _ => { - if self.ssh_client.is_some() { + if self.remote_client.is_some() { 1 } else { 0 @@ -2220,55 +2216,55 @@ impl Project { ); self.client_subscriptions.extend([ - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&cx.entity(), &cx.to_async()), - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&self.worktree_store, &cx.to_async()), - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&self.buffer_store, &cx.to_async()), - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&self.lsp_store, &cx.to_async()), - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&self.settings_observer, &cx.to_async()), - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&self.dap_store, &cx.to_async()), - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&self.breakpoint_store, &cx.to_async()), - self.client + self.collab_client .subscribe_to_entity(project_id)? .set_entity(&self.git_store, &cx.to_async()), ]); self.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.shared(project_id, self.client.clone().into(), cx) + buffer_store.shared(project_id, self.collab_client.clone().into(), cx) }); self.worktree_store.update(cx, |worktree_store, cx| { - worktree_store.shared(project_id, self.client.clone().into(), cx); + worktree_store.shared(project_id, self.collab_client.clone().into(), cx); }); self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.shared(project_id, self.client.clone().into(), cx) + lsp_store.shared(project_id, self.collab_client.clone().into(), cx) }); self.breakpoint_store.update(cx, |breakpoint_store, _| { - breakpoint_store.shared(project_id, self.client.clone().into()) + breakpoint_store.shared(project_id, self.collab_client.clone().into()) }); self.dap_store.update(cx, |dap_store, cx| { - dap_store.shared(project_id, self.client.clone().into(), cx); + dap_store.shared(project_id, self.collab_client.clone().into(), cx); }); self.task_store.update(cx, |task_store, cx| { - task_store.shared(project_id, self.client.clone().into(), cx); + task_store.shared(project_id, self.collab_client.clone().into(), cx); }); self.settings_observer.update(cx, |settings_observer, cx| { - settings_observer.shared(project_id, self.client.clone().into(), cx) + settings_observer.shared(project_id, self.collab_client.clone().into(), cx) }); self.git_store.update(cx, |git_store, cx| { - git_store.shared(project_id, self.client.clone().into(), cx) + git_store.shared(project_id, self.collab_client.clone().into(), cx) }); self.client_state = ProjectClientState::Shared { @@ -2293,7 +2289,7 @@ impl Project { }); if let Some(remote_id) = self.remote_id() { self.git_store.update(cx, |git_store, cx| { - git_store.shared(remote_id, self.client.clone().into(), cx) + git_store.shared(remote_id, self.collab_client.clone().into(), cx) }); } cx.emit(Event::Reshared); @@ -2370,7 +2366,7 @@ impl Project { git_store.unshared(cx); }); - self.client + self.collab_client .send(proto::UnshareProject { project_id: remote_id, }) @@ -2437,15 +2433,17 @@ impl Project { sharing_has_stopped, .. } => *sharing_has_stopped, - ProjectClientState::Local if self.is_via_ssh() => self.ssh_is_disconnected(cx), + ProjectClientState::Local if self.is_via_remote_server() => { + self.remote_client_is_disconnected(cx) + } _ => false, } } - fn ssh_is_disconnected(&self, cx: &App) -> bool { - self.ssh_client + fn remote_client_is_disconnected(&self, cx: &App) -> bool { + self.remote_client .as_ref() - .map(|ssh| ssh.read(cx).is_disconnected()) + .map(|remote| remote.read(cx).is_disconnected()) .unwrap_or(false) } @@ -2463,16 +2461,16 @@ impl Project { pub fn is_local(&self) -> bool { match &self.client_state { ProjectClientState::Local | ProjectClientState::Shared { .. } => { - self.ssh_client.is_none() + self.remote_client.is_none() } ProjectClientState::Remote { .. } => false, } } - pub fn is_via_ssh(&self) -> bool { + pub fn is_via_remote_server(&self) -> bool { match &self.client_state { ProjectClientState::Local | ProjectClientState::Shared { .. } => { - self.ssh_client.is_some() + self.remote_client.is_some() } ProjectClientState::Remote { .. } => false, } @@ -2496,7 +2494,7 @@ impl Project { language: Option>, cx: &mut Context, ) -> Entity { - if self.is_via_collab() || self.is_via_ssh() { + if self.is_via_collab() || self.is_via_remote_server() { panic!("called create_local_buffer on a remote project") } self.buffer_store.update(cx, |buffer_store, cx| { @@ -2620,10 +2618,10 @@ impl Project { ) -> Task>> { if let Some(buffer) = self.buffer_for_id(id, cx) { Task::ready(Ok(buffer)) - } else if self.is_local() || self.is_via_ssh() { + } else if self.is_local() || self.is_via_remote_server() { Task::ready(Err(anyhow!("buffer {id} does not exist"))) } else if let Some(project_id) = self.remote_id() { - let request = self.client.request(proto::OpenBufferById { + let request = self.collab_client.request(proto::OpenBufferById { project_id, id: id.into(), }); @@ -2741,7 +2739,7 @@ impl Project { for (buffer_id, operations) in operations_by_buffer_id.drain() { let request = this.read_with(cx, |this, _| { let project_id = this.remote_id()?; - Some(this.client.request(proto::UpdateBuffer { + Some(this.collab_client.request(proto::UpdateBuffer { buffer_id: buffer_id.into(), project_id, operations, @@ -2808,7 +2806,7 @@ impl Project { project.read_with(cx, |project, _| { if let Some(project_id) = project.remote_id() { project - .client + .collab_client .send(proto::UpdateLanguageServer { project_id, server_name: name.map(|name| String::from(name.0)), @@ -2846,8 +2844,8 @@ impl Project { self.register_buffer(buffer, cx).log_err(); } BufferStoreEvent::BufferDropped(buffer_id) => { - if let Some(ref ssh_client) = self.ssh_client { - ssh_client + if let Some(ref remote_client) = self.remote_client { + remote_client .read(cx) .proto_client() .send(proto::CloseBuffer { @@ -2995,16 +2993,14 @@ impl Project { } } - fn on_ssh_event( + fn on_remote_client_event( &mut self, - _: Entity, - event: &remote::SshRemoteEvent, + _: Entity, + event: &remote::RemoteClientEvent, cx: &mut Context, ) { match event { - remote::SshRemoteEvent::Disconnected => { - // if self.is_via_ssh() { - // self.collaborators.clear(); + remote::RemoteClientEvent::Disconnected => { self.worktree_store.update(cx, |store, cx| { store.disconnected_from_host(cx); }); @@ -3110,8 +3106,9 @@ impl Project { } fn on_worktree_released(&mut self, id_to_remove: WorktreeId, cx: &mut Context) { - if let Some(ssh) = &self.ssh_client { - ssh.read(cx) + if let Some(remote) = &self.remote_client { + remote + .read(cx) .proto_client() .send(proto::RemoveWorktree { worktree_id: id_to_remove.to_proto(), @@ -3144,8 +3141,9 @@ impl Project { } => { let operation = language::proto::serialize_operation(operation); - if let Some(ssh) = &self.ssh_client { - ssh.read(cx) + if let Some(remote) = &self.remote_client { + remote + .read(cx) .proto_client() .send(proto::UpdateBuffer { project_id: 0, @@ -3552,16 +3550,16 @@ impl Project { pub fn open_server_settings(&mut self, cx: &mut Context) -> Task>> { let guard = self.retain_remotely_created_models(cx); - let Some(ssh_client) = self.ssh_client.as_ref() else { + let Some(remote) = self.remote_client.as_ref() else { return Task::ready(Err(anyhow!("not an ssh project"))); }; - let proto_client = ssh_client.read(cx).proto_client(); + let proto_client = remote.read(cx).proto_client(); cx.spawn(async move |project, cx| { let buffer = proto_client .request(proto::OpenServerSettings { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, }) .await?; @@ -3948,10 +3946,11 @@ impl Project { ) -> Receiver> { let (tx, rx) = smol::channel::unbounded(); - let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.ssh_client { + let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.remote_client + { (ssh_client.read(cx).proto_client(), 0) } else if let Some(remote_id) = self.remote_id() { - (self.client.clone().into(), remote_id) + (self.collab_client.clone().into(), remote_id) } else { return rx; }; @@ -4095,14 +4094,14 @@ impl Project { is_dir: metadata.is_dir, }) }) - } else if let Some(ssh_client) = self.ssh_client.as_ref() { + } else if let Some(ssh_client) = self.remote_client.as_ref() { let path_style = ssh_client.read(cx).path_style(); let request_path = RemotePathBuf::from_str(path, path_style); let request = ssh_client .read(cx) .proto_client() .request(proto::GetPathMetadata { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, path: request_path.to_proto(), }); cx.background_spawn(async move { @@ -4202,10 +4201,10 @@ impl Project { ) -> Task>> { if self.is_local() { DirectoryLister::Local(cx.entity(), self.fs.clone()).list_directory(query, cx) - } else if let Some(session) = self.ssh_client.as_ref() { + } else if let Some(session) = self.remote_client.as_ref() { let path_buf = PathBuf::from(query); let request = proto::ListRemoteDirectory { - dev_server_id: SSH_PROJECT_ID, + dev_server_id: REMOTE_SERVER_PROJECT_ID, path: path_buf.to_proto(), config: Some(proto::ListRemoteDirectoryConfig { is_dir: true }), }; @@ -4420,7 +4419,7 @@ impl Project { mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, cx| { - if this.is_local() || this.is_via_ssh() { + if this.is_local() || this.is_via_remote_server() { this.unshare(cx)?; } else { this.disconnected_from_host(cx); @@ -4629,7 +4628,7 @@ impl Project { })? } - async fn handle_update_buffer_from_ssh( + async fn handle_update_buffer_from_remote_server( this: Entity, envelope: TypedEnvelope, cx: AsyncApp, @@ -4638,7 +4637,7 @@ impl Project { if let Some(remote_id) = this.remote_id() { let mut payload = envelope.payload.clone(); payload.project_id = remote_id; - cx.background_spawn(this.client.request(payload)) + cx.background_spawn(this.collab_client.request(payload)) .detach_and_log_err(cx); } this.buffer_store.clone() @@ -4652,9 +4651,9 @@ impl Project { cx: AsyncApp, ) -> Result { let buffer_store = this.read_with(&cx, |this, cx| { - if let Some(ssh) = &this.ssh_client { + if let Some(ssh) = &this.remote_client { let mut payload = envelope.payload.clone(); - payload.project_id = SSH_PROJECT_ID; + payload.project_id = REMOTE_SERVER_PROJECT_ID; cx.background_spawn(ssh.read(cx).proto_client().request(payload)) .detach_and_log_err(cx); } @@ -4704,7 +4703,7 @@ impl Project { mut cx: AsyncApp, ) -> Result { let response = this.update(&mut cx, |this, cx| { - let client = this.client.clone(); + let client = this.collab_client.clone(); this.buffer_store.update(cx, |this, cx| { this.handle_synchronize_buffers(envelope, cx, client) }) @@ -4841,7 +4840,7 @@ impl Project { } }; - let client = self.client.clone(); + let client = self.collab_client.clone(); cx.spawn(async move |this, cx| { let (buffers, incomplete_buffer_ids) = this.update(cx, |this, cx| { this.buffer_store.read(cx).buffer_version_info(cx) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index b009b357fe8eef1a7df61117857251178b437659..7e1be67e21380e8b1ae751600b3553a527067e46 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -2,13 +2,11 @@ use crate::{Project, ProjectPath}; use anyhow::{Context as _, Result}; use collections::HashMap; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; -use itertools::Itertools; use language::LanguageName; -use remote::{SshInfo, ssh_session::SshArgs}; +use remote::RemoteClient; use settings::{Settings, SettingsLocation}; use smol::channel::bounded; use std::{ - borrow::Cow, env::{self}, path::{Path, PathBuf}, sync::Arc, @@ -18,10 +16,7 @@ use terminal::{ TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings}, }; -use util::{ - ResultExt, - paths::{PathStyle, RemotePathBuf}, -}; +use util::{ResultExt, paths::RemotePathBuf}; /// The directory inside a Python virtual environment that contains executables const PYTHON_VENV_BIN_DIR: &str = if cfg!(target_os = "windows") { @@ -44,29 +39,6 @@ pub enum TerminalKind { Task(SpawnInTerminal), } -/// SshCommand describes how to connect to a remote server -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SshCommand { - pub arguments: Vec, -} - -impl SshCommand { - pub fn add_port_forwarding(&mut self, local_port: u16, host: String, remote_port: u16) { - self.arguments.push("-L".to_string()); - self.arguments - .push(format!("{}:{}:{}", local_port, host, remote_port)); - } -} - -#[derive(Debug)] -pub struct SshDetails { - pub host: String, - pub ssh_command: SshCommand, - pub envs: Option>, - pub path_style: PathStyle, - pub shell: String, -} - impl Project { pub fn active_project_directory(&self, cx: &App) -> Option> { self.active_entry() @@ -86,28 +58,6 @@ impl Project { } } - pub fn ssh_details(&self, cx: &App) -> Option { - if let Some(ssh_client) = &self.ssh_client { - let ssh_client = ssh_client.read(cx); - if let Some(SshInfo { - args: SshArgs { arguments, envs }, - path_style, - shell, - }) = ssh_client.ssh_info() - { - return Some(SshDetails { - host: ssh_client.connection_options().host, - ssh_command: SshCommand { arguments }, - envs, - path_style, - shell, - }); - } - } - - None - } - pub fn create_terminal( &mut self, kind: TerminalKind, @@ -168,14 +118,14 @@ impl Project { TerminalSettings::get(settings_location, cx) } - pub fn exec_in_shell(&self, command: String, cx: &App) -> std::process::Command { + pub fn exec_in_shell(&self, command: String, cx: &App) -> Result { let path = self.first_project_directory(cx); - let ssh_details = self.ssh_details(cx); + let remote_client = self.remote_client.as_ref(); let settings = self.terminal_settings(&path, cx).clone(); - - let builder = - ShellBuilder::new(ssh_details.as_ref().map(|ssh| &*ssh.shell), &settings.shell) - .non_interactive(); + let remote_shell = remote_client + .as_ref() + .and_then(|remote_client| remote_client.read(cx).shell()); + let builder = ShellBuilder::new(remote_shell.as_deref(), &settings.shell).non_interactive(); let (command, args) = builder.build(Some(command), &Vec::new()); let mut env = self @@ -185,29 +135,16 @@ impl Project { .unwrap_or_default(); env.extend(settings.env); - match self.ssh_details(cx) { - Some(SshDetails { - ssh_command, - envs, - path_style, - shell, - .. - }) => { - let (command, args) = wrap_for_ssh( - &shell, - &ssh_command, - Some((&command, &args)), - path.as_deref(), - env, - None, - path_style, - ); - let mut command = std::process::Command::new(command); - command.args(args); - if let Some(envs) = envs { - command.envs(envs); - } - command + match remote_client { + Some(remote_client) => { + let command_template = + remote_client + .read(cx) + .build_command(Some(command), &args, &env, None, None)?; + let mut command = std::process::Command::new(command_template.program); + command.args(command_template.args); + command.envs(command_template.env); + Ok(command) } None => { let mut command = std::process::Command::new(command); @@ -216,7 +153,7 @@ impl Project { if let Some(path) = path { command.current_dir(path); } - command + Ok(command) } } } @@ -227,13 +164,13 @@ impl Project { python_venv_directory: Option, cx: &mut Context, ) -> Result> { - let this = &mut *self; - let ssh_details = this.ssh_details(cx); + let is_via_remote = self.remote_client.is_some(); + let path: Option> = match &kind { TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())), TerminalKind::Task(spawn_task) => { if let Some(cwd) = &spawn_task.cwd { - if ssh_details.is_some() { + if is_via_remote { Some(Arc::from(cwd.as_ref())) } else { let cwd = cwd.to_string_lossy(); @@ -241,16 +178,14 @@ impl Project { Some(Arc::from(Path::new(tilde_substituted.as_ref()))) } } else { - this.active_project_directory(cx) + self.active_project_directory(cx) } } }; - let is_ssh_terminal = ssh_details.is_some(); - let mut settings_location = None; if let Some(path) = path.as_ref() - && let Some((worktree, _)) = this.find_worktree(path, cx) + && let Some((worktree, _)) = self.find_worktree(path, cx) { settings_location = Some(SettingsLocation { worktree_id: worktree.read(cx).id(), @@ -262,7 +197,7 @@ impl Project { let (completion_tx, completion_rx) = bounded(1); // Start with the environment that we might have inherited from the Zed CLI. - let mut env = this + let mut env = self .environment .read(cx) .get_cli_environment() @@ -271,14 +206,17 @@ impl Project { // precedence. env.extend(settings.env); - let local_path = if is_ssh_terminal { None } else { path.clone() }; + let local_path = if is_via_remote { None } else { path.clone() }; let mut python_venv_activate_command = Task::ready(None); - let (spawn_task, shell) = match kind { + let remote_client = self.remote_client.clone(); + let spawn_task; + let shell; + match kind { TerminalKind::Shell(_) => { if let Some(python_venv_directory) = &python_venv_directory { - python_venv_activate_command = this.python_activate_command( + python_venv_activate_command = self.python_activate_command( python_venv_directory, &settings.detect_venv, &settings.shell, @@ -286,63 +224,16 @@ impl Project { ); } - match ssh_details { - Some(SshDetails { - host, - ssh_command, - envs, - path_style, - shell, - }) => { - log::debug!("Connecting to a remote server: {ssh_command:?}"); - - // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed - // to properly display colors. - // We do not have the luxury of assuming the host has it installed, - // so we set it to a default that does not break the highlighting via ssh. - env.entry("TERM".to_string()) - .or_insert_with(|| "xterm-256color".to_string()); - - let (program, args) = wrap_for_ssh( - &shell, - &ssh_command, - None, - path.as_deref(), - env, - None, - path_style, - ); - env = HashMap::default(); - if let Some(envs) = envs { - env.extend(envs); - } - ( - Option::::None, - Shell::WithArguments { - program, - args, - title_override: Some(format!("{} — Terminal", host).into()), - }, - ) + spawn_task = None; + shell = match remote_client { + Some(remote_client) => { + create_remote_shell(None, &mut env, path, remote_client, cx)? } - None => (None, settings.shell), - } + None => settings.shell, + }; } - TerminalKind::Task(spawn_task) => { - let task_state = Some(TaskState { - id: spawn_task.id, - full_label: spawn_task.full_label, - label: spawn_task.label, - command_label: spawn_task.command_label, - hide: spawn_task.hide, - status: TaskStatus::Running, - show_summary: spawn_task.show_summary, - show_command: spawn_task.show_command, - show_rerun: spawn_task.show_rerun, - completion_rx, - }); - - env.extend(spawn_task.env); + TerminalKind::Task(task) => { + env.extend(task.env); if let Some(venv_path) = &python_venv_directory { env.insert( @@ -351,41 +242,38 @@ impl Project { ); } - match ssh_details { - Some(SshDetails { - host, - ssh_command, - envs, - path_style, - shell, - }) => { - log::debug!("Connecting to a remote server: {ssh_command:?}"); - env.entry("TERM".to_string()) - .or_insert_with(|| "xterm-256color".to_string()); - let (program, args) = wrap_for_ssh( - &shell, - &ssh_command, - spawn_task - .command - .as_ref() - .map(|command| (command, &spawn_task.args)), - path.as_deref(), - env, - python_venv_directory.as_deref(), - path_style, - ); - env = HashMap::default(); - if let Some(envs) = envs { - env.extend(envs); + spawn_task = Some(TaskState { + id: task.id, + full_label: task.full_label, + label: task.label, + command_label: task.command_label, + hide: task.hide, + status: TaskStatus::Running, + show_summary: task.show_summary, + show_command: task.show_command, + show_rerun: task.show_rerun, + completion_rx, + }); + shell = match remote_client { + Some(remote_client) => { + let path_style = remote_client.read(cx).path_style(); + if let Some(venv_directory) = &python_venv_directory + && let Ok(str) = + shlex::try_quote(venv_directory.to_string_lossy().as_ref()) + { + let path = + RemotePathBuf::new(PathBuf::from(str.to_string()), path_style) + .to_string(); + env.insert("PATH".into(), format!("{}:$PATH ", path)); } - ( - task_state, - Shell::WithArguments { - program, - args, - title_override: Some(format!("{} — Terminal", host).into()), - }, - ) + + create_remote_shell( + task.command.as_ref().map(|command| (command, &task.args)), + &mut env, + path, + remote_client, + cx, + )? } None => { if let Some(venv_path) = &python_venv_directory { @@ -393,18 +281,17 @@ impl Project { .log_err(); } - let shell = if let Some(program) = spawn_task.command { + if let Some(program) = task.command { Shell::WithArguments { program, - args: spawn_task.args, + args: task.args, title_override: None, } } else { Shell::System - }; - (task_state, shell) + } } - } + }; } }; TerminalBuilder::new( @@ -416,7 +303,7 @@ impl Project { settings.cursor_shape.unwrap_or_default(), settings.alternate_scroll, settings.max_scroll_history_lines, - is_ssh_terminal, + is_via_remote, cx.entity_id().as_u64(), completion_tx, cx, @@ -424,7 +311,7 @@ impl Project { .map(|builder| { let terminal_handle = cx.new(|cx| builder.subscribe(cx)); - this.terminals + self.terminals .local_handles .push(terminal_handle.downgrade()); @@ -442,7 +329,7 @@ impl Project { }) .detach(); - this.activate_python_virtual_environment( + self.activate_python_virtual_environment( python_venv_activate_command, &terminal_handle, cx, @@ -652,62 +539,42 @@ impl Project { } } -pub fn wrap_for_ssh( - shell: &str, - ssh_command: &SshCommand, - command: Option<(&String, &Vec)>, - path: Option<&Path>, - env: HashMap, - venv_directory: Option<&Path>, - path_style: PathStyle, -) -> (String, Vec) { - let to_run = if let Some((command, args)) = command { - let command: Option> = shlex::try_quote(command).ok(); - let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok()); - command.into_iter().chain(args).join(" ") - } else { - format!("exec {shell} -l") - }; - - let mut env_changes = String::new(); - for (k, v) in env.iter() { - if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) { - env_changes.push_str(&format!("{}={} ", k, v)); - } - } - if let Some(venv_directory) = venv_directory - && let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref()) - { - let path = RemotePathBuf::new(PathBuf::from(str.to_string()), path_style).to_string(); - env_changes.push_str(&format!("PATH={}:$PATH ", path)); - } - - let commands = if let Some(path) = path { - let path = RemotePathBuf::new(path.to_path_buf(), path_style).to_string(); - // shlex will wrap the command in single quotes (''), disabling ~ expansion, - // replace ith with something that works - let tilde_prefix = "~/"; - if path.starts_with(tilde_prefix) { - let trimmed_path = path - .trim_start_matches("/") - .trim_start_matches("~") - .trim_start_matches("/"); - - format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}") - } else { - format!("cd \"{path}\"; {env_changes} {to_run}") - } - } else { - format!("cd; {env_changes} {to_run}") +fn create_remote_shell( + spawn_command: Option<(&String, &Vec)>, + env: &mut HashMap, + working_directory: Option>, + remote_client: Entity, + cx: &mut App, +) -> Result { + // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed + // to properly display colors. + // We do not have the luxury of assuming the host has it installed, + // so we set it to a default that does not break the highlighting via ssh. + env.entry("TERM".to_string()) + .or_insert_with(|| "xterm-256color".to_string()); + + let (program, args) = match spawn_command { + Some((program, args)) => (Some(program.clone()), args), + None => (None, &Vec::new()), }; - let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&commands).unwrap()); - - let program = "ssh".to_string(); - let mut args = ssh_command.arguments.clone(); - args.push("-t".to_string()); - args.push(shell_invocation); - (program, args) + let command = remote_client.read(cx).build_command( + program, + args.as_slice(), + env, + working_directory.map(|path| path.display().to_string()), + None, + )?; + *env = command.env; + + log::debug!("Connecting to a remote server: {:?}", command.program); + let host = remote_client.read(cx).connection_options().host; + + Ok(Shell::WithArguments { + program: command.program, + args: command.args, + title_override: Some(format!("{} — Terminal", host).into()), + }) } fn add_environment_path(env: &mut HashMap, new_path: &Path) -> Result<()> { diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index b8905c73bc66ab3f9a911785fe812401dfdb6ee4..9033415ca40b4550e5f98bae2de8314be2e1a5fe 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -18,7 +18,7 @@ use gpui::{ use postage::oneshot; use rpc::{ AnyProtoClient, ErrorExt, TypedEnvelope, - proto::{self, FromProto, SSH_PROJECT_ID, ToProto}, + proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto}, }; use smol::{ channel::{Receiver, Sender}, @@ -278,7 +278,7 @@ impl WorktreeStore { let path = RemotePathBuf::new(abs_path.into(), path_style); let response = client .request(proto::AddWorktree { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, path: path.to_proto(), visible, }) @@ -298,7 +298,7 @@ impl WorktreeStore { let worktree = cx.update(|cx| { Worktree::remote( - SSH_PROJECT_ID, + REMOTE_SERVER_PROJECT_ID, 0, proto::WorktreeMetadata { id: response.worktree_id, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 5a30a3e9bcabc3c2c16f942cf864daa6b28f6b98..eeb2f7a49b82ce35199c8a773d70cf5fd358c034 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -653,7 +653,7 @@ impl ProjectPanel { let file_path = entry.path.clone(); let worktree_id = worktree.read(cx).id(); let entry_id = entry.id; - let is_via_ssh = project.read(cx).is_via_ssh(); + let is_via_ssh = project.read(cx).is_via_remote_server(); workspace .open_path_preview( @@ -5301,7 +5301,7 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::open_system)) .on_action(cx.listener(Self::open_in_terminal)) }) - .when(project.is_via_ssh(), |el| { + .when(project.is_via_remote_server(), |el| { el.on_action(cx.listener(Self::open_in_terminal)) }) .on_mouse_down( diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index d38e54685ffb78fe8621b12a0dd25bb6d1ab3f6e..e17ec5203bd5b7bcab03c6461c343156116cc563 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -16,8 +16,8 @@ pub use typed_envelope::*; include!(concat!(env!("OUT_DIR"), "/zed.messages.rs")); -pub const SSH_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 }; -pub const SSH_PROJECT_ID: u64 = 0; +pub const REMOTE_SERVER_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 }; +pub const REMOTE_SERVER_PROJECT_ID: u64 = 0; messages!( (Ack, Foreground), diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index 8ffe0ef07cf2e0c635794383ae08203c87a33f44..36da6897b92e4bc183aa7c0f51d5100e8836931e 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -64,8 +64,8 @@ impl DisconnectedOverlay { } let handle = cx.entity().downgrade(); - let ssh_connection_options = project.read(cx).ssh_connection_options(cx); - let host = if let Some(ssh_connection_options) = ssh_connection_options { + let remote_connection_options = project.read(cx).remote_connection_options(cx); + let host = if let Some(ssh_connection_options) = remote_connection_options { Host::SshRemoteProject(ssh_connection_options) } else { Host::RemoteProject diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index a9c3284d0bd5d9dc5e279ce317f2280914bd623f..f4fd1f1c1bbb12e2fbf11088baf859b08bfbf310 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -28,8 +28,8 @@ use paths::user_ssh_config_file; use picker::Picker; use project::Fs; use project::Project; -use remote::ssh_session::ConnectionIdentifier; -use remote::{SshConnectionOptions, SshRemoteClient}; +use remote::remote_client::ConnectionIdentifier; +use remote::{RemoteClient, SshConnectionOptions}; use settings::Settings; use settings::SettingsStore; use settings::update_settings_file; @@ -69,7 +69,7 @@ pub struct RemoteServerProjects { mode: Mode, focus_handle: FocusHandle, workspace: WeakEntity, - retained_connections: Vec>, + retained_connections: Vec>, ssh_config_updates: Task<()>, ssh_config_servers: BTreeSet, create_new_window: bool, @@ -597,7 +597,7 @@ impl RemoteServerProjects { let (path_style, project) = cx.update(|_, cx| { ( session.read(cx).path_style(), - project::Project::ssh( + project::Project::remote( session, app_state.client.clone(), app_state.node_runtime.clone(), diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index d07ea48c7e439651d6612bbea046f7711a6a124a..e3fb249d1632a35d888996da2665d00ea98b2c26 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -15,8 +15,9 @@ use gpui::{ use language::CursorShape; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use release_channel::ReleaseChannel; -use remote::ssh_session::{ConnectionIdentifier, SshPortForwardOption}; -use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient}; +use remote::{ + ConnectionIdentifier, RemoteClient, RemotePlatform, SshConnectionOptions, SshPortForwardOption, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; @@ -451,7 +452,7 @@ pub struct SshClientDelegate { known_password: Option, } -impl remote::SshClientDelegate for SshClientDelegate { +impl remote::RemoteClientDelegate for SshClientDelegate { fn ask_password(&self, prompt: String, tx: oneshot::Sender, cx: &mut AsyncApp) { let mut known_password = self.known_password.clone(); if let Some(password) = known_password.take() { @@ -473,7 +474,7 @@ impl remote::SshClientDelegate for SshClientDelegate { fn download_server_binary_locally( &self, - platform: SshPlatform, + platform: RemotePlatform, release_channel: ReleaseChannel, version: Option, cx: &mut AsyncApp, @@ -503,7 +504,7 @@ impl remote::SshClientDelegate for SshClientDelegate { fn get_download_params( &self, - platform: SshPlatform, + platform: RemotePlatform, release_channel: ReleaseChannel, version: Option, cx: &mut AsyncApp, @@ -543,13 +544,13 @@ pub fn connect_over_ssh( ui: Entity, window: &mut Window, cx: &mut App, -) -> Task>>> { +) -> Task>>> { let window = window.window_handle(); let known_password = connection_options.password.clone(); let (tx, rx) = oneshot::channel(); ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx)); - remote::SshRemoteClient::new( + remote::RemoteClient::ssh( unique_identifier, connection_options, rx, @@ -681,9 +682,9 @@ pub async fn open_ssh_project( window .update(cx, |workspace, _, cx| { - if let Some(client) = workspace.project().read(cx).ssh_client() { + if let Some(client) = workspace.project().read(cx).remote_client() { ExtensionStore::global(cx) - .update(cx, |store, cx| store.register_ssh_client(client, cx)); + .update(cx, |store, cx| store.register_remote_client(client, cx)); } }) .ok(); diff --git a/crates/remote/src/protocol.rs b/crates/remote/src/protocol.rs index e5a9c5b7a55bf7a49d720ba3d761c04cc597e4fd..867a31b1645980ad050d6e9b75fd6cfadb9dc50d 100644 --- a/crates/remote/src/protocol.rs +++ b/crates/remote/src/protocol.rs @@ -51,6 +51,16 @@ pub async fn write_message( Ok(()) } +pub async fn write_size_prefixed_buffer( + stream: &mut S, + buffer: &mut Vec, +) -> Result<()> { + let len = buffer.len() as u32; + stream.write_all(len.to_le_bytes().as_slice()).await?; + stream.write_all(buffer).await?; + Ok(()) +} + pub async fn read_message_raw( stream: &mut S, buffer: &mut Vec, diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs index 71895f1678c5e71819218f62d3831708c2e4a2bc..c698353d9edfc0d48c7039f321a2c88890e8c098 100644 --- a/crates/remote/src/remote.rs +++ b/crates/remote/src/remote.rs @@ -1,9 +1,11 @@ pub mod json_log; pub mod protocol; pub mod proxy; -pub mod ssh_session; +pub mod remote_client; +mod transport; -pub use ssh_session::{ - ConnectionState, SshClientDelegate, SshConnectionOptions, SshInfo, SshPlatform, - SshRemoteClient, SshRemoteEvent, +pub use remote_client::{ + ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent, + RemotePlatform, }; +pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption}; diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs new file mode 100644 index 0000000000000000000000000000000000000000..dd529ca87499b0daf2061fd990f7149828e3fce4 --- /dev/null +++ b/crates/remote/src/remote_client.rs @@ -0,0 +1,1478 @@ +use crate::{ + SshConnectionOptions, protocol::MessageId, proxy::ProxyLaunchError, + transport::ssh::SshRemoteConnection, +}; +use anyhow::{Context as _, Result, anyhow}; +use async_trait::async_trait; +use collections::HashMap; +use futures::{ + Future, FutureExt as _, StreamExt as _, + channel::{ + mpsc::{self, Sender, UnboundedReceiver, UnboundedSender}, + oneshot, + }, + future::{BoxFuture, Shared}, + select, select_biased, +}; +use gpui::{ + App, AppContext as _, AsyncApp, BackgroundExecutor, BorrowAppContext, Context, Entity, + EventEmitter, Global, SemanticVersion, Task, WeakEntity, +}; +use parking_lot::Mutex; + +use release_channel::ReleaseChannel; +use rpc::{ + AnyProtoClient, ErrorExt, ProtoClient, ProtoMessageHandlerSet, RpcError, + proto::{self, Envelope, EnvelopedMessage, PeerId, RequestMessage, build_typed_envelope}, +}; +use std::{ + collections::VecDeque, + fmt, + ops::ControlFlow, + path::PathBuf, + sync::{ + Arc, Weak, + atomic::{AtomicU32, AtomicU64, Ordering::SeqCst}, + }, + time::{Duration, Instant}, +}; +use util::{ + ResultExt, + paths::{PathStyle, RemotePathBuf}, +}; + +#[derive(Copy, Clone, Debug)] +pub struct RemotePlatform { + pub os: &'static str, + pub arch: &'static str, +} + +#[derive(Clone, Debug)] +pub struct CommandTemplate { + pub program: String, + pub args: Vec, + pub env: HashMap, +} + +pub trait RemoteClientDelegate: Send + Sync { + fn ask_password(&self, prompt: String, tx: oneshot::Sender, cx: &mut AsyncApp); + fn get_download_params( + &self, + platform: RemotePlatform, + release_channel: ReleaseChannel, + version: Option, + cx: &mut AsyncApp, + ) -> Task>>; + fn download_server_binary_locally( + &self, + platform: RemotePlatform, + release_channel: ReleaseChannel, + version: Option, + cx: &mut AsyncApp, + ) -> Task>; + fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp); +} + +const MAX_MISSED_HEARTBEATS: usize = 5; +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); +const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(5); + +const MAX_RECONNECT_ATTEMPTS: usize = 3; + +enum State { + Connecting, + Connected { + ssh_connection: Arc, + delegate: Arc, + + multiplex_task: Task>, + heartbeat_task: Task>, + }, + HeartbeatMissed { + missed_heartbeats: usize, + + ssh_connection: Arc, + delegate: Arc, + + multiplex_task: Task>, + heartbeat_task: Task>, + }, + Reconnecting, + ReconnectFailed { + ssh_connection: Arc, + delegate: Arc, + + error: anyhow::Error, + attempts: usize, + }, + ReconnectExhausted, + ServerNotRunning, +} + +impl fmt::Display for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Connecting => write!(f, "connecting"), + Self::Connected { .. } => write!(f, "connected"), + Self::Reconnecting => write!(f, "reconnecting"), + Self::ReconnectFailed { .. } => write!(f, "reconnect failed"), + Self::ReconnectExhausted => write!(f, "reconnect exhausted"), + Self::HeartbeatMissed { .. } => write!(f, "heartbeat missed"), + Self::ServerNotRunning { .. } => write!(f, "server not running"), + } + } +} + +impl State { + fn remote_connection(&self) -> Option> { + match self { + Self::Connected { ssh_connection, .. } => Some(ssh_connection.clone()), + Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection.clone()), + Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection.clone()), + _ => None, + } + } + + fn can_reconnect(&self) -> bool { + match self { + Self::Connected { .. } + | Self::HeartbeatMissed { .. } + | Self::ReconnectFailed { .. } => true, + State::Connecting + | State::Reconnecting + | State::ReconnectExhausted + | State::ServerNotRunning => false, + } + } + + fn is_reconnect_failed(&self) -> bool { + matches!(self, Self::ReconnectFailed { .. }) + } + + fn is_reconnect_exhausted(&self) -> bool { + matches!(self, Self::ReconnectExhausted { .. }) + } + + fn is_server_not_running(&self) -> bool { + matches!(self, Self::ServerNotRunning) + } + + fn is_reconnecting(&self) -> bool { + matches!(self, Self::Reconnecting { .. }) + } + + fn heartbeat_recovered(self) -> Self { + match self { + Self::HeartbeatMissed { + ssh_connection, + delegate, + multiplex_task, + heartbeat_task, + .. + } => Self::Connected { + ssh_connection, + delegate, + multiplex_task, + heartbeat_task, + }, + _ => self, + } + } + + fn heartbeat_missed(self) -> Self { + match self { + Self::Connected { + ssh_connection, + delegate, + multiplex_task, + heartbeat_task, + } => Self::HeartbeatMissed { + missed_heartbeats: 1, + ssh_connection, + delegate, + multiplex_task, + heartbeat_task, + }, + Self::HeartbeatMissed { + missed_heartbeats, + ssh_connection, + delegate, + multiplex_task, + heartbeat_task, + } => Self::HeartbeatMissed { + missed_heartbeats: missed_heartbeats + 1, + ssh_connection, + delegate, + multiplex_task, + heartbeat_task, + }, + _ => self, + } + } +} + +/// The state of the ssh connection. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ConnectionState { + Connecting, + Connected, + HeartbeatMissed, + Reconnecting, + Disconnected, +} + +impl From<&State> for ConnectionState { + fn from(value: &State) -> Self { + match value { + State::Connecting => Self::Connecting, + State::Connected { .. } => Self::Connected, + State::Reconnecting | State::ReconnectFailed { .. } => Self::Reconnecting, + State::HeartbeatMissed { .. } => Self::HeartbeatMissed, + State::ReconnectExhausted => Self::Disconnected, + State::ServerNotRunning => Self::Disconnected, + } + } +} + +pub struct RemoteClient { + client: Arc, + unique_identifier: String, + connection_options: SshConnectionOptions, + path_style: PathStyle, + state: Option, +} + +#[derive(Debug)] +pub enum RemoteClientEvent { + Disconnected, +} + +impl EventEmitter for RemoteClient {} + +// Identifies the socket on the remote server so that reconnects +// can re-join the same project. +pub enum ConnectionIdentifier { + Setup(u64), + Workspace(i64), +} + +static NEXT_ID: AtomicU64 = AtomicU64::new(1); + +impl ConnectionIdentifier { + pub fn setup() -> Self { + Self::Setup(NEXT_ID.fetch_add(1, SeqCst)) + } + + // This string gets used in a socket name, and so must be relatively short. + // The total length of: + // /home/{username}/.local/share/zed/server_state/{name}/stdout.sock + // Must be less than about 100 characters + // https://unix.stackexchange.com/questions/367008/why-is-socket-path-length-limited-to-a-hundred-chars + // So our strings should be at most 20 characters or so. + fn to_string(&self, cx: &App) -> String { + let identifier_prefix = match ReleaseChannel::global(cx) { + ReleaseChannel::Stable => "".to_string(), + release_channel => format!("{}-", release_channel.dev_name()), + }; + match self { + Self::Setup(setup_id) => format!("{identifier_prefix}setup-{setup_id}"), + Self::Workspace(workspace_id) => { + format!("{identifier_prefix}workspace-{workspace_id}",) + } + } + } +} + +impl RemoteClient { + pub fn ssh( + unique_identifier: ConnectionIdentifier, + connection_options: SshConnectionOptions, + cancellation: oneshot::Receiver<()>, + delegate: Arc, + cx: &mut App, + ) -> Task>>> { + let unique_identifier = unique_identifier.to_string(cx); + cx.spawn(async move |cx| { + let success = Box::pin(async move { + let (outgoing_tx, outgoing_rx) = mpsc::unbounded::(); + let (incoming_tx, incoming_rx) = mpsc::unbounded::(); + let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1); + + let client = + cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?; + + let ssh_connection = cx + .update(|cx| { + cx.update_default_global(|pool: &mut ConnectionPool, cx| { + pool.connect(connection_options.clone(), &delegate, cx) + }) + })? + .await + .map_err(|e| e.cloned())?; + + let path_style = ssh_connection.path_style(); + let this = cx.new(|_| Self { + client: client.clone(), + unique_identifier: unique_identifier.clone(), + connection_options, + path_style, + state: Some(State::Connecting), + })?; + + let io_task = ssh_connection.start_proxy( + unique_identifier, + false, + incoming_tx, + outgoing_rx, + connection_activity_tx, + delegate.clone(), + cx, + ); + + let multiplex_task = Self::monitor(this.downgrade(), io_task, cx); + + if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await { + log::error!("failed to establish connection: {}", error); + return Err(error); + } + + let heartbeat_task = Self::heartbeat(this.downgrade(), connection_activity_rx, cx); + + this.update(cx, |this, _| { + this.state = Some(State::Connected { + ssh_connection, + delegate, + multiplex_task, + heartbeat_task, + }); + })?; + + Ok(Some(this)) + }); + + select! { + _ = cancellation.fuse() => { + Ok(None) + } + result = success.fuse() => result + } + }) + } + + pub fn proto_client_from_channels( + incoming_rx: mpsc::UnboundedReceiver, + outgoing_tx: mpsc::UnboundedSender, + cx: &App, + name: &'static str, + ) -> AnyProtoClient { + ChannelClient::new(incoming_rx, outgoing_tx, cx, name).into() + } + + pub fn shutdown_processes( + &mut self, + shutdown_request: Option, + executor: BackgroundExecutor, + ) -> Option + use> { + let state = self.state.take()?; + log::info!("shutting down ssh processes"); + + let State::Connected { + multiplex_task, + heartbeat_task, + ssh_connection, + delegate, + } = state + else { + return None; + }; + + let client = self.client.clone(); + + Some(async move { + if let Some(shutdown_request) = shutdown_request { + client.send(shutdown_request).log_err(); + // We wait 50ms instead of waiting for a response, because + // waiting for a response would require us to wait on the main thread + // which we want to avoid in an `on_app_quit` callback. + executor.timer(Duration::from_millis(50)).await; + } + + // Drop `multiplex_task` because it owns our ssh_proxy_process, which is a + // child of master_process. + drop(multiplex_task); + // Now drop the rest of state, which kills master process. + drop(heartbeat_task); + drop(ssh_connection); + drop(delegate); + }) + } + + fn reconnect(&mut self, cx: &mut Context) -> Result<()> { + let can_reconnect = self + .state + .as_ref() + .map(|state| state.can_reconnect()) + .unwrap_or(false); + if !can_reconnect { + log::info!("aborting reconnect, because not in state that allows reconnecting"); + let error = if let Some(state) = self.state.as_ref() { + format!("invalid state, cannot reconnect while in state {state}") + } else { + "no state set".to_string() + }; + anyhow::bail!(error); + } + + let state = self.state.take().unwrap(); + let (attempts, ssh_connection, delegate) = match state { + State::Connected { + ssh_connection, + delegate, + multiplex_task, + heartbeat_task, + } + | State::HeartbeatMissed { + ssh_connection, + delegate, + multiplex_task, + heartbeat_task, + .. + } => { + drop(multiplex_task); + drop(heartbeat_task); + (0, ssh_connection, delegate) + } + State::ReconnectFailed { + attempts, + ssh_connection, + delegate, + .. + } => (attempts, ssh_connection, delegate), + State::Connecting + | State::Reconnecting + | State::ReconnectExhausted + | State::ServerNotRunning => unreachable!(), + }; + + let attempts = attempts + 1; + if attempts > MAX_RECONNECT_ATTEMPTS { + log::error!( + "Failed to reconnect to after {} attempts, giving up", + MAX_RECONNECT_ATTEMPTS + ); + self.set_state(State::ReconnectExhausted, cx); + return Ok(()); + } + + self.set_state(State::Reconnecting, cx); + + log::info!("Trying to reconnect to ssh server... Attempt {}", attempts); + + let unique_identifier = self.unique_identifier.clone(); + let client = self.client.clone(); + let reconnect_task = cx.spawn(async move |this, cx| { + macro_rules! failed { + ($error:expr, $attempts:expr, $ssh_connection:expr, $delegate:expr) => { + return State::ReconnectFailed { + error: anyhow!($error), + attempts: $attempts, + ssh_connection: $ssh_connection, + delegate: $delegate, + }; + }; + } + + if let Err(error) = ssh_connection + .kill() + .await + .context("Failed to kill ssh process") + { + failed!(error, attempts, ssh_connection, delegate); + }; + + let connection_options = ssh_connection.connection_options(); + + let (outgoing_tx, outgoing_rx) = mpsc::unbounded::(); + let (incoming_tx, incoming_rx) = mpsc::unbounded::(); + let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1); + + let (ssh_connection, io_task) = match async { + let ssh_connection = cx + .update_global(|pool: &mut ConnectionPool, cx| { + pool.connect(connection_options, &delegate, cx) + })? + .await + .map_err(|error| error.cloned())?; + + let io_task = ssh_connection.start_proxy( + unique_identifier, + true, + incoming_tx, + outgoing_rx, + connection_activity_tx, + delegate.clone(), + cx, + ); + anyhow::Ok((ssh_connection, io_task)) + } + .await + { + Ok((ssh_connection, io_task)) => (ssh_connection, io_task), + Err(error) => { + failed!(error, attempts, ssh_connection, delegate); + } + }; + + let multiplex_task = Self::monitor(this.clone(), io_task, cx); + client.reconnect(incoming_rx, outgoing_tx, cx); + + if let Err(error) = client.resync(HEARTBEAT_TIMEOUT).await { + failed!(error, attempts, ssh_connection, delegate); + }; + + State::Connected { + ssh_connection, + delegate, + multiplex_task, + heartbeat_task: Self::heartbeat(this.clone(), connection_activity_rx, cx), + } + }); + + cx.spawn(async move |this, cx| { + let new_state = reconnect_task.await; + this.update(cx, |this, cx| { + this.try_set_state(cx, |old_state| { + if old_state.is_reconnecting() { + match &new_state { + State::Connecting + | State::Reconnecting + | State::HeartbeatMissed { .. } + | State::ServerNotRunning => {} + State::Connected { .. } => { + log::info!("Successfully reconnected"); + } + State::ReconnectFailed { + error, attempts, .. + } => { + log::error!( + "Reconnect attempt {} failed: {:?}. Starting new attempt...", + attempts, + error + ); + } + State::ReconnectExhausted => { + log::error!("Reconnect attempt failed and all attempts exhausted"); + } + } + Some(new_state) + } else { + None + } + }); + + if this.state_is(State::is_reconnect_failed) { + this.reconnect(cx) + } else if this.state_is(State::is_reconnect_exhausted) { + Ok(()) + } else { + log::debug!("State has transition from Reconnecting into new state while attempting reconnect."); + Ok(()) + } + }) + }) + .detach_and_log_err(cx); + + Ok(()) + } + + fn heartbeat( + this: WeakEntity, + mut connection_activity_rx: mpsc::Receiver<()>, + cx: &mut AsyncApp, + ) -> Task> { + let Ok(client) = this.read_with(cx, |this, _| this.client.clone()) else { + return Task::ready(Err(anyhow!("SshRemoteClient lost"))); + }; + + cx.spawn(async move |cx| { + let mut missed_heartbeats = 0; + + let keepalive_timer = cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse(); + futures::pin_mut!(keepalive_timer); + + loop { + select_biased! { + result = connection_activity_rx.next().fuse() => { + if result.is_none() { + log::warn!("ssh heartbeat: connection activity channel has been dropped. stopping."); + return Ok(()); + } + + if missed_heartbeats != 0 { + missed_heartbeats = 0; + let _ =this.update(cx, |this, cx| { + this.handle_heartbeat_result(missed_heartbeats, cx) + })?; + } + } + _ = keepalive_timer => { + log::debug!("Sending heartbeat to server..."); + + let result = select_biased! { + _ = connection_activity_rx.next().fuse() => { + Ok(()) + } + ping_result = client.ping(HEARTBEAT_TIMEOUT).fuse() => { + ping_result + } + }; + + if result.is_err() { + missed_heartbeats += 1; + log::warn!( + "No heartbeat from server after {:?}. Missed heartbeat {} out of {}.", + HEARTBEAT_TIMEOUT, + missed_heartbeats, + MAX_MISSED_HEARTBEATS + ); + } else if missed_heartbeats != 0 { + missed_heartbeats = 0; + } else { + continue; + } + + let result = this.update(cx, |this, cx| { + this.handle_heartbeat_result(missed_heartbeats, cx) + })?; + if result.is_break() { + return Ok(()); + } + } + } + + keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse()); + } + }) + } + + fn handle_heartbeat_result( + &mut self, + missed_heartbeats: usize, + cx: &mut Context, + ) -> ControlFlow<()> { + let state = self.state.take().unwrap(); + let next_state = if missed_heartbeats > 0 { + state.heartbeat_missed() + } else { + state.heartbeat_recovered() + }; + + self.set_state(next_state, cx); + + if missed_heartbeats >= MAX_MISSED_HEARTBEATS { + log::error!( + "Missed last {} heartbeats. Reconnecting...", + missed_heartbeats + ); + + self.reconnect(cx) + .context("failed to start reconnect process after missing heartbeats") + .log_err(); + ControlFlow::Break(()) + } else { + ControlFlow::Continue(()) + } + } + + fn monitor( + this: WeakEntity, + io_task: Task>, + cx: &AsyncApp, + ) -> Task> { + cx.spawn(async move |cx| { + let result = io_task.await; + + match result { + Ok(exit_code) => { + if let Some(error) = ProxyLaunchError::from_exit_code(exit_code) { + match error { + ProxyLaunchError::ServerNotRunning => { + log::error!("failed to reconnect because server is not running"); + this.update(cx, |this, cx| { + this.set_state(State::ServerNotRunning, cx); + })?; + } + } + } else if exit_code > 0 { + log::error!("proxy process terminated unexpectedly"); + this.update(cx, |this, cx| { + this.reconnect(cx).ok(); + })?; + } + } + Err(error) => { + log::warn!("ssh io task died with error: {:?}. reconnecting...", error); + this.update(cx, |this, cx| { + this.reconnect(cx).ok(); + })?; + } + } + + Ok(()) + }) + } + + fn state_is(&self, check: impl FnOnce(&State) -> bool) -> bool { + self.state.as_ref().is_some_and(check) + } + + fn try_set_state(&mut self, cx: &mut Context, map: impl FnOnce(&State) -> Option) { + let new_state = self.state.as_ref().and_then(map); + if let Some(new_state) = new_state { + self.state.replace(new_state); + cx.notify(); + } + } + + fn set_state(&mut self, state: State, cx: &mut Context) { + log::info!("setting state to '{}'", &state); + + let is_reconnect_exhausted = state.is_reconnect_exhausted(); + let is_server_not_running = state.is_server_not_running(); + self.state.replace(state); + + if is_reconnect_exhausted || is_server_not_running { + cx.emit(RemoteClientEvent::Disconnected); + } + cx.notify(); + } + + pub fn shell(&self) -> Option { + Some(self.state.as_ref()?.remote_connection()?.shell()) + } + + pub fn build_command( + &self, + program: Option, + args: &[String], + env: &HashMap, + working_dir: Option, + port_forward: Option<(u16, String, u16)>, + ) -> Result { + let Some(connection) = self + .state + .as_ref() + .and_then(|state| state.remote_connection()) + else { + return Err(anyhow!("no connection")); + }; + connection.build_command(program, args, env, working_dir, port_forward) + } + + pub fn upload_directory( + &self, + src_path: PathBuf, + dest_path: RemotePathBuf, + cx: &App, + ) -> Task> { + let Some(connection) = self + .state + .as_ref() + .and_then(|state| state.remote_connection()) + else { + return Task::ready(Err(anyhow!("no ssh connection"))); + }; + connection.upload_directory(src_path, dest_path, cx) + } + + pub fn proto_client(&self) -> AnyProtoClient { + self.client.clone().into() + } + + pub fn host(&self) -> String { + self.connection_options.host.clone() + } + + pub fn connection_options(&self) -> SshConnectionOptions { + self.connection_options.clone() + } + + pub fn connection_state(&self) -> ConnectionState { + self.state + .as_ref() + .map(ConnectionState::from) + .unwrap_or(ConnectionState::Disconnected) + } + + pub fn is_disconnected(&self) -> bool { + self.connection_state() == ConnectionState::Disconnected + } + + pub fn path_style(&self) -> PathStyle { + self.path_style + } + + #[cfg(any(test, feature = "test-support"))] + pub fn simulate_disconnect(&self, client_cx: &mut App) -> Task<()> { + let opts = self.connection_options(); + client_cx.spawn(async move |cx| { + let connection = cx + .update_global(|c: &mut ConnectionPool, _| { + if let Some(ConnectionPoolEntry::Connecting(c)) = c.connections.get(&opts) { + c.clone() + } else { + panic!("missing test connection") + } + }) + .unwrap() + .await + .unwrap(); + + connection.simulate_disconnect(cx); + }) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn fake_server( + client_cx: &mut gpui::TestAppContext, + server_cx: &mut gpui::TestAppContext, + ) -> (SshConnectionOptions, AnyProtoClient) { + let port = client_cx + .update(|cx| cx.default_global::().connections.len() as u16 + 1); + let opts = SshConnectionOptions { + host: "".to_string(), + port: Some(port), + ..Default::default() + }; + let (outgoing_tx, _) = mpsc::unbounded::(); + let (_, incoming_rx) = mpsc::unbounded::(); + let server_client = + server_cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server")); + let connection: Arc = Arc::new(fake::FakeRemoteConnection { + connection_options: opts.clone(), + server_cx: fake::SendableCx::new(server_cx), + server_channel: server_client.clone(), + }); + + client_cx.update(|cx| { + cx.update_default_global(|c: &mut ConnectionPool, cx| { + c.connections.insert( + opts.clone(), + ConnectionPoolEntry::Connecting( + cx.background_spawn({ + let connection = connection.clone(); + async move { Ok(connection.clone()) } + }) + .shared(), + ), + ); + }) + }); + + (opts, server_client.into()) + } + + #[cfg(any(test, feature = "test-support"))] + pub async fn fake_client( + opts: SshConnectionOptions, + client_cx: &mut gpui::TestAppContext, + ) -> Entity { + let (_tx, rx) = oneshot::channel(); + client_cx + .update(|cx| { + Self::ssh( + ConnectionIdentifier::setup(), + opts, + rx, + Arc::new(fake::Delegate), + cx, + ) + }) + .await + .unwrap() + .unwrap() + } +} + +enum ConnectionPoolEntry { + Connecting(Shared, Arc>>>), + Connected(Weak), +} + +#[derive(Default)] +struct ConnectionPool { + connections: HashMap, +} + +impl Global for ConnectionPool {} + +impl ConnectionPool { + pub fn connect( + &mut self, + opts: SshConnectionOptions, + delegate: &Arc, + cx: &mut App, + ) -> Shared, Arc>>> { + let connection = self.connections.get(&opts); + match connection { + Some(ConnectionPoolEntry::Connecting(task)) => { + let delegate = delegate.clone(); + cx.spawn(async move |cx| { + delegate.set_status(Some("Waiting for existing connection attempt"), cx); + }) + .detach(); + return task.clone(); + } + Some(ConnectionPoolEntry::Connected(ssh)) => { + if let Some(ssh) = ssh.upgrade() + && !ssh.has_been_killed() + { + return Task::ready(Ok(ssh)).shared(); + } + self.connections.remove(&opts); + } + None => {} + } + + let task = cx + .spawn({ + let opts = opts.clone(); + let delegate = delegate.clone(); + async move |cx| { + let connection = SshRemoteConnection::new(opts.clone(), delegate, cx) + .await + .map(|connection| Arc::new(connection) as Arc); + + cx.update_global(|pool: &mut Self, _| { + debug_assert!(matches!( + pool.connections.get(&opts), + Some(ConnectionPoolEntry::Connecting(_)) + )); + match connection { + Ok(connection) => { + pool.connections.insert( + opts.clone(), + ConnectionPoolEntry::Connected(Arc::downgrade(&connection)), + ); + Ok(connection) + } + Err(error) => { + pool.connections.remove(&opts); + Err(Arc::new(error)) + } + } + })? + } + }) + .shared(); + + self.connections + .insert(opts.clone(), ConnectionPoolEntry::Connecting(task.clone())); + task + } +} + +#[async_trait(?Send)] +pub(crate) trait RemoteConnection: Send + Sync { + fn start_proxy( + &self, + unique_identifier: String, + reconnect: bool, + incoming_tx: UnboundedSender, + outgoing_rx: UnboundedReceiver, + connection_activity_tx: Sender<()>, + delegate: Arc, + cx: &mut AsyncApp, + ) -> Task>; + fn upload_directory( + &self, + src_path: PathBuf, + dest_path: RemotePathBuf, + cx: &App, + ) -> Task>; + async fn kill(&self) -> Result<()>; + fn has_been_killed(&self) -> bool; + fn build_command( + &self, + program: Option, + args: &[String], + env: &HashMap, + working_dir: Option, + port_forward: Option<(u16, String, u16)>, + ) -> Result; + fn connection_options(&self) -> SshConnectionOptions; + fn path_style(&self) -> PathStyle; + fn shell(&self) -> String; + + #[cfg(any(test, feature = "test-support"))] + fn simulate_disconnect(&self, _: &AsyncApp) {} +} + +type ResponseChannels = Mutex)>>>; + +struct ChannelClient { + next_message_id: AtomicU32, + outgoing_tx: Mutex>, + buffer: Mutex>, + response_channels: ResponseChannels, + message_handlers: Mutex, + max_received: AtomicU32, + name: &'static str, + task: Mutex>>, +} + +impl ChannelClient { + fn new( + incoming_rx: mpsc::UnboundedReceiver, + outgoing_tx: mpsc::UnboundedSender, + cx: &App, + name: &'static str, + ) -> Arc { + Arc::new_cyclic(|this| Self { + outgoing_tx: Mutex::new(outgoing_tx), + next_message_id: AtomicU32::new(0), + max_received: AtomicU32::new(0), + response_channels: ResponseChannels::default(), + message_handlers: Default::default(), + buffer: Mutex::new(VecDeque::new()), + name, + task: Mutex::new(Self::start_handling_messages( + this.clone(), + incoming_rx, + &cx.to_async(), + )), + }) + } + + fn start_handling_messages( + this: Weak, + mut incoming_rx: mpsc::UnboundedReceiver, + cx: &AsyncApp, + ) -> Task> { + cx.spawn(async move |cx| { + let peer_id = PeerId { owner_id: 0, id: 0 }; + while let Some(incoming) = incoming_rx.next().await { + let Some(this) = this.upgrade() else { + return anyhow::Ok(()); + }; + if let Some(ack_id) = incoming.ack_id { + let mut buffer = this.buffer.lock(); + while buffer.front().is_some_and(|msg| msg.id <= ack_id) { + buffer.pop_front(); + } + } + if let Some(proto::envelope::Payload::FlushBufferedMessages(_)) = &incoming.payload + { + log::debug!( + "{}:ssh message received. name:FlushBufferedMessages", + this.name + ); + { + let buffer = this.buffer.lock(); + for envelope in buffer.iter() { + this.outgoing_tx + .lock() + .unbounded_send(envelope.clone()) + .ok(); + } + } + let mut envelope = proto::Ack {}.into_envelope(0, Some(incoming.id), None); + envelope.id = this.next_message_id.fetch_add(1, SeqCst); + this.outgoing_tx.lock().unbounded_send(envelope).ok(); + continue; + } + + this.max_received.store(incoming.id, SeqCst); + + if let Some(request_id) = incoming.responding_to { + let request_id = MessageId(request_id); + let sender = this.response_channels.lock().remove(&request_id); + if let Some(sender) = sender { + let (tx, rx) = oneshot::channel(); + if incoming.payload.is_some() { + sender.send((incoming, tx)).ok(); + } + rx.await.ok(); + } + } else if let Some(envelope) = + build_typed_envelope(peer_id, Instant::now(), incoming) + { + let type_name = envelope.payload_type_name(); + let message_id = envelope.message_id(); + if let Some(future) = ProtoMessageHandlerSet::handle_message( + &this.message_handlers, + envelope, + this.clone().into(), + cx.clone(), + ) { + log::debug!("{}:ssh message received. name:{type_name}", this.name); + cx.foreground_executor() + .spawn(async move { + match future.await { + Ok(_) => { + log::debug!( + "{}:ssh message handled. name:{type_name}", + this.name + ); + } + Err(error) => { + log::error!( + "{}:error handling message. type:{}, error:{}", + this.name, + type_name, + format!("{error:#}").lines().fold( + String::new(), + |mut message, line| { + if !message.is_empty() { + message.push(' '); + } + message.push_str(line); + message + } + ) + ); + } + } + }) + .detach() + } else { + log::error!("{}:unhandled ssh message name:{type_name}", this.name); + if let Err(e) = AnyProtoClient::from(this.clone()).send_response( + message_id, + anyhow::anyhow!("no handler registered for {type_name}").to_proto(), + ) { + log::error!( + "{}:error sending error response for {type_name}:{e:#}", + this.name + ); + } + } + } + } + anyhow::Ok(()) + }) + } + + fn reconnect( + self: &Arc, + incoming_rx: UnboundedReceiver, + outgoing_tx: UnboundedSender, + cx: &AsyncApp, + ) { + *self.outgoing_tx.lock() = outgoing_tx; + *self.task.lock() = Self::start_handling_messages(Arc::downgrade(self), incoming_rx, cx); + } + + fn request( + &self, + payload: T, + ) -> impl 'static + Future> { + self.request_internal(payload, true) + } + + fn request_internal( + &self, + payload: T, + use_buffer: bool, + ) -> impl 'static + Future> { + log::debug!("ssh request start. name:{}", T::NAME); + let response = + self.request_dynamic(payload.into_envelope(0, None, None), T::NAME, use_buffer); + async move { + let response = response.await?; + log::debug!("ssh request finish. name:{}", T::NAME); + T::Response::from_envelope(response).context("received a response of the wrong type") + } + } + + async fn resync(&self, timeout: Duration) -> Result<()> { + smol::future::or( + async { + self.request_internal(proto::FlushBufferedMessages {}, false) + .await?; + + for envelope in self.buffer.lock().iter() { + self.outgoing_tx + .lock() + .unbounded_send(envelope.clone()) + .ok(); + } + Ok(()) + }, + async { + smol::Timer::after(timeout).await; + anyhow::bail!("Timed out resyncing remote client") + }, + ) + .await + } + + async fn ping(&self, timeout: Duration) -> Result<()> { + smol::future::or( + async { + self.request(proto::Ping {}).await?; + Ok(()) + }, + async { + smol::Timer::after(timeout).await; + anyhow::bail!("Timed out pinging remote client") + }, + ) + .await + } + + fn send(&self, payload: T) -> Result<()> { + log::debug!("ssh send name:{}", T::NAME); + self.send_dynamic(payload.into_envelope(0, None, None)) + } + + fn request_dynamic( + &self, + mut envelope: proto::Envelope, + type_name: &'static str, + use_buffer: bool, + ) -> impl 'static + Future> { + envelope.id = self.next_message_id.fetch_add(1, SeqCst); + let (tx, rx) = oneshot::channel(); + let mut response_channels_lock = self.response_channels.lock(); + response_channels_lock.insert(MessageId(envelope.id), tx); + drop(response_channels_lock); + + let result = if use_buffer { + self.send_buffered(envelope) + } else { + self.send_unbuffered(envelope) + }; + async move { + if let Err(error) = &result { + log::error!("failed to send message: {error}"); + anyhow::bail!("failed to send message: {error}"); + } + + let response = rx.await.context("connection lost")?.0; + if let Some(proto::envelope::Payload::Error(error)) = &response.payload { + return Err(RpcError::from_proto(error, type_name)); + } + Ok(response) + } + } + + pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> { + envelope.id = self.next_message_id.fetch_add(1, SeqCst); + self.send_buffered(envelope) + } + + fn send_buffered(&self, mut envelope: proto::Envelope) -> Result<()> { + envelope.ack_id = Some(self.max_received.load(SeqCst)); + self.buffer.lock().push_back(envelope.clone()); + // ignore errors on send (happen while we're reconnecting) + // assume that the global "disconnected" overlay is sufficient. + self.outgoing_tx.lock().unbounded_send(envelope).ok(); + Ok(()) + } + + fn send_unbuffered(&self, mut envelope: proto::Envelope) -> Result<()> { + envelope.ack_id = Some(self.max_received.load(SeqCst)); + self.outgoing_tx.lock().unbounded_send(envelope).ok(); + Ok(()) + } +} + +impl ProtoClient for ChannelClient { + fn request( + &self, + envelope: proto::Envelope, + request_type: &'static str, + ) -> BoxFuture<'static, Result> { + self.request_dynamic(envelope, request_type, true).boxed() + } + + fn send(&self, envelope: proto::Envelope, _message_type: &'static str) -> Result<()> { + self.send_dynamic(envelope) + } + + fn send_response(&self, envelope: Envelope, _message_type: &'static str) -> anyhow::Result<()> { + self.send_dynamic(envelope) + } + + fn message_handler_set(&self) -> &Mutex { + &self.message_handlers + } + + fn is_via_collab(&self) -> bool { + false + } +} + +#[cfg(any(test, feature = "test-support"))] +mod fake { + use super::{ChannelClient, RemoteClientDelegate, RemoteConnection, RemotePlatform}; + use crate::{SshConnectionOptions, remote_client::CommandTemplate}; + use anyhow::Result; + use async_trait::async_trait; + use collections::HashMap; + use futures::{ + FutureExt, SinkExt, StreamExt, + channel::{ + mpsc::{self, Sender}, + oneshot, + }, + select_biased, + }; + use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task, TestAppContext}; + use release_channel::ReleaseChannel; + use rpc::proto::Envelope; + use std::{path::PathBuf, sync::Arc}; + use util::paths::{PathStyle, RemotePathBuf}; + + pub(super) struct FakeRemoteConnection { + pub(super) connection_options: SshConnectionOptions, + pub(super) server_channel: Arc, + pub(super) server_cx: SendableCx, + } + + pub(super) struct SendableCx(AsyncApp); + impl SendableCx { + // SAFETY: When run in test mode, GPUI is always single threaded. + pub(super) fn new(cx: &TestAppContext) -> Self { + Self(cx.to_async()) + } + + // SAFETY: Enforce that we're on the main thread by requiring a valid AsyncApp + fn get(&self, _: &AsyncApp) -> AsyncApp { + self.0.clone() + } + } + + // SAFETY: There is no way to access a SendableCx from a different thread, see [`SendableCx::new`] and [`SendableCx::get`] + unsafe impl Send for SendableCx {} + unsafe impl Sync for SendableCx {} + + #[async_trait(?Send)] + impl RemoteConnection for FakeRemoteConnection { + async fn kill(&self) -> Result<()> { + Ok(()) + } + + fn has_been_killed(&self) -> bool { + false + } + + fn build_command( + &self, + program: Option, + args: &[String], + env: &HashMap, + _: Option, + _: Option<(u16, String, u16)>, + ) -> Result { + let ssh_program = program.unwrap_or_else(|| "sh".to_string()); + let mut ssh_args = Vec::new(); + ssh_args.push(ssh_program); + ssh_args.extend(args.iter().cloned()); + Ok(CommandTemplate { + program: "ssh".into(), + args: ssh_args, + env: env.clone(), + }) + } + + fn upload_directory( + &self, + _src_path: PathBuf, + _dest_path: RemotePathBuf, + _cx: &App, + ) -> Task> { + unreachable!() + } + + fn connection_options(&self) -> SshConnectionOptions { + self.connection_options.clone() + } + + fn simulate_disconnect(&self, cx: &AsyncApp) { + let (outgoing_tx, _) = mpsc::unbounded::(); + let (_, incoming_rx) = mpsc::unbounded::(); + self.server_channel + .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx)); + } + + fn start_proxy( + &self, + _unique_identifier: String, + _reconnect: bool, + mut client_incoming_tx: mpsc::UnboundedSender, + mut client_outgoing_rx: mpsc::UnboundedReceiver, + mut connection_activity_tx: Sender<()>, + _delegate: Arc, + cx: &mut AsyncApp, + ) -> Task> { + let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::(); + let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::(); + + self.server_channel.reconnect( + server_incoming_rx, + server_outgoing_tx, + &self.server_cx.get(cx), + ); + + cx.background_spawn(async move { + loop { + select_biased! { + server_to_client = server_outgoing_rx.next().fuse() => { + let Some(server_to_client) = server_to_client else { + return Ok(1) + }; + connection_activity_tx.try_send(()).ok(); + client_incoming_tx.send(server_to_client).await.ok(); + } + client_to_server = client_outgoing_rx.next().fuse() => { + let Some(client_to_server) = client_to_server else { + return Ok(1) + }; + server_incoming_tx.send(client_to_server).await.ok(); + } + } + } + }) + } + + fn path_style(&self) -> PathStyle { + PathStyle::current() + } + + fn shell(&self) -> String { + "sh".to_owned() + } + } + + pub(super) struct Delegate; + + impl RemoteClientDelegate for Delegate { + fn ask_password(&self, _: String, _: oneshot::Sender, _: &mut AsyncApp) { + unreachable!() + } + + fn download_server_binary_locally( + &self, + _: RemotePlatform, + _: ReleaseChannel, + _: Option, + _: &mut AsyncApp, + ) -> Task> { + unreachable!() + } + + fn get_download_params( + &self, + _platform: RemotePlatform, + _release_channel: ReleaseChannel, + _version: Option, + _cx: &mut AsyncApp, + ) -> Task>> { + unreachable!() + } + + fn set_status(&self, _: Option<&str>, _: &mut AsyncApp) {} + } +} diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs deleted file mode 100644 index 67940184705f9fc91aba29997a16e1df592099a1..0000000000000000000000000000000000000000 --- a/crates/remote/src/ssh_session.rs +++ /dev/null @@ -1,2749 +0,0 @@ -use crate::{ - json_log::LogRecord, - protocol::{ - MESSAGE_LEN_SIZE, MessageId, message_len_from_buffer, read_message_with_len, write_message, - }, - proxy::ProxyLaunchError, -}; -use anyhow::{Context as _, Result, anyhow}; -use async_trait::async_trait; -use collections::HashMap; -use futures::{ - AsyncReadExt as _, Future, FutureExt as _, StreamExt as _, - channel::{ - mpsc::{self, Sender, UnboundedReceiver, UnboundedSender}, - oneshot, - }, - future::{BoxFuture, Shared}, - select, select_biased, -}; -use gpui::{ - App, AppContext as _, AsyncApp, BackgroundExecutor, BorrowAppContext, Context, Entity, - EventEmitter, Global, SemanticVersion, Task, WeakEntity, -}; -use itertools::Itertools; -use parking_lot::Mutex; - -use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; -use rpc::{ - AnyProtoClient, ErrorExt, ProtoClient, ProtoMessageHandlerSet, RpcError, - proto::{self, Envelope, EnvelopedMessage, PeerId, RequestMessage, build_typed_envelope}, -}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use smol::{ - fs, - process::{self, Child, Stdio}, -}; -use std::{ - collections::VecDeque, - fmt, iter, - ops::ControlFlow, - path::{Path, PathBuf}, - sync::{ - Arc, Weak, - atomic::{AtomicU32, AtomicU64, Ordering::SeqCst}, - }, - time::{Duration, Instant}, -}; -use tempfile::TempDir; -use util::{ - ResultExt, - paths::{PathStyle, RemotePathBuf}, -}; - -#[derive(Clone)] -pub struct SshSocket { - connection_options: SshConnectionOptions, - #[cfg(not(target_os = "windows"))] - socket_path: PathBuf, - #[cfg(target_os = "windows")] - envs: HashMap, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)] -pub struct SshPortForwardOption { - #[serde(skip_serializing_if = "Option::is_none")] - pub local_host: Option, - pub local_port: u16, - #[serde(skip_serializing_if = "Option::is_none")] - pub remote_host: Option, - pub remote_port: u16, -} - -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -pub struct SshConnectionOptions { - pub host: String, - pub username: Option, - pub port: Option, - pub password: Option, - pub args: Option>, - pub port_forwards: Option>, - - pub nickname: Option, - pub upload_binary_over_ssh: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SshArgs { - pub arguments: Vec, - pub envs: Option>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SshInfo { - pub args: SshArgs, - pub path_style: PathStyle, - pub shell: String, -} - -#[macro_export] -macro_rules! shell_script { - ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{ - format!( - $fmt, - $( - $name = shlex::try_quote($arg).unwrap() - ),+ - ) - }}; -} - -fn parse_port_number(port_str: &str) -> Result { - port_str - .parse() - .with_context(|| format!("parsing port number: {port_str}")) -} - -fn parse_port_forward_spec(spec: &str) -> Result { - let parts: Vec<&str> = spec.split(':').collect(); - - match parts.len() { - 4 => { - let local_port = parse_port_number(parts[1])?; - let remote_port = parse_port_number(parts[3])?; - - Ok(SshPortForwardOption { - local_host: Some(parts[0].to_string()), - local_port, - remote_host: Some(parts[2].to_string()), - remote_port, - }) - } - 3 => { - let local_port = parse_port_number(parts[0])?; - let remote_port = parse_port_number(parts[2])?; - - Ok(SshPortForwardOption { - local_host: None, - local_port, - remote_host: Some(parts[1].to_string()), - remote_port, - }) - } - _ => anyhow::bail!("Invalid port forward format"), - } -} - -impl SshConnectionOptions { - pub fn parse_command_line(input: &str) -> Result { - let input = input.trim_start_matches("ssh "); - let mut hostname: Option = None; - let mut username: Option = None; - let mut port: Option = None; - let mut args = Vec::new(); - let mut port_forwards: Vec = Vec::new(); - - // disallowed: -E, -e, -F, -f, -G, -g, -M, -N, -n, -O, -q, -S, -s, -T, -t, -V, -v, -W - const ALLOWED_OPTS: &[&str] = &[ - "-4", "-6", "-A", "-a", "-C", "-K", "-k", "-X", "-x", "-Y", "-y", - ]; - const ALLOWED_ARGS: &[&str] = &[ - "-B", "-b", "-c", "-D", "-F", "-I", "-i", "-J", "-l", "-m", "-o", "-P", "-p", "-R", - "-w", - ]; - - let mut tokens = shlex::split(input).context("invalid input")?.into_iter(); - - 'outer: while let Some(arg) = tokens.next() { - if ALLOWED_OPTS.contains(&(&arg as &str)) { - args.push(arg.to_string()); - continue; - } - if arg == "-p" { - port = tokens.next().and_then(|arg| arg.parse().ok()); - continue; - } else if let Some(p) = arg.strip_prefix("-p") { - port = p.parse().ok(); - continue; - } - if arg == "-l" { - username = tokens.next(); - continue; - } else if let Some(l) = arg.strip_prefix("-l") { - username = Some(l.to_string()); - continue; - } - if arg == "-L" || arg.starts_with("-L") { - let forward_spec = if arg == "-L" { - tokens.next() - } else { - Some(arg.strip_prefix("-L").unwrap().to_string()) - }; - - if let Some(spec) = forward_spec { - port_forwards.push(parse_port_forward_spec(&spec)?); - } else { - anyhow::bail!("Missing port forward format"); - } - } - - for a in ALLOWED_ARGS { - if arg == *a { - args.push(arg); - if let Some(next) = tokens.next() { - args.push(next); - } - continue 'outer; - } else if arg.starts_with(a) { - args.push(arg); - continue 'outer; - } - } - if arg.starts_with("-") || hostname.is_some() { - anyhow::bail!("unsupported argument: {:?}", arg); - } - let mut input = &arg as &str; - // Destination might be: username1@username2@ip2@ip1 - if let Some((u, rest)) = input.rsplit_once('@') { - input = rest; - username = Some(u.to_string()); - } - if let Some((rest, p)) = input.split_once(':') { - input = rest; - port = p.parse().ok() - } - hostname = Some(input.to_string()) - } - - let Some(hostname) = hostname else { - anyhow::bail!("missing hostname"); - }; - - let port_forwards = match port_forwards.len() { - 0 => None, - _ => Some(port_forwards), - }; - - Ok(Self { - host: hostname, - username, - port, - port_forwards, - args: Some(args), - password: None, - nickname: None, - upload_binary_over_ssh: false, - }) - } - - pub fn ssh_url(&self) -> String { - let mut result = String::from("ssh://"); - if let Some(username) = &self.username { - // Username might be: username1@username2@ip2 - let username = urlencoding::encode(username); - result.push_str(&username); - result.push('@'); - } - result.push_str(&self.host); - if let Some(port) = self.port { - result.push(':'); - result.push_str(&port.to_string()); - } - result - } - - pub fn additional_args(&self) -> Vec { - let mut args = self.args.iter().flatten().cloned().collect::>(); - - if let Some(forwards) = &self.port_forwards { - args.extend(forwards.iter().map(|pf| { - let local_host = match &pf.local_host { - Some(host) => host, - None => "localhost", - }; - let remote_host = match &pf.remote_host { - Some(host) => host, - None => "localhost", - }; - - format!( - "-L{}:{}:{}:{}", - local_host, pf.local_port, remote_host, pf.remote_port - ) - })); - } - - args - } - - fn scp_url(&self) -> String { - if let Some(username) = &self.username { - format!("{}@{}", username, self.host) - } else { - self.host.clone() - } - } - - pub fn connection_string(&self) -> String { - let host = if let Some(username) = &self.username { - format!("{}@{}", username, self.host) - } else { - self.host.clone() - }; - if let Some(port) = &self.port { - format!("{}:{}", host, port) - } else { - host - } - } -} - -#[derive(Copy, Clone, Debug)] -pub struct SshPlatform { - pub os: &'static str, - pub arch: &'static str, -} - -pub trait SshClientDelegate: Send + Sync { - fn ask_password(&self, prompt: String, tx: oneshot::Sender, cx: &mut AsyncApp); - fn get_download_params( - &self, - platform: SshPlatform, - release_channel: ReleaseChannel, - version: Option, - cx: &mut AsyncApp, - ) -> Task>>; - - fn download_server_binary_locally( - &self, - platform: SshPlatform, - release_channel: ReleaseChannel, - version: Option, - cx: &mut AsyncApp, - ) -> Task>; - fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp); -} - -impl SshSocket { - #[cfg(not(target_os = "windows"))] - fn new(options: SshConnectionOptions, socket_path: PathBuf) -> Result { - Ok(Self { - connection_options: options, - socket_path, - }) - } - - #[cfg(target_os = "windows")] - fn new(options: SshConnectionOptions, temp_dir: &TempDir, secret: String) -> Result { - let askpass_script = temp_dir.path().join("askpass.bat"); - std::fs::write(&askpass_script, "@ECHO OFF\necho %ZED_SSH_ASKPASS%")?; - let mut envs = HashMap::default(); - envs.insert("SSH_ASKPASS_REQUIRE".into(), "force".into()); - envs.insert("SSH_ASKPASS".into(), askpass_script.display().to_string()); - envs.insert("ZED_SSH_ASKPASS".into(), secret); - Ok(Self { - connection_options: options, - envs, - }) - } - - // :WARNING: ssh unquotes arguments when executing on the remote :WARNING: - // e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l - // and passes -l as an argument to sh, not to ls. - // Furthermore, some setups (e.g. Coder) will change directory when SSH'ing - // into a machine. You must use `cd` to get back to $HOME. - // You need to do it like this: $ ssh host "cd; sh -c 'ls -l /tmp'" - fn ssh_command(&self, program: &str, args: &[&str]) -> process::Command { - let mut command = util::command::new_smol_command("ssh"); - let to_run = iter::once(&program) - .chain(args.iter()) - .map(|token| { - // We're trying to work with: sh, bash, zsh, fish, tcsh, ...? - debug_assert!( - !token.contains('\n'), - "multiline arguments do not work in all shells" - ); - shlex::try_quote(token).unwrap() - }) - .join(" "); - let to_run = format!("cd; {to_run}"); - log::debug!("ssh {} {:?}", self.connection_options.ssh_url(), to_run); - self.ssh_options(&mut command) - .arg(self.connection_options.ssh_url()) - .arg(to_run); - command - } - - async fn run_command(&self, program: &str, args: &[&str]) -> Result { - let output = self.ssh_command(program, args).output().await?; - anyhow::ensure!( - output.status.success(), - "failed to run command: {}", - String::from_utf8_lossy(&output.stderr) - ); - Ok(String::from_utf8_lossy(&output.stdout).to_string()) - } - - #[cfg(not(target_os = "windows"))] - fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command { - command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .args(self.connection_options.additional_args()) - .args(["-o", "ControlMaster=no", "-o"]) - .arg(format!("ControlPath={}", self.socket_path.display())) - } - - #[cfg(target_os = "windows")] - fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command { - command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .args(self.connection_options.additional_args()) - .envs(self.envs.clone()) - } - - // On Windows, we need to use `SSH_ASKPASS` to provide the password to ssh. - // On Linux, we use the `ControlPath` option to create a socket file that ssh can use to - #[cfg(not(target_os = "windows"))] - fn ssh_args(&self) -> SshArgs { - let mut arguments = self.connection_options.additional_args(); - arguments.extend(vec![ - "-o".to_string(), - "ControlMaster=no".to_string(), - "-o".to_string(), - format!("ControlPath={}", self.socket_path.display()), - self.connection_options.ssh_url(), - ]); - SshArgs { - arguments, - envs: None, - } - } - - #[cfg(target_os = "windows")] - fn ssh_args(&self) -> SshArgs { - let mut arguments = self.connection_options.additional_args(); - arguments.push(self.connection_options.ssh_url()); - SshArgs { - arguments, - envs: Some(self.envs.clone()), - } - } - - async fn platform(&self) -> Result { - let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?; - let Some((os, arch)) = uname.split_once(" ") else { - anyhow::bail!("unknown uname: {uname:?}") - }; - - let os = match os.trim() { - "Darwin" => "macos", - "Linux" => "linux", - _ => anyhow::bail!( - "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development" - ), - }; - // exclude armv5,6,7 as they are 32-bit. - let arch = if arch.starts_with("armv8") - || arch.starts_with("armv9") - || arch.starts_with("arm64") - || arch.starts_with("aarch64") - { - "aarch64" - } else if arch.starts_with("x86") { - "x86_64" - } else { - anyhow::bail!( - "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development" - ) - }; - - Ok(SshPlatform { os, arch }) - } - - async fn shell(&self) -> String { - match self.run_command("sh", &["-lc", "echo $SHELL"]).await { - Ok(shell) => shell.trim().to_owned(), - Err(e) => { - log::error!("Failed to get shell: {e}"); - "sh".to_owned() - } - } - } -} - -const MAX_MISSED_HEARTBEATS: usize = 5; -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); -const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(5); - -const MAX_RECONNECT_ATTEMPTS: usize = 3; - -enum State { - Connecting, - Connected { - ssh_connection: Arc, - delegate: Arc, - - multiplex_task: Task>, - heartbeat_task: Task>, - }, - HeartbeatMissed { - missed_heartbeats: usize, - - ssh_connection: Arc, - delegate: Arc, - - multiplex_task: Task>, - heartbeat_task: Task>, - }, - Reconnecting, - ReconnectFailed { - ssh_connection: Arc, - delegate: Arc, - - error: anyhow::Error, - attempts: usize, - }, - ReconnectExhausted, - ServerNotRunning, -} - -impl fmt::Display for State { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Connecting => write!(f, "connecting"), - Self::Connected { .. } => write!(f, "connected"), - Self::Reconnecting => write!(f, "reconnecting"), - Self::ReconnectFailed { .. } => write!(f, "reconnect failed"), - Self::ReconnectExhausted => write!(f, "reconnect exhausted"), - Self::HeartbeatMissed { .. } => write!(f, "heartbeat missed"), - Self::ServerNotRunning { .. } => write!(f, "server not running"), - } - } -} - -impl State { - fn ssh_connection(&self) -> Option<&dyn RemoteConnection> { - match self { - Self::Connected { ssh_connection, .. } => Some(ssh_connection.as_ref()), - Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection.as_ref()), - Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection.as_ref()), - _ => None, - } - } - - fn can_reconnect(&self) -> bool { - match self { - Self::Connected { .. } - | Self::HeartbeatMissed { .. } - | Self::ReconnectFailed { .. } => true, - State::Connecting - | State::Reconnecting - | State::ReconnectExhausted - | State::ServerNotRunning => false, - } - } - - fn is_reconnect_failed(&self) -> bool { - matches!(self, Self::ReconnectFailed { .. }) - } - - fn is_reconnect_exhausted(&self) -> bool { - matches!(self, Self::ReconnectExhausted { .. }) - } - - fn is_server_not_running(&self) -> bool { - matches!(self, Self::ServerNotRunning) - } - - fn is_reconnecting(&self) -> bool { - matches!(self, Self::Reconnecting { .. }) - } - - fn heartbeat_recovered(self) -> Self { - match self { - Self::HeartbeatMissed { - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - .. - } => Self::Connected { - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - }, - _ => self, - } - } - - fn heartbeat_missed(self) -> Self { - match self { - Self::Connected { - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - } => Self::HeartbeatMissed { - missed_heartbeats: 1, - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - }, - Self::HeartbeatMissed { - missed_heartbeats, - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - } => Self::HeartbeatMissed { - missed_heartbeats: missed_heartbeats + 1, - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - }, - _ => self, - } - } -} - -/// The state of the ssh connection. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ConnectionState { - Connecting, - Connected, - HeartbeatMissed, - Reconnecting, - Disconnected, -} - -impl From<&State> for ConnectionState { - fn from(value: &State) -> Self { - match value { - State::Connecting => Self::Connecting, - State::Connected { .. } => Self::Connected, - State::Reconnecting | State::ReconnectFailed { .. } => Self::Reconnecting, - State::HeartbeatMissed { .. } => Self::HeartbeatMissed, - State::ReconnectExhausted => Self::Disconnected, - State::ServerNotRunning => Self::Disconnected, - } - } -} - -pub struct SshRemoteClient { - client: Arc, - unique_identifier: String, - connection_options: SshConnectionOptions, - path_style: PathStyle, - state: Arc>>, -} - -#[derive(Debug)] -pub enum SshRemoteEvent { - Disconnected, -} - -impl EventEmitter for SshRemoteClient {} - -// Identifies the socket on the remote server so that reconnects -// can re-join the same project. -pub enum ConnectionIdentifier { - Setup(u64), - Workspace(i64), -} - -static NEXT_ID: AtomicU64 = AtomicU64::new(1); - -impl ConnectionIdentifier { - pub fn setup() -> Self { - Self::Setup(NEXT_ID.fetch_add(1, SeqCst)) - } - - // This string gets used in a socket name, and so must be relatively short. - // The total length of: - // /home/{username}/.local/share/zed/server_state/{name}/stdout.sock - // Must be less than about 100 characters - // https://unix.stackexchange.com/questions/367008/why-is-socket-path-length-limited-to-a-hundred-chars - // So our strings should be at most 20 characters or so. - fn to_string(&self, cx: &App) -> String { - let identifier_prefix = match ReleaseChannel::global(cx) { - ReleaseChannel::Stable => "".to_string(), - release_channel => format!("{}-", release_channel.dev_name()), - }; - match self { - Self::Setup(setup_id) => format!("{identifier_prefix}setup-{setup_id}"), - Self::Workspace(workspace_id) => { - format!("{identifier_prefix}workspace-{workspace_id}",) - } - } - } -} - -impl SshRemoteClient { - pub fn new( - unique_identifier: ConnectionIdentifier, - connection_options: SshConnectionOptions, - cancellation: oneshot::Receiver<()>, - delegate: Arc, - cx: &mut App, - ) -> Task>>> { - let unique_identifier = unique_identifier.to_string(cx); - cx.spawn(async move |cx| { - let success = Box::pin(async move { - let (outgoing_tx, outgoing_rx) = mpsc::unbounded::(); - let (incoming_tx, incoming_rx) = mpsc::unbounded::(); - let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1); - - let client = - cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?; - - let ssh_connection = cx - .update(|cx| { - cx.update_default_global(|pool: &mut ConnectionPool, cx| { - pool.connect(connection_options.clone(), &delegate, cx) - }) - })? - .await - .map_err(|e| e.cloned())?; - - let path_style = ssh_connection.path_style(); - let this = cx.new(|_| Self { - client: client.clone(), - unique_identifier: unique_identifier.clone(), - connection_options, - path_style, - state: Arc::new(Mutex::new(Some(State::Connecting))), - })?; - - let io_task = ssh_connection.start_proxy( - unique_identifier, - false, - incoming_tx, - outgoing_rx, - connection_activity_tx, - delegate.clone(), - cx, - ); - - let multiplex_task = Self::monitor(this.downgrade(), io_task, cx); - - if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await { - log::error!("failed to establish connection: {}", error); - return Err(error); - } - - let heartbeat_task = Self::heartbeat(this.downgrade(), connection_activity_rx, cx); - - this.update(cx, |this, _| { - *this.state.lock() = Some(State::Connected { - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - }); - })?; - - Ok(Some(this)) - }); - - select! { - _ = cancellation.fuse() => { - Ok(None) - } - result = success.fuse() => result - } - }) - } - - pub fn proto_client_from_channels( - incoming_rx: mpsc::UnboundedReceiver, - outgoing_tx: mpsc::UnboundedSender, - cx: &App, - name: &'static str, - ) -> AnyProtoClient { - ChannelClient::new(incoming_rx, outgoing_tx, cx, name).into() - } - - pub fn shutdown_processes( - &self, - shutdown_request: Option, - executor: BackgroundExecutor, - ) -> Option + use> { - let state = self.state.lock().take()?; - log::info!("shutting down ssh processes"); - - let State::Connected { - multiplex_task, - heartbeat_task, - ssh_connection, - delegate, - } = state - else { - return None; - }; - - let client = self.client.clone(); - - Some(async move { - if let Some(shutdown_request) = shutdown_request { - client.send(shutdown_request).log_err(); - // We wait 50ms instead of waiting for a response, because - // waiting for a response would require us to wait on the main thread - // which we want to avoid in an `on_app_quit` callback. - executor.timer(Duration::from_millis(50)).await; - } - - // Drop `multiplex_task` because it owns our ssh_proxy_process, which is a - // child of master_process. - drop(multiplex_task); - // Now drop the rest of state, which kills master process. - drop(heartbeat_task); - drop(ssh_connection); - drop(delegate); - }) - } - - fn reconnect(&mut self, cx: &mut Context) -> Result<()> { - let mut lock = self.state.lock(); - - let can_reconnect = lock - .as_ref() - .map(|state| state.can_reconnect()) - .unwrap_or(false); - if !can_reconnect { - log::info!("aborting reconnect, because not in state that allows reconnecting"); - let error = if let Some(state) = lock.as_ref() { - format!("invalid state, cannot reconnect while in state {state}") - } else { - "no state set".to_string() - }; - anyhow::bail!(error); - } - - let state = lock.take().unwrap(); - let (attempts, ssh_connection, delegate) = match state { - State::Connected { - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - } - | State::HeartbeatMissed { - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - .. - } => { - drop(multiplex_task); - drop(heartbeat_task); - (0, ssh_connection, delegate) - } - State::ReconnectFailed { - attempts, - ssh_connection, - delegate, - .. - } => (attempts, ssh_connection, delegate), - State::Connecting - | State::Reconnecting - | State::ReconnectExhausted - | State::ServerNotRunning => unreachable!(), - }; - - let attempts = attempts + 1; - if attempts > MAX_RECONNECT_ATTEMPTS { - log::error!( - "Failed to reconnect to after {} attempts, giving up", - MAX_RECONNECT_ATTEMPTS - ); - drop(lock); - self.set_state(State::ReconnectExhausted, cx); - return Ok(()); - } - drop(lock); - - self.set_state(State::Reconnecting, cx); - - log::info!("Trying to reconnect to ssh server... Attempt {}", attempts); - - let unique_identifier = self.unique_identifier.clone(); - let client = self.client.clone(); - let reconnect_task = cx.spawn(async move |this, cx| { - macro_rules! failed { - ($error:expr, $attempts:expr, $ssh_connection:expr, $delegate:expr) => { - return State::ReconnectFailed { - error: anyhow!($error), - attempts: $attempts, - ssh_connection: $ssh_connection, - delegate: $delegate, - }; - }; - } - - if let Err(error) = ssh_connection - .kill() - .await - .context("Failed to kill ssh process") - { - failed!(error, attempts, ssh_connection, delegate); - }; - - let connection_options = ssh_connection.connection_options(); - - let (outgoing_tx, outgoing_rx) = mpsc::unbounded::(); - let (incoming_tx, incoming_rx) = mpsc::unbounded::(); - let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1); - - let (ssh_connection, io_task) = match async { - let ssh_connection = cx - .update_global(|pool: &mut ConnectionPool, cx| { - pool.connect(connection_options, &delegate, cx) - })? - .await - .map_err(|error| error.cloned())?; - - let io_task = ssh_connection.start_proxy( - unique_identifier, - true, - incoming_tx, - outgoing_rx, - connection_activity_tx, - delegate.clone(), - cx, - ); - anyhow::Ok((ssh_connection, io_task)) - } - .await - { - Ok((ssh_connection, io_task)) => (ssh_connection, io_task), - Err(error) => { - failed!(error, attempts, ssh_connection, delegate); - } - }; - - let multiplex_task = Self::monitor(this.clone(), io_task, cx); - client.reconnect(incoming_rx, outgoing_tx, cx); - - if let Err(error) = client.resync(HEARTBEAT_TIMEOUT).await { - failed!(error, attempts, ssh_connection, delegate); - }; - - State::Connected { - ssh_connection, - delegate, - multiplex_task, - heartbeat_task: Self::heartbeat(this.clone(), connection_activity_rx, cx), - } - }); - - cx.spawn(async move |this, cx| { - let new_state = reconnect_task.await; - this.update(cx, |this, cx| { - this.try_set_state(cx, |old_state| { - if old_state.is_reconnecting() { - match &new_state { - State::Connecting - | State::Reconnecting - | State::HeartbeatMissed { .. } - | State::ServerNotRunning => {} - State::Connected { .. } => { - log::info!("Successfully reconnected"); - } - State::ReconnectFailed { - error, attempts, .. - } => { - log::error!( - "Reconnect attempt {} failed: {:?}. Starting new attempt...", - attempts, - error - ); - } - State::ReconnectExhausted => { - log::error!("Reconnect attempt failed and all attempts exhausted"); - } - } - Some(new_state) - } else { - None - } - }); - - if this.state_is(State::is_reconnect_failed) { - this.reconnect(cx) - } else if this.state_is(State::is_reconnect_exhausted) { - Ok(()) - } else { - log::debug!("State has transition from Reconnecting into new state while attempting reconnect."); - Ok(()) - } - }) - }) - .detach_and_log_err(cx); - - Ok(()) - } - - fn heartbeat( - this: WeakEntity, - mut connection_activity_rx: mpsc::Receiver<()>, - cx: &mut AsyncApp, - ) -> Task> { - let Ok(client) = this.read_with(cx, |this, _| this.client.clone()) else { - return Task::ready(Err(anyhow!("SshRemoteClient lost"))); - }; - - cx.spawn(async move |cx| { - let mut missed_heartbeats = 0; - - let keepalive_timer = cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse(); - futures::pin_mut!(keepalive_timer); - - loop { - select_biased! { - result = connection_activity_rx.next().fuse() => { - if result.is_none() { - log::warn!("ssh heartbeat: connection activity channel has been dropped. stopping."); - return Ok(()); - } - - if missed_heartbeats != 0 { - missed_heartbeats = 0; - let _ =this.update(cx, |this, cx| { - this.handle_heartbeat_result(missed_heartbeats, cx) - })?; - } - } - _ = keepalive_timer => { - log::debug!("Sending heartbeat to server..."); - - let result = select_biased! { - _ = connection_activity_rx.next().fuse() => { - Ok(()) - } - ping_result = client.ping(HEARTBEAT_TIMEOUT).fuse() => { - ping_result - } - }; - - if result.is_err() { - missed_heartbeats += 1; - log::warn!( - "No heartbeat from server after {:?}. Missed heartbeat {} out of {}.", - HEARTBEAT_TIMEOUT, - missed_heartbeats, - MAX_MISSED_HEARTBEATS - ); - } else if missed_heartbeats != 0 { - missed_heartbeats = 0; - } else { - continue; - } - - let result = this.update(cx, |this, cx| { - this.handle_heartbeat_result(missed_heartbeats, cx) - })?; - if result.is_break() { - return Ok(()); - } - } - } - - keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse()); - } - }) - } - - fn handle_heartbeat_result( - &mut self, - missed_heartbeats: usize, - cx: &mut Context, - ) -> ControlFlow<()> { - let state = self.state.lock().take().unwrap(); - let next_state = if missed_heartbeats > 0 { - state.heartbeat_missed() - } else { - state.heartbeat_recovered() - }; - - self.set_state(next_state, cx); - - if missed_heartbeats >= MAX_MISSED_HEARTBEATS { - log::error!( - "Missed last {} heartbeats. Reconnecting...", - missed_heartbeats - ); - - self.reconnect(cx) - .context("failed to start reconnect process after missing heartbeats") - .log_err(); - ControlFlow::Break(()) - } else { - ControlFlow::Continue(()) - } - } - - fn monitor( - this: WeakEntity, - io_task: Task>, - cx: &AsyncApp, - ) -> Task> { - cx.spawn(async move |cx| { - let result = io_task.await; - - match result { - Ok(exit_code) => { - if let Some(error) = ProxyLaunchError::from_exit_code(exit_code) { - match error { - ProxyLaunchError::ServerNotRunning => { - log::error!("failed to reconnect because server is not running"); - this.update(cx, |this, cx| { - this.set_state(State::ServerNotRunning, cx); - })?; - } - } - } else if exit_code > 0 { - log::error!("proxy process terminated unexpectedly"); - this.update(cx, |this, cx| { - this.reconnect(cx).ok(); - })?; - } - } - Err(error) => { - log::warn!("ssh io task died with error: {:?}. reconnecting...", error); - this.update(cx, |this, cx| { - this.reconnect(cx).ok(); - })?; - } - } - - Ok(()) - }) - } - - fn state_is(&self, check: impl FnOnce(&State) -> bool) -> bool { - self.state.lock().as_ref().is_some_and(check) - } - - fn try_set_state(&self, cx: &mut Context, map: impl FnOnce(&State) -> Option) { - let mut lock = self.state.lock(); - let new_state = lock.as_ref().and_then(map); - - if let Some(new_state) = new_state { - lock.replace(new_state); - cx.notify(); - } - } - - fn set_state(&self, state: State, cx: &mut Context) { - log::info!("setting state to '{}'", &state); - - let is_reconnect_exhausted = state.is_reconnect_exhausted(); - let is_server_not_running = state.is_server_not_running(); - self.state.lock().replace(state); - - if is_reconnect_exhausted || is_server_not_running { - cx.emit(SshRemoteEvent::Disconnected); - } - cx.notify(); - } - - pub fn ssh_info(&self) -> Option { - self.state - .lock() - .as_ref() - .and_then(|state| state.ssh_connection()) - .map(|ssh_connection| SshInfo { - args: ssh_connection.ssh_args(), - path_style: ssh_connection.path_style(), - shell: ssh_connection.shell(), - }) - } - - pub fn upload_directory( - &self, - src_path: PathBuf, - dest_path: RemotePathBuf, - cx: &App, - ) -> Task> { - let state = self.state.lock(); - let Some(connection) = state.as_ref().and_then(|state| state.ssh_connection()) else { - return Task::ready(Err(anyhow!("no ssh connection"))); - }; - connection.upload_directory(src_path, dest_path, cx) - } - - pub fn proto_client(&self) -> AnyProtoClient { - self.client.clone().into() - } - - pub fn connection_string(&self) -> String { - self.connection_options.connection_string() - } - - pub fn connection_options(&self) -> SshConnectionOptions { - self.connection_options.clone() - } - - pub fn connection_state(&self) -> ConnectionState { - self.state - .lock() - .as_ref() - .map(ConnectionState::from) - .unwrap_or(ConnectionState::Disconnected) - } - - pub fn is_disconnected(&self) -> bool { - self.connection_state() == ConnectionState::Disconnected - } - - pub fn path_style(&self) -> PathStyle { - self.path_style - } - - #[cfg(any(test, feature = "test-support"))] - pub fn simulate_disconnect(&self, client_cx: &mut App) -> Task<()> { - let opts = self.connection_options(); - client_cx.spawn(async move |cx| { - let connection = cx - .update_global(|c: &mut ConnectionPool, _| { - if let Some(ConnectionPoolEntry::Connecting(c)) = c.connections.get(&opts) { - c.clone() - } else { - panic!("missing test connection") - } - }) - .unwrap() - .await - .unwrap(); - - connection.simulate_disconnect(cx); - }) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn fake_server( - client_cx: &mut gpui::TestAppContext, - server_cx: &mut gpui::TestAppContext, - ) -> (SshConnectionOptions, AnyProtoClient) { - let port = client_cx - .update(|cx| cx.default_global::().connections.len() as u16 + 1); - let opts = SshConnectionOptions { - host: "".to_string(), - port: Some(port), - ..Default::default() - }; - let (outgoing_tx, _) = mpsc::unbounded::(); - let (_, incoming_rx) = mpsc::unbounded::(); - let server_client = - server_cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server")); - let connection: Arc = Arc::new(fake::FakeRemoteConnection { - connection_options: opts.clone(), - server_cx: fake::SendableCx::new(server_cx), - server_channel: server_client.clone(), - }); - - client_cx.update(|cx| { - cx.update_default_global(|c: &mut ConnectionPool, cx| { - c.connections.insert( - opts.clone(), - ConnectionPoolEntry::Connecting( - cx.background_spawn({ - let connection = connection.clone(); - async move { Ok(connection.clone()) } - }) - .shared(), - ), - ); - }) - }); - - (opts, server_client.into()) - } - - #[cfg(any(test, feature = "test-support"))] - pub async fn fake_client( - opts: SshConnectionOptions, - client_cx: &mut gpui::TestAppContext, - ) -> Entity { - let (_tx, rx) = oneshot::channel(); - client_cx - .update(|cx| { - Self::new( - ConnectionIdentifier::setup(), - opts, - rx, - Arc::new(fake::Delegate), - cx, - ) - }) - .await - .unwrap() - .unwrap() - } -} - -enum ConnectionPoolEntry { - Connecting(Shared, Arc>>>), - Connected(Weak), -} - -#[derive(Default)] -struct ConnectionPool { - connections: HashMap, -} - -impl Global for ConnectionPool {} - -impl ConnectionPool { - pub fn connect( - &mut self, - opts: SshConnectionOptions, - delegate: &Arc, - cx: &mut App, - ) -> Shared, Arc>>> { - let connection = self.connections.get(&opts); - match connection { - Some(ConnectionPoolEntry::Connecting(task)) => { - let delegate = delegate.clone(); - cx.spawn(async move |cx| { - delegate.set_status(Some("Waiting for existing connection attempt"), cx); - }) - .detach(); - return task.clone(); - } - Some(ConnectionPoolEntry::Connected(ssh)) => { - if let Some(ssh) = ssh.upgrade() - && !ssh.has_been_killed() - { - return Task::ready(Ok(ssh)).shared(); - } - self.connections.remove(&opts); - } - None => {} - } - - let task = cx - .spawn({ - let opts = opts.clone(); - let delegate = delegate.clone(); - async move |cx| { - let connection = SshRemoteConnection::new(opts.clone(), delegate, cx) - .await - .map(|connection| Arc::new(connection) as Arc); - - cx.update_global(|pool: &mut Self, _| { - debug_assert!(matches!( - pool.connections.get(&opts), - Some(ConnectionPoolEntry::Connecting(_)) - )); - match connection { - Ok(connection) => { - pool.connections.insert( - opts.clone(), - ConnectionPoolEntry::Connected(Arc::downgrade(&connection)), - ); - Ok(connection) - } - Err(error) => { - pool.connections.remove(&opts); - Err(Arc::new(error)) - } - } - })? - } - }) - .shared(); - - self.connections - .insert(opts.clone(), ConnectionPoolEntry::Connecting(task.clone())); - task - } -} - -impl From for AnyProtoClient { - fn from(client: SshRemoteClient) -> Self { - AnyProtoClient::new(client.client) - } -} - -#[async_trait(?Send)] -trait RemoteConnection: Send + Sync { - fn start_proxy( - &self, - unique_identifier: String, - reconnect: bool, - incoming_tx: UnboundedSender, - outgoing_rx: UnboundedReceiver, - connection_activity_tx: Sender<()>, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Task>; - fn upload_directory( - &self, - src_path: PathBuf, - dest_path: RemotePathBuf, - cx: &App, - ) -> Task>; - async fn kill(&self) -> Result<()>; - fn has_been_killed(&self) -> bool; - /// On Windows, we need to use `SSH_ASKPASS` to provide the password to ssh. - /// On Linux, we use the `ControlPath` option to create a socket file that ssh can use to - fn ssh_args(&self) -> SshArgs; - fn connection_options(&self) -> SshConnectionOptions; - fn path_style(&self) -> PathStyle; - fn shell(&self) -> String; - - #[cfg(any(test, feature = "test-support"))] - fn simulate_disconnect(&self, _: &AsyncApp) {} -} - -struct SshRemoteConnection { - socket: SshSocket, - master_process: Mutex>, - remote_binary_path: Option, - ssh_platform: SshPlatform, - ssh_path_style: PathStyle, - ssh_shell: String, - _temp_dir: TempDir, -} - -#[async_trait(?Send)] -impl RemoteConnection for SshRemoteConnection { - async fn kill(&self) -> Result<()> { - let Some(mut process) = self.master_process.lock().take() else { - return Ok(()); - }; - process.kill().ok(); - process.status().await?; - Ok(()) - } - - fn has_been_killed(&self) -> bool { - self.master_process.lock().is_none() - } - - fn ssh_args(&self) -> SshArgs { - self.socket.ssh_args() - } - - fn connection_options(&self) -> SshConnectionOptions { - self.socket.connection_options.clone() - } - - fn shell(&self) -> String { - self.ssh_shell.clone() - } - - fn upload_directory( - &self, - src_path: PathBuf, - dest_path: RemotePathBuf, - cx: &App, - ) -> Task> { - let mut command = util::command::new_smol_command("scp"); - let output = self - .socket - .ssh_options(&mut command) - .args( - self.socket - .connection_options - .port - .map(|port| vec!["-P".to_string(), port.to_string()]) - .unwrap_or_default(), - ) - .arg("-C") - .arg("-r") - .arg(&src_path) - .arg(format!( - "{}:{}", - self.socket.connection_options.scp_url(), - dest_path - )) - .output(); - - cx.background_spawn(async move { - let output = output.await?; - - anyhow::ensure!( - output.status.success(), - "failed to upload directory {} -> {}: {}", - src_path.display(), - dest_path.to_string(), - String::from_utf8_lossy(&output.stderr) - ); - - Ok(()) - }) - } - - fn start_proxy( - &self, - unique_identifier: String, - reconnect: bool, - incoming_tx: UnboundedSender, - outgoing_rx: UnboundedReceiver, - connection_activity_tx: Sender<()>, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Task> { - delegate.set_status(Some("Starting proxy"), cx); - - let Some(remote_binary_path) = self.remote_binary_path.clone() else { - return Task::ready(Err(anyhow!("Remote binary path not set"))); - }; - - let mut start_proxy_command = shell_script!( - "exec {binary_path} proxy --identifier {identifier}", - binary_path = &remote_binary_path.to_string(), - identifier = &unique_identifier, - ); - - for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { - if let Some(value) = std::env::var(env_var).ok() { - start_proxy_command = format!( - "{}={} {} ", - env_var, - shlex::try_quote(&value).unwrap(), - start_proxy_command, - ); - } - } - - if reconnect { - start_proxy_command.push_str(" --reconnect"); - } - - let ssh_proxy_process = match self - .socket - .ssh_command("sh", &["-lc", &start_proxy_command]) - // IMPORTANT: we kill this process when we drop the task that uses it. - .kill_on_drop(true) - .spawn() - { - Ok(process) => process, - Err(error) => { - return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error))); - } - }; - - Self::multiplex( - ssh_proxy_process, - incoming_tx, - outgoing_rx, - connection_activity_tx, - cx, - ) - } - - fn path_style(&self) -> PathStyle { - self.ssh_path_style - } -} - -impl SshRemoteConnection { - async fn new( - connection_options: SshConnectionOptions, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Result { - use askpass::AskPassResult; - - delegate.set_status(Some("Connecting"), cx); - - let url = connection_options.ssh_url(); - - let temp_dir = tempfile::Builder::new() - .prefix("zed-ssh-session") - .tempdir()?; - let askpass_delegate = askpass::AskPassDelegate::new(cx, { - let delegate = delegate.clone(); - move |prompt, tx, cx| delegate.ask_password(prompt, tx, cx) - }); - - let mut askpass = - askpass::AskPassSession::new(cx.background_executor(), askpass_delegate).await?; - - // Start the master SSH process, which does not do anything except for establish - // the connection and keep it open, allowing other ssh commands to reuse it - // via a control socket. - #[cfg(not(target_os = "windows"))] - let socket_path = temp_dir.path().join("ssh.sock"); - - let mut master_process = { - #[cfg(not(target_os = "windows"))] - let args = [ - "-N", - "-o", - "ControlPersist=no", - "-o", - "ControlMaster=yes", - "-o", - ]; - // On Windows, `ControlMaster` and `ControlPath` are not supported: - // https://github.com/PowerShell/Win32-OpenSSH/issues/405 - // https://github.com/PowerShell/Win32-OpenSSH/wiki/Project-Scope - #[cfg(target_os = "windows")] - let args = ["-N"]; - let mut master_process = util::command::new_smol_command("ssh"); - master_process - .kill_on_drop(true) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .env("SSH_ASKPASS_REQUIRE", "force") - .env("SSH_ASKPASS", askpass.script_path()) - .args(connection_options.additional_args()) - .args(args); - #[cfg(not(target_os = "windows"))] - master_process.arg(format!("ControlPath={}", socket_path.display())); - master_process.arg(&url).spawn()? - }; - // Wait for this ssh process to close its stdout, indicating that authentication - // has completed. - let mut stdout = master_process.stdout.take().unwrap(); - let mut output = Vec::new(); - - let result = select_biased! { - result = askpass.run().fuse() => { - match result { - AskPassResult::CancelledByUser => { - master_process.kill().ok(); - anyhow::bail!("SSH connection canceled") - } - AskPassResult::Timedout => { - anyhow::bail!("connecting to host timed out") - } - } - } - _ = stdout.read_to_end(&mut output).fuse() => { - anyhow::Ok(()) - } - }; - - if let Err(e) = result { - return Err(e.context("Failed to connect to host")); - } - - if master_process.try_status()?.is_some() { - output.clear(); - let mut stderr = master_process.stderr.take().unwrap(); - stderr.read_to_end(&mut output).await?; - - let error_message = format!( - "failed to connect: {}", - String::from_utf8_lossy(&output).trim() - ); - anyhow::bail!(error_message); - } - - #[cfg(not(target_os = "windows"))] - let socket = SshSocket::new(connection_options, socket_path)?; - #[cfg(target_os = "windows")] - let socket = SshSocket::new(connection_options, &temp_dir, askpass.get_password())?; - drop(askpass); - - let ssh_platform = socket.platform().await?; - let ssh_path_style = match ssh_platform.os { - "windows" => PathStyle::Windows, - _ => PathStyle::Posix, - }; - let ssh_shell = socket.shell().await; - - let mut this = Self { - socket, - master_process: Mutex::new(Some(master_process)), - _temp_dir: temp_dir, - remote_binary_path: None, - ssh_path_style, - ssh_platform, - ssh_shell, - }; - - let (release_channel, version, commit) = cx.update(|cx| { - ( - ReleaseChannel::global(cx), - AppVersion::global(cx), - AppCommitSha::try_global(cx), - ) - })?; - this.remote_binary_path = Some( - this.ensure_server_binary(&delegate, release_channel, version, commit, cx) - .await?, - ); - - Ok(this) - } - - fn multiplex( - mut ssh_proxy_process: Child, - incoming_tx: UnboundedSender, - mut outgoing_rx: UnboundedReceiver, - mut connection_activity_tx: Sender<()>, - cx: &AsyncApp, - ) -> Task> { - let mut child_stderr = ssh_proxy_process.stderr.take().unwrap(); - let mut child_stdout = ssh_proxy_process.stdout.take().unwrap(); - let mut child_stdin = ssh_proxy_process.stdin.take().unwrap(); - - let mut stdin_buffer = Vec::new(); - let mut stdout_buffer = Vec::new(); - let mut stderr_buffer = Vec::new(); - let mut stderr_offset = 0; - - let stdin_task = cx.background_spawn(async move { - while let Some(outgoing) = outgoing_rx.next().await { - write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?; - } - anyhow::Ok(()) - }); - - let stdout_task = cx.background_spawn({ - let mut connection_activity_tx = connection_activity_tx.clone(); - async move { - loop { - stdout_buffer.resize(MESSAGE_LEN_SIZE, 0); - let len = child_stdout.read(&mut stdout_buffer).await?; - - if len == 0 { - return anyhow::Ok(()); - } - - if len < MESSAGE_LEN_SIZE { - child_stdout.read_exact(&mut stdout_buffer[len..]).await?; - } - - let message_len = message_len_from_buffer(&stdout_buffer); - let envelope = - read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len) - .await?; - connection_activity_tx.try_send(()).ok(); - incoming_tx.unbounded_send(envelope).ok(); - } - } - }); - - let stderr_task: Task> = cx.background_spawn(async move { - loop { - stderr_buffer.resize(stderr_offset + 1024, 0); - - let len = child_stderr - .read(&mut stderr_buffer[stderr_offset..]) - .await?; - if len == 0 { - return anyhow::Ok(()); - } - - stderr_offset += len; - let mut start_ix = 0; - while let Some(ix) = stderr_buffer[start_ix..stderr_offset] - .iter() - .position(|b| b == &b'\n') - { - let line_ix = start_ix + ix; - let content = &stderr_buffer[start_ix..line_ix]; - start_ix = line_ix + 1; - if let Ok(record) = serde_json::from_slice::(content) { - record.log(log::logger()) - } else { - eprintln!("(remote) {}", String::from_utf8_lossy(content)); - } - } - stderr_buffer.drain(0..start_ix); - stderr_offset -= start_ix; - - connection_activity_tx.try_send(()).ok(); - } - }); - - cx.background_spawn(async move { - let result = futures::select! { - result = stdin_task.fuse() => { - result.context("stdin") - } - result = stdout_task.fuse() => { - result.context("stdout") - } - result = stderr_task.fuse() => { - result.context("stderr") - } - }; - - let status = ssh_proxy_process.status().await?.code().unwrap_or(1); - match result { - Ok(_) => Ok(status), - Err(error) => Err(error), - } - }) - } - - #[allow(unused)] - async fn ensure_server_binary( - &self, - delegate: &Arc, - release_channel: ReleaseChannel, - version: SemanticVersion, - commit: Option, - cx: &mut AsyncApp, - ) -> Result { - let version_str = match release_channel { - ReleaseChannel::Nightly => { - let commit = commit.map(|s| s.full()).unwrap_or_default(); - format!("{}-{}", version, commit) - } - ReleaseChannel::Dev => "build".to_string(), - _ => version.to_string(), - }; - let binary_name = format!( - "zed-remote-server-{}-{}", - release_channel.dev_name(), - version_str - ); - let dst_path = RemotePathBuf::new( - paths::remote_server_dir_relative().join(binary_name), - self.ssh_path_style, - ); - - let build_remote_server = std::env::var("ZED_BUILD_REMOTE_SERVER").ok(); - #[cfg(debug_assertions)] - if let Some(build_remote_server) = build_remote_server { - let src_path = self.build_local(build_remote_server, delegate, cx).await?; - let tmp_path = RemotePathBuf::new( - paths::remote_server_dir_relative().join(format!( - "download-{}-{}", - std::process::id(), - src_path.file_name().unwrap().to_string_lossy() - )), - self.ssh_path_style, - ); - self.upload_local_server_binary(&src_path, &tmp_path, delegate, cx) - .await?; - self.extract_server_binary(&dst_path, &tmp_path, delegate, cx) - .await?; - return Ok(dst_path); - } - - if self - .socket - .run_command(&dst_path.to_string(), &["version"]) - .await - .is_ok() - { - return Ok(dst_path); - } - - let wanted_version = cx.update(|cx| match release_channel { - ReleaseChannel::Nightly => Ok(None), - ReleaseChannel::Dev => { - anyhow::bail!( - "ZED_BUILD_REMOTE_SERVER is not set and no remote server exists at ({:?})", - dst_path - ) - } - _ => Ok(Some(AppVersion::global(cx))), - })??; - - let tmp_path_gz = RemotePathBuf::new( - PathBuf::from(format!("{}-download-{}.gz", dst_path, std::process::id())), - self.ssh_path_style, - ); - if !self.socket.connection_options.upload_binary_over_ssh - && let Some((url, body)) = delegate - .get_download_params(self.ssh_platform, release_channel, wanted_version, cx) - .await? - { - match self - .download_binary_on_server(&url, &body, &tmp_path_gz, delegate, cx) - .await - { - Ok(_) => { - self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) - .await?; - return Ok(dst_path); - } - Err(e) => { - log::error!( - "Failed to download binary on server, attempting to upload server: {}", - e - ) - } - } - } - - let src_path = delegate - .download_server_binary_locally(self.ssh_platform, release_channel, wanted_version, cx) - .await?; - self.upload_local_server_binary(&src_path, &tmp_path_gz, delegate, cx) - .await?; - self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) - .await?; - Ok(dst_path) - } - - async fn download_binary_on_server( - &self, - url: &str, - body: &str, - tmp_path_gz: &RemotePathBuf, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - if let Some(parent) = tmp_path_gz.parent() { - self.socket - .run_command( - "sh", - &[ - "-lc", - &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), - ], - ) - .await?; - } - - delegate.set_status(Some("Downloading remote development server on host"), cx); - - match self - .socket - .run_command( - "curl", - &[ - "-f", - "-L", - "-X", - "GET", - "-H", - "Content-Type: application/json", - "-d", - body, - url, - "-o", - &tmp_path_gz.to_string(), - ], - ) - .await - { - Ok(_) => {} - Err(e) => { - if self.socket.run_command("which", &["curl"]).await.is_ok() { - return Err(e); - } - - match self - .socket - .run_command( - "wget", - &[ - "--method=GET", - "--header=Content-Type: application/json", - "--body-data", - body, - url, - "-O", - &tmp_path_gz.to_string(), - ], - ) - .await - { - Ok(_) => {} - Err(e) => { - if self.socket.run_command("which", &["wget"]).await.is_ok() { - return Err(e); - } else { - anyhow::bail!("Neither curl nor wget is available"); - } - } - } - } - } - - Ok(()) - } - - async fn upload_local_server_binary( - &self, - src_path: &Path, - tmp_path_gz: &RemotePathBuf, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - if let Some(parent) = tmp_path_gz.parent() { - self.socket - .run_command( - "sh", - &[ - "-lc", - &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), - ], - ) - .await?; - } - - let src_stat = fs::metadata(&src_path).await?; - let size = src_stat.len(); - - let t0 = Instant::now(); - delegate.set_status(Some("Uploading remote development server"), cx); - log::info!( - "uploading remote development server to {:?} ({}kb)", - tmp_path_gz, - size / 1024 - ); - self.upload_file(src_path, tmp_path_gz) - .await - .context("failed to upload server binary")?; - log::info!("uploaded remote development server in {:?}", t0.elapsed()); - Ok(()) - } - - async fn extract_server_binary( - &self, - dst_path: &RemotePathBuf, - tmp_path: &RemotePathBuf, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - delegate.set_status(Some("Extracting remote development server"), cx); - let server_mode = 0o755; - - let orig_tmp_path = tmp_path.to_string(); - let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") { - shell_script!( - "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}", - server_mode = &format!("{:o}", server_mode), - dst_path = &dst_path.to_string(), - ) - } else { - shell_script!( - "chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}", - server_mode = &format!("{:o}", server_mode), - dst_path = &dst_path.to_string() - ) - }; - self.socket.run_command("sh", &["-lc", &script]).await?; - Ok(()) - } - - async fn upload_file(&self, src_path: &Path, dest_path: &RemotePathBuf) -> Result<()> { - log::debug!("uploading file {:?} to {:?}", src_path, dest_path); - let mut command = util::command::new_smol_command("scp"); - let output = self - .socket - .ssh_options(&mut command) - .args( - self.socket - .connection_options - .port - .map(|port| vec!["-P".to_string(), port.to_string()]) - .unwrap_or_default(), - ) - .arg(src_path) - .arg(format!( - "{}:{}", - self.socket.connection_options.scp_url(), - dest_path - )) - .output() - .await?; - - anyhow::ensure!( - output.status.success(), - "failed to upload file {} -> {}: {}", - src_path.display(), - dest_path.to_string(), - String::from_utf8_lossy(&output.stderr) - ); - Ok(()) - } - - #[cfg(debug_assertions)] - async fn build_local( - &self, - build_remote_server: String, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result { - use smol::process::{Command, Stdio}; - use std::env::VarError; - - async fn run_cmd(command: &mut Command) -> Result<()> { - let output = command - .kill_on_drop(true) - .stderr(Stdio::inherit()) - .output() - .await?; - anyhow::ensure!( - output.status.success(), - "Failed to run command: {command:?}" - ); - Ok(()) - } - - let use_musl = !build_remote_server.contains("nomusl"); - let triple = format!( - "{}-{}", - self.ssh_platform.arch, - match self.ssh_platform.os { - "linux" => - if use_musl { - "unknown-linux-musl" - } else { - "unknown-linux-gnu" - }, - "macos" => "apple-darwin", - _ => anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform), - } - ); - let mut rust_flags = match std::env::var("RUSTFLAGS") { - Ok(val) => val, - Err(VarError::NotPresent) => String::new(), - Err(e) => { - log::error!("Failed to get env var `RUSTFLAGS` value: {e}"); - String::new() - } - }; - if self.ssh_platform.os == "linux" && use_musl { - rust_flags.push_str(" -C target-feature=+crt-static"); - } - if build_remote_server.contains("mold") { - rust_flags.push_str(" -C link-arg=-fuse-ld=mold"); - } - - if self.ssh_platform.arch == std::env::consts::ARCH - && self.ssh_platform.os == std::env::consts::OS - { - delegate.set_status(Some("Building remote server binary from source"), cx); - log::info!("building remote server binary from source"); - run_cmd( - Command::new("cargo") - .args([ - "build", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - } else if build_remote_server.contains("cross") { - #[cfg(target_os = "windows")] - use util::paths::SanitizedPath; - - delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx); - log::info!("installing cross"); - run_cmd(Command::new("cargo").args([ - "install", - "cross", - "--git", - "https://github.com/cross-rs/cross", - ])) - .await?; - - delegate.set_status( - Some(&format!( - "Building remote server binary from source for {} with Docker", - &triple - )), - cx, - ); - log::info!("building remote server binary from source for {}", &triple); - - // On Windows, the binding needs to be set to the canonical path - #[cfg(target_os = "windows")] - let src = - SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string(); - #[cfg(not(target_os = "windows"))] - let src = "./target"; - run_cmd( - Command::new("cross") - .args([ - "build", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env( - "CROSS_CONTAINER_OPTS", - format!("--mount type=bind,src={src},dst=/app/target"), - ) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - } else { - let which = cx - .background_spawn(async move { which::which("zig") }) - .await; - - if which.is_err() { - #[cfg(not(target_os = "windows"))] - { - anyhow::bail!( - "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" - ) - } - #[cfg(target_os = "windows")] - { - anyhow::bail!( - "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" - ) - } - } - - delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); - log::info!("adding rustup target"); - run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?; - - delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx); - log::info!("installing cargo-zigbuild"); - run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?; - - delegate.set_status( - Some(&format!( - "Building remote binary from source for {triple} with Zig" - )), - cx, - ); - log::info!("building remote binary from source for {triple} with Zig"); - run_cmd( - Command::new("cargo") - .args([ - "zigbuild", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - }; - let bin_path = Path::new("target") - .join("remote_server") - .join(&triple) - .join("debug") - .join("remote_server"); - - let path = if !build_remote_server.contains("nocompress") { - delegate.set_status(Some("Compressing binary"), cx); - - #[cfg(not(target_os = "windows"))] - { - run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?; - } - #[cfg(target_os = "windows")] - { - // On Windows, we use 7z to compress the binary - let seven_zip = which::which("7z.exe").context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?; - let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple); - if smol::fs::metadata(&gz_path).await.is_ok() { - smol::fs::remove_file(&gz_path).await?; - } - run_cmd(Command::new(seven_zip).args([ - "a", - "-tgzip", - &gz_path, - &bin_path.to_string_lossy(), - ])) - .await?; - } - - let mut archive_path = bin_path; - archive_path.set_extension("gz"); - std::env::current_dir()?.join(archive_path) - } else { - bin_path - }; - - Ok(path) - } -} - -type ResponseChannels = Mutex)>>>; - -struct ChannelClient { - next_message_id: AtomicU32, - outgoing_tx: Mutex>, - buffer: Mutex>, - response_channels: ResponseChannels, - message_handlers: Mutex, - max_received: AtomicU32, - name: &'static str, - task: Mutex>>, -} - -impl ChannelClient { - fn new( - incoming_rx: mpsc::UnboundedReceiver, - outgoing_tx: mpsc::UnboundedSender, - cx: &App, - name: &'static str, - ) -> Arc { - Arc::new_cyclic(|this| Self { - outgoing_tx: Mutex::new(outgoing_tx), - next_message_id: AtomicU32::new(0), - max_received: AtomicU32::new(0), - response_channels: ResponseChannels::default(), - message_handlers: Default::default(), - buffer: Mutex::new(VecDeque::new()), - name, - task: Mutex::new(Self::start_handling_messages( - this.clone(), - incoming_rx, - &cx.to_async(), - )), - }) - } - - fn start_handling_messages( - this: Weak, - mut incoming_rx: mpsc::UnboundedReceiver, - cx: &AsyncApp, - ) -> Task> { - cx.spawn(async move |cx| { - let peer_id = PeerId { owner_id: 0, id: 0 }; - while let Some(incoming) = incoming_rx.next().await { - let Some(this) = this.upgrade() else { - return anyhow::Ok(()); - }; - if let Some(ack_id) = incoming.ack_id { - let mut buffer = this.buffer.lock(); - while buffer.front().is_some_and(|msg| msg.id <= ack_id) { - buffer.pop_front(); - } - } - if let Some(proto::envelope::Payload::FlushBufferedMessages(_)) = &incoming.payload - { - log::debug!( - "{}:ssh message received. name:FlushBufferedMessages", - this.name - ); - { - let buffer = this.buffer.lock(); - for envelope in buffer.iter() { - this.outgoing_tx - .lock() - .unbounded_send(envelope.clone()) - .ok(); - } - } - let mut envelope = proto::Ack {}.into_envelope(0, Some(incoming.id), None); - envelope.id = this.next_message_id.fetch_add(1, SeqCst); - this.outgoing_tx.lock().unbounded_send(envelope).ok(); - continue; - } - - this.max_received.store(incoming.id, SeqCst); - - if let Some(request_id) = incoming.responding_to { - let request_id = MessageId(request_id); - let sender = this.response_channels.lock().remove(&request_id); - if let Some(sender) = sender { - let (tx, rx) = oneshot::channel(); - if incoming.payload.is_some() { - sender.send((incoming, tx)).ok(); - } - rx.await.ok(); - } - } else if let Some(envelope) = - build_typed_envelope(peer_id, Instant::now(), incoming) - { - let type_name = envelope.payload_type_name(); - let message_id = envelope.message_id(); - if let Some(future) = ProtoMessageHandlerSet::handle_message( - &this.message_handlers, - envelope, - this.clone().into(), - cx.clone(), - ) { - log::debug!("{}:ssh message received. name:{type_name}", this.name); - cx.foreground_executor() - .spawn(async move { - match future.await { - Ok(_) => { - log::debug!( - "{}:ssh message handled. name:{type_name}", - this.name - ); - } - Err(error) => { - log::error!( - "{}:error handling message. type:{}, error:{}", - this.name, - type_name, - format!("{error:#}").lines().fold( - String::new(), - |mut message, line| { - if !message.is_empty() { - message.push(' '); - } - message.push_str(line); - message - } - ) - ); - } - } - }) - .detach() - } else { - log::error!("{}:unhandled ssh message name:{type_name}", this.name); - if let Err(e) = AnyProtoClient::from(this.clone()).send_response( - message_id, - anyhow::anyhow!("no handler registered for {type_name}").to_proto(), - ) { - log::error!( - "{}:error sending error response for {type_name}:{e:#}", - this.name - ); - } - } - } - } - anyhow::Ok(()) - }) - } - - fn reconnect( - self: &Arc, - incoming_rx: UnboundedReceiver, - outgoing_tx: UnboundedSender, - cx: &AsyncApp, - ) { - *self.outgoing_tx.lock() = outgoing_tx; - *self.task.lock() = Self::start_handling_messages(Arc::downgrade(self), incoming_rx, cx); - } - - fn request( - &self, - payload: T, - ) -> impl 'static + Future> { - self.request_internal(payload, true) - } - - fn request_internal( - &self, - payload: T, - use_buffer: bool, - ) -> impl 'static + Future> { - log::debug!("ssh request start. name:{}", T::NAME); - let response = - self.request_dynamic(payload.into_envelope(0, None, None), T::NAME, use_buffer); - async move { - let response = response.await?; - log::debug!("ssh request finish. name:{}", T::NAME); - T::Response::from_envelope(response).context("received a response of the wrong type") - } - } - - async fn resync(&self, timeout: Duration) -> Result<()> { - smol::future::or( - async { - self.request_internal(proto::FlushBufferedMessages {}, false) - .await?; - - for envelope in self.buffer.lock().iter() { - self.outgoing_tx - .lock() - .unbounded_send(envelope.clone()) - .ok(); - } - Ok(()) - }, - async { - smol::Timer::after(timeout).await; - anyhow::bail!("Timed out resyncing remote client") - }, - ) - .await - } - - async fn ping(&self, timeout: Duration) -> Result<()> { - smol::future::or( - async { - self.request(proto::Ping {}).await?; - Ok(()) - }, - async { - smol::Timer::after(timeout).await; - anyhow::bail!("Timed out pinging remote client") - }, - ) - .await - } - - pub fn send(&self, payload: T) -> Result<()> { - log::debug!("ssh send name:{}", T::NAME); - self.send_dynamic(payload.into_envelope(0, None, None)) - } - - fn request_dynamic( - &self, - mut envelope: proto::Envelope, - type_name: &'static str, - use_buffer: bool, - ) -> impl 'static + Future> { - envelope.id = self.next_message_id.fetch_add(1, SeqCst); - let (tx, rx) = oneshot::channel(); - let mut response_channels_lock = self.response_channels.lock(); - response_channels_lock.insert(MessageId(envelope.id), tx); - drop(response_channels_lock); - - let result = if use_buffer { - self.send_buffered(envelope) - } else { - self.send_unbuffered(envelope) - }; - async move { - if let Err(error) = &result { - log::error!("failed to send message: {error}"); - anyhow::bail!("failed to send message: {error}"); - } - - let response = rx.await.context("connection lost")?.0; - if let Some(proto::envelope::Payload::Error(error)) = &response.payload { - return Err(RpcError::from_proto(error, type_name)); - } - Ok(response) - } - } - - pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> { - envelope.id = self.next_message_id.fetch_add(1, SeqCst); - self.send_buffered(envelope) - } - - fn send_buffered(&self, mut envelope: proto::Envelope) -> Result<()> { - envelope.ack_id = Some(self.max_received.load(SeqCst)); - self.buffer.lock().push_back(envelope.clone()); - // ignore errors on send (happen while we're reconnecting) - // assume that the global "disconnected" overlay is sufficient. - self.outgoing_tx.lock().unbounded_send(envelope).ok(); - Ok(()) - } - - fn send_unbuffered(&self, mut envelope: proto::Envelope) -> Result<()> { - envelope.ack_id = Some(self.max_received.load(SeqCst)); - self.outgoing_tx.lock().unbounded_send(envelope).ok(); - Ok(()) - } -} - -impl ProtoClient for ChannelClient { - fn request( - &self, - envelope: proto::Envelope, - request_type: &'static str, - ) -> BoxFuture<'static, Result> { - self.request_dynamic(envelope, request_type, true).boxed() - } - - fn send(&self, envelope: proto::Envelope, _message_type: &'static str) -> Result<()> { - self.send_dynamic(envelope) - } - - fn send_response(&self, envelope: Envelope, _message_type: &'static str) -> anyhow::Result<()> { - self.send_dynamic(envelope) - } - - fn message_handler_set(&self) -> &Mutex { - &self.message_handlers - } - - fn is_via_collab(&self) -> bool { - false - } -} - -#[cfg(any(test, feature = "test-support"))] -mod fake { - use std::{path::PathBuf, sync::Arc}; - - use anyhow::Result; - use async_trait::async_trait; - use futures::{ - FutureExt, SinkExt, StreamExt, - channel::{ - mpsc::{self, Sender}, - oneshot, - }, - select_biased, - }; - use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task, TestAppContext}; - use release_channel::ReleaseChannel; - use rpc::proto::Envelope; - use util::paths::{PathStyle, RemotePathBuf}; - - use super::{ - ChannelClient, RemoteConnection, SshArgs, SshClientDelegate, SshConnectionOptions, - SshPlatform, - }; - - pub(super) struct FakeRemoteConnection { - pub(super) connection_options: SshConnectionOptions, - pub(super) server_channel: Arc, - pub(super) server_cx: SendableCx, - } - - pub(super) struct SendableCx(AsyncApp); - impl SendableCx { - // SAFETY: When run in test mode, GPUI is always single threaded. - pub(super) fn new(cx: &TestAppContext) -> Self { - Self(cx.to_async()) - } - - // SAFETY: Enforce that we're on the main thread by requiring a valid AsyncApp - fn get(&self, _: &AsyncApp) -> AsyncApp { - self.0.clone() - } - } - - // SAFETY: There is no way to access a SendableCx from a different thread, see [`SendableCx::new`] and [`SendableCx::get`] - unsafe impl Send for SendableCx {} - unsafe impl Sync for SendableCx {} - - #[async_trait(?Send)] - impl RemoteConnection for FakeRemoteConnection { - async fn kill(&self) -> Result<()> { - Ok(()) - } - - fn has_been_killed(&self) -> bool { - false - } - - fn ssh_args(&self) -> SshArgs { - SshArgs { - arguments: Vec::new(), - envs: None, - } - } - - fn upload_directory( - &self, - _src_path: PathBuf, - _dest_path: RemotePathBuf, - _cx: &App, - ) -> Task> { - unreachable!() - } - - fn connection_options(&self) -> SshConnectionOptions { - self.connection_options.clone() - } - - fn simulate_disconnect(&self, cx: &AsyncApp) { - let (outgoing_tx, _) = mpsc::unbounded::(); - let (_, incoming_rx) = mpsc::unbounded::(); - self.server_channel - .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx)); - } - - fn start_proxy( - &self, - _unique_identifier: String, - _reconnect: bool, - mut client_incoming_tx: mpsc::UnboundedSender, - mut client_outgoing_rx: mpsc::UnboundedReceiver, - mut connection_activity_tx: Sender<()>, - _delegate: Arc, - cx: &mut AsyncApp, - ) -> Task> { - let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::(); - let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::(); - - self.server_channel.reconnect( - server_incoming_rx, - server_outgoing_tx, - &self.server_cx.get(cx), - ); - - cx.background_spawn(async move { - loop { - select_biased! { - server_to_client = server_outgoing_rx.next().fuse() => { - let Some(server_to_client) = server_to_client else { - return Ok(1) - }; - connection_activity_tx.try_send(()).ok(); - client_incoming_tx.send(server_to_client).await.ok(); - } - client_to_server = client_outgoing_rx.next().fuse() => { - let Some(client_to_server) = client_to_server else { - return Ok(1) - }; - server_incoming_tx.send(client_to_server).await.ok(); - } - } - } - }) - } - - fn path_style(&self) -> PathStyle { - PathStyle::current() - } - - fn shell(&self) -> String { - "sh".to_owned() - } - } - - pub(super) struct Delegate; - - impl SshClientDelegate for Delegate { - fn ask_password(&self, _: String, _: oneshot::Sender, _: &mut AsyncApp) { - unreachable!() - } - - fn download_server_binary_locally( - &self, - _: SshPlatform, - _: ReleaseChannel, - _: Option, - _: &mut AsyncApp, - ) -> Task> { - unreachable!() - } - - fn get_download_params( - &self, - _platform: SshPlatform, - _release_channel: ReleaseChannel, - _version: Option, - _cx: &mut AsyncApp, - ) -> Task>> { - unreachable!() - } - - fn set_status(&self, _: Option<&str>, _: &mut AsyncApp) {} - } -} diff --git a/crates/remote/src/transport.rs b/crates/remote/src/transport.rs new file mode 100644 index 0000000000000000000000000000000000000000..aa086fd3f56196e71224ef346c9810e8638c5c47 --- /dev/null +++ b/crates/remote/src/transport.rs @@ -0,0 +1 @@ +pub mod ssh; diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs new file mode 100644 index 0000000000000000000000000000000000000000..193b96497375e5dee19aacaff114ba79d78a7191 --- /dev/null +++ b/crates/remote/src/transport/ssh.rs @@ -0,0 +1,1358 @@ +use crate::{ + RemoteClientDelegate, RemotePlatform, + json_log::LogRecord, + protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message}, + remote_client::{CommandTemplate, RemoteConnection}, +}; +use anyhow::{Context as _, Result, anyhow}; +use async_trait::async_trait; +use collections::HashMap; +use futures::{ + AsyncReadExt as _, FutureExt as _, StreamExt as _, + channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}, + select_biased, +}; +use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task}; +use itertools::Itertools; +use parking_lot::Mutex; +use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; +use rpc::proto::Envelope; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use smol::{ + fs, + process::{self, Child, Stdio}, +}; +use std::{ + iter, + path::{Path, PathBuf}, + sync::Arc, + time::Instant, +}; +use tempfile::TempDir; +use util::paths::{PathStyle, RemotePathBuf}; + +pub(crate) struct SshRemoteConnection { + socket: SshSocket, + master_process: Mutex>, + remote_binary_path: Option, + ssh_platform: RemotePlatform, + ssh_path_style: PathStyle, + ssh_shell: String, + _temp_dir: TempDir, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +pub struct SshConnectionOptions { + pub host: String, + pub username: Option, + pub port: Option, + pub password: Option, + pub args: Option>, + pub port_forwards: Option>, + + pub nickname: Option, + pub upload_binary_over_ssh: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)] +pub struct SshPortForwardOption { + #[serde(skip_serializing_if = "Option::is_none")] + pub local_host: Option, + pub local_port: u16, + #[serde(skip_serializing_if = "Option::is_none")] + pub remote_host: Option, + pub remote_port: u16, +} + +#[derive(Clone)] +struct SshSocket { + connection_options: SshConnectionOptions, + #[cfg(not(target_os = "windows"))] + socket_path: PathBuf, + envs: HashMap, +} + +macro_rules! shell_script { + ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{ + format!( + $fmt, + $( + $name = shlex::try_quote($arg).unwrap() + ),+ + ) + }}; +} + +#[async_trait(?Send)] +impl RemoteConnection for SshRemoteConnection { + async fn kill(&self) -> Result<()> { + let Some(mut process) = self.master_process.lock().take() else { + return Ok(()); + }; + process.kill().ok(); + process.status().await?; + Ok(()) + } + + fn has_been_killed(&self) -> bool { + self.master_process.lock().is_none() + } + + fn connection_options(&self) -> SshConnectionOptions { + self.socket.connection_options.clone() + } + + fn shell(&self) -> String { + self.ssh_shell.clone() + } + + fn build_command( + &self, + input_program: Option, + input_args: &[String], + input_env: &HashMap, + working_dir: Option, + port_forward: Option<(u16, String, u16)>, + ) -> Result { + use std::fmt::Write as _; + + let mut script = String::new(); + if let Some(working_dir) = working_dir { + let working_dir = + RemotePathBuf::new(working_dir.into(), self.ssh_path_style).to_string(); + + // shlex will wrap the command in single quotes (''), disabling ~ expansion, + // replace ith with something that works + const TILDE_PREFIX: &'static str = "~/"; + if working_dir.starts_with(TILDE_PREFIX) { + let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/"); + write!(&mut script, "cd \"$HOME/{working_dir}\"; ").unwrap(); + } else { + write!(&mut script, "cd \"{working_dir}\"; ").unwrap(); + } + } else { + write!(&mut script, "cd; ").unwrap(); + }; + + for (k, v) in input_env.iter() { + if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) { + write!(&mut script, "{}={} ", k, v).unwrap(); + } + } + + let shell = &self.ssh_shell; + + if let Some(input_program) = input_program { + let command = shlex::try_quote(&input_program)?; + script.push_str(&command); + for arg in input_args { + let arg = shlex::try_quote(&arg)?; + script.push_str(" "); + script.push_str(&arg); + } + } else { + write!(&mut script, "exec {shell} -l").unwrap(); + }; + + let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&script).unwrap()); + + let mut args = Vec::new(); + args.extend(self.socket.ssh_args()); + + if let Some((local_port, host, remote_port)) = port_forward { + args.push("-L".into()); + args.push(format!("{local_port}:{host}:{remote_port}")); + } + + args.push("-t".into()); + args.push(shell_invocation); + + Ok(CommandTemplate { + program: "ssh".into(), + args, + env: self.socket.envs.clone(), + }) + } + + fn upload_directory( + &self, + src_path: PathBuf, + dest_path: RemotePathBuf, + cx: &App, + ) -> Task> { + let mut command = util::command::new_smol_command("scp"); + let output = self + .socket + .ssh_options(&mut command) + .args( + self.socket + .connection_options + .port + .map(|port| vec!["-P".to_string(), port.to_string()]) + .unwrap_or_default(), + ) + .arg("-C") + .arg("-r") + .arg(&src_path) + .arg(format!( + "{}:{}", + self.socket.connection_options.scp_url(), + dest_path + )) + .output(); + + cx.background_spawn(async move { + let output = output.await?; + + anyhow::ensure!( + output.status.success(), + "failed to upload directory {} -> {}: {}", + src_path.display(), + dest_path.to_string(), + String::from_utf8_lossy(&output.stderr) + ); + + Ok(()) + }) + } + + fn start_proxy( + &self, + unique_identifier: String, + reconnect: bool, + incoming_tx: UnboundedSender, + outgoing_rx: UnboundedReceiver, + connection_activity_tx: Sender<()>, + delegate: Arc, + cx: &mut AsyncApp, + ) -> Task> { + delegate.set_status(Some("Starting proxy"), cx); + + let Some(remote_binary_path) = self.remote_binary_path.clone() else { + return Task::ready(Err(anyhow!("Remote binary path not set"))); + }; + + let mut start_proxy_command = shell_script!( + "exec {binary_path} proxy --identifier {identifier}", + binary_path = &remote_binary_path.to_string(), + identifier = &unique_identifier, + ); + + for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { + if let Some(value) = std::env::var(env_var).ok() { + start_proxy_command = format!( + "{}={} {} ", + env_var, + shlex::try_quote(&value).unwrap(), + start_proxy_command, + ); + } + } + + if reconnect { + start_proxy_command.push_str(" --reconnect"); + } + + let ssh_proxy_process = match self + .socket + .ssh_command("sh", &["-lc", &start_proxy_command]) + // IMPORTANT: we kill this process when we drop the task that uses it. + .kill_on_drop(true) + .spawn() + { + Ok(process) => process, + Err(error) => { + return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error))); + } + }; + + Self::multiplex( + ssh_proxy_process, + incoming_tx, + outgoing_rx, + connection_activity_tx, + cx, + ) + } + + fn path_style(&self) -> PathStyle { + self.ssh_path_style + } +} + +impl SshRemoteConnection { + pub(crate) async fn new( + connection_options: SshConnectionOptions, + delegate: Arc, + cx: &mut AsyncApp, + ) -> Result { + use askpass::AskPassResult; + + delegate.set_status(Some("Connecting"), cx); + + let url = connection_options.ssh_url(); + + let temp_dir = tempfile::Builder::new() + .prefix("zed-ssh-session") + .tempdir()?; + let askpass_delegate = askpass::AskPassDelegate::new(cx, { + let delegate = delegate.clone(); + move |prompt, tx, cx| delegate.ask_password(prompt, tx, cx) + }); + + let mut askpass = + askpass::AskPassSession::new(cx.background_executor(), askpass_delegate).await?; + + // Start the master SSH process, which does not do anything except for establish + // the connection and keep it open, allowing other ssh commands to reuse it + // via a control socket. + #[cfg(not(target_os = "windows"))] + let socket_path = temp_dir.path().join("ssh.sock"); + + let mut master_process = { + #[cfg(not(target_os = "windows"))] + let args = [ + "-N", + "-o", + "ControlPersist=no", + "-o", + "ControlMaster=yes", + "-o", + ]; + // On Windows, `ControlMaster` and `ControlPath` are not supported: + // https://github.com/PowerShell/Win32-OpenSSH/issues/405 + // https://github.com/PowerShell/Win32-OpenSSH/wiki/Project-Scope + #[cfg(target_os = "windows")] + let args = ["-N"]; + let mut master_process = util::command::new_smol_command("ssh"); + master_process + .kill_on_drop(true) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .env("SSH_ASKPASS_REQUIRE", "force") + .env("SSH_ASKPASS", askpass.script_path()) + .args(connection_options.additional_args()) + .args(args); + #[cfg(not(target_os = "windows"))] + master_process.arg(format!("ControlPath={}", socket_path.display())); + master_process.arg(&url).spawn()? + }; + // Wait for this ssh process to close its stdout, indicating that authentication + // has completed. + let mut stdout = master_process.stdout.take().unwrap(); + let mut output = Vec::new(); + + let result = select_biased! { + result = askpass.run().fuse() => { + match result { + AskPassResult::CancelledByUser => { + master_process.kill().ok(); + anyhow::bail!("SSH connection canceled") + } + AskPassResult::Timedout => { + anyhow::bail!("connecting to host timed out") + } + } + } + _ = stdout.read_to_end(&mut output).fuse() => { + anyhow::Ok(()) + } + }; + + if let Err(e) = result { + return Err(e.context("Failed to connect to host")); + } + + if master_process.try_status()?.is_some() { + output.clear(); + let mut stderr = master_process.stderr.take().unwrap(); + stderr.read_to_end(&mut output).await?; + + let error_message = format!( + "failed to connect: {}", + String::from_utf8_lossy(&output).trim() + ); + anyhow::bail!(error_message); + } + + #[cfg(not(target_os = "windows"))] + let socket = SshSocket::new(connection_options, socket_path)?; + #[cfg(target_os = "windows")] + let socket = SshSocket::new(connection_options, &temp_dir, askpass.get_password())?; + drop(askpass); + + let ssh_platform = socket.platform().await?; + let ssh_path_style = match ssh_platform.os { + "windows" => PathStyle::Windows, + _ => PathStyle::Posix, + }; + let ssh_shell = socket.shell().await; + + let mut this = Self { + socket, + master_process: Mutex::new(Some(master_process)), + _temp_dir: temp_dir, + remote_binary_path: None, + ssh_path_style, + ssh_platform, + ssh_shell, + }; + + let (release_channel, version, commit) = cx.update(|cx| { + ( + ReleaseChannel::global(cx), + AppVersion::global(cx), + AppCommitSha::try_global(cx), + ) + })?; + this.remote_binary_path = Some( + this.ensure_server_binary(&delegate, release_channel, version, commit, cx) + .await?, + ); + + Ok(this) + } + + fn multiplex( + mut ssh_proxy_process: Child, + incoming_tx: UnboundedSender, + mut outgoing_rx: UnboundedReceiver, + mut connection_activity_tx: Sender<()>, + cx: &AsyncApp, + ) -> Task> { + let mut child_stderr = ssh_proxy_process.stderr.take().unwrap(); + let mut child_stdout = ssh_proxy_process.stdout.take().unwrap(); + let mut child_stdin = ssh_proxy_process.stdin.take().unwrap(); + + let mut stdin_buffer = Vec::new(); + let mut stdout_buffer = Vec::new(); + let mut stderr_buffer = Vec::new(); + let mut stderr_offset = 0; + + let stdin_task = cx.background_spawn(async move { + while let Some(outgoing) = outgoing_rx.next().await { + write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?; + } + anyhow::Ok(()) + }); + + let stdout_task = cx.background_spawn({ + let mut connection_activity_tx = connection_activity_tx.clone(); + async move { + loop { + stdout_buffer.resize(MESSAGE_LEN_SIZE, 0); + let len = child_stdout.read(&mut stdout_buffer).await?; + + if len == 0 { + return anyhow::Ok(()); + } + + if len < MESSAGE_LEN_SIZE { + child_stdout.read_exact(&mut stdout_buffer[len..]).await?; + } + + let message_len = message_len_from_buffer(&stdout_buffer); + let envelope = + read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len) + .await?; + connection_activity_tx.try_send(()).ok(); + incoming_tx.unbounded_send(envelope).ok(); + } + } + }); + + let stderr_task: Task> = cx.background_spawn(async move { + loop { + stderr_buffer.resize(stderr_offset + 1024, 0); + + let len = child_stderr + .read(&mut stderr_buffer[stderr_offset..]) + .await?; + if len == 0 { + return anyhow::Ok(()); + } + + stderr_offset += len; + let mut start_ix = 0; + while let Some(ix) = stderr_buffer[start_ix..stderr_offset] + .iter() + .position(|b| b == &b'\n') + { + let line_ix = start_ix + ix; + let content = &stderr_buffer[start_ix..line_ix]; + start_ix = line_ix + 1; + if let Ok(record) = serde_json::from_slice::(content) { + record.log(log::logger()) + } else { + eprintln!("(remote) {}", String::from_utf8_lossy(content)); + } + } + stderr_buffer.drain(0..start_ix); + stderr_offset -= start_ix; + + connection_activity_tx.try_send(()).ok(); + } + }); + + cx.background_spawn(async move { + let result = futures::select! { + result = stdin_task.fuse() => { + result.context("stdin") + } + result = stdout_task.fuse() => { + result.context("stdout") + } + result = stderr_task.fuse() => { + result.context("stderr") + } + }; + + let status = ssh_proxy_process.status().await?.code().unwrap_or(1); + match result { + Ok(_) => Ok(status), + Err(error) => Err(error), + } + }) + } + + #[allow(unused)] + async fn ensure_server_binary( + &self, + delegate: &Arc, + release_channel: ReleaseChannel, + version: SemanticVersion, + commit: Option, + cx: &mut AsyncApp, + ) -> Result { + let version_str = match release_channel { + ReleaseChannel::Nightly => { + let commit = commit.map(|s| s.full()).unwrap_or_default(); + format!("{}-{}", version, commit) + } + ReleaseChannel::Dev => "build".to_string(), + _ => version.to_string(), + }; + let binary_name = format!( + "zed-remote-server-{}-{}", + release_channel.dev_name(), + version_str + ); + let dst_path = RemotePathBuf::new( + paths::remote_server_dir_relative().join(binary_name), + self.ssh_path_style, + ); + + let build_remote_server = std::env::var("ZED_BUILD_REMOTE_SERVER").ok(); + #[cfg(debug_assertions)] + if let Some(build_remote_server) = build_remote_server { + let src_path = self.build_local(build_remote_server, delegate, cx).await?; + let tmp_path = RemotePathBuf::new( + paths::remote_server_dir_relative().join(format!( + "download-{}-{}", + std::process::id(), + src_path.file_name().unwrap().to_string_lossy() + )), + self.ssh_path_style, + ); + self.upload_local_server_binary(&src_path, &tmp_path, delegate, cx) + .await?; + self.extract_server_binary(&dst_path, &tmp_path, delegate, cx) + .await?; + return Ok(dst_path); + } + + if self + .socket + .run_command(&dst_path.to_string(), &["version"]) + .await + .is_ok() + { + return Ok(dst_path); + } + + let wanted_version = cx.update(|cx| match release_channel { + ReleaseChannel::Nightly => Ok(None), + ReleaseChannel::Dev => { + anyhow::bail!( + "ZED_BUILD_REMOTE_SERVER is not set and no remote server exists at ({:?})", + dst_path + ) + } + _ => Ok(Some(AppVersion::global(cx))), + })??; + + let tmp_path_gz = RemotePathBuf::new( + PathBuf::from(format!("{}-download-{}.gz", dst_path, std::process::id())), + self.ssh_path_style, + ); + if !self.socket.connection_options.upload_binary_over_ssh + && let Some((url, body)) = delegate + .get_download_params(self.ssh_platform, release_channel, wanted_version, cx) + .await? + { + match self + .download_binary_on_server(&url, &body, &tmp_path_gz, delegate, cx) + .await + { + Ok(_) => { + self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) + .await?; + return Ok(dst_path); + } + Err(e) => { + log::error!( + "Failed to download binary on server, attempting to upload server: {}", + e + ) + } + } + } + + let src_path = delegate + .download_server_binary_locally(self.ssh_platform, release_channel, wanted_version, cx) + .await?; + self.upload_local_server_binary(&src_path, &tmp_path_gz, delegate, cx) + .await?; + self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) + .await?; + Ok(dst_path) + } + + async fn download_binary_on_server( + &self, + url: &str, + body: &str, + tmp_path_gz: &RemotePathBuf, + delegate: &Arc, + cx: &mut AsyncApp, + ) -> Result<()> { + if let Some(parent) = tmp_path_gz.parent() { + self.socket + .run_command( + "sh", + &[ + "-lc", + &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), + ], + ) + .await?; + } + + delegate.set_status(Some("Downloading remote development server on host"), cx); + + match self + .socket + .run_command( + "curl", + &[ + "-f", + "-L", + "-X", + "GET", + "-H", + "Content-Type: application/json", + "-d", + body, + url, + "-o", + &tmp_path_gz.to_string(), + ], + ) + .await + { + Ok(_) => {} + Err(e) => { + if self.socket.run_command("which", &["curl"]).await.is_ok() { + return Err(e); + } + + match self + .socket + .run_command( + "wget", + &[ + "--method=GET", + "--header=Content-Type: application/json", + "--body-data", + body, + url, + "-O", + &tmp_path_gz.to_string(), + ], + ) + .await + { + Ok(_) => {} + Err(e) => { + if self.socket.run_command("which", &["wget"]).await.is_ok() { + return Err(e); + } else { + anyhow::bail!("Neither curl nor wget is available"); + } + } + } + } + } + + Ok(()) + } + + async fn upload_local_server_binary( + &self, + src_path: &Path, + tmp_path_gz: &RemotePathBuf, + delegate: &Arc, + cx: &mut AsyncApp, + ) -> Result<()> { + if let Some(parent) = tmp_path_gz.parent() { + self.socket + .run_command( + "sh", + &[ + "-lc", + &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), + ], + ) + .await?; + } + + let src_stat = fs::metadata(&src_path).await?; + let size = src_stat.len(); + + let t0 = Instant::now(); + delegate.set_status(Some("Uploading remote development server"), cx); + log::info!( + "uploading remote development server to {:?} ({}kb)", + tmp_path_gz, + size / 1024 + ); + self.upload_file(src_path, tmp_path_gz) + .await + .context("failed to upload server binary")?; + log::info!("uploaded remote development server in {:?}", t0.elapsed()); + Ok(()) + } + + async fn extract_server_binary( + &self, + dst_path: &RemotePathBuf, + tmp_path: &RemotePathBuf, + delegate: &Arc, + cx: &mut AsyncApp, + ) -> Result<()> { + delegate.set_status(Some("Extracting remote development server"), cx); + let server_mode = 0o755; + + let orig_tmp_path = tmp_path.to_string(); + let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") { + shell_script!( + "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}", + server_mode = &format!("{:o}", server_mode), + dst_path = &dst_path.to_string(), + ) + } else { + shell_script!( + "chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}", + server_mode = &format!("{:o}", server_mode), + dst_path = &dst_path.to_string() + ) + }; + self.socket.run_command("sh", &["-lc", &script]).await?; + Ok(()) + } + + async fn upload_file(&self, src_path: &Path, dest_path: &RemotePathBuf) -> Result<()> { + log::debug!("uploading file {:?} to {:?}", src_path, dest_path); + let mut command = util::command::new_smol_command("scp"); + let output = self + .socket + .ssh_options(&mut command) + .args( + self.socket + .connection_options + .port + .map(|port| vec!["-P".to_string(), port.to_string()]) + .unwrap_or_default(), + ) + .arg(src_path) + .arg(format!( + "{}:{}", + self.socket.connection_options.scp_url(), + dest_path + )) + .output() + .await?; + + anyhow::ensure!( + output.status.success(), + "failed to upload file {} -> {}: {}", + src_path.display(), + dest_path.to_string(), + String::from_utf8_lossy(&output.stderr) + ); + Ok(()) + } + + #[cfg(debug_assertions)] + async fn build_local( + &self, + build_remote_server: String, + delegate: &Arc, + cx: &mut AsyncApp, + ) -> Result { + use smol::process::{Command, Stdio}; + use std::env::VarError; + + async fn run_cmd(command: &mut Command) -> Result<()> { + let output = command + .kill_on_drop(true) + .stderr(Stdio::inherit()) + .output() + .await?; + anyhow::ensure!( + output.status.success(), + "Failed to run command: {command:?}" + ); + Ok(()) + } + + let use_musl = !build_remote_server.contains("nomusl"); + let triple = format!( + "{}-{}", + self.ssh_platform.arch, + match self.ssh_platform.os { + "linux" => + if use_musl { + "unknown-linux-musl" + } else { + "unknown-linux-gnu" + }, + "macos" => "apple-darwin", + _ => anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform), + } + ); + let mut rust_flags = match std::env::var("RUSTFLAGS") { + Ok(val) => val, + Err(VarError::NotPresent) => String::new(), + Err(e) => { + log::error!("Failed to get env var `RUSTFLAGS` value: {e}"); + String::new() + } + }; + if self.ssh_platform.os == "linux" && use_musl { + rust_flags.push_str(" -C target-feature=+crt-static"); + } + if build_remote_server.contains("mold") { + rust_flags.push_str(" -C link-arg=-fuse-ld=mold"); + } + + if self.ssh_platform.arch == std::env::consts::ARCH + && self.ssh_platform.os == std::env::consts::OS + { + delegate.set_status(Some("Building remote server binary from source"), cx); + log::info!("building remote server binary from source"); + run_cmd( + Command::new("cargo") + .args([ + "build", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; + } else if build_remote_server.contains("cross") { + #[cfg(target_os = "windows")] + use util::paths::SanitizedPath; + + delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx); + log::info!("installing cross"); + run_cmd(Command::new("cargo").args([ + "install", + "cross", + "--git", + "https://github.com/cross-rs/cross", + ])) + .await?; + + delegate.set_status( + Some(&format!( + "Building remote server binary from source for {} with Docker", + &triple + )), + cx, + ); + log::info!("building remote server binary from source for {}", &triple); + + // On Windows, the binding needs to be set to the canonical path + #[cfg(target_os = "windows")] + let src = + SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string(); + #[cfg(not(target_os = "windows"))] + let src = "./target"; + run_cmd( + Command::new("cross") + .args([ + "build", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env( + "CROSS_CONTAINER_OPTS", + format!("--mount type=bind,src={src},dst=/app/target"), + ) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; + } else { + let which = cx + .background_spawn(async move { which::which("zig") }) + .await; + + if which.is_err() { + #[cfg(not(target_os = "windows"))] + { + anyhow::bail!( + "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" + ) + } + #[cfg(target_os = "windows")] + { + anyhow::bail!( + "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" + ) + } + } + + delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); + log::info!("adding rustup target"); + run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?; + + delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx); + log::info!("installing cargo-zigbuild"); + run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?; + + delegate.set_status( + Some(&format!( + "Building remote binary from source for {triple} with Zig" + )), + cx, + ); + log::info!("building remote binary from source for {triple} with Zig"); + run_cmd( + Command::new("cargo") + .args([ + "zigbuild", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; + }; + let bin_path = Path::new("target") + .join("remote_server") + .join(&triple) + .join("debug") + .join("remote_server"); + + let path = if !build_remote_server.contains("nocompress") { + delegate.set_status(Some("Compressing binary"), cx); + + #[cfg(not(target_os = "windows"))] + { + run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?; + } + #[cfg(target_os = "windows")] + { + // On Windows, we use 7z to compress the binary + let seven_zip = which::which("7z.exe").context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?; + let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple); + if smol::fs::metadata(&gz_path).await.is_ok() { + smol::fs::remove_file(&gz_path).await?; + } + run_cmd(Command::new(seven_zip).args([ + "a", + "-tgzip", + &gz_path, + &bin_path.to_string_lossy(), + ])) + .await?; + } + + let mut archive_path = bin_path; + archive_path.set_extension("gz"); + std::env::current_dir()?.join(archive_path) + } else { + bin_path + }; + + Ok(path) + } +} + +impl SshSocket { + #[cfg(not(target_os = "windows"))] + fn new(options: SshConnectionOptions, socket_path: PathBuf) -> Result { + Ok(Self { + connection_options: options, + envs: HashMap::default(), + socket_path, + }) + } + + #[cfg(target_os = "windows")] + fn new(options: SshConnectionOptions, temp_dir: &TempDir, secret: String) -> Result { + let askpass_script = temp_dir.path().join("askpass.bat"); + std::fs::write(&askpass_script, "@ECHO OFF\necho %ZED_SSH_ASKPASS%")?; + let mut envs = HashMap::default(); + envs.insert("SSH_ASKPASS_REQUIRE".into(), "force".into()); + envs.insert("SSH_ASKPASS".into(), askpass_script.display().to_string()); + envs.insert("ZED_SSH_ASKPASS".into(), secret); + Ok(Self { + connection_options: options, + envs, + }) + } + + // :WARNING: ssh unquotes arguments when executing on the remote :WARNING: + // e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l + // and passes -l as an argument to sh, not to ls. + // Furthermore, some setups (e.g. Coder) will change directory when SSH'ing + // into a machine. You must use `cd` to get back to $HOME. + // You need to do it like this: $ ssh host "cd; sh -c 'ls -l /tmp'" + fn ssh_command(&self, program: &str, args: &[&str]) -> process::Command { + let mut command = util::command::new_smol_command("ssh"); + let to_run = iter::once(&program) + .chain(args.iter()) + .map(|token| { + // We're trying to work with: sh, bash, zsh, fish, tcsh, ...? + debug_assert!( + !token.contains('\n'), + "multiline arguments do not work in all shells" + ); + shlex::try_quote(token).unwrap() + }) + .join(" "); + let to_run = format!("cd; {to_run}"); + log::debug!("ssh {} {:?}", self.connection_options.ssh_url(), to_run); + self.ssh_options(&mut command) + .arg(self.connection_options.ssh_url()) + .arg(to_run); + command + } + + async fn run_command(&self, program: &str, args: &[&str]) -> Result { + let output = self.ssh_command(program, args).output().await?; + anyhow::ensure!( + output.status.success(), + "failed to run command: {}", + String::from_utf8_lossy(&output.stderr) + ); + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } + + #[cfg(not(target_os = "windows"))] + fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command { + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .args(self.connection_options.additional_args()) + .args(["-o", "ControlMaster=no", "-o"]) + .arg(format!("ControlPath={}", self.socket_path.display())) + } + + #[cfg(target_os = "windows")] + fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command { + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .args(self.connection_options.additional_args()) + .envs(self.envs.clone()) + } + + // On Windows, we need to use `SSH_ASKPASS` to provide the password to ssh. + // On Linux, we use the `ControlPath` option to create a socket file that ssh can use to + #[cfg(not(target_os = "windows"))] + fn ssh_args(&self) -> Vec { + let mut arguments = self.connection_options.additional_args(); + arguments.extend(vec![ + "-o".to_string(), + "ControlMaster=no".to_string(), + "-o".to_string(), + format!("ControlPath={}", self.socket_path.display()), + self.connection_options.ssh_url(), + ]); + arguments + } + + #[cfg(target_os = "windows")] + fn ssh_args(&self) -> Vec { + let mut arguments = self.connection_options.additional_args(); + arguments.push(self.connection_options.ssh_url()); + arguments + } + + async fn platform(&self) -> Result { + let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?; + let Some((os, arch)) = uname.split_once(" ") else { + anyhow::bail!("unknown uname: {uname:?}") + }; + + let os = match os.trim() { + "Darwin" => "macos", + "Linux" => "linux", + _ => anyhow::bail!( + "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development" + ), + }; + // exclude armv5,6,7 as they are 32-bit. + let arch = if arch.starts_with("armv8") + || arch.starts_with("armv9") + || arch.starts_with("arm64") + || arch.starts_with("aarch64") + { + "aarch64" + } else if arch.starts_with("x86") { + "x86_64" + } else { + anyhow::bail!( + "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development" + ) + }; + + Ok(RemotePlatform { os, arch }) + } + + async fn shell(&self) -> String { + match self.run_command("sh", &["-lc", "echo $SHELL"]).await { + Ok(shell) => shell.trim().to_owned(), + Err(e) => { + log::error!("Failed to get shell: {e}"); + "sh".to_owned() + } + } + } +} + +fn parse_port_number(port_str: &str) -> Result { + port_str + .parse() + .with_context(|| format!("parsing port number: {port_str}")) +} + +fn parse_port_forward_spec(spec: &str) -> Result { + let parts: Vec<&str> = spec.split(':').collect(); + + match parts.len() { + 4 => { + let local_port = parse_port_number(parts[1])?; + let remote_port = parse_port_number(parts[3])?; + + Ok(SshPortForwardOption { + local_host: Some(parts[0].to_string()), + local_port, + remote_host: Some(parts[2].to_string()), + remote_port, + }) + } + 3 => { + let local_port = parse_port_number(parts[0])?; + let remote_port = parse_port_number(parts[2])?; + + Ok(SshPortForwardOption { + local_host: None, + local_port, + remote_host: Some(parts[1].to_string()), + remote_port, + }) + } + _ => anyhow::bail!("Invalid port forward format"), + } +} + +impl SshConnectionOptions { + pub fn parse_command_line(input: &str) -> Result { + let input = input.trim_start_matches("ssh "); + let mut hostname: Option = None; + let mut username: Option = None; + let mut port: Option = None; + let mut args = Vec::new(); + let mut port_forwards: Vec = Vec::new(); + + // disallowed: -E, -e, -F, -f, -G, -g, -M, -N, -n, -O, -q, -S, -s, -T, -t, -V, -v, -W + const ALLOWED_OPTS: &[&str] = &[ + "-4", "-6", "-A", "-a", "-C", "-K", "-k", "-X", "-x", "-Y", "-y", + ]; + const ALLOWED_ARGS: &[&str] = &[ + "-B", "-b", "-c", "-D", "-F", "-I", "-i", "-J", "-l", "-m", "-o", "-P", "-p", "-R", + "-w", + ]; + + let mut tokens = shlex::split(input).context("invalid input")?.into_iter(); + + 'outer: while let Some(arg) = tokens.next() { + if ALLOWED_OPTS.contains(&(&arg as &str)) { + args.push(arg.to_string()); + continue; + } + if arg == "-p" { + port = tokens.next().and_then(|arg| arg.parse().ok()); + continue; + } else if let Some(p) = arg.strip_prefix("-p") { + port = p.parse().ok(); + continue; + } + if arg == "-l" { + username = tokens.next(); + continue; + } else if let Some(l) = arg.strip_prefix("-l") { + username = Some(l.to_string()); + continue; + } + if arg == "-L" || arg.starts_with("-L") { + let forward_spec = if arg == "-L" { + tokens.next() + } else { + Some(arg.strip_prefix("-L").unwrap().to_string()) + }; + + if let Some(spec) = forward_spec { + port_forwards.push(parse_port_forward_spec(&spec)?); + } else { + anyhow::bail!("Missing port forward format"); + } + } + + for a in ALLOWED_ARGS { + if arg == *a { + args.push(arg); + if let Some(next) = tokens.next() { + args.push(next); + } + continue 'outer; + } else if arg.starts_with(a) { + args.push(arg); + continue 'outer; + } + } + if arg.starts_with("-") || hostname.is_some() { + anyhow::bail!("unsupported argument: {:?}", arg); + } + let mut input = &arg as &str; + // Destination might be: username1@username2@ip2@ip1 + if let Some((u, rest)) = input.rsplit_once('@') { + input = rest; + username = Some(u.to_string()); + } + if let Some((rest, p)) = input.split_once(':') { + input = rest; + port = p.parse().ok() + } + hostname = Some(input.to_string()) + } + + let Some(hostname) = hostname else { + anyhow::bail!("missing hostname"); + }; + + let port_forwards = match port_forwards.len() { + 0 => None, + _ => Some(port_forwards), + }; + + Ok(Self { + host: hostname, + username, + port, + port_forwards, + args: Some(args), + password: None, + nickname: None, + upload_binary_over_ssh: false, + }) + } + + pub fn ssh_url(&self) -> String { + let mut result = String::from("ssh://"); + if let Some(username) = &self.username { + // Username might be: username1@username2@ip2 + let username = urlencoding::encode(username); + result.push_str(&username); + result.push('@'); + } + result.push_str(&self.host); + if let Some(port) = self.port { + result.push(':'); + result.push_str(&port.to_string()); + } + result + } + + pub fn additional_args(&self) -> Vec { + let mut args = self.args.iter().flatten().cloned().collect::>(); + + if let Some(forwards) = &self.port_forwards { + args.extend(forwards.iter().map(|pf| { + let local_host = match &pf.local_host { + Some(host) => host, + None => "localhost", + }; + let remote_host = match &pf.remote_host { + Some(host) => host, + None => "localhost", + }; + + format!( + "-L{}:{}:{}:{}", + local_host, pf.local_port, remote_host, pf.remote_port + ) + })); + } + + args + } + + fn scp_url(&self) -> String { + if let Some(username) = &self.username { + format!("{}@{}", username, self.host) + } else { + self.host.clone() + } + } + + pub fn connection_string(&self) -> String { + let host = if let Some(username) = &self.username { + format!("{}@{}", username, self.host) + } else { + self.host.clone() + }; + if let Some(port) = &self.port { + format!("{}:{}", host, port) + } else { + host + } + } +} diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 6216ff77288938f7e4d424101c572f5bea13b69a..04028ebcac82f814652f32ad7439e32d650f5ad0 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -21,7 +21,7 @@ use project::{ }; use rpc::{ AnyProtoClient, TypedEnvelope, - proto::{self, SSH_PEER_ID, SSH_PROJECT_ID}, + proto::{self, REMOTE_SERVER_PEER_ID, REMOTE_SERVER_PROJECT_ID}, }; use settings::initial_server_settings_content; @@ -83,7 +83,7 @@ impl HeadlessProject { let worktree_store = cx.new(|cx| { let mut store = WorktreeStore::local(true, fs.clone()); - store.shared(SSH_PROJECT_ID, session.clone(), cx); + store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); store }); @@ -101,7 +101,7 @@ impl HeadlessProject { let buffer_store = cx.new(|cx| { let mut buffer_store = BufferStore::local(worktree_store.clone(), cx); - buffer_store.shared(SSH_PROJECT_ID, session.clone(), cx); + buffer_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); buffer_store }); @@ -119,7 +119,7 @@ impl HeadlessProject { breakpoint_store.clone(), cx, ); - dap_store.shared(SSH_PROJECT_ID, session.clone(), cx); + dap_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); dap_store }); @@ -131,7 +131,7 @@ impl HeadlessProject { fs.clone(), cx, ); - store.shared(SSH_PROJECT_ID, session.clone(), cx); + store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); store }); @@ -154,7 +154,7 @@ impl HeadlessProject { environment.clone(), cx, ); - task_store.shared(SSH_PROJECT_ID, session.clone(), cx); + task_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); task_store }); let settings_observer = cx.new(|cx| { @@ -164,7 +164,7 @@ impl HeadlessProject { task_store.clone(), cx, ); - observer.shared(SSH_PROJECT_ID, session.clone(), cx); + observer.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); observer }); @@ -185,7 +185,7 @@ impl HeadlessProject { fs.clone(), cx, ); - lsp_store.shared(SSH_PROJECT_ID, session.clone(), cx); + lsp_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); lsp_store }); @@ -213,15 +213,15 @@ impl HeadlessProject { ); // local_machine -> ssh handlers - session.subscribe_to_entity(SSH_PROJECT_ID, &worktree_store); - session.subscribe_to_entity(SSH_PROJECT_ID, &buffer_store); - session.subscribe_to_entity(SSH_PROJECT_ID, &cx.entity()); - session.subscribe_to_entity(SSH_PROJECT_ID, &lsp_store); - session.subscribe_to_entity(SSH_PROJECT_ID, &task_store); - session.subscribe_to_entity(SSH_PROJECT_ID, &toolchain_store); - session.subscribe_to_entity(SSH_PROJECT_ID, &dap_store); - session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer); - session.subscribe_to_entity(SSH_PROJECT_ID, &git_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &worktree_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &buffer_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &cx.entity()); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &lsp_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &task_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &toolchain_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &dap_store); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &settings_observer); + session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &git_store); session.add_request_handler(cx.weak_entity(), Self::handle_list_remote_directory); session.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata); @@ -288,7 +288,7 @@ impl HeadlessProject { } = event { cx.background_spawn(self.session.request(proto::UpdateBuffer { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, buffer_id: buffer.read(cx).remote_id().to_proto(), operations: vec![serialize_operation(operation)], })) @@ -310,7 +310,7 @@ impl HeadlessProject { } => { self.session .send(proto::UpdateLanguageServer { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, server_name: name.as_ref().map(|name| name.to_string()), language_server_id: language_server_id.to_proto(), variant: Some(message.clone()), @@ -320,7 +320,7 @@ impl HeadlessProject { LspStoreEvent::Notification(message) => { self.session .send(proto::Toast { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, notification_id: "lsp".to_string(), message: message.clone(), }) @@ -329,7 +329,7 @@ impl HeadlessProject { LspStoreEvent::LanguageServerLog(language_server_id, log_type, message) => { self.session .send(proto::LanguageServerLog { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, language_server_id: language_server_id.to_proto(), message: message.clone(), log_type: Some(log_type.to_proto()), @@ -338,7 +338,7 @@ impl HeadlessProject { } LspStoreEvent::LanguageServerPrompt(prompt) => { let request = self.session.request(proto::LanguageServerPromptRequest { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, actions: prompt .actions .iter() @@ -474,7 +474,7 @@ impl HeadlessProject { let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?; buffer_store.update(&mut cx, |buffer_store, cx| { buffer_store - .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx) + .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) .detach_and_log_err(cx); })?; @@ -500,7 +500,7 @@ impl HeadlessProject { let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?; buffer_store.update(&mut cx, |buffer_store, cx| { buffer_store - .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx) + .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) .detach_and_log_err(cx); })?; @@ -550,7 +550,7 @@ impl HeadlessProject { buffer_store.update(cx, |buffer_store, cx| { buffer_store - .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx) + .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) .detach_and_log_err(cx); }); @@ -586,7 +586,7 @@ impl HeadlessProject { response.buffer_ids.push(buffer_id.to_proto()); buffer_store .update(&mut cx, |buffer_store, cx| { - buffer_store.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx) + buffer_store.create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) })? .await?; } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 69fae7f399e506b26f005d30fdda6b7240df3e0b..e106a5ef18d59ebeb942564f24600635f78f89c7 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -22,7 +22,7 @@ use project::{ Project, ProjectPath, search::{SearchQuery, SearchResult}, }; -use remote::SshRemoteClient; +use remote::RemoteClient; use serde_json::json; use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content}; use smol::stream::StreamExt; @@ -1119,7 +1119,7 @@ async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) buffer.edit([(ix..ix + 1, "100")], None, cx); }); - let client = cx.read(|cx| project.read(cx).ssh_client().unwrap()); + let client = cx.read(|cx| project.read(cx).remote_client().unwrap()); client .update(cx, |client, cx| client.simulate_disconnect(cx)) .detach(); @@ -1782,7 +1782,7 @@ pub async fn init_test( }); init_logger(); - let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx); + let (opts, ssh_server_client) = RemoteClient::fake_server(cx, server_cx); let http_client = Arc::new(BlockedHttpClient); let node_runtime = NodeRuntime::unavailable(); let languages = Arc::new(LanguageRegistry::new(cx.executor())); @@ -1804,7 +1804,7 @@ pub async fn init_test( ) }); - let ssh = SshRemoteClient::fake_client(opts, cx).await; + let ssh = RemoteClient::fake_client(opts, cx).await; let project = build_project(ssh, cx); project .update(cx, { @@ -1819,7 +1819,7 @@ fn init_logger() { zlog::init_test(); } -fn build_project(ssh: Entity, cx: &mut TestAppContext) -> Entity { +fn build_project(ssh: Entity, cx: &mut TestAppContext) -> Entity { cx.update(|cx| { if !cx.has_global::() { let settings_store = SettingsStore::test(cx); @@ -1845,5 +1845,5 @@ fn build_project(ssh: Entity, cx: &mut TestAppContext) -> Entit language::init(cx); }); - cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx)) + cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, cx)) } diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index c6d1566d6038bb1b908c0b28eb8d24b85e5cc86d..cb671a72d9beab0983536571e81fcd78f3df21c8 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -19,14 +19,14 @@ use project::project_settings::ProjectSettings; use proto::CrashReport; use release_channel::{AppVersion, RELEASE_CHANNEL, ReleaseChannel}; -use remote::SshRemoteClient; +use remote::RemoteClient; use remote::{ json_log::LogRecord, protocol::{read_message, write_message}, proxy::ProxyLaunchError, }; use reqwest_client::ReqwestClient; -use rpc::proto::{self, Envelope, SSH_PROJECT_ID}; +use rpc::proto::{self, Envelope, REMOTE_SERVER_PROJECT_ID}; use rpc::{AnyProtoClient, TypedEnvelope}; use settings::{Settings, SettingsStore, watch_config_file}; use smol::channel::{Receiver, Sender}; @@ -396,7 +396,7 @@ fn start_server( }) .detach(); - SshRemoteClient::proto_client_from_channels(incoming_rx, outgoing_tx, cx, "server") + RemoteClient::proto_client_from_channels(incoming_rx, outgoing_tx, cx, "server") } fn init_paths() -> anyhow::Result<()> { @@ -867,34 +867,21 @@ where R: AsyncRead + Unpin, W: AsyncWrite + Unpin, { - use remote::protocol::read_message_raw; + use remote::protocol::{read_message_raw, write_size_prefixed_buffer}; let mut buffer = Vec::new(); loop { read_message_raw(&mut reader, &mut buffer) .await .with_context(|| format!("failed to read message from {}", socket_name))?; - write_size_prefixed_buffer(&mut writer, &mut buffer) .await .with_context(|| format!("failed to write message to {}", socket_name))?; - writer.flush().await?; - buffer.clear(); } } -async fn write_size_prefixed_buffer( - stream: &mut S, - buffer: &mut Vec, -) -> Result<()> { - let len = buffer.len() as u32; - stream.write_all(len.to_le_bytes().as_slice()).await?; - stream.write_all(buffer).await?; - Ok(()) -} - fn initialize_settings( session: AnyProtoClient, fs: Arc, @@ -910,7 +897,7 @@ fn initialize_settings( session .send(proto::Toast { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, notification_id: "server-settings-failed".to_string(), message: format!( "Error in settings on remote host {:?}: {}", @@ -922,7 +909,7 @@ fn initialize_settings( } else { session .send(proto::HideToast { - project_id: SSH_PROJECT_ID, + project_id: REMOTE_SERVER_PROJECT_ID, notification_id: "server-settings-failed".to_string(), }) .log_err(); diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index de4ddc00f49eded4ba64faa6d94baa1cc6ecf3aa..f907bd1d9f3f02869b2eb4b6ea4d65663e0d00af 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -36,7 +36,7 @@ impl ShellKind { } else if program == "csh" { ShellKind::Csh } else { - // Someother shell detected, the user might install and use a + // Some other shell detected, the user might install and use a // unix-like shell. ShellKind::Posix } @@ -203,14 +203,15 @@ pub struct ShellBuilder { impl ShellBuilder { /// Create a new ShellBuilder as configured. pub fn new(remote_system_shell: Option<&str>, shell: &Shell) -> Self { - let (program, args) = match shell { - Shell::System => match remote_system_shell { - Some(remote_shell) => (remote_shell.to_string(), Vec::new()), - None => (system_shell(), Vec::new()), + let (program, args) = match remote_system_shell { + Some(program) => (program.to_string(), Vec::new()), + None => match shell { + Shell::System => (system_shell(), Vec::new()), + Shell::Program(shell) => (shell.clone(), Vec::new()), + Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()), }, - Shell::Program(shell) => (shell.clone(), Vec::new()), - Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()), }; + let kind = ShellKind::new(&program); Self { program, diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index fe3301fb89095a7cf9f1a841395b47e9caf1475b..56715b604eeffe0b42302adcdf0d6fdd93919879 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1403,7 +1403,7 @@ impl InputHandler for TerminalInputHandler { window.invalidate_character_coordinates(); let project = this.project().read(cx); let telemetry = project.client().telemetry().clone(); - telemetry.log_edit_event("terminal", project.is_via_ssh()); + telemetry.log_edit_event("terminal", project.is_via_remote_server()); }) .ok(); } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 6b17911487261a254cd5e7a5e9256358c0b2e696..c3d7c4f793ed612a4dd819aca7552b48ccf6a3db 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -481,17 +481,28 @@ impl TerminalPanel { window: &mut Window, cx: &mut Context, ) -> Task>> { - let Ok((ssh_client, false)) = self.workspace.update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - ( - project.ssh_client().and_then(|it| it.read(cx).ssh_info()), - project.is_via_collab(), - ) - }) else { - return Task::ready(Err(anyhow!("Project is not local"))); + let remote_client = self + .workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + if project.is_via_collab() { + Err(anyhow!("cannot spawn tasks as a guest")) + } else { + Ok(project.remote_client()) + } + }) + .flatten(); + + let remote_client = match remote_client { + Ok(remote_client) => remote_client, + Err(e) => return Task::ready(Err(e)), }; - let builder = ShellBuilder::new(ssh_client.as_ref().map(|info| &*info.shell), &task.shell); + let remote_shell = remote_client + .as_ref() + .and_then(|remote_client| remote_client.read(cx).shell()); + + let builder = ShellBuilder::new(remote_shell.as_deref(), &task.shell); let command_label = builder.command_label(&task.command_label); let (command, args) = builder.build(task.command.clone(), &task.args); diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index c667edb509b6e7c5f906f038f19a1e50b5c65032..78f22faa13900f05fbd0cea9877858857aac626e 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -337,7 +337,7 @@ impl TitleBar { let room = room.read(cx); let project = self.project.read(cx); - let is_local = project.is_local() || project.is_via_ssh(); + let is_local = project.is_local() || project.is_via_remote_server(); let is_shared = is_local && project.is_shared(); let is_muted = room.is_muted(); let muted_by_user = room.muted_by_user(); diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index ad64dac9c69863222506bbca83d3d7379fc097ab..b08f139b25e53ef7e4761e55ad2686b3425969e9 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -299,8 +299,8 @@ impl TitleBar { } } - fn render_ssh_project_host(&self, cx: &mut Context) -> Option { - let options = self.project.read(cx).ssh_connection_options(cx)?; + fn render_remote_project_connection(&self, cx: &mut Context) -> Option { + let options = self.project.read(cx).remote_connection_options(cx)?; let host: SharedString = options.connection_string().into(); let nickname = options @@ -308,7 +308,7 @@ impl TitleBar { .map(|nick| nick.into()) .unwrap_or_else(|| host.clone()); - let (indicator_color, meta) = match self.project.read(cx).ssh_connection_state(cx)? { + let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? { remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")), remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")), remote::ConnectionState::HeartbeatMissed => ( @@ -324,7 +324,7 @@ impl TitleBar { } }; - let icon_color = match self.project.read(cx).ssh_connection_state(cx)? { + let icon_color = match self.project.read(cx).remote_connection_state(cx)? { remote::ConnectionState::Connecting => Color::Info, remote::ConnectionState::Connected => Color::Default, remote::ConnectionState::HeartbeatMissed => Color::Warning, @@ -379,8 +379,8 @@ impl TitleBar { } pub fn render_project_host(&self, cx: &mut Context) -> Option { - if self.project.read(cx).is_via_ssh() { - return self.render_ssh_project_host(cx); + if self.project.read(cx).is_via_remote_server() { + return self.render_remote_project_connection(cx); } if self.project.read(cx).is_disconnected(cx) { diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index b57c916db988f683cef6bae15ec562392a488be6..29fe6aae0252bcc1ca5767f71b7c668ecae1b9a8 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1924,7 +1924,9 @@ impl ShellExec { let Some(range) = input_range else { return }; - let mut process = project.read(cx).exec_in_shell(command, cx); + let Some(mut process) = project.read(cx).exec_in_shell(command, cx).log_err() else { + return; + }; process.stdout(Stdio::piped()); process.stderr(Stdio::piped()); diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs index 32d066c7eb74f9019348d3bcac9402ebb7216a4e..71394c874ae988d7b8fef3e3a224d25e1c290640 100644 --- a/crates/workspace/src/tasks.rs +++ b/crates/workspace/src/tasks.rs @@ -20,7 +20,7 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) { - match self.project.read(cx).ssh_connection_state(cx) { + match self.project.read(cx).remote_connection_state(cx) { None | Some(ConnectionState::Connected) => {} Some( ConnectionState::Connecting diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 044601df97840bc95a821cd4f2ccfc2f8b0abbbf..25e2cb1cfe934a88ec4cc3811bf3216e0765c0af 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -74,7 +74,7 @@ use project::{ DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId, debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, }; -use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIdentifier}; +use remote::{RemoteClientDelegate, SshConnectionOptions, remote_client::ConnectionIdentifier}; use schemars::JsonSchema; use serde::Deserialize; use session::AppSession; @@ -2084,7 +2084,7 @@ impl Workspace { cx: &mut Context, ) -> oneshot::Receiver>> { if self.project.read(cx).is_via_collab() - || self.project.read(cx).is_via_ssh() + || self.project.read(cx).is_via_remote_server() || !WorkspaceSettings::get_global(cx).use_system_path_prompts { let prompt = self.on_prompt_for_new_path.take().unwrap(); @@ -5249,7 +5249,7 @@ impl Workspace { fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { let paths = PathList::new(&self.root_paths(cx)); - if let Some(connection) = self.project.read(cx).ssh_connection_options(cx) { + if let Some(connection) = self.project.read(cx).remote_connection_options(cx) { WorkspaceLocation::Location( SerializedWorkspaceLocation::Ssh(SerializedSshConnection { host: connection.host, @@ -6917,7 +6917,7 @@ async fn join_channel_internal( return None; } - if (project.is_local() || project.is_via_ssh()) + if (project.is_local() || project.is_via_remote_server()) && project.visible_worktrees(cx).any(|tree| { tree.read(cx) .root_entry() @@ -7263,7 +7263,7 @@ pub fn open_ssh_project_with_new_connection( window: WindowHandle, connection_options: SshConnectionOptions, cancel_rx: oneshot::Receiver<()>, - delegate: Arc, + delegate: Arc, app_state: Arc, paths: Vec, cx: &mut App, @@ -7274,7 +7274,7 @@ pub fn open_ssh_project_with_new_connection( let session = match cx .update(|cx| { - remote::SshRemoteClient::new( + remote::RemoteClient::ssh( ConnectionIdentifier::Workspace(workspace_id.0), connection_options, cancel_rx, @@ -7289,7 +7289,7 @@ pub fn open_ssh_project_with_new_connection( }; let project = cx.update(|cx| { - project::Project::ssh( + project::Project::remote( session, app_state.client.clone(), app_state.node_runtime.clone(), diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index ac06f1fd9f4d9842eb34a67abeef2432074ecc91..9c12a5f1466323cf22233156d1e9bde741f10fa6 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -220,10 +220,10 @@ pub fn init( let installation_id = installation_id.clone(); let system_id = system_id.clone(); - let Some(ssh_client) = project.ssh_client() else { + let Some(remote_client) = project.remote_client() else { return; }; - ssh_client.update(cx, |client, cx| { + remote_client.update(cx, |client, cx| { if !TelemetrySettings::get_global(cx).diagnostics { return; } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 553444ebdbc2fe018f1cad39d0776c4c113eba6f..a3116971b494cc9d110edc58b9c9d6b50ff86c25 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -918,7 +918,7 @@ fn register_actions( capture_audio(workspace, window, cx); }); - if workspace.project().read(cx).is_via_ssh() { + if workspace.project().read(cx).is_via_remote_server() { workspace.register_action({ move |workspace, _: &OpenServerSettings, window, cx| { let open_server_settings = workspace @@ -1543,7 +1543,7 @@ pub fn open_new_ssh_project_from_project( cx: &mut Context, ) -> Task> { let app_state = workspace.app_state().clone(); - let Some(ssh_client) = workspace.project().read(cx).ssh_client() else { + let Some(ssh_client) = workspace.project().read(cx).remote_client() else { return Task::ready(Err(anyhow::anyhow!("Not an ssh project"))); }; let connection_options = ssh_client.read(cx).connection_options(); From d0aef3cec196216b741dec1f8bfad3e69ed83beb Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 26 Aug 2025 22:17:03 -0300 Subject: [PATCH 366/744] thread view: Fix cut-off review button (#36970) --- crates/agent_ui/src/acp/thread_view.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 30941f9e76d7615bc6ea6c076c9eb97a0ff239cf..f01a7958a8cfff7a6d45627a761fcdf62b76fd71 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1504,7 +1504,7 @@ impl AcpThreadView { let style = default_markdown_style(false, false, window, cx); let message_body = v_flex() .w_full() - .gap_2p5() + .gap_3() .children(chunks.iter().enumerate().filter_map( |(chunk_ix, chunk)| match chunk { AssistantMessageChunk::Message { block } => { @@ -3352,7 +3352,6 @@ impl AcpThreadView { let element = h_flex() .group("edited-code") .id(("file-container", index)) - .relative() .py_1() .pl_2() .pr_1() @@ -3364,6 +3363,7 @@ impl AcpThreadView { }) .child( h_flex() + .relative() .id(("file-name", index)) .pr_8() .gap_1p5() @@ -3371,6 +3371,16 @@ impl AcpThreadView { .overflow_x_scroll() .child(file_icon) .child(h_flex().gap_0p5().children(file_name).children(file_path)) + .child( + div() + .absolute() + .h_full() + .w_12() + .top_0() + .bottom_0() + .right_0() + .bg(overlay_gradient), + ) .on_click({ let buffer = buffer.clone(); cx.listener(move |this, _, window, cx| { @@ -3431,17 +3441,6 @@ impl AcpThreadView { } }), ), - ) - .child( - div() - .id("gradient-overlay") - .absolute() - .h_full() - .w_12() - .top_0() - .bottom_0() - .right(px(152.)) - .bg(overlay_gradient), ); Some(element) From e6e64017eabb20907dbdd75ddfe7e9c536b48756 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 26 Aug 2025 20:01:51 -0600 Subject: [PATCH 367/744] acp: Require gemini version 0.2.0 (#36960) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 20 ++-- crates/agent2/src/native_agent_server.rs | 4 + crates/agent_servers/src/acp.rs | 6 +- crates/agent_servers/src/agent_servers.rs | 2 + crates/agent_servers/src/claude.rs | 23 ++--- crates/agent_servers/src/custom.rs | 4 + crates/agent_servers/src/gemini.rs | 106 ++++++++++++++-------- crates/agent_ui/src/acp/thread_view.rs | 67 +++++++------- 8 files changed, 134 insertions(+), 98 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 4ded647a746f18e030529e4c14a00c1ffd2335e3..0da4b43394b76125b2ea9a310ae5bfe9bf0fac9a 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -789,15 +789,10 @@ pub enum ThreadStatus { #[derive(Debug, Clone)] pub enum LoadError { - NotInstalled { - error_message: SharedString, - install_message: SharedString, - install_command: String, - }, + NotInstalled, Unsupported { - error_message: SharedString, - upgrade_message: SharedString, - upgrade_command: String, + command: SharedString, + current_version: SharedString, }, Exited { status: ExitStatus, @@ -808,9 +803,12 @@ pub enum LoadError { impl Display for LoadError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - LoadError::NotInstalled { error_message, .. } - | LoadError::Unsupported { error_message, .. } => { - write!(f, "{error_message}") + LoadError::NotInstalled => write!(f, "not installed"), + LoadError::Unsupported { + command: path, + current_version, + } => { + write!(f, "version {current_version} from {path} is not supported") } LoadError::Exited { status } => write!(f, "Server exited with status {status}"), LoadError::Other(msg) => write!(f, "{}", msg), diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 9ff98ccd18dec4d9a17a1a7161cd3622dacf0d3f..0079dcc5724a77e02cd68be3deaee0735fcf56fa 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -42,6 +42,10 @@ impl AgentServer for NativeAgentServer { ui::IconName::ZedAgent } + fn install_command(&self) -> Option<&'static str> { + None + } + fn connect( &self, _root_dir: &Path, diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index b4e897374ad079265a85f2f504109abefcbc075f..b4f82a0a238ec65116688a90986f8792d78e05ed 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -56,7 +56,7 @@ impl AcpConnection { root_dir: &Path, cx: &mut AsyncApp, ) -> Result { - let mut child = util::command::new_smol_command(&command.path) + 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) @@ -150,6 +150,10 @@ impl AcpConnection { _io_task: io_task, }) } + + pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities { + &self.prompt_capabilities + } } impl AgentConnection for AcpConnection { diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 7c7e124ca71b684cdda7a24e02c82d1b6117a0cc..dc7d75c52d72928cfc3673bc1eb476f08206669f 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -46,6 +46,8 @@ pub trait AgentServer: Send { ) -> Task>>; fn into_any(self: Rc) -> Rc; + + fn install_command(&self) -> Option<&'static str>; } impl dyn AgentServer { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 250e564526d5360be7a84f5cfd9511e5f73a2c1f..3a16b0601a0a85a2a66d170455af6b4cb9f4ae8f 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -63,6 +63,10 @@ impl AgentServer for ClaudeCode { ui::IconName::AiClaude } + fn install_command(&self) -> Option<&'static str> { + Some("npm install -g @anthropic-ai/claude-code@latest") + } + fn connect( &self, _root_dir: &Path, @@ -108,11 +112,7 @@ impl AgentConnection for ClaudeAgentConnection { ) .await else { - return Err(LoadError::NotInstalled { - error_message: "Failed to find Claude Code binary".into(), - install_message: "Install Claude Code".into(), - install_command: "npm install -g @anthropic-ai/claude-code@latest".into(), - }.into()); + return Err(LoadError::NotInstalled.into()); }; let api_key = @@ -230,17 +230,8 @@ impl AgentConnection for ClaudeAgentConnection { || !help.contains("--session-id")) { LoadError::Unsupported { - error_message: format!( - "Your installed version of Claude Code ({}, version {}) does not have required features for use with Zed.", - command.path.to_string_lossy(), - version, - ) - .into(), - upgrade_message: "Upgrade Claude Code to latest".into(), - upgrade_command: format!( - "{} update", - command.path.to_string_lossy() - ), + command: command.path.to_string_lossy().to_string().into(), + current_version: version.to_string().into(), } } else { LoadError::Exited { status } diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index 72823026d7ce353e76485fbe76783b3cc2bbeb56..75928a26a8b4499d7b6ced8a8392191ac3ca2f32 100644 --- a/crates/agent_servers/src/custom.rs +++ b/crates/agent_servers/src/custom.rs @@ -57,6 +57,10 @@ impl crate::AgentServer for CustomAgentServer { }) } + fn install_command(&self) -> Option<&'static str> { + None + } + fn into_any(self: Rc) -> Rc { self } diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 5d6a70fa64d981b2f25aee13c9d1d3ac7f94468f..33d92060a49ccb79fd12605f2089bfa0a9d65608 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -1,6 +1,7 @@ use std::rc::Rc; use std::{any::Any, path::Path}; +use crate::acp::AcpConnection; use crate::{AgentServer, AgentServerCommand}; use acp_thread::{AgentConnection, LoadError}; use anyhow::Result; @@ -37,6 +38,10 @@ impl AgentServer for Gemini { ui::IconName::AiGemini } + fn install_command(&self) -> Option<&'static str> { + Some("npm install -g @google/gemini-cli@latest") + } + fn connect( &self, root_dir: &Path, @@ -52,48 +57,73 @@ impl AgentServer for Gemini { })?; let Some(mut command) = - AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await + AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx) + .await else { - return Err(LoadError::NotInstalled { - error_message: "Failed to find Gemini CLI binary".into(), - install_message: "Install Gemini CLI".into(), - install_command: Self::install_command().into(), - }.into()); + return Err(LoadError::NotInstalled.into()); }; - if let Some(api_key)= cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() { - command.env.get_or_insert_default().insert("GEMINI_API_KEY".to_owned(), api_key.key); + if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() { + command + .env + .get_or_insert_default() + .insert("GEMINI_API_KEY".to_owned(), api_key.key); } let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await; - if result.is_err() { - 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 { - return Err(LoadError::Unsupported { - error_message: format!( - "Your installed version of Gemini CLI ({}, version {}) doesn't support the Agentic Coding Protocol (ACP).", - command.path.to_string_lossy(), - current_version - ).into(), - upgrade_message: "Upgrade Gemini CLI to latest".into(), - upgrade_command: Self::upgrade_command().into(), - }.into()) + match &result { + Ok(connection) => { + if let Some(connection) = connection.clone().downcast::() + && !connection.prompt_capabilities().image + { + let version_output = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .arg("--version") + .kill_on_drop(true) + .output() + .await; + let current_version = + String::from_utf8(version_output?.stdout)?.trim().to_owned(); + if !connection.prompt_capabilities().image { + return Err(LoadError::Unsupported { + current_version: current_version.into(), + command: format!( + "{} {}", + command.path.to_string_lossy(), + command.args.join(" ") + ) + .into(), + } + .into()); + } + } + } + Err(_) => { + 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 { + return Err(LoadError::Unsupported { + current_version: current_version.into(), + command: command.path.to_string_lossy().to_string().into(), + } + .into()); + } } } result @@ -111,11 +141,11 @@ impl Gemini { } pub fn install_command() -> &'static str { - "npm install -g @google/gemini-cli@preview" + "npm install -g @google/gemini-cli@latest" } pub fn upgrade_command() -> &'static str { - "npm install -g @google/gemini-cli@preview" + "npm install -g @google/gemini-cli@latest" } } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f01a7958a8cfff7a6d45627a761fcdf62b76fd71..54d3421c3bff63bccf2b2cdeaa9dfb509dd7b96b 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2825,19 +2825,14 @@ impl AcpThreadView { cx: &mut Context, ) -> AnyElement { let (message, action_slot): (SharedString, _) = match e { - LoadError::NotInstalled { - error_message: _, - install_message: _, - install_command, - } => { - return self.render_not_installed(install_command.clone(), false, window, cx); + LoadError::NotInstalled => { + return self.render_not_installed(None, window, cx); } LoadError::Unsupported { - error_message: _, - upgrade_message: _, - upgrade_command, + command: path, + current_version, } => { - return self.render_not_installed(upgrade_command.clone(), true, window, cx); + return self.render_not_installed(Some((path, current_version)), window, cx); } LoadError::Exited { .. } => ("Server exited with status {status}".into(), None), LoadError::Other(msg) => ( @@ -2855,8 +2850,11 @@ impl AcpThreadView { .into_any_element() } - fn install_agent(&self, install_command: String, window: &mut Window, cx: &mut Context) { + fn install_agent(&self, window: &mut Window, cx: &mut Context) { telemetry::event!("Agent Install CLI", agent = self.agent.telemetry_id()); + let Some(install_command) = self.agent.install_command().map(|s| s.to_owned()) else { + return; + }; let task = self .workspace .update(cx, |workspace, cx| { @@ -2899,32 +2897,35 @@ impl AcpThreadView { fn render_not_installed( &self, - install_command: String, - is_upgrade: bool, + existing_version: Option<(&SharedString, &SharedString)>, window: &mut Window, cx: &mut Context, ) -> AnyElement { + let install_command = self.agent.install_command().unwrap_or_default(); + self.install_command_markdown.update(cx, |markdown, cx| { if !markdown.source().contains(&install_command) { markdown.replace(format!("```\n{}\n```", install_command), cx); } }); - let (heading_label, description_label, button_label, or_label) = if is_upgrade { - ( - "Upgrade Gemini CLI in Zed", - "Get access to the latest version with support for Zed.", - "Upgrade Gemini CLI", - "Or, to upgrade it manually:", - ) - } else { - ( - "Get Started with Gemini CLI in Zed", - "Use Google's new coding agent directly in Zed.", - "Install Gemini CLI", - "Or, to install it manually:", - ) - }; + let (heading_label, description_label, button_label) = + if let Some((path, version)) = existing_version { + ( + format!("Upgrade {} to work with Zed", self.agent.name()), + format!( + "Currently using {}, which is only version {}", + path, version + ), + format!("Upgrade {}", self.agent.name()), + ) + } else { + ( + format!("Get Started with {} in Zed", self.agent.name()), + "Use Google's new coding agent directly in Zed.".to_string(), + format!("Install {}", self.agent.name()), + ) + }; v_flex() .w_full() @@ -2954,12 +2955,10 @@ impl AcpThreadView { .icon_color(Color::Muted) .icon_size(IconSize::Small) .icon_position(IconPosition::Start) - .on_click(cx.listener(move |this, _, window, cx| { - this.install_agent(install_command.clone(), window, cx) - })), + .on_click(cx.listener(|this, _, window, cx| this.install_agent(window, cx))), ) .child( - Label::new(or_label) + Label::new("Or, run the following command in your terminal:") .size(LabelSize::Small) .color(Color::Muted), ) @@ -5403,6 +5402,10 @@ pub(crate) mod tests { "Test".into() } + fn install_command(&self) -> Option<&'static str> { + None + } + fn connect( &self, _root_dir: &Path, From a3e1611fa86b3de7f65a9d25f16c3a7b107afaf9 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 26 Aug 2025 22:52:24 -0400 Subject: [PATCH 368/744] Bump Zed to v0.203 (#36975) Release Notes: - N/A --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 42649b137f35cff3bcc9622229d1b396dd9d1d87..6ece2bb6bf9442a4af81a2928ef47209fd6f00df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20396,7 +20396,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.202.0" +version = "0.203.0" dependencies = [ "acp_tools", "activity_indicator", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6f4ead9ebb6d39d0cd14a5c6ad1fca07b9c1e83a..0ddfe3dde1b57de8f6fb5ae83d1bb3ccef8b12ff 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.202.0" +version = "0.203.0" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] From abd6009b41595713de847e641bf78e9e46051909 Mon Sep 17 00:00:00 2001 From: Caio Piccirillo <34453935+caiopiccirillo@users.noreply.github.com> Date: Wed, 27 Aug 2025 01:31:57 -0300 Subject: [PATCH 369/744] Enhance syntax highlight for C++20 keywords (#36817) Closes #36439 and #32999 ## C++20 modules: Before (Zed Preview v0.201.3): image After: image ## C++20 coroutines: Before (Zed Preview v0.201.3): image After: image ## Logical operators: Before (Zed Preview v0.201.3): image After: image ## Operator keyword: Before (Zed Preview v0.201.3): image After: image ## Goto: Before (Zed Preview v0.201.3): image After: image Release Notes: - Enhance keyword highlighting for C++ --- Cargo.lock | 3 +- Cargo.toml | 2 +- crates/languages/src/cpp/highlights.scm | 68 ++++++++++++++++++++----- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ece2bb6bf9442a4af81a2928ef47209fd6f00df..4325addc392214614a6654563d88041331f2ded9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17185,8 +17185,7 @@ dependencies = [ [[package]] name = "tree-sitter-cpp" version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743" +source = "git+https://github.com/tree-sitter/tree-sitter-cpp?rev=5cb9b693cfd7bfacab1d9ff4acac1a4150700609#5cb9b693cfd7bfacab1d9ff4acac1a4150700609" dependencies = [ "cc", "tree-sitter-language", diff --git a/Cargo.toml b/Cargo.toml index 6ec243a9b9de4d2ab322e0466e804fa542a1ed35..209c312aec4061c97d547a6157dece5ed5402cf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -624,7 +624,7 @@ tower-http = "0.4.4" tree-sitter = { version = "0.25.6", features = ["wasm"] } tree-sitter-bash = "0.25.0" tree-sitter-c = "0.23" -tree-sitter-cpp = "0.23" +tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" } tree-sitter-css = "0.23" tree-sitter-diff = "0.1.0" tree-sitter-elixir = "0.3" diff --git a/crates/languages/src/cpp/highlights.scm b/crates/languages/src/cpp/highlights.scm index 6fa8bd7b0858d3a1844ce2d322564ce9c39babea..bd988445bb155e8851ffa8bc3771bdd235fc7dff 100644 --- a/crates/languages/src/cpp/highlights.scm +++ b/crates/languages/src/cpp/highlights.scm @@ -3,8 +3,27 @@ (namespace_identifier) @namespace (concept_definition - (identifier) @concept) + name: (identifier) @concept) +(requires_clause + constraint: (template_type + name: (type_identifier) @concept)) + +(module_name + (identifier) @module) + +(module_declaration + name: (module_name + (identifier) @module)) + +(import_declaration + name: (module_name + (identifier) @module)) + +(import_declaration + partition: (module_partition + (module_name + (identifier) @module))) (call_expression function: (qualified_identifier @@ -61,6 +80,9 @@ (operator_name (identifier)? @operator) @function +(operator_name + "<=>" @operator.spaceship) + (destructor_name (identifier) @function) ((namespace_identifier) @type @@ -68,21 +90,17 @@ (auto) @type (type_identifier) @type -type :(primitive_type) @type.primitive -(sized_type_specifier) @type.primitive - -(requires_clause - constraint: (template_type - name: (type_identifier) @concept)) +type: (primitive_type) @type.builtin +(sized_type_specifier) @type.builtin (attribute - name: (identifier) @keyword) + name: (identifier) @attribute) -((identifier) @constant - (#match? @constant "^_*[A-Z][A-Z\\d_]*$")) +((identifier) @constant.builtin + (#match? @constant.builtin "^_*[A-Z][A-Z\\d_]*$")) (statement_identifier) @label -(this) @variable.special +(this) @variable.builtin ("static_assert") @function.builtin [ @@ -96,7 +114,9 @@ type :(primitive_type) @type.primitive "co_return" "co_yield" "concept" + "consteval" "constexpr" + "constinit" "continue" "decltype" "default" @@ -105,15 +125,20 @@ type :(primitive_type) @type.primitive "else" "enum" "explicit" + "export" "extern" "final" "for" "friend" + "goto" "if" + "import" "inline" + "module" "namespace" "new" "noexcept" + "operator" "override" "private" "protected" @@ -124,6 +149,7 @@ type :(primitive_type) @type.primitive "struct" "switch" "template" + "thread_local" "throw" "try" "typedef" @@ -146,7 +172,7 @@ type :(primitive_type) @type.primitive "#ifndef" "#include" (preproc_directive) -] @keyword +] @keyword.directive (comment) @comment @@ -224,10 +250,24 @@ type :(primitive_type) @type.primitive ">" "<=" ">=" - "<=>" - "||" "?" + "and" + "and_eq" + "bitand" + "bitor" + "compl" + "not" + "not_eq" + "or" + "or_eq" + "xor" + "xor_eq" ] @operator +"<=>" @operator.spaceship + +(binary_expression + operator: "<=>" @operator.spaceship) + (conditional_expression ":" @operator) (user_defined_literal (literal_suffix) @operator) From f4071bdd8ea2a3914f169cd3ec4578541b684de0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 27 Aug 2025 00:24:56 -0600 Subject: [PATCH 370/744] acp: Upgrade errors (#36980) - **Pass --engine-strict to gemini install command** - **Make it clearer that if upgrading fails, you need to fix i** Closes #ISSUE Release Notes: - N/A --- crates/agent_servers/src/gemini.rs | 4 ++-- crates/agent_ui/src/acp/thread_view.rs | 22 ++++++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 33d92060a49ccb79fd12605f2089bfa0a9d65608..6d17cc0512d382f9b1a550dcb978957318de0d74 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -39,7 +39,7 @@ impl AgentServer for Gemini { } fn install_command(&self) -> Option<&'static str> { - Some("npm install -g @google/gemini-cli@latest") + Some("npm install --engine-strict -g @google/gemini-cli@latest") } fn connect( @@ -141,7 +141,7 @@ impl Gemini { } pub fn install_command() -> &'static str { - "npm install -g @google/gemini-cli@latest" + "npm install --engine-strict -g @google/gemini-cli@latest" } pub fn upgrade_command() -> &'static str { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 54d3421c3bff63bccf2b2cdeaa9dfb509dd7b96b..70d32088c549955a82323ac13d068943d65e6853 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2913,10 +2913,17 @@ impl AcpThreadView { if let Some((path, version)) = existing_version { ( format!("Upgrade {} to work with Zed", self.agent.name()), - format!( - "Currently using {}, which is only version {}", - path, version - ), + if version.is_empty() { + format!( + "Currently using {}, which does not report a valid --version", + path, + ) + } else { + format!( + "Currently using {}, which is only version {}", + path, version + ) + }, format!("Upgrade {}", self.agent.name()), ) } else { @@ -2966,6 +2973,13 @@ impl AcpThreadView { self.install_command_markdown.clone(), default_markdown_style(false, false, window, cx), )) + .when_some(existing_version, |el, (path, _)| { + el.child( + Label::new(format!("If this does not work you will need to upgrade manually, or uninstall your existing version from {}", path)) + .size(LabelSize::Small) + .color(Color::Muted), + ) + }) .into_any_element() } From a03897012e9c22709f26597ce96e8bf4caf8974e Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 27 Aug 2025 09:06:33 +0200 Subject: [PATCH 371/744] Swap `NewlineBelow` and `NewlineAbove` bindings for default linux keymap (#36939) Closes https://github.com/zed-industries/zed/issues/33725 The default bindings for the `editor::NewlineAbove` and `editor::NewlineBelow` actions in the default keymap were accidentally swapped some time ago. This causes confusion, as normally these are the other way around. This PR fixes this by swapping these back, which also matches what [VSCode does by default](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf). Release Notes: - Swapped the default bindings for `editor::NewlineBelow` and `editor::NewlineAbove` for Linux and Windows to align more with other editors. --- assets/keymaps/default-linux.json | 4 ++-- assets/keymaps/default-windows.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 3cca560c0088a5be19ea42aeb4753db4d158bf4d..2610f9b7051cbce74ce6df13d49699c74e870395 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -130,8 +130,8 @@ "bindings": { "shift-enter": "editor::Newline", "enter": "editor::Newline", - "ctrl-enter": "editor::NewlineAbove", - "ctrl-shift-enter": "editor::NewlineBelow", + "ctrl-enter": "editor::NewlineBelow", + "ctrl-shift-enter": "editor::NewlineAbove", "ctrl-k ctrl-z": "editor::ToggleSoftWrap", "ctrl-k z": "editor::ToggleSoftWrap", "find": "buffer_search::Deploy", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index c7a6c3149ccab86d3efcf8f5db94e0b40be6a3c0..dbd377409f4423dd12bac06b651efd079772dbb5 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -134,8 +134,8 @@ "bindings": { "shift-enter": "editor::Newline", "enter": "editor::Newline", - "ctrl-enter": "editor::NewlineAbove", - "ctrl-shift-enter": "editor::NewlineBelow", + "ctrl-enter": "editor::NewlineBelow", + "ctrl-shift-enter": "editor::NewlineAbove", "ctrl-k ctrl-z": "editor::ToggleSoftWrap", "ctrl-k z": "editor::ToggleSoftWrap", "find": "buffer_search::Deploy", From ea347b0aa11c13b0acbccfc015e0c23c790811e3 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 27 Aug 2025 13:00:10 +0530 Subject: [PATCH 372/744] project: Handle capabilities parse for more methods when registerOptions doesn't exist (#36984) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #36938 Follow up to https://github.com/zed-industries/zed/pull/36554 When `registerOptions` is `None`, we should fall back instead of skipping capability registration. 1. `Option>`, where `T` is struct – handled in the attached PR ✅ 2. `Option`, where `T` is an enum that can be `Simple(bool)` or `Options(S)` – this PR ✅ 3. `Option`, where `T` is struct – we should fall back to default values for these options ⚠️ Release Notes: - Fixed an issue where hover popovers would not appear in language servers like Java. --- crates/project/src/lsp_store.rs | 58 +++++++++++++++------------------ 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index deebaedd74a9a56bba27632a640443e03d5f5517..b92d739360e55bff2a156a8e7d0dcd3b58aa1300 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11778,17 +11778,15 @@ impl LspStore { notify_server_capabilities_updated(&server, cx); } "textDocument/codeAction" => { - if let Some(options) = reg - .register_options - .map(serde_json::from_value) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.code_action_provider = - Some(lsp::CodeActionProviderCapability::Options(options)); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + let provider = match options { + OneOf::Left(value) => lsp::CodeActionProviderCapability::Simple(value), + OneOf::Right(caps) => caps, + }; + server.update_capabilities(|capabilities| { + capabilities.code_action_provider = Some(provider); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/definition" => { let options = parse_register_capabilities(reg)?; @@ -11810,16 +11808,15 @@ impl LspStore { } } "textDocument/hover" => { - if let Some(caps) = reg - .register_options - .map(serde_json::from_value) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.hover_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + let provider = match options { + OneOf::Left(value) => lsp::HoverProviderCapability::Simple(value), + OneOf::Right(caps) => caps, + }; + server.update_capabilities(|capabilities| { + capabilities.hover_provider = Some(provider); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/signatureHelp" => { if let Some(caps) = reg @@ -11904,16 +11901,15 @@ impl LspStore { } } "textDocument/documentColor" => { - if let Some(caps) = reg - .register_options - .map(serde_json::from_value) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.color_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + let provider = match options { + OneOf::Left(value) => lsp::ColorProviderCapability::Simple(value), + OneOf::Right(caps) => caps, + }; + server.update_capabilities(|capabilities| { + capabilities.color_provider = Some(provider); + }); + notify_server_capabilities_updated(&server, cx); } _ => log::warn!("unhandled capability registration: {reg:?}"), } From e5c0614e8852fbb7151fbee3d1f21f7559d4ab92 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 27 Aug 2025 11:18:15 +0200 Subject: [PATCH 373/744] Ensure we use the new agent when opening the panel for the first time (#36988) Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index d1cf748733127b4c5466d96d7507e5bf7c8a16ce..624cb5c0c685db4d1a8462bfcaaa79073a6612e4 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -618,6 +618,10 @@ impl AgentPanel { } cx.notify(); }); + } else { + panel.update(cx, |panel, cx| { + panel.new_agent_thread(AgentType::NativeAgent, window, cx); + }); } panel })?; From b4d4294bee6840e09814e399fb78dd7201c9247a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 27 Aug 2025 11:29:17 +0200 Subject: [PATCH 374/744] Restore token count for text threads (#36989) Release Notes: - N/A Co-authored-by: Bennet Bo Fenner --- crates/agent_ui/src/agent_panel.rs | 7 +- crates/agent_ui/src/text_thread_editor.rs | 110 +++++++++++----------- 2 files changed, 56 insertions(+), 61 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 624cb5c0c685db4d1a8462bfcaaa79073a6612e4..586a782bc35211fa77c744b229fb7bf9f1ef0057 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -29,7 +29,6 @@ use crate::{ slash_command::SlashCommandCompletionProvider, text_thread_editor::{ AgentPanelDelegate, TextThreadEditor, humanize_token_count, make_lsp_adapter_delegate, - render_remaining_tokens, }, thread_history::{HistoryEntryElement, ThreadHistory}, ui::{AgentOnboardingModal, EndTrialUpsell}, @@ -2875,12 +2874,8 @@ impl AgentPanel { Some(token_count) } - ActiveView::TextThread { context_editor, .. } => { - let element = render_remaining_tokens(context_editor, cx)?; - - Some(element.into_any_element()) - } ActiveView::ExternalAgentThread { .. } + | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None, } diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index e9e7eba4b668fd09eb98a45b43bea6eb72b15277..70ec94beeadb1ae84839bab6747715223f2540c9 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1857,6 +1857,53 @@ impl TextThreadEditor { .update(cx, |context, cx| context.summarize(true, cx)); } + fn render_remaining_tokens(&self, cx: &App) -> Option> { + let (token_count_color, token_count, max_token_count, tooltip) = + match token_state(&self.context, cx)? { + TokenState::NoTokensLeft { + max_token_count, + token_count, + } => ( + Color::Error, + token_count, + max_token_count, + Some("Token Limit Reached"), + ), + TokenState::HasMoreTokens { + max_token_count, + token_count, + over_warn_threshold, + } => { + let (color, tooltip) = if over_warn_threshold { + (Color::Warning, Some("Token Limit is Close to Exhaustion")) + } else { + (Color::Muted, None) + }; + (color, token_count, max_token_count, tooltip) + } + }; + + Some( + h_flex() + .id("token-count") + .gap_0p5() + .child( + Label::new(humanize_token_count(token_count)) + .size(LabelSize::Small) + .color(token_count_color), + ) + .child(Label::new("/").size(LabelSize::Small).color(Color::Muted)) + .child( + Label::new(humanize_token_count(max_token_count)) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .when_some(tooltip, |element, tooltip| { + element.tooltip(Tooltip::text(tooltip)) + }), + ) + } + fn render_send_button(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let focus_handle = self.focus_handle(cx); @@ -2420,9 +2467,14 @@ impl Render for TextThreadEditor { ) .child( h_flex() - .gap_1() - .child(self.render_language_model_selector(window, cx)) - .child(self.render_send_button(window, cx)), + .gap_2p5() + .children(self.render_remaining_tokens(cx)) + .child( + h_flex() + .gap_1() + .child(self.render_language_model_selector(window, cx)) + .child(self.render_send_button(window, cx)), + ), ), ) } @@ -2710,58 +2762,6 @@ impl FollowableItem for TextThreadEditor { } } -pub fn render_remaining_tokens( - context_editor: &Entity, - cx: &App, -) -> Option> { - let context = &context_editor.read(cx).context; - - let (token_count_color, token_count, max_token_count, tooltip) = match token_state(context, cx)? - { - TokenState::NoTokensLeft { - max_token_count, - token_count, - } => ( - Color::Error, - token_count, - max_token_count, - Some("Token Limit Reached"), - ), - TokenState::HasMoreTokens { - max_token_count, - token_count, - over_warn_threshold, - } => { - let (color, tooltip) = if over_warn_threshold { - (Color::Warning, Some("Token Limit is Close to Exhaustion")) - } else { - (Color::Muted, None) - }; - (color, token_count, max_token_count, tooltip) - } - }; - - Some( - h_flex() - .id("token-count") - .gap_0p5() - .child( - Label::new(humanize_token_count(token_count)) - .size(LabelSize::Small) - .color(token_count_color), - ) - .child(Label::new("/").size(LabelSize::Small).color(Color::Muted)) - .child( - Label::new(humanize_token_count(max_token_count)) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .when_some(tooltip, |element, tooltip| { - element.tooltip(Tooltip::text(tooltip)) - }), - ) -} - enum PendingSlashCommand {} fn invoked_slash_command_fold_placeholder( From c72e594afeeea634739d6db90b63c129600dee7c Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Wed, 27 Aug 2025 13:08:03 +0200 Subject: [PATCH 375/744] acp: Fix model selector sometimes showing no models (#36995) Release Notes: - N/A --- crates/agent2/src/agent.rs | 51 +++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 6fa36d33d50515c49f6d64c89146072316de7689..51e1fc631625fb8f7e5b84cb2520c85ed6b3d876 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -61,16 +61,19 @@ pub struct LanguageModels { model_list: acp_thread::AgentModelList, refresh_models_rx: watch::Receiver<()>, refresh_models_tx: watch::Sender<()>, + _authenticate_all_providers_task: Task<()>, } impl LanguageModels { - fn new(cx: &App) -> Self { + fn new(cx: &mut App) -> Self { let (refresh_models_tx, refresh_models_rx) = watch::channel(()); + let mut this = Self { models: HashMap::default(), model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()), refresh_models_rx, refresh_models_tx, + _authenticate_all_providers_task: Self::authenticate_all_language_model_providers(cx), }; this.refresh_list(cx); this @@ -150,6 +153,52 @@ impl LanguageModels { fn model_id(model: &Arc) -> acp_thread::AgentModelId { acp_thread::AgentModelId(format!("{}/{}", model.provider_id().0, model.id().0).into()) } + + fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> { + let authenticate_all_providers = LanguageModelRegistry::global(cx) + .read(cx) + .providers() + .iter() + .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) + .collect::>(); + + cx.background_spawn(async move { + for (provider_id, provider_name, authenticate_task) in authenticate_all_providers { + if let Err(err) = authenticate_task.await { + if matches!(err, language_model::AuthenticateError::CredentialsNotFound) { + // Since we're authenticating these providers in the + // background for the purposes of populating the + // language selector, we don't care about providers + // where the credentials are not found. + } else { + // Some providers have noisy failure states that we + // don't want to spam the logs with every time the + // language model selector is initialized. + // + // Ideally these should have more clear failure modes + // that we know are safe to ignore here, like what we do + // with `CredentialsNotFound` above. + match provider_id.0.as_ref() { + "lmstudio" | "ollama" => { + // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". + // + // These fail noisily, so we don't log them. + } + "copilot_chat" => { + // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. + } + _ => { + log::error!( + "Failed to authenticate provider: {}: {err}", + provider_name.0 + ); + } + } + } + } + } + }) + } } pub struct NativeAgent { From d99a17e35777a63e3079bbce82a4c06f49706df9 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 27 Aug 2025 09:12:52 -0300 Subject: [PATCH 376/744] docs: Add ACP-related content (#36966) Release Notes: - N/A --------- Co-authored-by: Conrad Irwin Co-authored-by: Bennet Bo Fenner Co-authored-by: Antonio Scandurra Co-authored-by: Matt Miller --- docs/src/SUMMARY.md | 1 + docs/src/ai/agent-panel.md | 27 ++++++----- docs/src/ai/external-agents.md | 82 ++++++++++++++++++++++++++++++++++ docs/src/ai/overview.md | 2 + 4 files changed, 102 insertions(+), 10 deletions(-) create mode 100644 docs/src/ai/external-agents.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 251cad6234f10d73f423680bcd600500daae65b2..9d07881914d1f73a7333d3dc67ad1d3ca6731bc5 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -47,6 +47,7 @@ - [Overview](./ai/overview.md) - [Agent Panel](./ai/agent-panel.md) - [Tools](./ai/tools.md) + - [External Agents](./ai/external-agents.md) - [Inline Assistant](./ai/inline-assistant.md) - [Edit Prediction](./ai/edit-prediction.md) - [Text Threads](./ai/text-threads.md) diff --git a/docs/src/ai/agent-panel.md b/docs/src/ai/agent-panel.md index f944eb88b06c8be21002ff319a972ff1843de39d..13d92278a441efd165511d88550754587bf7f97e 100644 --- a/docs/src/ai/agent-panel.md +++ b/docs/src/ai/agent-panel.md @@ -1,14 +1,15 @@ # Agent Panel -The Agent Panel provides you with a surface to interact with LLMs, enabling various types of tasks, such as generating code, asking questions about your codebase, and general inquiries like emails, documentation, and more. +The Agent Panel allows you to interact with many LLMs and coding agents that can support you in various types of tasks, such as generating code, codebase understanding, and other general inquiries like writing emails, documentation, and more. To open it, use the `agent: new thread` action in [the Command Palette](../getting-started.md#command-palette) or click the ✨ (sparkles) icon in the status bar. -If you're using the Agent Panel for the first time, you need to have at least one LLM provider configured. +If you're using the Agent Panel for the first time, you need to have at least one LLM or agent provider configured. You can do that by: 1. [subscribing to our Pro plan](https://zed.dev/pricing), so you have access to our hosted models -2. or by [bringing your own API keys](./llm-providers.md#use-your-own-keys) for your desired provider +2. [bringing your own API keys](./llm-providers.md#use-your-own-keys) for your desired provider +3. using an external agent like [Gemini CLI](./external-agents.md#gemini-cli) ## Overview {#overview} @@ -17,6 +18,15 @@ If you need extra room to type, you can expand the message editor with {#kb agen You should start to see the responses stream in with indications of [which tools](./tools.md) the model is using to fulfill your prompt. +> Note that, at the moment, not all features outlined below work for external agents, like [Gemini CLI](./external-agents.md#gemini-cli)—features like _checkpoints_, _token usage display_, and _model selection_ may be supported in the future. + +### Creating New Threads + +By default, the Agent Panel uses Zed's first-party agent. + +To change that, go to the plus button in the top-right of the Agent Panel and choose another option. +You choose to create a new [Text Thread](./text-threads.md) or, if you have [external agents](/.external-agents.md) connected, you can create new threads with them. + ### Editing Messages {#editing-messages} Any message that you send to the AI is editable. @@ -30,7 +40,7 @@ The checkpoint button appears even if you interrupt the thread midway through an ### Navigating History {#navigating-history} -To quickly navigate through recently opened threads, use the {#kb agent::ToggleNavigationMenu} binding, when focused on the panel's editor, or click the menu icon button at the top left of the panel to open the dropdown that shows you the six most recent threads. +To quickly navigate through recently opened threads, use the {#kb agent::ToggleNavigationMenu} binding, when focused on the panel's editor, or click the menu icon button at the top right of the panel to open the dropdown that shows you the six most recent threads. The items in this menu function similarly to tabs, and closing them doesn’t delete the thread; instead, it simply removes them from the recent list. @@ -70,16 +80,13 @@ So, if your active tab had edits made by the AI, you'll see diffs with the same Although Zed's agent is very efficient at reading through your code base to autonomously pick up relevant files, directories, and other context, manually adding context is still encouraged as a way to speed up and improve the AI's response quality. -If you have a tab open while using the Agent Panel, that tab appears as a suggested context in form of a dashed button. -You can also add other forms of context by either mentioning them with `@` or hitting the `+` icon button. - -You can even add previous threads as context by mentioning them with `@thread`, or by selecting the "New From Summary" option from the `+` menu to continue a longer conversation, keeping it within the context window. +To add any file, directory, symbol, previous threads, rules files, or even web pages as context, type `@` to mention them in the editor. Pasting images as context is also supported by the Agent Panel. ### Token Usage {#token-usage} -Zed surfaces how many tokens you are consuming for your currently active thread in the panel's toolbar. +Zed surfaces how many tokens you are consuming for your currently active thread nearby the profile selector in the panel's message editor. Depending on how many pieces of context you add, your token consumption can grow rapidly. With that in mind, once you get close to the model's context window, a banner appears below the message editor suggesting to start a new thread with the current one summarized and added as context. @@ -145,7 +152,7 @@ Zed's UI will inform about this via a warning icon that appears close to the mod ## Text Threads {#text-threads} -["Text threads"](./text-threads.md) present your conversation with the LLM in a different format—as raw text. +["Text Threads"](./text-threads.md) present your conversation with the LLM in a different format—as raw text. With text threads, you have full control over the conversation data. You can remove and edit responses from the LLM, swap roles, and include more context earlier in the conversation. diff --git a/docs/src/ai/external-agents.md b/docs/src/ai/external-agents.md new file mode 100644 index 0000000000000000000000000000000000000000..a67aa9d8894f917f1dea4ff505355a6a9d57eff6 --- /dev/null +++ b/docs/src/ai/external-agents.md @@ -0,0 +1,82 @@ +# External Agents + +Zed supports terminal-based agentic coding tools through the [Agent Client Protocol (ACP)](https://agentclientprotocol.com). + +Currently, [Gemini CLI](https://github.com/google-gemini/gemini-cli) serves as the reference implementation, and you can [add custom ACP-compatible agents](#add-custom-agents) as well. + +## Gemini CLI {#gemini-cli} + +Zed provides the ability to run [Gemini CLI](https://github.com/google-gemini/gemini-cli) directly in the [agent panel](./agent-panel.md). + +Under the hood we run Gemini CLI in the background, and talk to it over ACP. +This means that you're running the real Gemini CLI, with all of the advantages of that, but you can see and interact with files in your editor. + +### Getting Started + +As of Zed Stable v0.201.5 you should be able to use Gemini CLI directly from Zed. First open the agent panel with {#kb agent::ToggleFocus}, and then use the `+` button in the top right to start a New Gemini CLI thread. + +If you'd like to bind this to a keyboard shortcut, you can do so by editing your keybindings file to include: + +```json +[ + { + "bindings": { + "cmd-alt-g": ["agent::NewExternalAgentThread", { "agent": "gemini" }] + } + } +] +``` + +#### Installation + +If you don't yet have Gemini CLI installed, then Zed will install a version for you. If you do, then we will use the version of Gemini CLI on your path. + +You need to be running at least Gemini version `0.2.0-preview`, and if your version of Gemini is too old you will see an +error message. + +The instructions to upgrade Gemini depend on how you originally installed it, but typically, running `npm install -g gemini-cli@preview` should work. + +#### Authentication + +After you have Gemini CLI running, you'll be prompted to choose your authentication method. + +Most users should click the "Log in with Google". This will cause a browser window to pop-up and auth directly with Gemini CLI. Zed does not see your oauth or access tokens in this case. + +You can also use the "Gemini API Key". If you select this, and have the `GEMINI_API_KEY` set, then we will use that. Otherwise Zed will prompt you for an API key which will be stored securely in your keychain, and used to start Gemini CLI from within Zed. + +The "Vertex AI" option is for those who are using Vertex AI, and have already configured their environment correctly. + +For more information, see the [Gemini CLI docs](https://github.com/google-gemini/gemini-cli/blob/main/docs/index.md). + +### Usage + +Similar to Zed's first-party agent, you can use Gemini CLI to do anything that you need. + +You can @-mention files, recent threads, symbols, or fetch the web. + +Note that some first-party agent features don't yet work with Gemini CLI: editing past messages, resuming threads from history, and checkpointing. +We hope to add these features in the near future. + +## Add Custom Agents {#add-custom-agents} + +You can run any agent speaking ACP in Zed by changing your settings as follows: + +```json +{ + "agent_servers": { + "Custom Agent": { + "command": "node", + "args": ["~/projects/agent/index.js", "--acp"], + "env": {} + } + } +} +``` + +This can also be useful if you're in the middle of developing a new agent that speaks the protocol and you want to debug it. + +## Debugging Agents + +When using external agents in Zed, you can access the debug view via with `dev: open acp logs` from the Command Palette. This lets you see the messages being sent and received between Zed and the agent. + +![The debug view for ACP logs.](https://zed.dev/img/acp/acp-logs.webp) diff --git a/docs/src/ai/overview.md b/docs/src/ai/overview.md index 6f081cb243ffcfb77b4304373df67865cc71ee10..8bd45240fdad156e11f28e5ba92289c97de92218 100644 --- a/docs/src/ai/overview.md +++ b/docs/src/ai/overview.md @@ -6,6 +6,8 @@ Learn how to get started using AI with Zed and all its capabilities. - [Configuration](./configuration.md): Learn how to set up different language model providers like Anthropic, OpenAI, Ollama, Google AI, and more. +- [External Agents](./external-agents.md): Learn how to plug in your favorite agent into Zed. + - [Subscription](./subscription.md): Learn about Zed's hosted model service and other billing-related information. - [Privacy and Security](./privacy-and-security.md): Understand how Zed handles privacy and security with AI features. From 54f9b67de2f5e943b671a19861675c2b90d06486 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 27 Aug 2025 14:51:06 +0200 Subject: [PATCH 377/744] docs: Document more settings (#36993) Within our hosted docs, we are missing documentation for quite a lot of settings - sometimes for newer settings, sometimes for settings that are more than two years old. This leads (amongst other things) to feature requests for features that are already supported, false issue reports (because people couldn't find the setting for what caused the issue within the documentation) and generally just takes time for for both these affected by the missing documentation as well as these handling the questions around it. This change here takes a stab at the problem by adding more documentation for a lot supported setting (not all of it) as well as reorganizing some settings so that some stuff can (hopefully) be found more easily. Eventually, we should find a better method for this, but it's still better than informing people for the n-th time that we e.g. have `agent_font_size` for the agent panel. Manually audited twice but I'll take another thorough look before merging. Release Notes: - N/A --- docs/src/configuring-zed.md | 1310 ++++++++++++++++++++++++++++++----- 1 file changed, 1144 insertions(+), 166 deletions(-) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index a8a46896893d4b19827f90720e2076250e5a4be3..9634ca0f6cd93db963111fa464890a3c26478cb3 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -104,6 +104,70 @@ Non-negative `float` values } ``` +## Agent Font Size + +- Description: The font size for text in the agent panel. Inherits the UI font size if unset. +- Setting: `agent_font_size` +- Default: `null` + +**Options** + +`integer` values from `6` to `100` pixels (inclusive) + +## Allow Rewrap + +- Description: Controls where the `editor::Rewrap` action is allowed in the current language scope +- Setting: `allow_rewrap` +- Default: `"in_comments"` + +**Options** + +1. Allow rewrap in comments only: + +```json +{ + "allow_rewrap": "in_comments" +} +``` + +2. Allow rewrap everywhere: + +```json +{ + "allow_rewrap": "everywhere" +} +``` + +3. Never allow rewrap: + +```json +{ + "allow_rewrap": "never" +} +``` + +Note: This setting has no effect in Vim mode, as rewrap is already allowed everywhere. + +## Auto Indent + +- Description: Whether indentation should be adjusted based on the context whilst typing. This can be specified on a per-language basis. +- Setting: `auto_indent` +- Default: `true` + +**Options** + +`boolean` values + +## Auto Indent On Paste + +- Description: Whether indentation of pasted content should be adjusted based on the context +- Setting: `auto_indent_on_paste` +- Default: `true` + +**Options** + +`boolean` values + ## Auto Install extensions - Description: Define extensions to be autoinstalled or never be installed. @@ -182,42 +246,30 @@ Define extensions which should be installed (`true`) or never installed (`false` } ``` -## Restore on Startup +## Autoscroll on Clicks -- Description: Controls session restoration on startup. -- Setting: `restore_on_startup` -- Default: `last_session` +- Description: Whether to scroll when clicking near the edge of the visible text area. +- Setting: `autoscroll_on_clicks` +- Default: `false` **Options** -1. Restore all workspaces that were open when quitting Zed: - -```json -{ - "restore_on_startup": "last_session" -} -``` +`boolean` values -2. Restore the workspace that was closed last: +## Auto Signature Help -```json -{ - "restore_on_startup": "last_workspace" -} -``` +- Description: Show method signatures in the editor, when inside parentheses +- Setting: `auto_signature_help` +- Default: `false` -3. Always start with an empty editor: +**Options** -```json -{ - "restore_on_startup": "none" -} -``` +`boolean` values -## Autoscroll on Clicks +### Show Signature Help After Edits -- Description: Whether to scroll when clicking near the edge of the visible text area. -- Setting: `autoscroll_on_clicks` +- Description: Whether to show the signature help after completion or a bracket pair inserted. If `auto_signature_help` is enabled, this setting will be treated as enabled also. +- Setting: `show_signature_help_after_edits` - Default: `false` **Options** @@ -378,6 +430,24 @@ For example, to use `Nerd Font` as a fallback, add the following to your setting `"standard"`, `"comfortable"` or `{ "custom": float }` (`1` is compact, `2` is loose) +## Centered Layout + +- Description: Configuration for the centered layout mode. +- Setting: `centered_layout` +- Default: + +```json +"centered_layout": { + "left_padding": 0.2, + "right_padding": 0.2, +} +``` + +**Options** + +The `left_padding` and `right_padding` options define the relative width of the +left and right padding of the central pane from the workspace when the centered layout mode is activated. Valid values range is from `0` to `0.4`. + ## Close on File Delete - Description: Whether to automatically close editor tabs when their corresponding files are deleted from disk. @@ -402,23 +472,63 @@ Note: Dirty files (files with unsaved changes) will not be automatically closed `boolean` values -## Centered Layout +## Diagnostics Max Severity -- Description: Configuration for the centered layout mode. -- Setting: `centered_layout` -- Default: +- Description: Which level to use to filter out diagnostics displayed in the editor +- Setting: `diagnostics_max_severity` +- Default: `null` + +**Options** + +1. Allow all diagnostics (default): ```json -"centered_layout": { - "left_padding": 0.2, - "right_padding": 0.2, +{ + "diagnostics_max_severity": null +} +``` + +2. Show only errors: + +```json +{ + "diagnostics_max_severity": "error" +} +``` + +3. Show errors and warnings: + +```json +{ + "diagnostics_max_severity": "warning" +} +``` + +4. Show errors, warnings, and information: + +```json +{ + "diagnostics_max_severity": "information" +} +``` + +5. Show all including hints: + +```json +{ + "diagnostics_max_severity": "hint" } ``` +## Disable AI + +- Description: Whether to disable all AI features in Zed +- Setting: `disable_ai` +- Default: `false` + **Options** -The `left_padding` and `right_padding` options define the relative width of the -left and right padding of the central pane from the workspace when the centered layout mode is activated. Valid values range is from `0` to `0.4`. +`boolean` values ## Direnv Integration @@ -435,6 +545,42 @@ There are two options to choose from: 1. `shell_hook`: Use the shell hook to load direnv. This relies on direnv to activate upon entering the directory. Supports POSIX shells and fish. 2. `direct`: Use `direnv export json` to load direnv. This will load direnv directly without relying on the shell hook and might cause some inconsistencies. This allows direnv to work with any shell. +## Double Click In Multibuffer + +- Description: What to do when multibuffer is double clicked in some of its excerpts (parts of singleton buffers) +- Setting: `double_click_in_multibuffer` +- Default: `"select"` + +**Options** + +1. Behave as a regular buffer and select the whole word (default): + +```json +{ + "double_click_in_multibuffer": "select" +} +``` + +2. Open the excerpt clicked as a new buffer in the new tab: + +```json +{ + "double_click_in_multibuffer": "open" +} +``` + +For the case of "open", regular selection behavior can be achieved by holding `alt` when double clicking. + +## Drop Target Size + +- Description: Relative size of the drop target in the editor that will open dropped file as a split pane (0-0.5). For example, 0.25 means if you drop onto the top/bottom quarter of the pane a new vertical split will be used, if you drop onto the left/right quarter of the pane a new horizontal split will be used. +- Setting: `drop_target_size` +- Default: `0.2` + +**Options** + +`float` values between `0` and `0.5` + ## Edit Predictions - Description: Settings for edit predictions. @@ -581,6 +727,32 @@ List of `string` values "cursor_shape": "hollow" ``` +## Gutter + +- Description: Settings for the editor gutter +- Setting: `gutter` +- Default: + +```json +{ + "gutter": { + "line_numbers": true, + "runnables": true, + "breakpoints": true, + "folds": true, + "min_line_number_digits": 4 + } +} +``` + +**Options** + +- `line_numbers`: Whether to show line numbers in the gutter +- `runnables`: Whether to show runnable buttons in the gutter +- `breakpoints`: Whether to show breakpoints in the gutter +- `folds`: Whether to show fold buttons in the gutter +- `min_line_number_digits`: Minimum number of characters to reserve space for in the gutter + ## Hide Mouse - Description: Determines when the mouse cursor should be hidden in an editor or input box. @@ -1269,6 +1441,26 @@ Each option controls displaying of a particular toolbar element. If all elements `boolean` values +## Expand Excerpt Lines + +- Description: The default number of lines to expand excerpts in the multibuffer by +- Setting: `expand_excerpt_lines` +- Default: `5` + +**Options** + +Positive `integer` values + +## Extend Comment On Newline + +- Description: Whether to start a new line with a comment when a previous line is a comment as well. +- Setting: `extend_comment_on_newline` +- Default: `true` + +**Options** + +`boolean` values + ## Status Bar - Description: Control various elements in the status bar. Note that some items in the status bar have their own settings set elsewhere. @@ -1327,6 +1519,24 @@ While other options may be changed at a runtime and should be placed under `sett } ``` +## Global LSP Settings + +- Description: Configuration for global LSP settings that apply to all language servers +- Setting: `global_lsp_settings` +- Default: + +```json +{ + "global_lsp_settings": { + "button": true + } +} +``` + +**Options** + +- `button`: Whether to show the LSP status button in the status bar + ## LSP Highlight Debounce - Description: The debounce delay in milliseconds before querying highlights from the language server based on the current cursor location. @@ -1349,6 +1559,68 @@ While other options may be changed at a runtime and should be placed under `sett `integer` values representing milliseconds +## Features + +- Description: Features that can be globally enabled or disabled +- Setting: `features` +- Default: + +```json +{ + "features": { + "edit_prediction_provider": "zed" + } +} +``` + +### Edit Prediction Provider + +- Description: Which edit prediction provider to use +- Setting: `edit_prediction_provider` +- Default: `"zed"` + +**Options** + +1. Use Zeta as the edit prediction provider: + +```json +{ + "features": { + "edit_prediction_provider": "zed" + } +} +``` + +2. Use Copilot as the edit prediction provider: + +```json +{ + "features": { + "edit_prediction_provider": "copilot" + } +} +``` + +3. Use Supermaven as the edit prediction provider: + +```json +{ + "features": { + "edit_prediction_provider": "supermaven" + } +} +``` + +4. Turn off edit predictions across all providers + +```json +{ + "features": { + "edit_prediction_provider": "none" + } +} +``` + ## Format On Save - Description: Whether or not to perform a buffer format before saving. @@ -1892,27 +2164,71 @@ Example: } ``` -## Indent Guides +## Go to Definition Fallback -- Description: Configuration related to indent guides. Indent guides can be configured separately for each language. -- Setting: `indent_guides` -- Default: +- Description: What to do when the "go to definition" action fails to find a definition +- Setting: `go_to_definition_fallback` +- Default: `"find_all_references"` + +**Options** + +1. Do nothing: ```json { - "indent_guides": { - "enabled": true, - "line_width": 1, - "active_line_width": 1, - "coloring": "fixed", - "background_coloring": "disabled" - } + "go_to_definition_fallback": "none" } ``` -**Options** - -1. Disable indent guides +2. Find references for the same symbol (default): + +```json +{ + "go_to_definition_fallback": "find_all_references" +} +``` + +## Hard Tabs + +- Description: Whether to indent lines using tab characters or multiple spaces. +- Setting: `hard_tabs` +- Default: `false` + +**Options** + +`boolean` values + +## Helix Mode + +- Description: Whether or not to enable Helix mode. Enabling `helix_mode` also enables `vim_mode`. See the [Helix documentation](./helix.md) for more details. +- Setting: `helix_mode` +- Default: `false` + +**Options** + +`boolean` values + +## Indent Guides + +- Description: Configuration related to indent guides. Indent guides can be configured separately for each language. +- Setting: `indent_guides` +- Default: + +```json +{ + "indent_guides": { + "enabled": true, + "line_width": 1, + "active_line_width": 1, + "coloring": "fixed", + "background_coloring": "disabled" + } +} +``` + +**Options** + +1. Disable indent guides ```json { @@ -1961,40 +2277,6 @@ Example: } ``` -## Hard Tabs - -- Description: Whether to indent lines using tab characters or multiple spaces. -- Setting: `hard_tabs` -- Default: `false` - -**Options** - -`boolean` values - -## Multi Cursor Modifier - -- Description: Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier. -- Setting: `multi_cursor_modifier` -- Default: `alt` - -**Options** - -1. Maps to `Alt` on Linux and Windows and to `Option` on MacOS: - -```json -{ - "multi_cursor_modifier": "alt" -} -``` - -2. Maps `Control` on Linux and Windows and to `Command` on MacOS: - -```json -{ - "multi_cursor_modifier": "cmd_or_ctrl" // alias: "cmd", "ctrl" -} -``` - ## Hover Popover Enabled - Description: Whether or not to show the informational hover box when moving the mouse over symbols in the editor. @@ -2087,6 +2369,50 @@ Run the `icon theme selector: toggle` action in the command palette to see a cur Run the `icon theme selector: toggle` action in the command palette to see a current list of valid icon themes names. +## Image Viewer + +- Description: Settings for image viewer functionality +- Setting: `image_viewer` +- Default: + +```json +{ + "image_viewer": { + "unit": "binary" + } +} +``` + +**Options** + +### Unit + +- Description: The unit for image file sizes +- Setting: `unit` +- Default: `"binary"` + +**Options** + +1. Use binary units (KiB, MiB): + +```json +{ + "image_viewer": { + "unit": "binary" + } +} +``` + +2. Use decimal units (KB, MB): + +```json +{ + "image_viewer": { + "unit": "decimal" + } +} +``` + ## Inlay hints - Description: Configuration for displaying extra text with hints in the editor. @@ -2187,6 +2513,24 @@ Unspecified values have a `false` value, hints won't be toggled if all the modif } ``` +## JSX Tag Auto Close + +- Description: Whether to automatically close JSX tags +- Setting: `jsx_tag_auto_close` +- Default: + +```json +{ + "jsx_tag_auto_close": { + "enabled": true + } +} +``` + +**Options** + +- `enabled`: Whether to enable automatic JSX tag closing + ## Languages - Description: Configuration for specific languages. @@ -2226,141 +2570,546 @@ The following settings can be overridden for each specific language: - [`use_autoclose`](#use-autoclose) - [`always_treat_brackets_as_autoclosed`](#always-treat-brackets-as-autoclosed) -These values take in the same options as the root-level settings with the same name. +These values take in the same options as the root-level settings with the same name. + +## Language Models + +- Description: Configuration for language model providers +- Setting: `language_models` +- Default: + +```json +{ + "language_models": { + "anthropic": { + "api_url": "https://api.anthropic.com" + }, + "google": { + "api_url": "https://generativelanguage.googleapis.com" + }, + "ollama": { + "api_url": "http://localhost:11434" + }, + "openai": { + "api_url": "https://api.openai.com/v1" + } + } +} +``` + +**Options** + +Configuration for various AI model providers including API URLs and authentication settings. + +## Line Indicator Format + +- Description: Format for line indicator in the status bar +- Setting: `line_indicator_format` +- Default: `"short"` + +**Options** + +1. Short format: + +```json +{ + "line_indicator_format": "short" +} +``` + +2. Long format: + +```json +{ + "line_indicator_format": "long" +} +``` + +## Linked Edits + +- Description: Whether to perform linked edits of associated ranges, if the language server supports it. For example, when editing opening `` tag, the contents of the closing `` tag will be edited as well. +- Setting: `linked_edits` +- Default: `true` + +**Options** + +`boolean` values + +## LSP Document Colors + +- Description: Whether to show document color information from the language server +- Setting: `lsp_document_colors` +- Default: `true` + +**Options** + +`boolean` values + +## Max Tabs + +- Description: Maximum number of tabs to show in the tab bar +- Setting: `max_tabs` +- Default: `null` + +**Options** + +Positive `integer` values or `null` for unlimited tabs + +## Middle Click Paste (Linux only) + +- Description: Enable middle-click paste on Linux +- Setting: `middle_click_paste` +- Default: `true` + +**Options** + +`boolean` values + +## Multi Cursor Modifier + +- Description: Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier. +- Setting: `multi_cursor_modifier` +- Default: `alt` + +**Options** + +1. Maps to `Alt` on Linux and Windows and to `Option` on MacOS: + +```json +{ + "multi_cursor_modifier": "alt" +} +``` + +2. Maps `Control` on Linux and Windows and to `Command` on MacOS: + +```json +{ + "multi_cursor_modifier": "cmd_or_ctrl" // alias: "cmd", "ctrl" +} +``` + +## Node + +- Description: Configuration for Node.js integration +- Setting: `node` +- Default: + +```json +{ + "node": { + "ignore_system_version": false, + "path": null, + "npm_path": null + } +} +``` + +**Options** + +- `ignore_system_version`: Whether to ignore the system Node.js version +- `path`: Custom path to Node.js binary +- `npm_path`: Custom path to npm binary + +## Network Proxy + +- Description: Configure a network proxy for Zed. +- Setting: `proxy` +- Default: `null` + +**Options** + +The proxy setting must contain a URL to the proxy. + +The following URI schemes are supported: + +- `http` +- `https` +- `socks4` - SOCKS4 proxy with local DNS +- `socks4a` - SOCKS4 proxy with remote DNS +- `socks5` - SOCKS5 proxy with local DNS +- `socks5h` - SOCKS5 proxy with remote DNS + +`http` will be used when no scheme is specified. + +By default no proxy will be used, or Zed will attempt to retrieve proxy settings from environment variables, such as `http_proxy`, `HTTP_PROXY`, `https_proxy`, `HTTPS_PROXY`, `all_proxy`, `ALL_PROXY`, `no_proxy` and `NO_PROXY`. + +For example, to set an `http` proxy, add the following to your settings: + +```json +{ + "proxy": "http://127.0.0.1:10809" +} +``` + +Or to set a `socks5` proxy: + +```json +{ + "proxy": "socks5h://localhost:10808" +} +``` + +If you wish to exclude certain hosts from using the proxy, set the `NO_PROXY` environment variable. This accepts a comma-separated list of hostnames, host suffixes, IPv4/IPv6 addresses or blocks that should not use the proxy. For example if your environment included `NO_PROXY="google.com, 192.168.1.0/24"` all hosts in `192.168.1.*`, `google.com` and `*.google.com` would bypass the proxy. See [reqwest NoProxy docs](https://docs.rs/reqwest/latest/reqwest/struct.NoProxy.html#method.from_string) for more. + +## On Last Window Closed + +- Description: What to do when the last window is closed +- Setting: `on_last_window_closed` +- Default: `"platform_default"` + +**Options** + +1. Use platform default behavior: + +```json +{ + "on_last_window_closed": "platform_default" +} +``` + +2. Always quit the application: + +```json +{ + "on_last_window_closed": "quit_app" +} +``` + +## Profiles + +- Description: Configuration profiles that can be applied on top of existing settings +- Setting: `profiles` +- Default: `{}` + +**Options** + +Configuration object for defining settings profiles. Example: + +```json +{ + "profiles": { + "presentation": { + "buffer_font_size": 20, + "ui_font_size": 18, + "theme": "One Light" + } + } +} +``` + +## Preview tabs + +- Description: + Preview tabs allow you to open files in preview mode, where they close automatically when you switch to another file unless you explicitly pin them. This is useful for quickly viewing files without cluttering your workspace. Preview tabs display their file names in italics. \ + There are several ways to convert a preview tab into a regular tab: + + - Double-clicking on the file + - Double-clicking on the tab header + - Using the `project_panel::OpenPermanent` action + - Editing the file + - Dragging the file to a different pane + +- Setting: `preview_tabs` +- Default: + +```json +"preview_tabs": { + "enabled": true, + "enable_preview_from_file_finder": false, + "enable_preview_from_code_navigation": false, +} +``` + +### Enable preview from file finder + +- Description: Determines whether to open files in preview mode when selected from the file finder. +- Setting: `enable_preview_from_file_finder` +- Default: `false` + +**Options** + +`boolean` values + +### Enable preview from code navigation + +- Description: Determines whether a preview tab gets replaced when code navigation is used to navigate away from the tab. +- Setting: `enable_preview_from_code_navigation` +- Default: `false` + +**Options** + +`boolean` values + +## File Finder + +### File Icons + +- Description: Whether to show file icons in the file finder. +- Setting: `file_icons` +- Default: `true` + +### Modal Max Width + +- Description: Max-width of the file finder modal. It can take one of these values: `small`, `medium`, `large`, `xlarge`, and `full`. +- Setting: `modal_max_width` +- Default: `small` + +### Skip Focus For Active In Search + +- Description: Determines whether the file finder should skip focus for the active file in search results. +- Setting: `skip_focus_for_active_in_search` +- Default: `true` + +## Pane Split Direction Horizontal + +- Description: The direction that you want to split panes horizontally +- Setting: `pane_split_direction_horizontal` +- Default: `"up"` + +**Options** + +1. Split upward: + +```json +{ + "pane_split_direction_horizontal": "up" +} +``` + +2. Split downward: + +```json +{ + "pane_split_direction_horizontal": "down" +} +``` + +## Pane Split Direction Vertical + +- Description: The direction that you want to split panes vertically +- Setting: `pane_split_direction_vertical` +- Default: `"left"` + +**Options** + +1. Split to the left: + +```json +{ + "pane_split_direction_vertical": "left" +} +``` + +2. Split to the right: + +```json +{ + "pane_split_direction_vertical": "right" +} +``` + +## Preferred Line Length + +- Description: The column at which to soft-wrap lines, for buffers where soft-wrap is enabled. +- Setting: `preferred_line_length` +- Default: `80` + +**Options** + +`integer` values + +## Private Files + +- Description: Globs to match against file paths to determine if a file is private +- Setting: `private_files` +- Default: `["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"]` + +**Options** + +List of `string` glob patterns + +## Projects Online By Default + +- Description: Whether or not to show the online projects view by default. +- Setting: `projects_online_by_default` +- Default: `true` + +**Options** + +`boolean` values + +## Read SSH Config + +- Description: Whether to read SSH configuration files +- Setting: `read_ssh_config` +- Default: `true` + +**Options** + +`boolean` values + +## Redact Private Values + +- Description: Hide the values of variables from visual display in private files +- Setting: `redact_private_values` +- Default: `false` + +**Options** + +`boolean` values + +## Relative Line Numbers + +- Description: Whether to show relative line numbers in the gutter +- Setting: `relative_line_numbers` +- Default: `false` + +**Options** + +`boolean` values + +## Remove Trailing Whitespace On Save + +- Description: Whether or not to remove any trailing whitespace from lines of a buffer before saving it. +- Setting: `remove_trailing_whitespace_on_save` +- Default: `true` + +**Options** -## Network Proxy +`boolean` values -- Description: Configure a network proxy for Zed. -- Setting: `proxy` -- Default: `null` +## Resize All Panels In Dock + +- Description: Whether to resize all the panels in a dock when resizing the dock. Can be a combination of "left", "right" and "bottom". +- Setting: `resize_all_panels_in_dock` +- Default: `["left"]` **Options** -The proxy setting must contain a URL to the proxy. +List of strings containing any combination of: -The following URI schemes are supported: +- `"left"`: Resize left dock panels together +- `"right"`: Resize right dock panels together +- `"bottom"`: Resize bottom dock panels together -- `http` -- `https` -- `socks4` - SOCKS4 proxy with local DNS -- `socks4a` - SOCKS4 proxy with remote DNS -- `socks5` - SOCKS5 proxy with local DNS -- `socks5h` - SOCKS5 proxy with remote DNS +## Restore on File Reopen -`http` will be used when no scheme is specified. +- Description: Whether to attempt to restore previous file's state when opening it again. The state is stored per pane. +- Setting: `restore_on_file_reopen` +- Default: `true` -By default no proxy will be used, or Zed will attempt to retrieve proxy settings from environment variables, such as `http_proxy`, `HTTP_PROXY`, `https_proxy`, `HTTPS_PROXY`, `all_proxy`, `ALL_PROXY`, `no_proxy` and `NO_PROXY`. +**Options** -For example, to set an `http` proxy, add the following to your settings: +`boolean` values + +## Restore on Startup + +- Description: Controls session restoration on startup. +- Setting: `restore_on_startup` +- Default: `last_session` + +**Options** + +1. Restore all workspaces that were open when quitting Zed: ```json { - "proxy": "http://127.0.0.1:10809" + "restore_on_startup": "last_session" } ``` -Or to set a `socks5` proxy: +2. Restore the workspace that was closed last: ```json { - "proxy": "socks5h://localhost:10808" + "restore_on_startup": "last_workspace" } ``` -If you wish to exclude certain hosts from using the proxy, set the `NO_PROXY` environment variable. This accepts a comma-separated list of hostnames, host suffixes, IPv4/IPv6 addresses or blocks that should not use the proxy. For example if your environment included `NO_PROXY="google.com, 192.168.1.0/24"` all hosts in `192.168.1.*`, `google.com` and `*.google.com` would bypass the proxy. See [reqwest NoProxy docs](https://docs.rs/reqwest/latest/reqwest/struct.NoProxy.html#method.from_string) for more. - -## Preview tabs - -- Description: - Preview tabs allow you to open files in preview mode, where they close automatically when you switch to another file unless you explicitly pin them. This is useful for quickly viewing files without cluttering your workspace. Preview tabs display their file names in italics. \ - There are several ways to convert a preview tab into a regular tab: - - - Double-clicking on the file - - Double-clicking on the tab header - - Using the `project_panel::OpenPermanent` action - - Editing the file - - Dragging the file to a different pane - -- Setting: `preview_tabs` -- Default: +3. Always start with an empty editor: ```json -"preview_tabs": { - "enabled": true, - "enable_preview_from_file_finder": false, - "enable_preview_from_code_navigation": false, +{ + "restore_on_startup": "none" } ``` -### Enable preview from file finder +## Scroll Beyond Last Line -- Description: Determines whether to open files in preview mode when selected from the file finder. -- Setting: `enable_preview_from_file_finder` -- Default: `false` +- Description: Whether the editor will scroll beyond the last line +- Setting: `scroll_beyond_last_line` +- Default: `"one_page"` **Options** -`boolean` values +1. Scroll one page beyond the last line by one page: -### Enable preview from code navigation +```json +{ + "scroll_beyond_last_line": "one_page" +} +``` -- Description: Determines whether a preview tab gets replaced when code navigation is used to navigate away from the tab. -- Setting: `enable_preview_from_code_navigation` -- Default: `false` +2. The editor will scroll beyond the last line by the same amount of lines as `vertical_scroll_margin`: -**Options** +```json +{ + "scroll_beyond_last_line": "vertical_scroll_margin" +} +``` -`boolean` values +3. The editor will not scroll beyond the last line: -## File Finder +```json +{ + "scroll_beyond_last_line": "off" +} +``` -### File Icons +**Options** -- Description: Whether to show file icons in the file finder. -- Setting: `file_icons` -- Default: `true` +`boolean` values -### Modal Max Width +## Scroll Sensitivity -- Description: Max-width of the file finder modal. It can take one of these values: `small`, `medium`, `large`, `xlarge`, and `full`. -- Setting: `modal_max_width` -- Default: `small` +- Description: Scroll sensitivity multiplier. This multiplier is applied to both the horizontal and vertical delta values while scrolling. +- Setting: `scroll_sensitivity` +- Default: `1.0` -### Skip Focus For Active In Search +**Options** -- Description: Determines whether the file finder should skip focus for the active file in search results. -- Setting: `skip_focus_for_active_in_search` -- Default: `true` +Positive `float` values -## Preferred Line Length +### Fast Scroll Sensitivity -- Description: The column at which to soft-wrap lines, for buffers where soft-wrap is enabled. -- Setting: `preferred_line_length` -- Default: `80` +- Description: Scroll sensitivity multiplier for fast scrolling. This multiplier is applied to both the horizontal and vertical delta values while scrolling. Fast scrolling happens when a user holds the alt or option key while scrolling. +- Setting: `fast_scroll_sensitivity` +- Default: `4.0` **Options** -`integer` values +Positive `float` values -## Projects Online By Default +### Horizontal Scroll Margin -- Description: Whether or not to show the online projects view by default. -- Setting: `projects_online_by_default` -- Default: `true` +- Description: The number of characters to keep on either side when scrolling with the mouse +- Setting: `horizontal_scroll_margin` +- Default: `5` **Options** -`boolean` values +Non-negative `integer` values -## Remove Trailing Whitespace On Save +### Vertical Scroll Margin -- Description: Whether or not to remove any trailing whitespace from lines of a buffer before saving it. -- Setting: `remove_trailing_whitespace_on_save` -- Default: `true` +- Description: The number of lines to keep above/below the cursor when scrolling with the keyboard +- Setting: `vertical_scroll_margin` +- Default: `3` **Options** -`boolean` values +Non-negative `integer` values ## Search @@ -2377,6 +3126,12 @@ If you wish to exclude certain hosts from using the proxy, set the `NO_PROXY` en }, ``` +## Search Wrap + +- Description: If `search_wrap` is disabled, search result do not wrap around the end of the file +- Setting: `search_wrap` +- Default: `true` + ## Seed Search Query From Cursor - Description: When to populate a new search's query based on the text under the cursor. @@ -2546,6 +3301,56 @@ Positive integer values 4. `preferred_line_length` to wrap lines that overflow `preferred_line_length` config value 5. `bounded` to wrap lines at the minimum of `editor_width` and `preferred_line_length` +## Show Wrap Guides + +- Description: Whether to show wrap guides (vertical rulers) in the editor. Setting this to true will show a guide at the 'preferred_line_length' value if 'soft_wrap' is set to 'preferred_line_length', and will show any additional guides as specified by the 'wrap_guides' setting. +- Setting: `show_wrap_guides` +- Default: `true` + +**Options** + +`boolean` values + +## Use On Type Format + +- Description: Whether to use additional LSP queries to format (and amend) the code after every "trigger" symbol input, defined by LSP server capabilities +- Setting: `use_on_type_format` +- Default: `true` + +**Options** + +`boolean` values + +## Use Auto Surround + +- Description: Whether to automatically surround selected text when typing opening parenthesis, bracket, brace, single or double quote characters. For example, when you select text and type (, Zed will surround the text with (). +- Setting: `use_auto_surround` +- Default: `true` + +**Options** + +`boolean` values + +## Use System Path Prompts + +- Description: Whether to use the system provided dialogs for Open and Save As. When set to false, Zed will use the built-in keyboard-first pickers. +- Setting: `use_system_path_prompts` +- Default: `true` + +**Options** + +`boolean` values + +## Use System Prompts + +- Description: Whether to use the system provided dialogs for prompts, such as confirmation prompts. When set to false, Zed will use its built-in prompts. Note that on Linux, this option is ignored and Zed will always use the built-in prompts. +- Setting: `use_system_prompts` +- Default: `true` + +**Options** + +`boolean` values + ## Wrap Guides (Vertical Rulers) - Description: Where to display vertical rulers as wrap-guides. Disable by setting `show_wrap_guides` to `false`. @@ -2566,6 +3371,28 @@ List of `integer` column numbers `integer` values +## Tasks + +- Description: Configuration for tasks that can be run within Zed +- Setting: `tasks` +- Default: + +```json +{ + "tasks": { + "variables": {}, + "enabled": true, + "prefer_lsp": false + } +} +``` + +**Options** + +- `variables`: Custom variables for task configuration +- `enabled`: Whether tasks are enabled +- `prefer_lsp`: Whether to prefer LSP-provided tasks over Zed language extension ones + ## Telemetry - Description: Control what info is collected by Zed. @@ -3212,17 +4039,71 @@ Run the `theme selector: toggle` action in the command palette to see a current Run the `theme selector: toggle` action in the command palette to see a current list of valid themes names. +## Title Bar + +- Description: Whether or not to show various elements in the title bar +- Setting: `title_bar` +- Default: + +```json +"title_bar": { + "show_branch_icon": false, + "show_branch_name": true, + "show_project_items": true, + "show_onboarding_banner": true, + "show_user_picture": true, + "show_sign_in": true, + "show_menus": false +} +``` + +**Options** + +- `show_branch_icon`: Whether to show the branch icon beside branch switcher in the titlebar +- `show_branch_name`: Whether to show the branch name button in the titlebar +- `show_project_items`: Whether to show the project host and name in the titlebar +- `show_onboarding_banner`: Whether to show onboarding banners in the titlebar +- `show_user_picture`: Whether to show user picture in the titlebar +- `show_sign_in`: Whether to show the sign in button in the titlebar +- `show_menus`: Whether to show the menus in the titlebar + ## Vim -- Description: Whether or not to enable vim mode. See the [Vim documentation](./vim.md) for more details on configuration. +- Description: Whether or not to enable vim mode. - Setting: `vim_mode` - Default: `false` -## Helix Mode +## When Closing With No Tabs -- Description: Whether or not to enable Helix mode. Enabling `helix_mode` also enables `vim_mode`. See the [Helix documentation](./helix.md) for more details. -- Setting: `helix_mode` -- Default: `false` +- Description: Whether the window should be closed when using 'close active item' on a window with no tabs +- Setting: `when_closing_with_no_tabs` +- Default: `"platform_default"` + +**Options** + +1. Use platform default behavior: + +```json +{ + "when_closing_with_no_tabs": "platform_default" +} +``` + +2. Always close the window: + +```json +{ + "when_closing_with_no_tabs": "close_window" +} +``` + +3. Never close the window: + +```json +{ + "when_closing_with_no_tabs": "keep_window_open" +} +``` ## Project Panel @@ -3466,6 +4347,103 @@ Run the `theme selector: toggle` action in the command palette to see a current Visit [the Configuration page](./ai/configuration.md) under the AI section to learn more about all the agent-related settings. +## Collaboration Panel + +- Description: Customizations for the collaboration panel. +- Setting: `collaboration_panel` +- Default: + +```json +{ + "collaboration_panel": { + "button": true, + "dock": "left", + "default_width": 240 + } +} +``` + +**Options** + +- `button`: Whether to show the collaboration panel button in the status bar +- `dock`: Where to dock the collaboration panel. Can be `left` or `right` +- `default_width`: Default width of the collaboration panel + +## Chat Panel + +- Description: Customizations for the chat panel. +- Setting: `chat_panel` +- Default: + +```json +{ + "chat_panel": { + "button": "when_in_call", + "dock": "right", + "default_width": 240 + } +} +``` + +**Options** + +- `button`: When to show the chat panel button in the status bar. Can be `never`, `always`, or `when_in_call`. +- `dock`: Where to dock the chat panel. Can be 'left' or 'right' +- `default_width`: Default width of the chat panel + +## Debugger + +- Description: Configuration for debugger panel and settings +- Setting: `debugger` +- Default: + +```json +{ + "debugger": { + "stepping_granularity": "line", + "save_breakpoints": true, + "dock": "bottom", + "button": true + } +} +``` + +See the [debugger page](./debugger.md) for more information about debugging support within Zed. + +## Git Panel + +- Description: Setting to customize the behavior of the git panel. +- Setting: `git_panel` +- Default: + +```json +{ + "git_panel": { + "button": true, + "dock": "left", + "default_width": 360, + "status_style": "icon", + "fallback_branch_name": "main", + "sort_by_path": false, + "collapse_untracked_diff": false, + "scrollbar": { + "show": null + } + } +} +``` + +**Options** + +- `button`: Whether to show the git panel button in the status bar +- `dock`: Where to dock the git panel. Can be `left` or `right` +- `default_width`: Default width of the git panel +- `status_style`: How to display git status. Can be `label_color` or `icon` +- `fallback_branch_name`: What branch name to use if `init.defaultBranch` is not set +- `sort_by_path`: Whether to sort entries in the panel by path or by status (the default) +- `collapse_untracked_diff`: Whether to collapse untracked files in the diff panel +- `scrollbar`: When to show the scrollbar in the git panel + ## Outline Panel - Description: Customize outline Panel From 8cf663011f8aa1f8e1f75a7850945222e4bc0299 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Wed, 27 Aug 2025 14:53:07 +0200 Subject: [PATCH 378/744] acp: Add more logs to model selector to diagnose issue (#36997) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra Co-authored-by: Cole Miller Co-authored-by: Joseph T. Lyons Co-authored-by: Katie Geer --- crates/acp_thread/src/connection.rs | 7 +++++++ crates/agent_ui/src/acp/model_selector.rs | 9 ++++++++- crates/language_model/src/registry.rs | 1 + 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index af229b7545651c2f19f361afc7ea0abadcb5cc76..e4ab4c6ec5423e6b4b2abddb3a3cb3ac742282df 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -232,6 +232,13 @@ impl AgentModelList { AgentModelList::Grouped(groups) => groups.is_empty(), } } + + pub fn len(&self) -> usize { + match self { + AgentModelList::Flat(models) => models.len(), + AgentModelList::Grouped(groups) => groups.values().len(), + } + } } #[cfg(feature = "test-support")] diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs index 77c88c461d6e6fadcefd8eb7319bbbe2ff05fef4..052277d3e6ee8533d6e560a00211ea46bc2ccb71 100644 --- a/crates/agent_ui/src/acp/model_selector.rs +++ b/crates/agent_ui/src/acp/model_selector.rs @@ -71,7 +71,7 @@ impl AcpModelPickerDelegate { let (models, selected_model) = futures::join!(models_task, selected_model_task); this.update_in(cx, |this, window, cx| { - this.delegate.models = models.ok(); + this.delegate.models = models.log_err(); this.delegate.selected_model = selected_model.ok(); this.delegate.update_matches(this.query(cx), window, cx) })? @@ -144,6 +144,11 @@ impl PickerDelegate for AcpModelPickerDelegate { cx.spawn_in(window, async move |this, cx| { let filtered_models = match this .read_with(cx, |this, cx| { + if let Some(models) = this.delegate.models.as_ref() { + log::debug!("Filtering {} models.", models.len()); + } else { + log::debug!("No models available."); + } this.delegate.models.clone().map(move |models| { fuzzy_search(models, query, cx.background_executor().clone()) }) @@ -155,6 +160,8 @@ impl PickerDelegate for AcpModelPickerDelegate { None => AgentModelList::Flat(vec![]), }; + log::debug!("Filtered models. {} available.", filtered_models.len()); + this.update_in(cx, |this, window, cx| { this.delegate.filtered_entries = info_list_to_picker_entries(filtered_models).collect(); diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 531c3615dc8c5c082ef6fced950d33621b78dac0..bab258bca1728ac45f5ef5c0397149b93f0d6031 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -208,6 +208,7 @@ impl LanguageModelRegistry { ) -> impl Iterator> + 'a { self.providers .values() + .filter(|provider| provider.is_authenticated(cx)) .flat_map(|provider| provider.provided_models(cx)) } From 1b9c471204cc5b9818347d6d061f94a6b53b35d7 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 27 Aug 2025 10:51:26 -0400 Subject: [PATCH 379/744] Fix 'Edit in Debug.json' in debugger::Start modal (#37002) Closes https://github.com/zed-industries/zed/issues/36992 Release Notes: - N/A --- crates/debugger_ui/src/new_process_modal.rs | 30 +++++++++++++++------ 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index b30e3995ffdb994fdb9c936821b360ef7e6eff04..68770bc8b15fbf95824de167dbc8d7fada2b5075 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -1383,14 +1383,28 @@ impl PickerDelegate for DebugDelegate { .border_color(cx.theme().colors().border_variant) .children({ let action = menu::SecondaryConfirm.boxed_clone(); - KeyBinding::for_action(&*action, window, cx).map(|keybind| { - Button::new("edit-debug-task", "Edit in debug.json") - .label_size(LabelSize::Small) - .key_binding(keybind) - .on_click(move |_, window, cx| { - window.dispatch_action(action.boxed_clone(), cx) - }) - }) + if self.matches.is_empty() { + Some( + Button::new("edit-debug-json", "Edit debug.json") + .label_size(LabelSize::Small) + .on_click(cx.listener(|_picker, _, window, cx| { + window.dispatch_action( + zed_actions::OpenProjectDebugTasks.boxed_clone(), + cx, + ); + cx.emit(DismissEvent); + })), + ) + } else { + KeyBinding::for_action(&*action, window, cx).map(|keybind| { + Button::new("edit-debug-task", "Edit in debug.json") + .label_size(LabelSize::Small) + .key_binding(keybind) + .on_click(move |_, window, cx| { + window.dispatch_action(action.boxed_clone(), cx) + }) + }) + } }) .map(|this| { if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() { From 5d7f12ce880b0878353b40206682e30a4ec27405 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 27 Aug 2025 21:01:36 +0530 Subject: [PATCH 380/744] project: Add dynamic capabilities registration for "workspace/didChangeWorkspaceFolders" (#37005) Fixes missing capability registration for "workspace/didChangeWorkspaceFolders". ``` WARN [project::lsp_store] unhandled capability registration: Registration { id: "e288546c-4458-401a-a029-bbba759d5a71", method: "workspace/didChangeWorkspaceFolders", register_options: Some(Object {}) } ``` We already correctly send back events to server on workspace add and remove by checking this capability. https://github.com/zed-industries/zed/blob/cf89691b85e4652093548c0bf8b79d881e26562b/crates/lsp/src/lsp.rs#L1353 https://github.com/zed-industries/zed/blob/cf89691b85e4652093548c0bf8b79d881e26562b/crates/lsp/src/lsp.rs#L1388 Release Notes: - N/A --- crates/lsp/src/lsp.rs | 3 ++- crates/project/src/lsp_store.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 942225d09837c206f54aa324f9b58ec214f92ba2..1ad89db017bc9a0c6f9009cba8ad22f94a31c65d 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1383,7 +1383,8 @@ impl LanguageServer { self.notify::(¶ms).ok(); } } - /// Add new workspace folder to the list. + + /// Remove existing workspace folder from the list. pub fn remove_workspace_folder(&self, uri: Url) { if self .capabilities() diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index b92d739360e55bff2a156a8e7d0dcd3b58aa1300..ad9d0abf405b18f9048030621e960251057588de 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11702,6 +11702,20 @@ impl LspStore { "workspace/didChangeConfiguration" => { // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. } + "workspace/didChangeWorkspaceFolders" => { + // In this case register options is an empty object, we can ignore it + let caps = lsp::WorkspaceFoldersServerCapabilities { + supported: Some(true), + change_notifications: Some(OneOf::Right(reg.id)), + }; + server.update_capabilities(|capabilities| { + capabilities + .workspace + .get_or_insert_default() + .workspace_folders = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } "workspace/symbol" => { let options = parse_register_capabilities(reg)?; server.update_capabilities(|capabilities| { @@ -11944,6 +11958,18 @@ impl LspStore { "workspace/didChangeConfiguration" => { // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. } + "workspace/didChangeWorkspaceFolders" => { + server.update_capabilities(|capabilities| { + capabilities + .workspace + .get_or_insert_with(|| lsp::WorkspaceServerCapabilities { + workspace_folders: None, + file_operations: None, + }) + .workspace_folders = None; + }); + notify_server_capabilities_updated(&server, cx); + } "workspace/symbol" => { server.update_capabilities(|capabilities| { capabilities.workspace_symbol_provider = None From b5e9b65e8ccc2d31f6a59e0b1a971fe391396926 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Wed, 27 Aug 2025 17:39:39 +0200 Subject: [PATCH 381/744] acp: Fix model selector sometimes showing no models (#37006) Release Notes: - acp: Fix an issue where the model selector would sometimes be empty --------- Co-authored-by: Antonio Scandurra --- crates/acp_thread/src/connection.rs | 7 ------- crates/agent_ui/src/acp/model_selector.rs | 16 +++------------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index e4ab4c6ec5423e6b4b2abddb3a3cb3ac742282df..af229b7545651c2f19f361afc7ea0abadcb5cc76 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -232,13 +232,6 @@ impl AgentModelList { AgentModelList::Grouped(groups) => groups.is_empty(), } } - - pub fn len(&self) -> usize { - match self { - AgentModelList::Flat(models) => models.len(), - AgentModelList::Grouped(groups) => groups.values().len(), - } - } } #[cfg(feature = "test-support")] diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs index 052277d3e6ee8533d6e560a00211ea46bc2ccb71..cbb513696d88bbfcd95e15e051fc69322fd11281 100644 --- a/crates/agent_ui/src/acp/model_selector.rs +++ b/crates/agent_ui/src/acp/model_selector.rs @@ -71,13 +71,10 @@ impl AcpModelPickerDelegate { let (models, selected_model) = futures::join!(models_task, selected_model_task); this.update_in(cx, |this, window, cx| { - this.delegate.models = models.log_err(); + this.delegate.models = models.ok(); this.delegate.selected_model = selected_model.ok(); - this.delegate.update_matches(this.query(cx), window, cx) - })? - .await; - - Ok(()) + this.refresh(window, cx) + }) } refresh(&this, &session_id, cx).await.log_err(); @@ -144,11 +141,6 @@ impl PickerDelegate for AcpModelPickerDelegate { cx.spawn_in(window, async move |this, cx| { let filtered_models = match this .read_with(cx, |this, cx| { - if let Some(models) = this.delegate.models.as_ref() { - log::debug!("Filtering {} models.", models.len()); - } else { - log::debug!("No models available."); - } this.delegate.models.clone().map(move |models| { fuzzy_search(models, query, cx.background_executor().clone()) }) @@ -160,8 +152,6 @@ impl PickerDelegate for AcpModelPickerDelegate { None => AgentModelList::Flat(vec![]), }; - log::debug!("Filtered models. {} available.", filtered_models.len()); - this.update_in(cx, |this, window, cx| { this.delegate.filtered_entries = info_list_to_picker_entries(filtered_models).collect(); From 07373d15ef8e095571c5640be6e0eafb0ced02b7 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 27 Aug 2025 12:21:28 -0400 Subject: [PATCH 382/744] acp: Fix gemini process being leaked (#37012) Release Notes: - acp: Fixed a bug that caused external agent server subprocesses to be leaked. --------- Co-authored-by: Agus Zubiaga Co-authored-by: Bennet Bo Fenner Co-authored-by: Antonio Scandurra --- crates/agent_servers/src/acp.rs | 15 +++++++++------ crates/agent_ui/src/acp/thread_view.rs | 3 ++- crates/tab_switcher/src/tab_switcher.rs | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index b4f82a0a238ec65116688a90986f8792d78e05ed..bca47101d6a644a6804acea57ea6d5e887b8bac6 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -30,6 +30,8 @@ pub struct AcpConnection { auth_methods: Vec, prompt_capabilities: acp::PromptCapabilities, _io_task: Task>, + _wait_task: Task>, + _stderr_task: Task>, } pub struct AcpSession { @@ -86,7 +88,7 @@ impl AcpConnection { let io_task = cx.background_spawn(io_task); - cx.background_spawn(async move { + let stderr_task = cx.background_spawn(async move { let mut stderr = BufReader::new(stderr); let mut line = String::new(); while let Ok(n) = stderr.read_line(&mut line).await @@ -95,10 +97,10 @@ impl AcpConnection { log::warn!("agent stderr: {}", &line); line.clear(); } - }) - .detach(); + Ok(()) + }); - cx.spawn({ + let wait_task = cx.spawn({ let sessions = sessions.clone(); async move |cx| { let status = child.status().await?; @@ -114,8 +116,7 @@ impl AcpConnection { anyhow::Ok(()) } - }) - .detach(); + }); let connection = Rc::new(connection); @@ -148,6 +149,8 @@ impl AcpConnection { sessions, prompt_capabilities: response.agent_capabilities.prompt_capabilities, _io_task: io_task, + _wait_task: wait_task, + _stderr_task: stderr_task, }) } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 70d32088c549955a82323ac13d068943d65e6853..94b385c04e933fcdf2906db584319ee50702df3a 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -905,9 +905,10 @@ impl AcpThreadView { self.editing_message.take(); self.thread_feedback.clear(); - let Some(thread) = self.thread().cloned() else { + let Some(thread) = self.thread() else { return; }; + let thread = thread.downgrade(); if self.should_be_following { self.workspace .update(cx, |workspace, cx| { diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index bf3ce7b568f9388fee387caa654cbb9072df97b3..5f60bc03f2a74f06680007d26edf63168b9c256e 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -268,7 +268,7 @@ impl TabMatch { .flatten(); let colored_icon = icon.color(git_status_color.unwrap_or_default()); - let most_sever_diagostic_level = if show_diagnostics == ShowDiagnostics::Off { + let most_severe_diagnostic_level = if show_diagnostics == ShowDiagnostics::Off { None } else { let buffer_store = project.read(cx).buffer_store().read(cx); @@ -287,7 +287,7 @@ impl TabMatch { }; let decorations = - entry_diagnostic_aware_icon_decoration_and_color(most_sever_diagostic_level) + entry_diagnostic_aware_icon_decoration_and_color(most_severe_diagnostic_level) .filter(|(d, _)| { *d != IconDecorationKind::Triangle || show_diagnostics != ShowDiagnostics::Errors From fead511df9d332533385aed82bddb7988896262b Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:29:30 +0530 Subject: [PATCH 383/744] docs: Update Gemini CLI version requirements and install instructions (#37008) Gemini cli - 0.2.0 is no longer in preview it's the latest version and released as of today. Release Notes: - N/A Signed-off-by: Umesh Yadav --- docs/src/ai/external-agents.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/ai/external-agents.md b/docs/src/ai/external-agents.md index a67aa9d8894f917f1dea4ff505355a6a9d57eff6..1d11ca03116181e2d343d0945e53b079afe60897 100644 --- a/docs/src/ai/external-agents.md +++ b/docs/src/ai/external-agents.md @@ -31,10 +31,10 @@ If you'd like to bind this to a keyboard shortcut, you can do so by editing your If you don't yet have Gemini CLI installed, then Zed will install a version for you. If you do, then we will use the version of Gemini CLI on your path. -You need to be running at least Gemini version `0.2.0-preview`, and if your version of Gemini is too old you will see an +You need to be running at least Gemini version `0.2.0`, and if your version of Gemini is too old you will see an error message. -The instructions to upgrade Gemini depend on how you originally installed it, but typically, running `npm install -g gemini-cli@preview` should work. +The instructions to upgrade Gemini depend on how you originally installed it, but typically, running `npm install -g @google/gemini-cli@latest` should work. #### Authentication From 45ff22f793c5e6e243f05ce3785d7e771f955b2f Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Wed, 27 Aug 2025 10:17:34 -0700 Subject: [PATCH 384/744] Add bang to word chars for wrapping (#37019) Fixes #37010 Release Notes: - N/A --- crates/gpui/src/text_system/line_wrapper.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index 93ec6c854c31d3f312006f61d6994a4eee4b88ef..b1e1ee44593a4a48c1b1e54c57bb96f7b0430007 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -181,7 +181,7 @@ impl LineWrapper { matches!(c, '\u{0400}'..='\u{04FF}') || // Some other known special characters that should be treated as word characters, // e.g. `a-b`, `var_name`, `I'm`, '@mention`, `#hashtag`, `100%`, `3.1415`, `2^3`, `a~b`, etc. - matches!(c, '-' | '_' | '.' | '\'' | '$' | '%' | '@' | '#' | '^' | '~' | ',') || + matches!(c, '-' | '_' | '.' | '\'' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '!') || // Characters that used in URL, e.g. `https://github.com/zed-industries/zed?a=1&b=2` for better wrapping a long URL. matches!(c, '/' | ':' | '?' | '&' | '=') || // `⋯` character is special used in Zed, to keep this at the end of the line. From 9ca4fb16b243328ff7fcea2e14e4f0f870413df7 Mon Sep 17 00:00:00 2001 From: Floyd Wang Date: Thu, 28 Aug 2025 01:26:57 +0800 Subject: [PATCH 385/744] gpui: Support disabling window resizing and minimizing (#36859) Add support to disable both window resizing and minimizing. | | macOS | Windows | | - | - | - | | **Unresizable** | SCR-20250822-qpea | 2025-08-22 110757 | | **Unminimizable** | SCR-20250822-qpfl | 2025-08-22 110814 | Release Notes: - N/A --- crates/agent_ui/src/ui/agent_notification.rs | 1 + crates/collab_ui/src/collab_ui.rs | 1 + crates/gpui/examples/window.rs | 30 ++++++++++++++++++++ crates/gpui/examples/window_positioning.rs | 1 + crates/gpui/src/platform.rs | 16 +++++++++++ crates/gpui/src/platform/mac/window.rs | 16 ++++++++--- crates/gpui/src/platform/windows/window.rs | 15 +++++++--- crates/gpui/src/window.rs | 4 +++ crates/zed/src/zed.rs | 1 + 9 files changed, 77 insertions(+), 8 deletions(-) diff --git a/crates/agent_ui/src/ui/agent_notification.rs b/crates/agent_ui/src/ui/agent_notification.rs index 68480c047f9cab4cd72f1998422bc727993e1f5e..b2342a87b5be315dac28212a1ec73d0c054932c3 100644 --- a/crates/agent_ui/src/ui/agent_notification.rs +++ b/crates/agent_ui/src/ui/agent_notification.rs @@ -62,6 +62,7 @@ impl AgentNotification { app_id: Some(app_id.to_owned()), window_min_size: None, window_decorations: Some(WindowDecorations::Client), + ..Default::default() } } } diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index f9a2fa492562a89f66459510b1c4aa99edf57080..a49e38a8dd96a2e4883ce054477749b5c9f4eb7e 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -66,5 +66,6 @@ fn notification_window_options( app_id: Some(app_id.to_owned()), window_min_size: None, window_decorations: Some(WindowDecorations::Client), + ..Default::default() } } diff --git a/crates/gpui/examples/window.rs b/crates/gpui/examples/window.rs index 30f3697b223d6d85a9db573eb3659e9689af60a5..4445f24e4ec0f2809109964fd34610cad1299e90 100644 --- a/crates/gpui/examples/window.rs +++ b/crates/gpui/examples/window.rs @@ -152,6 +152,36 @@ impl Render for WindowDemo { ) .unwrap(); })) + .child(button("Unresizable", move |_, cx| { + cx.open_window( + WindowOptions { + is_resizable: false, + window_bounds: Some(window_bounds), + ..Default::default() + }, + |_, cx| { + cx.new(|_| SubWindow { + custom_titlebar: false, + }) + }, + ) + .unwrap(); + })) + .child(button("Unminimizable", move |_, cx| { + cx.open_window( + WindowOptions { + is_minimizable: false, + window_bounds: Some(window_bounds), + ..Default::default() + }, + |_, cx| { + cx.new(|_| SubWindow { + custom_titlebar: false, + }) + }, + ) + .unwrap(); + })) .child(button("Hide Application", |window, cx| { cx.hide(); diff --git a/crates/gpui/examples/window_positioning.rs b/crates/gpui/examples/window_positioning.rs index 0f0bb8ac288d7117867df9b12a104e4272903378..8180104e1e3d0315bd213a73122125fdef3ca744 100644 --- a/crates/gpui/examples/window_positioning.rs +++ b/crates/gpui/examples/window_positioning.rs @@ -62,6 +62,7 @@ fn build_window_options(display_id: DisplayId, bounds: Bounds) -> Window app_id: None, window_min_size: None, window_decorations: None, + ..Default::default() } } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index f64710bc562146f5372f0f26f7e732714350434d..8e9c52c2e750e5a158f0eedd461cee8bea02e54b 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1091,6 +1091,12 @@ pub struct WindowOptions { /// Whether the window should be movable by the user pub is_movable: bool, + /// Whether the window should be resizable by the user + pub is_resizable: bool, + + /// Whether the window should be minimized by the user + pub is_minimizable: bool, + /// The display to create the window on, if this is None, /// the window will be created on the main display pub display_id: Option, @@ -1133,6 +1139,14 @@ pub(crate) struct WindowParams { #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] pub is_movable: bool, + /// Whether the window should be resizable by the user + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + pub is_resizable: bool, + + /// Whether the window should be minimized by the user + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + pub is_minimizable: bool, + #[cfg_attr( any(target_os = "linux", target_os = "freebsd", target_os = "windows"), allow(dead_code) @@ -1191,6 +1205,8 @@ impl Default for WindowOptions { show: true, kind: WindowKind::Normal, is_movable: true, + is_resizable: true, + is_minimizable: true, display_id: None, window_background: WindowBackgroundAppearance::default(), app_id: None, diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 4425d4fe24c91a0bcf840b59de139ecf4f8187b0..fbea4748a395c7377bd7ef3ca30515b7dbc5ff4b 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -530,6 +530,8 @@ impl MacWindow { titlebar, kind, is_movable, + is_resizable, + is_minimizable, focus, show, display_id, @@ -545,10 +547,16 @@ impl MacWindow { let mut style_mask; if let Some(titlebar) = titlebar.as_ref() { - style_mask = NSWindowStyleMask::NSClosableWindowMask - | NSWindowStyleMask::NSMiniaturizableWindowMask - | NSWindowStyleMask::NSResizableWindowMask - | NSWindowStyleMask::NSTitledWindowMask; + style_mask = + NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSTitledWindowMask; + + if is_resizable { + style_mask |= NSWindowStyleMask::NSResizableWindowMask; + } + + if is_minimizable { + style_mask |= NSWindowStyleMask::NSMiniaturizableWindowMask; + } if titlebar.appears_transparent { style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask; diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 99e50733714ba3e280508585f479a90e7edd76d7..e3711d1a26b04a5fccbff3530c240f7a5fadd7ad 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -382,10 +382,17 @@ impl WindowsWindow { let (mut dwexstyle, dwstyle) = if params.kind == WindowKind::PopUp { (WS_EX_TOOLWINDOW, WINDOW_STYLE(0x0)) } else { - ( - WS_EX_APPWINDOW, - WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX, - ) + let mut dwstyle = WS_SYSMENU; + + if params.is_resizable { + dwstyle |= WS_THICKFRAME | WS_MAXIMIZEBOX; + } + + if params.is_minimizable { + dwstyle |= WS_MINIMIZEBOX; + } + + (WS_EX_APPWINDOW, dwstyle) }; if !disable_direct_composition { dwexstyle |= WS_EX_NOREDIRECTIONBITMAP; diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 0791dcc621a8a71ef350ad32f8cf9ab87fad8db2..cc0db3930329fe748dc1e60cc0d613e7351e5b68 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -939,6 +939,8 @@ impl Window { show, kind, is_movable, + is_resizable, + is_minimizable, display_id, window_background, app_id, @@ -956,6 +958,8 @@ impl Window { titlebar, kind, is_movable, + is_resizable, + is_minimizable, focus, show, display_id, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a3116971b494cc9d110edc58b9c9d6b50ff86c25..b0146cfd2bec5db501a638e125f3e95711db4842 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -301,6 +301,7 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO width: px(360.0), height: px(240.0), }), + ..Default::default() } } From 71f900346c768e0bbc3ecd3f9755eddefb001ec2 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 27 Aug 2025 14:12:34 -0400 Subject: [PATCH 386/744] Add ';' and '*' to word_chars to improve softwrap (#37024) Follow-up to: https://github.com/zed-industries/zed/pull/37019 See also: https://github.com/zed-industries/zed/issues/37010 Before/After: Screenshot 2025-08-27 at 13 54 52 Release Notes: - N/A --- crates/gpui/src/text_system/line_wrapper.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index b1e1ee44593a4a48c1b1e54c57bb96f7b0430007..d499d78551a5e0e268b575496bbdac5ddf59369c 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -181,7 +181,7 @@ impl LineWrapper { matches!(c, '\u{0400}'..='\u{04FF}') || // Some other known special characters that should be treated as word characters, // e.g. `a-b`, `var_name`, `I'm`, '@mention`, `#hashtag`, `100%`, `3.1415`, `2^3`, `a~b`, etc. - matches!(c, '-' | '_' | '.' | '\'' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '!') || + matches!(c, '-' | '_' | '.' | '\'' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '!' | ';' | '*') || // Characters that used in URL, e.g. `https://github.com/zed-industries/zed?a=1&b=2` for better wrapping a long URL. matches!(c, '/' | ':' | '?' | '&' | '=') || // `⋯` character is special used in Zed, to keep this at the end of the line. From c158eb2442e1054f760d43770bc8a20088ab11a8 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 27 Aug 2025 14:34:40 -0400 Subject: [PATCH 387/744] docs: Note that Gemini CLI is not supported over SSH (#37023) Release Notes: - N/A --- docs/src/ai/external-agents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/ai/external-agents.md b/docs/src/ai/external-agents.md index 1d11ca03116181e2d343d0945e53b079afe60897..b13cc0fe4b2db226de1e24c0cb86f53d5a7c6a23 100644 --- a/docs/src/ai/external-agents.md +++ b/docs/src/ai/external-agents.md @@ -54,7 +54,7 @@ Similar to Zed's first-party agent, you can use Gemini CLI to do anything that y You can @-mention files, recent threads, symbols, or fetch the web. -Note that some first-party agent features don't yet work with Gemini CLI: editing past messages, resuming threads from history, and checkpointing. +Note that some first-party agent features don't yet work with Gemini CLI: editing past messages, resuming threads from history, checkpointing, and using the agent in SSH projects. We hope to add these features in the near future. ## Add Custom Agents {#add-custom-agents} From e2bf8e5d9c9b1b14cad1c9c618bb724c04182a2c Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 27 Aug 2025 13:55:34 -0500 Subject: [PATCH 388/744] Remote LSP logs (#36709) Enables LSP log tracing in both remote collab and remote ssh environments. Server logs and server RPC traces can now be viewed remotely, and the LSP button is now shown in such projects too. Closes https://github.com/zed-industries/zed/issues/28557 Co-Authored-By: Kirill Co-Authored-By: Lukas Release Notes: - Enabled LSP log tracing in both remote collab and remote ssh environments --------- Co-authored-by: Kirill Bulatov Co-authored-by: Lukas Wirth --- Cargo.lock | 2 + crates/assistant_slash_command/Cargo.toml | 2 +- crates/assistant_tool/Cargo.toml | 2 +- crates/breadcrumbs/Cargo.toml | 2 +- .../20221109000000_test_schema.sql | 1 + .../20250827084812_worktree_in_servers.sql | 2 + crates/collab/src/db/queries/projects.rs | 4 +- crates/collab/src/db/queries/rooms.rs | 2 +- .../collab/src/db/tables/language_server.rs | 1 + crates/collab/src/rpc.rs | 4 +- crates/copilot/Cargo.toml | 2 +- crates/editor/Cargo.toml | 2 +- crates/language_tools/Cargo.toml | 3 +- crates/language_tools/src/language_tools.rs | 4 +- crates/language_tools/src/lsp_log.rs | 666 ++++++++++++------ crates/language_tools/src/lsp_log_tests.rs | 2 +- crates/language_tools/src/lsp_tool.rs | 87 ++- crates/project/src/lsp_store.rs | 90 ++- crates/project/src/project.rs | 21 + crates/project/src/project_tests.rs | 1 + crates/proto/proto/lsp.proto | 44 +- crates/proto/proto/zed.proto | 3 +- crates/proto/src/proto.rs | 7 +- crates/remote_server/Cargo.toml | 1 + crates/remote_server/src/headless_project.rs | 79 ++- crates/settings/src/settings.rs | 2 +- crates/workspace/Cargo.toml | 3 +- crates/workspace/src/pane_group.rs | 15 +- crates/workspace/src/workspace.rs | 237 +++++-- 29 files changed, 949 insertions(+), 342 deletions(-) create mode 100644 crates/collab/migrations/20250827084812_worktree_in_servers.sql diff --git a/Cargo.lock b/Cargo.lock index 4325addc392214614a6654563d88041331f2ded9..8088efd6ea5517ac9cc2cf9bc3224176a09b9e60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9213,6 +9213,7 @@ dependencies = [ "language", "lsp", "project", + "proto", "release_channel", "serde_json", "settings", @@ -13500,6 +13501,7 @@ dependencies = [ "language", "language_extension", "language_model", + "language_tools", "languages", "libc", "log", diff --git a/crates/assistant_slash_command/Cargo.toml b/crates/assistant_slash_command/Cargo.toml index f7b7af9b879492cbb48f4e88d8379b45cbc2d053..e33c2cda1abb3b37f5a3f10e9a76d4c3fcdd6808 100644 --- a/crates/assistant_slash_command/Cargo.toml +++ b/crates/assistant_slash_command/Cargo.toml @@ -25,7 +25,7 @@ parking_lot.workspace = true serde.workspace = true serde_json.workspace = true ui.workspace = true -workspace.workspace = true +workspace = { path = "../workspace", default-features = false } workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/assistant_tool/Cargo.toml b/crates/assistant_tool/Cargo.toml index c95695052a4778209010b2f9e7a4a57be4cb6cf7..951226adfd685b5ae19201ed9531f5a3c8d74e0b 100644 --- a/crates/assistant_tool/Cargo.toml +++ b/crates/assistant_tool/Cargo.toml @@ -28,7 +28,7 @@ serde.workspace = true serde_json.workspace = true text.workspace = true util.workspace = true -workspace.workspace = true +workspace = { path = "../workspace", default-features = false } workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml index c25cfc3c86f26a72b3af37246ab30a175a68969a..46f43d163068ae4b8018080c7873c5ec4db30131 100644 --- a/crates/breadcrumbs/Cargo.toml +++ b/crates/breadcrumbs/Cargo.toml @@ -19,7 +19,7 @@ itertools.workspace = true settings.workspace = true theme.workspace = true ui.workspace = true -workspace.workspace = true +workspace = { path = "../workspace", default-features = false } zed_actions.workspace = true workspace-hack.workspace = true diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 43581fd9421e5a8d10460a9ed15c565bd66a6e5e..b2e25458ef98b295b4d056a7f59521f4fa896f1a 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -175,6 +175,7 @@ CREATE TABLE "language_servers" ( "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, "name" VARCHAR NOT NULL, "capabilities" TEXT NOT NULL, + "worktree_id" BIGINT, PRIMARY KEY (project_id, id) ); diff --git a/crates/collab/migrations/20250827084812_worktree_in_servers.sql b/crates/collab/migrations/20250827084812_worktree_in_servers.sql new file mode 100644 index 0000000000000000000000000000000000000000..d4c6ffbbcccb2d2f23654cfc287b45bb8ea20508 --- /dev/null +++ b/crates/collab/migrations/20250827084812_worktree_in_servers.sql @@ -0,0 +1,2 @@ +ALTER TABLE language_servers + ADD COLUMN worktree_id BIGINT; diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 393f2c80f8e733aa2d2b3b5f4b811c9868e0a620..a3f0ea6cbc6e762e365f82e74b886234e62da109 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -694,6 +694,7 @@ impl Database { project_id: ActiveValue::set(project_id), id: ActiveValue::set(server.id as i64), name: ActiveValue::set(server.name.clone()), + worktree_id: ActiveValue::set(server.worktree_id.map(|id| id as i64)), capabilities: ActiveValue::set(update.capabilities.clone()), }) .on_conflict( @@ -704,6 +705,7 @@ impl Database { .update_columns([ language_server::Column::Name, language_server::Column::Capabilities, + language_server::Column::WorktreeId, ]) .to_owned(), ) @@ -1065,7 +1067,7 @@ impl Database { server: proto::LanguageServer { id: language_server.id as u64, name: language_server.name, - worktree_id: None, + worktree_id: language_server.worktree_id.map(|id| id as u64), }, capabilities: language_server.capabilities, }) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 9e7cabf9b29c91d7e486f42d5e6b12020b0f514e..0713ac2cb2810797b319b53583bc8c0e1756fe68 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -809,7 +809,7 @@ impl Database { server: proto::LanguageServer { id: language_server.id as u64, name: language_server.name, - worktree_id: None, + worktree_id: language_server.worktree_id.map(|id| id as u64), }, capabilities: language_server.capabilities, }) diff --git a/crates/collab/src/db/tables/language_server.rs b/crates/collab/src/db/tables/language_server.rs index 34c7514d917b313990521acf8542c31394d009fc..705aae292ba456622e9808f033a348f60c3835a4 100644 --- a/crates/collab/src/db/tables/language_server.rs +++ b/crates/collab/src/db/tables/language_server.rs @@ -10,6 +10,7 @@ pub struct Model { pub id: i64, pub name: String, pub capabilities: String, + pub worktree_id: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 73f327166a3f1fb40a1f232ea2fabcdedd3fb129..9e4dfd4854b4de67de522bfbbd1160fe880a05cb 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -476,7 +476,9 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_message_handler(broadcast_project_message_from_host::) - .add_message_handler(update_context); + .add_message_handler(update_context) + .add_request_handler(forward_mutating_project_request::) + .add_message_handler(broadcast_project_message_from_host::); Arc::new(server) } diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 0fc119f31125f4ef3925799fd98fd47cac7ca9da..470d1989583d9eb1ed06159a28686ddf2620156e 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -50,7 +50,7 @@ sum_tree.workspace = true task.workspace = true ui.workspace = true util.workspace = true -workspace.workspace = true +workspace = { path = "../workspace", default-features = false } workspace-hack.workspace = true itertools.workspace = true diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 339f98ae8bd88263f1fea12c535569864faae294..b7051c9b19ab748390c109b5c6cd449cb0f4886e 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -89,7 +89,7 @@ ui.workspace = true url.workspace = true util.workspace = true uuid.workspace = true -workspace.workspace = true +workspace = { path = "../workspace", default-features = false } zed_actions.workspace = true workspace-hack.workspace = true diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index 5aa914311a6eccc1cb68efa37e878ad12249d6fd..a10b7dc50b9838d6da1afb9838758b4dd4582563 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -24,13 +24,14 @@ itertools.workspace = true language.workspace = true lsp.workspace = true project.workspace = true +proto.workspace = true serde_json.workspace = true settings.workspace = true theme.workspace = true tree-sitter.workspace = true ui.workspace = true util.workspace = true -workspace.workspace = true +workspace = { path = "../workspace", default-features = false } zed_actions.workspace = true workspace-hack.workspace = true diff --git a/crates/language_tools/src/language_tools.rs b/crates/language_tools/src/language_tools.rs index cbf5756875f723b52fabbfe877c32265dd6f0aef..d6a006f47bc921f00c15edc8b45d3efe39364acc 100644 --- a/crates/language_tools/src/language_tools.rs +++ b/crates/language_tools/src/language_tools.rs @@ -1,5 +1,5 @@ mod key_context_view; -mod lsp_log; +pub mod lsp_log; pub mod lsp_tool; mod syntax_tree_view; @@ -14,7 +14,7 @@ use ui::{Context, Window}; use workspace::{Item, ItemHandle, SplitDirection, Workspace}; pub fn init(cx: &mut App) { - lsp_log::init(cx); + lsp_log::init(true, cx); syntax_tree_view::init(cx); key_context_view::init(cx); } diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index a71e434e5274392add0463830519834202b7ba58..d55b54a6d253214cd81bf0226278863c4d59c834 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -9,12 +9,16 @@ use gpui::{ use itertools::Itertools; use language::{LanguageServerId, language_settings::SoftWrap}; use lsp::{ - IoKind, LanguageServer, LanguageServerName, LanguageServerSelector, MessageType, - SetTraceParams, TraceValue, notification::SetTrace, + IoKind, LanguageServer, LanguageServerBinary, LanguageServerName, LanguageServerSelector, + MessageType, SetTraceParams, TraceValue, notification::SetTrace, +}; +use project::{ + LspStore, Project, ProjectItem, WorktreeId, lsp_store::LanguageServerLogType, + search::SearchQuery, }; -use project::{Project, WorktreeId, search::SearchQuery}; use std::{any::TypeId, borrow::Cow, sync::Arc}; use ui::{Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState, prelude::*}; +use util::ResultExt as _; use workspace::{ SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, item::{Item, ItemHandle}, @@ -28,6 +32,7 @@ const RECEIVE_LINE: &str = "\n// Receive:"; const MAX_STORED_LOG_ENTRIES: usize = 2000; pub struct LogStore { + store_logs: bool, projects: HashMap, ProjectState>, language_servers: HashMap, copilot_log_subscription: Option, @@ -46,6 +51,7 @@ trait Message: AsRef { } } +#[derive(Debug)] pub(super) struct LogMessage { message: String, typ: MessageType, @@ -73,8 +79,10 @@ impl Message for LogMessage { } } +#[derive(Debug)] pub(super) struct TraceMessage { message: String, + is_verbose: bool, } impl AsRef for TraceMessage { @@ -84,9 +92,18 @@ impl AsRef for TraceMessage { } impl Message for TraceMessage { - type Level = (); + type Level = TraceValue; + + fn should_include(&self, level: Self::Level) -> bool { + match level { + TraceValue::Off => false, + TraceValue::Messages => !self.is_verbose, + TraceValue::Verbose => true, + } + } } +#[derive(Debug)] struct RpcMessage { message: String, } @@ -101,7 +118,7 @@ impl Message for RpcMessage { type Level = (); } -pub(super) struct LanguageServerState { +pub struct LanguageServerState { name: Option, worktree_id: Option, kind: LanguageServerKind, @@ -113,24 +130,35 @@ pub(super) struct LanguageServerState { io_logs_subscription: Option, } +impl std::fmt::Debug for LanguageServerState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LanguageServerState") + .field("name", &self.name) + .field("worktree_id", &self.worktree_id) + .field("kind", &self.kind) + .field("log_messages", &self.log_messages) + .field("trace_messages", &self.trace_messages) + .field("rpc_state", &self.rpc_state) + .field("trace_level", &self.trace_level) + .field("log_level", &self.log_level) + .finish_non_exhaustive() + } +} + #[derive(PartialEq, Clone)] pub enum LanguageServerKind { Local { project: WeakEntity }, Remote { project: WeakEntity }, + LocalSsh { lsp_store: WeakEntity }, Global, } -impl LanguageServerKind { - fn is_remote(&self) -> bool { - matches!(self, LanguageServerKind::Remote { .. }) - } -} - impl std::fmt::Debug for LanguageServerKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"), LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"), + LanguageServerKind::LocalSsh { .. } => write!(f, "LanguageServerKind::LocalSsh"), LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"), } } @@ -141,12 +169,14 @@ impl LanguageServerKind { match self { Self::Local { project } => Some(project), Self::Remote { project } => Some(project), + Self::LocalSsh { .. } => None, Self::Global { .. } => None, } } } -struct LanguageServerRpcState { +#[derive(Debug)] +pub struct LanguageServerRpcState { rpc_messages: VecDeque, last_message_kind: Option, } @@ -167,7 +197,7 @@ pub struct LspLogToolbarItemView { _log_view_subscription: Option, } -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] enum MessageKind { Send, Receive, @@ -183,6 +213,13 @@ pub enum LogKind { } impl LogKind { + fn from_server_log_type(log_type: &LanguageServerLogType) -> Self { + match log_type { + LanguageServerLogType::Log(_) => Self::Logs, + LanguageServerLogType::Trace { .. } => Self::Trace, + LanguageServerLogType::Rpc { .. } => Self::Rpc, + } + } fn label(&self) -> &'static str { match self { LogKind::Rpc => RPC_MESSAGES, @@ -212,59 +249,53 @@ actions!( ] ); -pub(super) struct GlobalLogStore(pub WeakEntity); +pub struct GlobalLogStore(pub WeakEntity); impl Global for GlobalLogStore {} -pub fn init(cx: &mut App) { - let log_store = cx.new(LogStore::new); +pub fn init(store_logs: bool, cx: &mut App) { + let log_store = cx.new(|cx| LogStore::new(store_logs, cx)); cx.set_global(GlobalLogStore(log_store.downgrade())); cx.observe_new(move |workspace: &mut Workspace, _, cx| { - let project = workspace.project(); - if project.read(cx).is_local() || project.read(cx).is_via_remote_server() { - log_store.update(cx, |store, cx| { - store.add_project(project, cx); - }); - } + log_store.update(cx, |store, cx| { + store.add_project(workspace.project(), cx); + }); let log_store = log_store.clone(); workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| { - let project = workspace.project().read(cx); - if project.is_local() || project.is_via_remote_server() { - let project = workspace.project().clone(); - let log_store = log_store.clone(); - get_or_create_tool( - workspace, - SplitDirection::Right, - window, - cx, - move |window, cx| LspLogView::new(project, log_store, window, cx), - ); - } + let log_store = log_store.clone(); + let project = workspace.project().clone(); + get_or_create_tool( + workspace, + SplitDirection::Right, + window, + cx, + move |window, cx| LspLogView::new(project, log_store, window, cx), + ); }); }) .detach(); } impl LogStore { - pub fn new(cx: &mut Context) -> Self { + pub fn new(store_logs: bool, cx: &mut Context) -> Self { let (io_tx, mut io_rx) = mpsc::unbounded(); let copilot_subscription = Copilot::global(cx).map(|copilot| { let copilot = &copilot; - cx.subscribe(copilot, |this, copilot, edit_prediction_event, cx| { + cx.subscribe(copilot, |log_store, copilot, edit_prediction_event, cx| { if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event && let Some(server) = copilot.read(cx).language_server() { let server_id = server.server_id(); - let weak_this = cx.weak_entity(); - this.copilot_log_subscription = + let weak_lsp_store = cx.weak_entity(); + log_store.copilot_log_subscription = Some(server.on_notification::( move |params, cx| { - weak_this - .update(cx, |this, cx| { - this.add_language_server_log( + weak_lsp_store + .update(cx, |lsp_store, cx| { + lsp_store.add_language_server_log( server_id, MessageType::LOG, ¶ms.message, @@ -274,8 +305,9 @@ impl LogStore { .ok(); }, )); + let name = LanguageServerName::new_static("copilot"); - this.add_language_server( + log_store.add_language_server( LanguageServerKind::Global, server.server_id(), Some(name), @@ -287,26 +319,27 @@ impl LogStore { }) }); - let this = Self { + let log_store = Self { copilot_log_subscription: None, _copilot_subscription: copilot_subscription, projects: HashMap::default(), language_servers: HashMap::default(), + store_logs, io_tx, }; - cx.spawn(async move |this, cx| { + cx.spawn(async move |log_store, cx| { while let Some((server_id, io_kind, message)) = io_rx.next().await { - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| { - this.on_io(server_id, io_kind, &message, cx); + if let Some(log_store) = log_store.upgrade() { + log_store.update(cx, |log_store, cx| { + log_store.on_io(server_id, io_kind, &message, cx); })?; } } anyhow::Ok(()) }) .detach_and_log_err(cx); - this + log_store } pub fn add_project(&mut self, project: &Entity, cx: &mut Context) { @@ -320,20 +353,19 @@ impl LogStore { this.language_servers .retain(|_, state| state.kind.project() != Some(&weak_project)); }), - cx.subscribe(project, |this, project, event, cx| { - let server_kind = if project.read(cx).is_via_remote_server() { - LanguageServerKind::Remote { + cx.subscribe(project, move |log_store, project, event, cx| { + let server_kind = if project.read(cx).is_local() { + LanguageServerKind::Local { project: project.downgrade(), } } else { - LanguageServerKind::Local { + LanguageServerKind::Remote { project: project.downgrade(), } }; - match event { project::Event::LanguageServerAdded(id, name, worktree_id) => { - this.add_language_server( + log_store.add_language_server( server_kind, *id, Some(name.clone()), @@ -346,20 +378,78 @@ impl LogStore { cx, ); } + project::Event::LanguageServerBufferRegistered { + server_id, + buffer_id, + name, + .. + } if project.read(cx).is_via_collab() => { + let worktree_id = project + .read(cx) + .buffer_for_id(*buffer_id, cx) + .and_then(|buffer| { + Some(buffer.read(cx).project_path(cx)?.worktree_id) + }); + let name = name.clone().or_else(|| { + project + .read(cx) + .lsp_store() + .read(cx) + .language_server_statuses + .get(server_id) + .map(|status| status.name.clone()) + }); + log_store.add_language_server( + server_kind, + *server_id, + name, + worktree_id, + None, + cx, + ); + } project::Event::LanguageServerRemoved(id) => { - this.remove_language_server(*id, cx); + log_store.remove_language_server(*id, cx); } project::Event::LanguageServerLog(id, typ, message) => { - this.add_language_server(server_kind, *id, None, None, None, cx); + log_store.add_language_server( + server_kind, + *id, + None, + None, + None, + cx, + ); match typ { project::LanguageServerLogType::Log(typ) => { - this.add_language_server_log(*id, *typ, message, cx); + log_store.add_language_server_log(*id, *typ, message, cx); } - project::LanguageServerLogType::Trace(_) => { - this.add_language_server_trace(*id, message, cx); + project::LanguageServerLogType::Trace { verbose_info } => { + log_store.add_language_server_trace( + *id, + message, + verbose_info.clone(), + cx, + ); + } + project::LanguageServerLogType::Rpc { received } => { + let kind = if *received { + MessageKind::Receive + } else { + MessageKind::Send + }; + log_store.add_language_server_rpc(*id, kind, message, cx); } } } + project::Event::ToggleLspLogs { server_id, enabled } => { + // we do not support any other log toggling yet + if *enabled { + log_store.enable_rpc_trace_for_language_server(*server_id); + } else { + log_store.disable_rpc_trace_for_language_server(*server_id); + } + } _ => {} } }), @@ -375,7 +465,7 @@ impl LogStore { self.language_servers.get_mut(&id) } - fn add_language_server( + pub fn add_language_server( &mut self, kind: LanguageServerKind, server_id: LanguageServerId, @@ -426,20 +516,35 @@ impl LogStore { message: &str, cx: &mut Context, ) -> Option<()> { + let store_logs = self.store_logs; let language_server_state = self.get_language_server_state(id)?; let log_lines = &mut language_server_state.log_messages; - Self::add_language_server_message( + let message = message.trim_end().to_string(); + if !store_logs { + // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway + self.emit_event( + Event::NewServerLogEntry { + id, + kind: LanguageServerLogType::Log(typ), + text: message, + }, + cx, + ); + } else if let Some(new_message) = Self::push_new_message( log_lines, - id, - LogMessage { - message: message.trim_end().to_string(), - typ, - }, + LogMessage { message, typ }, language_server_state.log_level, - LogKind::Logs, - cx, - ); + ) { + self.emit_event( + Event::NewServerLogEntry { + id, + kind: LanguageServerLogType::Log(typ), + text: new_message, + }, + cx, + ); + } Some(()) } @@ -447,46 +552,127 @@ impl LogStore { &mut self, id: LanguageServerId, message: &str, + verbose_info: Option, cx: &mut Context, ) -> Option<()> { + let store_logs = self.store_logs; let language_server_state = self.get_language_server_state(id)?; let log_lines = &mut language_server_state.trace_messages; - Self::add_language_server_message( + if !store_logs { + // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway + self.emit_event( + Event::NewServerLogEntry { + id, + kind: LanguageServerLogType::Trace { verbose_info }, + text: message.trim().to_string(), + }, + cx, + ); + } else if let Some(new_message) = Self::push_new_message( log_lines, - id, TraceMessage { message: message.trim().to_string(), + is_verbose: false, }, - (), - LogKind::Trace, - cx, - ); + TraceValue::Messages, + ) { + if let Some(verbose_message) = verbose_info.as_ref() { + Self::push_new_message( + log_lines, + TraceMessage { + message: verbose_message.clone(), + is_verbose: true, + }, + TraceValue::Verbose, + ); + } + self.emit_event( + Event::NewServerLogEntry { + id, + kind: LanguageServerLogType::Trace { verbose_info }, + text: new_message, + }, + cx, + ); + } Some(()) } - fn add_language_server_message( + fn push_new_message( log_lines: &mut VecDeque, - id: LanguageServerId, message: T, current_severity: ::Level, - kind: LogKind, - cx: &mut Context, - ) { + ) -> Option { while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { log_lines.pop_front(); } - let text = message.as_ref().to_string(); let visible = message.should_include(current_severity); + + let visible_message = visible.then(|| message.as_ref().to_string()); log_lines.push_back(message); + visible_message + } - if visible { - cx.emit(Event::NewServerLogEntry { id, kind, text }); - cx.notify(); + fn add_language_server_rpc( + &mut self, + language_server_id: LanguageServerId, + kind: MessageKind, + message: &str, + cx: &mut Context<'_, Self>, + ) { + let store_logs = self.store_logs; + let Some(state) = self + .get_language_server_state(language_server_id) + .and_then(|state| state.rpc_state.as_mut()) + else { + return; + }; + + let received = kind == MessageKind::Receive; + let rpc_log_lines = &mut state.rpc_messages; + if state.last_message_kind != Some(kind) { + while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { + rpc_log_lines.pop_front(); + } + let line_before_message = match kind { + MessageKind::Send => SEND_LINE, + MessageKind::Receive => RECEIVE_LINE, + }; + if store_logs { + rpc_log_lines.push_back(RpcMessage { + message: line_before_message.to_string(), + }); + } + // Do not send a synthetic message over the wire, it will be derived from the actual RPC message + cx.emit(Event::NewServerLogEntry { + id: language_server_id, + kind: LanguageServerLogType::Rpc { received }, + text: line_before_message.to_string(), + }); + } + + while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { + rpc_log_lines.pop_front(); + } + + if store_logs { + rpc_log_lines.push_back(RpcMessage { + message: message.trim().to_owned(), + }); } + + self.emit_event( + Event::NewServerLogEntry { + id: language_server_id, + kind: LanguageServerLogType::Rpc { received }, + text: message.to_owned(), + }, + cx, + ); } - fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context) { + pub fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context) { self.language_servers.remove(&id); cx.notify(); } @@ -516,11 +702,11 @@ impl LogStore { None } } - LanguageServerKind::Global => Some(*id), + LanguageServerKind::Global | LanguageServerKind::LocalSsh { .. } => Some(*id), }) } - fn enable_rpc_trace_for_language_server( + pub fn enable_rpc_trace_for_language_server( &mut self, server_id: LanguageServerId, ) -> Option<&mut LanguageServerRpcState> { @@ -663,50 +849,45 @@ impl LogStore { } }; - let state = self - .get_language_server_state(language_server_id)? - .rpc_state - .as_mut()?; let kind = if is_received { MessageKind::Receive } else { MessageKind::Send }; - let rpc_log_lines = &mut state.rpc_messages; - if state.last_message_kind != Some(kind) { - while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - rpc_log_lines.pop_front(); - } - let line_before_message = match kind { - MessageKind::Send => SEND_LINE, - MessageKind::Receive => RECEIVE_LINE, - }; - rpc_log_lines.push_back(RpcMessage { - message: line_before_message.to_string(), - }); - cx.emit(Event::NewServerLogEntry { - id: language_server_id, - kind: LogKind::Rpc, - text: line_before_message.to_string(), - }); - } + self.add_language_server_rpc(language_server_id, kind, message, cx); + cx.notify(); + Some(()) + } - while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - rpc_log_lines.pop_front(); + fn emit_event(&mut self, e: Event, cx: &mut Context) { + match &e { + Event::NewServerLogEntry { id, kind, text } => { + if let Some(state) = self.get_language_server_state(*id) { + let downstream_client = match &state.kind { + LanguageServerKind::Remote { project } + | LanguageServerKind::Local { project } => project + .upgrade() + .map(|project| project.read(cx).lsp_store()), + LanguageServerKind::LocalSsh { lsp_store } => lsp_store.upgrade(), + LanguageServerKind::Global => None, + } + .and_then(|lsp_store| lsp_store.read(cx).downstream_client()); + if let Some((client, project_id)) = downstream_client { + client + .send(proto::LanguageServerLog { + project_id, + language_server_id: id.to_proto(), + message: text.clone(), + log_type: Some(kind.to_proto()), + }) + .ok(); + } + } + } } - let message = message.trim(); - rpc_log_lines.push_back(RpcMessage { - message: message.to_string(), - }); - cx.emit(Event::NewServerLogEntry { - id: language_server_id, - kind: LogKind::Rpc, - text: message.to_string(), - }); - cx.notify(); - Some(()) + cx.emit(e); } } @@ -751,13 +932,14 @@ impl LspLogView { cx.notify(); }); + let events_subscriptions = cx.subscribe_in( &log_store, window, move |log_view, _, e, window, cx| match e { Event::NewServerLogEntry { id, kind, text } => { if log_view.current_server_id == Some(*id) - && *kind == log_view.active_entry_kind + && LogKind::from_server_log_type(kind) == log_view.active_entry_kind { log_view.editor.update(cx, |editor, cx| { editor.set_read_only(false); @@ -800,7 +982,7 @@ impl LspLogView { window.focus(&log_view.editor.focus_handle(cx)); }); - let mut this = Self { + let mut lsp_log_view = Self { focus_handle, editor, editor_subscriptions, @@ -815,9 +997,9 @@ impl LspLogView { ], }; if let Some(server_id) = server_id { - this.show_logs_for_server(server_id, window, cx); + lsp_log_view.show_logs_for_server(server_id, window, cx); } - this + lsp_log_view } fn editor_for_logs( @@ -838,7 +1020,7 @@ impl LspLogView { } fn editor_for_server_info( - server: &LanguageServer, + info: ServerInfo, window: &mut Window, cx: &mut Context, ) -> (Entity, Vec) { @@ -853,22 +1035,21 @@ impl LspLogView { * Capabilities: {CAPABILITIES} * Configuration: {CONFIGURATION}", - NAME = server.name(), - ID = server.server_id(), - BINARY = server.binary(), - WORKSPACE_FOLDERS = server - .workspace_folders() - .into_iter() - .filter_map(|path| path - .to_file_path() - .ok() - .map(|path| path.to_string_lossy().into_owned())) - .collect::>() - .join(", "), - CAPABILITIES = serde_json::to_string_pretty(&server.capabilities()) + NAME = info.name, + ID = info.id, + BINARY = info.binary.as_ref().map_or_else( + || "Unknown".to_string(), + |bin| bin.path.as_path().to_string_lossy().to_string() + ), + WORKSPACE_FOLDERS = info.workspace_folders.join(", "), + CAPABILITIES = serde_json::to_string_pretty(&info.capabilities) .unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")), - CONFIGURATION = serde_json::to_string_pretty(server.configuration()) - .unwrap_or_else(|e| format!("Failed to serialize configuration: {e}")), + CONFIGURATION = info + .configuration + .map(|configuration| serde_json::to_string_pretty(&configuration)) + .transpose() + .unwrap_or_else(|e| Some(format!("Failed to serialize configuration: {e}"))) + .unwrap_or_else(|| "Unknown".to_string()), ); let editor = initialize_new_editor(server_info, false, window, cx); let editor_subscription = cx.subscribe( @@ -891,7 +1072,9 @@ impl LspLogView { .language_servers .iter() .map(|(server_id, state)| match &state.kind { - LanguageServerKind::Local { .. } | LanguageServerKind::Remote { .. } => { + LanguageServerKind::Local { .. } + | LanguageServerKind::Remote { .. } + | LanguageServerKind::LocalSsh { .. } => { let worktree_root_name = state .worktree_id .and_then(|id| self.project.read(cx).worktree_for_id(id, cx)) @@ -1003,11 +1186,17 @@ impl LspLogView { window: &mut Window, cx: &mut Context, ) { + let trace_level = self + .log_store + .update(cx, |this, _| { + Some(this.get_language_server_state(server_id)?.trace_level) + }) + .unwrap_or(TraceValue::Messages); let log_contents = self .log_store .read(cx) .server_trace(server_id) - .map(|v| log_contents(v, ())); + .map(|v| log_contents(v, trace_level)); if let Some(log_contents) = log_contents { self.current_server_id = Some(server_id); self.active_entry_kind = LogKind::Trace; @@ -1025,6 +1214,7 @@ impl LspLogView { window: &mut Window, cx: &mut Context, ) { + self.toggle_rpc_trace_for_server(server_id, true, window, cx); let rpc_log = self.log_store.update(cx, |log_store, _| { log_store .enable_rpc_trace_for_language_server(server_id) @@ -1069,12 +1259,33 @@ impl LspLogView { window: &mut Window, cx: &mut Context, ) { - self.log_store.update(cx, |log_store, _| { + self.log_store.update(cx, |log_store, cx| { if enabled { log_store.enable_rpc_trace_for_language_server(server_id); } else { log_store.disable_rpc_trace_for_language_server(server_id); } + + if let Some(server_state) = log_store.language_servers.get(&server_id) { + if let LanguageServerKind::Remote { project } = &server_state.kind { + project + .update(cx, |project, cx| { + if let Some((client, project_id)) = + project.lsp_store().read(cx).upstream_client() + { + client + .send(proto::ToggleLspLogs { + project_id, + log_type: proto::toggle_lsp_logs::LogType::Rpc as i32, + server_id: server_id.to_proto(), + enabled, + }) + .log_err(); + } + }) + .ok(); + } + }; }); if !enabled && Some(server_id) == self.current_server_id { self.show_logs_for_server(server_id, window, cx); @@ -1113,13 +1324,38 @@ impl LspLogView { window: &mut Window, cx: &mut Context, ) { - let lsp_store = self.project.read(cx).lsp_store(); - let Some(server) = lsp_store.read(cx).language_server_for_id(server_id) else { + let Some(server_info) = self + .project + .read(cx) + .lsp_store() + .update(cx, |lsp_store, _| { + lsp_store + .language_server_for_id(server_id) + .as_ref() + .map(|language_server| ServerInfo::new(language_server)) + .or_else(move || { + let capabilities = + lsp_store.lsp_server_capabilities.get(&server_id)?.clone(); + let name = lsp_store + .language_server_statuses + .get(&server_id) + .map(|status| status.name.clone())?; + Some(ServerInfo { + id: server_id, + capabilities, + binary: None, + name, + workspace_folders: Vec::new(), + configuration: None, + }) + }) + }) + else { return; }; self.current_server_id = Some(server_id); self.active_entry_kind = LogKind::ServerInfo; - let (editor, editor_subscriptions) = Self::editor_for_server_info(&server, window, cx); + let (editor, editor_subscriptions) = Self::editor_for_server_info(server_info, window, cx); self.editor = editor; self.editor_subscriptions = editor_subscriptions; cx.notify(); @@ -1416,7 +1652,6 @@ impl Render for LspLogToolbarItemView { let view_selector = current_server.map(|server| { let server_id = server.server_id; - let is_remote = server.server_kind.is_remote(); let rpc_trace_enabled = server.rpc_trace_enabled; let log_view = log_view.clone(); PopoverMenu::new("LspViewSelector") @@ -1438,55 +1673,53 @@ impl Render for LspLogToolbarItemView { view.show_logs_for_server(server_id, window, cx); }), ) - .when(!is_remote, |this| { - this.entry( - SERVER_TRACE, - None, - window.handler_for(&log_view, move |view, window, cx| { - view.show_trace_for_server(server_id, window, cx); - }), - ) - .custom_entry( - { - let log_toolbar_view = log_toolbar_view.clone(); - move |window, _| { - h_flex() - .w_full() - .justify_between() - .child(Label::new(RPC_MESSAGES)) - .child( - div().child( - Checkbox::new( - "LspLogEnableRpcTrace", - if rpc_trace_enabled { + .entry( + SERVER_TRACE, + None, + window.handler_for(&log_view, move |view, window, cx| { + view.show_trace_for_server(server_id, window, cx); + }), + ) + .custom_entry( + { + let log_toolbar_view = log_toolbar_view.clone(); + move |window, _| { + h_flex() + .w_full() + .justify_between() + .child(Label::new(RPC_MESSAGES)) + .child( + div().child( + Checkbox::new( + "LspLogEnableRpcTrace", + if rpc_trace_enabled { + ToggleState::Selected + } else { + ToggleState::Unselected + }, + ) + .on_click(window.listener_for( + &log_toolbar_view, + move |view, selection, window, cx| { + let enabled = matches!( + selection, ToggleState::Selected - } else { - ToggleState::Unselected - }, - ) - .on_click(window.listener_for( - &log_toolbar_view, - move |view, selection, window, cx| { - let enabled = matches!( - selection, - ToggleState::Selected - ); - view.toggle_rpc_logging_for_server( - server_id, enabled, window, cx, - ); - cx.stop_propagation(); - }, - )), - ), - ) - .into_any_element() - } - }, - window.handler_for(&log_view, move |view, window, cx| { - view.show_rpc_trace_for_server(server_id, window, cx); - }), - ) - }) + ); + view.toggle_rpc_logging_for_server( + server_id, enabled, window, cx, + ); + cx.stop_propagation(); + }, + )), + ), + ) + .into_any_element() + } + }, + window.handler_for(&log_view, move |view, window, cx| { + view.show_rpc_trace_for_server(server_id, window, cx); + }), + ) .entry( SERVER_INFO, None, @@ -1696,12 +1929,6 @@ const SERVER_LOGS: &str = "Server Logs"; const SERVER_TRACE: &str = "Server Trace"; const SERVER_INFO: &str = "Server Info"; -impl Default for LspLogToolbarItemView { - fn default() -> Self { - Self::new() - } -} - impl LspLogToolbarItemView { pub fn new() -> Self { Self { @@ -1734,10 +1961,41 @@ impl LspLogToolbarItemView { } } +struct ServerInfo { + id: LanguageServerId, + capabilities: lsp::ServerCapabilities, + binary: Option, + name: LanguageServerName, + workspace_folders: Vec, + configuration: Option, +} + +impl ServerInfo { + fn new(server: &LanguageServer) -> Self { + Self { + id: server.server_id(), + capabilities: server.capabilities(), + binary: Some(server.binary().clone()), + name: server.name(), + workspace_folders: server + .workspace_folders() + .into_iter() + .filter_map(|path| { + path.to_file_path() + .ok() + .map(|path| path.to_string_lossy().into_owned()) + }) + .collect::>(), + configuration: Some(server.configuration().clone()), + } + } +} + +#[derive(Debug)] pub enum Event { NewServerLogEntry { id: LanguageServerId, - kind: LogKind, + kind: LanguageServerLogType, text: String, }, } diff --git a/crates/language_tools/src/lsp_log_tests.rs b/crates/language_tools/src/lsp_log_tests.rs index ad2b653fdcfd4dc228cac58da7ed15f844b4bb26..a7dbaa2a601bc6ebd60685635cd0802750452053 100644 --- a/crates/language_tools/src/lsp_log_tests.rs +++ b/crates/language_tools/src/lsp_log_tests.rs @@ -51,7 +51,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { }, ); - let log_store = cx.new(LogStore::new); + let log_store = cx.new(|cx| LogStore::new(true, cx)); log_store.update(cx, |store, cx| store.add_project(&project, cx)); let _rust_buffer = project diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index dd3e80212fda08f43718a664d2cfd6d377182273..2d6a99a0bc2f0d2ed498aaada204574740293343 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -122,8 +122,7 @@ impl LanguageServerState { let lsp_logs = cx .try_global::() .and_then(|lsp_logs| lsp_logs.0.upgrade()); - let lsp_store = self.lsp_store.upgrade(); - let Some((lsp_logs, lsp_store)) = lsp_logs.zip(lsp_store) else { + let Some(lsp_logs) = lsp_logs else { return menu; }; @@ -210,10 +209,11 @@ impl LanguageServerState { }; let server_selector = server_info.server_selector(); - // TODO currently, Zed remote does not work well with the LSP logs - // https://github.com/zed-industries/zed/issues/28557 - let has_logs = lsp_store.read(cx).as_local().is_some() - && lsp_logs.read(cx).has_server_logs(&server_selector); + let is_remote = self + .lsp_store + .update(cx, |lsp_store, _| lsp_store.as_remote().is_some()) + .unwrap_or(false); + let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector); let status_color = server_info .binary_status @@ -241,10 +241,10 @@ impl LanguageServerState { .as_ref() .or_else(|| server_info.binary_status.as_ref()?.message.as_ref()) .cloned(); - let hover_label = if has_logs { - Some("View Logs") - } else if message.is_some() { + let hover_label = if message.is_some() { Some("View Message") + } else if has_logs { + Some("View Logs") } else { None }; @@ -288,16 +288,7 @@ impl LanguageServerState { let server_name = server_info.name.clone(); let workspace = self.workspace.clone(); move |window, cx| { - if has_logs { - lsp_logs.update(cx, |lsp_logs, cx| { - lsp_logs.open_server_trace( - workspace.clone(), - server_selector.clone(), - window, - cx, - ); - }); - } else if let Some(message) = &message { + if let Some(message) = &message { let Some(create_buffer) = workspace .update(cx, |workspace, cx| { workspace @@ -347,6 +338,15 @@ impl LanguageServerState { anyhow::Ok(()) }) .detach(); + } else if has_logs { + lsp_logs.update(cx, |lsp_logs, cx| { + lsp_logs.open_server_trace( + workspace.clone(), + server_selector.clone(), + window, + cx, + ); + }); } else { cx.propagate(); } @@ -529,26 +529,48 @@ impl LspTool { }); let lsp_store = workspace.project().read(cx).lsp_store(); + let mut language_servers = LanguageServers::default(); + for (_, status) in lsp_store.read(cx).language_server_statuses() { + language_servers.binary_statuses.insert( + status.name.clone(), + LanguageServerBinaryStatus { + status: BinaryStatus::None, + message: None, + }, + ); + } + let lsp_store_subscription = cx.subscribe_in(&lsp_store, window, |lsp_tool, _, e, window, cx| { lsp_tool.on_lsp_store_event(e, window, cx) }); - let state = cx.new(|_| LanguageServerState { + let server_state = cx.new(|_| LanguageServerState { workspace: workspace.weak_handle(), items: Vec::new(), lsp_store: lsp_store.downgrade(), active_editor: None, - language_servers: LanguageServers::default(), + language_servers, }); - Self { - server_state: state, + let mut lsp_tool = Self { + server_state, popover_menu_handle, lsp_menu: None, lsp_menu_refresh: Task::ready(()), _subscriptions: vec![settings_subscription, lsp_store_subscription], + }; + if !lsp_tool + .server_state + .read(cx) + .language_servers + .binary_statuses + .is_empty() + { + lsp_tool.refresh_lsp_menu(true, window, cx); } + + lsp_tool } fn on_lsp_store_event( @@ -708,6 +730,25 @@ impl LspTool { } } } + state + .lsp_store + .update(cx, |lsp_store, cx| { + for (server_id, status) in lsp_store.language_server_statuses() { + if let Some(worktree) = status.worktree.and_then(|worktree_id| { + lsp_store + .worktree_store() + .read(cx) + .worktree_for_id(worktree_id, cx) + }) { + server_ids_to_worktrees.insert(server_id, worktree.clone()); + server_names_to_worktrees + .entry(status.name.clone()) + .or_default() + .insert((worktree, server_id)); + } + } + }) + .ok(); let mut servers_per_worktree = BTreeMap::>::new(); let mut servers_without_worktree = Vec::::new(); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index ad9d0abf405b18f9048030621e960251057588de..2bc95bf81d85ca6e09a246408a4ef8bfa28b91e3 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -977,7 +977,9 @@ impl LocalLspStore { this.update(&mut cx, |_, cx| { cx.emit(LspStoreEvent::LanguageServerLog( server_id, - LanguageServerLogType::Trace(params.verbose), + LanguageServerLogType::Trace { + verbose_info: params.verbose, + }, params.message, )); }) @@ -3482,13 +3484,13 @@ pub struct LspStore { buffer_store: Entity, worktree_store: Entity, pub languages: Arc, - language_server_statuses: BTreeMap, + pub language_server_statuses: BTreeMap, active_entry: Option, _maintain_workspace_config: (Task>, watch::Sender<()>), _maintain_buffer_languages: Task<()>, diagnostic_summaries: HashMap, HashMap>>, - pub(super) lsp_server_capabilities: HashMap, + pub lsp_server_capabilities: HashMap, lsp_document_colors: HashMap, lsp_code_lens: HashMap, running_lsp_requests: HashMap>)>, @@ -3565,6 +3567,7 @@ pub struct LanguageServerStatus { pub pending_work: BTreeMap, pub has_pending_diagnostic_updates: bool, progress_tokens: HashSet, + pub worktree: Option, } #[derive(Clone, Debug)] @@ -7483,7 +7486,7 @@ impl LspStore { server: Some(proto::LanguageServer { id: server_id.to_proto(), name: status.name.to_string(), - worktree_id: None, + worktree_id: status.worktree.map(|id| id.to_proto()), }), capabilities: serde_json::to_string(&server.capabilities()) .expect("serializing server LSP capabilities"), @@ -7527,6 +7530,7 @@ impl LspStore { pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), + worktree: server.worktree_id.map(WorktreeId::from_proto), }, ) }) @@ -8892,6 +8896,7 @@ impl LspStore { pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), + worktree: server.worktree_id.map(WorktreeId::from_proto), }, ); cx.emit(LspStoreEvent::LanguageServerAdded( @@ -10905,6 +10910,7 @@ impl LspStore { pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), + worktree: Some(key.worktree_id), }, ); @@ -12190,6 +12196,14 @@ impl LspStore { let data = self.lsp_code_lens.get_mut(&buffer_id)?; Some(data.update.take()?.1) } + + pub fn downstream_client(&self) -> Option<(AnyProtoClient, u64)> { + self.downstream_client.clone() + } + + pub fn worktree_store(&self) -> Entity { + self.worktree_store.clone() + } } // Registration with registerOptions as null, should fallback to true. @@ -12699,45 +12713,69 @@ impl PartialEq for LanguageServerPromptRequest { #[derive(Clone, Debug, PartialEq)] pub enum LanguageServerLogType { Log(MessageType), - Trace(Option), + Trace { verbose_info: Option }, + Rpc { received: bool }, } impl LanguageServerLogType { pub fn to_proto(&self) -> proto::language_server_log::LogType { match self { Self::Log(log_type) => { - let message_type = match *log_type { - MessageType::ERROR => 1, - MessageType::WARNING => 2, - MessageType::INFO => 3, - MessageType::LOG => 4, + use proto::log_message::LogLevel; + let level = match *log_type { + MessageType::ERROR => LogLevel::Error, + MessageType::WARNING => LogLevel::Warning, + MessageType::INFO => LogLevel::Info, + MessageType::LOG => LogLevel::Log, other => { - log::warn!("Unknown lsp log message type: {:?}", other); - 4 + log::warn!("Unknown lsp log message type: {other:?}"); + LogLevel::Log } }; - proto::language_server_log::LogType::LogMessageType(message_type) + proto::language_server_log::LogType::Log(proto::LogMessage { + level: level as i32, + }) } - Self::Trace(message) => { - proto::language_server_log::LogType::LogTrace(proto::LspLogTrace { - message: message.clone(), + Self::Trace { verbose_info } => { + proto::language_server_log::LogType::Trace(proto::TraceMessage { + verbose_info: verbose_info.to_owned(), }) } + Self::Rpc { received } => { + let kind = if *received { + proto::rpc_message::Kind::Received + } else { + proto::rpc_message::Kind::Sent + }; + let kind = kind as i32; + proto::language_server_log::LogType::Rpc(proto::RpcMessage { kind }) + } } } pub fn from_proto(log_type: proto::language_server_log::LogType) -> Self { + use proto::log_message::LogLevel; + use proto::rpc_message; match log_type { - proto::language_server_log::LogType::LogMessageType(message_type) => { - Self::Log(match message_type { - 1 => MessageType::ERROR, - 2 => MessageType::WARNING, - 3 => MessageType::INFO, - 4 => MessageType::LOG, - _ => MessageType::LOG, - }) - } - proto::language_server_log::LogType::LogTrace(trace) => Self::Trace(trace.message), + proto::language_server_log::LogType::Log(message_type) => Self::Log( + match LogLevel::from_i32(message_type.level).unwrap_or(LogLevel::Log) { + LogLevel::Error => MessageType::ERROR, + LogLevel::Warning => MessageType::WARNING, + LogLevel::Info => MessageType::INFO, + LogLevel::Log => MessageType::LOG, + }, + ), + proto::language_server_log::LogType::Trace(trace_message) => Self::Trace { + verbose_info: trace_message.verbose_info, + }, + proto::language_server_log::LogType::Rpc(message) => Self::Rpc { + received: match rpc_message::Kind::from_i32(message.kind) + .unwrap_or(rpc_message::Kind::Received) + { + rpc_message::Kind::Received => true, + rpc_message::Kind::Sent => false, + }, + }, } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9e3900198cbc9aa4845428235196763511c0751c..63ce309e7a1cf9f8c6c3d7868be7ed343d1c010a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -280,6 +280,11 @@ pub enum Event { server_id: LanguageServerId, buffer_id: BufferId, buffer_abs_path: PathBuf, + name: Option, + }, + ToggleLspLogs { + server_id: LanguageServerId, + enabled: bool, }, Toast { notification_id: SharedString, @@ -1001,6 +1006,7 @@ impl Project { client.add_entity_request_handler(Self::handle_open_buffer_by_path); client.add_entity_request_handler(Self::handle_open_new_buffer); client.add_entity_message_handler(Self::handle_create_buffer_for_peer); + client.add_entity_message_handler(Self::handle_toggle_lsp_logs); WorktreeStore::init(&client); BufferStore::init(&client); @@ -2971,6 +2977,7 @@ impl Project { buffer_id, server_id: *language_server_id, buffer_abs_path: PathBuf::from(&update.buffer_abs_path), + name: name.clone(), }); } } @@ -4697,6 +4704,20 @@ impl Project { })? } + async fn handle_toggle_lsp_logs( + project: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result<()> { + project.update(&mut cx, |_, cx| { + cx.emit(Event::ToggleLspLogs { + server_id: LanguageServerId::from_proto(envelope.payload.server_id), + enabled: envelope.payload.enabled, + }) + })?; + Ok(()) + } + async fn handle_synchronize_buffers( this: Entity, envelope: TypedEnvelope, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 6dcd07482e6e3b6ef858f61a04ce312304925dd7..ed15ba845ad815006d5f8de4368a0e61f77c7940 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1951,6 +1951,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC server_id: LanguageServerId(1), buffer_id, buffer_abs_path: PathBuf::from(path!("/dir/a.rs")), + name: Some(fake_server.server.name()) } ); assert_eq!( diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 473ef5c38cc6f401a05556c1f02271e83bd8fa97..16f6217b29d50a4a2eb9198565f688335c218802 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -610,11 +610,36 @@ message ServerMetadataUpdated { message LanguageServerLog { uint64 project_id = 1; uint64 language_server_id = 2; + string message = 3; oneof log_type { - uint32 log_message_type = 3; - LspLogTrace log_trace = 4; + LogMessage log = 4; + TraceMessage trace = 5; + RpcMessage rpc = 6; + } +} + +message LogMessage { + LogLevel level = 1; + + enum LogLevel { + LOG = 0; + INFO = 1; + WARNING = 2; + ERROR = 3; + } +} + +message TraceMessage { + optional string verbose_info = 1; +} + +message RpcMessage { + Kind kind = 1; + + enum Kind { + RECEIVED = 0; + SENT = 1; } - string message = 5; } message LspLogTrace { @@ -932,3 +957,16 @@ message MultiLspQuery { message MultiLspQueryResponse { repeated LspResponse responses = 1; } + +message ToggleLspLogs { + uint64 project_id = 1; + LogType log_type = 2; + uint64 server_id = 3; + bool enabled = 4; + + enum LogType { + LOG = 0; + TRACE = 1; + RPC = 2; + } +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 70689bcd6306195fce0d5c6449bf3dd9f5d43539..2222bdec082759cb75ffcdb2c7a95435f36eba11 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -396,7 +396,8 @@ message Envelope { GitCloneResponse git_clone_response = 364; LspQuery lsp_query = 365; - LspQueryResponse lsp_query_response = 366; // current max + LspQueryResponse lsp_query_response = 366; + ToggleLspLogs toggle_lsp_logs = 367; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index e17ec5203bd5b7bcab03c6461c343156116cc563..04495fb898b1d9bdbf229bb69e1e44b8afa6d1fb 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -312,7 +312,8 @@ messages!( (GetDefaultBranch, Background), (GetDefaultBranchResponse, Background), (GitClone, Background), - (GitCloneResponse, Background) + (GitCloneResponse, Background), + (ToggleLspLogs, Background), ); request_messages!( @@ -481,7 +482,8 @@ request_messages!( (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse), (PullWorkspaceDiagnostics, Ack), (GetDefaultBranch, GetDefaultBranchResponse), - (GitClone, GitCloneResponse) + (GitClone, GitCloneResponse), + (ToggleLspLogs, Ack), ); lsp_messages!( @@ -612,6 +614,7 @@ entity_messages!( GitReset, GitCheckoutFiles, SetIndexText, + ToggleLspLogs, Push, Fetch, diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 5dbb9a2771c4e3fda04ed014f993f843b44dd976..249968b246c0615d1a8d60c4446eeac6bcac7451 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -43,6 +43,7 @@ gpui_tokio.workspace = true http_client.workspace = true language.workspace = true language_extension.workspace = true +language_tools.workspace = true languages.workspace = true log.workspace = true lsp.workspace = true diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 04028ebcac82f814652f32ad7439e32d650f5ad0..1e197fdd338432a7731933cb8051bb2bb4265c18 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -1,5 +1,7 @@ use ::proto::{FromProto, ToProto}; use anyhow::{Context as _, Result, anyhow}; +use language_tools::lsp_log::{GlobalLogStore, LanguageServerKind}; +use lsp::LanguageServerId; use extension::ExtensionHostProxy; use extension_host::headless_host::HeadlessExtensionStore; @@ -65,6 +67,7 @@ impl HeadlessProject { settings::init(cx); language::init(cx); project::Project::init_settings(cx); + language_tools::lsp_log::init(false, cx); } pub fn new( @@ -235,6 +238,7 @@ impl HeadlessProject { session.add_entity_request_handler(Self::handle_open_new_buffer); session.add_entity_request_handler(Self::handle_find_search_candidates); session.add_entity_request_handler(Self::handle_open_server_settings); + session.add_entity_message_handler(Self::handle_toggle_lsp_logs); session.add_entity_request_handler(BufferStore::handle_update_buffer); session.add_entity_message_handler(BufferStore::handle_close_buffer); @@ -298,11 +302,40 @@ impl HeadlessProject { fn on_lsp_store_event( &mut self, - _lsp_store: Entity, + lsp_store: Entity, event: &LspStoreEvent, cx: &mut Context, ) { match event { + LspStoreEvent::LanguageServerAdded(id, name, worktree_id) => { + let log_store = cx + .try_global::() + .and_then(|lsp_logs| lsp_logs.0.upgrade()); + if let Some(log_store) = log_store { + log_store.update(cx, |log_store, cx| { + log_store.add_language_server( + LanguageServerKind::LocalSsh { + lsp_store: self.lsp_store.downgrade(), + }, + *id, + Some(name.clone()), + *worktree_id, + lsp_store.read(cx).language_server_for_id(*id), + cx, + ); + }); + } + } + LspStoreEvent::LanguageServerRemoved(id) => { + let log_store = cx + .try_global::() + .and_then(|lsp_logs| lsp_logs.0.upgrade()); + if let Some(log_store) = log_store { + log_store.update(cx, |log_store, cx| { + log_store.remove_language_server(*id, cx); + }); + } + } LspStoreEvent::LanguageServerUpdate { language_server_id, name, @@ -326,16 +359,6 @@ impl HeadlessProject { }) .log_err(); } - LspStoreEvent::LanguageServerLog(language_server_id, log_type, message) => { - self.session - .send(proto::LanguageServerLog { - project_id: REMOTE_SERVER_PROJECT_ID, - language_server_id: language_server_id.to_proto(), - message: message.clone(), - log_type: Some(log_type.to_proto()), - }) - .log_err(); - } LspStoreEvent::LanguageServerPrompt(prompt) => { let request = self.session.request(proto::LanguageServerPromptRequest { project_id: REMOTE_SERVER_PROJECT_ID, @@ -509,7 +532,31 @@ impl HeadlessProject { }) } - pub async fn handle_open_server_settings( + async fn handle_toggle_lsp_logs( + _: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result<()> { + let server_id = LanguageServerId::from_proto(envelope.payload.server_id); + let lsp_logs = cx + .update(|cx| { + cx.try_global::() + .and_then(|lsp_logs| lsp_logs.0.upgrade()) + })? + .context("lsp logs store is missing")?; + + lsp_logs.update(&mut cx, |lsp_logs, _| { + // we do not support any other log toggling yet + if envelope.payload.enabled { + lsp_logs.enable_rpc_trace_for_language_server(server_id); + } else { + lsp_logs.disable_rpc_trace_for_language_server(server_id); + } + })?; + Ok(()) + } + + async fn handle_open_server_settings( this: Entity, _: TypedEnvelope, mut cx: AsyncApp, @@ -562,7 +609,7 @@ impl HeadlessProject { }) } - pub async fn handle_find_search_candidates( + async fn handle_find_search_candidates( this: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, @@ -594,7 +641,7 @@ impl HeadlessProject { Ok(response) } - pub async fn handle_list_remote_directory( + async fn handle_list_remote_directory( this: Entity, envelope: TypedEnvelope, cx: AsyncApp, @@ -626,7 +673,7 @@ impl HeadlessProject { }) } - pub async fn handle_get_path_metadata( + async fn handle_get_path_metadata( this: Entity, envelope: TypedEnvelope, cx: AsyncApp, @@ -644,7 +691,7 @@ impl HeadlessProject { }) } - pub async fn handle_shutdown_remote_server( + async fn handle_shutdown_remote_server( _this: Entity, _envelope: TypedEnvelope, cx: AsyncApp, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 1966755d626af6e155440379982af180e9ccbc95..a0717333159e508ea42a1b95bd9f2226e6392871 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -30,7 +30,7 @@ pub struct ActiveSettingsProfileName(pub String); impl Global for ActiveSettingsProfileName {} -#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord, serde::Serialize)] pub struct WorktreeId(usize); impl From for usize { diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 869aa5322eba7fdaf417606dd62ae73a0c3702b3..ce9be0c7edb47106fced94485d5e5d30abe0dedb 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -13,6 +13,7 @@ path = "src/workspace.rs" doctest = false [features] +default = ["call"] test-support = [ "call/test-support", "client/test-support", @@ -29,7 +30,7 @@ test-support = [ any_vec.workspace = true anyhow.workspace = true async-recursion.workspace = true -call.workspace = true +call = { workspace = true, optional = true } client.workspace = true clock.workspace = true collections.workspace = true diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 9c2d09fd26308b95ab145b557a516b5e6603a0e4..5c0814803173dc33fba4b50fc663d70c15cc0694 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -4,11 +4,14 @@ use crate::{ workspace_settings::{PaneSplitDirectionHorizontal, PaneSplitDirectionVertical}, }; use anyhow::Result; + +#[cfg(feature = "call")] use call::{ActiveCall, ParticipantLocation}; + use collections::HashMap; use gpui::{ - Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, MouseButton, Pixels, - Point, StyleRefinement, WeakEntity, Window, point, size, + Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, Pixels, Point, + StyleRefinement, WeakEntity, Window, point, size, }; use parking_lot::Mutex; use project::Project; @@ -197,6 +200,7 @@ pub enum Member { pub struct PaneRenderContext<'a> { pub project: &'a Entity, pub follower_states: &'a HashMap, + #[cfg(feature = "call")] pub active_call: Option<&'a Entity>, pub active_pane: &'a Entity, pub app_state: &'a Arc, @@ -258,6 +262,11 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> { let mut leader_color; let status_box; match leader_id { + #[cfg(not(feature = "call"))] + CollaboratorId::PeerId(_) => { + return LeaderDecoration::default(); + } + #[cfg(feature = "call")] CollaboratorId::PeerId(peer_id) => { let Some(leader) = self.active_call.as_ref().and_then(|call| { let room = call.read(cx).room()?.read(cx); @@ -315,7 +324,7 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> { |this, (leader_project_id, leader_user_id)| { let app_state = self.app_state.clone(); this.cursor_pointer().on_mouse_down( - MouseButton::Left, + gpui::MouseButton::Left, move |_, _, cx| { crate::join_in_room_project( leader_project_id, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 25e2cb1cfe934a88ec4cc3811bf3216e0765c0af..b6577ff325c1f5a4ff0d9c861f357fb8f4007696 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -9,6 +9,7 @@ pub mod pane_group; mod path_list; mod persistence; pub mod searchable; +#[cfg(feature = "call")] pub mod shared_screen; mod status_bar; pub mod tasks; @@ -22,11 +23,17 @@ pub use dock::Panel; pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; -use anyhow::{Context as _, Result, anyhow}; +#[cfg(feature = "call")] use call::{ActiveCall, call_settings::CallSettings}; +#[cfg(feature = "call")] +use client::{Status, proto::ErrorCode}; +#[cfg(feature = "call")] +use shared_screen::SharedScreen; + +use anyhow::{Context as _, Result, anyhow}; use client::{ - ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore, - proto::{self, ErrorCode, PanelId, PeerId}, + ChannelId, Client, ErrorExt, TypedEnvelope, UserStore, + proto::{self, PanelId, PeerId}, }; use collections::{HashMap, HashSet, hash_map}; use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; @@ -79,7 +86,6 @@ use schemars::JsonSchema; use serde::Deserialize; use session::AppSession; use settings::{Settings, update_settings_file}; -use shared_screen::SharedScreen; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, @@ -886,6 +892,7 @@ impl Global for GlobalAppState {} pub struct WorkspaceStore { workspaces: HashSet>, + #[cfg(feature = "call")] client: Arc, _subscriptions: Vec, } @@ -1117,6 +1124,7 @@ pub struct Workspace { window_edited: bool, last_window_title: Option, dirty_items: HashMap, + #[cfg(feature = "call")] active_call: Option<(Entity, Vec)>, leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: Option, @@ -1158,6 +1166,7 @@ pub struct FollowerState { struct FollowerView { view: Box, + #[cfg(feature = "call")] location: Option, } @@ -1357,10 +1366,15 @@ impl Workspace { let session_id = app_state.session.read(cx).id().to_owned(); + #[cfg(feature = "call")] let mut active_call = None; - if let Some(call) = ActiveCall::try_global(cx) { - let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)]; - active_call = Some((call, subscriptions)); + #[cfg(feature = "call")] + { + if let Some(call) = ActiveCall::try_global(cx) { + let subscriptions = + vec![cx.subscribe_in(&call, window, Self::on_active_call_event)]; + active_call = Some((call, subscriptions)); + } } let (serializable_items_tx, serializable_items_rx) = @@ -1446,6 +1460,7 @@ impl Workspace { window_edited: false, last_window_title: None, dirty_items: Default::default(), + #[cfg(feature = "call")] active_call, database_id: workspace_id, app_state, @@ -2250,6 +2265,7 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) -> Task> { + #[cfg(feature = "call")] let active_call = self.active_call().cloned(); // On Linux and Windows, closing the last window should restore the last workspace. @@ -2258,51 +2274,58 @@ impl Workspace { && cx.windows().len() == 1; cx.spawn_in(window, async move |this, cx| { - let workspace_count = cx.update(|_window, cx| { - cx.windows() - .iter() - .filter(|window| window.downcast::().is_some()) - .count() - })?; - - if let Some(active_call) = active_call - && workspace_count == 1 - && active_call.read_with(cx, |call, _| call.room().is_some())? + #[cfg(feature = "call")] { - if close_intent == CloseIntent::CloseWindow { - let answer = cx.update(|window, cx| { - window.prompt( - PromptLevel::Warning, - "Do you want to leave the current call?", - None, - &["Close window and hang up", "Cancel"], - cx, - ) - })?; + let workspace_count = cx.update(|_window, cx| { + cx.windows() + .iter() + .filter(|window| window.downcast::().is_some()) + .count() + })?; + if let Some(active_call) = active_call + && workspace_count == 1 + && active_call.read_with(cx, |call, _| call.room().is_some())? + { + if close_intent == CloseIntent::CloseWindow { + let answer = cx.update(|window, cx| { + window.prompt( + PromptLevel::Warning, + "Do you want to leave the current call?", + None, + &["Close window and hang up", "Cancel"], + cx, + ) + })?; - if answer.await.log_err() == Some(1) { - return anyhow::Ok(false); - } else { - active_call - .update(cx, |call, cx| call.hang_up(cx))? - .await - .log_err(); + if answer.await.log_err() == Some(1) { + return anyhow::Ok(false); + } else { + { + active_call + .update(cx, |call, cx| call.hang_up(cx))? + .await + .log_err(); + } + } } - } - if close_intent == CloseIntent::ReplaceWindow { - _ = active_call.update(cx, |this, cx| { - let workspace = cx - .windows() - .iter() - .filter_map(|window| window.downcast::()) - .next() - .unwrap(); - let project = workspace.read(cx)?.project.clone(); - if project.read(cx).is_shared() { - this.unshare_project(project, cx)?; + if close_intent == CloseIntent::ReplaceWindow { + #[cfg(feature = "call")] + { + _ = active_call.update(cx, |active_call, cx| { + let workspace = cx + .windows() + .iter() + .filter_map(|window| window.downcast::()) + .next() + .unwrap(); + let project = workspace.read(cx)?.project.clone(); + if project.read(cx).is_shared() { + active_call.unshare_project(project, cx)?; + } + anyhow::Ok(()) + })?; } - Ok::<_, anyhow::Error>(()) - })?; + } } } @@ -3486,6 +3509,7 @@ impl Workspace { item } + #[cfg(feature = "call")] pub fn open_shared_screen( &mut self, peer_id: PeerId, @@ -3907,8 +3931,11 @@ impl Workspace { pane.update(cx, |pane, _| { pane.track_alternate_file_items(); }); - if *local { - self.unfollow_in_pane(pane, window, cx); + #[cfg(feature = "call")] + { + if *local { + self.unfollow_in_pane(pane, window, cx); + } } serialize_workspace = *focus_changed || pane != self.active_pane(); if pane == self.active_pane() { @@ -3973,6 +4000,17 @@ impl Workspace { } } + #[cfg(not(feature = "call"))] + pub fn unfollow_in_pane( + &mut self, + _pane: &Entity, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + None + } + + #[cfg(feature = "call")] pub fn unfollow_in_pane( &mut self, pane: &Entity, @@ -4122,6 +4160,7 @@ impl Workspace { cx.notify(); } + #[cfg(feature = "call")] pub fn start_following( &mut self, leader_id: impl Into, @@ -4185,6 +4224,16 @@ impl Workspace { } } + #[cfg(not(feature = "call"))] + pub fn follow_next_collaborator( + &mut self, + _: &FollowNextCollaborator, + _window: &mut Window, + _cx: &mut Context, + ) { + } + + #[cfg(feature = "call")] pub fn follow_next_collaborator( &mut self, _: &FollowNextCollaborator, @@ -4233,6 +4282,16 @@ impl Workspace { } } + #[cfg(not(feature = "call"))] + pub fn follow( + &mut self, + _leader_id: impl Into, + _window: &mut Window, + _cx: &mut Context, + ) { + } + + #[cfg(feature = "call")] pub fn follow( &mut self, leader_id: impl Into, @@ -4285,6 +4344,17 @@ impl Workspace { } } + #[cfg(not(feature = "call"))] + pub fn unfollow( + &mut self, + _leader_id: impl Into, + _window: &mut Window, + _cx: &mut Context, + ) -> Option<()> { + None + } + + #[cfg(feature = "call")] pub fn unfollow( &mut self, leader_id: impl Into, @@ -4595,6 +4665,7 @@ impl Workspace { anyhow::bail!("no id for view"); }; let id = ViewId::from_proto(id)?; + #[cfg(feature = "call")] let panel_id = view.panel_id.and_then(proto::PanelId::from_i32); let pane = this.update(cx, |this, _cx| { @@ -4667,6 +4738,7 @@ impl Workspace { id, FollowerView { view: item, + #[cfg(feature = "call")] location: panel_id, }, ); @@ -4721,6 +4793,7 @@ impl Workspace { view.map(|view| { entry.insert(FollowerView { view, + #[cfg(feature = "call")] location: None, }) }) @@ -4911,6 +4984,17 @@ impl Workspace { ) } + #[cfg(not(feature = "call"))] + fn active_item_for_peer( + &self, + _peer_id: PeerId, + _window: &mut Window, + _cx: &mut Context, + ) -> Option<(Option, Box)> { + None + } + + #[cfg(feature = "call")] fn active_item_for_peer( &self, peer_id: PeerId, @@ -4952,6 +5036,7 @@ impl Workspace { item_to_activate } + #[cfg(feature = "call")] fn shared_screen_for_peer( &self, peer_id: PeerId, @@ -5002,10 +5087,12 @@ impl Workspace { } } + #[cfg(feature = "call")] pub fn active_call(&self) -> Option<&Entity> { self.active_call.as_ref().map(|(call, _)| call) } + #[cfg(feature = "call")] fn on_active_call_event( &mut self, _: &Entity, @@ -5918,6 +6005,17 @@ impl Workspace { } } +#[cfg(not(feature = "call"))] +fn leader_border_for_pane( + _follower_states: &HashMap, + _pane: &Entity, + _: &Window, + _cx: &App, +) -> Option
{ + None +} + +#[cfg(feature = "call")] fn leader_border_for_pane( follower_states: &HashMap, pane: &Entity, @@ -6384,6 +6482,7 @@ impl Render for Workspace { &PaneRenderContext { follower_states: &self.follower_states, + #[cfg(feature = "call")] active_call: self.active_call(), active_pane: &self.active_pane, app_state: &self.app_state, @@ -6448,6 +6547,7 @@ impl Render for Workspace { &PaneRenderContext { follower_states: &self.follower_states, + #[cfg(feature = "call")] active_call: self.active_call(), active_pane: &self.active_pane, app_state: &self.app_state, @@ -6510,6 +6610,7 @@ impl Render for Workspace { &PaneRenderContext { follower_states: &self.follower_states, + #[cfg(feature = "call")] active_call: self.active_call(), active_pane: &self.active_pane, app_state: &self.app_state, @@ -6558,6 +6659,7 @@ impl Render for Workspace { &PaneRenderContext { follower_states: &self.follower_states, + #[cfg(feature = "call")] active_call: self.active_call(), active_pane: &self.active_pane, app_state: &self.app_state, @@ -6631,10 +6733,22 @@ impl WorkspaceStore { client.add_request_handler(cx.weak_entity(), Self::handle_follow), client.add_message_handler(cx.weak_entity(), Self::handle_update_followers), ], + #[cfg(feature = "call")] client, } } + #[cfg(not(feature = "call"))] + pub fn update_followers( + &self, + _project_id: Option, + _update: proto::update_followers::Variant, + _cx: &App, + ) -> Option<()> { + None + } + + #[cfg(feature = "call")] pub fn update_followers( &self, project_id: Option, @@ -6800,6 +6914,7 @@ actions!( ] ); +#[cfg(feature = "call")] async fn join_channel_internal( channel_id: ChannelId, app_state: &Arc, @@ -6947,6 +7062,17 @@ async fn join_channel_internal( anyhow::Ok(false) } +#[cfg(not(feature = "call"))] +pub fn join_channel( + _channel_id: ChannelId, + _app_state: Arc, + _requesting_window: Option>, + _cx: &mut App, +) -> Task> { + Task::ready(Ok(())) +} + +#[cfg(feature = "call")] pub fn join_channel( channel_id: ChannelId, app_state: Arc, @@ -7454,6 +7580,17 @@ fn serialize_ssh_project( }) } +#[cfg(not(feature = "call"))] +pub fn join_in_room_project( + _project_id: u64, + _follow_user_id: u64, + _app_state: Arc, + _cx: &mut App, +) -> Task> { + Task::ready(Ok(())) +} + +#[cfg(feature = "call")] pub fn join_in_room_project( project_id: u64, follow_user_id: u64, From d43cf2c4866be7226bc0d5120e5c830e3f18b7f9 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 27 Aug 2025 15:10:10 -0400 Subject: [PATCH 389/744] Link out to release channel FAQ in Docs (#37029) This PR links users to the FAQ on the release channels, which has more in-depth coverage of the process. Release Notes: - N/A --- docs/src/development/releases.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/src/development/releases.md b/docs/src/development/releases.md index 5b821f3cf2046b982ed4ca99fe729075174cf532..52c66c3420237b6c54f03582bb008834fe78a496 100644 --- a/docs/src/development/releases.md +++ b/docs/src/development/releases.md @@ -1,13 +1,6 @@ # Zed Releases -Zed currently maintains two public releases for macOS: - -- [Stable](https://zed.dev/download): This is the primary version that people download and use. -- [Preview](https://zed.dev/releases/preview): which receives updates a week ahead of Stable for early adopters. - -Typically we cut a new minor release every Wednesday. The current Preview becomes Stable, and the new Preview contains everything on main up until that point. - -If bugs are found and fixed during the week, they may be cherry-picked into the release branches and so new patch versions for preview and stable can become available throughout the week. +Read about Zed's release channels [here](https://zed.dev/faq#what-are-the-release-channels). ## Wednesday release process From 58f896e5cd7309d4526c136314e6ddb934ed1c3d Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 27 Aug 2025 15:59:08 -0400 Subject: [PATCH 390/744] Update Wednesday release process docs (#37033) Release Notes: - N/A --- docs/src/development/releases.md | 62 +++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/docs/src/development/releases.md b/docs/src/development/releases.md index 52c66c3420237b6c54f03582bb008834fe78a496..80458b0cdf88faa67f5367ae69e0a12c3d094351 100644 --- a/docs/src/development/releases.md +++ b/docs/src/development/releases.md @@ -4,20 +4,56 @@ Read about Zed's release channels [here](https://zed.dev/faq#what-are-the-releas ## Wednesday release process -You will need write access to the Zed repository to do this: +You will need write access to the Zed repository to do this. -- Checkout `main` and ensure your working copy is clean. -- Run `./script/bump-zed-minor-versions` and push the tags - and branches as instructed. -- Wait for the builds to appear on [the Releases tab on GitHub](https://github.com/zed-industries/zed/releases) (typically takes around 30 minutes) -- While you're waiting: - - Start creating the new release notes for preview. You can start with the output of `./script/get-preview-channel-changes`. - - Start drafting the release tweets. -- Once the builds are ready: - - Copy the release notes from the previous Preview release(s) to the current Stable release. - - Download the artifacts for each release and test that you can run them locally. - - Publish the releases on GitHub. - - Tweet the tweets (Credentials are in 1Password). +Credentials for various services used in this process can be found in 1Password. + +--- + +1. Checkout `main` and ensure your working copy is clean. + +1. Run `git fetch && git pull` to ensure you have the latest commits locally. + +1. Run `git fetch --tags --force` to forcibly ensure your local tags are in sync with the remote. + +1. Run `./script/get-stable-channel-release-notes`. + + - Follow the instructions at the end of the script and aggregate the release notes into one structure. + +1. Run `./script/bump-zed-minor-versions`. + + - Push the tags and branches as instructed. + +1. Run `./script/get-preview-channel-changes`. + + - Take the script's output and build release notes by organizing each release note line into a category. + - Use a prior release for the initial outline. + - Make sure to append the `Credit` line, if present, to the end of the release note line. + +1. Once release drafts are up on [GitHub Releases](https://github.com/zed-industries/zed/releases), paste both preview and stable release notes into each and **save**. + + - **Do not publish the drafts, yet.** + +1. Check the release assets. + + - Ensure the stable and preview release jobs have finished without error. + - Ensure each build has the proper number of assets—releases currently have 10 assets each. + - Download the artifacts for each release and test that you can run them locally. + +1. Publish each build, one at a time. + + - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the status of each build. + +1. Publish the release email that has been sent to [Kit](https://kit.com). + + - Make sure to double check that the email is correct before publishing. + - We sometimes correct things here and there that didn't translate from GitHub's renderer to Kit's. + +1. Build social media posts based on the popular items in stable. + + - You can use the [prior week's post chain](https://zed.dev/channel/tweets-23331) as your outline. + - Stage the copy and assets using [Buffer](https://buffer.com), for both X and BlueSky. + - Publish both, one at a time, ensuring both are posted to each respective platform. ## Patch release process From 5444fbd8fed6865e25546dcbf83dcf2d7b24af48 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:59:56 +0200 Subject: [PATCH 391/744] python: Look for local venvs in all directories between root of the worktree and current pyproject.toml (#37037) cc @michael-ud - if you can build Zed, I'd appreciate it if you could give this a go with your project. Otherwise I can provide a link to download of current nightly via an e-mail for you to try out (if you want). This change will land in Preview (if merged) on next Wednesday and then it'll be in Stable a week after that. Related to: #20402 Release Notes: - python: Zed now searches for virtual environments in intermediate directories between a root of the worktree and the location of pyproject.toml applicable to the currently focused file. --- crates/language/src/toolchain.rs | 2 +- crates/languages/src/python.rs | 18 ++++++++++-------- crates/project/src/project_tests.rs | 7 ++++--- crates/project/src/toolchain_store.rs | 7 +------ 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 73c142c8ca02c986b0602c1f19a8c479c041f6f7..66879e56da0a4a7cdac5b64acbce9e31313bf373 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -52,7 +52,7 @@ pub trait ToolchainLister: Send + Sync { async fn list( &self, worktree_root: PathBuf, - subroot_relative_path: Option>, + subroot_relative_path: Arc, project_env: Option>, ) -> ToolchainList; // Returns a term which we should use in UI to refer to a toolchain. diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index d21b5dabd34c311d6e08140b2bc7ed363f79f273..0f78d5c5dfaaf3f24a337cab32ed9656354ac911 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -759,7 +759,7 @@ impl ToolchainLister for PythonToolchainProvider { async fn list( &self, worktree_root: PathBuf, - subroot_relative_path: Option>, + subroot_relative_path: Arc, project_env: Option>, ) -> ToolchainList { let env = project_env.unwrap_or_default(); @@ -771,13 +771,15 @@ impl ToolchainLister for PythonToolchainProvider { ); let mut config = Configuration::default(); - let mut directories = vec![worktree_root.clone()]; - if let Some(subroot_relative_path) = subroot_relative_path { - debug_assert!(subroot_relative_path.is_relative()); - directories.push(worktree_root.join(subroot_relative_path)); - } - - config.workspace_directories = Some(directories); + debug_assert!(subroot_relative_path.is_relative()); + // `.ancestors()` will yield at least one path, so in case of empty `subroot_relative_path`, we'll just use + // worktree root as the workspace directory. + config.workspace_directories = Some( + subroot_relative_path + .ancestors() + .map(|ancestor| worktree_root.join(ancestor)) + .collect(), + ); for locator in locators.iter() { locator.configure(&config); } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index ed15ba845ad815006d5f8de4368a0e61f77c7940..a8f911883d619214a35f3ef5d80f83d6dc1b3894 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -9189,13 +9189,14 @@ fn python_lang(fs: Arc) -> Arc { async fn list( &self, worktree_root: PathBuf, - subroot_relative_path: Option>, + subroot_relative_path: Arc, _: Option>, ) -> ToolchainList { // This lister will always return a path .venv directories within ancestors let ancestors = subroot_relative_path - .into_iter() - .flat_map(|path| path.ancestors().map(ToOwned::to_owned).collect::>()); + .ancestors() + .map(ToOwned::to_owned) + .collect::>(); let mut toolchains = vec![]; for ancestor in ancestors { let venv_path = worktree_root.join(ancestor).join(".venv"); diff --git a/crates/project/src/toolchain_store.rs b/crates/project/src/toolchain_store.rs index ac87e6424821a5d28dbf48b92b077183a21d8608..57d492e26fc7b59df02df0128ed6b9ade132c6d9 100644 --- a/crates/project/src/toolchain_store.rs +++ b/crates/project/src/toolchain_store.rs @@ -389,12 +389,7 @@ impl LocalToolchainStore { cx.background_spawn(async move { Some(( toolchains - .list( - worktree_root, - Some(relative_path.path.clone()) - .filter(|_| *relative_path.path != *Path::new("")), - project_env, - ) + .list(worktree_root, relative_path.path.clone(), project_env) .await, relative_path.path, )) From 4e4bfd6f4ea40e88aedf819f41dcba3d20835dc9 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 27 Aug 2025 17:07:32 -0400 Subject: [PATCH 392/744] editor: Add "Wrap Selections in Tag" action (#36948) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds the ability for a user to select one or more blocks of text and wrap each selection in an HTML tag — which works by placing multiple cursors inside the open and close tags so the appropriate element name can be typed in to all places simultaneously. This is similar to the emmet "Wrap with Abbreviation" functionality discussed in #15588 but is a simpler version that does not rely on Emmet's language server. Here's a preview of the feature in action: https://github.com/user-attachments/assets/1931e717-136c-4766-a585-e4ba939d9adf Some notes and questions: - The current implementation is a hardcoded with regards to supported languages. I'd love some direction on how much of this information to push into the relevant language structs. - I can see this feature as something that languages added by an extension would want to enable support for — is this something you'd want? - The syntax is hardcoded to support HTML/XML/JSX-like languages. I don't suppose this is a problem but figured I'd point it out anyway. - I called it "Wrap in tag" but open to whatever naming you feel is appropriate. - The implementation doesn't use `manipulate_lines` — I wasn't sure how make use of that without extra overhead / bookkeeping — does this seem fine? - I could also investigate adding wrap in abbreviation support by communicating with the Emmet language server but I think I'll need some direction on how to handle Emmet's custom LSP message. I could do this either in addition to or instead of this feature — though imo this feature is a nice "shortcut" regardless. Release Notes: - Added a new "Wrap Selections in Tag" action that lets you wrap one or more selections in tags based on language. Works in HTML, JSX, and similar languages, and places cursors inside both opening and closing tags so you can type the tag name once and apply it everywhere. --------- Co-authored-by: Smit Barmase --- crates/editor/src/actions.rs | 4 +- crates/editor/src/editor.rs | 80 +++++++++++++ crates/editor/src/editor_tests.rs | 123 ++++++++++++++++++++ crates/editor/src/element.rs | 3 + crates/language/src/language.rs | 16 +++ crates/languages/src/javascript/config.toml | 1 + crates/languages/src/tsx/config.toml | 1 + crates/languages/src/typescript/config.toml | 1 + extensions/html/languages/html/config.toml | 1 + 9 files changed, 229 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index ce02c4d2bf39c6bc5513280a1d81b071a9e6cd6a..3cc6c28464449907abbd19235f9123e44cca78ba 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -753,6 +753,8 @@ actions!( UniqueLinesCaseInsensitive, /// Removes duplicate lines (case-sensitive). UniqueLinesCaseSensitive, - UnwrapSyntaxNode + UnwrapSyntaxNode, + /// Wraps selections in tag specified by language. + WrapSelectionsInTag ] ); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 52549902dd603ee8ffdc7c50dd331c87c95828cb..2d96ddf7a4eccdf89cc52389aec996b0777afd32 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10447,6 +10447,86 @@ impl Editor { }) } + fn enable_wrap_selections_in_tag(&self, cx: &App) -> bool { + let snapshot = self.buffer.read(cx).snapshot(cx); + for selection in self.selections.disjoint_anchors().iter() { + if snapshot + .language_at(selection.start) + .and_then(|lang| lang.config().wrap_characters.as_ref()) + .is_some() + { + return true; + } + } + false + } + + fn wrap_selections_in_tag( + &mut self, + _: &WrapSelectionsInTag, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); + + let snapshot = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + let mut boundaries = Vec::new(); + + for selection in self.selections.all::(cx).iter() { + let Some(wrap_config) = snapshot + .language_at(selection.start) + .and_then(|lang| lang.config().wrap_characters.clone()) + else { + continue; + }; + + let open_tag = format!("{}{}", wrap_config.start_prefix, wrap_config.start_suffix); + let close_tag = format!("{}{}", wrap_config.end_prefix, wrap_config.end_suffix); + + let start_before = snapshot.anchor_before(selection.start); + let end_after = snapshot.anchor_after(selection.end); + + edits.push((start_before..start_before, open_tag)); + edits.push((end_after..end_after, close_tag)); + + boundaries.push(( + start_before, + end_after, + wrap_config.start_prefix.len(), + wrap_config.end_suffix.len(), + )); + } + + if edits.is_empty() { + return; + } + + self.transact(window, cx, |this, window, cx| { + let buffer = this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + buffer.snapshot(cx) + }); + + let mut new_selections = Vec::with_capacity(boundaries.len() * 2); + for (start_before, end_after, start_prefix_len, end_suffix_len) in + boundaries.into_iter() + { + let open_offset = start_before.to_offset(&buffer) + start_prefix_len; + let close_offset = end_after.to_offset(&buffer).saturating_sub(end_suffix_len); + new_selections.push(open_offset..open_offset); + new_selections.push(close_offset..close_offset); + } + + this.change_selections(Default::default(), window, cx, |s| { + s.select_ranges(new_selections); + }); + + this.request_autoscroll(Autoscroll::fit(), cx); + }); + } + pub fn reload_file(&mut self, _: &ReloadFile, window: &mut Window, cx: &mut Context) { let Some(project) = self.project.clone() else { return; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 2cfdb92593e2250a5615eb4d4d545c1552d13ecc..85471c7ce96e172f7bd5ade399ed0ba1cd6d4a02 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -4403,6 +4403,129 @@ async fn test_unique_lines_single_selection(cx: &mut TestAppContext) { "}); } +#[gpui::test] +async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let js_language = Arc::new(Language::new( + LanguageConfig { + name: "JavaScript".into(), + wrap_characters: Some(language::WrapCharactersConfig { + start_prefix: "<".into(), + start_suffix: ">".into(), + end_prefix: "".into(), + }), + ..LanguageConfig::default() + }, + None, + )); + + cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx)); + + cx.set_state(indoc! {" + «testˇ» + "}); + cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx)); + cx.assert_editor_state(indoc! {" + <«ˇ»>test + "}); + + cx.set_state(indoc! {" + «test + testˇ» + "}); + cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx)); + cx.assert_editor_state(indoc! {" + <«ˇ»>test + test + "}); + + cx.set_state(indoc! {" + teˇst + "}); + cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx)); + cx.assert_editor_state(indoc! {" + te<«ˇ»>st + "}); +} + +#[gpui::test] +async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let js_language = Arc::new(Language::new( + LanguageConfig { + name: "JavaScript".into(), + wrap_characters: Some(language::WrapCharactersConfig { + start_prefix: "<".into(), + start_suffix: ">".into(), + end_prefix: "".into(), + }), + ..LanguageConfig::default() + }, + None, + )); + + cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx)); + + cx.set_state(indoc! {" + «testˇ» + «testˇ» «testˇ» + «testˇ» + "}); + cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx)); + cx.assert_editor_state(indoc! {" + <«ˇ»>test + <«ˇ»>test <«ˇ»>test + <«ˇ»>test + "}); + + cx.set_state(indoc! {" + «test + testˇ» + «test + testˇ» + "}); + cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx)); + cx.assert_editor_state(indoc! {" + <«ˇ»>test + test + <«ˇ»>test + test + "}); +} + +#[gpui::test] +async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let plaintext_language = Arc::new(Language::new( + LanguageConfig { + name: "Plain Text".into(), + ..LanguageConfig::default() + }, + None, + )); + + cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx)); + + cx.set_state(indoc! {" + «testˇ» + "}); + cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx)); + cx.assert_editor_state(indoc! {" + «testˇ» + "}); +} + #[gpui::test] async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 91034829f7896600e690d8438bf7de23d4d19983..a63c18e003907f16a1383bbfb12085e1044d9eb9 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -585,6 +585,9 @@ impl EditorElement { register_action(editor, window, Editor::edit_log_breakpoint); register_action(editor, window, Editor::enable_breakpoint); register_action(editor, window, Editor::disable_breakpoint); + if editor.read(cx).enable_wrap_selections_in_tag(cx) { + register_action(editor, window, Editor::wrap_selections_in_tag); + } } fn register_key_listeners(&self, window: &mut Window, _: &mut App, layout: &EditorLayout) { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7ae77c9141d35363975f07b91b45f032da62d21f..b349122193f1f31b323e03ff0421dfc3705c92fa 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -720,6 +720,9 @@ pub struct LanguageConfig { /// How to soft-wrap long lines of text. #[serde(default)] pub soft_wrap: Option, + /// When set, selections can be wrapped using prefix/suffix pairs on both sides. + #[serde(default)] + pub wrap_characters: Option, /// The name of a Prettier parser that will be used for this language when no file path is available. /// If there's a parser name in the language settings, that will be used instead. #[serde(default)] @@ -923,6 +926,7 @@ impl Default for LanguageConfig { hard_tabs: None, tab_size: None, soft_wrap: None, + wrap_characters: None, prettier_parser_name: None, hidden: false, jsx_tag_auto_close: None, @@ -932,6 +936,18 @@ impl Default for LanguageConfig { } } +#[derive(Clone, Debug, Deserialize, JsonSchema)] +pub struct WrapCharactersConfig { + /// Opening token split into a prefix and suffix. The first caret goes + /// after the prefix (i.e., between prefix and suffix). + pub start_prefix: String, + pub start_suffix: String, + /// Closing token split into a prefix and suffix. The second caret goes + /// after the prefix (i.e., between prefix and suffix). + pub end_prefix: String, + pub end_suffix: String, +} + fn auto_indent_using_last_non_empty_line_default() -> bool { true } diff --git a/crates/languages/src/javascript/config.toml b/crates/languages/src/javascript/config.toml index 0df57d985e82595bdabb97517f56e79591343e7b..128eac0e4dda2b5b437c494e862970c23a8df3a1 100644 --- a/crates/languages/src/javascript/config.toml +++ b/crates/languages/src/javascript/config.toml @@ -6,6 +6,7 @@ first_line_pattern = '^#!.*\b(?:[/ ]node|deno run.*--ext[= ]js)\b' line_comments = ["// "] block_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 } documentation_comment = { start = "/**", prefix = "* ", end = "*/", tab_size = 1 } +wrap_characters = { start_prefix = "<", start_suffix = ">", end_prefix = "" } autoclose_before = ";:.,=}])>" brackets = [ { start = "{", end = "}", close = true, newline = true }, diff --git a/crates/languages/src/tsx/config.toml b/crates/languages/src/tsx/config.toml index 5849b9842fd7f3483f89bbedbdb7b74b3fc1572d..b5ef5bd56df2097bc920f02b87d07e4118d7b0d1 100644 --- a/crates/languages/src/tsx/config.toml +++ b/crates/languages/src/tsx/config.toml @@ -4,6 +4,7 @@ path_suffixes = ["tsx"] line_comments = ["// "] block_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 } documentation_comment = { start = "/**", prefix = "* ", end = "*/", tab_size = 1 } +wrap_characters = { start_prefix = "<", start_suffix = ">", end_prefix = "" } autoclose_before = ";:.,=}])>" brackets = [ { start = "{", end = "}", close = true, newline = true }, diff --git a/crates/languages/src/typescript/config.toml b/crates/languages/src/typescript/config.toml index d7e3e4bd3d1569f96636b7f7572deea306b46df7..2344f6209da7756049438669ee55d5376fdb47f8 100644 --- a/crates/languages/src/typescript/config.toml +++ b/crates/languages/src/typescript/config.toml @@ -5,6 +5,7 @@ first_line_pattern = '^#!.*\b(?:deno run|ts-node|bun|tsx|[/ ]node)\b' line_comments = ["// "] block_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 } documentation_comment = { start = "/**", prefix = "* ", end = "*/", tab_size = 1 } +wrap_characters = { start_prefix = "<", start_suffix = ">", end_prefix = "" } autoclose_before = ";:.,=}])>" brackets = [ { start = "{", end = "}", close = true, newline = true }, diff --git a/extensions/html/languages/html/config.toml b/extensions/html/languages/html/config.toml index f74db2888eb71e6e9f9afcbb1b41ab98e232a7a7..388949d95caf56803690b5533c871978a3f0d100 100644 --- a/extensions/html/languages/html/config.toml +++ b/extensions/html/languages/html/config.toml @@ -3,6 +3,7 @@ grammar = "html" path_suffixes = ["html", "htm", "shtml"] autoclose_before = ">})" block_comment = { start = "", tab_size = 0 } +wrap_characters = { start_prefix = "<", start_suffix = ">", end_prefix = "" } brackets = [ { start = "{", end = "}", close = true, newline = true }, { start = "[", end = "]", close = true, newline = true }, From 48299b5b2402748a7501047cc11204e452289fcf Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Thu, 28 Aug 2025 00:17:21 +0300 Subject: [PATCH 393/744] search: Preserve `SearchOptions` across dismisses (#36954) Closes #36931 and #21956 Preserves `SearchOptions` across dismisses of the buffer search bar. This behavior is consistent with VSCode, which seems reasonable. The `configured_options` field is then no longer being used. The configuration is still read during initialization of the `BufferSearchBar`, but not after. Something to consider is that there are other elements in the search bar which are not kept across dismisses such as replace status. However these are visually separated in the UI, leading me to believe this is a okay change to make. Release Notes: - Preserve search options between buffer search dismisses --- crates/search/src/buffer_search.rs | 62 ++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index a38dc8c35b3a0caef230247505a6131d40170bca..b2096d48ef5ba4e3b74eb0955bb11ef32cb5498a 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -749,14 +749,16 @@ impl BufferSearchBar { return false; }; - self.configured_options = + let configured_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search); - if self.dismissed - && (self.configured_options != self.default_options - || self.configured_options != self.search_options) - { - self.search_options = self.configured_options; - self.default_options = self.configured_options; + let settings_changed = configured_options != self.configured_options; + + if self.dismissed && settings_changed { + // Only update configuration options when search bar is dismissed, + // so we don't miss updates even after calling show twice + self.configured_options = configured_options; + self.search_options = configured_options; + self.default_options = configured_options; } self.dismissed = false; @@ -2750,11 +2752,6 @@ mod tests { "Search bar should be present and visible" ); search_bar.deploy(&deploy, window, cx); - assert_eq!( - search_bar.configured_options, - SearchOptions::NONE, - "Should have configured search options matching the settings" - ); assert_eq!( search_bar.search_options, SearchOptions::WHOLE_WORD, @@ -2765,21 +2762,22 @@ mod tests { search_bar.deploy(&deploy, window, cx); assert_eq!( search_bar.search_options, - SearchOptions::NONE, - "After hiding and showing the search bar, default options should be used" + SearchOptions::WHOLE_WORD, + "After hiding and showing the search bar, search options should be preserved" ); search_bar.toggle_search_option(SearchOptions::REGEX, window, cx); search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); assert_eq!( search_bar.search_options, - SearchOptions::REGEX | SearchOptions::WHOLE_WORD, + SearchOptions::REGEX, "Should enable the options toggled" ); assert!( !search_bar.dismissed, "Search bar should be present and visible" ); + search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); }); update_search_settings( @@ -2800,11 +2798,6 @@ mod tests { ); search_bar.deploy(&deploy, window, cx); - assert_eq!( - search_bar.configured_options, - SearchOptions::CASE_SENSITIVE, - "Should have configured search options matching the settings" - ); assert_eq!( search_bar.search_options, SearchOptions::REGEX | SearchOptions::WHOLE_WORD, @@ -2812,10 +2805,37 @@ mod tests { ); search_bar.dismiss(&Dismiss, window, cx); search_bar.deploy(&deploy, window, cx); + assert_eq!( + search_bar.configured_options, + SearchOptions::CASE_SENSITIVE, + "After a settings update and toggling the search bar, configured options should be updated" + ); assert_eq!( search_bar.search_options, SearchOptions::CASE_SENSITIVE, - "After hiding and showing the search bar, default options should be used" + "After a settings update and toggling the search bar, configured options should be used" + ); + }); + + update_search_settings( + SearchSettings { + button: true, + whole_word: true, + case_sensitive: true, + include_ignored: false, + regex: false, + }, + cx, + ); + + search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.deploy(&deploy, window, cx); + search_bar.dismiss(&Dismiss, window, cx); + search_bar.show(window, cx); + assert_eq!( + search_bar.search_options, + SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD, + "Calling deploy on an already deployed search bar should not prevent settings updates from being detected" ); }); } From 9a97f9465b08954ef3dc1729d3ad6abed0485620 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Thu, 28 Aug 2025 00:57:08 +0200 Subject: [PATCH 394/744] rust: Improve highlighting within macros (#37049) This makes sure we do not apply the highlights for snake case identifiers as well as paths for attributes too broadly to all types of macros, which should make macros much more readable overall whilst keeping the highlighting for the attribute items. | Before | After | | --- | --- | | Bildschirmfoto 2025-08-28 um 00 37
58 | Bildschirmfoto 2025-08-28 um 00
37 38 | Release Notes: - rust: Improved highlighting within macros. --- crates/languages/src/rust/highlights.scm | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/languages/src/rust/highlights.scm b/crates/languages/src/rust/highlights.scm index 1c46061827cd504df669aadacd0a489172d1ce5a..3f44c5fc0e46d280f63d0b212cc237ba4cbb0e8b 100644 --- a/crates/languages/src/rust/highlights.scm +++ b/crates/languages/src/rust/highlights.scm @@ -195,12 +195,13 @@ operator: "/" @operator (attribute_item (attribute [ (identifier) @attribute (scoped_identifier name: (identifier) @attribute) + (token_tree (identifier) @attribute (#match? @attribute "^[a-z\\d_]*$")) + (token_tree (identifier) @variable "::" (identifier) @type (#match? @type "^[A-Z]")) ])) + (inner_attribute_item (attribute [ (identifier) @attribute (scoped_identifier name: (identifier) @attribute) + (token_tree (identifier) @attribute (#match? @attribute "^[a-z\\d_]*$")) + (token_tree (identifier) @variable "::" (identifier) @type (#match? @type "^[A-Z]")) ])) -; Match nested snake case identifiers in attribute items. -(token_tree (identifier) @attribute (#match? @attribute "^[a-z\\d_]*$")) -; Override the attribute match for paths in scoped type/enum identifiers. -(token_tree (identifier) @variable "::" (identifier) @type (#match? @type "^[A-Z]")) From b233df8343b5f7316296995ced951dc166e56c7d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 28 Aug 2025 02:24:19 +0300 Subject: [PATCH 395/744] Revert "Remote LSP logs (#36709)" (#37051) This reverts commit e2bf8e5d9c9b1b14cad1c9c618bb724c04182a2c. See https://github.com/zed-industries/zed/pull/37050#issuecomment-3230017137 for the context: musl builds started to fail and the amount of `cfg!`s to fix this is too large. Instead, the lsp_log.rs has to be split and repurposed better for the remote headless server. Release Notes: - N/A --- Cargo.lock | 2 - crates/assistant_slash_command/Cargo.toml | 2 +- crates/assistant_tool/Cargo.toml | 2 +- crates/breadcrumbs/Cargo.toml | 2 +- .../20221109000000_test_schema.sql | 1 - .../20250827084812_worktree_in_servers.sql | 2 - crates/collab/src/db/queries/projects.rs | 4 +- crates/collab/src/db/queries/rooms.rs | 2 +- .../collab/src/db/tables/language_server.rs | 1 - crates/collab/src/rpc.rs | 4 +- crates/copilot/Cargo.toml | 2 +- crates/editor/Cargo.toml | 2 +- crates/language_tools/Cargo.toml | 3 +- crates/language_tools/src/language_tools.rs | 4 +- crates/language_tools/src/lsp_log.rs | 666 ++++++------------ crates/language_tools/src/lsp_log_tests.rs | 2 +- crates/language_tools/src/lsp_tool.rs | 87 +-- crates/project/src/lsp_store.rs | 90 +-- crates/project/src/project.rs | 21 - crates/project/src/project_tests.rs | 1 - crates/proto/proto/lsp.proto | 44 +- crates/proto/proto/zed.proto | 3 +- crates/proto/src/proto.rs | 7 +- crates/remote_server/Cargo.toml | 1 - crates/remote_server/src/headless_project.rs | 79 +-- crates/settings/src/settings.rs | 2 +- crates/workspace/Cargo.toml | 3 +- crates/workspace/src/pane_group.rs | 15 +- crates/workspace/src/workspace.rs | 237 ++----- 29 files changed, 342 insertions(+), 949 deletions(-) delete mode 100644 crates/collab/migrations/20250827084812_worktree_in_servers.sql diff --git a/Cargo.lock b/Cargo.lock index 8088efd6ea5517ac9cc2cf9bc3224176a09b9e60..4325addc392214614a6654563d88041331f2ded9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9213,7 +9213,6 @@ dependencies = [ "language", "lsp", "project", - "proto", "release_channel", "serde_json", "settings", @@ -13501,7 +13500,6 @@ dependencies = [ "language", "language_extension", "language_model", - "language_tools", "languages", "libc", "log", diff --git a/crates/assistant_slash_command/Cargo.toml b/crates/assistant_slash_command/Cargo.toml index e33c2cda1abb3b37f5a3f10e9a76d4c3fcdd6808..f7b7af9b879492cbb48f4e88d8379b45cbc2d053 100644 --- a/crates/assistant_slash_command/Cargo.toml +++ b/crates/assistant_slash_command/Cargo.toml @@ -25,7 +25,7 @@ parking_lot.workspace = true serde.workspace = true serde_json.workspace = true ui.workspace = true -workspace = { path = "../workspace", default-features = false } +workspace.workspace = true workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/assistant_tool/Cargo.toml b/crates/assistant_tool/Cargo.toml index 951226adfd685b5ae19201ed9531f5a3c8d74e0b..c95695052a4778209010b2f9e7a4a57be4cb6cf7 100644 --- a/crates/assistant_tool/Cargo.toml +++ b/crates/assistant_tool/Cargo.toml @@ -28,7 +28,7 @@ serde.workspace = true serde_json.workspace = true text.workspace = true util.workspace = true -workspace = { path = "../workspace", default-features = false } +workspace.workspace = true workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml index 46f43d163068ae4b8018080c7873c5ec4db30131..c25cfc3c86f26a72b3af37246ab30a175a68969a 100644 --- a/crates/breadcrumbs/Cargo.toml +++ b/crates/breadcrumbs/Cargo.toml @@ -19,7 +19,7 @@ itertools.workspace = true settings.workspace = true theme.workspace = true ui.workspace = true -workspace = { path = "../workspace", default-features = false } +workspace.workspace = true zed_actions.workspace = true workspace-hack.workspace = true diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index b2e25458ef98b295b4d056a7f59521f4fa896f1a..43581fd9421e5a8d10460a9ed15c565bd66a6e5e 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -175,7 +175,6 @@ CREATE TABLE "language_servers" ( "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, "name" VARCHAR NOT NULL, "capabilities" TEXT NOT NULL, - "worktree_id" BIGINT, PRIMARY KEY (project_id, id) ); diff --git a/crates/collab/migrations/20250827084812_worktree_in_servers.sql b/crates/collab/migrations/20250827084812_worktree_in_servers.sql deleted file mode 100644 index d4c6ffbbcccb2d2f23654cfc287b45bb8ea20508..0000000000000000000000000000000000000000 --- a/crates/collab/migrations/20250827084812_worktree_in_servers.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE language_servers - ADD COLUMN worktree_id BIGINT; diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index a3f0ea6cbc6e762e365f82e74b886234e62da109..393f2c80f8e733aa2d2b3b5f4b811c9868e0a620 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -694,7 +694,6 @@ impl Database { project_id: ActiveValue::set(project_id), id: ActiveValue::set(server.id as i64), name: ActiveValue::set(server.name.clone()), - worktree_id: ActiveValue::set(server.worktree_id.map(|id| id as i64)), capabilities: ActiveValue::set(update.capabilities.clone()), }) .on_conflict( @@ -705,7 +704,6 @@ impl Database { .update_columns([ language_server::Column::Name, language_server::Column::Capabilities, - language_server::Column::WorktreeId, ]) .to_owned(), ) @@ -1067,7 +1065,7 @@ impl Database { server: proto::LanguageServer { id: language_server.id as u64, name: language_server.name, - worktree_id: language_server.worktree_id.map(|id| id as u64), + worktree_id: None, }, capabilities: language_server.capabilities, }) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 0713ac2cb2810797b319b53583bc8c0e1756fe68..9e7cabf9b29c91d7e486f42d5e6b12020b0f514e 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -809,7 +809,7 @@ impl Database { server: proto::LanguageServer { id: language_server.id as u64, name: language_server.name, - worktree_id: language_server.worktree_id.map(|id| id as u64), + worktree_id: None, }, capabilities: language_server.capabilities, }) diff --git a/crates/collab/src/db/tables/language_server.rs b/crates/collab/src/db/tables/language_server.rs index 705aae292ba456622e9808f033a348f60c3835a4..34c7514d917b313990521acf8542c31394d009fc 100644 --- a/crates/collab/src/db/tables/language_server.rs +++ b/crates/collab/src/db/tables/language_server.rs @@ -10,7 +10,6 @@ pub struct Model { pub id: i64, pub name: String, pub capabilities: String, - pub worktree_id: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 9e4dfd4854b4de67de522bfbbd1160fe880a05cb..73f327166a3f1fb40a1f232ea2fabcdedd3fb129 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -476,9 +476,7 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_message_handler(broadcast_project_message_from_host::) - .add_message_handler(update_context) - .add_request_handler(forward_mutating_project_request::) - .add_message_handler(broadcast_project_message_from_host::); + .add_message_handler(update_context); Arc::new(server) } diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 470d1989583d9eb1ed06159a28686ddf2620156e..0fc119f31125f4ef3925799fd98fd47cac7ca9da 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -50,7 +50,7 @@ sum_tree.workspace = true task.workspace = true ui.workspace = true util.workspace = true -workspace = { path = "../workspace", default-features = false } +workspace.workspace = true workspace-hack.workspace = true itertools.workspace = true diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index b7051c9b19ab748390c109b5c6cd449cb0f4886e..339f98ae8bd88263f1fea12c535569864faae294 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -89,7 +89,7 @@ ui.workspace = true url.workspace = true util.workspace = true uuid.workspace = true -workspace = { path = "../workspace", default-features = false } +workspace.workspace = true zed_actions.workspace = true workspace-hack.workspace = true diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index a10b7dc50b9838d6da1afb9838758b4dd4582563..5aa914311a6eccc1cb68efa37e878ad12249d6fd 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -24,14 +24,13 @@ itertools.workspace = true language.workspace = true lsp.workspace = true project.workspace = true -proto.workspace = true serde_json.workspace = true settings.workspace = true theme.workspace = true tree-sitter.workspace = true ui.workspace = true util.workspace = true -workspace = { path = "../workspace", default-features = false } +workspace.workspace = true zed_actions.workspace = true workspace-hack.workspace = true diff --git a/crates/language_tools/src/language_tools.rs b/crates/language_tools/src/language_tools.rs index d6a006f47bc921f00c15edc8b45d3efe39364acc..cbf5756875f723b52fabbfe877c32265dd6f0aef 100644 --- a/crates/language_tools/src/language_tools.rs +++ b/crates/language_tools/src/language_tools.rs @@ -1,5 +1,5 @@ mod key_context_view; -pub mod lsp_log; +mod lsp_log; pub mod lsp_tool; mod syntax_tree_view; @@ -14,7 +14,7 @@ use ui::{Context, Window}; use workspace::{Item, ItemHandle, SplitDirection, Workspace}; pub fn init(cx: &mut App) { - lsp_log::init(true, cx); + lsp_log::init(cx); syntax_tree_view::init(cx); key_context_view::init(cx); } diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index d55b54a6d253214cd81bf0226278863c4d59c834..a71e434e5274392add0463830519834202b7ba58 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -9,16 +9,12 @@ use gpui::{ use itertools::Itertools; use language::{LanguageServerId, language_settings::SoftWrap}; use lsp::{ - IoKind, LanguageServer, LanguageServerBinary, LanguageServerName, LanguageServerSelector, - MessageType, SetTraceParams, TraceValue, notification::SetTrace, -}; -use project::{ - LspStore, Project, ProjectItem, WorktreeId, lsp_store::LanguageServerLogType, - search::SearchQuery, + IoKind, LanguageServer, LanguageServerName, LanguageServerSelector, MessageType, + SetTraceParams, TraceValue, notification::SetTrace, }; +use project::{Project, WorktreeId, search::SearchQuery}; use std::{any::TypeId, borrow::Cow, sync::Arc}; use ui::{Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState, prelude::*}; -use util::ResultExt as _; use workspace::{ SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, item::{Item, ItemHandle}, @@ -32,7 +28,6 @@ const RECEIVE_LINE: &str = "\n// Receive:"; const MAX_STORED_LOG_ENTRIES: usize = 2000; pub struct LogStore { - store_logs: bool, projects: HashMap, ProjectState>, language_servers: HashMap, copilot_log_subscription: Option, @@ -51,7 +46,6 @@ trait Message: AsRef { } } -#[derive(Debug)] pub(super) struct LogMessage { message: String, typ: MessageType, @@ -79,10 +73,8 @@ impl Message for LogMessage { } } -#[derive(Debug)] pub(super) struct TraceMessage { message: String, - is_verbose: bool, } impl AsRef for TraceMessage { @@ -92,18 +84,9 @@ impl AsRef for TraceMessage { } impl Message for TraceMessage { - type Level = TraceValue; - - fn should_include(&self, level: Self::Level) -> bool { - match level { - TraceValue::Off => false, - TraceValue::Messages => !self.is_verbose, - TraceValue::Verbose => true, - } - } + type Level = (); } -#[derive(Debug)] struct RpcMessage { message: String, } @@ -118,7 +101,7 @@ impl Message for RpcMessage { type Level = (); } -pub struct LanguageServerState { +pub(super) struct LanguageServerState { name: Option, worktree_id: Option, kind: LanguageServerKind, @@ -130,35 +113,24 @@ pub struct LanguageServerState { io_logs_subscription: Option, } -impl std::fmt::Debug for LanguageServerState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("LanguageServerState") - .field("name", &self.name) - .field("worktree_id", &self.worktree_id) - .field("kind", &self.kind) - .field("log_messages", &self.log_messages) - .field("trace_messages", &self.trace_messages) - .field("rpc_state", &self.rpc_state) - .field("trace_level", &self.trace_level) - .field("log_level", &self.log_level) - .finish_non_exhaustive() - } -} - #[derive(PartialEq, Clone)] pub enum LanguageServerKind { Local { project: WeakEntity }, Remote { project: WeakEntity }, - LocalSsh { lsp_store: WeakEntity }, Global, } +impl LanguageServerKind { + fn is_remote(&self) -> bool { + matches!(self, LanguageServerKind::Remote { .. }) + } +} + impl std::fmt::Debug for LanguageServerKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"), LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"), - LanguageServerKind::LocalSsh { .. } => write!(f, "LanguageServerKind::LocalSsh"), LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"), } } @@ -169,14 +141,12 @@ impl LanguageServerKind { match self { Self::Local { project } => Some(project), Self::Remote { project } => Some(project), - Self::LocalSsh { .. } => None, Self::Global { .. } => None, } } } -#[derive(Debug)] -pub struct LanguageServerRpcState { +struct LanguageServerRpcState { rpc_messages: VecDeque, last_message_kind: Option, } @@ -197,7 +167,7 @@ pub struct LspLogToolbarItemView { _log_view_subscription: Option, } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone, PartialEq, Eq)] enum MessageKind { Send, Receive, @@ -213,13 +183,6 @@ pub enum LogKind { } impl LogKind { - fn from_server_log_type(log_type: &LanguageServerLogType) -> Self { - match log_type { - LanguageServerLogType::Log(_) => Self::Logs, - LanguageServerLogType::Trace { .. } => Self::Trace, - LanguageServerLogType::Rpc { .. } => Self::Rpc, - } - } fn label(&self) -> &'static str { match self { LogKind::Rpc => RPC_MESSAGES, @@ -249,53 +212,59 @@ actions!( ] ); -pub struct GlobalLogStore(pub WeakEntity); +pub(super) struct GlobalLogStore(pub WeakEntity); impl Global for GlobalLogStore {} -pub fn init(store_logs: bool, cx: &mut App) { - let log_store = cx.new(|cx| LogStore::new(store_logs, cx)); +pub fn init(cx: &mut App) { + let log_store = cx.new(LogStore::new); cx.set_global(GlobalLogStore(log_store.downgrade())); cx.observe_new(move |workspace: &mut Workspace, _, cx| { - log_store.update(cx, |store, cx| { - store.add_project(workspace.project(), cx); - }); + let project = workspace.project(); + if project.read(cx).is_local() || project.read(cx).is_via_remote_server() { + log_store.update(cx, |store, cx| { + store.add_project(project, cx); + }); + } let log_store = log_store.clone(); workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| { - let log_store = log_store.clone(); - let project = workspace.project().clone(); - get_or_create_tool( - workspace, - SplitDirection::Right, - window, - cx, - move |window, cx| LspLogView::new(project, log_store, window, cx), - ); + let project = workspace.project().read(cx); + if project.is_local() || project.is_via_remote_server() { + let project = workspace.project().clone(); + let log_store = log_store.clone(); + get_or_create_tool( + workspace, + SplitDirection::Right, + window, + cx, + move |window, cx| LspLogView::new(project, log_store, window, cx), + ); + } }); }) .detach(); } impl LogStore { - pub fn new(store_logs: bool, cx: &mut Context) -> Self { + pub fn new(cx: &mut Context) -> Self { let (io_tx, mut io_rx) = mpsc::unbounded(); let copilot_subscription = Copilot::global(cx).map(|copilot| { let copilot = &copilot; - cx.subscribe(copilot, |log_store, copilot, edit_prediction_event, cx| { + cx.subscribe(copilot, |this, copilot, edit_prediction_event, cx| { if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event && let Some(server) = copilot.read(cx).language_server() { let server_id = server.server_id(); - let weak_lsp_store = cx.weak_entity(); - log_store.copilot_log_subscription = + let weak_this = cx.weak_entity(); + this.copilot_log_subscription = Some(server.on_notification::( move |params, cx| { - weak_lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.add_language_server_log( + weak_this + .update(cx, |this, cx| { + this.add_language_server_log( server_id, MessageType::LOG, ¶ms.message, @@ -305,9 +274,8 @@ impl LogStore { .ok(); }, )); - let name = LanguageServerName::new_static("copilot"); - log_store.add_language_server( + this.add_language_server( LanguageServerKind::Global, server.server_id(), Some(name), @@ -319,27 +287,26 @@ impl LogStore { }) }); - let log_store = Self { + let this = Self { copilot_log_subscription: None, _copilot_subscription: copilot_subscription, projects: HashMap::default(), language_servers: HashMap::default(), - store_logs, io_tx, }; - cx.spawn(async move |log_store, cx| { + cx.spawn(async move |this, cx| { while let Some((server_id, io_kind, message)) = io_rx.next().await { - if let Some(log_store) = log_store.upgrade() { - log_store.update(cx, |log_store, cx| { - log_store.on_io(server_id, io_kind, &message, cx); + if let Some(this) = this.upgrade() { + this.update(cx, |this, cx| { + this.on_io(server_id, io_kind, &message, cx); })?; } } anyhow::Ok(()) }) .detach_and_log_err(cx); - log_store + this } pub fn add_project(&mut self, project: &Entity, cx: &mut Context) { @@ -353,19 +320,20 @@ impl LogStore { this.language_servers .retain(|_, state| state.kind.project() != Some(&weak_project)); }), - cx.subscribe(project, move |log_store, project, event, cx| { - let server_kind = if project.read(cx).is_local() { - LanguageServerKind::Local { + cx.subscribe(project, |this, project, event, cx| { + let server_kind = if project.read(cx).is_via_remote_server() { + LanguageServerKind::Remote { project: project.downgrade(), } } else { - LanguageServerKind::Remote { + LanguageServerKind::Local { project: project.downgrade(), } }; + match event { project::Event::LanguageServerAdded(id, name, worktree_id) => { - log_store.add_language_server( + this.add_language_server( server_kind, *id, Some(name.clone()), @@ -378,78 +346,20 @@ impl LogStore { cx, ); } - project::Event::LanguageServerBufferRegistered { - server_id, - buffer_id, - name, - .. - } if project.read(cx).is_via_collab() => { - let worktree_id = project - .read(cx) - .buffer_for_id(*buffer_id, cx) - .and_then(|buffer| { - Some(buffer.read(cx).project_path(cx)?.worktree_id) - }); - let name = name.clone().or_else(|| { - project - .read(cx) - .lsp_store() - .read(cx) - .language_server_statuses - .get(server_id) - .map(|status| status.name.clone()) - }); - log_store.add_language_server( - server_kind, - *server_id, - name, - worktree_id, - None, - cx, - ); - } project::Event::LanguageServerRemoved(id) => { - log_store.remove_language_server(*id, cx); + this.remove_language_server(*id, cx); } project::Event::LanguageServerLog(id, typ, message) => { - log_store.add_language_server( - server_kind, - *id, - None, - None, - None, - cx, - ); + this.add_language_server(server_kind, *id, None, None, None, cx); match typ { project::LanguageServerLogType::Log(typ) => { - log_store.add_language_server_log(*id, *typ, message, cx); + this.add_language_server_log(*id, *typ, message, cx); } - project::LanguageServerLogType::Trace { verbose_info } => { - log_store.add_language_server_trace( - *id, - message, - verbose_info.clone(), - cx, - ); - } - project::LanguageServerLogType::Rpc { received } => { - let kind = if *received { - MessageKind::Receive - } else { - MessageKind::Send - }; - log_store.add_language_server_rpc(*id, kind, message, cx); + project::LanguageServerLogType::Trace(_) => { + this.add_language_server_trace(*id, message, cx); } } } - project::Event::ToggleLspLogs { server_id, enabled } => { - // we do not support any other log toggling yet - if *enabled { - log_store.enable_rpc_trace_for_language_server(*server_id); - } else { - log_store.disable_rpc_trace_for_language_server(*server_id); - } - } _ => {} } }), @@ -465,7 +375,7 @@ impl LogStore { self.language_servers.get_mut(&id) } - pub fn add_language_server( + fn add_language_server( &mut self, kind: LanguageServerKind, server_id: LanguageServerId, @@ -516,35 +426,20 @@ impl LogStore { message: &str, cx: &mut Context, ) -> Option<()> { - let store_logs = self.store_logs; let language_server_state = self.get_language_server_state(id)?; let log_lines = &mut language_server_state.log_messages; - let message = message.trim_end().to_string(); - if !store_logs { - // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway - self.emit_event( - Event::NewServerLogEntry { - id, - kind: LanguageServerLogType::Log(typ), - text: message, - }, - cx, - ); - } else if let Some(new_message) = Self::push_new_message( + Self::add_language_server_message( log_lines, - LogMessage { message, typ }, + id, + LogMessage { + message: message.trim_end().to_string(), + typ, + }, language_server_state.log_level, - ) { - self.emit_event( - Event::NewServerLogEntry { - id, - kind: LanguageServerLogType::Log(typ), - text: new_message, - }, - cx, - ); - } + LogKind::Logs, + cx, + ); Some(()) } @@ -552,127 +447,46 @@ impl LogStore { &mut self, id: LanguageServerId, message: &str, - verbose_info: Option, cx: &mut Context, ) -> Option<()> { - let store_logs = self.store_logs; let language_server_state = self.get_language_server_state(id)?; let log_lines = &mut language_server_state.trace_messages; - if !store_logs { - // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway - self.emit_event( - Event::NewServerLogEntry { - id, - kind: LanguageServerLogType::Trace { verbose_info }, - text: message.trim().to_string(), - }, - cx, - ); - } else if let Some(new_message) = Self::push_new_message( + Self::add_language_server_message( log_lines, + id, TraceMessage { message: message.trim().to_string(), - is_verbose: false, }, - TraceValue::Messages, - ) { - if let Some(verbose_message) = verbose_info.as_ref() { - Self::push_new_message( - log_lines, - TraceMessage { - message: verbose_message.clone(), - is_verbose: true, - }, - TraceValue::Verbose, - ); - } - self.emit_event( - Event::NewServerLogEntry { - id, - kind: LanguageServerLogType::Trace { verbose_info }, - text: new_message, - }, - cx, - ); - } + (), + LogKind::Trace, + cx, + ); Some(()) } - fn push_new_message( + fn add_language_server_message( log_lines: &mut VecDeque, + id: LanguageServerId, message: T, current_severity: ::Level, - ) -> Option { + kind: LogKind, + cx: &mut Context, + ) { while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { log_lines.pop_front(); } + let text = message.as_ref().to_string(); let visible = message.should_include(current_severity); - - let visible_message = visible.then(|| message.as_ref().to_string()); log_lines.push_back(message); - visible_message - } - - fn add_language_server_rpc( - &mut self, - language_server_id: LanguageServerId, - kind: MessageKind, - message: &str, - cx: &mut Context<'_, Self>, - ) { - let store_logs = self.store_logs; - let Some(state) = self - .get_language_server_state(language_server_id) - .and_then(|state| state.rpc_state.as_mut()) - else { - return; - }; - - let received = kind == MessageKind::Receive; - let rpc_log_lines = &mut state.rpc_messages; - if state.last_message_kind != Some(kind) { - while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - rpc_log_lines.pop_front(); - } - let line_before_message = match kind { - MessageKind::Send => SEND_LINE, - MessageKind::Receive => RECEIVE_LINE, - }; - if store_logs { - rpc_log_lines.push_back(RpcMessage { - message: line_before_message.to_string(), - }); - } - // Do not send a synthetic message over the wire, it will be derived from the actual RPC message - cx.emit(Event::NewServerLogEntry { - id: language_server_id, - kind: LanguageServerLogType::Rpc { received }, - text: line_before_message.to_string(), - }); - } - - while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - rpc_log_lines.pop_front(); - } - if store_logs { - rpc_log_lines.push_back(RpcMessage { - message: message.trim().to_owned(), - }); + if visible { + cx.emit(Event::NewServerLogEntry { id, kind, text }); + cx.notify(); } - - self.emit_event( - Event::NewServerLogEntry { - id: language_server_id, - kind: LanguageServerLogType::Rpc { received }, - text: message.to_owned(), - }, - cx, - ); } - pub fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context) { + fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context) { self.language_servers.remove(&id); cx.notify(); } @@ -702,11 +516,11 @@ impl LogStore { None } } - LanguageServerKind::Global | LanguageServerKind::LocalSsh { .. } => Some(*id), + LanguageServerKind::Global => Some(*id), }) } - pub fn enable_rpc_trace_for_language_server( + fn enable_rpc_trace_for_language_server( &mut self, server_id: LanguageServerId, ) -> Option<&mut LanguageServerRpcState> { @@ -849,45 +663,50 @@ impl LogStore { } }; + let state = self + .get_language_server_state(language_server_id)? + .rpc_state + .as_mut()?; let kind = if is_received { MessageKind::Receive } else { MessageKind::Send }; - self.add_language_server_rpc(language_server_id, kind, message, cx); - cx.notify(); - Some(()) - } - - fn emit_event(&mut self, e: Event, cx: &mut Context) { - match &e { - Event::NewServerLogEntry { id, kind, text } => { - if let Some(state) = self.get_language_server_state(*id) { - let downstream_client = match &state.kind { - LanguageServerKind::Remote { project } - | LanguageServerKind::Local { project } => project - .upgrade() - .map(|project| project.read(cx).lsp_store()), - LanguageServerKind::LocalSsh { lsp_store } => lsp_store.upgrade(), - LanguageServerKind::Global => None, - } - .and_then(|lsp_store| lsp_store.read(cx).downstream_client()); - if let Some((client, project_id)) = downstream_client { - client - .send(proto::LanguageServerLog { - project_id, - language_server_id: id.to_proto(), - message: text.clone(), - log_type: Some(kind.to_proto()), - }) - .ok(); - } - } + let rpc_log_lines = &mut state.rpc_messages; + if state.last_message_kind != Some(kind) { + while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { + rpc_log_lines.pop_front(); } + let line_before_message = match kind { + MessageKind::Send => SEND_LINE, + MessageKind::Receive => RECEIVE_LINE, + }; + rpc_log_lines.push_back(RpcMessage { + message: line_before_message.to_string(), + }); + cx.emit(Event::NewServerLogEntry { + id: language_server_id, + kind: LogKind::Rpc, + text: line_before_message.to_string(), + }); + } + + while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { + rpc_log_lines.pop_front(); } - cx.emit(e); + let message = message.trim(); + rpc_log_lines.push_back(RpcMessage { + message: message.to_string(), + }); + cx.emit(Event::NewServerLogEntry { + id: language_server_id, + kind: LogKind::Rpc, + text: message.to_string(), + }); + cx.notify(); + Some(()) } } @@ -932,14 +751,13 @@ impl LspLogView { cx.notify(); }); - let events_subscriptions = cx.subscribe_in( &log_store, window, move |log_view, _, e, window, cx| match e { Event::NewServerLogEntry { id, kind, text } => { if log_view.current_server_id == Some(*id) - && LogKind::from_server_log_type(kind) == log_view.active_entry_kind + && *kind == log_view.active_entry_kind { log_view.editor.update(cx, |editor, cx| { editor.set_read_only(false); @@ -982,7 +800,7 @@ impl LspLogView { window.focus(&log_view.editor.focus_handle(cx)); }); - let mut lsp_log_view = Self { + let mut this = Self { focus_handle, editor, editor_subscriptions, @@ -997,9 +815,9 @@ impl LspLogView { ], }; if let Some(server_id) = server_id { - lsp_log_view.show_logs_for_server(server_id, window, cx); + this.show_logs_for_server(server_id, window, cx); } - lsp_log_view + this } fn editor_for_logs( @@ -1020,7 +838,7 @@ impl LspLogView { } fn editor_for_server_info( - info: ServerInfo, + server: &LanguageServer, window: &mut Window, cx: &mut Context, ) -> (Entity, Vec) { @@ -1035,21 +853,22 @@ impl LspLogView { * Capabilities: {CAPABILITIES} * Configuration: {CONFIGURATION}", - NAME = info.name, - ID = info.id, - BINARY = info.binary.as_ref().map_or_else( - || "Unknown".to_string(), - |bin| bin.path.as_path().to_string_lossy().to_string() - ), - WORKSPACE_FOLDERS = info.workspace_folders.join(", "), - CAPABILITIES = serde_json::to_string_pretty(&info.capabilities) + NAME = server.name(), + ID = server.server_id(), + BINARY = server.binary(), + WORKSPACE_FOLDERS = server + .workspace_folders() + .into_iter() + .filter_map(|path| path + .to_file_path() + .ok() + .map(|path| path.to_string_lossy().into_owned())) + .collect::>() + .join(", "), + CAPABILITIES = serde_json::to_string_pretty(&server.capabilities()) .unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")), - CONFIGURATION = info - .configuration - .map(|configuration| serde_json::to_string_pretty(&configuration)) - .transpose() - .unwrap_or_else(|e| Some(format!("Failed to serialize configuration: {e}"))) - .unwrap_or_else(|| "Unknown".to_string()), + CONFIGURATION = serde_json::to_string_pretty(server.configuration()) + .unwrap_or_else(|e| format!("Failed to serialize configuration: {e}")), ); let editor = initialize_new_editor(server_info, false, window, cx); let editor_subscription = cx.subscribe( @@ -1072,9 +891,7 @@ impl LspLogView { .language_servers .iter() .map(|(server_id, state)| match &state.kind { - LanguageServerKind::Local { .. } - | LanguageServerKind::Remote { .. } - | LanguageServerKind::LocalSsh { .. } => { + LanguageServerKind::Local { .. } | LanguageServerKind::Remote { .. } => { let worktree_root_name = state .worktree_id .and_then(|id| self.project.read(cx).worktree_for_id(id, cx)) @@ -1186,17 +1003,11 @@ impl LspLogView { window: &mut Window, cx: &mut Context, ) { - let trace_level = self - .log_store - .update(cx, |this, _| { - Some(this.get_language_server_state(server_id)?.trace_level) - }) - .unwrap_or(TraceValue::Messages); let log_contents = self .log_store .read(cx) .server_trace(server_id) - .map(|v| log_contents(v, trace_level)); + .map(|v| log_contents(v, ())); if let Some(log_contents) = log_contents { self.current_server_id = Some(server_id); self.active_entry_kind = LogKind::Trace; @@ -1214,7 +1025,6 @@ impl LspLogView { window: &mut Window, cx: &mut Context, ) { - self.toggle_rpc_trace_for_server(server_id, true, window, cx); let rpc_log = self.log_store.update(cx, |log_store, _| { log_store .enable_rpc_trace_for_language_server(server_id) @@ -1259,33 +1069,12 @@ impl LspLogView { window: &mut Window, cx: &mut Context, ) { - self.log_store.update(cx, |log_store, cx| { + self.log_store.update(cx, |log_store, _| { if enabled { log_store.enable_rpc_trace_for_language_server(server_id); } else { log_store.disable_rpc_trace_for_language_server(server_id); } - - if let Some(server_state) = log_store.language_servers.get(&server_id) { - if let LanguageServerKind::Remote { project } = &server_state.kind { - project - .update(cx, |project, cx| { - if let Some((client, project_id)) = - project.lsp_store().read(cx).upstream_client() - { - client - .send(proto::ToggleLspLogs { - project_id, - log_type: proto::toggle_lsp_logs::LogType::Rpc as i32, - server_id: server_id.to_proto(), - enabled, - }) - .log_err(); - } - }) - .ok(); - } - }; }); if !enabled && Some(server_id) == self.current_server_id { self.show_logs_for_server(server_id, window, cx); @@ -1324,38 +1113,13 @@ impl LspLogView { window: &mut Window, cx: &mut Context, ) { - let Some(server_info) = self - .project - .read(cx) - .lsp_store() - .update(cx, |lsp_store, _| { - lsp_store - .language_server_for_id(server_id) - .as_ref() - .map(|language_server| ServerInfo::new(language_server)) - .or_else(move || { - let capabilities = - lsp_store.lsp_server_capabilities.get(&server_id)?.clone(); - let name = lsp_store - .language_server_statuses - .get(&server_id) - .map(|status| status.name.clone())?; - Some(ServerInfo { - id: server_id, - capabilities, - binary: None, - name, - workspace_folders: Vec::new(), - configuration: None, - }) - }) - }) - else { + let lsp_store = self.project.read(cx).lsp_store(); + let Some(server) = lsp_store.read(cx).language_server_for_id(server_id) else { return; }; self.current_server_id = Some(server_id); self.active_entry_kind = LogKind::ServerInfo; - let (editor, editor_subscriptions) = Self::editor_for_server_info(server_info, window, cx); + let (editor, editor_subscriptions) = Self::editor_for_server_info(&server, window, cx); self.editor = editor; self.editor_subscriptions = editor_subscriptions; cx.notify(); @@ -1652,6 +1416,7 @@ impl Render for LspLogToolbarItemView { let view_selector = current_server.map(|server| { let server_id = server.server_id; + let is_remote = server.server_kind.is_remote(); let rpc_trace_enabled = server.rpc_trace_enabled; let log_view = log_view.clone(); PopoverMenu::new("LspViewSelector") @@ -1673,53 +1438,55 @@ impl Render for LspLogToolbarItemView { view.show_logs_for_server(server_id, window, cx); }), ) - .entry( - SERVER_TRACE, - None, - window.handler_for(&log_view, move |view, window, cx| { - view.show_trace_for_server(server_id, window, cx); - }), - ) - .custom_entry( - { - let log_toolbar_view = log_toolbar_view.clone(); - move |window, _| { - h_flex() - .w_full() - .justify_between() - .child(Label::new(RPC_MESSAGES)) - .child( - div().child( - Checkbox::new( - "LspLogEnableRpcTrace", - if rpc_trace_enabled { - ToggleState::Selected - } else { - ToggleState::Unselected - }, - ) - .on_click(window.listener_for( - &log_toolbar_view, - move |view, selection, window, cx| { - let enabled = matches!( - selection, + .when(!is_remote, |this| { + this.entry( + SERVER_TRACE, + None, + window.handler_for(&log_view, move |view, window, cx| { + view.show_trace_for_server(server_id, window, cx); + }), + ) + .custom_entry( + { + let log_toolbar_view = log_toolbar_view.clone(); + move |window, _| { + h_flex() + .w_full() + .justify_between() + .child(Label::new(RPC_MESSAGES)) + .child( + div().child( + Checkbox::new( + "LspLogEnableRpcTrace", + if rpc_trace_enabled { ToggleState::Selected - ); - view.toggle_rpc_logging_for_server( - server_id, enabled, window, cx, - ); - cx.stop_propagation(); - }, - )), - ), - ) - .into_any_element() - } - }, - window.handler_for(&log_view, move |view, window, cx| { - view.show_rpc_trace_for_server(server_id, window, cx); - }), - ) + } else { + ToggleState::Unselected + }, + ) + .on_click(window.listener_for( + &log_toolbar_view, + move |view, selection, window, cx| { + let enabled = matches!( + selection, + ToggleState::Selected + ); + view.toggle_rpc_logging_for_server( + server_id, enabled, window, cx, + ); + cx.stop_propagation(); + }, + )), + ), + ) + .into_any_element() + } + }, + window.handler_for(&log_view, move |view, window, cx| { + view.show_rpc_trace_for_server(server_id, window, cx); + }), + ) + }) .entry( SERVER_INFO, None, @@ -1929,6 +1696,12 @@ const SERVER_LOGS: &str = "Server Logs"; const SERVER_TRACE: &str = "Server Trace"; const SERVER_INFO: &str = "Server Info"; +impl Default for LspLogToolbarItemView { + fn default() -> Self { + Self::new() + } +} + impl LspLogToolbarItemView { pub fn new() -> Self { Self { @@ -1961,41 +1734,10 @@ impl LspLogToolbarItemView { } } -struct ServerInfo { - id: LanguageServerId, - capabilities: lsp::ServerCapabilities, - binary: Option, - name: LanguageServerName, - workspace_folders: Vec, - configuration: Option, -} - -impl ServerInfo { - fn new(server: &LanguageServer) -> Self { - Self { - id: server.server_id(), - capabilities: server.capabilities(), - binary: Some(server.binary().clone()), - name: server.name(), - workspace_folders: server - .workspace_folders() - .into_iter() - .filter_map(|path| { - path.to_file_path() - .ok() - .map(|path| path.to_string_lossy().into_owned()) - }) - .collect::>(), - configuration: Some(server.configuration().clone()), - } - } -} - -#[derive(Debug)] pub enum Event { NewServerLogEntry { id: LanguageServerId, - kind: LanguageServerLogType, + kind: LogKind, text: String, }, } diff --git a/crates/language_tools/src/lsp_log_tests.rs b/crates/language_tools/src/lsp_log_tests.rs index a7dbaa2a601bc6ebd60685635cd0802750452053..ad2b653fdcfd4dc228cac58da7ed15f844b4bb26 100644 --- a/crates/language_tools/src/lsp_log_tests.rs +++ b/crates/language_tools/src/lsp_log_tests.rs @@ -51,7 +51,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { }, ); - let log_store = cx.new(|cx| LogStore::new(true, cx)); + let log_store = cx.new(LogStore::new); log_store.update(cx, |store, cx| store.add_project(&project, cx)); let _rust_buffer = project diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index 2d6a99a0bc2f0d2ed498aaada204574740293343..dd3e80212fda08f43718a664d2cfd6d377182273 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -122,7 +122,8 @@ impl LanguageServerState { let lsp_logs = cx .try_global::() .and_then(|lsp_logs| lsp_logs.0.upgrade()); - let Some(lsp_logs) = lsp_logs else { + let lsp_store = self.lsp_store.upgrade(); + let Some((lsp_logs, lsp_store)) = lsp_logs.zip(lsp_store) else { return menu; }; @@ -209,11 +210,10 @@ impl LanguageServerState { }; let server_selector = server_info.server_selector(); - let is_remote = self - .lsp_store - .update(cx, |lsp_store, _| lsp_store.as_remote().is_some()) - .unwrap_or(false); - let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector); + // TODO currently, Zed remote does not work well with the LSP logs + // https://github.com/zed-industries/zed/issues/28557 + let has_logs = lsp_store.read(cx).as_local().is_some() + && lsp_logs.read(cx).has_server_logs(&server_selector); let status_color = server_info .binary_status @@ -241,10 +241,10 @@ impl LanguageServerState { .as_ref() .or_else(|| server_info.binary_status.as_ref()?.message.as_ref()) .cloned(); - let hover_label = if message.is_some() { - Some("View Message") - } else if has_logs { + let hover_label = if has_logs { Some("View Logs") + } else if message.is_some() { + Some("View Message") } else { None }; @@ -288,7 +288,16 @@ impl LanguageServerState { let server_name = server_info.name.clone(); let workspace = self.workspace.clone(); move |window, cx| { - if let Some(message) = &message { + if has_logs { + lsp_logs.update(cx, |lsp_logs, cx| { + lsp_logs.open_server_trace( + workspace.clone(), + server_selector.clone(), + window, + cx, + ); + }); + } else if let Some(message) = &message { let Some(create_buffer) = workspace .update(cx, |workspace, cx| { workspace @@ -338,15 +347,6 @@ impl LanguageServerState { anyhow::Ok(()) }) .detach(); - } else if has_logs { - lsp_logs.update(cx, |lsp_logs, cx| { - lsp_logs.open_server_trace( - workspace.clone(), - server_selector.clone(), - window, - cx, - ); - }); } else { cx.propagate(); } @@ -529,48 +529,26 @@ impl LspTool { }); let lsp_store = workspace.project().read(cx).lsp_store(); - let mut language_servers = LanguageServers::default(); - for (_, status) in lsp_store.read(cx).language_server_statuses() { - language_servers.binary_statuses.insert( - status.name.clone(), - LanguageServerBinaryStatus { - status: BinaryStatus::None, - message: None, - }, - ); - } - let lsp_store_subscription = cx.subscribe_in(&lsp_store, window, |lsp_tool, _, e, window, cx| { lsp_tool.on_lsp_store_event(e, window, cx) }); - let server_state = cx.new(|_| LanguageServerState { + let state = cx.new(|_| LanguageServerState { workspace: workspace.weak_handle(), items: Vec::new(), lsp_store: lsp_store.downgrade(), active_editor: None, - language_servers, + language_servers: LanguageServers::default(), }); - let mut lsp_tool = Self { - server_state, + Self { + server_state: state, popover_menu_handle, lsp_menu: None, lsp_menu_refresh: Task::ready(()), _subscriptions: vec![settings_subscription, lsp_store_subscription], - }; - if !lsp_tool - .server_state - .read(cx) - .language_servers - .binary_statuses - .is_empty() - { - lsp_tool.refresh_lsp_menu(true, window, cx); } - - lsp_tool } fn on_lsp_store_event( @@ -730,25 +708,6 @@ impl LspTool { } } } - state - .lsp_store - .update(cx, |lsp_store, cx| { - for (server_id, status) in lsp_store.language_server_statuses() { - if let Some(worktree) = status.worktree.and_then(|worktree_id| { - lsp_store - .worktree_store() - .read(cx) - .worktree_for_id(worktree_id, cx) - }) { - server_ids_to_worktrees.insert(server_id, worktree.clone()); - server_names_to_worktrees - .entry(status.name.clone()) - .or_default() - .insert((worktree, server_id)); - } - } - }) - .ok(); let mut servers_per_worktree = BTreeMap::>::new(); let mut servers_without_worktree = Vec::::new(); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 2bc95bf81d85ca6e09a246408a4ef8bfa28b91e3..ad9d0abf405b18f9048030621e960251057588de 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -977,9 +977,7 @@ impl LocalLspStore { this.update(&mut cx, |_, cx| { cx.emit(LspStoreEvent::LanguageServerLog( server_id, - LanguageServerLogType::Trace { - verbose_info: params.verbose, - }, + LanguageServerLogType::Trace(params.verbose), params.message, )); }) @@ -3484,13 +3482,13 @@ pub struct LspStore { buffer_store: Entity, worktree_store: Entity, pub languages: Arc, - pub language_server_statuses: BTreeMap, + language_server_statuses: BTreeMap, active_entry: Option, _maintain_workspace_config: (Task>, watch::Sender<()>), _maintain_buffer_languages: Task<()>, diagnostic_summaries: HashMap, HashMap>>, - pub lsp_server_capabilities: HashMap, + pub(super) lsp_server_capabilities: HashMap, lsp_document_colors: HashMap, lsp_code_lens: HashMap, running_lsp_requests: HashMap>)>, @@ -3567,7 +3565,6 @@ pub struct LanguageServerStatus { pub pending_work: BTreeMap, pub has_pending_diagnostic_updates: bool, progress_tokens: HashSet, - pub worktree: Option, } #[derive(Clone, Debug)] @@ -7486,7 +7483,7 @@ impl LspStore { server: Some(proto::LanguageServer { id: server_id.to_proto(), name: status.name.to_string(), - worktree_id: status.worktree.map(|id| id.to_proto()), + worktree_id: None, }), capabilities: serde_json::to_string(&server.capabilities()) .expect("serializing server LSP capabilities"), @@ -7530,7 +7527,6 @@ impl LspStore { pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), - worktree: server.worktree_id.map(WorktreeId::from_proto), }, ) }) @@ -8896,7 +8892,6 @@ impl LspStore { pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), - worktree: server.worktree_id.map(WorktreeId::from_proto), }, ); cx.emit(LspStoreEvent::LanguageServerAdded( @@ -10910,7 +10905,6 @@ impl LspStore { pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), - worktree: Some(key.worktree_id), }, ); @@ -12196,14 +12190,6 @@ impl LspStore { let data = self.lsp_code_lens.get_mut(&buffer_id)?; Some(data.update.take()?.1) } - - pub fn downstream_client(&self) -> Option<(AnyProtoClient, u64)> { - self.downstream_client.clone() - } - - pub fn worktree_store(&self) -> Entity { - self.worktree_store.clone() - } } // Registration with registerOptions as null, should fallback to true. @@ -12713,69 +12699,45 @@ impl PartialEq for LanguageServerPromptRequest { #[derive(Clone, Debug, PartialEq)] pub enum LanguageServerLogType { Log(MessageType), - Trace { verbose_info: Option }, - Rpc { received: bool }, + Trace(Option), } impl LanguageServerLogType { pub fn to_proto(&self) -> proto::language_server_log::LogType { match self { Self::Log(log_type) => { - use proto::log_message::LogLevel; - let level = match *log_type { - MessageType::ERROR => LogLevel::Error, - MessageType::WARNING => LogLevel::Warning, - MessageType::INFO => LogLevel::Info, - MessageType::LOG => LogLevel::Log, + let message_type = match *log_type { + MessageType::ERROR => 1, + MessageType::WARNING => 2, + MessageType::INFO => 3, + MessageType::LOG => 4, other => { - log::warn!("Unknown lsp log message type: {other:?}"); - LogLevel::Log + log::warn!("Unknown lsp log message type: {:?}", other); + 4 } }; - proto::language_server_log::LogType::Log(proto::LogMessage { - level: level as i32, - }) + proto::language_server_log::LogType::LogMessageType(message_type) } - Self::Trace { verbose_info } => { - proto::language_server_log::LogType::Trace(proto::TraceMessage { - verbose_info: verbose_info.to_owned(), + Self::Trace(message) => { + proto::language_server_log::LogType::LogTrace(proto::LspLogTrace { + message: message.clone(), }) } - Self::Rpc { received } => { - let kind = if *received { - proto::rpc_message::Kind::Received - } else { - proto::rpc_message::Kind::Sent - }; - let kind = kind as i32; - proto::language_server_log::LogType::Rpc(proto::RpcMessage { kind }) - } } } pub fn from_proto(log_type: proto::language_server_log::LogType) -> Self { - use proto::log_message::LogLevel; - use proto::rpc_message; match log_type { - proto::language_server_log::LogType::Log(message_type) => Self::Log( - match LogLevel::from_i32(message_type.level).unwrap_or(LogLevel::Log) { - LogLevel::Error => MessageType::ERROR, - LogLevel::Warning => MessageType::WARNING, - LogLevel::Info => MessageType::INFO, - LogLevel::Log => MessageType::LOG, - }, - ), - proto::language_server_log::LogType::Trace(trace_message) => Self::Trace { - verbose_info: trace_message.verbose_info, - }, - proto::language_server_log::LogType::Rpc(message) => Self::Rpc { - received: match rpc_message::Kind::from_i32(message.kind) - .unwrap_or(rpc_message::Kind::Received) - { - rpc_message::Kind::Received => true, - rpc_message::Kind::Sent => false, - }, - }, + proto::language_server_log::LogType::LogMessageType(message_type) => { + Self::Log(match message_type { + 1 => MessageType::ERROR, + 2 => MessageType::WARNING, + 3 => MessageType::INFO, + 4 => MessageType::LOG, + _ => MessageType::LOG, + }) + } + proto::language_server_log::LogType::LogTrace(trace) => Self::Trace(trace.message), } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 63ce309e7a1cf9f8c6c3d7868be7ed343d1c010a..9e3900198cbc9aa4845428235196763511c0751c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -280,11 +280,6 @@ pub enum Event { server_id: LanguageServerId, buffer_id: BufferId, buffer_abs_path: PathBuf, - name: Option, - }, - ToggleLspLogs { - server_id: LanguageServerId, - enabled: bool, }, Toast { notification_id: SharedString, @@ -1006,7 +1001,6 @@ impl Project { client.add_entity_request_handler(Self::handle_open_buffer_by_path); client.add_entity_request_handler(Self::handle_open_new_buffer); client.add_entity_message_handler(Self::handle_create_buffer_for_peer); - client.add_entity_message_handler(Self::handle_toggle_lsp_logs); WorktreeStore::init(&client); BufferStore::init(&client); @@ -2977,7 +2971,6 @@ impl Project { buffer_id, server_id: *language_server_id, buffer_abs_path: PathBuf::from(&update.buffer_abs_path), - name: name.clone(), }); } } @@ -4704,20 +4697,6 @@ impl Project { })? } - async fn handle_toggle_lsp_logs( - project: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - project.update(&mut cx, |_, cx| { - cx.emit(Event::ToggleLspLogs { - server_id: LanguageServerId::from_proto(envelope.payload.server_id), - enabled: envelope.payload.enabled, - }) - })?; - Ok(()) - } - async fn handle_synchronize_buffers( this: Entity, envelope: TypedEnvelope, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index a8f911883d619214a35f3ef5d80f83d6dc1b3894..f49713d2080d48c9576118bff5fcd241f092234c 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1951,7 +1951,6 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC server_id: LanguageServerId(1), buffer_id, buffer_abs_path: PathBuf::from(path!("/dir/a.rs")), - name: Some(fake_server.server.name()) } ); assert_eq!( diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 16f6217b29d50a4a2eb9198565f688335c218802..473ef5c38cc6f401a05556c1f02271e83bd8fa97 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -610,36 +610,11 @@ message ServerMetadataUpdated { message LanguageServerLog { uint64 project_id = 1; uint64 language_server_id = 2; - string message = 3; oneof log_type { - LogMessage log = 4; - TraceMessage trace = 5; - RpcMessage rpc = 6; - } -} - -message LogMessage { - LogLevel level = 1; - - enum LogLevel { - LOG = 0; - INFO = 1; - WARNING = 2; - ERROR = 3; - } -} - -message TraceMessage { - optional string verbose_info = 1; -} - -message RpcMessage { - Kind kind = 1; - - enum Kind { - RECEIVED = 0; - SENT = 1; + uint32 log_message_type = 3; + LspLogTrace log_trace = 4; } + string message = 5; } message LspLogTrace { @@ -957,16 +932,3 @@ message MultiLspQuery { message MultiLspQueryResponse { repeated LspResponse responses = 1; } - -message ToggleLspLogs { - uint64 project_id = 1; - LogType log_type = 2; - uint64 server_id = 3; - bool enabled = 4; - - enum LogType { - LOG = 0; - TRACE = 1; - RPC = 2; - } -} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 2222bdec082759cb75ffcdb2c7a95435f36eba11..70689bcd6306195fce0d5c6449bf3dd9f5d43539 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -396,8 +396,7 @@ message Envelope { GitCloneResponse git_clone_response = 364; LspQuery lsp_query = 365; - LspQueryResponse lsp_query_response = 366; - ToggleLspLogs toggle_lsp_logs = 367; // current max + LspQueryResponse lsp_query_response = 366; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 04495fb898b1d9bdbf229bb69e1e44b8afa6d1fb..e17ec5203bd5b7bcab03c6461c343156116cc563 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -312,8 +312,7 @@ messages!( (GetDefaultBranch, Background), (GetDefaultBranchResponse, Background), (GitClone, Background), - (GitCloneResponse, Background), - (ToggleLspLogs, Background), + (GitCloneResponse, Background) ); request_messages!( @@ -482,8 +481,7 @@ request_messages!( (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse), (PullWorkspaceDiagnostics, Ack), (GetDefaultBranch, GetDefaultBranchResponse), - (GitClone, GitCloneResponse), - (ToggleLspLogs, Ack), + (GitClone, GitCloneResponse) ); lsp_messages!( @@ -614,7 +612,6 @@ entity_messages!( GitReset, GitCheckoutFiles, SetIndexText, - ToggleLspLogs, Push, Fetch, diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 249968b246c0615d1a8d60c4446eeac6bcac7451..5dbb9a2771c4e3fda04ed014f993f843b44dd976 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -43,7 +43,6 @@ gpui_tokio.workspace = true http_client.workspace = true language.workspace = true language_extension.workspace = true -language_tools.workspace = true languages.workspace = true log.workspace = true lsp.workspace = true diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 1e197fdd338432a7731933cb8051bb2bb4265c18..04028ebcac82f814652f32ad7439e32d650f5ad0 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -1,7 +1,5 @@ use ::proto::{FromProto, ToProto}; use anyhow::{Context as _, Result, anyhow}; -use language_tools::lsp_log::{GlobalLogStore, LanguageServerKind}; -use lsp::LanguageServerId; use extension::ExtensionHostProxy; use extension_host::headless_host::HeadlessExtensionStore; @@ -67,7 +65,6 @@ impl HeadlessProject { settings::init(cx); language::init(cx); project::Project::init_settings(cx); - language_tools::lsp_log::init(false, cx); } pub fn new( @@ -238,7 +235,6 @@ impl HeadlessProject { session.add_entity_request_handler(Self::handle_open_new_buffer); session.add_entity_request_handler(Self::handle_find_search_candidates); session.add_entity_request_handler(Self::handle_open_server_settings); - session.add_entity_message_handler(Self::handle_toggle_lsp_logs); session.add_entity_request_handler(BufferStore::handle_update_buffer); session.add_entity_message_handler(BufferStore::handle_close_buffer); @@ -302,40 +298,11 @@ impl HeadlessProject { fn on_lsp_store_event( &mut self, - lsp_store: Entity, + _lsp_store: Entity, event: &LspStoreEvent, cx: &mut Context, ) { match event { - LspStoreEvent::LanguageServerAdded(id, name, worktree_id) => { - let log_store = cx - .try_global::() - .and_then(|lsp_logs| lsp_logs.0.upgrade()); - if let Some(log_store) = log_store { - log_store.update(cx, |log_store, cx| { - log_store.add_language_server( - LanguageServerKind::LocalSsh { - lsp_store: self.lsp_store.downgrade(), - }, - *id, - Some(name.clone()), - *worktree_id, - lsp_store.read(cx).language_server_for_id(*id), - cx, - ); - }); - } - } - LspStoreEvent::LanguageServerRemoved(id) => { - let log_store = cx - .try_global::() - .and_then(|lsp_logs| lsp_logs.0.upgrade()); - if let Some(log_store) = log_store { - log_store.update(cx, |log_store, cx| { - log_store.remove_language_server(*id, cx); - }); - } - } LspStoreEvent::LanguageServerUpdate { language_server_id, name, @@ -359,6 +326,16 @@ impl HeadlessProject { }) .log_err(); } + LspStoreEvent::LanguageServerLog(language_server_id, log_type, message) => { + self.session + .send(proto::LanguageServerLog { + project_id: REMOTE_SERVER_PROJECT_ID, + language_server_id: language_server_id.to_proto(), + message: message.clone(), + log_type: Some(log_type.to_proto()), + }) + .log_err(); + } LspStoreEvent::LanguageServerPrompt(prompt) => { let request = self.session.request(proto::LanguageServerPromptRequest { project_id: REMOTE_SERVER_PROJECT_ID, @@ -532,31 +509,7 @@ impl HeadlessProject { }) } - async fn handle_toggle_lsp_logs( - _: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let server_id = LanguageServerId::from_proto(envelope.payload.server_id); - let lsp_logs = cx - .update(|cx| { - cx.try_global::() - .and_then(|lsp_logs| lsp_logs.0.upgrade()) - })? - .context("lsp logs store is missing")?; - - lsp_logs.update(&mut cx, |lsp_logs, _| { - // we do not support any other log toggling yet - if envelope.payload.enabled { - lsp_logs.enable_rpc_trace_for_language_server(server_id); - } else { - lsp_logs.disable_rpc_trace_for_language_server(server_id); - } - })?; - Ok(()) - } - - async fn handle_open_server_settings( + pub async fn handle_open_server_settings( this: Entity, _: TypedEnvelope, mut cx: AsyncApp, @@ -609,7 +562,7 @@ impl HeadlessProject { }) } - async fn handle_find_search_candidates( + pub async fn handle_find_search_candidates( this: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, @@ -641,7 +594,7 @@ impl HeadlessProject { Ok(response) } - async fn handle_list_remote_directory( + pub async fn handle_list_remote_directory( this: Entity, envelope: TypedEnvelope, cx: AsyncApp, @@ -673,7 +626,7 @@ impl HeadlessProject { }) } - async fn handle_get_path_metadata( + pub async fn handle_get_path_metadata( this: Entity, envelope: TypedEnvelope, cx: AsyncApp, @@ -691,7 +644,7 @@ impl HeadlessProject { }) } - async fn handle_shutdown_remote_server( + pub async fn handle_shutdown_remote_server( _this: Entity, _envelope: TypedEnvelope, cx: AsyncApp, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index a0717333159e508ea42a1b95bd9f2226e6392871..1966755d626af6e155440379982af180e9ccbc95 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -30,7 +30,7 @@ pub struct ActiveSettingsProfileName(pub String); impl Global for ActiveSettingsProfileName {} -#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord, serde::Serialize)] +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct WorktreeId(usize); impl From for usize { diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index ce9be0c7edb47106fced94485d5e5d30abe0dedb..869aa5322eba7fdaf417606dd62ae73a0c3702b3 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -13,7 +13,6 @@ path = "src/workspace.rs" doctest = false [features] -default = ["call"] test-support = [ "call/test-support", "client/test-support", @@ -30,7 +29,7 @@ test-support = [ any_vec.workspace = true anyhow.workspace = true async-recursion.workspace = true -call = { workspace = true, optional = true } +call.workspace = true client.workspace = true clock.workspace = true collections.workspace = true diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 5c0814803173dc33fba4b50fc663d70c15cc0694..9c2d09fd26308b95ab145b557a516b5e6603a0e4 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -4,14 +4,11 @@ use crate::{ workspace_settings::{PaneSplitDirectionHorizontal, PaneSplitDirectionVertical}, }; use anyhow::Result; - -#[cfg(feature = "call")] use call::{ActiveCall, ParticipantLocation}; - use collections::HashMap; use gpui::{ - Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, Pixels, Point, - StyleRefinement, WeakEntity, Window, point, size, + Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, MouseButton, Pixels, + Point, StyleRefinement, WeakEntity, Window, point, size, }; use parking_lot::Mutex; use project::Project; @@ -200,7 +197,6 @@ pub enum Member { pub struct PaneRenderContext<'a> { pub project: &'a Entity, pub follower_states: &'a HashMap, - #[cfg(feature = "call")] pub active_call: Option<&'a Entity>, pub active_pane: &'a Entity, pub app_state: &'a Arc, @@ -262,11 +258,6 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> { let mut leader_color; let status_box; match leader_id { - #[cfg(not(feature = "call"))] - CollaboratorId::PeerId(_) => { - return LeaderDecoration::default(); - } - #[cfg(feature = "call")] CollaboratorId::PeerId(peer_id) => { let Some(leader) = self.active_call.as_ref().and_then(|call| { let room = call.read(cx).room()?.read(cx); @@ -324,7 +315,7 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> { |this, (leader_project_id, leader_user_id)| { let app_state = self.app_state.clone(); this.cursor_pointer().on_mouse_down( - gpui::MouseButton::Left, + MouseButton::Left, move |_, _, cx| { crate::join_in_room_project( leader_project_id, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b6577ff325c1f5a4ff0d9c861f357fb8f4007696..25e2cb1cfe934a88ec4cc3811bf3216e0765c0af 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -9,7 +9,6 @@ pub mod pane_group; mod path_list; mod persistence; pub mod searchable; -#[cfg(feature = "call")] pub mod shared_screen; mod status_bar; pub mod tasks; @@ -23,17 +22,11 @@ pub use dock::Panel; pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; -#[cfg(feature = "call")] -use call::{ActiveCall, call_settings::CallSettings}; -#[cfg(feature = "call")] -use client::{Status, proto::ErrorCode}; -#[cfg(feature = "call")] -use shared_screen::SharedScreen; - use anyhow::{Context as _, Result, anyhow}; +use call::{ActiveCall, call_settings::CallSettings}; use client::{ - ChannelId, Client, ErrorExt, TypedEnvelope, UserStore, - proto::{self, PanelId, PeerId}, + ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore, + proto::{self, ErrorCode, PanelId, PeerId}, }; use collections::{HashMap, HashSet, hash_map}; use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; @@ -86,6 +79,7 @@ use schemars::JsonSchema; use serde::Deserialize; use session::AppSession; use settings::{Settings, update_settings_file}; +use shared_screen::SharedScreen; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, @@ -892,7 +886,6 @@ impl Global for GlobalAppState {} pub struct WorkspaceStore { workspaces: HashSet>, - #[cfg(feature = "call")] client: Arc, _subscriptions: Vec, } @@ -1124,7 +1117,6 @@ pub struct Workspace { window_edited: bool, last_window_title: Option, dirty_items: HashMap, - #[cfg(feature = "call")] active_call: Option<(Entity, Vec)>, leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: Option, @@ -1166,7 +1158,6 @@ pub struct FollowerState { struct FollowerView { view: Box, - #[cfg(feature = "call")] location: Option, } @@ -1366,15 +1357,10 @@ impl Workspace { let session_id = app_state.session.read(cx).id().to_owned(); - #[cfg(feature = "call")] let mut active_call = None; - #[cfg(feature = "call")] - { - if let Some(call) = ActiveCall::try_global(cx) { - let subscriptions = - vec![cx.subscribe_in(&call, window, Self::on_active_call_event)]; - active_call = Some((call, subscriptions)); - } + if let Some(call) = ActiveCall::try_global(cx) { + let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)]; + active_call = Some((call, subscriptions)); } let (serializable_items_tx, serializable_items_rx) = @@ -1460,7 +1446,6 @@ impl Workspace { window_edited: false, last_window_title: None, dirty_items: Default::default(), - #[cfg(feature = "call")] active_call, database_id: workspace_id, app_state, @@ -2265,7 +2250,6 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) -> Task> { - #[cfg(feature = "call")] let active_call = self.active_call().cloned(); // On Linux and Windows, closing the last window should restore the last workspace. @@ -2274,58 +2258,51 @@ impl Workspace { && cx.windows().len() == 1; cx.spawn_in(window, async move |this, cx| { - #[cfg(feature = "call")] + let workspace_count = cx.update(|_window, cx| { + cx.windows() + .iter() + .filter(|window| window.downcast::().is_some()) + .count() + })?; + + if let Some(active_call) = active_call + && workspace_count == 1 + && active_call.read_with(cx, |call, _| call.room().is_some())? { - let workspace_count = cx.update(|_window, cx| { - cx.windows() - .iter() - .filter(|window| window.downcast::().is_some()) - .count() - })?; - if let Some(active_call) = active_call - && workspace_count == 1 - && active_call.read_with(cx, |call, _| call.room().is_some())? - { - if close_intent == CloseIntent::CloseWindow { - let answer = cx.update(|window, cx| { - window.prompt( - PromptLevel::Warning, - "Do you want to leave the current call?", - None, - &["Close window and hang up", "Cancel"], - cx, - ) - })?; + if close_intent == CloseIntent::CloseWindow { + let answer = cx.update(|window, cx| { + window.prompt( + PromptLevel::Warning, + "Do you want to leave the current call?", + None, + &["Close window and hang up", "Cancel"], + cx, + ) + })?; - if answer.await.log_err() == Some(1) { - return anyhow::Ok(false); - } else { - { - active_call - .update(cx, |call, cx| call.hang_up(cx))? - .await - .log_err(); - } - } + if answer.await.log_err() == Some(1) { + return anyhow::Ok(false); + } else { + active_call + .update(cx, |call, cx| call.hang_up(cx))? + .await + .log_err(); } - if close_intent == CloseIntent::ReplaceWindow { - #[cfg(feature = "call")] - { - _ = active_call.update(cx, |active_call, cx| { - let workspace = cx - .windows() - .iter() - .filter_map(|window| window.downcast::()) - .next() - .unwrap(); - let project = workspace.read(cx)?.project.clone(); - if project.read(cx).is_shared() { - active_call.unshare_project(project, cx)?; - } - anyhow::Ok(()) - })?; + } + if close_intent == CloseIntent::ReplaceWindow { + _ = active_call.update(cx, |this, cx| { + let workspace = cx + .windows() + .iter() + .filter_map(|window| window.downcast::()) + .next() + .unwrap(); + let project = workspace.read(cx)?.project.clone(); + if project.read(cx).is_shared() { + this.unshare_project(project, cx)?; } - } + Ok::<_, anyhow::Error>(()) + })?; } } @@ -3509,7 +3486,6 @@ impl Workspace { item } - #[cfg(feature = "call")] pub fn open_shared_screen( &mut self, peer_id: PeerId, @@ -3931,11 +3907,8 @@ impl Workspace { pane.update(cx, |pane, _| { pane.track_alternate_file_items(); }); - #[cfg(feature = "call")] - { - if *local { - self.unfollow_in_pane(pane, window, cx); - } + if *local { + self.unfollow_in_pane(pane, window, cx); } serialize_workspace = *focus_changed || pane != self.active_pane(); if pane == self.active_pane() { @@ -4000,17 +3973,6 @@ impl Workspace { } } - #[cfg(not(feature = "call"))] - pub fn unfollow_in_pane( - &mut self, - _pane: &Entity, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - None - } - - #[cfg(feature = "call")] pub fn unfollow_in_pane( &mut self, pane: &Entity, @@ -4160,7 +4122,6 @@ impl Workspace { cx.notify(); } - #[cfg(feature = "call")] pub fn start_following( &mut self, leader_id: impl Into, @@ -4224,16 +4185,6 @@ impl Workspace { } } - #[cfg(not(feature = "call"))] - pub fn follow_next_collaborator( - &mut self, - _: &FollowNextCollaborator, - _window: &mut Window, - _cx: &mut Context, - ) { - } - - #[cfg(feature = "call")] pub fn follow_next_collaborator( &mut self, _: &FollowNextCollaborator, @@ -4282,16 +4233,6 @@ impl Workspace { } } - #[cfg(not(feature = "call"))] - pub fn follow( - &mut self, - _leader_id: impl Into, - _window: &mut Window, - _cx: &mut Context, - ) { - } - - #[cfg(feature = "call")] pub fn follow( &mut self, leader_id: impl Into, @@ -4344,17 +4285,6 @@ impl Workspace { } } - #[cfg(not(feature = "call"))] - pub fn unfollow( - &mut self, - _leader_id: impl Into, - _window: &mut Window, - _cx: &mut Context, - ) -> Option<()> { - None - } - - #[cfg(feature = "call")] pub fn unfollow( &mut self, leader_id: impl Into, @@ -4665,7 +4595,6 @@ impl Workspace { anyhow::bail!("no id for view"); }; let id = ViewId::from_proto(id)?; - #[cfg(feature = "call")] let panel_id = view.panel_id.and_then(proto::PanelId::from_i32); let pane = this.update(cx, |this, _cx| { @@ -4738,7 +4667,6 @@ impl Workspace { id, FollowerView { view: item, - #[cfg(feature = "call")] location: panel_id, }, ); @@ -4793,7 +4721,6 @@ impl Workspace { view.map(|view| { entry.insert(FollowerView { view, - #[cfg(feature = "call")] location: None, }) }) @@ -4984,17 +4911,6 @@ impl Workspace { ) } - #[cfg(not(feature = "call"))] - fn active_item_for_peer( - &self, - _peer_id: PeerId, - _window: &mut Window, - _cx: &mut Context, - ) -> Option<(Option, Box)> { - None - } - - #[cfg(feature = "call")] fn active_item_for_peer( &self, peer_id: PeerId, @@ -5036,7 +4952,6 @@ impl Workspace { item_to_activate } - #[cfg(feature = "call")] fn shared_screen_for_peer( &self, peer_id: PeerId, @@ -5087,12 +5002,10 @@ impl Workspace { } } - #[cfg(feature = "call")] pub fn active_call(&self) -> Option<&Entity> { self.active_call.as_ref().map(|(call, _)| call) } - #[cfg(feature = "call")] fn on_active_call_event( &mut self, _: &Entity, @@ -6005,17 +5918,6 @@ impl Workspace { } } -#[cfg(not(feature = "call"))] -fn leader_border_for_pane( - _follower_states: &HashMap, - _pane: &Entity, - _: &Window, - _cx: &App, -) -> Option
{ - None -} - -#[cfg(feature = "call")] fn leader_border_for_pane( follower_states: &HashMap, pane: &Entity, @@ -6482,7 +6384,6 @@ impl Render for Workspace { &PaneRenderContext { follower_states: &self.follower_states, - #[cfg(feature = "call")] active_call: self.active_call(), active_pane: &self.active_pane, app_state: &self.app_state, @@ -6547,7 +6448,6 @@ impl Render for Workspace { &PaneRenderContext { follower_states: &self.follower_states, - #[cfg(feature = "call")] active_call: self.active_call(), active_pane: &self.active_pane, app_state: &self.app_state, @@ -6610,7 +6510,6 @@ impl Render for Workspace { &PaneRenderContext { follower_states: &self.follower_states, - #[cfg(feature = "call")] active_call: self.active_call(), active_pane: &self.active_pane, app_state: &self.app_state, @@ -6659,7 +6558,6 @@ impl Render for Workspace { &PaneRenderContext { follower_states: &self.follower_states, - #[cfg(feature = "call")] active_call: self.active_call(), active_pane: &self.active_pane, app_state: &self.app_state, @@ -6733,22 +6631,10 @@ impl WorkspaceStore { client.add_request_handler(cx.weak_entity(), Self::handle_follow), client.add_message_handler(cx.weak_entity(), Self::handle_update_followers), ], - #[cfg(feature = "call")] client, } } - #[cfg(not(feature = "call"))] - pub fn update_followers( - &self, - _project_id: Option, - _update: proto::update_followers::Variant, - _cx: &App, - ) -> Option<()> { - None - } - - #[cfg(feature = "call")] pub fn update_followers( &self, project_id: Option, @@ -6914,7 +6800,6 @@ actions!( ] ); -#[cfg(feature = "call")] async fn join_channel_internal( channel_id: ChannelId, app_state: &Arc, @@ -7062,17 +6947,6 @@ async fn join_channel_internal( anyhow::Ok(false) } -#[cfg(not(feature = "call"))] -pub fn join_channel( - _channel_id: ChannelId, - _app_state: Arc, - _requesting_window: Option>, - _cx: &mut App, -) -> Task> { - Task::ready(Ok(())) -} - -#[cfg(feature = "call")] pub fn join_channel( channel_id: ChannelId, app_state: Arc, @@ -7580,17 +7454,6 @@ fn serialize_ssh_project( }) } -#[cfg(not(feature = "call"))] -pub fn join_in_room_project( - _project_id: u64, - _follow_user_id: u64, - _app_state: Arc, - _cx: &mut App, -) -> Task> { - Task::ready(Ok(())) -} - -#[cfg(feature = "call")] pub fn join_in_room_project( project_id: u64, follow_user_id: u64, From 8af212e7853f88bd193d27e334f94bfc8d8bc5d1 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Wed, 27 Aug 2025 18:34:14 -0600 Subject: [PATCH 396/744] Fix watching of Git repo in presence of scanner restarts (#37052) The scanner is restarted after loading initial settings, and there was an optimization to not re-discover and re-watch git repositories if they already exist in the snapshot. #35865 added cleanup of watches that occurred when the scanner restarts, and so in some cases repos were no longer watched. Release Notes: - Linux: Fixed a case where Git repositories might not be watched for changes, causing branch switching to not update the UI. Co-authored-by: Julia --- crates/worktree/src/worktree.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index cf61ee2669663f172fa1238db4d359970e23e4bc..845af538021326a0e609c9f6098ebf20ed1dc704 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3151,16 +3151,6 @@ impl BackgroundScannerState { .work_directory_abs_path(&work_directory) .log_err()?; - if self - .snapshot - .git_repositories - .get(&work_dir_entry.id) - .is_some() - { - log::trace!("existing git repository for {work_directory:?}"); - return None; - } - let dot_git_abs_path: Arc = self .snapshot .abs_path From 4e1a9010590d68b5bc97ecb4424cb73eb6e3f780 Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Thu, 28 Aug 2025 02:25:00 +0100 Subject: [PATCH 397/744] helix: Improve "x" behavior (#35611) Closes #32020 Release Notes: - Helix: Improve `x` behaviour. Will respect modifiers (`5 x`). Pressing `x` on a empty line, will select current+next line, because helix considers current line to be already selected without the need of pressing `x`. --- assets/keymaps/vim.json | 2 +- crates/vim/src/helix.rs | 188 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 187 insertions(+), 3 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 67add61bd35845c2e46c31c74d8ef4baf422aaf3..0a88baee027a3ae4d72409f5f142ceda3f4d9717 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -435,7 +435,7 @@ "g b": "vim::WindowBottom", "shift-r": "editor::Paste", - "x": "editor::SelectLine", + "x": "vim::HelixSelectLine", "shift-x": "editor::SelectLine", "%": "editor::SelectAll", // Window mode diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 726022021d8d834f31c0c5e6a0fcc24f329d13b9..abde3a8ce6e8755bb49826fb408a6af36661f00c 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -1,8 +1,10 @@ use editor::display_map::DisplaySnapshot; -use editor::{DisplayPoint, Editor, SelectionEffects, ToOffset, ToPoint, movement}; +use editor::{ + DisplayPoint, Editor, HideMouseCursorOrigin, SelectionEffects, ToOffset, ToPoint, movement, +}; use gpui::{Action, actions}; use gpui::{Context, Window}; -use language::{CharClassifier, CharKind}; +use language::{CharClassifier, CharKind, Point}; use text::{Bias, SelectionGoal}; use crate::motion; @@ -25,11 +27,14 @@ actions!( HelixAppend, /// Goes to the location of the last modification. HelixGotoLastModification, + /// Select entire line or multiple lines, extending downwards. + HelixSelectLine, ] ); pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::helix_normal_after); + Vim::action(editor, cx, Vim::helix_select_lines); Vim::action(editor, cx, Vim::helix_insert); Vim::action(editor, cx, Vim::helix_append); Vim::action(editor, cx, Vim::helix_yank); @@ -442,6 +447,47 @@ impl Vim { ) { self.jump(".".into(), false, false, window, cx); } + + pub fn helix_select_lines( + &mut self, + _: &HelixSelectLine, + window: &mut Window, + cx: &mut Context, + ) { + let count = Vim::take_count(cx).unwrap_or(1); + self.update_editor(cx, |_, editor, cx| { + editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = editor.selections.all::(cx); + let max_point = display_map.buffer_snapshot.max_point(); + let buffer_snapshot = &display_map.buffer_snapshot; + + for selection in &mut selections { + // Start always goes to column 0 of the first selected line + let start_row = selection.start.row; + let current_end_row = selection.end.row; + + // Check if cursor is on empty line by checking first character + let line_start_offset = buffer_snapshot.point_to_offset(Point::new(start_row, 0)); + let first_char = buffer_snapshot.chars_at(line_start_offset).next(); + let extra_line = if first_char == Some('\n') { 1 } else { 0 }; + + let end_row = current_end_row + count as u32 + extra_line; + + selection.start = Point::new(start_row, 0); + selection.end = if end_row > max_point.row { + max_point + } else { + Point::new(end_row, 0) + }; + selection.reversed = false; + } + + editor.change_selections(Default::default(), window, cx, |s| { + s.select(selections); + }); + }); + } } #[cfg(test)] @@ -850,4 +896,142 @@ mod test { Mode::HelixNormal, ); } + + #[gpui::test] + async fn test_helix_select_lines(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.set_state( + "line one\nline ˇtwo\nline three\nline four", + Mode::HelixNormal, + ); + cx.simulate_keystrokes("2 x"); + cx.assert_state( + "line one\n«line two\nline three\nˇ»line four", + Mode::HelixNormal, + ); + + // Test extending existing line selection + cx.set_state( + indoc! {" + li«ˇne one + li»ne two + line three + line four"}, + Mode::HelixNormal, + ); + cx.simulate_keystrokes("x"); + cx.assert_state( + indoc! {" + «line one + line two + ˇ»line three + line four"}, + Mode::HelixNormal, + ); + + // Pressing x in empty line, select next line (because helix considers cursor a selection) + cx.set_state( + indoc! {" + line one + ˇ + line three + line four"}, + Mode::HelixNormal, + ); + cx.simulate_keystrokes("x"); + cx.assert_state( + indoc! {" + line one + « + line three + ˇ»line four"}, + Mode::HelixNormal, + ); + + // Empty line with count selects extra + count lines + cx.set_state( + indoc! {" + line one + ˇ + line three + line four + line five"}, + Mode::HelixNormal, + ); + cx.simulate_keystrokes("2 x"); + cx.assert_state( + indoc! {" + line one + « + line three + line four + ˇ»line five"}, + Mode::HelixNormal, + ); + + // Compare empty vs non-empty line behavior + cx.set_state( + indoc! {" + ˇnon-empty line + line two + line three"}, + Mode::HelixNormal, + ); + cx.simulate_keystrokes("x"); + cx.assert_state( + indoc! {" + «non-empty line + ˇ»line two + line three"}, + Mode::HelixNormal, + ); + + // Same test but with empty line - should select one extra + cx.set_state( + indoc! {" + ˇ + line two + line three"}, + Mode::HelixNormal, + ); + cx.simulate_keystrokes("x"); + cx.assert_state( + indoc! {" + « + line two + ˇ»line three"}, + Mode::HelixNormal, + ); + + // Test selecting multiple lines with count + cx.set_state( + indoc! {" + ˇline one + line two + line threeˇ + line four + line five"}, + Mode::HelixNormal, + ); + cx.simulate_keystrokes("x"); + cx.assert_state( + indoc! {" + «line one + ˇ»line two + «line three + ˇ»line four + line five"}, + Mode::HelixNormal, + ); + cx.simulate_keystrokes("x"); + cx.assert_state( + indoc! {" + «line one + line two + line three + line four + ˇ»line five"}, + Mode::HelixNormal, + ); + } } From 0a9f40787216f18ee2f4dc79211877d1af496284 Mon Sep 17 00:00:00 2001 From: Dino Date: Thu, 28 Aug 2025 02:27:02 +0100 Subject: [PATCH 398/744] search: Add support for case-sensitivity pattern items (#34762) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This Pull Request introduces support for pattern items in the buffer search. It does so by splitting the `query` methods into two new methods: - `BufferSearchBar.raw_query` – returns the text from the search query editor - `BufferSearchBar.query` - returns the search query with pattern items removed Whenever the search query is updated, processing of the `EditorEvent::Edited` event ends up calling the `BufferSearchBar.apply_pattern_items` method, which parses the pattern items from the raw query, and updates the buffer search bar's search options accordingly. This `apply_pattern_items` function avoids updating the `BufferSearchBar.default_options` field in order to be able to reset the search options when a pattern items is removed. Lastly, new pattern items can easily be added by updating the `PATTERN_ITEMS` array. ### Screen Capture https://github.com/user-attachments/assets/ebd83c38-e480-4c24-9b8c-6edde69cf392 --- Closes #32390 Release Notes: - Added support for the `\c` and `\C` query pattern items to control case-sensitivity in buffer search --------- Co-authored-by: Conrad Irwin --- crates/project/src/search.rs | 132 +++++++++++++++++++++++++++- crates/search/src/buffer_search.rs | 21 +++-- crates/search/src/project_search.rs | 2 +- 3 files changed, 146 insertions(+), 9 deletions(-) diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index ee216a99763282d8d30a0b17c3df3b8da3213db7..f2c6091e0cb00b8da1a752e3d25afe3389e8c818 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -143,7 +143,7 @@ impl SearchQuery { pub fn regex( query: impl ToString, whole_word: bool, - case_sensitive: bool, + mut case_sensitive: bool, include_ignored: bool, one_match_per_line: bool, files_to_include: PathMatcher, @@ -153,6 +153,14 @@ impl SearchQuery { ) -> Result { let mut query = query.to_string(); let initial_query = Arc::from(query.as_str()); + + if let Some((case_sensitive_from_pattern, new_query)) = + Self::case_sensitive_from_pattern(&query) + { + case_sensitive = case_sensitive_from_pattern; + query = new_query + } + if whole_word { let mut word_query = String::new(); if let Some(first) = query.get(0..1) @@ -192,6 +200,45 @@ impl SearchQuery { }) } + /// Extracts case sensitivity settings from pattern items in the provided + /// query and returns the same query, with the pattern items removed. + /// + /// The following pattern modifiers are supported: + /// + /// - `\c` (case_sensitive: false) + /// - `\C` (case_sensitive: true) + /// + /// If no pattern item were found, `None` will be returned. + fn case_sensitive_from_pattern(query: &str) -> Option<(bool, String)> { + if !(query.contains("\\c") || query.contains("\\C")) { + return None; + } + + let mut was_escaped = false; + let mut new_query = String::new(); + let mut is_case_sensitive = None; + + for c in query.chars() { + if was_escaped { + if c == 'c' { + is_case_sensitive = Some(false); + } else if c == 'C' { + is_case_sensitive = Some(true); + } else { + new_query.push('\\'); + new_query.push(c); + } + was_escaped = false + } else if c == '\\' { + was_escaped = true + } else { + new_query.push(c); + } + } + + is_case_sensitive.map(|c| (c, new_query)) + } + pub fn from_proto(message: proto::SearchQuery) -> Result { let files_to_include = if message.files_to_include.is_empty() { message @@ -596,4 +643,87 @@ mod tests { } } } + + #[test] + fn test_case_sensitive_pattern_items() { + let case_sensitive = false; + let search_query = SearchQuery::regex( + "test\\C", + false, + case_sensitive, + false, + false, + Default::default(), + Default::default(), + false, + None, + ) + .expect("Should be able to create a regex SearchQuery"); + + assert_eq!( + search_query.case_sensitive(), + true, + "Case sensitivity should be enabled when \\C pattern item is present in the query." + ); + + let case_sensitive = true; + let search_query = SearchQuery::regex( + "test\\c", + true, + case_sensitive, + false, + false, + Default::default(), + Default::default(), + false, + None, + ) + .expect("Should be able to create a regex SearchQuery"); + + assert_eq!( + search_query.case_sensitive(), + false, + "Case sensitivity should be disabled when \\c pattern item is present, even if initially set to true." + ); + + let case_sensitive = false; + let search_query = SearchQuery::regex( + "test\\c\\C", + false, + case_sensitive, + false, + false, + Default::default(), + Default::default(), + false, + None, + ) + .expect("Should be able to create a regex SearchQuery"); + + assert_eq!( + search_query.case_sensitive(), + true, + "Case sensitivity should be enabled when \\C is the last pattern item, even after a \\c." + ); + + let case_sensitive = false; + let search_query = SearchQuery::regex( + "tests\\\\C", + false, + case_sensitive, + false, + false, + Default::default(), + Default::default(), + false, + None, + ) + .expect("Should be able to create a regex SearchQuery"); + + assert_eq!( + search_query.case_sensitive(), + false, + "Case sensitivity should not be enabled when \\C pattern item is preceded by a backslash." + ); + } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index b2096d48ef5ba4e3b74eb0955bb11ef32cb5498a..92992dced6066d7242ad15f666de5a3b98f76ed0 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1516,18 +1516,25 @@ mod tests { cx, ) }); - let cx = cx.add_empty_window(); - let editor = - cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx)); - - let search_bar = cx.new_window_entity(|window, cx| { + let mut editor = None; + let window = cx.add_window(|window, cx| { + let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure( + "keymaps/default-macos.json", + cx, + ) + .unwrap(); + cx.bind_keys(default_key_bindings); + editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx))); let mut search_bar = BufferSearchBar::new(None, window, cx); - search_bar.set_active_pane_item(Some(&editor), window, cx); + search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx); search_bar.show(window, cx); search_bar }); + let search_bar = window.root(cx).unwrap(); + + let cx = VisualTestContext::from_window(*window, cx).into_mut(); - (editor, search_bar, cx) + (editor.unwrap(), search_bar, cx) } #[gpui::test] diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8ac12588afa2acf75aba1091407bd6f8b83d51ce..1ee959f111bd5741a655551aa71030fd9d7c15c9 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1139,7 +1139,7 @@ impl ProjectSearchView { fn build_search_query(&mut self, cx: &mut Context) -> Option { // Do not bail early in this function, as we want to fill out `self.panels_with_errors`. - let text = self.query_editor.read(cx).text(cx); + let text = self.search_query_text(cx); let open_buffers = if self.included_opened_only { Some(self.open_buffers(cx)) } else { From 78c2f1621d1ce84a745d9c1aec4177bcab6f4f03 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Thu, 28 Aug 2025 05:51:22 +0200 Subject: [PATCH 399/744] Add macOS window tabs (#33334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/zed-industries/zed/issues/14722 Closes https://github.com/zed-industries/zed/issues/4948 Closes https://github.com/zed-industries/zed/issues/7136 Follow up of https://github.com/zed-industries/zed/pull/20557 and https://github.com/zed-industries/zed/pull/32238. Based on the discussions in the previous PRs and the pairing session with @ConradIrwin I've decided to rewrite it from scratch, to properly incorporate all the requirements. The feature is opt-in, the settings is set to false by default. Once enabled via the Zed settings, it will behave according to the user’s system preference, without requiring a restart — the next window opened will adopt the new behavior (similar to Ghostty). I’m not entirely sure if the changes to the Window class are the best approach. I’ve tried to keep things flexible enough that other applications built with GPUI won’t be affected (while giving them the option to still use it), but I’d appreciate input on whether this direction makes sense long-term. https://github.com/user-attachments/assets/9573e094-4394-41ad-930c-5375a8204cbf ### Features * System-aware tabbing behavior * Respects the three system modes: Always, Never, and Fullscreen (default on macOS) * Changing the Zed setting does not require a restart — the next window reflects the change * Full theme support * Integrates with light and dark themes * [One Dark](https://github.com/user-attachments/assets/d1f55ff7-2339-4b09-9faf-d3d610ba7ca2) * [One Light](https://github.com/user-attachments/assets/7776e30c-2686-493e-9598-cdcd7e476ecf) * Supports opaque/blurred/transparent themes as best as possible * [One Dark - blurred](https://github.com/user-attachments/assets/c4521311-66cb-4cee-9e37-15146f6869aa) * Dynamic layout adjustments * Only reserves tab bar space when tabs are actually visible * [With tabs](https://github.com/user-attachments/assets/3b6db943-58c5-4f55-bdf4-33d23ca7d820) * [Without tabs](https://github.com/user-attachments/assets/2d175959-5efc-4e4f-a15c-0108925c582e) * VS Code compatibility * Supports the `window.nativeTabs` setting in the VS Code settings importer * Command palette integration * Adds commands for managing tabs to the command palette * These can be assigned to keyboard shortcuts as well, but didn't add defaults as to not reserve precious default key combinations Happy to pair again if things can be improved codewise, or if explanations are necessary for certain choices! Release Notes: * Added support for native macOS window tabbing. When you set `"use_system_window_tabs": true`, Zed will merge windows in the same was as macOS: by default this happens only when full screened, but you can adjust your macOS settings to have this happen on all windows. --------- Co-authored-by: Conrad Irwin --- assets/settings/default.json | 2 + crates/agent_ui/src/ui/agent_notification.rs | 1 + crates/collab_ui/src/collab_ui.rs | 1 + crates/gpui/examples/window_positioning.rs | 1 + crates/gpui/src/app.rs | 305 +++++++++++- crates/gpui/src/platform.rs | 27 +- crates/gpui/src/platform/mac/window.rs | 337 ++++++++++++- crates/gpui/src/window.rs | 118 ++++- crates/rules_library/src/rules_library.rs | 2 +- crates/title_bar/src/platform_title_bar.rs | 22 +- crates/title_bar/src/system_window_tabs.rs | 477 +++++++++++++++++++ crates/title_bar/src/title_bar.rs | 5 +- crates/workspace/src/workspace.rs | 67 ++- crates/workspace/src/workspace_settings.rs | 7 + crates/zed/src/main.rs | 19 +- crates/zed/src/zed.rs | 8 + docs/src/configuring-zed.md | 10 + 17 files changed, 1357 insertions(+), 52 deletions(-) create mode 100644 crates/title_bar/src/system_window_tabs.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 804198090fb4f5649160f6534bd3ed54b1368bce..ef57412842c302571be6827119e4abe6505a43b3 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -363,6 +363,8 @@ // Whether to show code action buttons in the editor toolbar. "code_actions": false }, + // Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). + "use_system_window_tabs": false, // Titlebar related settings "title_bar": { // Whether to show the branch icon beside branch switcher in the titlebar. diff --git a/crates/agent_ui/src/ui/agent_notification.rs b/crates/agent_ui/src/ui/agent_notification.rs index b2342a87b5be315dac28212a1ec73d0c054932c3..af2a022f147b79a0a299c17dd26c7e9a8b62aeb9 100644 --- a/crates/agent_ui/src/ui/agent_notification.rs +++ b/crates/agent_ui/src/ui/agent_notification.rs @@ -62,6 +62,7 @@ impl AgentNotification { app_id: Some(app_id.to_owned()), window_min_size: None, window_decorations: Some(WindowDecorations::Client), + tabbing_identifier: None, ..Default::default() } } diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index a49e38a8dd96a2e4883ce054477749b5c9f4eb7e..b369d324adb617907d80b773e0982c1723b1bae6 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -66,6 +66,7 @@ fn notification_window_options( app_id: Some(app_id.to_owned()), window_min_size: None, window_decorations: Some(WindowDecorations::Client), + tabbing_identifier: None, ..Default::default() } } diff --git a/crates/gpui/examples/window_positioning.rs b/crates/gpui/examples/window_positioning.rs index 8180104e1e3d0315bd213a73122125fdef3ca744..ca6cd535d67aa8b2e700b2d0bc632056e928e0e7 100644 --- a/crates/gpui/examples/window_positioning.rs +++ b/crates/gpui/examples/window_positioning.rs @@ -62,6 +62,7 @@ fn build_window_options(display_id: DisplayId, bounds: Bounds) -> Window app_id: None, window_min_size: None, window_decorations: None, + tabbing_identifier: None, ..Default::default() } } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index b59d7e717ad9dadc222e36fb54f2cc0d01466b75..669a95bd91577577fc460ba30bdacc867e3f3e60 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -7,7 +7,7 @@ use std::{ path::{Path, PathBuf}, rc::{Rc, Weak}, sync::{Arc, atomic::Ordering::SeqCst}, - time::Duration, + time::{Duration, Instant}, }; use anyhow::{Context as _, Result, anyhow}; @@ -17,6 +17,7 @@ use futures::{ channel::oneshot, future::{LocalBoxFuture, Shared}, }; +use itertools::Itertools; use parking_lot::RwLock; use slotmap::SlotMap; @@ -39,8 +40,8 @@ use crate::{ Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder, PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle, - Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, - Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator, + Reservation, ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task, + TextSystem, Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator, colors::{Colors, GlobalColors}, current_platform, hash, init_app_menus, }; @@ -237,6 +238,303 @@ type WindowClosedHandler = Box; type ReleaseListener = Box; type NewEntityListener = Box, &mut App) + 'static>; +#[doc(hidden)] +#[derive(Clone, PartialEq, Eq)] +pub struct SystemWindowTab { + pub id: WindowId, + pub title: SharedString, + pub handle: AnyWindowHandle, + pub last_active_at: Instant, +} + +impl SystemWindowTab { + /// Create a new instance of the window tab. + pub fn new(title: SharedString, handle: AnyWindowHandle) -> Self { + Self { + id: handle.id, + title, + handle, + last_active_at: Instant::now(), + } + } +} + +/// A controller for managing window tabs. +#[derive(Default)] +pub struct SystemWindowTabController { + visible: Option, + tab_groups: FxHashMap>, +} + +impl Global for SystemWindowTabController {} + +impl SystemWindowTabController { + /// Create a new instance of the window tab controller. + pub fn new() -> Self { + Self { + visible: None, + tab_groups: FxHashMap::default(), + } + } + + /// Initialize the global window tab controller. + pub fn init(cx: &mut App) { + cx.set_global(SystemWindowTabController::new()); + } + + /// Get all tab groups. + pub fn tab_groups(&self) -> &FxHashMap> { + &self.tab_groups + } + + /// Get the next tab group window handle. + pub fn get_next_tab_group_window(cx: &mut App, id: WindowId) -> Option<&AnyWindowHandle> { + let controller = cx.global::(); + let current_group = controller + .tab_groups + .iter() + .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group)); + + let current_group = current_group?; + let mut group_ids: Vec<_> = controller.tab_groups.keys().collect(); + let idx = group_ids.iter().position(|g| *g == current_group)?; + let next_idx = (idx + 1) % group_ids.len(); + + controller + .tab_groups + .get(group_ids[next_idx]) + .and_then(|tabs| { + tabs.iter() + .max_by_key(|tab| tab.last_active_at) + .or_else(|| tabs.first()) + .map(|tab| &tab.handle) + }) + } + + /// Get the previous tab group window handle. + pub fn get_prev_tab_group_window(cx: &mut App, id: WindowId) -> Option<&AnyWindowHandle> { + let controller = cx.global::(); + let current_group = controller + .tab_groups + .iter() + .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group)); + + let current_group = current_group?; + let mut group_ids: Vec<_> = controller.tab_groups.keys().collect(); + let idx = group_ids.iter().position(|g| *g == current_group)?; + let prev_idx = if idx == 0 { + group_ids.len() - 1 + } else { + idx - 1 + }; + + controller + .tab_groups + .get(group_ids[prev_idx]) + .and_then(|tabs| { + tabs.iter() + .max_by_key(|tab| tab.last_active_at) + .or_else(|| tabs.first()) + .map(|tab| &tab.handle) + }) + } + + /// Get all tabs in the same window. + pub fn tabs(&self, id: WindowId) -> Option<&Vec> { + let tab_group = self + .tab_groups + .iter() + .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group)); + + if let Some(tab_group) = tab_group { + self.tab_groups.get(&tab_group) + } else { + None + } + } + + /// Initialize the visibility of the system window tab controller. + pub fn init_visible(cx: &mut App, visible: bool) { + let mut controller = cx.global_mut::(); + if controller.visible.is_none() { + controller.visible = Some(visible); + } + } + + /// Get the visibility of the system window tab controller. + pub fn is_visible(&self) -> bool { + self.visible.unwrap_or(false) + } + + /// Set the visibility of the system window tab controller. + pub fn set_visible(cx: &mut App, visible: bool) { + let mut controller = cx.global_mut::(); + controller.visible = Some(visible); + } + + /// Update the last active of a window. + pub fn update_last_active(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + for windows in controller.tab_groups.values_mut() { + for tab in windows.iter_mut() { + if tab.id == id { + tab.last_active_at = Instant::now(); + } + } + } + } + + /// Update the position of a tab within its group. + pub fn update_tab_position(cx: &mut App, id: WindowId, ix: usize) { + let mut controller = cx.global_mut::(); + for (_, windows) in controller.tab_groups.iter_mut() { + if let Some(current_pos) = windows.iter().position(|tab| tab.id == id) { + if ix < windows.len() && current_pos != ix { + let window_tab = windows.remove(current_pos); + windows.insert(ix, window_tab); + } + break; + } + } + } + + /// Update the title of a tab. + pub fn update_tab_title(cx: &mut App, id: WindowId, title: SharedString) { + let controller = cx.global::(); + let tab = controller + .tab_groups + .values() + .flat_map(|windows| windows.iter()) + .find(|tab| tab.id == id); + + if tab.map_or(true, |t| t.title == title) { + return; + } + + let mut controller = cx.global_mut::(); + for windows in controller.tab_groups.values_mut() { + for tab in windows.iter_mut() { + if tab.id == id { + tab.title = title.clone(); + } + } + } + } + + /// Insert a tab into a tab group. + pub fn add_tab(cx: &mut App, id: WindowId, tabs: Vec) { + let mut controller = cx.global_mut::(); + let Some(tab) = tabs.clone().into_iter().find(|tab| tab.id == id) else { + return; + }; + + let mut expected_tab_ids: Vec<_> = tabs + .iter() + .filter(|tab| tab.id != id) + .map(|tab| tab.id) + .sorted() + .collect(); + + let mut tab_group_id = None; + for (group_id, group_tabs) in &controller.tab_groups { + let tab_ids: Vec<_> = group_tabs.iter().map(|tab| tab.id).sorted().collect(); + if tab_ids == expected_tab_ids { + tab_group_id = Some(*group_id); + break; + } + } + + if let Some(tab_group_id) = tab_group_id { + if let Some(tabs) = controller.tab_groups.get_mut(&tab_group_id) { + tabs.push(tab); + } + } else { + let new_group_id = controller.tab_groups.len(); + controller.tab_groups.insert(new_group_id, tabs); + } + } + + /// Remove a tab from a tab group. + pub fn remove_tab(cx: &mut App, id: WindowId) -> Option { + let mut controller = cx.global_mut::(); + let mut removed_tab = None; + + controller.tab_groups.retain(|_, tabs| { + if let Some(pos) = tabs.iter().position(|tab| tab.id == id) { + removed_tab = Some(tabs.remove(pos)); + } + !tabs.is_empty() + }); + + removed_tab + } + + /// Move a tab to a new tab group. + pub fn move_tab_to_new_window(cx: &mut App, id: WindowId) { + let mut removed_tab = Self::remove_tab(cx, id); + let mut controller = cx.global_mut::(); + + if let Some(tab) = removed_tab { + let new_group_id = controller.tab_groups.keys().max().map_or(0, |k| k + 1); + controller.tab_groups.insert(new_group_id, vec![tab]); + } + } + + /// Merge all tab groups into a single group. + pub fn merge_all_windows(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + let Some(initial_tabs) = controller.tabs(id) else { + return; + }; + + let mut all_tabs = initial_tabs.clone(); + for tabs in controller.tab_groups.values() { + all_tabs.extend( + tabs.iter() + .filter(|tab| !initial_tabs.contains(tab)) + .cloned(), + ); + } + + controller.tab_groups.clear(); + controller.tab_groups.insert(0, all_tabs); + } + + /// Selects the next tab in the tab group in the trailing direction. + pub fn select_next_tab(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + let Some(tabs) = controller.tabs(id) else { + return; + }; + + let current_index = tabs.iter().position(|tab| tab.id == id).unwrap(); + let next_index = (current_index + 1) % tabs.len(); + + let _ = &tabs[next_index].handle.update(cx, |_, window, _| { + window.activate_window(); + }); + } + + /// Selects the previous tab in the tab group in the leading direction. + pub fn select_previous_tab(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + let Some(tabs) = controller.tabs(id) else { + return; + }; + + let current_index = tabs.iter().position(|tab| tab.id == id).unwrap(); + let previous_index = if current_index == 0 { + tabs.len() - 1 + } else { + current_index - 1 + }; + + let _ = &tabs[previous_index].handle.update(cx, |_, window, _| { + window.activate_window(); + }); + } +} + /// Contains the state of the full application, and passed as a reference to a variety of callbacks. /// Other [Context] derefs to this type. /// You need a reference to an `App` to access the state of a [Entity]. @@ -372,6 +670,7 @@ impl App { }); init_app_menus(platform.as_ref(), &app.borrow()); + SystemWindowTabController::init(&mut app.borrow_mut()); platform.on_keyboard_layout_change(Box::new({ let app = Rc::downgrade(&app); diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 8e9c52c2e750e5a158f0eedd461cee8bea02e54b..eb1d73814388a26503e9ada782bc358dc712b53c 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -40,8 +40,8 @@ use crate::{ DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput, Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene, - ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, Task, TaskLabel, Window, - WindowControlArea, hash, point, px, size, + ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, SystemWindowTab, Task, + TaskLabel, Window, WindowControlArea, hash, point, px, size, }; use anyhow::Result; use async_task::Runnable; @@ -502,9 +502,26 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn sprite_atlas(&self) -> Arc; // macOS specific methods + fn get_title(&self) -> String { + String::new() + } + fn tabbed_windows(&self) -> Option> { + None + } + fn tab_bar_visible(&self) -> bool { + false + } fn set_edited(&mut self, _edited: bool) {} fn show_character_palette(&self) {} fn titlebar_double_click(&self) {} + fn on_move_tab_to_new_window(&self, _callback: Box) {} + fn on_merge_all_windows(&self, _callback: Box) {} + fn on_select_previous_tab(&self, _callback: Box) {} + fn on_select_next_tab(&self, _callback: Box) {} + fn on_toggle_tab_bar(&self, _callback: Box) {} + fn merge_all_windows(&self) {} + fn move_tab_to_new_window(&self) {} + fn toggle_window_tab_overview(&self) {} #[cfg(target_os = "windows")] fn get_raw_handle(&self) -> windows::HWND; @@ -1113,6 +1130,9 @@ pub struct WindowOptions { /// Whether to use client or server side decorations. Wayland only /// Note that this may be ignored. pub window_decorations: Option, + + /// Tab group name, allows opening the window as a native tab on macOS 10.12+. Windows with the same tabbing identifier will be grouped together. + pub tabbing_identifier: Option, } /// The variables that can be configured when creating a new window @@ -1160,6 +1180,8 @@ pub(crate) struct WindowParams { pub display_id: Option, pub window_min_size: Option>, + #[cfg(target_os = "macos")] + pub tabbing_identifier: Option, } /// Represents the status of how a window should be opened. @@ -1212,6 +1234,7 @@ impl Default for WindowOptions { app_id: None, window_min_size: None, window_decorations: None, + tabbing_identifier: None, } } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index fbea4748a395c7377bd7ef3ca30515b7dbc5ff4b..0262cbb1213ca670cece780959c740f292764630 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -4,8 +4,10 @@ use crate::{ ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, - ScaledPixels, Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, - WindowControlArea, WindowKind, WindowParams, platform::PlatformInputHandler, point, px, size, + ScaledPixels, SharedString, Size, SystemWindowTab, Timer, WindowAppearance, + WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowKind, WindowParams, + dispatch_get_main_queue, dispatch_sys::dispatch_async_f, platform::PlatformInputHandler, point, + px, size, }; use block::ConcreteBlock; use cocoa::{ @@ -24,6 +26,7 @@ use cocoa::{ NSUserDefaults, }, }; + use core_graphics::display::{CGDirectDisplayID, CGPoint, CGRect}; use ctor::ctor; use futures::channel::oneshot; @@ -82,6 +85,12 @@ type NSDragOperation = NSUInteger; const NSDragOperationNone: NSDragOperation = 0; #[allow(non_upper_case_globals)] const NSDragOperationCopy: NSDragOperation = 1; +#[derive(PartialEq)] +pub enum UserTabbingPreference { + Never, + Always, + InFullScreen, +} #[link(name = "CoreGraphics", kind = "framework")] unsafe extern "C" { @@ -343,6 +352,36 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C conclude_drag_operation as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(addTitlebarAccessoryViewController:), + add_titlebar_accessory_view_controller as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(moveTabToNewWindow:), + move_tab_to_new_window as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(mergeAllWindows:), + merge_all_windows as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(selectNextTab:), + select_next_tab as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(selectPreviousTab:), + select_previous_tab as extern "C" fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(toggleTabBar:), + toggle_tab_bar as extern "C" fn(&Object, Sel, id), + ); + decl.register() } } @@ -375,6 +414,11 @@ struct MacWindowState { // Whether the next left-mouse click is also the focusing click. first_mouse: bool, fullscreen_restore_bounds: Bounds, + move_tab_to_new_window_callback: Option>, + merge_all_windows_callback: Option>, + select_next_tab_callback: Option>, + select_previous_tab_callback: Option>, + toggle_tab_bar_callback: Option>, } impl MacWindowState { @@ -536,6 +580,7 @@ impl MacWindow { show, display_id, window_min_size, + tabbing_identifier, }: WindowParams, executor: ForegroundExecutor, renderer_context: renderer::Context, @@ -543,7 +588,12 @@ impl MacWindow { unsafe { let pool = NSAutoreleasePool::new(nil); - let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO]; + let allows_automatic_window_tabbing = tabbing_identifier.is_some(); + if allows_automatic_window_tabbing { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES]; + } else { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO]; + } let mut style_mask; if let Some(titlebar) = titlebar.as_ref() { @@ -668,6 +718,11 @@ impl MacWindow { external_files_dragged: false, first_mouse: false, fullscreen_restore_bounds: Bounds::default(), + move_tab_to_new_window_callback: None, + merge_all_windows_callback: None, + select_next_tab_callback: None, + select_previous_tab_callback: None, + toggle_tab_bar_callback: None, }))); (*native_window).set_ivar( @@ -722,6 +777,11 @@ impl MacWindow { WindowKind::Normal => { native_window.setLevel_(NSNormalWindowLevel); native_window.setAcceptsMouseMovedEvents_(YES); + + if let Some(tabbing_identifier) = tabbing_identifier { + let tabbing_id = NSString::alloc(nil).init_str(tabbing_identifier.as_str()); + let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; + } } WindowKind::PopUp => { // Use a tracking area to allow receiving MouseMoved events even when @@ -750,6 +810,38 @@ impl MacWindow { } } + let app = NSApplication::sharedApplication(nil); + let main_window: id = msg_send![app, mainWindow]; + if allows_automatic_window_tabbing + && !main_window.is_null() + && main_window != native_window + { + let main_window_is_fullscreen = main_window + .styleMask() + .contains(NSWindowStyleMask::NSFullScreenWindowMask); + let user_tabbing_preference = Self::get_user_tabbing_preference() + .unwrap_or(UserTabbingPreference::InFullScreen); + let should_add_as_tab = user_tabbing_preference == UserTabbingPreference::Always + || user_tabbing_preference == UserTabbingPreference::InFullScreen + && main_window_is_fullscreen; + + if should_add_as_tab { + let main_window_can_tab: BOOL = + msg_send![main_window, respondsToSelector: sel!(addTabbedWindow:ordered:)]; + let main_window_visible: BOOL = msg_send![main_window, isVisible]; + + if main_window_can_tab == YES && main_window_visible == YES { + let _: () = msg_send![main_window, addTabbedWindow: native_window ordered: NSWindowOrderingMode::NSWindowAbove]; + + // Ensure the window is visible immediately after adding the tab, since the tab bar is updated with a new entry at this point. + // Note: Calling orderFront here can break fullscreen mode (makes fullscreen windows exit fullscreen), so only do this if the main window is not fullscreen. + if !main_window_is_fullscreen { + let _: () = msg_send![native_window, orderFront: nil]; + } + } + } + } + if focus && show { native_window.makeKeyAndOrderFront_(nil); } else if show { @@ -804,6 +896,33 @@ impl MacWindow { window_handles } } + + pub fn get_user_tabbing_preference() -> Option { + unsafe { + let defaults: id = NSUserDefaults::standardUserDefaults(); + let domain = NSString::alloc(nil).init_str("NSGlobalDomain"); + let key = NSString::alloc(nil).init_str("AppleWindowTabbingMode"); + + let dict: id = msg_send![defaults, persistentDomainForName: domain]; + let value: id = if !dict.is_null() { + msg_send![dict, objectForKey: key] + } else { + nil + }; + + let value_str = if !value.is_null() { + CStr::from_ptr(NSString::UTF8String(value)).to_string_lossy() + } else { + "".into() + }; + + match value_str.as_ref() { + "manual" => Some(UserTabbingPreference::Never), + "always" => Some(UserTabbingPreference::Always), + _ => Some(UserTabbingPreference::InFullScreen), + } + } + } } impl Drop for MacWindow { @@ -859,6 +978,46 @@ impl PlatformWindow for MacWindow { .detach(); } + fn merge_all_windows(&self) { + let native_window = self.0.lock().native_window; + unsafe extern "C" fn merge_windows_async(context: *mut std::ffi::c_void) { + let native_window = context as id; + let _: () = msg_send![native_window, mergeAllWindows:nil]; + } + + unsafe { + dispatch_async_f( + dispatch_get_main_queue(), + native_window as *mut std::ffi::c_void, + Some(merge_windows_async), + ); + } + } + + fn move_tab_to_new_window(&self) { + let native_window = self.0.lock().native_window; + unsafe extern "C" fn move_tab_async(context: *mut std::ffi::c_void) { + let native_window = context as id; + let _: () = msg_send![native_window, moveTabToNewWindow:nil]; + let _: () = msg_send![native_window, makeKeyAndOrderFront: nil]; + } + + unsafe { + dispatch_async_f( + dispatch_get_main_queue(), + native_window as *mut std::ffi::c_void, + Some(move_tab_async), + ); + } + } + + fn toggle_window_tab_overview(&self) { + let native_window = self.0.lock().native_window; + unsafe { + let _: () = msg_send![native_window, toggleTabOverview:nil]; + } + } + fn scale_factor(&self) -> f32 { self.0.as_ref().lock().scale_factor() } @@ -1059,6 +1218,17 @@ impl PlatformWindow for MacWindow { } } + fn get_title(&self) -> String { + unsafe { + let title: id = msg_send![self.0.lock().native_window, title]; + if title.is_null() { + "".to_string() + } else { + title.to_str().to_string() + } + } + } + fn set_app_id(&mut self, _app_id: &str) {} fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { @@ -1220,6 +1390,62 @@ impl PlatformWindow for MacWindow { self.0.lock().appearance_changed_callback = Some(callback); } + fn tabbed_windows(&self) -> Option> { + unsafe { + let windows: id = msg_send![self.0.lock().native_window, tabbedWindows]; + if windows.is_null() { + return None; + } + + let count: NSUInteger = msg_send![windows, count]; + let mut result = Vec::new(); + for i in 0..count { + let window: id = msg_send![windows, objectAtIndex:i]; + if msg_send![window, isKindOfClass: WINDOW_CLASS] { + let handle = get_window_state(&*window).lock().handle; + let title: id = msg_send![window, title]; + let title = SharedString::from(title.to_str().to_string()); + + result.push(SystemWindowTab::new(title, handle)); + } + } + + Some(result) + } + } + + fn tab_bar_visible(&self) -> bool { + unsafe { + let tab_group: id = msg_send![self.0.lock().native_window, tabGroup]; + if tab_group.is_null() { + false + } else { + let tab_bar_visible: BOOL = msg_send![tab_group, isTabBarVisible]; + tab_bar_visible == YES + } + } + } + + fn on_move_tab_to_new_window(&self, callback: Box) { + self.0.as_ref().lock().move_tab_to_new_window_callback = Some(callback); + } + + fn on_merge_all_windows(&self, callback: Box) { + self.0.as_ref().lock().merge_all_windows_callback = Some(callback); + } + + fn on_select_next_tab(&self, callback: Box) { + self.0.as_ref().lock().select_next_tab_callback = Some(callback); + } + + fn on_select_previous_tab(&self, callback: Box) { + self.0.as_ref().lock().select_previous_tab_callback = Some(callback); + } + + fn on_toggle_tab_bar(&self, callback: Box) { + self.0.as_ref().lock().toggle_tab_bar_callback = Some(callback); + } + fn draw(&self, scene: &crate::Scene) { let mut this = self.0.lock(); this.renderer.draw(scene); @@ -1661,6 +1887,7 @@ extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) { .occlusionState() .contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible) { + lock.move_traffic_light(); lock.start_display_link(); } else { lock.stop_display_link(); @@ -1722,7 +1949,7 @@ extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) { extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) { let window_state = unsafe { get_window_state(this) }; - let lock = window_state.lock(); + let mut lock = window_state.lock(); let is_active = unsafe { lock.native_window.isKeyWindow() == YES }; // When opening a pop-up while the application isn't active, Cocoa sends a spurious @@ -1743,9 +1970,34 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) let executor = lock.executor.clone(); drop(lock); + + // If window is becoming active, trigger immediate synchronous frame request. + if selector == sel!(windowDidBecomeKey:) && is_active { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.lock(); + + if let Some(mut callback) = lock.request_frame_callback.take() { + #[cfg(not(feature = "macos-blade"))] + lock.renderer.set_presents_with_transaction(true); + lock.stop_display_link(); + drop(lock); + callback(Default::default()); + + let mut lock = window_state.lock(); + lock.request_frame_callback = Some(callback); + #[cfg(not(feature = "macos-blade"))] + lock.renderer.set_presents_with_transaction(false); + lock.start_display_link(); + } + } + executor .spawn(async move { let mut lock = window_state.as_ref().lock(); + if is_active { + lock.move_traffic_light(); + } + if let Some(mut callback) = lock.activate_callback.take() { drop(lock); callback(is_active); @@ -2281,3 +2533,80 @@ unsafe fn remove_layer_background(layer: id) { } } } + +extern "C" fn add_titlebar_accessory_view_controller(this: &Object, _: Sel, view_controller: id) { + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), addTitlebarAccessoryViewController: view_controller]; + + // Hide the native tab bar and set its height to 0, since we render our own. + let accessory_view: id = msg_send![view_controller, view]; + let _: () = msg_send![accessory_view, setHidden: YES]; + let mut frame: NSRect = msg_send![accessory_view, frame]; + frame.size.height = 0.0; + let _: () = msg_send![accessory_view, setFrame: frame]; + } +} + +extern "C" fn move_tab_to_new_window(this: &Object, _: Sel, _: id) { + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), moveTabToNewWindow:nil]; + + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.move_tab_to_new_window_callback.take() { + drop(lock); + callback(); + window_state.lock().move_tab_to_new_window_callback = Some(callback); + } + } +} + +extern "C" fn merge_all_windows(this: &Object, _: Sel, _: id) { + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), mergeAllWindows:nil]; + + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.merge_all_windows_callback.take() { + drop(lock); + callback(); + window_state.lock().merge_all_windows_callback = Some(callback); + } + } +} + +extern "C" fn select_next_tab(this: &Object, _sel: Sel, _id: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.select_next_tab_callback.take() { + drop(lock); + callback(); + window_state.lock().select_next_tab_callback = Some(callback); + } +} + +extern "C" fn select_previous_tab(this: &Object, _sel: Sel, _id: id) { + let window_state = unsafe { get_window_state(this) }; + let mut lock = window_state.as_ref().lock(); + if let Some(mut callback) = lock.select_previous_tab_callback.take() { + drop(lock); + callback(); + window_state.lock().select_previous_tab_callback = Some(callback); + } +} + +extern "C" fn toggle_tab_bar(this: &Object, _sel: Sel, _id: id) { + unsafe { + let _: () = msg_send![super(this, class!(NSWindow)), toggleTabBar:nil]; + + let window_state = get_window_state(this); + let mut lock = window_state.as_ref().lock(); + lock.move_traffic_light(); + + if let Some(mut callback) = lock.toggle_tab_bar_callback.take() { + drop(lock); + callback(); + window_state.lock().toggle_tab_bar_callback = Some(callback); + } + } +} diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index cc0db3930329fe748dc1e60cc0d613e7351e5b68..e6ea3fef07b221b96dbfe5b19f794106eb11b4dd 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -12,11 +12,11 @@ use crate::{ PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size, - StrikethroughStyle, Style, SubscriberSet, Subscription, TabHandles, TaffyLayoutEngine, Task, - TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, - WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, - WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, - transparent_black, + StrikethroughStyle, Style, SubscriberSet, Subscription, SystemWindowTab, + SystemWindowTabController, TabHandles, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, + TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, + WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, + point, prelude::*, px, rems, size, transparent_black, }; use anyhow::{Context as _, Result, anyhow}; use collections::{FxHashMap, FxHashSet}; @@ -946,6 +946,8 @@ impl Window { app_id, window_min_size, window_decorations, + #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] + tabbing_identifier, } = options; let bounds = window_bounds @@ -964,8 +966,17 @@ impl Window { show, display_id, window_min_size, + #[cfg(target_os = "macos")] + tabbing_identifier, }, )?; + + let tab_bar_visible = platform_window.tab_bar_visible(); + SystemWindowTabController::init_visible(cx, tab_bar_visible); + if let Some(tabs) = platform_window.tabbed_windows() { + SystemWindowTabController::add_tab(cx, handle.window_id(), tabs); + } + let display_id = platform_window.display().map(|display| display.id()); let sprite_atlas = platform_window.sprite_atlas(); let mouse_position = platform_window.mouse_position(); @@ -995,9 +1006,13 @@ impl Window { } platform_window.on_close(Box::new({ + let window_id = handle.window_id(); let mut cx = cx.to_async(); move || { let _ = handle.update(&mut cx, |_, window, _| window.remove_window()); + let _ = cx.update(|cx| { + SystemWindowTabController::remove_tab(cx, window_id); + }); } })); platform_window.on_request_frame(Box::new({ @@ -1086,7 +1101,11 @@ impl Window { .activation_observers .clone() .retain(&(), |callback| callback(window, cx)); + + window.bounds_changed(cx); window.refresh(); + + SystemWindowTabController::update_last_active(cx, window.handle.id); }) .log_err(); } @@ -1127,6 +1146,57 @@ impl Window { .unwrap_or(None) }) }); + platform_window.on_move_tab_to_new_window({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::move_tab_to_new_window(cx, handle.window_id()); + }) + .log_err(); + }) + }); + platform_window.on_merge_all_windows({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::merge_all_windows(cx, handle.window_id()); + }) + .log_err(); + }) + }); + platform_window.on_select_next_tab({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::select_next_tab(cx, handle.window_id()); + }) + .log_err(); + }) + }); + platform_window.on_select_previous_tab({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::select_previous_tab(cx, handle.window_id()) + }) + .log_err(); + }) + }); + platform_window.on_toggle_tab_bar({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, window, cx| { + let tab_bar_visible = window.platform_window.tab_bar_visible(); + SystemWindowTabController::set_visible(cx, tab_bar_visible); + }) + .log_err(); + }) + }); if let Some(app_id) = app_id { platform_window.set_app_id(&app_id); @@ -4279,11 +4349,47 @@ impl Window { } /// Perform titlebar double-click action. - /// This is MacOS specific. + /// This is macOS specific. pub fn titlebar_double_click(&self) { self.platform_window.titlebar_double_click(); } + /// Gets the window's title at the platform level. + /// This is macOS specific. + pub fn window_title(&self) -> String { + self.platform_window.get_title() + } + + /// Returns a list of all tabbed windows and their titles. + /// This is macOS specific. + pub fn tabbed_windows(&self) -> Option> { + self.platform_window.tabbed_windows() + } + + /// Returns the tab bar visibility. + /// This is macOS specific. + pub fn tab_bar_visible(&self) -> bool { + self.platform_window.tab_bar_visible() + } + + /// Merges all open windows into a single tabbed window. + /// This is macOS specific. + pub fn merge_all_windows(&self) { + self.platform_window.merge_all_windows() + } + + /// Moves the tab to a new containing window. + /// This is macOS specific. + pub fn move_tab_to_new_window(&self) { + self.platform_window.move_tab_to_new_window() + } + + /// Shows or hides the window tab overview. + /// This is macOS specific. + pub fn toggle_window_tab_overview(&self) { + self.platform_window.toggle_window_tab_overview() + } + /// Toggles the inspector mode on this window. #[cfg(any(feature = "inspector", debug_assertions))] pub fn toggle_inspector(&mut self, cx: &mut App) { diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 5ad3996e784a27f0552059bf5a2e55addb11f0fd..3d7962fa17d7fa4a4c3b12e88c90adcecd06667d 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -414,7 +414,7 @@ impl RulesLibrary { }); Self { title_bar: if !cfg!(target_os = "macos") { - Some(cx.new(|_| PlatformTitleBar::new("rules-library-title-bar"))) + Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", cx))) } else { None }, diff --git a/crates/title_bar/src/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs index ef6ef93eed9ecd648bd5689eb14cb5cd5481463e..bc1057a4d4bd98a21031cb93d71d9f654d090b2c 100644 --- a/crates/title_bar/src/platform_title_bar.rs +++ b/crates/title_bar/src/platform_title_bar.rs @@ -1,28 +1,35 @@ use gpui::{ - AnyElement, Context, Decorations, Hsla, InteractiveElement, IntoElement, MouseButton, + AnyElement, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px, }; use smallvec::SmallVec; use std::mem; use ui::prelude::*; -use crate::platforms::{platform_linux, platform_mac, platform_windows}; +use crate::{ + platforms::{platform_linux, platform_mac, platform_windows}, + system_window_tabs::SystemWindowTabs, +}; pub struct PlatformTitleBar { id: ElementId, platform_style: PlatformStyle, children: SmallVec<[AnyElement; 2]>, should_move: bool, + system_window_tabs: Entity, } impl PlatformTitleBar { - pub fn new(id: impl Into) -> Self { + pub fn new(id: impl Into, cx: &mut Context) -> Self { let platform_style = PlatformStyle::platform(); + let system_window_tabs = cx.new(|_cx| SystemWindowTabs::new()); + Self { id: id.into(), platform_style, children: SmallVec::new(), should_move: false, + system_window_tabs, } } @@ -66,7 +73,7 @@ impl Render for PlatformTitleBar { let close_action = Box::new(workspace::CloseWindow); let children = mem::take(&mut self.children); - h_flex() + let title_bar = h_flex() .window_control_area(WindowControlArea::Drag) .w_full() .h(height) @@ -162,7 +169,12 @@ impl Render for PlatformTitleBar { title_bar.child(platform_windows::WindowsWindowControls::new(height)) } } - }) + }); + + v_flex() + .w_full() + .child(title_bar) + .child(self.system_window_tabs.clone().into_any_element()) } } diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs new file mode 100644 index 0000000000000000000000000000000000000000..cc50fbc2b99b56c2d8dab95e0c56deb33da2bb4b --- /dev/null +++ b/crates/title_bar/src/system_window_tabs.rs @@ -0,0 +1,477 @@ +use settings::Settings; + +use gpui::{ + AnyWindowHandle, Context, Hsla, InteractiveElement, MouseButton, ParentElement, ScrollHandle, + Styled, SystemWindowTab, SystemWindowTabController, Window, WindowId, actions, canvas, div, +}; + +use theme::ThemeSettings; +use ui::{ + Color, ContextMenu, DynamicSpacing, IconButton, IconButtonShape, IconName, IconSize, Label, + LabelSize, Tab, h_flex, prelude::*, right_click_menu, +}; +use workspace::{ + CloseWindow, ItemSettings, Workspace, + item::{ClosePosition, ShowCloseButton}, +}; + +actions!( + window, + [ + ShowNextWindowTab, + ShowPreviousWindowTab, + MergeAllWindows, + MoveTabToNewWindow + ] +); + +#[derive(Clone)] +pub struct DraggedWindowTab { + pub id: WindowId, + pub ix: usize, + pub handle: AnyWindowHandle, + pub title: String, + pub width: Pixels, + pub is_active: bool, + pub active_background_color: Hsla, + pub inactive_background_color: Hsla, +} + +pub struct SystemWindowTabs { + tab_bar_scroll_handle: ScrollHandle, + measured_tab_width: Pixels, + last_dragged_tab: Option, +} + +impl SystemWindowTabs { + pub fn new() -> Self { + Self { + tab_bar_scroll_handle: ScrollHandle::new(), + measured_tab_width: px(0.), + last_dragged_tab: None, + } + } + + pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, _, _| { + workspace.register_action_renderer(|div, _, window, cx| { + let window_id = window.window_handle().window_id(); + let controller = cx.global::(); + + let tab_groups = controller.tab_groups(); + let tabs = controller.tabs(window_id); + let Some(tabs) = tabs else { + return div; + }; + + div.when(tabs.len() > 1, |div| { + div.on_action(move |_: &ShowNextWindowTab, window, cx| { + SystemWindowTabController::select_next_tab( + cx, + window.window_handle().window_id(), + ); + }) + .on_action(move |_: &ShowPreviousWindowTab, window, cx| { + SystemWindowTabController::select_previous_tab( + cx, + window.window_handle().window_id(), + ); + }) + .on_action(move |_: &MoveTabToNewWindow, window, cx| { + SystemWindowTabController::move_tab_to_new_window( + cx, + window.window_handle().window_id(), + ); + window.move_tab_to_new_window(); + }) + }) + .when(tab_groups.len() > 1, |div| { + div.on_action(move |_: &MergeAllWindows, window, cx| { + SystemWindowTabController::merge_all_windows( + cx, + window.window_handle().window_id(), + ); + window.merge_all_windows(); + }) + }) + }); + }) + .detach(); + } + + fn render_tab( + &self, + ix: usize, + item: SystemWindowTab, + tabs: Vec, + active_background_color: Hsla, + inactive_background_color: Hsla, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement + use<> { + let entity = cx.entity(); + let settings = ItemSettings::get_global(cx); + let close_side = &settings.close_position; + let show_close_button = &settings.show_close_button; + + let rem_size = window.rem_size(); + let width = self.measured_tab_width.max(rem_size * 10); + let is_active = window.window_handle().window_id() == item.id; + let title = item.title.to_string(); + + let label = Label::new(&title) + .size(LabelSize::Small) + .truncate() + .color(if is_active { + Color::Default + } else { + Color::Muted + }); + + let tab = h_flex() + .id(ix) + .group("tab") + .w_full() + .overflow_hidden() + .h(Tab::content_height(cx)) + .relative() + .px(DynamicSpacing::Base16.px(cx)) + .justify_center() + .border_l_1() + .border_color(cx.theme().colors().border) + .cursor_pointer() + .on_drag( + DraggedWindowTab { + id: item.id, + ix, + handle: item.handle, + title: item.title.to_string(), + width, + is_active, + active_background_color, + inactive_background_color, + }, + move |tab, _, _, cx| { + entity.update(cx, |this, _cx| { + this.last_dragged_tab = Some(tab.clone()); + }); + cx.new(|_| tab.clone()) + }, + ) + .drag_over::({ + let tab_ix = ix; + move |element, dragged_tab: &DraggedWindowTab, _, cx| { + let mut styled_tab = element + .bg(cx.theme().colors().drop_target_background) + .border_color(cx.theme().colors().drop_target_border) + .border_0(); + + if tab_ix < dragged_tab.ix { + styled_tab = styled_tab.border_l_2(); + } else if tab_ix > dragged_tab.ix { + styled_tab = styled_tab.border_r_2(); + } + + styled_tab + } + }) + .on_drop({ + let tab_ix = ix; + cx.listener(move |this, dragged_tab: &DraggedWindowTab, _window, cx| { + this.last_dragged_tab = None; + Self::handle_tab_drop(dragged_tab, tab_ix, cx); + }) + }) + .on_click(move |_, _, cx| { + let _ = item.handle.update(cx, |_, window, _| { + window.activate_window(); + }); + }) + .child(label) + .map(|this| match show_close_button { + ShowCloseButton::Hidden => this, + _ => this.child( + div() + .absolute() + .top_2() + .w_4() + .h_4() + .map(|this| match close_side { + ClosePosition::Left => this.left_1(), + ClosePosition::Right => this.right_1(), + }) + .child( + IconButton::new("close", IconName::Close) + .shape(IconButtonShape::Square) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .on_click({ + move |_, window, cx| { + if item.handle.window_id() + == window.window_handle().window_id() + { + window.dispatch_action(Box::new(CloseWindow), cx); + } else { + let _ = item.handle.update(cx, |_, window, cx| { + window.dispatch_action(Box::new(CloseWindow), cx); + }); + } + } + }) + .map(|this| match show_close_button { + ShowCloseButton::Hover => this.visible_on_hover("tab"), + _ => this, + }), + ), + ), + }) + .into_any(); + + let menu = right_click_menu(ix) + .trigger(|_, _, _| tab) + .menu(move |window, cx| { + let focus_handle = cx.focus_handle(); + let tabs = tabs.clone(); + let other_tabs = tabs.clone(); + let move_tabs = tabs.clone(); + let merge_tabs = tabs.clone(); + + ContextMenu::build(window, cx, move |mut menu, _window_, _cx| { + menu = menu.entry("Close Tab", None, move |window, cx| { + Self::handle_right_click_action( + cx, + window, + &tabs, + |tab| tab.id == item.id, + |window, cx| { + window.dispatch_action(Box::new(CloseWindow), cx); + }, + ); + }); + + menu = menu.entry("Close Other Tabs", None, move |window, cx| { + Self::handle_right_click_action( + cx, + window, + &other_tabs, + |tab| tab.id != item.id, + |window, cx| { + window.dispatch_action(Box::new(CloseWindow), cx); + }, + ); + }); + + menu = menu.entry("Move Tab to New Window", None, move |window, cx| { + Self::handle_right_click_action( + cx, + window, + &move_tabs, + |tab| tab.id == item.id, + |window, cx| { + SystemWindowTabController::move_tab_to_new_window( + cx, + window.window_handle().window_id(), + ); + window.move_tab_to_new_window(); + }, + ); + }); + + menu = menu.entry("Show All Tabs", None, move |window, cx| { + Self::handle_right_click_action( + cx, + window, + &merge_tabs, + |tab| tab.id == item.id, + |window, _cx| { + window.toggle_window_tab_overview(); + }, + ); + }); + + menu.context(focus_handle) + }) + }); + + div() + .flex_1() + .min_w(rem_size * 10) + .when(is_active, |this| this.bg(active_background_color)) + .border_t_1() + .border_color(if is_active { + active_background_color + } else { + cx.theme().colors().border + }) + .child(menu) + } + + fn handle_tab_drop(dragged_tab: &DraggedWindowTab, ix: usize, cx: &mut Context) { + SystemWindowTabController::update_tab_position(cx, dragged_tab.id, ix); + } + + fn handle_right_click_action( + cx: &mut App, + window: &mut Window, + tabs: &Vec, + predicate: P, + mut action: F, + ) where + P: Fn(&SystemWindowTab) -> bool, + F: FnMut(&mut Window, &mut App), + { + for tab in tabs { + if predicate(tab) { + if tab.id == window.window_handle().window_id() { + action(window, cx); + } else { + let _ = tab.handle.update(cx, |_view, window, cx| { + action(window, cx); + }); + } + } + } + } +} + +impl Render for SystemWindowTabs { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let active_background_color = cx.theme().colors().title_bar_background; + let inactive_background_color = cx.theme().colors().tab_bar_background; + let entity = cx.entity(); + + let controller = cx.global::(); + let visible = controller.is_visible(); + let current_window_tab = vec![SystemWindowTab::new( + SharedString::from(window.window_title()), + window.window_handle(), + )]; + let tabs = controller + .tabs(window.window_handle().window_id()) + .unwrap_or(¤t_window_tab) + .clone(); + + let tab_items = tabs + .iter() + .enumerate() + .map(|(ix, item)| { + self.render_tab( + ix, + item.clone(), + tabs.clone(), + active_background_color, + inactive_background_color, + window, + cx, + ) + }) + .collect::>(); + + let number_of_tabs = tab_items.len().max(1); + if !window.tab_bar_visible() && !visible { + return h_flex().into_any_element(); + } + + h_flex() + .w_full() + .h(Tab::container_height(cx)) + .bg(inactive_background_color) + .on_mouse_up_out( + MouseButton::Left, + cx.listener(|this, _event, window, cx| { + if let Some(tab) = this.last_dragged_tab.take() { + SystemWindowTabController::move_tab_to_new_window(cx, tab.id); + if tab.id == window.window_handle().window_id() { + window.move_tab_to_new_window(); + } else { + let _ = tab.handle.update(cx, |_, window, _cx| { + window.move_tab_to_new_window(); + }); + } + } + }), + ) + .child( + h_flex() + .id("window tabs") + .w_full() + .h(Tab::container_height(cx)) + .bg(inactive_background_color) + .overflow_x_scroll() + .track_scroll(&self.tab_bar_scroll_handle) + .children(tab_items) + .child( + canvas( + |_, _, _| (), + move |bounds, _, _, cx| { + let entity = entity.clone(); + entity.update(cx, |this, cx| { + let width = bounds.size.width / number_of_tabs as f32; + if width != this.measured_tab_width { + this.measured_tab_width = width; + cx.notify(); + } + }); + }, + ) + .absolute() + .size_full(), + ), + ) + .child( + h_flex() + .h_full() + .px(DynamicSpacing::Base06.rems(cx)) + .border_t_1() + .border_l_1() + .border_color(cx.theme().colors().border) + .child( + IconButton::new("plus", IconName::Plus) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .on_click(|_event, window, cx| { + window.dispatch_action( + Box::new(zed_actions::OpenRecent { + create_new_window: true, + }), + cx, + ); + }), + ), + ) + .into_any_element() + } +} + +impl Render for DraggedWindowTab { + fn render( + &mut self, + _window: &mut gpui::Window, + cx: &mut gpui::Context, + ) -> impl gpui::IntoElement { + let ui_font = ThemeSettings::get_global(cx).ui_font.clone(); + let label = Label::new(self.title.clone()) + .size(LabelSize::Small) + .truncate() + .color(if self.is_active { + Color::Default + } else { + Color::Muted + }); + + h_flex() + .h(Tab::container_height(cx)) + .w(self.width) + .px(DynamicSpacing::Base16.px(cx)) + .justify_center() + .bg(if self.is_active { + self.active_background_color + } else { + self.inactive_background_color + }) + .border_1() + .border_color(cx.theme().colors().border) + .font(ui_font) + .child(label) + } +} diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index b08f139b25e53ef7e4761e55ad2686b3425969e9..ac5e9201b3be083fef43e58c2e717cb59a0ba185 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -3,6 +3,7 @@ mod collab; mod onboarding_banner; pub mod platform_title_bar; mod platforms; +mod system_window_tabs; mod title_bar_settings; #[cfg(feature = "stories")] @@ -11,6 +12,7 @@ mod stories; use crate::{ application_menu::{ApplicationMenu, show_menus}, platform_title_bar::PlatformTitleBar, + system_window_tabs::SystemWindowTabs, }; #[cfg(not(target_os = "macos"))] @@ -65,6 +67,7 @@ actions!( pub fn init(cx: &mut App) { TitleBarSettings::register(cx); + SystemWindowTabs::init(cx); cx.observe_new(|workspace: &mut Workspace, window, cx| { let Some(window) = window else { @@ -284,7 +287,7 @@ impl TitleBar { ) }); - let platform_titlebar = cx.new(|_| PlatformTitleBar::new(id)); + let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx)); Self { platform_titlebar, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 25e2cb1cfe934a88ec4cc3811bf3216e0765c0af..1fee2793f0c50efee9f2fd8040d3bcf8df2af08a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -42,9 +42,9 @@ use gpui::{ Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, - PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task, - Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId, WindowOptions, actions, canvas, - point, relative, size, transparent_black, + PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, + SystemWindowTabController, Task, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId, + WindowOptions, actions, canvas, point, relative, size, transparent_black, }; pub use history_manager::*; pub use item::{ @@ -4375,6 +4375,11 @@ impl Workspace { return; } window.set_window_title(&title); + SystemWindowTabController::update_tab_title( + cx, + window.window_handle().window_id(), + SharedString::from(&title), + ); self.last_window_title = Some(title); } @@ -5797,17 +5802,22 @@ impl Workspace { return; }; let windows = cx.windows(); - let Some(next_window) = windows - .iter() - .cycle() - .skip_while(|window| window.window_id() != current_window_id) - .nth(1) - else { - return; - }; - next_window - .update(cx, |_, window, _| window.activate_window()) - .ok(); + let next_window = + SystemWindowTabController::get_next_tab_group_window(cx, current_window_id).or_else( + || { + windows + .iter() + .cycle() + .skip_while(|window| window.window_id() != current_window_id) + .nth(1) + }, + ); + + if let Some(window) = next_window { + window + .update(cx, |_, window, _| window.activate_window()) + .ok(); + } } pub fn activate_previous_window(&mut self, cx: &mut Context) { @@ -5815,18 +5825,23 @@ impl Workspace { return; }; let windows = cx.windows(); - let Some(prev_window) = windows - .iter() - .rev() - .cycle() - .skip_while(|window| window.window_id() != current_window_id) - .nth(1) - else { - return; - }; - prev_window - .update(cx, |_, window, _| window.activate_window()) - .ok(); + let prev_window = + SystemWindowTabController::get_prev_tab_group_window(cx, current_window_id).or_else( + || { + windows + .iter() + .rev() + .cycle() + .skip_while(|window| window.window_id() != current_window_id) + .nth(1) + }, + ); + + if let Some(window) = prev_window { + window + .update(cx, |_, window, _| window.activate_window()) + .ok(); + } } pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 3b6bc1ea970d0e7502e36f75630c9e8dd05906b5..0d7fb9bb9c1ae6f8ff4a6644132c4a347da4117d 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -29,6 +29,7 @@ pub struct WorkspaceSettings { pub on_last_window_closed: OnLastWindowClosed, pub resize_all_panels_in_dock: Vec, pub close_on_file_delete: bool, + pub use_system_window_tabs: bool, pub zoomed_padding: bool, } @@ -203,6 +204,10 @@ pub struct WorkspaceSettingsContent { /// /// Default: false pub close_on_file_delete: Option, + /// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). + /// + /// Default: false + pub use_system_window_tabs: Option, /// Whether to show padding for zoomed panels. /// When enabled, zoomed bottom panels will have some top padding, /// while zoomed left/right panels will have padding to the right/left (respectively). @@ -357,6 +362,8 @@ impl Settings for WorkspaceSettings { current.max_tabs = Some(n) } + vscode.bool_setting("window.nativeTabs", &mut current.use_system_window_tabs); + // some combination of "window.restoreWindows" and "workbench.startupEditor" might // map to our "restore_on_startup" diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e99c8b564b2cd7915fc352f9caee97b77eccaaf5..5e7934c3094755b39535ef054f077dbc9fb180af 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -2,7 +2,7 @@ mod reliability; mod zed; use agent_ui::AgentPanel; -use anyhow::{Context as _, Result}; +use anyhow::{Context as _, Error, Result}; use clap::{Parser, command}; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::{Client, ProxySettings, UserStore, parse_zed_link}; @@ -947,9 +947,13 @@ async fn installation_id() -> Result { async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp) -> Result<()> { if let Some(locations) = restorable_workspace_locations(cx, &app_state).await { + let use_system_window_tabs = cx + .update(|cx| WorkspaceSettings::get(None, cx).use_system_window_tabs) + .unwrap_or(false); + let mut results: Vec> = Vec::new(); let mut tasks = Vec::new(); - for (location, paths) in locations { + for (index, (location, paths)) in locations.into_iter().enumerate() { match location { SerializedWorkspaceLocation::Local => { let app_state = app_state.clone(); @@ -964,7 +968,14 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp })?; open_task.await.map(|_| ()) }); - tasks.push(task); + + // If we're using system window tabs and this is the first workspace, + // wait for it to finish so that the other windows can be added as tabs. + if use_system_window_tabs && index == 0 { + results.push(task.await); + } else { + tasks.push(task); + } } SerializedWorkspaceLocation::Ssh(ssh) => { let app_state = app_state.clone(); @@ -998,7 +1009,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp } // Wait for all workspaces to open concurrently - let results = future::join_all(tasks).await; + results.extend(future::join_all(tasks).await); // Show notifications for any errors that occurred let mut error_count = 0; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index b0146cfd2bec5db501a638e125f3e95711db4842..a12249d6a4841691fb302f8cb26b28951f829c3d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -282,6 +282,8 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO _ => gpui::WindowDecorations::Client, }; + let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs; + WindowOptions { titlebar: Some(TitlebarOptions { title: None, @@ -301,6 +303,11 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO width: px(360.0), height: px(240.0), }), + tabbing_identifier: if use_system_window_tabs { + Some(String::from("zed")) + } else { + None + }, ..Default::default() } } @@ -4507,6 +4514,7 @@ mod tests { "zed", "zed_predict_onboarding", "zeta", + "window", ]; assert_eq!( all_namespaces, diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 9634ca0f6cd93db963111fa464890a3c26478cb3..fb9306acc5a4b21b709904618a6438e58c30039f 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1421,6 +1421,16 @@ or Each option controls displaying of a particular toolbar element. If all elements are hidden, the editor toolbar is not displayed. +## Use System Tabs + +- Description: Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). +- Setting: `use_system_window_tabs` +- Default: `false` + +**Options** + +This setting enables integration with macOS’s native window tabbing feature. When set to `true`, Zed windows can be grouped together as tabs in a single macOS window, following the system-wide tabbing preferences set by the user (such as "Always", "In Full Screen", or "Never"). This setting is only available on macOS. + ## Enable Language Server - Description: Whether or not to use language servers to provide code intelligence. From 38e5c8fb66ac19f5836e3c0590cc9dcf3e77e87e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Thu, 28 Aug 2025 15:16:13 +0800 Subject: [PATCH 400/744] keymap_editor: Fix incorrect keystroke being reported (#36998) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes two bugs and also changes one behavior in the **Keymap Editor**. As shown in the video, when I press `ctrl-shift-2` in the Keymap Editor, the first keystroke is displayed as `ctrl-shift-@`, which is incorrect. On macOS and Linux, it should be `ctrl-@`. https://github.com/user-attachments/assets/69cfcfa0-b422-45d6-8e69-80f8608180fd Also, after pressing `ctrl-shift-2` and then releasing `2` and `ctrl`, a `shift` keystroke was incorrectly added. https://github.com/user-attachments/assets/892124fd-847d-4fde-9b20-a27ba49ac934 Now, when you enter a sequence like `+ctrl+alt-alt+f` in the Keymap Editor, it will output `ctrl-f` instead of `ctrl-alt-f`, matching VS Code’s behavior. Release Notes: - Fixed incorrect keystroke reporting in the Keymap Editor. --- crates/gpui/src/window.rs | 7 + .../src/ui_components/keystroke_input.rs | 149 +++++++++++++++--- 2 files changed, 137 insertions(+), 19 deletions(-) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index e6ea3fef07b221b96dbfe5b19f794106eb11b4dd..4504f512551b678b9304a4c180f54b15c34af956 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4578,6 +4578,13 @@ impl Window { } None } + + /// For testing: set the current modifier keys state. + /// This does not generate any events. + #[cfg(any(test, feature = "test-support"))] + pub fn set_modifiers(&mut self, modifiers: Modifiers) { + self.modifiers = modifiers; + } } // #[derive(Clone, Copy, Eq, PartialEq, Hash)] diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index ca50d5c03dcde33f4fd1b83cfeb56eb1b341d818..5d27598d9150d1be9f58aeb9a81c1b68744ca4c0 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -303,17 +303,12 @@ impl KeystrokeInput { return; } - let mut keystroke = + let keystroke = KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref()); if let Some(last) = self.keystrokes.last() && last.display_key.is_empty() && (!self.search || self.previous_modifiers.modified()) { - let display_key = keystroke.display_key.clone(); - let inner_key = keystroke.inner.key.clone(); - keystroke = last.clone(); - keystroke.display_key = display_key; - keystroke.inner.key = inner_key; self.keystrokes.pop(); } @@ -329,18 +324,19 @@ impl KeystrokeInput { return; } - self.keystrokes.push(keystroke.clone()); + self.keystrokes.push(keystroke); self.keystrokes_changed(cx); + // The reason we use the real modifiers from the window instead of the keystroke's modifiers + // is that for keystrokes like `ctrl-$` the modifiers reported by keystroke is `ctrl` which + // is wrong, it should be `ctrl-shift`. The window's modifiers are always correct. + let real_modifiers = window.modifiers(); if self.search { - self.previous_modifiers = keystroke.display_modifiers; + self.previous_modifiers = real_modifiers; return; } - if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX - && keystroke.display_modifiers.modified() - { - self.keystrokes - .push(Self::dummy(keystroke.display_modifiers)); + if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && real_modifiers.modified() { + self.keystrokes.push(Self::dummy(real_modifiers)); } } @@ -718,8 +714,11 @@ mod tests { // Combine current modifiers with keystroke modifiers keystroke.modifiers |= self.current_modifiers; + let real_modifiers = keystroke.modifiers; + keystroke = to_gpui_keystroke(keystroke); self.update_input(|input, window, cx| { + window.set_modifiers(real_modifiers); input.handle_keystroke(&keystroke, window, cx); }); @@ -747,6 +746,7 @@ mod tests { }; self.update_input(|input, window, cx| { + window.set_modifiers(new_modifiers); input.on_modifiers_changed(&event, window, cx); }); @@ -954,6 +954,100 @@ mod tests { } } + /// For GPUI, when you press `ctrl-shift-2`, it produces `ctrl-@` without the shift modifier. + fn to_gpui_keystroke(mut keystroke: Keystroke) -> Keystroke { + if keystroke.modifiers.shift { + match keystroke.key.as_str() { + "`" => { + keystroke.key = "~".into(); + keystroke.modifiers.shift = false; + } + "1" => { + keystroke.key = "!".into(); + keystroke.modifiers.shift = false; + } + "2" => { + keystroke.key = "@".into(); + keystroke.modifiers.shift = false; + } + "3" => { + keystroke.key = "#".into(); + keystroke.modifiers.shift = false; + } + "4" => { + keystroke.key = "$".into(); + keystroke.modifiers.shift = false; + } + "5" => { + keystroke.key = "%".into(); + keystroke.modifiers.shift = false; + } + "6" => { + keystroke.key = "^".into(); + keystroke.modifiers.shift = false; + } + "7" => { + keystroke.key = "&".into(); + keystroke.modifiers.shift = false; + } + "8" => { + keystroke.key = "*".into(); + keystroke.modifiers.shift = false; + } + "9" => { + keystroke.key = "(".into(); + keystroke.modifiers.shift = false; + } + "0" => { + keystroke.key = ")".into(); + keystroke.modifiers.shift = false; + } + "-" => { + keystroke.key = "_".into(); + keystroke.modifiers.shift = false; + } + "=" => { + keystroke.key = "+".into(); + keystroke.modifiers.shift = false; + } + "[" => { + keystroke.key = "{".into(); + keystroke.modifiers.shift = false; + } + "]" => { + keystroke.key = "}".into(); + keystroke.modifiers.shift = false; + } + "\\" => { + keystroke.key = "|".into(); + keystroke.modifiers.shift = false; + } + ";" => { + keystroke.key = ":".into(); + keystroke.modifiers.shift = false; + } + "'" => { + keystroke.key = "\"".into(); + keystroke.modifiers.shift = false; + } + "," => { + keystroke.key = "<".into(); + keystroke.modifiers.shift = false; + } + "." => { + keystroke.key = ">".into(); + keystroke.modifiers.shift = false; + } + "/" => { + keystroke.key = "?".into(); + keystroke.modifiers.shift = false; + } + _ => {} + } + } + keystroke + } + struct KeystrokeUpdateTracker { initial_keystrokes: Vec, _subscription: Subscription, @@ -1057,7 +1151,15 @@ mod tests { .send_events(&["+cmd", "shift-f", "-cmd"]) // In search mode, when completing a modifier-only keystroke with a key, // only the original modifiers are preserved, not the keystroke's modifiers - .expect_keystrokes(&["cmd-f"]); + // + // Update: + // This behavior was changed to preserve all modifiers in search mode, this is now reflected in the expected keystrokes. + // Specifically, considering the sequence: `+cmd +shift -shift 2`, we expect it to produce the same result as `+cmd +shift 2` + // which is `cmd-@`. But in the case of `+cmd +shift -shift 2`, the keystroke we receive is `cmd-2`, which means that + // we need to dynamically map the key from `2` to `@` when the shift modifier is not present, which is not possible. + // Therefore, we now preserve all modifiers in search mode to ensure consistent behavior. + // And also, VSCode seems to preserve all modifiers in search mode as well. + .expect_keystrokes(&["cmd-shift-f"]); } #[gpui::test] @@ -1234,7 +1336,7 @@ mod tests { .await .with_search_mode(true) .send_events(&["+ctrl", "+shift", "-shift", "a", "-ctrl"]) - .expect_keystrokes(&["ctrl-shift-a"]); + .expect_keystrokes(&["ctrl-a"]); } #[gpui::test] @@ -1342,7 +1444,7 @@ mod tests { .await .with_search_mode(true) .send_events(&["+ctrl+alt", "-ctrl", "j"]) - .expect_keystrokes(&["ctrl-alt-j"]); + .expect_keystrokes(&["alt-j"]); } #[gpui::test] @@ -1364,11 +1466,11 @@ mod tests { .send_events(&["+ctrl+alt", "-ctrl", "+shift"]) .expect_keystrokes(&["ctrl-shift-alt-"]) .send_keystroke("j") - .expect_keystrokes(&["ctrl-shift-alt-j"]) + .expect_keystrokes(&["shift-alt-j"]) .send_keystroke("i") - .expect_keystrokes(&["ctrl-shift-alt-j", "shift-alt-i"]) + .expect_keystrokes(&["shift-alt-j", "shift-alt-i"]) .send_events(&["-shift-alt", "+cmd"]) - .expect_keystrokes(&["ctrl-shift-alt-j", "shift-alt-i", "cmd-"]); + .expect_keystrokes(&["shift-alt-j", "shift-alt-i", "cmd-"]); } #[gpui::test] @@ -1401,4 +1503,13 @@ mod tests { .send_events(&["+ctrl", "-ctrl", "+alt", "-alt", "+shift", "-shift"]) .expect_empty(); } + + #[gpui::test] + async fn test_not_search_shifted_keys(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(false) + .send_events(&["+ctrl", "+shift", "4", "-all"]) + .expect_keystrokes(&["ctrl-$"]); + } } From 73b38c83068bdafd59d4f3f77adde8fa9e3b1281 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 28 Aug 2025 03:53:32 -0400 Subject: [PATCH 401/744] debugger: Add ability to only show stack frame entries from visible work trees (#37061) This PR adds a toggleable filter to the stack frame list that filters out entries that don't exist within a user's project (visible work trees). This works by keeping a vector of entry indices that exist within a user's project and updates the list state based on these entries when filtering the list. I went with this approach so the stack frame list wouldn't have to rebuild itself whenever the filter is toggled and it could persist its state across toggles (uncollapsing a collapse list). It was also easier to keep track of selected entries on toggle using the vector as well. ### Preview https://github.com/user-attachments/assets/d86c7485-c885-4bbb-bebb-2f6385674925 Release Notes: - debugger: Add option to only show stack frames from user's project in stack frame list --- assets/icons/list_filter.svg | 1 + crates/dap_adapters/src/python.rs | 1 + crates/debugger_ui/src/debugger_ui.rs | 17 ++ crates/debugger_ui/src/persistence.rs | 9 +- crates/debugger_ui/src/session/running.rs | 23 ++ .../src/session/running/stack_frame_list.rs | 199 +++++++++++- .../debugger_ui/src/tests/stack_frame_list.rs | 285 ++++++++++++++++++ crates/icons/src/icons.rs | 1 + 8 files changed, 522 insertions(+), 14 deletions(-) create mode 100644 assets/icons/list_filter.svg diff --git a/assets/icons/list_filter.svg b/assets/icons/list_filter.svg new file mode 100644 index 0000000000000000000000000000000000000000..82f41f5f6832a8cb35e2703e0f8ce36d148454dd --- /dev/null +++ b/assets/icons/list_filter.svg @@ -0,0 +1 @@ + diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 614cd0e05d1821539c74eb4e78321fb1e0c29445..6781e5cbd62d1abc9abfa58223b0771f26cc0c88 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -234,6 +234,7 @@ impl PythonDebugAdapter { .await .map_err(|e| format!("{e:#?}"))? .success(); + if !did_succeed { return Err("Failed to create base virtual environment".into()); } diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 581cc16ff4c9a41e24036bcd3ff000ad47d3a076..689e3cd878b574d31963231df9bcff317ea6d64c 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -85,6 +85,10 @@ actions!( Rerun, /// Toggles expansion of the selected item in the debugger UI. ToggleExpandItem, + /// Toggle the user frame filter in the stack frame list + /// When toggled on, only frames from the user's code are shown + /// When toggled off, all frames are shown + ToggleUserFrames, ] ); @@ -272,12 +276,25 @@ pub fn init(cx: &mut App) { } }) .on_action({ + let active_item = active_item.clone(); move |_: &ToggleIgnoreBreakpoints, _, cx| { active_item .update(cx, |item, cx| item.toggle_ignore_breakpoints(cx)) .ok(); } }) + .on_action(move |_: &ToggleUserFrames, _, cx| { + if let Some((thread_status, stack_frame_list)) = active_item + .read_with(cx, |item, cx| { + (item.thread_status(cx), item.stack_frame_list().clone()) + }) + .ok() + { + stack_frame_list.update(cx, |stack_frame_list, cx| { + stack_frame_list.toggle_frame_filter(thread_status, cx); + }) + } + }) }); }) .detach(); diff --git a/crates/debugger_ui/src/persistence.rs b/crates/debugger_ui/src/persistence.rs index cff2ba83355208d702cf7936c46ea5b167c7c649..ab68fea1154182fe266bb150d762f8be0995d733 100644 --- a/crates/debugger_ui/src/persistence.rs +++ b/crates/debugger_ui/src/persistence.rs @@ -270,12 +270,9 @@ pub(crate) fn deserialize_pane_layout( .children .iter() .map(|child| match child { - DebuggerPaneItem::Frames => Box::new(SubView::new( - stack_frame_list.focus_handle(cx), - stack_frame_list.clone().into(), - DebuggerPaneItem::Frames, - cx, - )), + DebuggerPaneItem::Frames => { + Box::new(SubView::stack_frame_list(stack_frame_list.clone(), cx)) + } DebuggerPaneItem::Variables => Box::new(SubView::new( variable_list.focus_handle(cx), variable_list.clone().into(), diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index a0e7c9a10173c31edc193fd0309913b60c6f8a95..25146dc7e42bd2359ccd4a16644ba9e41565a0a8 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -158,6 +158,29 @@ impl SubView { }) } + pub(crate) fn stack_frame_list( + stack_frame_list: Entity, + cx: &mut App, + ) -> Entity { + let weak_list = stack_frame_list.downgrade(); + let this = Self::new( + stack_frame_list.focus_handle(cx), + stack_frame_list.into(), + DebuggerPaneItem::Frames, + cx, + ); + + this.update(cx, |this, _| { + this.with_actions(Box::new(move |_, cx| { + weak_list + .update(cx, |this, _| this.render_control_strip()) + .unwrap_or_else(|_| div().into_any_element()) + })); + }); + + this + } + pub(crate) fn console(console: Entity, cx: &mut App) -> Entity { let weak_console = console.downgrade(); let this = Self::new( diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index a4ea4ab654929f00b05e9146bfd662aad2f8bd6d..f80173c365a047da39733c94964c473bef579e1c 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -4,16 +4,17 @@ use std::time::Duration; use anyhow::{Context as _, Result, anyhow}; use dap::StackFrameId; +use db::kvp::KEY_VALUE_STORE; use gpui::{ - AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, MouseButton, - Stateful, Subscription, Task, WeakEntity, list, + Action, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, + MouseButton, Stateful, Subscription, Task, WeakEntity, list, }; use util::debug_panic; -use crate::StackTraceView; +use crate::{StackTraceView, ToggleUserFrames}; use language::PointUtf16; use project::debugger::breakpoint_store::ActiveStackFrame; -use project::debugger::session::{Session, SessionEvent, StackFrame}; +use project::debugger::session::{Session, SessionEvent, StackFrame, ThreadStatus}; use project::{ProjectItem, ProjectPath}; use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*}; use workspace::{ItemHandle, Workspace}; @@ -26,6 +27,34 @@ pub enum StackFrameListEvent { BuiltEntries, } +/// Represents the filter applied to the stack frame list +#[derive(PartialEq, Eq, Copy, Clone)] +enum StackFrameFilter { + /// Show all frames + All, + /// Show only frames from the user's code + OnlyUserFrames, +} + +impl StackFrameFilter { + fn from_str_or_default(s: impl AsRef) -> Self { + match s.as_ref() { + "user" => StackFrameFilter::OnlyUserFrames, + "all" => StackFrameFilter::All, + _ => StackFrameFilter::All, + } + } +} + +impl From for String { + fn from(filter: StackFrameFilter) -> Self { + match filter { + StackFrameFilter::All => "all".to_string(), + StackFrameFilter::OnlyUserFrames => "user".to_string(), + } + } +} + pub struct StackFrameList { focus_handle: FocusHandle, _subscription: Subscription, @@ -37,6 +66,8 @@ pub struct StackFrameList { opened_stack_frame_id: Option, scrollbar_state: ScrollbarState, list_state: ListState, + list_filter: StackFrameFilter, + filter_entries_indices: Vec, error: Option, _refresh_task: Task<()>, } @@ -73,6 +104,16 @@ impl StackFrameList { let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.)); let scrollbar_state = ScrollbarState::new(list_state.clone()); + let list_filter = KEY_VALUE_STORE + .read_kvp(&format!( + "stack-frame-list-filter-{}", + session.read(cx).adapter().0 + )) + .ok() + .flatten() + .map(StackFrameFilter::from_str_or_default) + .unwrap_or(StackFrameFilter::All); + let mut this = Self { session, workspace, @@ -80,9 +121,11 @@ impl StackFrameList { state, _subscription, entries: Default::default(), + filter_entries_indices: Vec::default(), error: None, selected_ix: None, opened_stack_frame_id: None, + list_filter, list_state, scrollbar_state, _refresh_task: Task::ready(()), @@ -103,7 +146,15 @@ impl StackFrameList { ) -> Vec { self.entries .iter() - .flat_map(|frame| match frame { + .enumerate() + .filter(|(ix, _)| { + self.list_filter == StackFrameFilter::All + || self + .filter_entries_indices + .binary_search_by_key(&ix, |ix| ix) + .is_ok() + }) + .flat_map(|(_, frame)| match frame { StackFrameEntry::Normal(frame) => vec![frame.clone()], StackFrameEntry::Label(frame) if show_labels => vec![frame.clone()], StackFrameEntry::Collapsed(frames) if show_collapsed => frames.clone(), @@ -126,7 +177,15 @@ impl StackFrameList { self.stack_frames(cx) .unwrap_or_default() .into_iter() - .map(|stack_frame| stack_frame.dap) + .enumerate() + .filter(|(ix, _)| { + self.list_filter == StackFrameFilter::All + || self + .filter_entries_indices + .binary_search_by_key(&ix, |ix| ix) + .is_ok() + }) + .map(|(_, stack_frame)| stack_frame.dap) .collect() } @@ -192,7 +251,32 @@ impl StackFrameList { return; } }; - for stack_frame in &stack_frames { + + let worktree_prefixes: Vec<_> = self + .workspace + .read_with(cx, |workspace, cx| { + workspace + .visible_worktrees(cx) + .map(|tree| tree.read(cx).abs_path()) + .collect() + }) + .unwrap_or_default(); + + let mut filter_entries_indices = Vec::default(); + for (ix, stack_frame) in stack_frames.iter().enumerate() { + let frame_in_visible_worktree = stack_frame.dap.source.as_ref().is_some_and(|source| { + source.path.as_ref().is_some_and(|path| { + worktree_prefixes + .iter() + .filter_map(|tree| tree.to_str()) + .any(|tree| path.starts_with(tree)) + }) + }); + + if frame_in_visible_worktree { + filter_entries_indices.push(ix); + } + match stack_frame.dap.presentation_hint { Some(dap::StackFramePresentationHint::Deemphasize) | Some(dap::StackFramePresentationHint::Subtle) => { @@ -225,8 +309,10 @@ impl StackFrameList { let collapsed_entries = std::mem::take(&mut collapsed_entries); if !collapsed_entries.is_empty() { entries.push(StackFrameEntry::Collapsed(collapsed_entries)); + self.filter_entries_indices.push(entries.len() - 1); } self.entries = entries; + self.filter_entries_indices = filter_entries_indices; if let Some(ix) = first_stack_frame_with_path .or(first_stack_frame) @@ -242,7 +328,14 @@ impl StackFrameList { self.selected_ix = ix; } - self.list_state.reset(self.entries.len()); + match self.list_filter { + StackFrameFilter::All => { + self.list_state.reset(self.entries.len()); + } + StackFrameFilter::OnlyUserFrames => { + self.list_state.reset(self.filter_entries_indices.len()); + } + } cx.emit(StackFrameListEvent::BuiltEntries); cx.notify(); } @@ -572,6 +665,11 @@ impl StackFrameList { } fn render_entry(&self, ix: usize, cx: &mut Context) -> AnyElement { + let ix = match self.list_filter { + StackFrameFilter::All => ix, + StackFrameFilter::OnlyUserFrames => self.filter_entries_indices[ix], + }; + match &self.entries[ix] { StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx), StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx), @@ -702,6 +800,67 @@ impl StackFrameList { self.activate_selected_entry(window, cx); } + pub(crate) fn toggle_frame_filter( + &mut self, + thread_status: Option, + cx: &mut Context, + ) { + self.list_filter = match self.list_filter { + StackFrameFilter::All => StackFrameFilter::OnlyUserFrames, + StackFrameFilter::OnlyUserFrames => StackFrameFilter::All, + }; + + if let Some(database_id) = self + .workspace + .read_with(cx, |workspace, _| workspace.database_id()) + .ok() + .flatten() + { + let database_id: i64 = database_id.into(); + let save_task = KEY_VALUE_STORE.write_kvp( + format!( + "stack-frame-list-filter-{}-{}", + self.session.read(cx).adapter().0, + database_id, + ), + self.list_filter.into(), + ); + cx.background_spawn(save_task).detach(); + } + + if let Some(ThreadStatus::Stopped) = thread_status { + match self.list_filter { + StackFrameFilter::All => { + self.list_state.reset(self.entries.len()); + } + StackFrameFilter::OnlyUserFrames => { + self.list_state.reset(self.filter_entries_indices.len()); + if !self + .selected_ix + .map(|ix| self.filter_entries_indices.contains(&ix)) + .unwrap_or_default() + { + self.selected_ix = None; + } + } + } + + if let Some(ix) = self.selected_ix { + let scroll_to = match self.list_filter { + StackFrameFilter::All => ix, + StackFrameFilter::OnlyUserFrames => self + .filter_entries_indices + .binary_search_by_key(&ix, |ix| *ix) + .expect("This index will always exist"), + }; + self.list_state.scroll_to_reveal_item(scroll_to); + } + + cx.emit(StackFrameListEvent::BuiltEntries); + cx.notify(); + } + } + fn render_list(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { div().p_1().size_full().child( list( @@ -711,6 +870,30 @@ impl StackFrameList { .size_full(), ) } + + pub(crate) fn render_control_strip(&self) -> AnyElement { + let tooltip_title = match self.list_filter { + StackFrameFilter::All => "Show stack frames from your project", + StackFrameFilter::OnlyUserFrames => "Show all stack frames", + }; + + h_flex() + .child( + IconButton::new( + "filter-by-visible-worktree-stack-frame-list", + IconName::ListFilter, + ) + .tooltip(move |window, cx| { + Tooltip::for_action(tooltip_title, &ToggleUserFrames, window, cx) + }) + .toggle_state(self.list_filter == StackFrameFilter::OnlyUserFrames) + .icon_size(IconSize::Small) + .on_click(|_, window, cx| { + window.dispatch_action(ToggleUserFrames.boxed_clone(), cx) + }), + ) + .into_any_element() + } } impl Render for StackFrameList { diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 95a6903c14a1cbd5f750d6e11437cb0bf92887c7..023056224e177bb053f5188ced59c059c9c8ad32 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -752,3 +752,288 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo }); }); } + +#[gpui::test] +async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let test_file_content = r#" + function main() { + doSomething(); + } + + function doSomething() { + console.log('doing something'); + } + "# + .unindent(); + + fs.insert_tree( + path!("/project"), + json!({ + "src": { + "test.js": test_file_content, + } + }), + ) + .await; + + let project = Project::test(fs, [path!("/project").as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let session = start_debug_session(&workspace, cx, |_| {}).unwrap(); + let client = session.update(cx, |session, _| session.adapter_client().unwrap()); + + client.on_request::(move |_, _| { + Ok(dap::ThreadsResponse { + threads: vec![dap::Thread { + id: 1, + name: "Thread 1".into(), + }], + }) + }); + + client.on_request::(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] })); + + let stack_frames = vec![ + StackFrame { + id: 1, + name: "main".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some(path!("/project/src/test.js").into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 2, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + StackFrame { + id: 2, + name: "node:internal/modules/cjs/loader".into(), + source: Some(dap::Source { + name: Some("loader.js".into()), + path: Some(path!("/usr/lib/node/internal/modules/cjs/loader.js").into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 100, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize), + }, + StackFrame { + id: 3, + name: "node:internal/modules/run_main".into(), + source: Some(dap::Source { + name: Some("run_main.js".into()), + path: Some(path!("/usr/lib/node/internal/modules/run_main.js").into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 50, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize), + }, + StackFrame { + id: 4, + name: "doSomething".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some(path!("/project/src/test.js").into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + ]; + + // Store a copy for assertions + let stack_frames_for_assertions = stack_frames.clone(); + + client.on_request::({ + let stack_frames = Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } + }); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + // trigger threads to load + active_debug_session_panel(workspace, cx).update(cx, |session, cx| { + session.running_state().update(cx, |running_state, cx| { + running_state + .session() + .update(cx, |session, cx| session.threads(cx)); + }); + }); + + cx.run_until_parked(); + + // select first thread + active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| { + session.running_state().update(cx, |running_state, cx| { + running_state.select_current_thread( + &running_state + .session() + .update(cx, |session, cx| session.threads(cx)), + window, + cx, + ); + }); + }); + + cx.run_until_parked(); + + // trigger stack frames to load + active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| { + let stack_frame_list = debug_panel_item + .running_state() + .update(cx, |state, _| state.stack_frame_list().clone()); + + stack_frame_list.update(cx, |stack_frame_list, cx| { + stack_frame_list.dap_stack_frames(cx); + }); + }); + + cx.run_until_parked(); + + active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| { + let stack_frame_list = debug_panel_item + .running_state() + .update(cx, |state, _| state.stack_frame_list().clone()); + + stack_frame_list.update(cx, |stack_frame_list, cx| { + stack_frame_list.build_entries(true, window, cx); + + // Verify we have the expected collapsed structure + assert_eq!( + stack_frame_list.entries(), + &vec![ + StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()), + StackFrameEntry::Collapsed(vec![ + stack_frames_for_assertions[1].clone(), + stack_frames_for_assertions[2].clone() + ]), + StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()), + ] + ); + + // Test 1: Verify filtering works + let all_frames = stack_frame_list.flatten_entries(true, false); + assert_eq!(all_frames.len(), 4, "Should see all 4 frames initially"); + + // Toggle to user frames only + stack_frame_list + .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx); + + let user_frames = stack_frame_list.dap_stack_frames(cx); + assert_eq!(user_frames.len(), 2, "Should only see 2 user frames"); + assert_eq!(user_frames[0].name, "main"); + assert_eq!(user_frames[1].name, "doSomething"); + + // Test 2: Verify filtering toggles correctly + // Check we can toggle back and see all frames again + + // Toggle back to all frames + stack_frame_list + .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx); + + let all_frames_again = stack_frame_list.flatten_entries(true, false); + assert_eq!( + all_frames_again.len(), + 4, + "Should see all 4 frames after toggling back" + ); + + // Test 3: Verify collapsed entries stay expanded + stack_frame_list.expand_collapsed_entry(1, cx); + assert_eq!( + stack_frame_list.entries(), + &vec![ + StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()), + StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()), + StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()), + StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()), + ] + ); + + // Toggle filter twice + stack_frame_list + .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx); + stack_frame_list + .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx); + + // Verify entries remain expanded + assert_eq!( + stack_frame_list.entries(), + &vec![ + StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()), + StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()), + StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()), + StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()), + ], + "Expanded entries should remain expanded after toggling filter" + ); + }); + }); +} diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index f7363395ae76fd99f0642d7d0f4117371e575b5d..f3609f7ea8706f33eb07eaaf456731e14c85555a 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -146,6 +146,7 @@ pub enum IconName { Library, LineHeight, ListCollapse, + ListFilter, ListTodo, ListTree, ListX, From ff03dda90ae6c5d3c9aedc8768431456cfe9de7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Thu, 28 Aug 2025 16:40:43 +0800 Subject: [PATCH 402/744] Refactor `KeybindingKeystroke` (#37065) This pull request refactors the `KeybindingKeystroke` struct and related code to improve platform abstraction. The changes centralize platform-specific logic within `KeybindingKeystroke` and update its usage throughout the codebase, making the API more consistent and less error-prone. Release Notes: - N/A --- crates/editor/src/editor.rs | 24 ++-- crates/gpui/src/keymap.rs | 2 +- crates/gpui/src/keymap/binding.rs | 2 +- crates/gpui/src/platform/keystroke.rs | 117 +++++++++++++++--- crates/gpui/src/platform/mac/platform.rs | 10 +- crates/gpui/src/platform/windows/keyboard.rs | 32 +++-- crates/settings/src/keymap_file.rs | 10 +- crates/settings_ui/src/keybindings.rs | 50 +++----- .../src/ui_components/keystroke_input.rs | 38 +++--- crates/ui/src/components/keybinding.rs | 19 ++- crates/zed/src/zed.rs | 2 +- 11 files changed, 183 insertions(+), 123 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2d96ddf7a4eccdf89cc52389aec996b0777afd32..ea7cce5d8b741268fe0d4182b66638c0495bb211 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2588,7 +2588,7 @@ impl Editor { || binding .keystrokes() .first() - .is_some_and(|keystroke| keystroke.display_modifiers.modified()) + .is_some_and(|keystroke| keystroke.modifiers().modified()) })) } @@ -7686,16 +7686,16 @@ impl Editor { .keystroke() { modifiers_held = modifiers_held - || (&accept_keystroke.display_modifiers == modifiers - && accept_keystroke.display_modifiers.modified()); + || (accept_keystroke.modifiers() == modifiers + && accept_keystroke.modifiers().modified()); }; if let Some(accept_partial_keystroke) = self .accept_edit_prediction_keybind(true, window, cx) .keystroke() { modifiers_held = modifiers_held - || (&accept_partial_keystroke.display_modifiers == modifiers - && accept_partial_keystroke.display_modifiers.modified()); + || (accept_partial_keystroke.modifiers() == modifiers + && accept_partial_keystroke.modifiers().modified()); } if modifiers_held { @@ -9044,7 +9044,7 @@ impl Editor { let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; - let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() { + let modifiers_color = if *accept_keystroke.modifiers() == window.modifiers() { Color::Accent } else { Color::Muted @@ -9056,19 +9056,19 @@ impl Editor { .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .text_size(TextSize::XSmall.rems(cx)) .child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + accept_keystroke.modifiers(), PlatformStyle::platform(), Some(modifiers_color), Some(IconSize::XSmall.rems().into()), true, ))) .when(is_platform_style_mac, |parent| { - parent.child(accept_keystroke.display_key.clone()) + parent.child(accept_keystroke.key().to_string()) }) .when(!is_platform_style_mac, |parent| { parent.child( Key::new( - util::capitalize(&accept_keystroke.display_key), + util::capitalize(accept_keystroke.key()), Some(Color::Default), ) .size(Some(IconSize::XSmall.rems().into())), @@ -9249,7 +9249,7 @@ impl Editor { accept_keystroke.as_ref(), |el, accept_keystroke| { el.child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + accept_keystroke.modifiers(), PlatformStyle::platform(), Some(Color::Default), Some(IconSize::XSmall.rems().into()), @@ -9319,7 +9319,7 @@ impl Editor { .child(completion), ) .when_some(accept_keystroke, |el, accept_keystroke| { - if !accept_keystroke.display_modifiers.modified() { + if !accept_keystroke.modifiers().modified() { return el; } @@ -9338,7 +9338,7 @@ impl Editor { .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .when(is_platform_style_mac, |parent| parent.gap_1()) .child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + accept_keystroke.modifiers(), PlatformStyle::platform(), Some(if !has_completion { Color::Muted diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index b3db09d8214d18cae77de4fbeff1ad7f722d83fa..12f082eb60799bdf9a0cdfaf7d546fa2bdf13e04 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -638,7 +638,7 @@ mod tests { fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) { let actual = keymap .bindings_for_action(action) - .map(|binding| binding.keystrokes[0].inner.unparse()) + .map(|binding| binding.keystrokes[0].inner().unparse()) .collect::>(); assert_eq!(actual, expected, "{:?}", action); } diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index a7cf9d5c540c74119bfb5c634a086a6259c2e852..fc4b32941b85f4cdea31aaba7198d3e7043ee481 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -57,7 +57,7 @@ impl KeyBinding { .split_whitespace() .map(|source| { let keystroke = Keystroke::parse(source)?; - Ok(KeybindingKeystroke::new( + Ok(KeybindingKeystroke::new_with_mapper( keystroke, use_key_equivalents, keyboard_mapper, diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 6ce17c3a01cd1eeef1d49e44c80b9e81f045b71b..4a2bfc785e3eb7e13a845bb67b4524255affb3bb 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -36,11 +36,13 @@ pub struct Keystroke { #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct KeybindingKeystroke { /// The GPUI representation of the keystroke. - pub inner: Keystroke, + inner: Keystroke, /// The modifiers to display. - pub display_modifiers: Modifiers, + #[cfg(target_os = "windows")] + display_modifiers: Modifiers, /// The key to display. - pub display_key: String, + #[cfg(target_os = "windows")] + display_key: String, } /// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use @@ -262,8 +264,17 @@ impl Keystroke { } impl KeybindingKeystroke { - /// Create a new keybinding keystroke from the given keystroke - pub fn new( + #[cfg(target_os = "windows")] + pub(crate) fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self { + KeybindingKeystroke { + inner, + display_modifiers, + display_key, + } + } + + /// Create a new keybinding keystroke from the given keystroke using the given keyboard mapper. + pub fn new_with_mapper( inner: Keystroke, use_key_equivalents: bool, keyboard_mapper: &dyn PlatformKeyboardMapper, @@ -271,19 +282,95 @@ impl KeybindingKeystroke { keyboard_mapper.map_key_equivalent(inner, use_key_equivalents) } - pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self { - let key = keystroke.key.clone(); - let modifiers = keystroke.modifiers; - KeybindingKeystroke { - inner: keystroke, - display_modifiers: modifiers, - display_key: key, + /// Create a new keybinding keystroke from the given keystroke, without any platform-specific mapping. + pub fn from_keystroke(keystroke: Keystroke) -> Self { + #[cfg(target_os = "windows")] + { + let key = keystroke.key.clone(); + let modifiers = keystroke.modifiers; + KeybindingKeystroke { + inner: keystroke, + display_modifiers: modifiers, + display_key: key, + } + } + #[cfg(not(target_os = "windows"))] + { + KeybindingKeystroke { inner: keystroke } + } + } + + /// Returns the GPUI representation of the keystroke. + pub fn inner(&self) -> &Keystroke { + &self.inner + } + + /// Returns the modifiers. + /// + /// Platform-specific behavior: + /// - On macOS and Linux, this modifiers is the same as `inner.modifiers`, which is the GPUI representation of the keystroke. + /// - On Windows, this modifiers is the display modifiers, for example, a `ctrl-@` keystroke will have `inner.modifiers` as + /// `Modifiers::control()` and `display_modifiers` as `Modifiers::control_shift()`. + pub fn modifiers(&self) -> &Modifiers { + #[cfg(target_os = "windows")] + { + &self.display_modifiers + } + #[cfg(not(target_os = "windows"))] + { + &self.inner.modifiers } } + /// Returns the key. + /// + /// Platform-specific behavior: + /// - On macOS and Linux, this key is the same as `inner.key`, which is the GPUI representation of the keystroke. + /// - On Windows, this key is the display key, for example, a `ctrl-@` keystroke will have `inner.key` as `@` and `display_key` as `2`. + pub fn key(&self) -> &str { + #[cfg(target_os = "windows")] + { + &self.display_key + } + #[cfg(not(target_os = "windows"))] + { + &self.inner.key + } + } + + /// Sets the modifiers. On Windows this modifies both `inner.modifiers` and `display_modifiers`. + pub fn set_modifiers(&mut self, modifiers: Modifiers) { + self.inner.modifiers = modifiers; + #[cfg(target_os = "windows")] + { + self.display_modifiers = modifiers; + } + } + + /// Sets the key. On Windows this modifies both `inner.key` and `display_key`. + pub fn set_key(&mut self, key: String) { + #[cfg(target_os = "windows")] + { + self.display_key = key.clone(); + } + self.inner.key = key; + } + /// Produces a representation of this key that Parse can understand. pub fn unparse(&self) -> String { - unparse(&self.display_modifiers, &self.display_key) + #[cfg(target_os = "windows")] + { + unparse(&self.display_modifiers, &self.display_key) + } + #[cfg(not(target_os = "windows"))] + { + unparse(&self.inner.modifiers, &self.inner.key) + } + } + + /// Removes the key_char + pub fn remove_key_char(&mut self) { + self.inner.key_char = None; } } @@ -350,8 +437,8 @@ impl std::fmt::Display for Keystroke { impl std::fmt::Display for KeybindingKeystroke { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - display_modifiers(&self.display_modifiers, f)?; - display_key(&self.display_key, f) + display_modifiers(self.modifiers(), f)?; + display_key(self.key(), f) } } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 30453def00bbacf7e8cc820020bf8ec831afc514..dea04d89a06acac526a8b033681829fdc1e148fd 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -354,19 +354,19 @@ impl MacPlatform { let mut mask = NSEventModifierFlags::empty(); for (modifier, flag) in &[ ( - keystroke.display_modifiers.platform, + keystroke.modifiers().platform, NSEventModifierFlags::NSCommandKeyMask, ), ( - keystroke.display_modifiers.control, + keystroke.modifiers().control, NSEventModifierFlags::NSControlKeyMask, ), ( - keystroke.display_modifiers.alt, + keystroke.modifiers().alt, NSEventModifierFlags::NSAlternateKeyMask, ), ( - keystroke.display_modifiers.shift, + keystroke.modifiers().shift, NSEventModifierFlags::NSShiftKeyMask, ), ] { @@ -379,7 +379,7 @@ impl MacPlatform { .initWithTitle_action_keyEquivalent_( ns_string(name), selector, - ns_string(key_to_native(&keystroke.display_key).as_ref()), + ns_string(key_to_native(keystroke.key()).as_ref()), ) .autorelease(); if Self::os_version() >= SemanticVersion::new(12, 0, 0) { diff --git a/crates/gpui/src/platform/windows/keyboard.rs b/crates/gpui/src/platform/windows/keyboard.rs index 0eb97fbb0c500d6481b6add7d6e716c795e69fa2..259ebaebff794d4ed7203420c8c66188998c5fa4 100644 --- a/crates/gpui/src/platform/windows/keyboard.rs +++ b/crates/gpui/src/platform/windows/keyboard.rs @@ -83,11 +83,7 @@ impl PlatformKeyboardMapper for WindowsKeyboardMapper { ..keystroke.modifiers }; - KeybindingKeystroke { - inner: keystroke, - display_modifiers: modifiers, - display_key: key, - } + KeybindingKeystroke::new(keystroke, modifiers, key) } fn get_key_equivalents(&self) -> Option<&HashMap> { @@ -335,9 +331,9 @@ mod tests { key_char: None, }; let mapped = mapper.map_key_equivalent(keystroke.clone(), true); - assert_eq!(mapped.inner, keystroke); - assert_eq!(mapped.display_key, "a"); - assert_eq!(mapped.display_modifiers, Modifiers::control()); + assert_eq!(*mapped.inner(), keystroke); + assert_eq!(mapped.key(), "a"); + assert_eq!(*mapped.modifiers(), Modifiers::control()); // Shifted case, ctrl-$ let keystroke = Keystroke { @@ -346,9 +342,9 @@ mod tests { key_char: None, }; let mapped = mapper.map_key_equivalent(keystroke.clone(), true); - assert_eq!(mapped.inner, keystroke); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); + assert_eq!(*mapped.inner(), keystroke); + assert_eq!(mapped.key(), "4"); + assert_eq!(*mapped.modifiers(), Modifiers::control_shift()); // Shifted case, but shift is true let keystroke = Keystroke { @@ -357,9 +353,9 @@ mod tests { key_char: None, }; let mapped = mapper.map_key_equivalent(keystroke, true); - assert_eq!(mapped.inner.modifiers, Modifiers::control()); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); + assert_eq!(mapped.inner().modifiers, Modifiers::control()); + assert_eq!(mapped.key(), "4"); + assert_eq!(*mapped.modifiers(), Modifiers::control_shift()); // Windows style let keystroke = Keystroke { @@ -368,9 +364,9 @@ mod tests { key_char: None, }; let mapped = mapper.map_key_equivalent(keystroke, true); - assert_eq!(mapped.inner.modifiers, Modifiers::control()); - assert_eq!(mapped.inner.key, "$"); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); + assert_eq!(mapped.inner().modifiers, Modifiers::control()); + assert_eq!(mapped.inner().key, "$"); + assert_eq!(mapped.key(), "4"); + assert_eq!(*mapped.modifiers(), Modifiers::control_shift()); } } diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 0e8303c4c17d0b13774b721b7f7bb3565a40cb97..91fcca8d5cbddf9dd30b867b3b89848cbc86de1e 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -820,7 +820,11 @@ impl KeymapFile { .split_whitespace() .map(|source| { let keystroke = Keystroke::parse(source)?; - Ok(KeybindingKeystroke::new(keystroke, false, keyboard_mapper)) + Ok(KeybindingKeystroke::new_with_mapper( + keystroke, + false, + keyboard_mapper, + )) }) .collect::, InvalidKeystrokeError>>() else { @@ -830,7 +834,7 @@ impl KeymapFile { || !keystrokes .iter() .zip(target.keystrokes) - .all(|(a, b)| a.inner.should_match(b)) + .all(|(a, b)| a.inner().should_match(b)) { continue; } @@ -1065,7 +1069,7 @@ mod tests { keystrokes .split(' ') .map(|s| { - KeybindingKeystroke::new( + KeybindingKeystroke::new_with_mapper( Keystroke::parse(s).expect("Keystrokes valid"), false, &DummyKeyboardMapper, diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 76c716600768bb318e2983267b961de440cbcf8f..161e1e768ddd8a111e001198d8aad352169d1cef 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -14,9 +14,9 @@ use gpui::{ Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or}, - KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point, - ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, - TextStyleRefinement, WeakEntity, actions, anchored, deferred, div, + KeyContext, KeybindingKeystroke, MouseButton, PlatformKeyboardMapper, Point, ScrollStrategy, + ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, + actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; use notifications::status_toast::{StatusToast, ToastIcon}; @@ -420,7 +420,7 @@ fn keystrokes_match_exactly( ) -> bool { keystrokes1.len() == keystrokes2.len() && keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| { - k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers + k1.inner().key == k2.inner().key && k1.inner().modifiers == k2.inner().modifiers }) } @@ -532,7 +532,7 @@ impl KeymapEditor { let keystroke_query = keystroke_query .into_iter() - .map(|keystroke| keystroke.inner.unparse()) + .map(|keystroke| keystroke.inner().unparse()) .collect::>() .join(" "); @@ -606,13 +606,13 @@ impl KeymapEditor { let query = &keystroke_query[query_cursor]; let keystroke = &keystrokes[keystroke_cursor]; let matches = query - .inner + .inner() .modifiers - .is_subset_of(&keystroke.inner.modifiers) - && ((query.inner.key.is_empty() - || query.inner.key == keystroke.inner.key) - && query.inner.key_char.as_ref().is_none_or( - |q_kc| q_kc == &keystroke.inner.key, + .is_subset_of(&keystroke.inner().modifiers) + && ((query.inner().key.is_empty() + || query.inner().key == keystroke.inner().key) + && query.inner().key_char.as_ref().is_none_or( + |q_kc| q_kc == &keystroke.inner().key, )); if matches { found_count += 1; @@ -2256,12 +2256,10 @@ impl KeybindingEditorModal { let fs = self.fs.clone(); let tab_size = cx.global::().json_tab_size(); - let new_keystrokes = self - .validate_keystrokes(cx) - .map_err(InputError::error)? - .into_iter() - .map(remove_key_char) - .collect::>(); + let mut new_keystrokes = self.validate_keystrokes(cx).map_err(InputError::error)?; + new_keystrokes + .iter_mut() + .for_each(|ks| ks.remove_key_char()); let new_context = self.validate_context(cx).map_err(InputError::error)?; let new_action_args = self @@ -2454,24 +2452,6 @@ impl KeybindingEditorModal { } } -fn remove_key_char( - KeybindingKeystroke { - inner, - display_modifiers, - display_key, - }: KeybindingKeystroke, -) -> KeybindingKeystroke { - KeybindingKeystroke { - inner: Keystroke { - modifiers: inner.modifiers, - key: inner.key, - key_char: None, - }, - display_modifiers, - display_key, - } -} - impl Render for KeybindingEditorModal { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let theme = cx.theme().colors(); diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index 5d27598d9150d1be9f58aeb9a81c1b68744ca4c0..e6b2ff710555403048c56bb1f249d71971d0e91b 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -116,7 +116,7 @@ impl KeystrokeInput { && self .keystrokes .last() - .is_some_and(|last| last.display_key.is_empty()) + .is_some_and(|last| last.key().is_empty()) { return &self.keystrokes[..self.keystrokes.len() - 1]; } @@ -124,15 +124,11 @@ impl KeystrokeInput { } fn dummy(modifiers: Modifiers) -> KeybindingKeystroke { - KeybindingKeystroke { - inner: Keystroke { - modifiers, - key: "".to_string(), - key_char: None, - }, - display_modifiers: modifiers, - display_key: "".to_string(), - } + KeybindingKeystroke::from_keystroke(Keystroke { + modifiers, + key: "".to_string(), + key_char: None, + }) } fn keystrokes_changed(&self, cx: &mut Context) { @@ -258,7 +254,7 @@ impl KeystrokeInput { self.keystrokes_changed(cx); if let Some(last) = self.keystrokes.last_mut() - && last.display_key.is_empty() + && last.key().is_empty() && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX { if !self.search && !event.modifiers.modified() { @@ -267,15 +263,14 @@ impl KeystrokeInput { } if self.search { if self.previous_modifiers.modified() { - last.display_modifiers |= event.modifiers; - last.inner.modifiers |= event.modifiers; + let modifiers = *last.modifiers() | event.modifiers; + last.set_modifiers(modifiers); } else { self.keystrokes.push(Self::dummy(event.modifiers)); } self.previous_modifiers |= event.modifiers; } else { - last.display_modifiers = event.modifiers; - last.inner.modifiers = event.modifiers; + last.set_modifiers(event.modifiers); return; } } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX { @@ -303,10 +298,13 @@ impl KeystrokeInput { return; } - let keystroke = - KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref()); + let keystroke = KeybindingKeystroke::new_with_mapper( + keystroke.clone(), + false, + cx.keyboard_mapper().as_ref(), + ); if let Some(last) = self.keystrokes.last() - && last.display_key.is_empty() + && last.key().is_empty() && (!self.search || self.previous_modifiers.modified()) { self.keystrokes.pop(); @@ -825,7 +823,7 @@ mod tests { input .keystrokes .iter() - .map(|keystroke| keystroke.inner.clone()) + .map(|keystroke| keystroke.inner().clone()) .collect() }); Self::expect_keystrokes_equal(&actual, expected); @@ -1094,7 +1092,7 @@ mod tests { } fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String { - ks.iter().map(|ks| ks.inner.unparse()).join(" ") + ks.iter().map(|ks| ks.inner().unparse()).join(" ") } } } diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 81817045dc6ce07d4f40a12e3850fef3e1b234f9..98703f65f4cf3ccec9e483c70f5a43ac8d53a280 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -124,7 +124,7 @@ impl RenderOnce for KeyBinding { "KEY_BINDING-{}", self.keystrokes .iter() - .map(|k| k.display_key.to_string()) + .map(|k| k.key().to_string()) .collect::>() .join(" ") ) @@ -165,8 +165,8 @@ pub fn render_keybinding_keystroke( if use_text { let element = Key::new( keystroke_text( - &keystroke.display_modifiers, - &keystroke.display_key, + keystroke.modifiers(), + keystroke.key(), platform_style, vim_mode, ), @@ -178,18 +178,13 @@ pub fn render_keybinding_keystroke( } else { let mut elements = Vec::new(); elements.extend(render_modifiers( - &keystroke.display_modifiers, + keystroke.modifiers(), platform_style, color, size, true, )); - elements.push(render_key( - &keystroke.display_key, - color, - platform_style, - size, - )); + elements.push(render_key(keystroke.key(), color, platform_style, size)); elements } } @@ -418,8 +413,8 @@ pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &A .iter() .map(|keystroke| { keystroke_text( - &keystroke.display_modifiers, - &keystroke.display_key, + keystroke.modifiers(), + keystroke.key(), platform_style, vim_enabled, ) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a12249d6a4841691fb302f8cb26b28951f829c3d..587065f9b123ff4681494ece99f4051c7a50025e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4738,7 +4738,7 @@ mod tests { // and key strokes contain the given key bindings .into_iter() - .any(|binding| binding.keystrokes().iter().any(|k| k.display_key == key)), + .any(|binding| binding.keystrokes().iter().any(|k| k.key() == key)), "On {} Failed to find {} with key binding {}", line, action.name(), From 54609d4d00ad06635f4b63874102c2e45081f273 Mon Sep 17 00:00:00 2001 From: Lorenzo Stella Date: Thu, 28 Aug 2025 12:35:30 +0200 Subject: [PATCH 403/744] Fix boolean settings in "Agent Settings" documentation page (#37068) This fixes some errors in the examples in the "Agent Settings" page at https://zed.dev/docs/ai/agent-settings#agent-settings, where strings "true" and "false" are used in place of the proper boolean JSON values: strings don't work for all those settings, and are marked as errors when editing settings.json, while booleans do work. Release Notes: - N/A --- docs/src/ai/agent-settings.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/src/ai/agent-settings.md b/docs/src/ai/agent-settings.md index ff97bcb8eeb941d2c072b95dbcc8089da927df42..d78f812e4704123be34144e84709df71474c82c0 100644 --- a/docs/src/ai/agent-settings.md +++ b/docs/src/ai/agent-settings.md @@ -131,7 +131,7 @@ The default value is `false`. ```json { "agent": { - "always_allow_tool_actions": "true" + "always_allow_tool_actions": true } } ``` @@ -146,7 +146,7 @@ The default value is `false`. ```json { "agent": { - "single_file_review": "true" + "single_file_review": true } } ``` @@ -163,7 +163,7 @@ The default value is `false`. ```json { "agent": { - "play_sound_when_agent_done": "true" + "play_sound_when_agent_done": true } } ``` @@ -179,7 +179,7 @@ The default value is `false`. ```json { "agent": { - "use_modifier_to_send": "true" + "use_modifier_to_send": true } } ``` @@ -194,7 +194,7 @@ It is set to `true` by default, but if set to false, the card's height is capped ```json { "agent": { - "expand_edit_card": "false" + "expand_edit_card": false } } ``` @@ -207,7 +207,7 @@ It is set to `true` by default, but if set to false, the card will be fully coll ```json { "agent": { - "expand_terminal_card": "false" + "expand_terminal_card": false } } ``` @@ -220,7 +220,7 @@ The default value is `true`. ```json { "agent": { - "enable_feedback": "false" + "enable_feedback": false } } ``` From 4981c33bf36f3b2f40d91fbd9433e16e1a0e32c7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 28 Aug 2025 12:57:09 +0200 Subject: [PATCH 404/744] acp: Don't cancel editing when scrolling message out of view (#37020) Release Notes: - agent: Fixed a bug that canceled editing when scrolling the user message out of view. Co-authored-by: Bennet Bo Fenner --- crates/agent_ui/src/acp/entry_view_state.rs | 11 +++++++++-- crates/agent_ui/src/acp/thread_view.rs | 14 +++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index becf6953fd8e63bd6e208c74234c91beb94c4b44..76b3709325a0c84a72bc71db8a67a3d4bd72dd06 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -6,8 +6,8 @@ use agent2::HistoryStore; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; use gpui::{ - AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle, - TextStyleRefinement, WeakEntity, Window, + AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable, + ScrollHandle, TextStyleRefinement, WeakEntity, Window, }; use language::language_settings::SoftWrap; use project::Project; @@ -247,6 +247,13 @@ pub enum Entry { } impl Entry { + pub fn focus_handle(&self, cx: &App) -> Option { + match self { + Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)), + Self::AssistantMessage(_) | Self::Content(_) => None, + } + } + pub fn message_editor(&self) -> Option<&Entity> { match self { Self::UserMessage(editor) => Some(editor), diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 94b385c04e933fcdf2906db584319ee50702df3a..57d90734ef00d1160eb017d1e1257f63577cbebc 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -479,11 +479,14 @@ impl AcpThreadView { .set(thread.read(cx).prompt_capabilities()); let count = thread.read(cx).entries().len(); - this.list_state.splice(0..0, count); this.entry_view_state.update(cx, |view_state, cx| { for ix in 0..count { view_state.sync_entry(ix, &thread, window, cx); } + this.list_state.splice_focusable( + 0..0, + (0..count).map(|ix| view_state.entry(ix)?.focus_handle(cx)), + ); }); if let Some(resume) = resume_thread { @@ -1116,9 +1119,14 @@ impl AcpThreadView { let len = thread.read(cx).entries().len(); let index = len - 1; self.entry_view_state.update(cx, |view_state, cx| { - view_state.sync_entry(index, thread, window, cx) + view_state.sync_entry(index, thread, window, cx); + self.list_state.splice_focusable( + index..index, + [view_state + .entry(index) + .and_then(|entry| entry.focus_handle(cx))], + ); }); - self.list_state.splice(index..index, 1); } AcpThreadEvent::EntryUpdated(index) => { self.entry_view_state.update(cx, |view_state, cx| { From 39d86eeb7f93001ba526eb97f58102034d522f8c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 28 Aug 2025 14:00:44 +0200 Subject: [PATCH 405/744] Trim API key when submitting requests to LLM providers (#37082) This prevents the common footgun of copy/pasting an API key starting/ending with extra newlines, which would lead to a "bad request" error. Closes #37038 Release Notes: - agent: Support pasting language model API keys that contain newlines. --- crates/anthropic/src/anthropic.rs | 4 ++-- crates/deepseek/src/deepseek.rs | 2 +- crates/google_ai/src/google_ai.rs | 1 + crates/mistral/src/mistral.rs | 2 +- crates/open_ai/src/open_ai.rs | 4 ++-- crates/open_router/src/open_router.rs | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/anthropic/src/anthropic.rs b/crates/anthropic/src/anthropic.rs index 3ff1666755d439cf52a14ea635a06a7c3414d9f6..773bb557de1895e57bdeb5612e01e2839af3244b 100644 --- a/crates/anthropic/src/anthropic.rs +++ b/crates/anthropic/src/anthropic.rs @@ -373,7 +373,7 @@ pub async fn complete( .uri(uri) .header("Anthropic-Version", "2023-06-01") .header("Anthropic-Beta", beta_headers) - .header("X-Api-Key", api_key) + .header("X-Api-Key", api_key.trim()) .header("Content-Type", "application/json"); let serialized_request = @@ -526,7 +526,7 @@ pub async fn stream_completion_with_rate_limit_info( .uri(uri) .header("Anthropic-Version", "2023-06-01") .header("Anthropic-Beta", beta_headers) - .header("X-Api-Key", api_key) + .header("X-Api-Key", api_key.trim()) .header("Content-Type", "application/json"); let serialized_request = serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?; diff --git a/crates/deepseek/src/deepseek.rs b/crates/deepseek/src/deepseek.rs index c49270febe3b2b3702b808e2219f6e45d7252267..c2554c67e93b4c1d3772e60a62063fdae0511f05 100644 --- a/crates/deepseek/src/deepseek.rs +++ b/crates/deepseek/src/deepseek.rs @@ -268,7 +268,7 @@ pub async fn stream_completion( .method(Method::POST) .uri(uri) .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)); + .header("Authorization", format!("Bearer {}", api_key.trim())); let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?; let mut response = client.send(request).await?; diff --git a/crates/google_ai/src/google_ai.rs b/crates/google_ai/src/google_ai.rs index ca0aa309b1296e021402d921b7a7809f1e593e2b..92fd53189327fabccdc1472ac0fa2a20dc665646 100644 --- a/crates/google_ai/src/google_ai.rs +++ b/crates/google_ai/src/google_ai.rs @@ -13,6 +13,7 @@ pub async fn stream_generate_content( api_key: &str, mut request: GenerateContentRequest, ) -> Result>> { + let api_key = api_key.trim(); validate_generate_content_request(&request)?; // The `model` field is emptied as it is provided as a path parameter. diff --git a/crates/mistral/src/mistral.rs b/crates/mistral/src/mistral.rs index 5b4d05377c7132f47828aa6afafbb5c850e940a8..55986e7e5bfd69ec91f11089753562e9e1984fcc 100644 --- a/crates/mistral/src/mistral.rs +++ b/crates/mistral/src/mistral.rs @@ -482,7 +482,7 @@ pub async fn stream_completion( .method(Method::POST) .uri(uri) .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)); + .header("Authorization", format!("Bearer {}", api_key.trim())); let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?; let mut response = client.send(request).await?; diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 08be82b8303291f9fa7795b2f04cbe8392d6d581..f9a983b433b9d918424f9696269dd0bbd72adefd 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -461,7 +461,7 @@ pub async fn stream_completion( .method(Method::POST) .uri(uri) .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)); + .header("Authorization", format!("Bearer {}", api_key.trim())); let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?; let mut response = client.send(request).await?; @@ -565,7 +565,7 @@ pub fn embed<'a>( .method(Method::POST) .uri(uri) .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)) + .header("Authorization", format!("Bearer {}", api_key.trim())) .body(body) .map(|request| client.send(request)); diff --git a/crates/open_router/src/open_router.rs b/crates/open_router/src/open_router.rs index b7e6d69d8fbe7c342e833cd13ad069399fb44a26..65ef519d2c887e57e67d68ca6fcaea64ad67ee3e 100644 --- a/crates/open_router/src/open_router.rs +++ b/crates/open_router/src/open_router.rs @@ -424,7 +424,7 @@ pub async fn complete( .method(Method::POST) .uri(uri) .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)) + .header("Authorization", format!("Bearer {}", api_key.trim())) .header("HTTP-Referer", "https://zed.dev") .header("X-Title", "Zed Editor"); From f127ba82d15fbe4add458281902bfb89fc35158a Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 28 Aug 2025 15:32:44 +0300 Subject: [PATCH 406/744] Remote LSP logs (#37083) Take 2: https://github.com/zed-industries/zed/pull/36709 but without the very bad `cfg`-based approach for storing the RPC logs. -------------- Enables LSP log tracing in both remote collab and remote ssh environments. Server logs and server RPC traces can now be viewed remotely, and the LSP button is now shown in such projects too. Closes https://github.com/zed-industries/zed/issues/28557 Co-Authored-By: Kirill Co-Authored-By: Lukas Release Notes: - Enabled LSP log tracing in both remote collab and remote ssh environments --------- Co-authored-by: Ben Kunkle Co-authored-by: Lukas Wirth --- Cargo.lock | 1 + .../20221109000000_test_schema.sql | 1 + .../20250827084812_worktree_in_servers.sql | 2 + crates/collab/src/db/queries/projects.rs | 4 +- crates/collab/src/db/queries/rooms.rs | 2 +- .../collab/src/db/tables/language_server.rs | 1 + crates/collab/src/rpc.rs | 4 +- crates/language_tools/Cargo.toml | 1 + crates/language_tools/src/language_tools.rs | 10 +- .../src/{lsp_tool.rs => lsp_button.rs} | 141 ++- .../src/{lsp_log.rs => lsp_log_view.rs} | 952 +++++------------- ...lsp_log_tests.rs => lsp_log_view_tests.rs} | 14 +- crates/project/src/lsp_store.rs | 128 ++- crates/project/src/lsp_store/log_store.rs | 704 +++++++++++++ crates/project/src/project.rs | 64 +- crates/project/src/project_tests.rs | 1 + crates/proto/proto/lsp.proto | 44 +- crates/proto/proto/zed.proto | 3 +- crates/proto/src/proto.rs | 7 +- crates/remote_server/src/headless_project.rs | 79 +- crates/settings/src/settings.rs | 2 +- crates/zed/src/zed.rs | 17 +- 22 files changed, 1332 insertions(+), 850 deletions(-) create mode 100644 crates/collab/migrations/20250827084812_worktree_in_servers.sql rename crates/language_tools/src/{lsp_tool.rs => lsp_button.rs} (90%) rename crates/language_tools/src/{lsp_log.rs => lsp_log_view.rs} (65%) rename crates/language_tools/src/{lsp_log_tests.rs => lsp_log_view_tests.rs} (91%) create mode 100644 crates/project/src/lsp_store/log_store.rs diff --git a/Cargo.lock b/Cargo.lock index 4325addc392214614a6654563d88041331f2ded9..8bddc8b008c1a1f048c11ecbb04d5f092ed19d5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9213,6 +9213,7 @@ dependencies = [ "language", "lsp", "project", + "proto", "release_channel", "serde_json", "settings", diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 43581fd9421e5a8d10460a9ed15c565bd66a6e5e..b2e25458ef98b295b4d056a7f59521f4fa896f1a 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -175,6 +175,7 @@ CREATE TABLE "language_servers" ( "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, "name" VARCHAR NOT NULL, "capabilities" TEXT NOT NULL, + "worktree_id" BIGINT, PRIMARY KEY (project_id, id) ); diff --git a/crates/collab/migrations/20250827084812_worktree_in_servers.sql b/crates/collab/migrations/20250827084812_worktree_in_servers.sql new file mode 100644 index 0000000000000000000000000000000000000000..d4c6ffbbcccb2d2f23654cfc287b45bb8ea20508 --- /dev/null +++ b/crates/collab/migrations/20250827084812_worktree_in_servers.sql @@ -0,0 +1,2 @@ +ALTER TABLE language_servers + ADD COLUMN worktree_id BIGINT; diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 393f2c80f8e733aa2d2b3b5f4b811c9868e0a620..a3f0ea6cbc6e762e365f82e74b886234e62da109 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -694,6 +694,7 @@ impl Database { project_id: ActiveValue::set(project_id), id: ActiveValue::set(server.id as i64), name: ActiveValue::set(server.name.clone()), + worktree_id: ActiveValue::set(server.worktree_id.map(|id| id as i64)), capabilities: ActiveValue::set(update.capabilities.clone()), }) .on_conflict( @@ -704,6 +705,7 @@ impl Database { .update_columns([ language_server::Column::Name, language_server::Column::Capabilities, + language_server::Column::WorktreeId, ]) .to_owned(), ) @@ -1065,7 +1067,7 @@ impl Database { server: proto::LanguageServer { id: language_server.id as u64, name: language_server.name, - worktree_id: None, + worktree_id: language_server.worktree_id.map(|id| id as u64), }, capabilities: language_server.capabilities, }) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 9e7cabf9b29c91d7e486f42d5e6b12020b0f514e..0713ac2cb2810797b319b53583bc8c0e1756fe68 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -809,7 +809,7 @@ impl Database { server: proto::LanguageServer { id: language_server.id as u64, name: language_server.name, - worktree_id: None, + worktree_id: language_server.worktree_id.map(|id| id as u64), }, capabilities: language_server.capabilities, }) diff --git a/crates/collab/src/db/tables/language_server.rs b/crates/collab/src/db/tables/language_server.rs index 34c7514d917b313990521acf8542c31394d009fc..705aae292ba456622e9808f033a348f60c3835a4 100644 --- a/crates/collab/src/db/tables/language_server.rs +++ b/crates/collab/src/db/tables/language_server.rs @@ -10,6 +10,7 @@ pub struct Model { pub id: i64, pub name: String, pub capabilities: String, + pub worktree_id: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 73f327166a3f1fb40a1f232ea2fabcdedd3fb129..9e4dfd4854b4de67de522bfbbd1160fe880a05cb 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -476,7 +476,9 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_message_handler(broadcast_project_message_from_host::) - .add_message_handler(update_context); + .add_message_handler(update_context) + .add_request_handler(forward_mutating_project_request::) + .add_message_handler(broadcast_project_message_from_host::); Arc::new(server) } diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index 5aa914311a6eccc1cb68efa37e878ad12249d6fd..b8f85d8d90068be9ad6849528f28522a96206cc8 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -24,6 +24,7 @@ itertools.workspace = true language.workspace = true lsp.workspace = true project.workspace = true +proto.workspace = true serde_json.workspace = true settings.workspace = true theme.workspace = true diff --git a/crates/language_tools/src/language_tools.rs b/crates/language_tools/src/language_tools.rs index cbf5756875f723b52fabbfe877c32265dd6f0aef..c784a67313a904df34c9f2ae071ed5b0e4c11751 100644 --- a/crates/language_tools/src/language_tools.rs +++ b/crates/language_tools/src/language_tools.rs @@ -1,20 +1,20 @@ mod key_context_view; -mod lsp_log; -pub mod lsp_tool; +pub mod lsp_button; +pub mod lsp_log_view; mod syntax_tree_view; #[cfg(test)] -mod lsp_log_tests; +mod lsp_log_view_tests; use gpui::{App, AppContext, Entity}; -pub use lsp_log::{LogStore, LspLogToolbarItemView, LspLogView}; +pub use lsp_log_view::LspLogView; pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView}; use ui::{Context, Window}; use workspace::{Item, ItemHandle, SplitDirection, Workspace}; pub fn init(cx: &mut App) { - lsp_log::init(cx); + lsp_log_view::init(true, cx); syntax_tree_view::init(cx); key_context_view::init(cx); } diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_button.rs similarity index 90% rename from crates/language_tools/src/lsp_tool.rs rename to crates/language_tools/src/lsp_button.rs index dd3e80212fda08f43718a664d2cfd6d377182273..f91c4cc61c7e56dc75ad36aa91a4582598995e15 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_button.rs @@ -11,7 +11,10 @@ use editor::{Editor, EditorEvent}; use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions}; use language::{BinaryStatus, BufferId, ServerHealth}; use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector}; -use project::{LspStore, LspStoreEvent, Worktree, project_settings::ProjectSettings}; +use project::{ + LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore, + project_settings::ProjectSettings, +}; use settings::{Settings as _, SettingsStore}; use ui::{ Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide, @@ -20,7 +23,7 @@ use ui::{ use workspace::{StatusItemView, Workspace}; -use crate::lsp_log::GlobalLogStore; +use crate::lsp_log_view; actions!( lsp_tool, @@ -30,7 +33,7 @@ actions!( ] ); -pub struct LspTool { +pub struct LspButton { server_state: Entity, popover_menu_handle: PopoverMenuHandle, lsp_menu: Option>, @@ -121,9 +124,8 @@ impl LanguageServerState { menu = menu.align_popover_bottom(); let lsp_logs = cx .try_global::() - .and_then(|lsp_logs| lsp_logs.0.upgrade()); - let lsp_store = self.lsp_store.upgrade(); - let Some((lsp_logs, lsp_store)) = lsp_logs.zip(lsp_store) else { + .map(|lsp_logs| lsp_logs.0.clone()); + let Some(lsp_logs) = lsp_logs else { return menu; }; @@ -210,10 +212,11 @@ impl LanguageServerState { }; let server_selector = server_info.server_selector(); - // TODO currently, Zed remote does not work well with the LSP logs - // https://github.com/zed-industries/zed/issues/28557 - let has_logs = lsp_store.read(cx).as_local().is_some() - && lsp_logs.read(cx).has_server_logs(&server_selector); + let is_remote = self + .lsp_store + .update(cx, |lsp_store, _| lsp_store.as_remote().is_some()) + .unwrap_or(false); + let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector); let status_color = server_info .binary_status @@ -241,10 +244,10 @@ impl LanguageServerState { .as_ref() .or_else(|| server_info.binary_status.as_ref()?.message.as_ref()) .cloned(); - let hover_label = if has_logs { - Some("View Logs") - } else if message.is_some() { + let hover_label = if message.is_some() { Some("View Message") + } else if has_logs { + Some("View Logs") } else { None }; @@ -288,16 +291,7 @@ impl LanguageServerState { let server_name = server_info.name.clone(); let workspace = self.workspace.clone(); move |window, cx| { - if has_logs { - lsp_logs.update(cx, |lsp_logs, cx| { - lsp_logs.open_server_trace( - workspace.clone(), - server_selector.clone(), - window, - cx, - ); - }); - } else if let Some(message) = &message { + if let Some(message) = &message { let Some(create_buffer) = workspace .update(cx, |workspace, cx| { workspace @@ -347,6 +341,14 @@ impl LanguageServerState { anyhow::Ok(()) }) .detach(); + } else if has_logs { + lsp_log_view::open_server_trace( + &lsp_logs, + workspace.clone(), + server_selector.clone(), + window, + cx, + ); } else { cx.propagate(); } @@ -510,7 +512,7 @@ impl ServerData<'_> { } } -impl LspTool { +impl LspButton { pub fn new( workspace: &Workspace, popover_menu_handle: PopoverMenuHandle, @@ -518,37 +520,59 @@ impl LspTool { cx: &mut Context, ) -> Self { let settings_subscription = - cx.observe_global_in::(window, move |lsp_tool, window, cx| { + cx.observe_global_in::(window, move |lsp_button, window, cx| { if ProjectSettings::get_global(cx).global_lsp_settings.button { - if lsp_tool.lsp_menu.is_none() { - lsp_tool.refresh_lsp_menu(true, window, cx); + if lsp_button.lsp_menu.is_none() { + lsp_button.refresh_lsp_menu(true, window, cx); } - } else if lsp_tool.lsp_menu.take().is_some() { + } else if lsp_button.lsp_menu.take().is_some() { cx.notify(); } }); let lsp_store = workspace.project().read(cx).lsp_store(); + let mut language_servers = LanguageServers::default(); + for (_, status) in lsp_store.read(cx).language_server_statuses() { + language_servers.binary_statuses.insert( + status.name.clone(), + LanguageServerBinaryStatus { + status: BinaryStatus::None, + message: None, + }, + ); + } + let lsp_store_subscription = - cx.subscribe_in(&lsp_store, window, |lsp_tool, _, e, window, cx| { - lsp_tool.on_lsp_store_event(e, window, cx) + cx.subscribe_in(&lsp_store, window, |lsp_button, _, e, window, cx| { + lsp_button.on_lsp_store_event(e, window, cx) }); - let state = cx.new(|_| LanguageServerState { + let server_state = cx.new(|_| LanguageServerState { workspace: workspace.weak_handle(), items: Vec::new(), lsp_store: lsp_store.downgrade(), active_editor: None, - language_servers: LanguageServers::default(), + language_servers, }); - Self { - server_state: state, + let mut lsp_button = Self { + server_state, popover_menu_handle, lsp_menu: None, lsp_menu_refresh: Task::ready(()), _subscriptions: vec![settings_subscription, lsp_store_subscription], + }; + if !lsp_button + .server_state + .read(cx) + .language_servers + .binary_statuses + .is_empty() + { + lsp_button.refresh_lsp_menu(true, window, cx); } + + lsp_button } fn on_lsp_store_event( @@ -708,6 +732,25 @@ impl LspTool { } } } + state + .lsp_store + .update(cx, |lsp_store, cx| { + for (server_id, status) in lsp_store.language_server_statuses() { + if let Some(worktree) = status.worktree.and_then(|worktree_id| { + lsp_store + .worktree_store() + .read(cx) + .worktree_for_id(worktree_id, cx) + }) { + server_ids_to_worktrees.insert(server_id, worktree.clone()); + server_names_to_worktrees + .entry(status.name.clone()) + .or_default() + .insert((worktree, server_id)); + } + } + }) + .ok(); let mut servers_per_worktree = BTreeMap::>::new(); let mut servers_without_worktree = Vec::::new(); @@ -852,18 +895,18 @@ impl LspTool { ) { if create_if_empty || self.lsp_menu.is_some() { let state = self.server_state.clone(); - self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_tool, cx| { + self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_button, cx| { cx.background_executor() .timer(Duration::from_millis(30)) .await; - lsp_tool - .update_in(cx, |lsp_tool, window, cx| { - lsp_tool.regenerate_items(cx); + lsp_button + .update_in(cx, |lsp_button, window, cx| { + lsp_button.regenerate_items(cx); let menu = ContextMenu::build(window, cx, |menu, _, cx| { state.update(cx, |state, cx| state.fill_menu(menu, cx)) }); - lsp_tool.lsp_menu = Some(menu.clone()); - lsp_tool.popover_menu_handle.refresh_menu( + lsp_button.lsp_menu = Some(menu.clone()); + lsp_button.popover_menu_handle.refresh_menu( window, cx, Rc::new(move |_, _| Some(menu.clone())), @@ -876,7 +919,7 @@ impl LspTool { } } -impl StatusItemView for LspTool { +impl StatusItemView for LspButton { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn workspace::ItemHandle>, @@ -899,9 +942,9 @@ impl StatusItemView for LspTool { let _editor_subscription = cx.subscribe_in( &editor, window, - |lsp_tool, _, e: &EditorEvent, window, cx| match e { + |lsp_button, _, e: &EditorEvent, window, cx| match e { EditorEvent::ExcerptsAdded { buffer, .. } => { - let updated = lsp_tool.server_state.update(cx, |state, cx| { + let updated = lsp_button.server_state.update(cx, |state, cx| { if let Some(active_editor) = state.active_editor.as_mut() { let buffer_id = buffer.read(cx).remote_id(); active_editor.editor_buffers.insert(buffer_id) @@ -910,13 +953,13 @@ impl StatusItemView for LspTool { } }); if updated { - lsp_tool.refresh_lsp_menu(false, window, cx); + lsp_button.refresh_lsp_menu(false, window, cx); } } EditorEvent::ExcerptsRemoved { removed_buffer_ids, .. } => { - let removed = lsp_tool.server_state.update(cx, |state, _| { + let removed = lsp_button.server_state.update(cx, |state, _| { let mut removed = false; if let Some(active_editor) = state.active_editor.as_mut() { for id in removed_buffer_ids { @@ -930,7 +973,7 @@ impl StatusItemView for LspTool { removed }); if removed { - lsp_tool.refresh_lsp_menu(false, window, cx); + lsp_button.refresh_lsp_menu(false, window, cx); } } _ => {} @@ -960,7 +1003,7 @@ impl StatusItemView for LspTool { } } -impl Render for LspTool { +impl Render for LspButton { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl ui::IntoElement { if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() { return div(); @@ -1005,11 +1048,11 @@ impl Render for LspTool { (None, "All Servers Operational") }; - let lsp_tool = cx.entity(); + let lsp_button = cx.entity(); div().child( PopoverMenu::new("lsp-tool") - .menu(move |_, cx| lsp_tool.read(cx).lsp_menu.clone()) + .menu(move |_, cx| lsp_button.read(cx).lsp_menu.clone()) .anchor(Corner::BottomLeft) .with_handle(self.popover_menu_handle.clone()) .trigger_with_tooltip( diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log_view.rs similarity index 65% rename from crates/language_tools/src/lsp_log.rs rename to crates/language_tools/src/lsp_log_view.rs index a71e434e5274392add0463830519834202b7ba58..e54411f1d43a6e99352b8ef4dfc48cca423badb6 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log_view.rs @@ -1,20 +1,24 @@ -use collections::{HashMap, VecDeque}; +use collections::VecDeque; use copilot::Copilot; use editor::{Editor, EditorEvent, actions::MoveToEnd, scroll::Autoscroll}; -use futures::{StreamExt, channel::mpsc}; use gpui::{ - AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, Global, - IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div, + AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, + ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div, }; use itertools::Itertools; use language::{LanguageServerId, language_settings::SoftWrap}; use lsp::{ - IoKind, LanguageServer, LanguageServerName, LanguageServerSelector, MessageType, + LanguageServer, LanguageServerBinary, LanguageServerName, LanguageServerSelector, MessageType, SetTraceParams, TraceValue, notification::SetTrace, }; -use project::{Project, WorktreeId, search::SearchQuery}; +use project::{ + Project, + lsp_store::log_store::{self, Event, LanguageServerKind, LogKind, LogStore, Message}, + search::SearchQuery, +}; use std::{any::TypeId, borrow::Cow, sync::Arc}; use ui::{Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState, prelude::*}; +use util::ResultExt as _; use workspace::{ SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, item::{Item, ItemHandle}, @@ -23,132 +27,53 @@ use workspace::{ use crate::get_or_create_tool; -const SEND_LINE: &str = "\n// Send:"; -const RECEIVE_LINE: &str = "\n// Receive:"; -const MAX_STORED_LOG_ENTRIES: usize = 2000; - -pub struct LogStore { - projects: HashMap, ProjectState>, - language_servers: HashMap, - copilot_log_subscription: Option, - _copilot_subscription: Option, - io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>, -} - -struct ProjectState { - _subscriptions: [gpui::Subscription; 2], -} - -trait Message: AsRef { - type Level: Copy + std::fmt::Debug; - fn should_include(&self, _: Self::Level) -> bool { - true - } -} - -pub(super) struct LogMessage { - message: String, - typ: MessageType, -} - -impl AsRef for LogMessage { - fn as_ref(&self) -> &str { - &self.message - } -} - -impl Message for LogMessage { - type Level = MessageType; - - fn should_include(&self, level: Self::Level) -> bool { - match (self.typ, level) { - (MessageType::ERROR, _) => true, - (_, MessageType::ERROR) => false, - (MessageType::WARNING, _) => true, - (_, MessageType::WARNING) => false, - (MessageType::INFO, _) => true, - (_, MessageType::INFO) => false, - _ => true, - } - } -} - -pub(super) struct TraceMessage { - message: String, -} - -impl AsRef for TraceMessage { - fn as_ref(&self) -> &str { - &self.message - } -} - -impl Message for TraceMessage { - type Level = (); -} - -struct RpcMessage { - message: String, -} - -impl AsRef for RpcMessage { - fn as_ref(&self) -> &str { - &self.message - } -} - -impl Message for RpcMessage { - type Level = (); -} - -pub(super) struct LanguageServerState { - name: Option, - worktree_id: Option, - kind: LanguageServerKind, - log_messages: VecDeque, - trace_messages: VecDeque, - rpc_state: Option, - trace_level: TraceValue, - log_level: MessageType, - io_logs_subscription: Option, -} - -#[derive(PartialEq, Clone)] -pub enum LanguageServerKind { - Local { project: WeakEntity }, - Remote { project: WeakEntity }, - Global, -} - -impl LanguageServerKind { - fn is_remote(&self) -> bool { - matches!(self, LanguageServerKind::Remote { .. }) - } -} - -impl std::fmt::Debug for LanguageServerKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"), - LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"), - LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"), - } - } -} - -impl LanguageServerKind { - fn project(&self) -> Option<&WeakEntity> { - match self { - Self::Local { project } => Some(project), - Self::Remote { project } => Some(project), - Self::Global { .. } => None, - } - } -} - -struct LanguageServerRpcState { - rpc_messages: VecDeque, - last_message_kind: Option, +pub fn open_server_trace( + log_store: &Entity, + workspace: WeakEntity, + server: LanguageServerSelector, + window: &mut Window, + cx: &mut App, +) { + log_store.update(cx, |_, cx| { + cx.spawn_in(window, async move |log_store, cx| { + let Some(log_store) = log_store.upgrade() else { + return; + }; + workspace + .update_in(cx, |workspace, window, cx| { + let project = workspace.project().clone(); + let tool_log_store = log_store.clone(); + let log_view = get_or_create_tool( + workspace, + SplitDirection::Right, + window, + cx, + move |window, cx| LspLogView::new(project, tool_log_store, window, cx), + ); + log_view.update(cx, |log_view, cx| { + let server_id = match server { + LanguageServerSelector::Id(id) => Some(id), + LanguageServerSelector::Name(name) => { + log_store.read(cx).language_servers.iter().find_map( + |(id, state)| { + if state.name.as_ref() == Some(&name) { + Some(*id) + } else { + None + } + }, + ) + } + }; + if let Some(server_id) = server_id { + log_view.show_rpc_trace_for_server(server_id, window, cx); + } + }); + }) + .ok(); + }) + .detach(); + }) } pub struct LspLogView { @@ -167,32 +92,6 @@ pub struct LspLogToolbarItemView { _log_view_subscription: Option, } -#[derive(Copy, Clone, PartialEq, Eq)] -enum MessageKind { - Send, - Receive, -} - -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub enum LogKind { - Rpc, - Trace, - #[default] - Logs, - ServerInfo, -} - -impl LogKind { - fn label(&self) -> &'static str { - match self { - LogKind::Rpc => RPC_MESSAGES, - LogKind::Trace => SERVER_TRACE, - LogKind::Logs => SERVER_LOGS, - LogKind::ServerInfo => SERVER_INFO, - } - } -} - #[derive(Clone, Debug, PartialEq)] pub(crate) struct LogMenuItem { pub server_id: LanguageServerId, @@ -212,59 +111,24 @@ actions!( ] ); -pub(super) struct GlobalLogStore(pub WeakEntity); - -impl Global for GlobalLogStore {} - -pub fn init(cx: &mut App) { - let log_store = cx.new(LogStore::new); - cx.set_global(GlobalLogStore(log_store.downgrade())); - - cx.observe_new(move |workspace: &mut Workspace, _, cx| { - let project = workspace.project(); - if project.read(cx).is_local() || project.read(cx).is_via_remote_server() { - log_store.update(cx, |store, cx| { - store.add_project(project, cx); - }); - } - - let log_store = log_store.clone(); - workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| { - let project = workspace.project().read(cx); - if project.is_local() || project.is_via_remote_server() { - let project = workspace.project().clone(); - let log_store = log_store.clone(); - get_or_create_tool( - workspace, - SplitDirection::Right, - window, - cx, - move |window, cx| LspLogView::new(project, log_store, window, cx), - ); - } - }); - }) - .detach(); -} - -impl LogStore { - pub fn new(cx: &mut Context) -> Self { - let (io_tx, mut io_rx) = mpsc::unbounded(); +pub fn init(store_logs: bool, cx: &mut App) { + let log_store = log_store::init(store_logs, cx); - let copilot_subscription = Copilot::global(cx).map(|copilot| { + log_store.update(cx, |_, cx| { + Copilot::global(cx).map(|copilot| { let copilot = &copilot; - cx.subscribe(copilot, |this, copilot, edit_prediction_event, cx| { + cx.subscribe(copilot, |log_store, copilot, edit_prediction_event, cx| { if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event && let Some(server) = copilot.read(cx).language_server() { let server_id = server.server_id(); - let weak_this = cx.weak_entity(); - this.copilot_log_subscription = + let weak_lsp_store = cx.weak_entity(); + log_store.copilot_log_subscription = Some(server.on_notification::( move |params, cx| { - weak_this - .update(cx, |this, cx| { - this.add_language_server_log( + weak_lsp_store + .update(cx, |lsp_store, cx| { + lsp_store.add_language_server_log( server_id, MessageType::LOG, ¶ms.message, @@ -274,8 +138,9 @@ impl LogStore { .ok(); }, )); + let name = LanguageServerName::new_static("copilot"); - this.add_language_server( + log_store.add_language_server( LanguageServerKind::Global, server.server_id(), Some(name), @@ -285,429 +150,29 @@ impl LogStore { ); } }) - }); - - let this = Self { - copilot_log_subscription: None, - _copilot_subscription: copilot_subscription, - projects: HashMap::default(), - language_servers: HashMap::default(), - io_tx, - }; - - cx.spawn(async move |this, cx| { - while let Some((server_id, io_kind, message)) = io_rx.next().await { - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| { - this.on_io(server_id, io_kind, &message, cx); - })?; - } - } - anyhow::Ok(()) + .detach(); }) - .detach_and_log_err(cx); - this - } - - pub fn add_project(&mut self, project: &Entity, cx: &mut Context) { - let weak_project = project.downgrade(); - self.projects.insert( - project.downgrade(), - ProjectState { - _subscriptions: [ - cx.observe_release(project, move |this, _, _| { - this.projects.remove(&weak_project); - this.language_servers - .retain(|_, state| state.kind.project() != Some(&weak_project)); - }), - cx.subscribe(project, |this, project, event, cx| { - let server_kind = if project.read(cx).is_via_remote_server() { - LanguageServerKind::Remote { - project: project.downgrade(), - } - } else { - LanguageServerKind::Local { - project: project.downgrade(), - } - }; + }); - match event { - project::Event::LanguageServerAdded(id, name, worktree_id) => { - this.add_language_server( - server_kind, - *id, - Some(name.clone()), - *worktree_id, - project - .read(cx) - .lsp_store() - .read(cx) - .language_server_for_id(*id), - cx, - ); - } - project::Event::LanguageServerRemoved(id) => { - this.remove_language_server(*id, cx); - } - project::Event::LanguageServerLog(id, typ, message) => { - this.add_language_server(server_kind, *id, None, None, None, cx); - match typ { - project::LanguageServerLogType::Log(typ) => { - this.add_language_server_log(*id, *typ, message, cx); - } - project::LanguageServerLogType::Trace(_) => { - this.add_language_server_trace(*id, message, cx); - } - } - } - _ => {} - } - }), - ], - }, - ); - } - - pub(super) fn get_language_server_state( - &mut self, - id: LanguageServerId, - ) -> Option<&mut LanguageServerState> { - self.language_servers.get_mut(&id) - } - - fn add_language_server( - &mut self, - kind: LanguageServerKind, - server_id: LanguageServerId, - name: Option, - worktree_id: Option, - server: Option>, - cx: &mut Context, - ) -> Option<&mut LanguageServerState> { - let server_state = self.language_servers.entry(server_id).or_insert_with(|| { - cx.notify(); - LanguageServerState { - name: None, - worktree_id: None, - kind, - rpc_state: None, - log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), - trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), - trace_level: TraceValue::Off, - log_level: MessageType::LOG, - io_logs_subscription: None, - } + cx.observe_new(move |workspace: &mut Workspace, _, cx| { + log_store.update(cx, |store, cx| { + store.add_project(workspace.project(), cx); }); - if let Some(name) = name { - server_state.name = Some(name); - } - if let Some(worktree_id) = worktree_id { - server_state.worktree_id = Some(worktree_id); - } - - if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) { - let io_tx = self.io_tx.clone(); - let server_id = server.server_id(); - server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| { - io_tx - .unbounded_send((server_id, io_kind, message.to_string())) - .ok(); - })); - } - - Some(server_state) - } - - fn add_language_server_log( - &mut self, - id: LanguageServerId, - typ: MessageType, - message: &str, - cx: &mut Context, - ) -> Option<()> { - let language_server_state = self.get_language_server_state(id)?; - - let log_lines = &mut language_server_state.log_messages; - Self::add_language_server_message( - log_lines, - id, - LogMessage { - message: message.trim_end().to_string(), - typ, - }, - language_server_state.log_level, - LogKind::Logs, - cx, - ); - Some(()) - } - - fn add_language_server_trace( - &mut self, - id: LanguageServerId, - message: &str, - cx: &mut Context, - ) -> Option<()> { - let language_server_state = self.get_language_server_state(id)?; - - let log_lines = &mut language_server_state.trace_messages; - Self::add_language_server_message( - log_lines, - id, - TraceMessage { - message: message.trim().to_string(), - }, - (), - LogKind::Trace, - cx, - ); - Some(()) - } - - fn add_language_server_message( - log_lines: &mut VecDeque, - id: LanguageServerId, - message: T, - current_severity: ::Level, - kind: LogKind, - cx: &mut Context, - ) { - while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - log_lines.pop_front(); - } - let text = message.as_ref().to_string(); - let visible = message.should_include(current_severity); - log_lines.push_back(message); - - if visible { - cx.emit(Event::NewServerLogEntry { id, kind, text }); - cx.notify(); - } - } - - fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context) { - self.language_servers.remove(&id); - cx.notify(); - } - - pub(super) fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque> { - Some(&self.language_servers.get(&server_id)?.log_messages) - } - - pub(super) fn server_trace( - &self, - server_id: LanguageServerId, - ) -> Option<&VecDeque> { - Some(&self.language_servers.get(&server_id)?.trace_messages) - } - - fn server_ids_for_project<'a>( - &'a self, - lookup_project: &'a WeakEntity, - ) -> impl Iterator + 'a { - self.language_servers - .iter() - .filter_map(move |(id, state)| match &state.kind { - LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => { - if project == lookup_project { - Some(*id) - } else { - None - } - } - LanguageServerKind::Global => Some(*id), - }) - } - - fn enable_rpc_trace_for_language_server( - &mut self, - server_id: LanguageServerId, - ) -> Option<&mut LanguageServerRpcState> { - let rpc_state = self - .language_servers - .get_mut(&server_id)? - .rpc_state - .get_or_insert_with(|| LanguageServerRpcState { - rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), - last_message_kind: None, - }); - Some(rpc_state) - } - - pub fn disable_rpc_trace_for_language_server( - &mut self, - server_id: LanguageServerId, - ) -> Option<()> { - self.language_servers.get_mut(&server_id)?.rpc_state.take(); - Some(()) - } - - pub fn has_server_logs(&self, server: &LanguageServerSelector) -> bool { - match server { - LanguageServerSelector::Id(id) => self.language_servers.contains_key(id), - LanguageServerSelector::Name(name) => self - .language_servers - .iter() - .any(|(_, state)| state.name.as_ref() == Some(name)), - } - } - - pub fn open_server_log( - &mut self, - workspace: WeakEntity, - server: LanguageServerSelector, - window: &mut Window, - cx: &mut Context, - ) { - cx.spawn_in(window, async move |log_store, cx| { - let Some(log_store) = log_store.upgrade() else { - return; - }; - workspace - .update_in(cx, |workspace, window, cx| { - let project = workspace.project().clone(); - let tool_log_store = log_store.clone(); - let log_view = get_or_create_tool( - workspace, - SplitDirection::Right, - window, - cx, - move |window, cx| LspLogView::new(project, tool_log_store, window, cx), - ); - log_view.update(cx, |log_view, cx| { - let server_id = match server { - LanguageServerSelector::Id(id) => Some(id), - LanguageServerSelector::Name(name) => { - log_store.read(cx).language_servers.iter().find_map( - |(id, state)| { - if state.name.as_ref() == Some(&name) { - Some(*id) - } else { - None - } - }, - ) - } - }; - if let Some(server_id) = server_id { - log_view.show_logs_for_server(server_id, window, cx); - } - }); - }) - .ok(); - }) - .detach(); - } - - pub fn open_server_trace( - &mut self, - workspace: WeakEntity, - server: LanguageServerSelector, - window: &mut Window, - cx: &mut Context, - ) { - cx.spawn_in(window, async move |log_store, cx| { - let Some(log_store) = log_store.upgrade() else { - return; - }; - workspace - .update_in(cx, |workspace, window, cx| { - let project = workspace.project().clone(); - let tool_log_store = log_store.clone(); - let log_view = get_or_create_tool( - workspace, - SplitDirection::Right, - window, - cx, - move |window, cx| LspLogView::new(project, tool_log_store, window, cx), - ); - log_view.update(cx, |log_view, cx| { - let server_id = match server { - LanguageServerSelector::Id(id) => Some(id), - LanguageServerSelector::Name(name) => { - log_store.read(cx).language_servers.iter().find_map( - |(id, state)| { - if state.name.as_ref() == Some(&name) { - Some(*id) - } else { - None - } - }, - ) - } - }; - if let Some(server_id) = server_id { - log_view.show_rpc_trace_for_server(server_id, window, cx); - } - }); - }) - .ok(); - }) - .detach(); - } - - fn on_io( - &mut self, - language_server_id: LanguageServerId, - io_kind: IoKind, - message: &str, - cx: &mut Context, - ) -> Option<()> { - let is_received = match io_kind { - IoKind::StdOut => true, - IoKind::StdIn => false, - IoKind::StdErr => { - self.add_language_server_log(language_server_id, MessageType::LOG, message, cx); - return Some(()); - } - }; - - let state = self - .get_language_server_state(language_server_id)? - .rpc_state - .as_mut()?; - let kind = if is_received { - MessageKind::Receive - } else { - MessageKind::Send - }; - - let rpc_log_lines = &mut state.rpc_messages; - if state.last_message_kind != Some(kind) { - while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - rpc_log_lines.pop_front(); - } - let line_before_message = match kind { - MessageKind::Send => SEND_LINE, - MessageKind::Receive => RECEIVE_LINE, - }; - rpc_log_lines.push_back(RpcMessage { - message: line_before_message.to_string(), - }); - cx.emit(Event::NewServerLogEntry { - id: language_server_id, - kind: LogKind::Rpc, - text: line_before_message.to_string(), - }); - } - - while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - rpc_log_lines.pop_front(); - } - - let message = message.trim(); - rpc_log_lines.push_back(RpcMessage { - message: message.to_string(), - }); - cx.emit(Event::NewServerLogEntry { - id: language_server_id, - kind: LogKind::Rpc, - text: message.to_string(), + let log_store = log_store.clone(); + workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| { + let log_store = log_store.clone(); + let project = workspace.project().clone(); + get_or_create_tool( + workspace, + SplitDirection::Right, + window, + cx, + move |window, cx| LspLogView::new(project, log_store, window, cx), + ); }); - cx.notify(); - Some(()) - } + }) + .detach(); } impl LspLogView { @@ -751,13 +216,14 @@ impl LspLogView { cx.notify(); }); + let events_subscriptions = cx.subscribe_in( &log_store, window, move |log_view, _, e, window, cx| match e { Event::NewServerLogEntry { id, kind, text } => { if log_view.current_server_id == Some(*id) - && *kind == log_view.active_entry_kind + && LogKind::from_server_log_type(kind) == log_view.active_entry_kind { log_view.editor.update(cx, |editor, cx| { editor.set_read_only(false); @@ -800,7 +266,7 @@ impl LspLogView { window.focus(&log_view.editor.focus_handle(cx)); }); - let mut this = Self { + let mut lsp_log_view = Self { focus_handle, editor, editor_subscriptions, @@ -815,9 +281,9 @@ impl LspLogView { ], }; if let Some(server_id) = server_id { - this.show_logs_for_server(server_id, window, cx); + lsp_log_view.show_logs_for_server(server_id, window, cx); } - this + lsp_log_view } fn editor_for_logs( @@ -838,7 +304,7 @@ impl LspLogView { } fn editor_for_server_info( - server: &LanguageServer, + info: ServerInfo, window: &mut Window, cx: &mut Context, ) -> (Entity, Vec) { @@ -853,22 +319,21 @@ impl LspLogView { * Capabilities: {CAPABILITIES} * Configuration: {CONFIGURATION}", - NAME = server.name(), - ID = server.server_id(), - BINARY = server.binary(), - WORKSPACE_FOLDERS = server - .workspace_folders() - .into_iter() - .filter_map(|path| path - .to_file_path() - .ok() - .map(|path| path.to_string_lossy().into_owned())) - .collect::>() - .join(", "), - CAPABILITIES = serde_json::to_string_pretty(&server.capabilities()) + NAME = info.name, + ID = info.id, + BINARY = info.binary.as_ref().map_or_else( + || "Unknown".to_string(), + |bin| bin.path.as_path().to_string_lossy().to_string() + ), + WORKSPACE_FOLDERS = info.workspace_folders.join(", "), + CAPABILITIES = serde_json::to_string_pretty(&info.capabilities) .unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")), - CONFIGURATION = serde_json::to_string_pretty(server.configuration()) - .unwrap_or_else(|e| format!("Failed to serialize configuration: {e}")), + CONFIGURATION = info + .configuration + .map(|configuration| serde_json::to_string_pretty(&configuration)) + .transpose() + .unwrap_or_else(|e| Some(format!("Failed to serialize configuration: {e}"))) + .unwrap_or_else(|| "Unknown".to_string()), ); let editor = initialize_new_editor(server_info, false, window, cx); let editor_subscription = cx.subscribe( @@ -891,7 +356,9 @@ impl LspLogView { .language_servers .iter() .map(|(server_id, state)| match &state.kind { - LanguageServerKind::Local { .. } | LanguageServerKind::Remote { .. } => { + LanguageServerKind::Local { .. } + | LanguageServerKind::Remote { .. } + | LanguageServerKind::LocalSsh { .. } => { let worktree_root_name = state .worktree_id .and_then(|id| self.project.read(cx).worktree_for_id(id, cx)) @@ -1003,11 +470,17 @@ impl LspLogView { window: &mut Window, cx: &mut Context, ) { + let trace_level = self + .log_store + .update(cx, |this, _| { + Some(this.get_language_server_state(server_id)?.trace_level) + }) + .unwrap_or(TraceValue::Messages); let log_contents = self .log_store .read(cx) .server_trace(server_id) - .map(|v| log_contents(v, ())); + .map(|v| log_contents(v, trace_level)); if let Some(log_contents) = log_contents { self.current_server_id = Some(server_id); self.active_entry_kind = LogKind::Trace; @@ -1025,6 +498,7 @@ impl LspLogView { window: &mut Window, cx: &mut Context, ) { + self.toggle_rpc_trace_for_server(server_id, true, window, cx); let rpc_log = self.log_store.update(cx, |log_store, _| { log_store .enable_rpc_trace_for_language_server(server_id) @@ -1069,12 +543,33 @@ impl LspLogView { window: &mut Window, cx: &mut Context, ) { - self.log_store.update(cx, |log_store, _| { + self.log_store.update(cx, |log_store, cx| { if enabled { log_store.enable_rpc_trace_for_language_server(server_id); } else { log_store.disable_rpc_trace_for_language_server(server_id); } + + if let Some(server_state) = log_store.language_servers.get(&server_id) { + if let LanguageServerKind::Remote { project } = &server_state.kind { + project + .update(cx, |project, cx| { + if let Some((client, project_id)) = + project.lsp_store().read(cx).upstream_client() + { + client + .send(proto::ToggleLspLogs { + project_id, + log_type: proto::toggle_lsp_logs::LogType::Rpc as i32, + server_id: server_id.to_proto(), + enabled, + }) + .log_err(); + } + }) + .ok(); + } + }; }); if !enabled && Some(server_id) == self.current_server_id { self.show_logs_for_server(server_id, window, cx); @@ -1113,13 +608,38 @@ impl LspLogView { window: &mut Window, cx: &mut Context, ) { - let lsp_store = self.project.read(cx).lsp_store(); - let Some(server) = lsp_store.read(cx).language_server_for_id(server_id) else { + let Some(server_info) = self + .project + .read(cx) + .lsp_store() + .update(cx, |lsp_store, _| { + lsp_store + .language_server_for_id(server_id) + .as_ref() + .map(|language_server| ServerInfo::new(language_server)) + .or_else(move || { + let capabilities = + lsp_store.lsp_server_capabilities.get(&server_id)?.clone(); + let name = lsp_store + .language_server_statuses + .get(&server_id) + .map(|status| status.name.clone())?; + Some(ServerInfo { + id: server_id, + capabilities, + binary: None, + name, + workspace_folders: Vec::new(), + configuration: None, + }) + }) + }) + else { return; }; self.current_server_id = Some(server_id); self.active_entry_kind = LogKind::ServerInfo; - let (editor, editor_subscriptions) = Self::editor_for_server_info(&server, window, cx); + let (editor, editor_subscriptions) = Self::editor_for_server_info(server_info, window, cx); self.editor = editor; self.editor_subscriptions = editor_subscriptions; cx.notify(); @@ -1416,7 +936,6 @@ impl Render for LspLogToolbarItemView { let view_selector = current_server.map(|server| { let server_id = server.server_id; - let is_remote = server.server_kind.is_remote(); let rpc_trace_enabled = server.rpc_trace_enabled; let log_view = log_view.clone(); PopoverMenu::new("LspViewSelector") @@ -1438,55 +957,53 @@ impl Render for LspLogToolbarItemView { view.show_logs_for_server(server_id, window, cx); }), ) - .when(!is_remote, |this| { - this.entry( - SERVER_TRACE, - None, - window.handler_for(&log_view, move |view, window, cx| { - view.show_trace_for_server(server_id, window, cx); - }), - ) - .custom_entry( - { - let log_toolbar_view = log_toolbar_view.clone(); - move |window, _| { - h_flex() - .w_full() - .justify_between() - .child(Label::new(RPC_MESSAGES)) - .child( - div().child( - Checkbox::new( - "LspLogEnableRpcTrace", - if rpc_trace_enabled { + .entry( + SERVER_TRACE, + None, + window.handler_for(&log_view, move |view, window, cx| { + view.show_trace_for_server(server_id, window, cx); + }), + ) + .custom_entry( + { + let log_toolbar_view = log_toolbar_view.clone(); + move |window, _| { + h_flex() + .w_full() + .justify_between() + .child(Label::new(RPC_MESSAGES)) + .child( + div().child( + Checkbox::new( + "LspLogEnableRpcTrace", + if rpc_trace_enabled { + ToggleState::Selected + } else { + ToggleState::Unselected + }, + ) + .on_click(window.listener_for( + &log_toolbar_view, + move |view, selection, window, cx| { + let enabled = matches!( + selection, ToggleState::Selected - } else { - ToggleState::Unselected - }, - ) - .on_click(window.listener_for( - &log_toolbar_view, - move |view, selection, window, cx| { - let enabled = matches!( - selection, - ToggleState::Selected - ); - view.toggle_rpc_logging_for_server( - server_id, enabled, window, cx, - ); - cx.stop_propagation(); - }, - )), - ), - ) - .into_any_element() - } - }, - window.handler_for(&log_view, move |view, window, cx| { - view.show_rpc_trace_for_server(server_id, window, cx); - }), - ) - }) + ); + view.toggle_rpc_logging_for_server( + server_id, enabled, window, cx, + ); + cx.stop_propagation(); + }, + )), + ), + ) + .into_any_element() + } + }, + window.handler_for(&log_view, move |view, window, cx| { + view.show_rpc_trace_for_server(server_id, window, cx); + }), + ) .entry( SERVER_INFO, None, @@ -1696,12 +1213,6 @@ const SERVER_LOGS: &str = "Server Logs"; const SERVER_TRACE: &str = "Server Trace"; const SERVER_INFO: &str = "Server Info"; -impl Default for LspLogToolbarItemView { - fn default() -> Self { - Self::new() - } -} - impl LspLogToolbarItemView { pub fn new() -> Self { Self { @@ -1734,14 +1245,35 @@ impl LspLogToolbarItemView { } } -pub enum Event { - NewServerLogEntry { - id: LanguageServerId, - kind: LogKind, - text: String, - }, +struct ServerInfo { + id: LanguageServerId, + capabilities: lsp::ServerCapabilities, + binary: Option, + name: LanguageServerName, + workspace_folders: Vec, + configuration: Option, +} + +impl ServerInfo { + fn new(server: &LanguageServer) -> Self { + Self { + id: server.server_id(), + capabilities: server.capabilities(), + binary: Some(server.binary().clone()), + name: server.name(), + workspace_folders: server + .workspace_folders() + .into_iter() + .filter_map(|path| { + path.to_file_path() + .ok() + .map(|path| path.to_string_lossy().into_owned()) + }) + .collect::>(), + configuration: Some(server.configuration().clone()), + } + } } -impl EventEmitter for LogStore {} impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} diff --git a/crates/language_tools/src/lsp_log_tests.rs b/crates/language_tools/src/lsp_log_view_tests.rs similarity index 91% rename from crates/language_tools/src/lsp_log_tests.rs rename to crates/language_tools/src/lsp_log_view_tests.rs index ad2b653fdcfd4dc228cac58da7ed15f844b4bb26..bfd093e3db1c1bc0dc04b111d2072339f1314b8e 100644 --- a/crates/language_tools/src/lsp_log_tests.rs +++ b/crates/language_tools/src/lsp_log_view_tests.rs @@ -1,20 +1,22 @@ use std::sync::Arc; -use crate::lsp_log::LogMenuItem; +use crate::lsp_log_view::LogMenuItem; use super::*; use futures::StreamExt; use gpui::{AppContext as _, SemanticVersion, TestAppContext, VisualTestContext}; use language::{FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, tree_sitter_rust}; use lsp::LanguageServerName; -use lsp_log::LogKind; -use project::{FakeFs, Project}; +use project::{ + FakeFs, Project, + lsp_store::log_store::{LanguageServerKind, LogKind, LogStore}, +}; use serde_json::json; use settings::SettingsStore; use util::path; #[gpui::test] -async fn test_lsp_logs(cx: &mut TestAppContext) { +async fn test_lsp_log_view(cx: &mut TestAppContext) { zlog::init_test(); init_test(cx); @@ -51,7 +53,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { }, ); - let log_store = cx.new(LogStore::new); + let log_store = cx.new(|cx| LogStore::new(true, cx)); log_store.update(cx, |store, cx| store.add_project(&project, cx)); let _rust_buffer = project @@ -94,7 +96,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { rpc_trace_enabled: false, selected_entry: LogKind::Logs, trace_level: lsp::TraceValue::Off, - server_kind: lsp_log::LanguageServerKind::Local { + server_kind: LanguageServerKind::Local { project: project.downgrade() } }] diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index ad9d0abf405b18f9048030621e960251057588de..d11e5679968095d0a6541ab320ac3476dc7f794a 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11,18 +11,22 @@ //! Most of the interesting work happens at the local layer, as bulk of the complexity is with managing the lifecycle of language servers. The actual implementation of the LSP protocol is handled by [`lsp`] crate. pub mod clangd_ext; pub mod json_language_server_ext; +pub mod log_store; pub mod lsp_ext_command; pub mod rust_analyzer_ext; use crate::{ CodeAction, ColorPresentation, Completion, CompletionResponse, CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, LocationLink, LspAction, LspPullDiagnostics, - ManifestProvidersStore, ProjectItem, ProjectPath, ProjectTransaction, PulledDiagnostics, - ResolveState, Symbol, + ManifestProvidersStore, Project, ProjectItem, ProjectPath, ProjectTransaction, + PulledDiagnostics, ResolveState, Symbol, buffer_store::{BufferStore, BufferStoreEvent}, environment::ProjectEnvironment, lsp_command::{self, *}, - lsp_store, + lsp_store::{ + self, + log_store::{GlobalLogStore, LanguageServerKind}, + }, manifest_tree::{ LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, ManifestQueryDelegate, ManifestTree, @@ -977,7 +981,9 @@ impl LocalLspStore { this.update(&mut cx, |_, cx| { cx.emit(LspStoreEvent::LanguageServerLog( server_id, - LanguageServerLogType::Trace(params.verbose), + LanguageServerLogType::Trace { + verbose_info: params.verbose, + }, params.message, )); }) @@ -3482,13 +3488,13 @@ pub struct LspStore { buffer_store: Entity, worktree_store: Entity, pub languages: Arc, - language_server_statuses: BTreeMap, + pub language_server_statuses: BTreeMap, active_entry: Option, _maintain_workspace_config: (Task>, watch::Sender<()>), _maintain_buffer_languages: Task<()>, diagnostic_summaries: HashMap, HashMap>>, - pub(super) lsp_server_capabilities: HashMap, + pub lsp_server_capabilities: HashMap, lsp_document_colors: HashMap, lsp_code_lens: HashMap, running_lsp_requests: HashMap>)>, @@ -3565,6 +3571,7 @@ pub struct LanguageServerStatus { pub pending_work: BTreeMap, pub has_pending_diagnostic_updates: bool, progress_tokens: HashSet, + pub worktree: Option, } #[derive(Clone, Debug)] @@ -7483,7 +7490,7 @@ impl LspStore { server: Some(proto::LanguageServer { id: server_id.to_proto(), name: status.name.to_string(), - worktree_id: None, + worktree_id: status.worktree.map(|id| id.to_proto()), }), capabilities: serde_json::to_string(&server.capabilities()) .expect("serializing server LSP capabilities"), @@ -7508,9 +7515,15 @@ impl LspStore { pub(crate) fn set_language_server_statuses_from_proto( &mut self, + project: WeakEntity, language_servers: Vec, server_capabilities: Vec, + cx: &mut Context, ) { + let lsp_logs = cx + .try_global::() + .map(|lsp_store| lsp_store.0.clone()); + self.language_server_statuses = language_servers .into_iter() .zip(server_capabilities) @@ -7520,13 +7533,34 @@ impl LspStore { self.lsp_server_capabilities .insert(server_id, server_capabilities); } + + let name = LanguageServerName::from_proto(server.name); + let worktree = server.worktree_id.map(WorktreeId::from_proto); + + if let Some(lsp_logs) = &lsp_logs { + lsp_logs.update(cx, |lsp_logs, cx| { + lsp_logs.add_language_server( + // Only remote clients get their language servers set from proto + LanguageServerKind::Remote { + project: project.clone(), + }, + server_id, + Some(name.clone()), + worktree, + None, + cx, + ); + }); + } + ( server_id, LanguageServerStatus { - name: LanguageServerName::from_proto(server.name), + name, pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), + worktree, }, ) }) @@ -8892,6 +8926,7 @@ impl LspStore { pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), + worktree: server.worktree_id.map(WorktreeId::from_proto), }, ); cx.emit(LspStoreEvent::LanguageServerAdded( @@ -10905,6 +10940,7 @@ impl LspStore { pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), + worktree: Some(key.worktree_id), }, ); @@ -12190,6 +12226,14 @@ impl LspStore { let data = self.lsp_code_lens.get_mut(&buffer_id)?; Some(data.update.take()?.1) } + + pub fn downstream_client(&self) -> Option<(AnyProtoClient, u64)> { + self.downstream_client.clone() + } + + pub fn worktree_store(&self) -> Entity { + self.worktree_store.clone() + } } // Registration with registerOptions as null, should fallback to true. @@ -12699,45 +12743,69 @@ impl PartialEq for LanguageServerPromptRequest { #[derive(Clone, Debug, PartialEq)] pub enum LanguageServerLogType { Log(MessageType), - Trace(Option), + Trace { verbose_info: Option }, + Rpc { received: bool }, } impl LanguageServerLogType { pub fn to_proto(&self) -> proto::language_server_log::LogType { match self { Self::Log(log_type) => { - let message_type = match *log_type { - MessageType::ERROR => 1, - MessageType::WARNING => 2, - MessageType::INFO => 3, - MessageType::LOG => 4, + use proto::log_message::LogLevel; + let level = match *log_type { + MessageType::ERROR => LogLevel::Error, + MessageType::WARNING => LogLevel::Warning, + MessageType::INFO => LogLevel::Info, + MessageType::LOG => LogLevel::Log, other => { - log::warn!("Unknown lsp log message type: {:?}", other); - 4 + log::warn!("Unknown lsp log message type: {other:?}"); + LogLevel::Log } }; - proto::language_server_log::LogType::LogMessageType(message_type) + proto::language_server_log::LogType::Log(proto::LogMessage { + level: level as i32, + }) } - Self::Trace(message) => { - proto::language_server_log::LogType::LogTrace(proto::LspLogTrace { - message: message.clone(), + Self::Trace { verbose_info } => { + proto::language_server_log::LogType::Trace(proto::TraceMessage { + verbose_info: verbose_info.to_owned(), }) } + Self::Rpc { received } => { + let kind = if *received { + proto::rpc_message::Kind::Received + } else { + proto::rpc_message::Kind::Sent + }; + let kind = kind as i32; + proto::language_server_log::LogType::Rpc(proto::RpcMessage { kind }) + } } } pub fn from_proto(log_type: proto::language_server_log::LogType) -> Self { + use proto::log_message::LogLevel; + use proto::rpc_message; match log_type { - proto::language_server_log::LogType::LogMessageType(message_type) => { - Self::Log(match message_type { - 1 => MessageType::ERROR, - 2 => MessageType::WARNING, - 3 => MessageType::INFO, - 4 => MessageType::LOG, - _ => MessageType::LOG, - }) - } - proto::language_server_log::LogType::LogTrace(trace) => Self::Trace(trace.message), + proto::language_server_log::LogType::Log(message_type) => Self::Log( + match LogLevel::from_i32(message_type.level).unwrap_or(LogLevel::Log) { + LogLevel::Error => MessageType::ERROR, + LogLevel::Warning => MessageType::WARNING, + LogLevel::Info => MessageType::INFO, + LogLevel::Log => MessageType::LOG, + }, + ), + proto::language_server_log::LogType::Trace(trace_message) => Self::Trace { + verbose_info: trace_message.verbose_info, + }, + proto::language_server_log::LogType::Rpc(message) => Self::Rpc { + received: match rpc_message::Kind::from_i32(message.kind) + .unwrap_or(rpc_message::Kind::Received) + { + rpc_message::Kind::Received => true, + rpc_message::Kind::Sent => false, + }, + }, } } } diff --git a/crates/project/src/lsp_store/log_store.rs b/crates/project/src/lsp_store/log_store.rs new file mode 100644 index 0000000000000000000000000000000000000000..1fbdb494a303b47bea181c5046e51f3c0b21c5c1 --- /dev/null +++ b/crates/project/src/lsp_store/log_store.rs @@ -0,0 +1,704 @@ +use std::{collections::VecDeque, sync::Arc}; + +use collections::HashMap; +use futures::{StreamExt, channel::mpsc}; +use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, Subscription, WeakEntity}; +use lsp::{ + IoKind, LanguageServer, LanguageServerId, LanguageServerName, LanguageServerSelector, + MessageType, TraceValue, +}; +use rpc::proto; +use settings::WorktreeId; + +use crate::{LanguageServerLogType, LspStore, Project, ProjectItem as _}; + +const SEND_LINE: &str = "\n// Send:"; +const RECEIVE_LINE: &str = "\n// Receive:"; +const MAX_STORED_LOG_ENTRIES: usize = 2000; + +const RPC_MESSAGES: &str = "RPC Messages"; +const SERVER_LOGS: &str = "Server Logs"; +const SERVER_TRACE: &str = "Server Trace"; +const SERVER_INFO: &str = "Server Info"; + +pub fn init(store_logs: bool, cx: &mut App) -> Entity { + let log_store = cx.new(|cx| LogStore::new(store_logs, cx)); + cx.set_global(GlobalLogStore(log_store.clone())); + log_store +} + +pub struct GlobalLogStore(pub Entity); + +impl Global for GlobalLogStore {} + +#[derive(Debug)] +pub enum Event { + NewServerLogEntry { + id: LanguageServerId, + kind: LanguageServerLogType, + text: String, + }, +} + +impl EventEmitter for LogStore {} + +pub struct LogStore { + store_logs: bool, + projects: HashMap, ProjectState>, + pub copilot_log_subscription: Option, + pub language_servers: HashMap, + io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>, +} + +struct ProjectState { + _subscriptions: [Subscription; 2], +} + +pub trait Message: AsRef { + type Level: Copy + std::fmt::Debug; + fn should_include(&self, _: Self::Level) -> bool { + true + } +} + +#[derive(Debug)] +pub struct LogMessage { + message: String, + typ: MessageType, +} + +impl AsRef for LogMessage { + fn as_ref(&self) -> &str { + &self.message + } +} + +impl Message for LogMessage { + type Level = MessageType; + + fn should_include(&self, level: Self::Level) -> bool { + match (self.typ, level) { + (MessageType::ERROR, _) => true, + (_, MessageType::ERROR) => false, + (MessageType::WARNING, _) => true, + (_, MessageType::WARNING) => false, + (MessageType::INFO, _) => true, + (_, MessageType::INFO) => false, + _ => true, + } + } +} + +#[derive(Debug)] +pub struct TraceMessage { + message: String, + is_verbose: bool, +} + +impl AsRef for TraceMessage { + fn as_ref(&self) -> &str { + &self.message + } +} + +impl Message for TraceMessage { + type Level = TraceValue; + + fn should_include(&self, level: Self::Level) -> bool { + match level { + TraceValue::Off => false, + TraceValue::Messages => !self.is_verbose, + TraceValue::Verbose => true, + } + } +} + +#[derive(Debug)] +pub struct RpcMessage { + message: String, +} + +impl AsRef for RpcMessage { + fn as_ref(&self) -> &str { + &self.message + } +} + +impl Message for RpcMessage { + type Level = (); +} + +pub struct LanguageServerState { + pub name: Option, + pub worktree_id: Option, + pub kind: LanguageServerKind, + log_messages: VecDeque, + trace_messages: VecDeque, + pub rpc_state: Option, + pub trace_level: TraceValue, + pub log_level: MessageType, + io_logs_subscription: Option, +} + +impl std::fmt::Debug for LanguageServerState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LanguageServerState") + .field("name", &self.name) + .field("worktree_id", &self.worktree_id) + .field("kind", &self.kind) + .field("log_messages", &self.log_messages) + .field("trace_messages", &self.trace_messages) + .field("rpc_state", &self.rpc_state) + .field("trace_level", &self.trace_level) + .field("log_level", &self.log_level) + .finish_non_exhaustive() + } +} + +#[derive(PartialEq, Clone)] +pub enum LanguageServerKind { + Local { project: WeakEntity }, + Remote { project: WeakEntity }, + LocalSsh { lsp_store: WeakEntity }, + Global, +} + +impl std::fmt::Debug for LanguageServerKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"), + LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"), + LanguageServerKind::LocalSsh { .. } => write!(f, "LanguageServerKind::LocalSsh"), + LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"), + } + } +} + +impl LanguageServerKind { + pub fn project(&self) -> Option<&WeakEntity> { + match self { + Self::Local { project } => Some(project), + Self::Remote { project } => Some(project), + Self::LocalSsh { .. } => None, + Self::Global { .. } => None, + } + } +} + +#[derive(Debug)] +pub struct LanguageServerRpcState { + pub rpc_messages: VecDeque, + last_message_kind: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum MessageKind { + Send, + Receive, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum LogKind { + Rpc, + Trace, + #[default] + Logs, + ServerInfo, +} + +impl LogKind { + pub fn from_server_log_type(log_type: &LanguageServerLogType) -> Self { + match log_type { + LanguageServerLogType::Log(_) => Self::Logs, + LanguageServerLogType::Trace { .. } => Self::Trace, + LanguageServerLogType::Rpc { .. } => Self::Rpc, + } + } + + pub fn label(&self) -> &'static str { + match self { + LogKind::Rpc => RPC_MESSAGES, + LogKind::Trace => SERVER_TRACE, + LogKind::Logs => SERVER_LOGS, + LogKind::ServerInfo => SERVER_INFO, + } + } +} + +impl LogStore { + pub fn new(store_logs: bool, cx: &mut Context) -> Self { + let (io_tx, mut io_rx) = mpsc::unbounded(); + + let log_store = Self { + projects: HashMap::default(), + language_servers: HashMap::default(), + copilot_log_subscription: None, + store_logs, + io_tx, + }; + cx.spawn(async move |log_store, cx| { + while let Some((server_id, io_kind, message)) = io_rx.next().await { + if let Some(log_store) = log_store.upgrade() { + log_store.update(cx, |log_store, cx| { + log_store.on_io(server_id, io_kind, &message, cx); + })?; + } + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + log_store + } + + pub fn add_project(&mut self, project: &Entity, cx: &mut Context) { + let weak_project = project.downgrade(); + self.projects.insert( + project.downgrade(), + ProjectState { + _subscriptions: [ + cx.observe_release(project, move |this, _, _| { + this.projects.remove(&weak_project); + this.language_servers + .retain(|_, state| state.kind.project() != Some(&weak_project)); + }), + cx.subscribe(project, move |log_store, project, event, cx| { + let server_kind = if project.read(cx).is_local() { + LanguageServerKind::Local { + project: project.downgrade(), + } + } else { + LanguageServerKind::Remote { + project: project.downgrade(), + } + }; + match event { + crate::Event::LanguageServerAdded(id, name, worktree_id) => { + log_store.add_language_server( + server_kind, + *id, + Some(name.clone()), + *worktree_id, + project + .read(cx) + .lsp_store() + .read(cx) + .language_server_for_id(*id), + cx, + ); + } + crate::Event::LanguageServerBufferRegistered { + server_id, + buffer_id, + name, + .. + } => { + let worktree_id = project + .read(cx) + .buffer_for_id(*buffer_id, cx) + .and_then(|buffer| { + Some(buffer.read(cx).project_path(cx)?.worktree_id) + }); + let name = name.clone().or_else(|| { + project + .read(cx) + .lsp_store() + .read(cx) + .language_server_statuses + .get(server_id) + .map(|status| status.name.clone()) + }); + log_store.add_language_server( + server_kind, + *server_id, + name, + worktree_id, + None, + cx, + ); + } + crate::Event::LanguageServerRemoved(id) => { + log_store.remove_language_server(*id, cx); + } + crate::Event::LanguageServerLog(id, typ, message) => { + log_store.add_language_server( + server_kind, + *id, + None, + None, + None, + cx, + ); + match typ { + crate::LanguageServerLogType::Log(typ) => { + log_store.add_language_server_log(*id, *typ, message, cx); + } + crate::LanguageServerLogType::Trace { verbose_info } => { + log_store.add_language_server_trace( + *id, + message, + verbose_info.clone(), + cx, + ); + } + crate::LanguageServerLogType::Rpc { received } => { + let kind = if *received { + MessageKind::Receive + } else { + MessageKind::Send + }; + log_store.add_language_server_rpc(*id, kind, message, cx); + } + } + } + crate::Event::ToggleLspLogs { server_id, enabled } => { + // we do not support any other log toggling yet + if *enabled { + log_store.enable_rpc_trace_for_language_server(*server_id); + } else { + log_store.disable_rpc_trace_for_language_server(*server_id); + } + } + _ => {} + } + }), + ], + }, + ); + } + + pub fn get_language_server_state( + &mut self, + id: LanguageServerId, + ) -> Option<&mut LanguageServerState> { + self.language_servers.get_mut(&id) + } + + pub fn add_language_server( + &mut self, + kind: LanguageServerKind, + server_id: LanguageServerId, + name: Option, + worktree_id: Option, + server: Option>, + cx: &mut Context, + ) -> Option<&mut LanguageServerState> { + let server_state = self.language_servers.entry(server_id).or_insert_with(|| { + cx.notify(); + LanguageServerState { + name: None, + worktree_id: None, + kind, + rpc_state: None, + log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), + trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), + trace_level: TraceValue::Off, + log_level: MessageType::LOG, + io_logs_subscription: None, + } + }); + + if let Some(name) = name { + server_state.name = Some(name); + } + if let Some(worktree_id) = worktree_id { + server_state.worktree_id = Some(worktree_id); + } + + if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) { + let io_tx = self.io_tx.clone(); + let server_id = server.server_id(); + server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| { + io_tx + .unbounded_send((server_id, io_kind, message.to_string())) + .ok(); + })); + } + + Some(server_state) + } + + pub fn add_language_server_log( + &mut self, + id: LanguageServerId, + typ: MessageType, + message: &str, + cx: &mut Context, + ) -> Option<()> { + let store_logs = self.store_logs; + let language_server_state = self.get_language_server_state(id)?; + + let log_lines = &mut language_server_state.log_messages; + let message = message.trim_end().to_string(); + if !store_logs { + // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway + self.emit_event( + Event::NewServerLogEntry { + id, + kind: LanguageServerLogType::Log(typ), + text: message, + }, + cx, + ); + } else if let Some(new_message) = Self::push_new_message( + log_lines, + LogMessage { message, typ }, + language_server_state.log_level, + ) { + self.emit_event( + Event::NewServerLogEntry { + id, + kind: LanguageServerLogType::Log(typ), + text: new_message, + }, + cx, + ); + } + Some(()) + } + + fn add_language_server_trace( + &mut self, + id: LanguageServerId, + message: &str, + verbose_info: Option, + cx: &mut Context, + ) -> Option<()> { + let store_logs = self.store_logs; + let language_server_state = self.get_language_server_state(id)?; + + let log_lines = &mut language_server_state.trace_messages; + if !store_logs { + // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway + self.emit_event( + Event::NewServerLogEntry { + id, + kind: LanguageServerLogType::Trace { verbose_info }, + text: message.trim().to_string(), + }, + cx, + ); + } else if let Some(new_message) = Self::push_new_message( + log_lines, + TraceMessage { + message: message.trim().to_string(), + is_verbose: false, + }, + TraceValue::Messages, + ) { + if let Some(verbose_message) = verbose_info.as_ref() { + Self::push_new_message( + log_lines, + TraceMessage { + message: verbose_message.clone(), + is_verbose: true, + }, + TraceValue::Verbose, + ); + } + self.emit_event( + Event::NewServerLogEntry { + id, + kind: LanguageServerLogType::Trace { verbose_info }, + text: new_message, + }, + cx, + ); + } + Some(()) + } + + fn push_new_message( + log_lines: &mut VecDeque, + message: T, + current_severity: ::Level, + ) -> Option { + while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { + log_lines.pop_front(); + } + let visible = message.should_include(current_severity); + + let visible_message = visible.then(|| message.as_ref().to_string()); + log_lines.push_back(message); + visible_message + } + + fn add_language_server_rpc( + &mut self, + language_server_id: LanguageServerId, + kind: MessageKind, + message: &str, + cx: &mut Context<'_, Self>, + ) { + let store_logs = self.store_logs; + let Some(state) = self + .get_language_server_state(language_server_id) + .and_then(|state| state.rpc_state.as_mut()) + else { + return; + }; + + let received = kind == MessageKind::Receive; + let rpc_log_lines = &mut state.rpc_messages; + if state.last_message_kind != Some(kind) { + while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { + rpc_log_lines.pop_front(); + } + let line_before_message = match kind { + MessageKind::Send => SEND_LINE, + MessageKind::Receive => RECEIVE_LINE, + }; + if store_logs { + rpc_log_lines.push_back(RpcMessage { + message: line_before_message.to_string(), + }); + } + // Do not send a synthetic message over the wire, it will be derived from the actual RPC message + cx.emit(Event::NewServerLogEntry { + id: language_server_id, + kind: LanguageServerLogType::Rpc { received }, + text: line_before_message.to_string(), + }); + } + + while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { + rpc_log_lines.pop_front(); + } + + if store_logs { + rpc_log_lines.push_back(RpcMessage { + message: message.trim().to_owned(), + }); + } + + self.emit_event( + Event::NewServerLogEntry { + id: language_server_id, + kind: LanguageServerLogType::Rpc { received }, + text: message.to_owned(), + }, + cx, + ); + } + + pub fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context) { + self.language_servers.remove(&id); + cx.notify(); + } + + pub fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque> { + Some(&self.language_servers.get(&server_id)?.log_messages) + } + + pub fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque> { + Some(&self.language_servers.get(&server_id)?.trace_messages) + } + + pub fn server_ids_for_project<'a>( + &'a self, + lookup_project: &'a WeakEntity, + ) -> impl Iterator + 'a { + self.language_servers + .iter() + .filter_map(move |(id, state)| match &state.kind { + LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => { + if project == lookup_project { + Some(*id) + } else { + None + } + } + LanguageServerKind::Global | LanguageServerKind::LocalSsh { .. } => Some(*id), + }) + } + + pub fn enable_rpc_trace_for_language_server( + &mut self, + server_id: LanguageServerId, + ) -> Option<&mut LanguageServerRpcState> { + let rpc_state = self + .language_servers + .get_mut(&server_id)? + .rpc_state + .get_or_insert_with(|| LanguageServerRpcState { + rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), + last_message_kind: None, + }); + Some(rpc_state) + } + + pub fn disable_rpc_trace_for_language_server( + &mut self, + server_id: LanguageServerId, + ) -> Option<()> { + self.language_servers.get_mut(&server_id)?.rpc_state.take(); + Some(()) + } + + pub fn has_server_logs(&self, server: &LanguageServerSelector) -> bool { + match server { + LanguageServerSelector::Id(id) => self.language_servers.contains_key(id), + LanguageServerSelector::Name(name) => self + .language_servers + .iter() + .any(|(_, state)| state.name.as_ref() == Some(name)), + } + } + + fn on_io( + &mut self, + language_server_id: LanguageServerId, + io_kind: IoKind, + message: &str, + cx: &mut Context, + ) -> Option<()> { + let is_received = match io_kind { + IoKind::StdOut => true, + IoKind::StdIn => false, + IoKind::StdErr => { + self.add_language_server_log(language_server_id, MessageType::LOG, message, cx); + return Some(()); + } + }; + + let kind = if is_received { + MessageKind::Receive + } else { + MessageKind::Send + }; + + self.add_language_server_rpc(language_server_id, kind, message, cx); + cx.notify(); + Some(()) + } + + fn emit_event(&mut self, e: Event, cx: &mut Context) { + match &e { + Event::NewServerLogEntry { id, kind, text } => { + if let Some(state) = self.get_language_server_state(*id) { + let downstream_client = match &state.kind { + LanguageServerKind::Remote { project } + | LanguageServerKind::Local { project } => project + .upgrade() + .map(|project| project.read(cx).lsp_store()), + LanguageServerKind::LocalSsh { lsp_store } => lsp_store.upgrade(), + LanguageServerKind::Global => None, + } + .and_then(|lsp_store| lsp_store.read(cx).downstream_client()); + if let Some((client, project_id)) = downstream_client { + client + .send(proto::LanguageServerLog { + project_id, + language_server_id: id.to_proto(), + message: text.clone(), + log_type: Some(kind.to_proto()), + }) + .ok(); + } + } + } + } + + cx.emit(e); + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9e3900198cbc9aa4845428235196763511c0751c..86b2e08d629c68d60e241b677288750d32a4bcbd 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -280,6 +280,11 @@ pub enum Event { server_id: LanguageServerId, buffer_id: BufferId, buffer_abs_path: PathBuf, + name: Option, + }, + ToggleLspLogs { + server_id: LanguageServerId, + enabled: bool, }, Toast { notification_id: SharedString, @@ -1001,6 +1006,7 @@ impl Project { client.add_entity_request_handler(Self::handle_open_buffer_by_path); client.add_entity_request_handler(Self::handle_open_new_buffer); client.add_entity_message_handler(Self::handle_create_buffer_for_peer); + client.add_entity_message_handler(Self::handle_toggle_lsp_logs); WorktreeStore::init(&client); BufferStore::init(&client); @@ -1475,7 +1481,7 @@ impl Project { })?; let lsp_store = cx.new(|cx| { - let mut lsp_store = LspStore::new_remote( + LspStore::new_remote( buffer_store.clone(), worktree_store.clone(), languages.clone(), @@ -1483,12 +1489,7 @@ impl Project { remote_id, fs.clone(), cx, - ); - lsp_store.set_language_server_statuses_from_proto( - response.payload.language_servers, - response.payload.language_server_capabilities, - ); - lsp_store + ) })?; let task_store = cx.new(|cx| { @@ -1522,7 +1523,7 @@ impl Project { ) })?; - let this = cx.new(|cx| { + let project = cx.new(|cx| { let replica_id = response.payload.replica_id as ReplicaId; let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx); @@ -1553,7 +1554,7 @@ impl Project { cx.subscribe(&dap_store, Self::on_dap_store_event).detach(); - let mut this = Self { + let mut project = Self { buffer_ordered_messages_tx: tx, buffer_store: buffer_store.clone(), image_store, @@ -1596,13 +1597,25 @@ impl Project { toolchain_store: None, agent_location: None, }; - this.set_role(role, cx); + project.set_role(role, cx); for worktree in worktrees { - this.add_worktree(&worktree, cx); + project.add_worktree(&worktree, cx); } - this + project })?; + let weak_project = project.downgrade(); + lsp_store + .update(&mut cx, |lsp_store, cx| { + lsp_store.set_language_server_statuses_from_proto( + weak_project, + response.payload.language_servers, + response.payload.language_server_capabilities, + cx, + ); + }) + .ok(); + let subscriptions = subscriptions .into_iter() .map(|s| match s { @@ -1618,7 +1631,7 @@ impl Project { EntitySubscription::SettingsObserver(subscription) => { subscription.set_entity(&settings_observer, &cx) } - EntitySubscription::Project(subscription) => subscription.set_entity(&this, &cx), + EntitySubscription::Project(subscription) => subscription.set_entity(&project, &cx), EntitySubscription::LspStore(subscription) => { subscription.set_entity(&lsp_store, &cx) } @@ -1638,13 +1651,13 @@ impl Project { .update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))? .await?; - this.update(&mut cx, |this, cx| { + project.update(&mut cx, |this, cx| { this.set_collaborators_from_proto(response.payload.collaborators, cx)?; this.client_subscriptions.extend(subscriptions); anyhow::Ok(()) })??; - Ok(this) + Ok(project) } fn new_search_history() -> SearchHistory { @@ -2315,10 +2328,14 @@ impl Project { self.join_project_response_message_id = message_id; self.set_worktrees_from_proto(message.worktrees, cx)?; self.set_collaborators_from_proto(message.collaborators, cx)?; - self.lsp_store.update(cx, |lsp_store, _| { + + let project = cx.weak_entity(); + self.lsp_store.update(cx, |lsp_store, cx| { lsp_store.set_language_server_statuses_from_proto( + project, message.language_servers, message.language_server_capabilities, + cx, ) }); self.enqueue_buffer_ordered_message(BufferOrderedMessage::Resync) @@ -2971,6 +2988,7 @@ impl Project { buffer_id, server_id: *language_server_id, buffer_abs_path: PathBuf::from(&update.buffer_abs_path), + name: name.clone(), }); } } @@ -4697,6 +4715,20 @@ impl Project { })? } + async fn handle_toggle_lsp_logs( + project: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result<()> { + project.update(&mut cx, |_, cx| { + cx.emit(Event::ToggleLspLogs { + server_id: LanguageServerId::from_proto(envelope.payload.server_id), + enabled: envelope.payload.enabled, + }) + })?; + Ok(()) + } + async fn handle_synchronize_buffers( this: Entity, envelope: TypedEnvelope, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index f49713d2080d48c9576118bff5fcd241f092234c..a8f911883d619214a35f3ef5d80f83d6dc1b3894 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1951,6 +1951,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC server_id: LanguageServerId(1), buffer_id, buffer_abs_path: PathBuf::from(path!("/dir/a.rs")), + name: Some(fake_server.server.name()) } ); assert_eq!( diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 473ef5c38cc6f401a05556c1f02271e83bd8fa97..16f6217b29d50a4a2eb9198565f688335c218802 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -610,11 +610,36 @@ message ServerMetadataUpdated { message LanguageServerLog { uint64 project_id = 1; uint64 language_server_id = 2; + string message = 3; oneof log_type { - uint32 log_message_type = 3; - LspLogTrace log_trace = 4; + LogMessage log = 4; + TraceMessage trace = 5; + RpcMessage rpc = 6; + } +} + +message LogMessage { + LogLevel level = 1; + + enum LogLevel { + LOG = 0; + INFO = 1; + WARNING = 2; + ERROR = 3; + } +} + +message TraceMessage { + optional string verbose_info = 1; +} + +message RpcMessage { + Kind kind = 1; + + enum Kind { + RECEIVED = 0; + SENT = 1; } - string message = 5; } message LspLogTrace { @@ -932,3 +957,16 @@ message MultiLspQuery { message MultiLspQueryResponse { repeated LspResponse responses = 1; } + +message ToggleLspLogs { + uint64 project_id = 1; + LogType log_type = 2; + uint64 server_id = 3; + bool enabled = 4; + + enum LogType { + LOG = 0; + TRACE = 1; + RPC = 2; + } +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 70689bcd6306195fce0d5c6449bf3dd9f5d43539..2222bdec082759cb75ffcdb2c7a95435f36eba11 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -396,7 +396,8 @@ message Envelope { GitCloneResponse git_clone_response = 364; LspQuery lsp_query = 365; - LspQueryResponse lsp_query_response = 366; // current max + LspQueryResponse lsp_query_response = 366; + ToggleLspLogs toggle_lsp_logs = 367; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index e17ec5203bd5b7bcab03c6461c343156116cc563..04495fb898b1d9bdbf229bb69e1e44b8afa6d1fb 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -312,7 +312,8 @@ messages!( (GetDefaultBranch, Background), (GetDefaultBranchResponse, Background), (GitClone, Background), - (GitCloneResponse, Background) + (GitCloneResponse, Background), + (ToggleLspLogs, Background), ); request_messages!( @@ -481,7 +482,8 @@ request_messages!( (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse), (PullWorkspaceDiagnostics, Ack), (GetDefaultBranch, GetDefaultBranchResponse), - (GitClone, GitCloneResponse) + (GitClone, GitCloneResponse), + (ToggleLspLogs, Ack), ); lsp_messages!( @@ -612,6 +614,7 @@ entity_messages!( GitReset, GitCheckoutFiles, SetIndexText, + ToggleLspLogs, Push, Fetch, diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 04028ebcac82f814652f32ad7439e32d650f5ad0..c81a69c2b308d2623ada78b1f38df80f96f8fe14 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -1,5 +1,6 @@ use ::proto::{FromProto, ToProto}; use anyhow::{Context as _, Result, anyhow}; +use lsp::LanguageServerId; use extension::ExtensionHostProxy; use extension_host::headless_host::HeadlessExtensionStore; @@ -14,6 +15,7 @@ use project::{ buffer_store::{BufferStore, BufferStoreEvent}, debugger::{breakpoint_store::BreakpointStore, dap_store::DapStore}, git_store::GitStore, + lsp_store::log_store::{self, GlobalLogStore, LanguageServerKind}, project_settings::SettingsObserver, search::SearchQuery, task_store::TaskStore, @@ -65,6 +67,7 @@ impl HeadlessProject { settings::init(cx); language::init(cx); project::Project::init_settings(cx); + log_store::init(false, cx); } pub fn new( @@ -235,6 +238,7 @@ impl HeadlessProject { session.add_entity_request_handler(Self::handle_open_new_buffer); session.add_entity_request_handler(Self::handle_find_search_candidates); session.add_entity_request_handler(Self::handle_open_server_settings); + session.add_entity_message_handler(Self::handle_toggle_lsp_logs); session.add_entity_request_handler(BufferStore::handle_update_buffer); session.add_entity_message_handler(BufferStore::handle_close_buffer); @@ -298,11 +302,40 @@ impl HeadlessProject { fn on_lsp_store_event( &mut self, - _lsp_store: Entity, + lsp_store: Entity, event: &LspStoreEvent, cx: &mut Context, ) { match event { + LspStoreEvent::LanguageServerAdded(id, name, worktree_id) => { + let log_store = cx + .try_global::() + .map(|lsp_logs| lsp_logs.0.clone()); + if let Some(log_store) = log_store { + log_store.update(cx, |log_store, cx| { + log_store.add_language_server( + LanguageServerKind::LocalSsh { + lsp_store: self.lsp_store.downgrade(), + }, + *id, + Some(name.clone()), + *worktree_id, + lsp_store.read(cx).language_server_for_id(*id), + cx, + ); + }); + } + } + LspStoreEvent::LanguageServerRemoved(id) => { + let log_store = cx + .try_global::() + .map(|lsp_logs| lsp_logs.0.clone()); + if let Some(log_store) = log_store { + log_store.update(cx, |log_store, cx| { + log_store.remove_language_server(*id, cx); + }); + } + } LspStoreEvent::LanguageServerUpdate { language_server_id, name, @@ -326,16 +359,6 @@ impl HeadlessProject { }) .log_err(); } - LspStoreEvent::LanguageServerLog(language_server_id, log_type, message) => { - self.session - .send(proto::LanguageServerLog { - project_id: REMOTE_SERVER_PROJECT_ID, - language_server_id: language_server_id.to_proto(), - message: message.clone(), - log_type: Some(log_type.to_proto()), - }) - .log_err(); - } LspStoreEvent::LanguageServerPrompt(prompt) => { let request = self.session.request(proto::LanguageServerPromptRequest { project_id: REMOTE_SERVER_PROJECT_ID, @@ -509,7 +532,31 @@ impl HeadlessProject { }) } - pub async fn handle_open_server_settings( + async fn handle_toggle_lsp_logs( + _: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result<()> { + let server_id = LanguageServerId::from_proto(envelope.payload.server_id); + let lsp_logs = cx + .update(|cx| { + cx.try_global::() + .map(|lsp_logs| lsp_logs.0.clone()) + })? + .context("lsp logs store is missing")?; + + lsp_logs.update(&mut cx, |lsp_logs, _| { + // we do not support any other log toggling yet + if envelope.payload.enabled { + lsp_logs.enable_rpc_trace_for_language_server(server_id); + } else { + lsp_logs.disable_rpc_trace_for_language_server(server_id); + } + })?; + Ok(()) + } + + async fn handle_open_server_settings( this: Entity, _: TypedEnvelope, mut cx: AsyncApp, @@ -562,7 +609,7 @@ impl HeadlessProject { }) } - pub async fn handle_find_search_candidates( + async fn handle_find_search_candidates( this: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, @@ -594,7 +641,7 @@ impl HeadlessProject { Ok(response) } - pub async fn handle_list_remote_directory( + async fn handle_list_remote_directory( this: Entity, envelope: TypedEnvelope, cx: AsyncApp, @@ -626,7 +673,7 @@ impl HeadlessProject { }) } - pub async fn handle_get_path_metadata( + async fn handle_get_path_metadata( this: Entity, envelope: TypedEnvelope, cx: AsyncApp, @@ -644,7 +691,7 @@ impl HeadlessProject { }) } - pub async fn handle_shutdown_remote_server( + async fn handle_shutdown_remote_server( _this: Entity, _envelope: TypedEnvelope, cx: AsyncApp, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 1966755d626af6e155440379982af180e9ccbc95..a0717333159e508ea42a1b95bd9f2226e6392871 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -30,7 +30,7 @@ pub struct ActiveSettingsProfileName(pub String); impl Global for ActiveSettingsProfileName {} -#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord, serde::Serialize)] pub struct WorktreeId(usize); impl From for usize { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 587065f9b123ff4681494ece99f4051c7a50025e..5a180e4b42705332bd51dffe43943d131a42907f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -32,7 +32,8 @@ use gpui::{ }; use image_viewer::ImageInfo; use language::Capability; -use language_tools::lsp_tool::{self, LspTool}; +use language_tools::lsp_button::{self, LspButton}; +use language_tools::lsp_log_view::LspLogToolbarItemView; use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType}; use migrator::{migrate_keymap, migrate_settings}; use onboarding::DOCS_URL; @@ -396,12 +397,12 @@ pub fn initialize_workspace( let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx)); let image_info = cx.new(|_cx| ImageInfo::new(workspace)); - let lsp_tool_menu_handle = PopoverMenuHandle::default(); - let lsp_tool = - cx.new(|cx| LspTool::new(workspace, lsp_tool_menu_handle.clone(), window, cx)); + let lsp_button_menu_handle = PopoverMenuHandle::default(); + let lsp_button = + cx.new(|cx| LspButton::new(workspace, lsp_button_menu_handle.clone(), window, cx)); workspace.register_action({ - move |_, _: &lsp_tool::ToggleMenu, window, cx| { - lsp_tool_menu_handle.toggle(window, cx); + move |_, _: &lsp_button::ToggleMenu, window, cx| { + lsp_button_menu_handle.toggle(window, cx); } }); @@ -409,7 +410,7 @@ pub fn initialize_workspace( cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace)); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(search_button, window, cx); - status_bar.add_left_item(lsp_tool, window, cx); + status_bar.add_left_item(lsp_button, window, cx); status_bar.add_left_item(diagnostic_summary, window, cx); status_bar.add_left_item(activity_indicator, window, cx); status_bar.add_right_item(edit_prediction_button, window, cx); @@ -988,7 +989,7 @@ fn initialize_pane( toolbar.add_item(diagnostic_editor_controls, window, cx); let project_search_bar = cx.new(|_| ProjectSearchBar::new()); toolbar.add_item(project_search_bar, window, cx); - let lsp_log_item = cx.new(|_| language_tools::LspLogToolbarItemView::new()); + let lsp_log_item = cx.new(|_| LspLogToolbarItemView::new()); toolbar.add_item(lsp_log_item, window, cx); let dap_log_item = cx.new(|_| debugger_tools::DapLogToolbarItemView::new()); toolbar.add_item(dap_log_item, window, cx); From 213ee32b94ceeb2662f498e8e47181511bb07fb3 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:47:19 -0300 Subject: [PATCH 407/744] docs: Make unsupported features more prominent in external agents (#37090) Use the notes component to better highlight that in the docs UI. Release Notes: - N/A --- docs/src/ai/agent-panel.md | 2 +- docs/src/ai/external-agents.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/ai/agent-panel.md b/docs/src/ai/agent-panel.md index 13d92278a441efd165511d88550754587bf7f97e..ff5fdf84ce354f0c28c1986de84e28fa1c3c7ada 100644 --- a/docs/src/ai/agent-panel.md +++ b/docs/src/ai/agent-panel.md @@ -18,7 +18,7 @@ If you need extra room to type, you can expand the message editor with {#kb agen You should start to see the responses stream in with indications of [which tools](./tools.md) the model is using to fulfill your prompt. -> Note that, at the moment, not all features outlined below work for external agents, like [Gemini CLI](./external-agents.md#gemini-cli)—features like _checkpoints_, _token usage display_, and _model selection_ may be supported in the future. +> Note that, at the moment, not all features outlined below work for external agents, like [Gemini CLI](./external-agents.md#gemini-cli)—features like _restoring threads from history_, _checkpoints_, _token usage display_, _model selection_, and others may be supported in the future. ### Creating New Threads diff --git a/docs/src/ai/external-agents.md b/docs/src/ai/external-agents.md index b13cc0fe4b2db226de1e24c0cb86f53d5a7c6a23..3d263afdb060a5acc99e4315ac32c7063386317f 100644 --- a/docs/src/ai/external-agents.md +++ b/docs/src/ai/external-agents.md @@ -54,8 +54,8 @@ Similar to Zed's first-party agent, you can use Gemini CLI to do anything that y You can @-mention files, recent threads, symbols, or fetch the web. -Note that some first-party agent features don't yet work with Gemini CLI: editing past messages, resuming threads from history, checkpointing, and using the agent in SSH projects. -We hope to add these features in the near future. +> Note that some first-party agent features don't yet work with Gemini CLI: editing past messages, resuming threads from history, checkpointing, and using the agent in SSH projects. +> We hope to add these features in the near future. ## Add Custom Agents {#add-custom-agents} From 24ee98b3e13a164e0c9403963787951dc069f661 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Thu, 28 Aug 2025 19:42:59 +0530 Subject: [PATCH 408/744] agent2: Fix model deduplication to use provider ID and model ID (#37088) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #37043 Previously claude sonnet 4 was missing from copilot as it was colliding with zed's claude-sonnet-4 model id. Now we do deduplication based upon model and provider id both. | Before | After | |--------|--------| | CleanShot 2025-08-28 at 18 31
28@2x | CleanShot 2025-08-28 at 18 31
42@2x | Release Notes: - Fixed an issue where models with the same ID from different providers (such as Claude Sonnet 4 from both Zed and Copilot) were incorrectly deduplicated in the model selector—now all variants are shown. --- crates/agent2/src/agent.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 51e1fc631625fb8f7e5b84cb2520c85ed6b3d876..ea80df8fb52cffab80c8c64307b75de7f0954a56 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -93,7 +93,7 @@ impl LanguageModels { let mut recommended = Vec::new(); for provider in &providers { for model in provider.recommended_models(cx) { - recommended_models.insert(model.id()); + recommended_models.insert((model.provider_id(), model.id())); recommended.push(Self::map_language_model_to_info(&model, provider)); } } @@ -110,7 +110,7 @@ impl LanguageModels { for model in provider.provided_models(cx) { let model_info = Self::map_language_model_to_info(&model, &provider); let model_id = model_info.id.clone(); - if !recommended_models.contains(&model.id()) { + if !recommended_models.contains(&(model.provider_id(), model.id())) { provider_models.push(model_info); } models.insert(model_id, model); From 835e5ba662e5bade1a4bd91a45e64faa7f64aff4 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 28 Aug 2025 16:40:43 +0200 Subject: [PATCH 409/744] Inject venv environment via the toolchain (#36576) Instead of manually constructing the venv we now ask the python toolchain for the relevant information, unifying the approach of vent inspection Fixes https://github.com/zed-industries/zed/issues/27350 Release Notes: - Improved the detection of python virtual environments for terminals and tasks in remote projects. --- crates/agent2/src/tools/terminal_tool.rs | 8 +- crates/assistant_tools/src/terminal_tool.rs | 9 +- crates/debugger_ui/src/session/running.rs | 15 +- crates/language/src/toolchain.rs | 30 +- crates/languages/src/python.rs | 44 +- crates/project/src/debugger/dap_store.rs | 1 + crates/project/src/project_tests.rs | 3 + crates/project/src/terminals.rs | 789 ++++++++---------- crates/proto/build.rs | 1 + crates/remote/src/remote_client.rs | 12 +- crates/remote/src/transport/ssh.rs | 13 +- crates/task/src/shell_builder.rs | 39 +- crates/terminal/src/terminal.rs | 63 +- crates/terminal_view/src/persistence.rs | 12 +- crates/terminal_view/src/terminal_panel.rs | 253 ++++-- .../src/terminal_path_like_target.rs | 6 +- crates/terminal_view/src/terminal_view.rs | 29 +- crates/util/src/util.rs | 12 + crates/workspace/src/persistence.rs | 16 +- 19 files changed, 704 insertions(+), 651 deletions(-) diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index f41b909d0b286b80bb3c9e8e8c18d0d03f3e05c7..2270a7c32f076bee774c7c8177c4276985adc0b6 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -2,7 +2,7 @@ use agent_client_protocol as acp; use anyhow::Result; use futures::{FutureExt as _, future::Shared}; use gpui::{App, AppContext, Entity, SharedString, Task}; -use project::{Project, terminals::TerminalKind}; +use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{ @@ -144,14 +144,14 @@ impl AgentTool for TerminalTool { let terminal = self .project .update(cx, |project, cx| { - project.create_terminal( - TerminalKind::Task(task::SpawnInTerminal { + project.create_terminal_task( + task::SpawnInTerminal { command: Some(program), args, cwd: working_dir.clone(), env, ..Default::default() - }), + }, cx, ) })? diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index b28e55e78aef40554a8ebe60108bd81da3f9d95a..774f32426540e077e5bde72081db789329f86262 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -15,7 +15,7 @@ use language::LineEnding; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use portable_pty::{CommandBuilder, PtySize, native_pty_system}; -use project::{Project, terminals::TerminalKind}; +use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -213,17 +213,16 @@ impl Tool for TerminalTool { async move |cx| { let program = program.await; let env = env.await; - project .update(cx, |project, cx| { - project.create_terminal( - TerminalKind::Task(task::SpawnInTerminal { + project.create_terminal_task( + task::SpawnInTerminal { command: Some(program), args, cwd, env, ..Default::default() - }), + }, cx, ) })? diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 25146dc7e42bd2359ccd4a16644ba9e41565a0a8..46e5f35aecb0ad13e55ceb8d2dd12e7ae791a2c5 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -36,7 +36,6 @@ use module_list::ModuleList; use project::{ DebugScenarioContext, Project, WorktreeId, debugger::session::{self, Session, SessionEvent, SessionStateEvent, ThreadId, ThreadStatus}, - terminals::TerminalKind, }; use rpc::proto::ViewId; use serde_json::Value; @@ -1040,12 +1039,11 @@ impl RunningState { }; let terminal = project .update(cx, |project, cx| { - project.create_terminal( - TerminalKind::Task(task_with_shell.clone()), + project.create_terminal_task( + task_with_shell.clone(), cx, ) - })? - .await?; + })?.await?; let terminal_view = cx.new_window_entity(|window, cx| { TerminalView::new( @@ -1189,7 +1187,7 @@ impl RunningState { .filter(|title| !title.is_empty()) .or_else(|| command.clone()) .unwrap_or_else(|| "Debug terminal".to_string()); - let kind = TerminalKind::Task(task::SpawnInTerminal { + let kind = task::SpawnInTerminal { id: task::TaskId("debug".to_string()), full_label: title.clone(), label: title.clone(), @@ -1207,12 +1205,13 @@ impl RunningState { show_summary: false, show_command: false, show_rerun: false, - }); + }; let workspace = self.workspace.clone(); let weak_project = project.downgrade(); - let terminal_task = project.update(cx, |project, cx| project.create_terminal(kind, cx)); + let terminal_task = + project.update(cx, |project, cx| project.create_terminal_task(kind, cx)); let terminal_task = cx.spawn_in(window, async move |_, cx| { let terminal = terminal_task.await?; diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 66879e56da0a4a7cdac5b64acbce9e31313bf373..2a8dfd58418812b94c625845dce9724e145c7388 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -11,13 +11,14 @@ use std::{ use async_trait::async_trait; use collections::HashMap; +use fs::Fs; use gpui::{AsyncApp, SharedString}; use settings::WorktreeId; use crate::{LanguageName, ManifestName}; /// Represents a single toolchain. -#[derive(Clone, Debug, Eq)] +#[derive(Clone, Eq, Debug)] pub struct Toolchain { /// User-facing label pub name: SharedString, @@ -29,21 +30,29 @@ pub struct Toolchain { impl std::hash::Hash for Toolchain { fn hash(&self, state: &mut H) { - self.name.hash(state); - self.path.hash(state); - self.language_name.hash(state); + let Self { + name, + path, + language_name, + as_json: _, + } = self; + name.hash(state); + path.hash(state); + language_name.hash(state); } } impl PartialEq for Toolchain { fn eq(&self, other: &Self) -> bool { + let Self { + name, + path, + language_name, + as_json: _, + } = self; // Do not use as_json for comparisons; it shouldn't impact equality, as it's not user-surfaced. // Thus, there could be multiple entries that look the same in the UI. - (&self.name, &self.path, &self.language_name).eq(&( - &other.name, - &other.path, - &other.language_name, - )) + (name, path, language_name).eq(&(&other.name, &other.path, &other.language_name)) } } @@ -59,6 +68,7 @@ pub trait ToolchainLister: Send + Sync { fn term(&self) -> SharedString; /// Returns the name of the manifest file for this toolchain. fn manifest_name(&self) -> ManifestName; + async fn activation_script(&self, toolchain: &Toolchain, fs: &dyn Fs) -> Option; } #[async_trait(?Send)] @@ -82,7 +92,7 @@ pub trait LocalLanguageToolchainStore: Send + Sync + 'static { ) -> Option; } -#[async_trait(?Send )] +#[async_trait(?Send)] impl LanguageToolchainStore for T { async fn active_toolchain( self: Arc, diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 0f78d5c5dfaaf3f24a337cab32ed9656354ac911..37d38de9dab6bb5968b446e7009a42c5f2e86e86 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -2,6 +2,7 @@ use anyhow::{Context as _, ensure}; use anyhow::{Result, anyhow}; use async_trait::async_trait; use collections::HashMap; +use futures::AsyncBufReadExt; use gpui::{App, Task}; use gpui::{AsyncApp, SharedString}; use language::Toolchain; @@ -30,8 +31,6 @@ use std::{ borrow::Cow, ffi::OsString, fmt::Write, - fs, - io::{self, BufRead}, path::{Path, PathBuf}, sync::Arc, }; @@ -741,14 +740,16 @@ fn env_priority(kind: Option) -> usize { /// Return the name of environment declared in Option { - fs::File::open(worktree_root.join(".venv")) - .and_then(|file| { - let mut venv_name = String::new(); - io::BufReader::new(file).read_line(&mut venv_name)?; - Ok(venv_name.trim().to_string()) - }) - .ok() +async fn get_worktree_venv_declaration(worktree_root: &Path) -> Option { + let file = async_fs::File::open(worktree_root.join(".venv")) + .await + .ok()?; + let mut venv_name = String::new(); + smol::io::BufReader::new(file) + .read_line(&mut venv_name) + .await + .ok()?; + Some(venv_name.trim().to_string()) } #[async_trait] @@ -793,7 +794,7 @@ impl ToolchainLister for PythonToolchainProvider { .map_or(Vec::new(), |mut guard| std::mem::take(&mut guard)); let wr = worktree_root; - let wr_venv = get_worktree_venv_declaration(&wr); + let wr_venv = get_worktree_venv_declaration(&wr).await; // Sort detected environments by: // environment name matching activation file (/.venv) // environment project dir matching worktree_root @@ -858,7 +859,7 @@ impl ToolchainLister for PythonToolchainProvider { .into_iter() .filter_map(|toolchain| { let mut name = String::from("Python"); - if let Some(ref version) = toolchain.version { + if let Some(version) = &toolchain.version { _ = write!(name, " {version}"); } @@ -879,7 +880,7 @@ impl ToolchainLister for PythonToolchainProvider { name: name.into(), path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(), language_name: LanguageName::new("Python"), - as_json: serde_json::to_value(toolchain).ok()?, + as_json: serde_json::to_value(toolchain.clone()).ok()?, }) }) .collect(); @@ -893,6 +894,23 @@ impl ToolchainLister for PythonToolchainProvider { fn term(&self) -> SharedString { self.term.clone() } + async fn activation_script(&self, toolchain: &Toolchain, fs: &dyn Fs) -> Option { + let toolchain = serde_json::from_value::( + toolchain.as_json.clone(), + ) + .ok()?; + let mut activation_script = None; + if let Some(prefix) = &toolchain.prefix { + #[cfg(not(target_os = "windows"))] + let path = prefix.join(BINARY_DIR).join("activate"); + #[cfg(target_os = "windows")] + let path = prefix.join(BINARY_DIR).join("activate.ps1"); + if fs.is_file(&path).await { + activation_script = Some(format!(". {}", path.display())); + } + } + activation_script + } } pub struct EnvironmentApi<'a> { diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index d8c6d3acc1116e9a97b2f6ca3fc54ec098029cbe..859574c82a5b4470d477df555b314498cbfcd0e0 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -276,6 +276,7 @@ impl DapStore { &binary.arguments, &binary.envs, binary.cwd.map(|path| path.display().to_string()), + None, port_forwarding, ) })??; diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index a8f911883d619214a35f3ef5d80f83d6dc1b3894..c814d6207e92608c13502a4da3a0781836acce0e 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -9222,6 +9222,9 @@ fn python_lang(fs: Arc) -> Arc { fn manifest_name(&self) -> ManifestName { SharedString::new_static("pyproject.toml").into() } + async fn activation_script(&self, _: &Toolchain, _: &dyn Fs) -> Option { + None + } } Arc::new( Language::new( diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 7e1be67e21380e8b1ae751600b3553a527067e46..aad5ce941125c2c747df3a76473a9dbffba0b80e 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,44 +1,28 @@ -use crate::{Project, ProjectPath}; -use anyhow::{Context as _, Result}; +use anyhow::Result; use collections::HashMap; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; +use itertools::Itertools; use language::LanguageName; use remote::RemoteClient; use settings::{Settings, SettingsLocation}; use smol::channel::bounded; use std::{ - env::{self}, + borrow::Cow, path::{Path, PathBuf}, sync::Arc, }; use task::{Shell, ShellBuilder, SpawnInTerminal}; use terminal::{ - TaskState, TaskStatus, Terminal, TerminalBuilder, - terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings}, + TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings, }; -use util::{ResultExt, paths::RemotePathBuf}; +use util::{get_default_system_shell, get_system_shell, maybe}; -/// The directory inside a Python virtual environment that contains executables -const PYTHON_VENV_BIN_DIR: &str = if cfg!(target_os = "windows") { - "Scripts" -} else { - "bin" -}; +use crate::{Project, ProjectPath}; pub struct Terminals { pub(crate) local_handles: Vec>, } -/// Terminals are opened either for the users shell, or to run a task. - -#[derive(Debug)] -pub enum TerminalKind { - /// Run a shell at the given path (or $HOME if None) - Shell(Option), - /// Run a task. - Task(SpawnInTerminal), -} - impl Project { pub fn active_project_directory(&self, cx: &App) -> Option> { self.active_entry() @@ -58,54 +42,33 @@ impl Project { } } - pub fn create_terminal( + pub fn create_terminal_task( &mut self, - kind: TerminalKind, + spawn_task: SpawnInTerminal, cx: &mut Context, ) -> Task>> { - let path: Option> = match &kind { - TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())), - TerminalKind::Task(spawn_task) => { - if let Some(cwd) = &spawn_task.cwd { - Some(Arc::from(cwd.as_ref())) - } else { - self.active_project_directory(cx) - } - } - }; - - let mut settings_location = None; - if let Some(path) = path.as_ref() - && let Some((worktree, _)) = self.find_worktree(path, cx) - { - settings_location = Some(SettingsLocation { - worktree_id: worktree.read(cx).id(), - path, + let is_via_remote = self.remote_client.is_some(); + let project_path_context = self + .active_entry() + .and_then(|entry_id| self.worktree_id_for_entry(entry_id, cx)) + .or_else(|| self.visible_worktrees(cx).next().map(|wt| wt.read(cx).id())) + .map(|worktree_id| ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), }); - } - let venv = TerminalSettings::get(settings_location, cx) - .detect_venv - .clone(); - cx.spawn(async move |project, cx| { - let python_venv_directory = if let Some(path) = path { - project - .update(cx, |this, cx| this.python_venv_directory(path, venv, cx))? - .await + let path: Option> = if let Some(cwd) = &spawn_task.cwd { + if is_via_remote { + Some(Arc::from(cwd.as_ref())) } else { - None - }; - project.update(cx, |project, cx| { - project.create_terminal_with_venv(kind, python_venv_directory, cx) - })? - }) - } + let cwd = cwd.to_string_lossy(); + let tilde_substituted = shellexpand::tilde(&cwd); + Some(Arc::from(Path::new(tilde_substituted.as_ref()))) + } + } else { + self.active_project_directory(cx) + }; - pub fn terminal_settings<'a>( - &'a self, - path: &'a Option, - cx: &'a App, - ) -> &'a TerminalSettings { let mut settings_location = None; if let Some(path) = path.as_ref() && let Some((worktree, _)) = self.find_worktree(path, cx) @@ -115,74 +78,176 @@ impl Project { path, }); } - TerminalSettings::get(settings_location, cx) - } + let settings = TerminalSettings::get(settings_location, cx).clone(); + let detect_venv = settings.detect_venv.as_option().is_some(); - pub fn exec_in_shell(&self, command: String, cx: &App) -> Result { - let path = self.first_project_directory(cx); - let remote_client = self.remote_client.as_ref(); - let settings = self.terminal_settings(&path, cx).clone(); - let remote_shell = remote_client - .as_ref() - .and_then(|remote_client| remote_client.read(cx).shell()); - let builder = ShellBuilder::new(remote_shell.as_deref(), &settings.shell).non_interactive(); - let (command, args) = builder.build(Some(command), &Vec::new()); + let (completion_tx, completion_rx) = bounded(1); + // Start with the environment that we might have inherited from the Zed CLI. let mut env = self .environment .read(cx) .get_cli_environment() .unwrap_or_default(); + // Then extend it with the explicit env variables from the settings, so they take + // precedence. env.extend(settings.env); - match remote_client { - Some(remote_client) => { - let command_template = - remote_client - .read(cx) - .build_command(Some(command), &args, &env, None, None)?; - let mut command = std::process::Command::new(command_template.program); - command.args(command_template.args); - command.envs(command_template.env); - Ok(command) - } - None => { - let mut command = std::process::Command::new(command); - command.args(args); - command.envs(env); - if let Some(path) = path { - command.current_dir(path); - } - Ok(command) - } - } + let local_path = if is_via_remote { None } else { path.clone() }; + let task_state = Some(TaskState { + id: spawn_task.id, + full_label: spawn_task.full_label, + label: spawn_task.label, + command_label: spawn_task.command_label, + hide: spawn_task.hide, + status: TaskStatus::Running, + show_summary: spawn_task.show_summary, + show_command: spawn_task.show_command, + show_rerun: spawn_task.show_rerun, + completion_rx, + }); + let remote_client = self.remote_client.clone(); + let shell = match &remote_client { + Some(remote_client) => remote_client + .read(cx) + .shell() + .unwrap_or_else(get_default_system_shell), + None => match &settings.shell { + Shell::Program(program) => program.clone(), + Shell::WithArguments { + program, + args: _, + title_override: _, + } => program.clone(), + Shell::System => get_system_shell(), + }, + }; + + let toolchain = project_path_context + .filter(|_| detect_venv) + .map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx)); + let lang_registry = self.languages.clone(); + let fs = self.fs.clone(); + cx.spawn(async move |project, cx| { + let activation_script = maybe!(async { + let toolchain = toolchain?.await?; + lang_registry + .language_for_name(&toolchain.language_name.0) + .await + .ok()? + .toolchain_lister()? + .activation_script(&toolchain, fs.as_ref()) + .await + }) + .await; + + project.update(cx, move |this, cx| { + let shell = { + env.extend(spawn_task.env); + match remote_client { + Some(remote_client) => create_remote_shell( + spawn_task + .command + .as_ref() + .map(|command| (command, &spawn_task.args)), + &mut env, + path, + remote_client, + activation_script.clone(), + cx, + )?, + None => match activation_script.clone() { + Some(activation_script) => { + let to_run = if let Some(command) = spawn_task.command { + let command: Option> = shlex::try_quote(&command).ok(); + let args = spawn_task + .args + .iter() + .filter_map(|arg| shlex::try_quote(arg).ok()); + command.into_iter().chain(args).join(" ") + } else { + format!("exec {shell} -l") + }; + Shell::WithArguments { + program: get_default_system_shell(), + args: vec![ + "-c".to_owned(), + format!("{activation_script}; {to_run}",), + ], + title_override: None, + } + } + None => { + if let Some(program) = spawn_task.command { + Shell::WithArguments { + program, + args: spawn_task.args, + title_override: None, + } + } else { + Shell::System + } + } + }, + } + }; + TerminalBuilder::new( + local_path.map(|path| path.to_path_buf()), + task_state, + shell, + env, + settings.cursor_shape.unwrap_or_default(), + settings.alternate_scroll, + settings.max_scroll_history_lines, + is_via_remote, + cx.entity_id().as_u64(), + Some(completion_tx), + cx, + activation_script, + ) + .map(|builder| { + let terminal_handle = cx.new(|cx| builder.subscribe(cx)); + + this.terminals + .local_handles + .push(terminal_handle.downgrade()); + + let id = terminal_handle.entity_id(); + cx.observe_release(&terminal_handle, move |project, _terminal, cx| { + let handles = &mut project.terminals.local_handles; + + if let Some(index) = handles + .iter() + .position(|terminal| terminal.entity_id() == id) + { + handles.remove(index); + cx.notify(); + } + }) + .detach(); + + terminal_handle + }) + })? + }) } - pub fn create_terminal_with_venv( + pub fn create_terminal_shell( &mut self, - kind: TerminalKind, - python_venv_directory: Option, + cwd: Option, cx: &mut Context, - ) -> Result> { + ) -> Task>> { + let project_path_context = self + .active_entry() + .and_then(|entry_id| self.worktree_id_for_entry(entry_id, cx)) + .or_else(|| self.visible_worktrees(cx).next().map(|wt| wt.read(cx).id())) + .map(|worktree_id| ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), + }); + let path = cwd.map(|p| Arc::from(&*p)); let is_via_remote = self.remote_client.is_some(); - let path: Option> = match &kind { - TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())), - TerminalKind::Task(spawn_task) => { - if let Some(cwd) = &spawn_task.cwd { - if is_via_remote { - Some(Arc::from(cwd.as_ref())) - } else { - let cwd = cwd.to_string_lossy(); - let tilde_substituted = shellexpand::tilde(&cwd); - Some(Arc::from(Path::new(tilde_substituted.as_ref()))) - } - } else { - self.active_project_directory(cx) - } - } - }; - let mut settings_location = None; if let Some(path) = path.as_ref() && let Some((worktree, _)) = self.find_worktree(path, cx) @@ -193,8 +258,7 @@ impl Project { }); } let settings = TerminalSettings::get(settings_location, cx).clone(); - - let (completion_tx, completion_rx) = bounded(1); + let detect_venv = settings.detect_venv.as_option().is_some(); // Start with the environment that we might have inherited from the Zed CLI. let mut env = self @@ -208,107 +272,111 @@ impl Project { let local_path = if is_via_remote { None } else { path.clone() }; - let mut python_venv_activate_command = Task::ready(None); - + let toolchain = project_path_context + .filter(|_| detect_venv) + .map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx)); let remote_client = self.remote_client.clone(); - let spawn_task; - let shell; - match kind { - TerminalKind::Shell(_) => { - if let Some(python_venv_directory) = &python_venv_directory { - python_venv_activate_command = self.python_activate_command( - python_venv_directory, - &settings.detect_venv, - &settings.shell, - cx, - ); - } - - spawn_task = None; - shell = match remote_client { - Some(remote_client) => { - create_remote_shell(None, &mut env, path, remote_client, cx)? - } - None => settings.shell, - }; - } - TerminalKind::Task(task) => { - env.extend(task.env); - - if let Some(venv_path) = &python_venv_directory { - env.insert( - "VIRTUAL_ENV".to_string(), - venv_path.to_string_lossy().to_string(), - ); - } - - spawn_task = Some(TaskState { - id: task.id, - full_label: task.full_label, - label: task.label, - command_label: task.command_label, - hide: task.hide, - status: TaskStatus::Running, - show_summary: task.show_summary, - show_command: task.show_command, - show_rerun: task.show_rerun, - completion_rx, - }); - shell = match remote_client { - Some(remote_client) => { - let path_style = remote_client.read(cx).path_style(); - if let Some(venv_directory) = &python_venv_directory - && let Ok(str) = - shlex::try_quote(venv_directory.to_string_lossy().as_ref()) - { - let path = - RemotePathBuf::new(PathBuf::from(str.to_string()), path_style) - .to_string(); - env.insert("PATH".into(), format!("{}:$PATH ", path)); - } + let shell = match &remote_client { + Some(remote_client) => remote_client + .read(cx) + .shell() + .unwrap_or_else(get_default_system_shell), + None => match &settings.shell { + Shell::Program(program) => program.clone(), + Shell::WithArguments { + program, + args: _, + title_override: _, + } => program.clone(), + Shell::System => get_system_shell(), + }, + }; - create_remote_shell( - task.command.as_ref().map(|command| (command, &task.args)), + let lang_registry = self.languages.clone(); + let fs = self.fs.clone(); + cx.spawn(async move |project, cx| { + let activation_script = maybe!(async { + let toolchain = toolchain?.await?; + let language = lang_registry + .language_for_name(&toolchain.language_name.0) + .await + .ok(); + let lister = language?.toolchain_lister(); + lister?.activation_script(&toolchain, fs.as_ref()).await + }) + .await; + project.update(cx, move |this, cx| { + let shell = { + match remote_client { + Some(remote_client) => create_remote_shell( + None, &mut env, path, remote_client, + activation_script.clone(), cx, - )? + )?, + None => match activation_script.clone() { + Some(activation_script) => Shell::WithArguments { + program: get_default_system_shell(), + args: vec![ + "-c".to_owned(), + format!("{activation_script}; exec {shell} -l",), + ], + title_override: Some(shell.into()), + }, + None => settings.shell, + }, } - None => { - if let Some(venv_path) = &python_venv_directory { - add_environment_path(&mut env, &venv_path.join(PYTHON_VENV_BIN_DIR)) - .log_err(); + }; + TerminalBuilder::new( + local_path.map(|path| path.to_path_buf()), + None, + shell, + env, + settings.cursor_shape.unwrap_or_default(), + settings.alternate_scroll, + settings.max_scroll_history_lines, + is_via_remote, + cx.entity_id().as_u64(), + None, + cx, + activation_script, + ) + .map(|builder| { + let terminal_handle = cx.new(|cx| builder.subscribe(cx)); + + this.terminals + .local_handles + .push(terminal_handle.downgrade()); + + let id = terminal_handle.entity_id(); + cx.observe_release(&terminal_handle, move |project, _terminal, cx| { + let handles = &mut project.terminals.local_handles; + + if let Some(index) = handles + .iter() + .position(|terminal| terminal.entity_id() == id) + { + handles.remove(index); + cx.notify(); } + }) + .detach(); - if let Some(program) = task.command { - Shell::WithArguments { - program, - args: task.args, - title_override: None, - } - } else { - Shell::System - } - } - }; - } - }; - TerminalBuilder::new( - local_path.map(|path| path.to_path_buf()), - python_venv_directory, - spawn_task, - shell, - env, - settings.cursor_shape.unwrap_or_default(), - settings.alternate_scroll, - settings.max_scroll_history_lines, - is_via_remote, - cx.entity_id().as_u64(), - completion_tx, - cx, - ) - .map(|builder| { + terminal_handle + }) + })? + }) + } + + pub fn clone_terminal( + &mut self, + terminal: &Entity, + cx: &mut Context<'_, Project>, + cwd: impl FnOnce() -> Option, + ) -> Result> { + terminal.read(cx).clone_builder(cx, cwd).map(|builder| { let terminal_handle = cx.new(|cx| builder.subscribe(cx)); self.terminals @@ -329,211 +397,72 @@ impl Project { }) .detach(); - self.activate_python_virtual_environment( - python_venv_activate_command, - &terminal_handle, - cx, - ); - terminal_handle }) } - fn python_venv_directory( - &self, - abs_path: Arc, - venv_settings: VenvSettings, - cx: &Context, - ) -> Task> { - cx.spawn(async move |this, cx| { - if let Some((worktree, relative_path)) = this - .update(cx, |this, cx| this.find_worktree(&abs_path, cx)) - .ok()? - { - let toolchain = this - .update(cx, |this, cx| { - this.active_toolchain( - ProjectPath { - worktree_id: worktree.read(cx).id(), - path: relative_path.into(), - }, - LanguageName::new("Python"), - cx, - ) - }) - .ok()? - .await; - - if let Some(toolchain) = toolchain { - let toolchain_path = Path::new(toolchain.path.as_ref()); - return Some(toolchain_path.parent()?.parent()?.to_path_buf()); - } - } - let venv_settings = venv_settings.as_option()?; - this.update(cx, move |this, cx| { - if let Some(path) = this.find_venv_in_worktree(&abs_path, &venv_settings, cx) { - return Some(path); - } - this.find_venv_on_filesystem(&abs_path, &venv_settings, cx) - }) - .ok() - .flatten() - }) - } - - fn find_venv_in_worktree( - &self, - abs_path: &Path, - venv_settings: &terminal_settings::VenvSettingsContent, - cx: &App, - ) -> Option { - venv_settings - .directories - .iter() - .map(|name| abs_path.join(name)) - .find(|venv_path| { - let bin_path = venv_path.join(PYTHON_VENV_BIN_DIR); - self.find_worktree(&bin_path, cx) - .and_then(|(worktree, relative_path)| { - worktree.read(cx).entry_for_path(&relative_path) - }) - .is_some_and(|entry| entry.is_dir()) - }) - } - - fn find_venv_on_filesystem( - &self, - abs_path: &Path, - venv_settings: &terminal_settings::VenvSettingsContent, - cx: &App, - ) -> Option { - let (worktree, _) = self.find_worktree(abs_path, cx)?; - let fs = worktree.read(cx).as_local()?.fs(); - venv_settings - .directories - .iter() - .map(|name| abs_path.join(name)) - .find(|venv_path| { - let bin_path = venv_path.join(PYTHON_VENV_BIN_DIR); - // One-time synchronous check is acceptable for terminal/task initialization - smol::block_on(fs.metadata(&bin_path)) - .ok() - .flatten() - .is_some_and(|meta| meta.is_dir) - }) - } - - fn activate_script_kind(shell: Option<&str>) -> ActivateScript { - let shell_env = std::env::var("SHELL").ok(); - let shell_path = shell.or_else(|| shell_env.as_deref()); - let shell = std::path::Path::new(shell_path.unwrap_or("")) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(""); - match shell { - "fish" => ActivateScript::Fish, - "tcsh" => ActivateScript::Csh, - "nu" => ActivateScript::Nushell, - "powershell" | "pwsh" => ActivateScript::PowerShell, - _ => ActivateScript::Default, + pub fn terminal_settings<'a>( + &'a self, + path: &'a Option, + cx: &'a App, + ) -> &'a TerminalSettings { + let mut settings_location = None; + if let Some(path) = path.as_ref() + && let Some((worktree, _)) = self.find_worktree(path, cx) + { + settings_location = Some(SettingsLocation { + worktree_id: worktree.read(cx).id(), + path, + }); } + TerminalSettings::get(settings_location, cx) } - fn python_activate_command( - &self, - venv_base_directory: &Path, - venv_settings: &VenvSettings, - shell: &Shell, - cx: &mut App, - ) -> Task> { - let Some(venv_settings) = venv_settings.as_option() else { - return Task::ready(None); - }; - let activate_keyword = match venv_settings.activate_script { - terminal_settings::ActivateScript::Default => match std::env::consts::OS { - "windows" => ".", - _ => ".", - }, - terminal_settings::ActivateScript::Nushell => "overlay use", - terminal_settings::ActivateScript::PowerShell => ".", - terminal_settings::ActivateScript::Pyenv => "pyenv", - _ => "source", - }; - let script_kind = - if venv_settings.activate_script == terminal_settings::ActivateScript::Default { - match shell { - Shell::Program(program) => Self::activate_script_kind(Some(program)), - Shell::WithArguments { - program, - args: _, - title_override: _, - } => Self::activate_script_kind(Some(program)), - Shell::System => Self::activate_script_kind(None), - } - } else { - venv_settings.activate_script - }; - - let activate_script_name = match script_kind { - terminal_settings::ActivateScript::Default - | terminal_settings::ActivateScript::Pyenv => "activate", - terminal_settings::ActivateScript::Csh => "activate.csh", - terminal_settings::ActivateScript::Fish => "activate.fish", - terminal_settings::ActivateScript::Nushell => "activate.nu", - terminal_settings::ActivateScript::PowerShell => "activate.ps1", - }; + pub fn exec_in_shell(&self, command: String, cx: &App) -> Result { + let path = self.first_project_directory(cx); + let remote_client = self.remote_client.as_ref(); + let settings = self.terminal_settings(&path, cx).clone(); + let remote_shell = remote_client + .as_ref() + .and_then(|remote_client| remote_client.read(cx).shell()); + let builder = ShellBuilder::new(remote_shell.as_deref(), &settings.shell).non_interactive(); + let (command, args) = builder.build(Some(command), &Vec::new()); - let line_ending = match std::env::consts::OS { - "windows" => "\r", - _ => "\n", - }; + let mut env = self + .environment + .read(cx) + .get_cli_environment() + .unwrap_or_default(); + env.extend(settings.env); - if venv_settings.venv_name.is_empty() { - let path = venv_base_directory - .join(PYTHON_VENV_BIN_DIR) - .join(activate_script_name) - .to_string_lossy() - .to_string(); - - let is_valid_path = self.resolve_abs_path(path.as_ref(), cx); - cx.background_spawn(async move { - let quoted = shlex::try_quote(&path).ok()?; - if is_valid_path.await.is_some_and(|meta| meta.is_file()) { - Some(format!( - "{} {} ; clear{}", - activate_keyword, quoted, line_ending - )) - } else { - None + match remote_client { + Some(remote_client) => { + let command_template = remote_client.read(cx).build_command( + Some(command), + &args, + &env, + None, + // todo + None, + None, + )?; + let mut command = std::process::Command::new(command_template.program); + command.args(command_template.args); + command.envs(command_template.env); + Ok(command) + } + None => { + let mut command = std::process::Command::new(command); + command.args(args); + command.envs(env); + if let Some(path) = path { + command.current_dir(path); } - }) - } else { - Task::ready(Some(format!( - "{activate_keyword} {activate_script_name} {name}; clear{line_ending}", - name = venv_settings.venv_name - ))) + Ok(command) + } } } - fn activate_python_virtual_environment( - &self, - command: Task>, - terminal_handle: &Entity, - cx: &mut App, - ) { - terminal_handle.update(cx, |_, cx| { - cx.spawn(async move |this, cx| { - if let Some(command) = command.await { - this.update(cx, |this, _| { - this.input(command.into_bytes()); - }) - .ok(); - } - }) - .detach() - }); - } - pub fn local_terminal_handles(&self) -> &Vec> { &self.terminals.local_handles } @@ -544,6 +473,7 @@ fn create_remote_shell( env: &mut HashMap, working_directory: Option>, remote_client: Entity, + activation_script: Option, cx: &mut App, ) -> Result { // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed @@ -563,6 +493,7 @@ fn create_remote_shell( args.as_slice(), env, working_directory.map(|path| path.display().to_string()), + activation_script, None, )?; *env = command.env; @@ -576,57 +507,3 @@ fn create_remote_shell( title_override: Some(format!("{} — Terminal", host).into()), }) } - -fn add_environment_path(env: &mut HashMap, new_path: &Path) -> Result<()> { - let mut env_paths = vec![new_path.to_path_buf()]; - if let Some(path) = env.get("PATH").or(env::var("PATH").ok().as_ref()) { - let mut paths = std::env::split_paths(&path).collect::>(); - env_paths.append(&mut paths); - } - - let paths = std::env::join_paths(env_paths).context("failed to create PATH env variable")?; - env.insert("PATH".to_string(), paths.to_string_lossy().to_string()); - - Ok(()) -} - -#[cfg(test)] -mod tests { - use collections::HashMap; - - #[test] - fn test_add_environment_path_with_existing_path() { - let tmp_path = std::path::PathBuf::from("/tmp/new"); - let mut env = HashMap::default(); - let old_path = if cfg!(windows) { - "/usr/bin;/usr/local/bin" - } else { - "/usr/bin:/usr/local/bin" - }; - env.insert("PATH".to_string(), old_path.to_string()); - env.insert("OTHER".to_string(), "aaa".to_string()); - - super::add_environment_path(&mut env, &tmp_path).unwrap(); - if cfg!(windows) { - assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new;{}", old_path)); - } else { - assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new:{}", old_path)); - } - assert_eq!(env.get("OTHER").unwrap(), "aaa"); - } - - #[test] - fn test_add_environment_path_with_empty_path() { - let tmp_path = std::path::PathBuf::from("/tmp/new"); - let mut env = HashMap::default(); - env.insert("OTHER".to_string(), "aaa".to_string()); - let os_path = std::env::var("PATH").unwrap(); - super::add_environment_path(&mut env, &tmp_path).unwrap(); - if cfg!(windows) { - assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new;{}", os_path)); - } else { - assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new:{}", os_path)); - } - assert_eq!(env.get("OTHER").unwrap(), "aaa"); - } -} diff --git a/crates/proto/build.rs b/crates/proto/build.rs index 2997e302b6e62348eef6d65f158b22f00c992f7c..184d0e53d57bf06f5cd3667adea86ec4a2dde282 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -1,4 +1,5 @@ fn main() { + println!("cargo:rerun-if-changed=proto"); let mut build = prost_build::Config::new(); build .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index dd529ca87499b0daf2061fd990f7149828e3fce4..2b8d9e4a94fb9988e801c5ef9202ee603959d36b 100644 --- a/crates/remote/src/remote_client.rs +++ b/crates/remote/src/remote_client.rs @@ -757,6 +757,7 @@ impl RemoteClient { args: &[String], env: &HashMap, working_dir: Option, + activation_script: Option, port_forward: Option<(u16, String, u16)>, ) -> Result { let Some(connection) = self @@ -766,7 +767,14 @@ impl RemoteClient { else { return Err(anyhow!("no connection")); }; - connection.build_command(program, args, env, working_dir, port_forward) + connection.build_command( + program, + args, + env, + working_dir, + activation_script, + port_forward, + ) } pub fn upload_directory( @@ -998,6 +1006,7 @@ pub(crate) trait RemoteConnection: Send + Sync { args: &[String], env: &HashMap, working_dir: Option, + activation_script: Option, port_forward: Option<(u16, String, u16)>, ) -> Result; fn connection_options(&self) -> SshConnectionOptions; @@ -1364,6 +1373,7 @@ mod fake { args: &[String], env: &HashMap, _: Option, + _: Option, _: Option<(u16, String, u16)>, ) -> Result { let ssh_program = program.unwrap_or_else(|| "sh".to_string()); diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 193b96497375e5dee19aacaff114ba79d78a7191..750fc6dc586c227c6461ab4650c1b46b6bb01dc6 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -30,7 +30,10 @@ use std::{ time::Instant, }; use tempfile::TempDir; -use util::paths::{PathStyle, RemotePathBuf}; +use util::{ + get_default_system_shell, + paths::{PathStyle, RemotePathBuf}, +}; pub(crate) struct SshRemoteConnection { socket: SshSocket, @@ -113,6 +116,7 @@ impl RemoteConnection for SshRemoteConnection { input_args: &[String], input_env: &HashMap, working_dir: Option, + activation_script: Option, port_forward: Option<(u16, String, u16)>, ) -> Result { use std::fmt::Write as _; @@ -134,6 +138,9 @@ impl RemoteConnection for SshRemoteConnection { } else { write!(&mut script, "cd; ").unwrap(); }; + if let Some(activation_script) = activation_script { + write!(&mut script, " {activation_script};").unwrap(); + } for (k, v) in input_env.iter() { if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) { @@ -155,7 +162,8 @@ impl RemoteConnection for SshRemoteConnection { write!(&mut script, "exec {shell} -l").unwrap(); }; - let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&script).unwrap()); + let sys_shell = get_default_system_shell(); + let shell_invocation = format!("{sys_shell} -c {}", shlex::try_quote(&script).unwrap()); let mut args = Vec::new(); args.extend(self.socket.ssh_args()); @@ -167,7 +175,6 @@ impl RemoteConnection for SshRemoteConnection { args.push("-t".into()); args.push(shell_invocation); - Ok(CommandTemplate { program: "ssh".into(), args, diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index f907bd1d9f3f02869b2eb4b6ea4d65663e0d00af..38a5a970b73c334892bc494f94b9b759b015677e 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -1,3 +1,7 @@ +use std::fmt; + +use util::get_system_shell; + use crate::Shell; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] @@ -11,9 +15,22 @@ pub enum ShellKind { Cmd, } +impl fmt::Display for ShellKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ShellKind::Posix => write!(f, "sh"), + ShellKind::Csh => write!(f, "csh"), + ShellKind::Fish => write!(f, "fish"), + ShellKind::Powershell => write!(f, "powershell"), + ShellKind::Nushell => write!(f, "nu"), + ShellKind::Cmd => write!(f, "cmd"), + } + } +} + impl ShellKind { pub fn system() -> Self { - Self::new(&system_shell()) + Self::new(&get_system_shell()) } pub fn new(program: &str) -> Self { @@ -22,12 +39,12 @@ impl ShellKind { #[cfg(not(windows))] let (_, program) = program.rsplit_once('/').unwrap_or(("", program)); if program == "powershell" - || program == "powershell.exe" + || program.ends_with("powershell.exe") || program == "pwsh" - || program == "pwsh.exe" + || program.ends_with("pwsh.exe") { ShellKind::Powershell - } else if program == "cmd" || program == "cmd.exe" { + } else if program == "cmd" || program.ends_with("cmd.exe") { ShellKind::Cmd } else if program == "nu" { ShellKind::Nushell @@ -178,18 +195,6 @@ impl ShellKind { } } -fn system_shell() -> String { - if cfg!(target_os = "windows") { - // `alacritty_terminal` uses this as default on Windows. See: - // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130 - // We could use `util::get_windows_system_shell()` here, but we are running tasks here, so leave it to `powershell.exe` - // should be okay. - "powershell.exe".to_string() - } else { - std::env::var("SHELL").unwrap_or("/bin/sh".to_string()) - } -} - /// ShellBuilder is used to turn a user-requested task into a /// program that can be executed by the shell. pub struct ShellBuilder { @@ -206,7 +211,7 @@ impl ShellBuilder { let (program, args) = match remote_system_shell { Some(program) => (program.to_string(), Vec::new()), None => match shell { - Shell::System => (system_shell(), Vec::new()), + Shell::System => (get_system_shell(), Vec::new()), Shell::Program(shell) => (shell.clone(), Vec::new()), Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()), }, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index b38a69f095049c80388d3c0ec2ab397fb4d2bec4..a5e0227533cf0e3ecbc9a8f2c6c55fa1254473e3 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -344,7 +344,6 @@ pub struct TerminalBuilder { impl TerminalBuilder { pub fn new( working_directory: Option, - python_venv_directory: Option, task: Option, shell: Shell, mut env: HashMap, @@ -353,8 +352,9 @@ impl TerminalBuilder { max_scroll_history_lines: Option, is_ssh_terminal: bool, window_id: u64, - completion_tx: Sender>, + completion_tx: Option>>, cx: &App, + activation_script: Option, ) -> Result { // If the parent environment doesn't have a locale set // (As is the case when launched from a .app on MacOS), @@ -428,13 +428,10 @@ impl TerminalBuilder { .clone() .or_else(|| Some(home_dir().to_path_buf())), drain_on_exit: true, - env: env.into_iter().collect(), + env: env.clone().into_iter().collect(), } }; - // Setup Alacritty's env, which modifies the current process's environment - alacritty_terminal::tty::setup_env(); - let default_cursor_style = AlacCursorStyle::from(cursor_shape); let scrolling_history = if task.is_some() { // Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling. @@ -517,11 +514,19 @@ impl TerminalBuilder { hyperlink_regex_searches: RegexSearches::new(), vi_mode_enabled: false, is_ssh_terminal, - python_venv_directory, last_mouse_move_time: Instant::now(), last_hyperlink_search_position: None, #[cfg(windows)] shell_program, + activation_script, + template: CopyTemplate { + shell, + env, + cursor_shape, + alternate_scroll, + max_scroll_history_lines, + window_id, + }, }; Ok(TerminalBuilder { @@ -683,7 +688,7 @@ pub enum SelectionPhase { pub struct Terminal { pty_tx: Notifier, - completion_tx: Sender>, + completion_tx: Option>>, term: Arc>>, term_config: Config, events: VecDeque, @@ -695,7 +700,6 @@ pub struct Terminal { pub breadcrumb_text: String, pub pty_info: PtyProcessInfo, title_override: Option, - pub python_venv_directory: Option, scroll_px: Pixels, next_link_id: usize, selection_phase: SelectionPhase, @@ -707,6 +711,17 @@ pub struct Terminal { last_hyperlink_search_position: Option>, #[cfg(windows)] shell_program: Option, + template: CopyTemplate, + activation_script: Option, +} + +struct CopyTemplate { + shell: Shell, + env: HashMap, + cursor_shape: CursorShape, + alternate_scroll: AlternateScroll, + max_scroll_history_lines: Option, + window_id: u64, } pub struct TaskState { @@ -1895,7 +1910,9 @@ impl Terminal { } }); - self.completion_tx.try_send(e).ok(); + if let Some(tx) = &self.completion_tx { + tx.try_send(e).ok(); + } let task = match &mut self.task { Some(task) => task, None => { @@ -1950,6 +1967,28 @@ impl Terminal { pub fn vi_mode_enabled(&self) -> bool { self.vi_mode_enabled } + + pub fn clone_builder( + &self, + cx: &App, + cwd: impl FnOnce() -> Option, + ) -> Result { + let working_directory = self.working_directory().or_else(cwd); + TerminalBuilder::new( + working_directory, + None, + self.template.shell.clone(), + self.template.env.clone(), + self.template.cursor_shape, + self.template.alternate_scroll, + self.template.max_scroll_history_lines, + self.is_ssh_terminal, + self.template.window_id, + None, + cx, + self.activation_script.clone(), + ) + } } // Helper function to convert a grid row to a string @@ -2164,7 +2203,6 @@ mod tests { let (completion_tx, completion_rx) = smol::channel::unbounded(); let terminal = cx.new(|cx| { TerminalBuilder::new( - None, None, None, task::Shell::WithArguments { @@ -2178,8 +2216,9 @@ mod tests { None, false, 0, - completion_tx, + Some(completion_tx), cx, + None, ) .unwrap() .subscribe(cx) diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index c7ebd314e4618300058b1a2e083d97d3bb569df3..9759fe8337bc4a870fb6fe0a903edf5c542f5d4f 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -3,9 +3,9 @@ use async_recursion::async_recursion; use collections::HashSet; use futures::{StreamExt as _, stream::FuturesUnordered}; use gpui::{AppContext as _, AsyncWindowContext, Axis, Entity, Task, WeakEntity}; -use project::{Project, terminals::TerminalKind}; +use project::Project; use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use ui::{App, Context, Pixels, Window}; use util::ResultExt as _; @@ -246,11 +246,9 @@ async fn deserialize_pane_group( .update(cx, |workspace, cx| default_working_directory(workspace, cx)) .ok() .flatten(); - let kind = TerminalKind::Shell( - working_directory.as_deref().map(Path::to_path_buf), - ); - let terminal = - project.update(cx, |project, cx| project.create_terminal(kind, cx)); + let terminal = project.update(cx, |project, cx| { + project.create_terminal_shell(working_directory, cx) + }); Some(Some(terminal)) } else { Some(None) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index c3d7c4f793ed612a4dd819aca7552b48ccf6a3db..45e36c199048f9699c920939f7c6f8921d25c5e9 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -16,7 +16,7 @@ use gpui::{ Task, WeakEntity, Window, actions, }; use itertools::Itertools; -use project::{Fs, Project, ProjectEntryId, terminals::TerminalKind}; +use project::{Fs, Project, ProjectEntryId}; use search::{BufferSearchBar, buffer_search::DivRegistrar}; use settings::Settings; use task::{RevealStrategy, RevealTarget, ShellBuilder, SpawnInTerminal, TaskId}; @@ -376,14 +376,19 @@ impl TerminalPanel { } self.serialize(cx); } - pane::Event::Split(direction) => { - let Some(new_pane) = self.new_pane_with_cloned_active_terminal(window, cx) else { - return; - }; + &pane::Event::Split(direction) => { + let fut = self.new_pane_with_cloned_active_terminal(window, cx); let pane = pane.clone(); - let direction = *direction; - self.center.split(&pane, &new_pane, direction).log_err(); - window.focus(&new_pane.focus_handle(cx)); + cx.spawn_in(window, async move |panel, cx| { + let Some(new_pane) = fut.await else { + return; + }; + _ = panel.update_in(cx, |panel, window, cx| { + panel.center.split(&pane, &new_pane, direction).log_err(); + window.focus(&new_pane.focus_handle(cx)); + }); + }) + .detach(); } pane::Event::Focus => { self.active_pane = pane.clone(); @@ -400,57 +405,62 @@ impl TerminalPanel { &mut self, window: &mut Window, cx: &mut Context, - ) -> Option> { - let workspace = self.workspace.upgrade()?; + ) -> Task>> { + let Some(workspace) = self.workspace.upgrade() else { + return Task::ready(None); + }; let workspace = workspace.read(cx); let database_id = workspace.database_id(); let weak_workspace = self.workspace.clone(); let project = workspace.project().clone(); - let (working_directory, python_venv_directory) = self - .active_pane + let active_pane = &self.active_pane; + let terminal_view = active_pane .read(cx) .active_item() - .and_then(|item| item.downcast::()) - .map(|terminal_view| { - let terminal = terminal_view.read(cx).terminal().read(cx); - ( - terminal - .working_directory() - .or_else(|| default_working_directory(workspace, cx)), - terminal.python_venv_directory.clone(), - ) - }) - .unwrap_or((None, None)); - let kind = TerminalKind::Shell(working_directory); - let terminal = project - .update(cx, |project, cx| { - project.create_terminal_with_venv(kind, python_venv_directory, cx) - }) - .ok()?; - - let terminal_view = Box::new(cx.new(|cx| { - TerminalView::new( - terminal.clone(), - weak_workspace.clone(), - database_id, - project.downgrade(), - window, - cx, - ) - })); - let pane = new_terminal_pane( - weak_workspace, - project, - self.active_pane.read(cx).is_zoomed(), - window, - cx, - ); - self.apply_tab_bar_buttons(&pane, cx); - pane.update(cx, |pane, cx| { - pane.add_item(terminal_view, true, true, None, window, cx); + .and_then(|item| item.downcast::()); + let working_directory = terminal_view.as_ref().and_then(|terminal_view| { + let terminal = terminal_view.read(cx).terminal().read(cx); + terminal + .working_directory() + .or_else(|| default_working_directory(workspace, cx)) }); + let is_zoomed = active_pane.read(cx).is_zoomed(); + cx.spawn_in(window, async move |panel, cx| { + let terminal = project + .update(cx, |project, cx| match terminal_view { + Some(view) => Task::ready(project.clone_terminal( + &view.read(cx).terminal.clone(), + cx, + || working_directory, + )), + None => project.create_terminal_shell(working_directory, cx), + }) + .ok()? + .await + .ok()?; - Some(pane) + panel + .update_in(cx, move |terminal_panel, window, cx| { + let terminal_view = Box::new(cx.new(|cx| { + TerminalView::new( + terminal.clone(), + weak_workspace.clone(), + database_id, + project.downgrade(), + window, + cx, + ) + })); + let pane = new_terminal_pane(weak_workspace, project, is_zoomed, window, cx); + terminal_panel.apply_tab_bar_buttons(&pane, cx); + pane.update(cx, |pane, cx| { + pane.add_item(terminal_view, true, true, None, window, cx); + }); + Some(pane) + }) + .ok() + .flatten() + }) } pub fn open_terminal( @@ -465,8 +475,8 @@ impl TerminalPanel { terminal_panel .update(cx, |panel, cx| { - panel.add_terminal( - TerminalKind::Shell(Some(action.working_directory.clone())), + panel.add_terminal_shell( + Some(action.working_directory.clone()), RevealStrategy::Always, window, cx, @@ -571,15 +581,16 @@ impl TerminalPanel { ) -> Task>> { let reveal = spawn_task.reveal; let reveal_target = spawn_task.reveal_target; - let kind = TerminalKind::Task(spawn_task); match reveal_target { RevealTarget::Center => self .workspace .update(cx, |workspace, cx| { - Self::add_center_terminal(workspace, kind, window, cx) + Self::add_center_terminal(workspace, window, cx, |project, cx| { + project.create_terminal_task(spawn_task, cx) + }) }) .unwrap_or_else(|e| Task::ready(Err(e))), - RevealTarget::Dock => self.add_terminal(kind, reveal, window, cx), + RevealTarget::Dock => self.add_terminal_task(spawn_task, reveal, window, cx), } } @@ -594,11 +605,14 @@ impl TerminalPanel { return; }; - let kind = TerminalKind::Shell(default_working_directory(workspace, cx)); - terminal_panel .update(cx, |this, cx| { - this.add_terminal(kind, RevealStrategy::Always, window, cx) + this.add_terminal_shell( + default_working_directory(workspace, cx), + RevealStrategy::Always, + window, + cx, + ) }) .detach_and_log_err(cx); } @@ -660,9 +674,13 @@ impl TerminalPanel { pub fn add_center_terminal( workspace: &mut Workspace, - kind: TerminalKind, window: &mut Window, cx: &mut Context, + create_terminal: impl FnOnce( + &mut Project, + &mut Context, + ) -> Task>> + + 'static, ) -> Task>> { if !is_enabled_in_workspace(workspace, cx) { return Task::ready(Err(anyhow!( @@ -671,9 +689,7 @@ impl TerminalPanel { } let project = workspace.project().downgrade(); cx.spawn_in(window, async move |workspace, cx| { - let terminal = project - .update(cx, |project, cx| project.create_terminal(kind, cx))? - .await?; + let terminal = project.update(cx, create_terminal)?.await?; workspace.update_in(cx, |workspace, window, cx| { let terminal_view = cx.new(|cx| { @@ -692,9 +708,9 @@ impl TerminalPanel { }) } - pub fn add_terminal( + pub fn add_terminal_task( &mut self, - kind: TerminalKind, + task: SpawnInTerminal, reveal_strategy: RevealStrategy, window: &mut Window, cx: &mut Context, @@ -710,7 +726,66 @@ impl TerminalPanel { })?; let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?; let terminal = project - .update(cx, |project, cx| project.create_terminal(kind, cx))? + .update(cx, |project, cx| project.create_terminal_task(task, cx))? + .await?; + let result = workspace.update_in(cx, |workspace, window, cx| { + let terminal_view = Box::new(cx.new(|cx| { + TerminalView::new( + terminal.clone(), + workspace.weak_handle(), + workspace.database_id(), + workspace.project().downgrade(), + window, + cx, + ) + })); + + match reveal_strategy { + RevealStrategy::Always => { + workspace.focus_panel::(window, cx); + } + RevealStrategy::NoFocus => { + workspace.open_panel::(window, cx); + } + RevealStrategy::Never => {} + } + + pane.update(cx, |pane, cx| { + let focus = pane.has_focus(window, cx) + || matches!(reveal_strategy, RevealStrategy::Always); + pane.add_item(terminal_view, true, focus, None, window, cx); + }); + + Ok(terminal.downgrade()) + })?; + terminal_panel.update(cx, |terminal_panel, cx| { + terminal_panel.pending_terminals_to_add = + terminal_panel.pending_terminals_to_add.saturating_sub(1); + terminal_panel.serialize(cx) + })?; + result + }) + } + + pub fn add_terminal_shell( + &mut self, + cwd: Option, + reveal_strategy: RevealStrategy, + window: &mut Window, + cx: &mut Context, + ) -> Task>> { + let workspace = self.workspace.clone(); + cx.spawn_in(window, async move |terminal_panel, cx| { + if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? { + anyhow::bail!("terminal not yet supported for remote projects"); + } + let pane = terminal_panel.update(cx, |terminal_panel, _| { + terminal_panel.pending_terminals_to_add += 1; + terminal_panel.active_pane.clone() + })?; + let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?; + let terminal = project + .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))? .await?; let result = workspace.update_in(cx, |workspace, window, cx| { let terminal_view = Box::new(cx.new(|cx| { @@ -819,7 +894,7 @@ impl TerminalPanel { })??; let new_terminal = project .update(cx, |project, cx| { - project.create_terminal(TerminalKind::Task(spawn_task), cx) + project.create_terminal_task(spawn_task, cx) })? .await?; terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| { @@ -1248,18 +1323,29 @@ impl Render for TerminalPanel { let panes = terminal_panel.center.panes(); if let Some(&pane) = panes.get(action.0) { window.focus(&pane.read(cx).focus_handle(cx)); - } else if let Some(new_pane) = - terminal_panel.new_pane_with_cloned_active_terminal(window, cx) - { - terminal_panel - .center - .split( - &terminal_panel.active_pane, - &new_pane, - SplitDirection::Right, - ) - .log_err(); - window.focus(&new_pane.focus_handle(cx)); + } else { + let future = + terminal_panel.new_pane_with_cloned_active_terminal(window, cx); + cx.spawn_in(window, async move |terminal_panel, cx| { + if let Some(new_pane) = future.await { + _ = terminal_panel.update_in( + cx, + |terminal_panel, window, cx| { + terminal_panel + .center + .split( + &terminal_panel.active_pane, + &new_pane, + SplitDirection::Right, + ) + .log_err(); + let new_pane = new_pane.read(cx); + window.focus(&new_pane.focus_handle(cx)); + }, + ); + } + }) + .detach(); } }), ) @@ -1395,13 +1481,14 @@ impl Panel for TerminalPanel { return; } cx.defer_in(window, |this, window, cx| { - let Ok(kind) = this.workspace.update(cx, |workspace, cx| { - TerminalKind::Shell(default_working_directory(workspace, cx)) - }) else { + let Ok(kind) = this + .workspace + .update(cx, |workspace, cx| default_working_directory(workspace, cx)) + else { return; }; - this.add_terminal(kind, RevealStrategy::Always, window, cx) + this.add_terminal_shell(kind, RevealStrategy::Always, window, cx) .detach_and_log_err(cx) }) } diff --git a/crates/terminal_view/src/terminal_path_like_target.rs b/crates/terminal_view/src/terminal_path_like_target.rs index e20df7f0010480d782eb16375ca3480d4f390742..226a8f4c3d9bca398df778fa2043fb5872383b56 100644 --- a/crates/terminal_view/src/terminal_path_like_target.rs +++ b/crates/terminal_view/src/terminal_path_like_target.rs @@ -364,7 +364,7 @@ fn possibly_open_target( mod tests { use super::*; use gpui::TestAppContext; - use project::{Project, terminals::TerminalKind}; + use project::Project; use serde_json::json; use std::path::{Path, PathBuf}; use terminal::{HoveredWord, alacritty_terminal::index::Point as AlacPoint}; @@ -405,8 +405,8 @@ mod tests { app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let terminal = project - .update(cx, |project, cx| { - project.create_terminal(TerminalKind::Shell(None), cx) + .update(cx, |project: &mut Project, cx| { + project.create_terminal_shell(None, cx) }) .await .expect("Failed to create a terminal"); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 9aa855acb7aa18a0431fcfc07e7a32932162e4f2..9e479464af224c4d85119d6ca2e0b25c360f9c3d 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -15,7 +15,7 @@ use gpui::{ deferred, div, }; use persistence::TERMINAL_DB; -use project::{Project, search::SearchQuery, terminals::TerminalKind}; +use project::{Project, search::SearchQuery}; use schemars::JsonSchema; use task::TaskId; use terminal::{ @@ -204,12 +204,9 @@ impl TerminalView { cx: &mut Context, ) { let working_directory = default_working_directory(workspace, cx); - TerminalPanel::add_center_terminal( - workspace, - TerminalKind::Shell(working_directory), - window, - cx, - ) + TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| { + project.create_terminal_shell(working_directory, cx) + }) .detach_and_log_err(cx); } @@ -1333,16 +1330,10 @@ impl Item for TerminalView { let terminal = self .project .update(cx, |project, cx| { - let terminal = self.terminal().read(cx); - let working_directory = terminal - .working_directory() - .or_else(|| Some(project.active_project_directory(cx)?.to_path_buf())); - let python_venv_directory = terminal.python_venv_directory.clone(); - project.create_terminal_with_venv( - TerminalKind::Shell(working_directory), - python_venv_directory, - cx, - ) + let cwd = project + .active_project_directory(cx) + .map(|it| it.to_path_buf()); + project.clone_terminal(self.terminal(), cx, || cwd) }) .ok()? .log_err()?; @@ -1498,9 +1489,7 @@ impl SerializableItem for TerminalView { .flatten(); let terminal = project - .update(cx, |project, cx| { - project.create_terminal(TerminalKind::Shell(cwd), cx) - })? + .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))? .await?; cx.update(|window, cx| { cx.new(|cx| { diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 69a2c88706bff8b459ec7b678976a84ae4f943bf..0aceec5d7ae4b672afc6111bd4f2389d7b1b6af7 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -1057,6 +1057,18 @@ pub fn get_system_shell() -> String { } } +pub fn get_default_system_shell() -> String { + #[cfg(target_os = "windows")] + { + get_windows_system_shell() + } + + #[cfg(not(target_os = "windows"))] + { + "/bin/sh".to_string() + } +} + #[derive(Debug)] pub enum ConnectionResult { Timeout, diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index c4ba93bcec60f158d22904674996ba63202a64a6..3ef9ff65eb0fe5aedfd5e72aa18f1481a011fce7 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -824,7 +824,6 @@ impl WorkspaceDb { conn.exec_bound( sql!( DELETE FROM breakpoints WHERE workspace_id = ?1; - DELETE FROM toolchains WHERE workspace_id = ?1; ) )?(workspace.id).context("Clearing old breakpoints")?; @@ -1097,7 +1096,6 @@ impl WorkspaceDb { query! { pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> { - DELETE FROM toolchains WHERE workspace_id = ?1; DELETE FROM workspaces WHERE workspace_id IS ? } @@ -1424,24 +1422,24 @@ impl WorkspaceDb { &self, workspace_id: WorkspaceId, worktree_id: WorktreeId, - relative_path: String, + relative_worktree_path: String, language_name: LanguageName, ) -> Result> { self.write(move |this| { let mut select = this .select_bound(sql!( - SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? AND relative_path = ? + SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? AND relative_worktree_path = ? )) - .context("Preparing insertion")?; + .context("select toolchain")?; let toolchain: Vec<(String, String, String)> = - select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize(), relative_path))?; + select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize(), relative_worktree_path))?; Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain { name: name.into(), path: path.into(), language_name, - as_json: serde_json::Value::from_str(&raw_json).ok()? + as_json: serde_json::Value::from_str(&raw_json).ok()?, }))) }) .await @@ -1456,7 +1454,7 @@ impl WorkspaceDb { .select_bound(sql!( SELECT name, path, worktree_id, relative_worktree_path, language_name, raw_json FROM toolchains WHERE workspace_id = ? )) - .context("Preparing insertion")?; + .context("select toolchains")?; let toolchain: Vec<(String, String, u64, String, String, String)> = select(workspace_id)?; @@ -1465,7 +1463,7 @@ impl WorkspaceDb { name: name.into(), path: path.into(), language_name: LanguageName::new(&language_name), - as_json: serde_json::Value::from_str(&raw_json).ok()? + as_json: serde_json::Value::from_str(&raw_json).ok()?, }, WorktreeId::from_proto(worktree_id), Arc::from(relative_worktree_path.as_ref())))).collect()) }) .await From c8e99125bdeafb08cb185c64251bf6ea1a523564 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Thu, 28 Aug 2025 20:58:22 +0530 Subject: [PATCH 410/744] language_models: Fix tool calling for `x-ai/grok-code-fast-1` model via OpenRouter (#37094) Closes #37022 Closes #36994 This update ensures all Grok models use the JsonSchemaSubset format for tool schemas. A previous fix for this issue was too specific, only targeting grok-4 models. This caused other variants, like grok-code-fast-1, to be missed. We've now broadened the logic to correctly apply the setting to the entire Grok model family. Release Notes: - Fix tool calling for `x-ai/grok-code-fast-1` model via OpenRouter. --- crates/language_models/src/provider/open_router.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 8f2abfce35852c617ddee22de18432908660fe95..aaa0bd620ccf1a961b8c97c0c9fe3ba348b51cca 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -381,7 +381,7 @@ impl LanguageModel for OpenRouterLanguageModel { fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { let model_id = self.model.id().trim().to_lowercase(); - if model_id.contains("gemini") || model_id.contains("grok-4") { + if model_id.contains("gemini") || model_id.contains("grok") { LanguageModelToolSchemaFormat::JsonSchemaSubset } else { LanguageModelToolSchemaFormat::JsonSchema From 2cb697e9f42a5b5988e5d3ab73ffb18fd112eb20 Mon Sep 17 00:00:00 2001 From: Liam <33645555+lj3954@users.noreply.github.com> Date: Thu, 28 Aug 2025 15:32:15 +0000 Subject: [PATCH 411/744] copilot: Use updated Copilot Chat model schema (#33007) Use the latest Copilot Chat model schema, matching what is used in VSCode, to get more data about available models than was previously accessible. Replace hardcoded default model (gpt-4.1) with the default model included in JSON. Other data like premium request multipliers could be used in the future if Zed implements a way for models to display additional details about themselves, such as with tooltips on hover. Release Notes: - N/A --------- Co-authored-by: Peter Tripp --- crates/copilot/src/copilot_chat.rs | 52 +++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/crates/copilot/src/copilot_chat.rs b/crates/copilot/src/copilot_chat.rs index e8e2251648e4b941fe616b4524337fe565513950..bfddba0e2f8a41e3ed234b21ee52454d104c9dd2 100644 --- a/crates/copilot/src/copilot_chat.rs +++ b/crates/copilot/src/copilot_chat.rs @@ -62,12 +62,6 @@ impl CopilotChatConfiguration { } } -// Copilot's base model; defined by Microsoft in premium requests table -// This will be moved to the front of the Copilot model list, and will be used for -// 'fast' requests (e.g. title generation) -// https://docs.github.com/en/copilot/managing-copilot/monitoring-usage-and-entitlements/about-premium-requests -const DEFAULT_MODEL_ID: &str = "gpt-4.1"; - #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Role { @@ -101,22 +95,39 @@ where Ok(models) } -#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] pub struct Model { + billing: ModelBilling, capabilities: ModelCapabilities, id: String, name: String, policy: Option, vendor: ModelVendor, + is_chat_default: bool, + // The model with this value true is selected by VSCode copilot if a premium request limit is + // reached. Zed does not currently implement this behaviour + is_chat_fallback: bool, model_picker_enabled: bool, } +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] +struct ModelBilling { + is_premium: bool, + multiplier: f64, + // List of plans a model is restricted to + // Field is not present if a model is available for all plans + #[serde(default)] + restricted_to: Option>, +} + #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] struct ModelCapabilities { family: String, #[serde(default)] limits: ModelLimits, supports: ModelSupportedFeatures, + #[serde(rename = "type")] + model_type: String, } #[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] @@ -602,6 +613,7 @@ async fn get_models( .into_iter() .filter(|model| { model.model_picker_enabled + && model.capabilities.model_type.as_str() == "chat" && model .policy .as_ref() @@ -610,9 +622,7 @@ async fn get_models( .dedup_by(|a, b| a.capabilities.family == b.capabilities.family) .collect(); - if let Some(default_model_position) = - models.iter().position(|model| model.id == DEFAULT_MODEL_ID) - { + if let Some(default_model_position) = models.iter().position(|model| model.is_chat_default) { let default_model = models.remove(default_model_position); models.insert(0, default_model); } @@ -630,7 +640,9 @@ async fn request_models( .uri(models_url.as_ref()) .header("Authorization", format!("Bearer {}", api_token)) .header("Content-Type", "application/json") - .header("Copilot-Integration-Id", "vscode-chat"); + .header("Copilot-Integration-Id", "vscode-chat") + .header("Editor-Version", "vscode/1.103.2") + .header("x-github-api-version", "2025-05-01"); let request = request_builder.body(AsyncBody::empty())?; @@ -801,6 +813,10 @@ mod tests { let json = r#"{ "data": [ { + "billing": { + "is_premium": false, + "multiplier": 0 + }, "capabilities": { "family": "gpt-4", "limits": { @@ -814,6 +830,8 @@ mod tests { "type": "chat" }, "id": "gpt-4", + "is_chat_default": false, + "is_chat_fallback": false, "model_picker_enabled": false, "name": "GPT 4", "object": "model", @@ -825,6 +843,16 @@ mod tests { "some-unknown-field": 123 }, { + "billing": { + "is_premium": true, + "multiplier": 1, + "restricted_to": [ + "pro", + "pro_plus", + "business", + "enterprise" + ] + }, "capabilities": { "family": "claude-3.7-sonnet", "limits": { @@ -848,6 +876,8 @@ mod tests { "type": "chat" }, "id": "claude-3.7-sonnet", + "is_chat_default": false, + "is_chat_fallback": false, "model_picker_enabled": true, "name": "Claude 3.7 Sonnet", "object": "model", From 4b0609840b6ba78e7b9c0e4b7e772914a8cb311d Mon Sep 17 00:00:00 2001 From: Kai Ren Date: Thu, 28 Aug 2025 17:40:48 +0200 Subject: [PATCH 412/744] go: Fix highlighting of fields (#37026) Closes #36420 ## Synopsis The issue in #36420 is caused by #7276, which bound the appropriate tree-sitter queries to the `@variable.member` color. However, I have found neither this color's declaration nor its other usages in the codebase (neither on the latest `main` nor on 79c1003b344ee513cf97ee8313c38c7c3f02c916). Other languages use for such situations the `@property` color. ## Solution Just change the used `@variable.member` color to the `@property` one. Seems fully inline with the changes illustrated in #7276. ## Screenshots Screenshot 2025-08-28 at 13 18 38 Screenshot 2025-08-28 at 13 20 08 ## Changelog Release Notes: - go: Fixed highlighting of fields. --- crates/languages/src/go.rs | 3 ++- crates/languages/src/go/highlights.scm | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 24e2ca2f56fff5ba1a3d92ca5e0bf16ac1a9463b..86f8e1faaa969449f45f38ee5cf8e8cde9ccff29 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -764,6 +764,7 @@ mod tests { let highlight_type = grammar.highlight_id_for_name("type").unwrap(); let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap(); let highlight_number = grammar.highlight_id_for_name("number").unwrap(); + let highlight_field = grammar.highlight_id_for_name("property").unwrap(); assert_eq!( adapter @@ -828,7 +829,7 @@ mod tests { Some(CodeLabel { text: "two.Three a.Bcd".to_string(), filter_range: 0..9, - runs: vec![(12..15, highlight_type)], + runs: vec![(4..9, highlight_field), (12..15, highlight_type)], }) ); } diff --git a/crates/languages/src/go/highlights.scm b/crates/languages/src/go/highlights.scm index 5aa23fca90b7e0295fc08af6a75a038e3abb0e3a..bb0eaab88a1c0c79a04496d453831cf396d706b6 100644 --- a/crates/languages/src/go/highlights.scm +++ b/crates/languages/src/go/highlights.scm @@ -1,13 +1,13 @@ (identifier) @variable (type_identifier) @type -(field_identifier) @variable.member +(field_identifier) @property (package_identifier) @namespace (keyed_element . (literal_element - (identifier) @variable.member)) + (identifier) @property)) (call_expression function: (identifier) @function) From 4ef9294123d639a270d8932944654ff23b2cf286 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Thu, 28 Aug 2025 18:44:30 +0200 Subject: [PATCH 413/744] html: Add outline (#37098) We were missing an outline definition for HTML flies, hence this PR adds one for that image Release Notes: - N/A --- extensions/html/languages/html/outline.scm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extensions/html/languages/html/outline.scm b/extensions/html/languages/html/outline.scm index a9b95da55651bde5a28691ca1ca92d636a444e8c..e7f9dc4fab01b89e68a2b668425fc7655b7d275e 100644 --- a/extensions/html/languages/html/outline.scm +++ b/extensions/html/languages/html/outline.scm @@ -1 +1,5 @@ (comment) @annotation + +(element + (start_tag + (tag_name) @name)) @item From 29fc324a78d5fdf664cec63c950df59135114634 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Thu, 28 Aug 2025 19:07:06 +0200 Subject: [PATCH 414/744] html: Bump to v0.2.2 (#37102) This PR bumps the HTML extension to v0.2.2. Changes: - https://github.com/zed-industries/zed/pull/28184 - https://github.com/zed-industries/zed/pull/36948 - https://github.com/zed-industries/zed/pull/37098 Release Notes: - N/A --- Cargo.lock | 2 +- extensions/html/Cargo.toml | 2 +- extensions/html/extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8bddc8b008c1a1f048c11ecbb04d5f092ed19d5a..4ecd8b42c79dee10549a73edfafd302e981ff488 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20590,7 +20590,7 @@ dependencies = [ [[package]] name = "zed_html" -version = "0.2.1" +version = "0.2.2" dependencies = [ "zed_extension_api 0.1.0", ] diff --git a/extensions/html/Cargo.toml b/extensions/html/Cargo.toml index eacafeb2e4c7e2b663cb0353ae8fa5ad4640bc86..27425da67185b80935c55b8bdc9e02088650a06b 100644 --- a/extensions/html/Cargo.toml +++ b/extensions/html/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_html" -version = "0.2.1" +version = "0.2.2" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/html/extension.toml b/extensions/html/extension.toml index 0f6ea79f56d7f3b3d94aa0e987053f344860967a..c1d45d40602c6a2a87864cca3b534f465266b0a4 100644 --- a/extensions/html/extension.toml +++ b/extensions/html/extension.toml @@ -1,7 +1,7 @@ id = "html" name = "HTML" description = "HTML support." -version = "0.2.1" +version = "0.2.2" schema_version = 1 authors = ["Isaac Clayton "] repository = "https://github.com/zed-industries/zed" From 4469b14512ec69168356b21db424da9f327066e3 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 28 Aug 2025 14:15:08 -0400 Subject: [PATCH 415/744] collab_ui: Show channel list while reconnecting (#37107) This PR makes it so the channel list will still be shown while reconnecting to Collab instead of showing the signed-out state. In order to model the transitional states that occur while reconnecting, we needed to introduce a new `Status::Reauthenticated` state that we go through when signing in as part of a reconnect. This is because we cannot tell from `Status::Authenticated` alone if we're authenticating for the first time or reauthenticating. Release Notes: - N/A --- crates/client/src/client.rs | 32 +++++++++++++++++++++++++--- crates/client/src/user.rs | 4 +++- crates/collab_ui/src/collab_panel.rs | 2 +- crates/workspace/src/workspace.rs | 3 ++- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 2bbe7dd1b5a838c1f4e3bace2d91c396692983f4..bdbf049b75ef1e0de351c65be7382a94d73448e6 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -287,6 +287,7 @@ pub enum Status { }, ConnectionLost, Reauthenticating, + Reauthenticated, Reconnecting, ReconnectionError { next_reconnection: Instant, @@ -298,6 +299,21 @@ impl Status { matches!(self, Self::Connected { .. }) } + pub fn was_connected(&self) -> bool { + matches!( + self, + Self::ConnectionLost + | Self::Reauthenticating + | Self::Reauthenticated + | Self::Reconnecting + ) + } + + /// Returns whether the client is currently connected or was connected at some point. + pub fn is_or_was_connected(&self) -> bool { + self.is_connected() || self.was_connected() + } + pub fn is_signing_in(&self) -> bool { matches!( self, @@ -857,11 +873,13 @@ impl Client { try_provider: bool, cx: &AsyncApp, ) -> Result { - if self.status().borrow().is_signed_out() { + let is_reauthenticating = if self.status().borrow().is_signed_out() { self.set_status(Status::Authenticating, cx); + false } else { self.set_status(Status::Reauthenticating, cx); - } + true + }; let mut credentials = None; @@ -919,7 +937,14 @@ impl Client { self.cloud_client .set_credentials(credentials.user_id as u32, credentials.access_token.clone()); self.state.write().credentials = Some(credentials.clone()); - self.set_status(Status::Authenticated, cx); + self.set_status( + if is_reauthenticating { + Status::Reauthenticated + } else { + Status::Authenticated + }, + cx, + ); Ok(credentials) } @@ -1034,6 +1059,7 @@ impl Client { | Status::Authenticating | Status::AuthenticationError | Status::Reauthenticating + | Status::Reauthenticated | Status::ReconnectionError { .. } => false, Status::Connected { .. } | Status::Connecting | Status::Reconnecting => { return ConnectionResult::Result(Ok(())); diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index d23eb37519c00c2567683e5417aa2d82f10a2f58..a4c66e582c34e468432747d580e13c86b3ec33c8 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -216,7 +216,9 @@ impl UserStore { return Ok(()); }; match status { - Status::Authenticated | Status::Connected { .. } => { + Status::Authenticated + | Status::Reauthenticated + | Status::Connected { .. } => { if let Some(user_id) = client.user_id() { let response = client .cloud_client() diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index d85a6610a5b2fadde46f27be2602f62c6b8b7d62..90096542942e18ff9a0355d6319e5dcf590a870c 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -3047,7 +3047,7 @@ impl Render for CollabPanel { .on_action(cx.listener(CollabPanel::move_channel_down)) .track_focus(&self.focus_handle) .size_full() - .child(if !self.client.status().borrow().is_connected() { + .child(if !self.client.status().borrow().is_or_was_connected() { self.render_signed_out(cx) } else { self.render_signed_in(window, cx) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1fee2793f0c50efee9f2fd8040d3bcf8df2af08a..76693e716e3cafe31e60eeecfcaeeb5bb267fb77 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6890,7 +6890,8 @@ async fn join_channel_internal( | Status::Authenticating | Status::Authenticated | Status::Reconnecting - | Status::Reauthenticating => continue, + | Status::Reauthenticating + | Status::Reauthenticated => continue, Status::Connected { .. } => break 'outer, Status::SignedOut | Status::AuthenticationError => { return Err(ErrorCode::SignedOut.into()); From 27777d4b8f8b59e58de35688eb1ce4ce66061053 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 28 Aug 2025 14:18:25 -0400 Subject: [PATCH 416/744] Have ACP respect always_allow_tool_actions (#37104) Release Notes: - ACP agents now respect the always_allow_tool_actions setting --- crates/agent_servers/src/acp.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index bca47101d6a644a6804acea57ea6d5e887b8bac6..d929d1fc501fb2093f47f8bdeb4d3695b7b87ebf 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -3,6 +3,7 @@ use acp_thread::AgentConnection; use acp_tools::AcpConnectionRegistry; use action_log::ActionLog; use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; +use agent_settings::AgentSettings; use anyhow::anyhow; use collections::HashMap; use futures::AsyncBufReadExt as _; @@ -10,6 +11,7 @@ use futures::channel::oneshot; use futures::io::BufReader; use project::Project; use serde::Deserialize; +use settings::Settings as _; use std::{any::Any, cell::RefCell}; use std::{path::Path, rc::Rc}; use thiserror::Error; @@ -342,6 +344,28 @@ impl acp::Client for ClientDelegate { arguments: acp::RequestPermissionRequest, ) -> Result { let cx = &mut self.cx.clone(); + + // If always_allow_tool_actions is enabled, then auto-choose the first "Allow" button + if AgentSettings::try_read_global(cx, |settings| settings.always_allow_tool_actions) + .unwrap_or(false) + { + // Don't use AllowAlways, because then if you were to turn off always_allow_tool_actions, + // some tools would (incorrectly) continue to auto-accept. + if let Some(allow_once_option) = arguments.options.iter().find_map(|option| { + if matches!(option.kind, acp::PermissionOptionKind::AllowOnce) { + Some(option.id.clone()) + } else { + None + } + }) { + return Ok(acp::RequestPermissionResponse { + outcome: acp::RequestPermissionOutcome::Selected { + option_id: allow_once_option, + }, + }); + } + } + let rx = self .sessions .borrow() From 909d7215c084458f200c9c713d53c9d5070dddde Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 28 Aug 2025 14:49:03 -0400 Subject: [PATCH 417/744] Update patch and nightly release docs (#37109) Release Notes: - N/A --- docs/src/development/releases.md | 56 ++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/docs/src/development/releases.md b/docs/src/development/releases.md index 80458b0cdf88faa67f5367ae69e0a12c3d094351..d1f99401d6b78545c34a64b47a146cecacc7eec1 100644 --- a/docs/src/development/releases.md +++ b/docs/src/development/releases.md @@ -37,16 +37,17 @@ Credentials for various services used in this process can be found in 1Password. 1. Check the release assets. - Ensure the stable and preview release jobs have finished without error. - - Ensure each build has the proper number of assets—releases currently have 10 assets each. - - Download the artifacts for each release and test that you can run them locally. + - Ensure each draft has the proper number of assets—releases currently have 10 assets each. + - Download the artifacts for each release draft and test that you can run them locally. -1. Publish each build, one at a time. +1. Publish stable / preview drafts, one at a time. - - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the status of each build. + - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the progress of the website rebuild. + The release will be public once the rebuild has completed. 1. Publish the release email that has been sent to [Kit](https://kit.com). - - Make sure to double check that the email is correct before publishing. + - Make sure to double-check that the email is correct before publishing. - We sometimes correct things here and there that didn't translate from GitHub's renderer to Kit's. 1. Build social media posts based on the popular items in stable. @@ -57,22 +58,43 @@ Credentials for various services used in this process can be found in 1Password. ## Patch release process -If your PR fixes a panic or a crash, you should cherry-pick it to the current stable and preview branches. If your PR fixes a regression in recently released code, you should cherry-pick it to preview. +If your PR fixes a panic or a crash, you should cherry-pick it to the current stable and preview branches. +If your PR fixes a regression in recently released code, you should cherry-pick it to preview. You will need write access to the Zed repository to do this: -- Send a PR containing your change to `main` as normal. -- Leave a comment on the PR `/cherry-pick v0.XXX.x`. Once your PR is merged, the GitHub bot will send a PR to the branch. - - In case of a merge conflict, you will have to cherry-pick manually and push the change to the `v0.XXX.x` branch. -- After the commits are cherry-picked onto the branch, run `./script/trigger-release {preview|stable}`. This will bump the version numbers, create a new release tag, and kick off a release build. - - This can also be run from the [GitHub Actions UI](https://github.com/zed-industries/zed/actions/workflows/bump_patch_version.yml): - ![](https://github.com/zed-industries/zed/assets/1486634/9e31ae95-09e1-4c7f-9591-944f4f5b63ea) -- Wait for the builds to appear on [the Releases tab on GitHub](https://github.com/zed-industries/zed/releases) (typically takes around 30 minutes) -- Proof-read and edit the release notes as needed. -- Download the artifacts for each release and test that you can run them locally. -- Publish the release. +--- + +1. Send a PR containing your change to `main` as normal. + +1. Once it is merged, cherry-pick the commit locally to either of the release branches (`v0.XXX.x`). + + - In some cases, you may have to handle a merge conflict. + More often than not, this will happen when cherry-picking to stable, as the stable branch is more "stale" than the preview branch. + +1. After the commit is cherry-picked, run `./script/trigger-release {preview|stable}`. + This will bump the version numbers, create a new release tag, and kick off a release build. + + - This can also be run from the [GitHub Actions UI](https://github.com/zed-industries/zed/actions/workflows/bump_patch_version.yml): + ![](https://github.com/zed-industries/zed/assets/1486634/9e31ae95-09e1-4c7f-9591-944f4f5b63ea) + +1. Once release drafts are up on [GitHub Releases](https://github.com/zed-industries/zed/releases), proofread and edit the release notes as needed and **save**. + + - **Do not publish the drafts, yet.** + +1. Check the release assets. + + - Ensure the stable / preview release jobs have finished without error. + - Ensure each draft has the proper number of assets—releases currently have 10 assets each. + - Download the artifacts for each release draft and test that you can run them locally. + +1. Publish stable / preview drafts, one at a time. + + - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the progress of the website rebuild. + The release will be public once the rebuild has completed. ## Nightly release process In addition to the public releases, we also have a nightly build that we encourage employees to use. -Nightly is released by cron once a day, and can be shipped as often as you'd like. There are no release notes or announcements, so you can just merge your changes to main and run `./script/trigger-release nightly`. +Nightly is released by cron once a day, and can be shipped as often as you'd like. +There are no release notes or announcements, so you can just merge your changes to main and run `./script/trigger-release nightly`. From 69933d5b81c6a21db45cf27de1a19e68d4f1f3ac Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 28 Aug 2025 12:59:31 -0600 Subject: [PATCH 418/744] Add support for Claude Code auth (#37103) Co-authored-by: Antonio Scandurra Closes #ISSUE Release Notes: - N/A Co-authored-by: Antonio Scandurra --- crates/agent_servers/src/custom.rs | 15 ++- crates/agent_ui/src/acp/thread_view.rs | 125 ++++++++++++++++++++- crates/terminal_view/src/terminal_panel.rs | 2 +- 3 files changed, 136 insertions(+), 6 deletions(-) diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index 75928a26a8b4499d7b6ced8a8392191ac3ca2f32..0669e0a68ef019975fdbdcbe155fa3dc6aeb0b96 100644 --- a/crates/agent_servers/src/custom.rs +++ b/crates/agent_servers/src/custom.rs @@ -2,6 +2,7 @@ use crate::{AgentServerCommand, AgentServerSettings}; use acp_thread::AgentConnection; use anyhow::Result; use gpui::{App, Entity, SharedString, Task}; +use language_models::provider::anthropic::AnthropicLanguageModelProvider; use project::Project; use std::{path::Path, rc::Rc}; use ui::IconName; @@ -49,10 +50,22 @@ impl crate::AgentServer for CustomAgentServer { cx: &mut App, ) -> Task>> { let server_name = self.name(); - let command = self.command.clone(); + let mut command = self.command.clone(); let root_dir = root_dir.to_path_buf(); + // TODO: Remove this once we have Claude properly cx.spawn(async move |mut cx| { + if let Some(api_key) = cx + .update(AnthropicLanguageModelProvider::api_key)? + .await + .ok() + { + command + .env + .get_or_insert_default() + .insert("ANTHROPIC_API_KEY".to_owned(), api_key.key); + } + crate::acp::connect(server_name, command, &root_dir, &mut cx).await }) } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 57d90734ef00d1160eb017d1e1257f63577cbebc..2b18ebcd1d72e721e57d91469c835cb70e7812f4 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -9,7 +9,7 @@ use agent_client_protocol::{self as acp, PromptCapabilities}; use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore}; -use anyhow::bail; +use anyhow::{Result, anyhow, bail}; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; use client::zed_urls; @@ -18,6 +18,7 @@ use editor::scroll::Autoscroll; use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; use fs::Fs; +use futures::FutureExt as _; use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, @@ -39,6 +40,8 @@ use std::path::Path; use std::sync::Arc; use std::time::Instant; use std::{collections::BTreeMap, rc::Rc, time::Duration}; +use task::SpawnInTerminal; +use terminal_view::terminal_panel::TerminalPanel; use text::Anchor; use theme::ThemeSettings; use ui::{ @@ -93,6 +96,10 @@ impl ThreadError { error.downcast_ref::() { Self::ModelRequestLimitReached(error.plan) + } else if let Some(acp_error) = error.downcast_ref::() + && acp_error.code == acp::ErrorCode::AUTH_REQUIRED.code + { + Self::AuthenticationRequired(acp_error.message.clone().into()) } else { let string = error.to_string(); // TODO: we should have Gemini return better errors here. @@ -898,7 +905,7 @@ impl AcpThreadView { fn send_impl( &mut self, - contents: Task, Vec>)>>, + contents: Task, Vec>)>>, window: &mut Window, cx: &mut Context, ) { @@ -1234,6 +1241,31 @@ impl AcpThreadView { }); return; } + } else if method.0.as_ref() == "anthropic-api-key" { + let registry = LanguageModelRegistry::global(cx); + let provider = registry + .read(cx) + .provider(&language_model::ANTHROPIC_PROVIDER_ID) + .unwrap(); + if !provider.is_authenticated(cx) { + let this = cx.weak_entity(); + let agent = self.agent.clone(); + let connection = connection.clone(); + window.defer(cx, |window, cx| { + Self::handle_auth_required( + this, + AuthRequired { + description: Some("ANTHROPIC_API_KEY must be set".to_owned()), + provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID), + }, + agent, + connection, + window, + cx, + ); + }); + return; + } } else if method.0.as_ref() == "vertex-ai" && std::env::var("GOOGLE_API_KEY").is_err() && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err() @@ -1265,7 +1297,15 @@ impl AcpThreadView { self.thread_error.take(); configuration_view.take(); pending_auth_method.replace(method.clone()); - let authenticate = connection.authenticate(method, cx); + let authenticate = if method.0.as_ref() == "claude-login" { + if let Some(workspace) = self.workspace.upgrade() { + Self::spawn_claude_login(&workspace, window, cx) + } else { + Task::ready(Ok(())) + } + } else { + connection.authenticate(method, cx) + }; cx.notify(); self.auth_task = Some(cx.spawn_in(window, { @@ -1289,6 +1329,13 @@ impl AcpThreadView { this.update_in(cx, |this, window, cx| { if let Err(err) = result { + if let ThreadState::Unauthenticated { + pending_auth_method, + .. + } = &mut this.thread_state + { + pending_auth_method.take(); + } this.handle_thread_error(err, cx); } else { this.thread_state = Self::initial_state( @@ -1307,6 +1354,76 @@ impl AcpThreadView { })); } + fn spawn_claude_login( + workspace: &Entity, + window: &mut Window, + cx: &mut App, + ) -> Task> { + let Some(terminal_panel) = workspace.read(cx).panel::(cx) else { + return Task::ready(Ok(())); + }; + let project = workspace.read(cx).project().read(cx); + let cwd = project.first_project_directory(cx); + let shell = project.terminal_settings(&cwd, cx).shell.clone(); + + let terminal = terminal_panel.update(cx, |terminal_panel, cx| { + terminal_panel.spawn_task( + &SpawnInTerminal { + id: task::TaskId("claude-login".into()), + full_label: "claude /login".to_owned(), + label: "claude /login".to_owned(), + command: Some("claude".to_owned()), + args: vec!["/login".to_owned()], + command_label: "claude /login".to_owned(), + cwd, + use_new_terminal: true, + allow_concurrent_runs: true, + hide: task::HideStrategy::Always, + shell, + ..Default::default() + }, + window, + cx, + ) + }); + cx.spawn(async move |cx| { + let terminal = terminal.await?; + let mut exit_status = terminal + .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))? + .fuse(); + + let logged_in = cx + .spawn({ + let terminal = terminal.clone(); + async move |cx| { + loop { + cx.background_executor().timer(Duration::from_secs(1)).await; + let content = + terminal.update(cx, |terminal, _cx| terminal.get_content())?; + if content.contains("Login successful") { + return anyhow::Ok(()); + } + } + } + }) + .fuse(); + futures::pin_mut!(logged_in); + futures::select_biased! { + result = logged_in => { + if let Err(e) = result { + log::error!("{e}"); + return Err(anyhow!("exited before logging in")); + } + } + _ = exit_status => { + return Err(anyhow!("exited before logging in")); + } + } + terminal.update(cx, |terminal, _| terminal.kill_active_task())?; + Ok(()) + }) + } + fn authorize_tool_call( &mut self, tool_call_id: acp::ToolCallId, @@ -4024,7 +4141,7 @@ impl AcpThreadView { workspace: Entity, window: &mut Window, cx: &mut App, - ) -> Task> { + ) -> Task> { let markdown_language_task = workspace .read(cx) .app_state() diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 45e36c199048f9699c920939f7c6f8921d25c5e9..848737aeb24ef52a6819e57882ab022edef94e25 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -485,7 +485,7 @@ impl TerminalPanel { .detach_and_log_err(cx); } - fn spawn_task( + pub fn spawn_task( &mut self, task: &SpawnInTerminal, window: &mut Window, From 47aaaa8bcffeb00756a824f0628f48ae6af782af Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Thu, 28 Aug 2025 13:32:30 -0600 Subject: [PATCH 419/744] Make `SanitizedPath` wrap `Path` instead of `Arc` to avoid allocation (#37106) Release Notes: - N/A --- .../src/file_command.rs | 2 +- crates/fs/src/fs.rs | 8 +- crates/fs/src/fs_watcher.rs | 4 +- crates/gpui/src/platform/windows/platform.rs | 2 +- crates/paths/src/paths.rs | 2 +- crates/project/src/git_store.rs | 3 +- crates/project/src/lsp_store.rs | 4 +- crates/project/src/project.rs | 5 +- crates/project/src/worktree_store.rs | 27 ++-- crates/remote/src/transport/ssh.rs | 2 +- crates/util/src/paths.rs | 122 +++++++++++++----- crates/workspace/src/path_list.rs | 2 +- crates/workspace/src/workspace.rs | 2 +- crates/worktree/src/worktree.rs | 56 ++++---- 14 files changed, 156 insertions(+), 85 deletions(-) diff --git a/crates/assistant_slash_commands/src/file_command.rs b/crates/assistant_slash_commands/src/file_command.rs index a973d653e4527be808618f76d60af59e4a891947..261e15bc0ae8b9e886d4d146696db78e5c0c831d 100644 --- a/crates/assistant_slash_commands/src/file_command.rs +++ b/crates/assistant_slash_commands/src/file_command.rs @@ -492,7 +492,7 @@ mod custom_path_matcher { pub fn new(globs: &[String]) -> Result { let globs = globs .iter() - .map(|glob| Glob::new(&SanitizedPath::from(glob).to_glob_string())) + .map(|glob| Glob::new(&SanitizedPath::new(glob).to_glob_string())) .collect::, _>>()?; let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect(); let sources_with_trailing_slash = globs diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 75312c5c0cec4a4b285ff0320d90eeda1c0a4c6a..a5cf9b88254deff5b9a07402207f19875827d7f0 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -495,7 +495,8 @@ impl Fs for RealFs { }; // todo(windows) // When new version of `windows-rs` release, make this operation `async` - let path = SanitizedPath::from(path.canonicalize()?); + let path = path.canonicalize()?; + let path = SanitizedPath::new(&path); let path_string = path.to_string(); let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_string))?.get()?; file.DeleteAsync(StorageDeleteOption::Default)?.get()?; @@ -522,7 +523,8 @@ impl Fs for RealFs { // todo(windows) // When new version of `windows-rs` release, make this operation `async` - let path = SanitizedPath::from(path.canonicalize()?); + let path = path.canonicalize()?; + let path = SanitizedPath::new(&path); let path_string = path.to_string(); let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_string))?.get()?; folder.DeleteAsync(StorageDeleteOption::Default)?.get()?; @@ -783,7 +785,7 @@ impl Fs for RealFs { { target = parent.join(target); if let Ok(canonical) = self.canonicalize(&target).await { - target = SanitizedPath::from(canonical).as_path().to_path_buf(); + target = SanitizedPath::new(&canonical).as_path().to_path_buf(); } } watcher.add(&target).ok(); diff --git a/crates/fs/src/fs_watcher.rs b/crates/fs/src/fs_watcher.rs index 6ad03ba6dfa2003b1642cbb542e3a9cf0bf13ec9..07374b7f40455f09cf52d31ddd1a1f64ab6abcd3 100644 --- a/crates/fs/src/fs_watcher.rs +++ b/crates/fs/src/fs_watcher.rs @@ -42,7 +42,7 @@ impl Drop for FsWatcher { impl Watcher for FsWatcher { fn add(&self, path: &std::path::Path) -> anyhow::Result<()> { - let root_path = SanitizedPath::from(path); + let root_path = SanitizedPath::new_arc(path); let tx = self.tx.clone(); let pending_paths = self.pending_path_events.clone(); @@ -70,7 +70,7 @@ impl Watcher for FsWatcher { .paths .iter() .filter_map(|event_path| { - let event_path = SanitizedPath::from(event_path); + let event_path = SanitizedPath::new(event_path); event_path.starts_with(&root_path).then(|| PathEvent { path: event_path.as_path().to_path_buf(), kind, diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 5ac2be2f238e9c6986a5496af5e69a6ef658c0f7..3a6ccff90f06156345a71482fe723c76d4c2ca39 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -851,7 +851,7 @@ fn file_save_dialog( if !directory.to_string_lossy().is_empty() && let Some(full_path) = directory.canonicalize().log_err() { - let full_path = SanitizedPath::from(full_path); + let full_path = SanitizedPath::new(&full_path); let full_path_string = full_path.to_string(); let path_item: IShellItem = unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? }; diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index aab0354c9696f8bcdde5fd4bb00bd3651ac4b888..c2c3c89305939bc32c635549c23d64d565f8fbb0 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -63,7 +63,7 @@ pub fn set_custom_data_dir(dir: &str) -> &'static PathBuf { let abs_path = path .canonicalize() .expect("failed to canonicalize custom data directory's path to an absolute path"); - path = PathBuf::from(util::paths::SanitizedPath::from(abs_path)) + path = util::paths::SanitizedPath::new(&abs_path).into() } std::fs::create_dir_all(&path).expect("failed to create custom data directory"); path diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index a1c0508c3ed5355685828487229967c83b59cbd3..b7ff3e7fefc9b2e8aede04d3cd0fca88c16c2a62 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -62,7 +62,7 @@ use std::{ }; use sum_tree::{Edit, SumTree, TreeSet}; use text::{Bias, BufferId}; -use util::{ResultExt, debug_panic, post_inc}; +use util::{ResultExt, debug_panic, paths::SanitizedPath, post_inc}; use worktree::{ File, PathChange, PathKey, PathProgress, PathSummary, PathTarget, ProjectEntryId, UpdatedGitRepositoriesSet, UpdatedGitRepository, Worktree, @@ -3234,6 +3234,7 @@ impl Repository { let git_store = self.git_store.upgrade()?; let worktree_store = git_store.read(cx).worktree_store.read(cx); let abs_path = self.snapshot.work_directory_abs_path.join(&path.0); + let abs_path = SanitizedPath::new(&abs_path); let (worktree, relative_path) = worktree_store.find_worktree(abs_path, cx)?; Some(ProjectPath { worktree_id: worktree.read(cx).id(), diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index d11e5679968095d0a6541ab320ac3476dc7f794a..b4c7c0bc37fc0409570ece3c5e3df00b1b1cd89f 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3186,7 +3186,7 @@ impl LocalLspStore { } else { let (path, pattern) = match &watcher.glob_pattern { lsp::GlobPattern::String(s) => { - let watcher_path = SanitizedPath::from(s); + let watcher_path = SanitizedPath::new(s); let path = glob_literal_prefix(watcher_path.as_path()); let pattern = watcher_path .as_path() @@ -3278,7 +3278,7 @@ impl LocalLspStore { let worktree_root_path = tree.abs_path(); match &watcher.glob_pattern { lsp::GlobPattern::String(s) => { - let watcher_path = SanitizedPath::from(s); + let watcher_path = SanitizedPath::new(s); let relative = watcher_path .as_path() .strip_prefix(&worktree_root_path) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 86b2e08d629c68d60e241b677288750d32a4bcbd..68e04cfd3bec25638964e8fccd675279c450795d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2061,13 +2061,12 @@ impl Project { exclude_sub_dirs: bool, cx: &App, ) -> Option { - let sanitized_path = SanitizedPath::from(path); - let path = sanitized_path.as_path(); + let path = SanitizedPath::new(path).as_path(); self.worktrees(cx) .filter_map(|worktree| { let worktree = worktree.read(cx); let abs_path = worktree.as_local()?.abs_path(); - let contains = path == abs_path + let contains = path == abs_path.as_ref() || (path.starts_with(abs_path) && (!exclude_sub_dirs || !metadata.is_dir)); contains.then(|| worktree.is_visible()) }) diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 9033415ca40b4550e5f98bae2de8314be2e1a5fe..b814e46bd1584cb076a057623b8b66800365876d 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -61,7 +61,7 @@ pub struct WorktreeStore { worktrees_reordered: bool, #[allow(clippy::type_complexity)] loading_worktrees: - HashMap, Arc>>>>, + HashMap, Shared, Arc>>>>, state: WorktreeStoreState, } @@ -153,10 +153,10 @@ impl WorktreeStore { pub fn find_worktree( &self, - abs_path: impl Into, + abs_path: impl AsRef, cx: &App, ) -> Option<(Entity, PathBuf)> { - let abs_path: SanitizedPath = abs_path.into(); + let abs_path = SanitizedPath::new(&abs_path); for tree in self.worktrees() { if let Ok(relative_path) = abs_path.as_path().strip_prefix(tree.read(cx).abs_path()) { return Some((tree.clone(), relative_path.into())); @@ -212,11 +212,11 @@ impl WorktreeStore { pub fn create_worktree( &mut self, - abs_path: impl Into, + abs_path: impl AsRef, visible: bool, cx: &mut Context, ) -> Task>> { - let abs_path: SanitizedPath = abs_path.into(); + let abs_path: Arc = SanitizedPath::new_arc(&abs_path); if !self.loading_worktrees.contains_key(&abs_path) { let task = match &self.state { WorktreeStoreState::Remote { @@ -227,8 +227,7 @@ impl WorktreeStore { if upstream_client.is_via_collab() { Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab")))) } else { - let abs_path = - RemotePathBuf::new(abs_path.as_path().to_path_buf(), *path_style); + let abs_path = RemotePathBuf::new(abs_path.to_path_buf(), *path_style); self.create_ssh_worktree(upstream_client.clone(), abs_path, visible, cx) } } @@ -321,15 +320,21 @@ impl WorktreeStore { fn create_local_worktree( &mut self, fs: Arc, - abs_path: impl Into, + abs_path: Arc, visible: bool, cx: &mut Context, ) -> Task, Arc>> { let next_entry_id = self.next_entry_id.clone(); - let path: SanitizedPath = abs_path.into(); cx.spawn(async move |this, cx| { - let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, cx).await; + let worktree = Worktree::local( + SanitizedPath::cast_arc(abs_path.clone()), + visible, + fs, + next_entry_id, + cx, + ) + .await; let worktree = worktree?; @@ -337,7 +342,7 @@ impl WorktreeStore { if visible { cx.update(|cx| { - cx.add_recent_document(path.as_path()); + cx.add_recent_document(abs_path.as_path()); }) .log_err(); } diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 750fc6dc586c227c6461ab4650c1b46b6bb01dc6..0036a687a6f73b57723e8c3c9fcffc56cab626c2 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -902,7 +902,7 @@ impl SshRemoteConnection { // On Windows, the binding needs to be set to the canonical path #[cfg(target_os = "windows")] let src = - SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string(); + SanitizedPath::new(&smol::fs::canonicalize("./target").await?).to_glob_string(); #[cfg(not(target_os = "windows"))] let src = "./target"; run_cmd( diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 1192b14812580bf21e262620a3ccefc90c5acd54..318900d540172035b29ae25ad5f42dbbac87bf60 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -3,6 +3,7 @@ use regex::Regex; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::fmt::{Display, Formatter}; +use std::mem; use std::path::StripPrefixError; use std::sync::{Arc, OnceLock}; use std::{ @@ -99,21 +100,86 @@ impl> PathExt for T { } } -/// Due to the issue of UNC paths on Windows, which can cause bugs in various parts of Zed, introducing this `SanitizedPath` -/// leverages Rust's type system to ensure that all paths entering Zed are always "sanitized" by removing the `\\\\?\\` prefix. -/// On non-Windows operating systems, this struct is effectively a no-op. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct SanitizedPath(pub Arc); +/// In memory, this is identical to `Path`. On non-Windows conversions to this type are no-ops. On +/// windows, these conversions sanitize UNC paths by removing the `\\\\?\\` prefix. +#[derive(Eq, PartialEq, Hash, Ord, PartialOrd)] +#[repr(transparent)] +pub struct SanitizedPath(Path); impl SanitizedPath { - pub fn starts_with(&self, prefix: &SanitizedPath) -> bool { + pub fn new + ?Sized>(path: &T) -> &Self { + #[cfg(not(target_os = "windows"))] + return Self::unchecked_new(path.as_ref()); + + #[cfg(target_os = "windows")] + return Self::unchecked_new(dunce::simplified(path.as_ref())); + } + + pub fn unchecked_new + ?Sized>(path: &T) -> &Self { + // safe because `Path` and `SanitizedPath` have the same repr and Drop impl + unsafe { mem::transmute::<&Path, &Self>(path.as_ref()) } + } + + pub fn from_arc(path: Arc) -> Arc { + // safe because `Path` and `SanitizedPath` have the same repr and Drop impl + #[cfg(not(target_os = "windows"))] + return unsafe { mem::transmute::, Arc>(path) }; + + // TODO: could avoid allocating here if dunce::simplified results in the same path + #[cfg(target_os = "windows")] + return Self::new(&path).into(); + } + + pub fn new_arc + ?Sized>(path: &T) -> Arc { + Self::new(path).into() + } + + pub fn cast_arc(path: Arc) -> Arc { + // safe because `Path` and `SanitizedPath` have the same repr and Drop impl + unsafe { mem::transmute::, Arc>(path) } + } + + pub fn cast_arc_ref(path: &Arc) -> &Arc { + // safe because `Path` and `SanitizedPath` have the same repr and Drop impl + unsafe { mem::transmute::<&Arc, &Arc>(path) } + } + + pub fn starts_with(&self, prefix: &Self) -> bool { self.0.starts_with(&prefix.0) } - pub fn as_path(&self) -> &Arc { + pub fn as_path(&self) -> &Path { &self.0 } + pub fn file_name(&self) -> Option<&std::ffi::OsStr> { + self.0.file_name() + } + + pub fn extension(&self) -> Option<&std::ffi::OsStr> { + self.0.extension() + } + + pub fn join>(&self, path: P) -> PathBuf { + self.0.join(path) + } + + pub fn parent(&self) -> Option<&Self> { + self.0.parent().map(Self::unchecked_new) + } + + pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> { + self.0.strip_prefix(base.as_path()) + } + + pub fn to_str(&self) -> Option<&str> { + self.0.to_str() + } + + pub fn to_path_buf(&self) -> PathBuf { + self.0.to_path_buf() + } + pub fn to_glob_string(&self) -> String { #[cfg(target_os = "windows")] { @@ -124,13 +190,11 @@ impl SanitizedPath { self.0.to_string_lossy().to_string() } } +} - pub fn join(&self, path: &Self) -> Self { - self.0.join(&path.0).into() - } - - pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> { - self.0.strip_prefix(base.as_path()) +impl std::fmt::Debug for SanitizedPath { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(&self.0, formatter) } } @@ -140,29 +204,23 @@ impl Display for SanitizedPath { } } -impl From for Arc { - fn from(sanitized_path: SanitizedPath) -> Self { - sanitized_path.0 +impl From<&SanitizedPath> for Arc { + fn from(sanitized_path: &SanitizedPath) -> Self { + let path: Arc = sanitized_path.0.into(); + // safe because `Path` and `SanitizedPath` have the same repr and Drop impl + unsafe { mem::transmute(path) } } } -impl From for PathBuf { - fn from(sanitized_path: SanitizedPath) -> Self { - sanitized_path.0.as_ref().into() +impl From<&SanitizedPath> for PathBuf { + fn from(sanitized_path: &SanitizedPath) -> Self { + sanitized_path.as_path().into() } } -impl> From for SanitizedPath { - #[cfg(not(target_os = "windows"))] - fn from(path: T) -> Self { - let path = path.as_ref(); - SanitizedPath(path.into()) - } - - #[cfg(target_os = "windows")] - fn from(path: T) -> Self { - let path = path.as_ref(); - SanitizedPath(dunce::simplified(path).into()) +impl AsRef for SanitizedPath { + fn as_ref(&self) -> &Path { + &self.0 } } @@ -1195,14 +1253,14 @@ mod tests { #[cfg(target_os = "windows")] fn test_sanitized_path() { let path = Path::new("C:\\Users\\someone\\test_file.rs"); - let sanitized_path = SanitizedPath::from(path); + let sanitized_path = SanitizedPath::new(path); assert_eq!( sanitized_path.to_string(), "C:\\Users\\someone\\test_file.rs" ); let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs"); - let sanitized_path = SanitizedPath::from(path); + let sanitized_path = SanitizedPath::new(path); assert_eq!( sanitized_path.to_string(), "C:\\Users\\someone\\test_file.rs" diff --git a/crates/workspace/src/path_list.rs b/crates/workspace/src/path_list.rs index cf463e6b2295f78a8492c0f4aafd6ca6b8ed1788..01e2ffda949faf502de087fb0077cdbc758001ab 100644 --- a/crates/workspace/src/path_list.rs +++ b/crates/workspace/src/path_list.rs @@ -26,7 +26,7 @@ impl PathList { let mut indexed_paths: Vec<(usize, PathBuf)> = paths .iter() .enumerate() - .map(|(ix, path)| (ix, SanitizedPath::from(path).into())) + .map(|(ix, path)| (ix, SanitizedPath::new(path).into())) .collect(); indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b)); let order = indexed_paths.iter().map(|e| e.0).collect::>().into(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 76693e716e3cafe31e60eeecfcaeeb5bb267fb77..0f119c14003d0f54f2f3a5323cb5e9106716a24d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2576,7 +2576,7 @@ impl Workspace { }; let this = this.clone(); - let abs_path: Arc = SanitizedPath::from(abs_path.clone()).into(); + let abs_path: Arc = SanitizedPath::new(&abs_path).as_path().into(); let fs = fs.clone(); let pane = pane.clone(); let task = cx.spawn(async move |cx| { diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 845af538021326a0e609c9f6098ebf20ed1dc704..711c99ce28bbbc557a293d8b644ac6594f31ad7f 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -158,7 +158,7 @@ pub struct RemoteWorktree { #[derive(Clone)] pub struct Snapshot { id: WorktreeId, - abs_path: SanitizedPath, + abs_path: Arc, root_name: String, root_char_bag: CharBag, entries_by_path: SumTree, @@ -457,7 +457,7 @@ enum ScanState { scanning: bool, }, RootUpdated { - new_path: Option, + new_path: Option>, }, } @@ -763,8 +763,8 @@ impl Worktree { pub fn abs_path(&self) -> Arc { match self { - Worktree::Local(worktree) => worktree.abs_path.clone().into(), - Worktree::Remote(worktree) => worktree.abs_path.clone().into(), + Worktree::Local(worktree) => SanitizedPath::cast_arc(worktree.abs_path.clone()), + Worktree::Remote(worktree) => SanitizedPath::cast_arc(worktree.abs_path.clone()), } } @@ -1813,7 +1813,7 @@ impl LocalWorktree { // Otherwise, the FS watcher would do it on the `RootUpdated` event, // but with a noticeable delay, so we handle it proactively. local.update_abs_path_and_refresh( - Some(SanitizedPath::from(abs_path.clone())), + Some(SanitizedPath::new_arc(&abs_path)), cx, ); Task::ready(Ok(this.root_entry().cloned())) @@ -2090,7 +2090,7 @@ impl LocalWorktree { fn update_abs_path_and_refresh( &mut self, - new_path: Option, + new_path: Option>, cx: &Context, ) { if let Some(new_path) = new_path { @@ -2340,7 +2340,7 @@ impl Snapshot { pub fn new(id: u64, root_name: String, abs_path: Arc) -> Self { Snapshot { id: WorktreeId::from_usize(id as usize), - abs_path: abs_path.into(), + abs_path: SanitizedPath::from_arc(abs_path), root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(), root_name, always_included_entries: Default::default(), @@ -2368,7 +2368,7 @@ impl Snapshot { // // This is definitely a bug, but it's not clear if we should handle it here or not. pub fn abs_path(&self) -> &Arc { - self.abs_path.as_path() + SanitizedPath::cast_arc_ref(&self.abs_path) } fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree { @@ -2464,7 +2464,7 @@ impl Snapshot { Some(removed_entry.path) } - fn update_abs_path(&mut self, abs_path: SanitizedPath, root_name: String) { + fn update_abs_path(&mut self, abs_path: Arc, root_name: String) { self.abs_path = abs_path; if root_name != self.root_name { self.root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect(); @@ -2483,7 +2483,7 @@ impl Snapshot { update.removed_entries.len() ); self.update_abs_path( - SanitizedPath::from(PathBuf::from_proto(update.abs_path)), + SanitizedPath::new_arc(&PathBuf::from_proto(update.abs_path)), update.root_name, ); @@ -3849,7 +3849,11 @@ impl BackgroundScanner { root_entry.is_ignored = true; state.insert_entry(root_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()); } - state.enqueue_scan_dir(root_abs_path.into(), &root_entry, &scan_job_tx); + state.enqueue_scan_dir( + SanitizedPath::cast_arc(root_abs_path), + &root_entry, + &scan_job_tx, + ); } }; @@ -3930,8 +3934,9 @@ impl BackgroundScanner { self.forcibly_load_paths(&request.relative_paths).await; let root_path = self.state.lock().snapshot.abs_path.clone(); - let root_canonical_path = match self.fs.canonicalize(root_path.as_path()).await { - Ok(path) => SanitizedPath::from(path), + let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await; + let root_canonical_path = match &root_canonical_path { + Ok(path) => SanitizedPath::new(path), Err(err) => { log::error!("failed to canonicalize root path {root_path:?}: {err}"); return true; @@ -3959,8 +3964,8 @@ impl BackgroundScanner { } self.reload_entries_for_paths( - root_path, - root_canonical_path, + &root_path, + &root_canonical_path, &request.relative_paths, abs_paths, None, @@ -3972,8 +3977,9 @@ impl BackgroundScanner { async fn process_events(&self, mut abs_paths: Vec) { let root_path = self.state.lock().snapshot.abs_path.clone(); - let root_canonical_path = match self.fs.canonicalize(root_path.as_path()).await { - Ok(path) => SanitizedPath::from(path), + let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await; + let root_canonical_path = match &root_canonical_path { + Ok(path) => SanitizedPath::new(path), Err(err) => { let new_path = self .state @@ -3982,7 +3988,7 @@ impl BackgroundScanner { .root_file_handle .clone() .and_then(|handle| handle.current_path(&self.fs).log_err()) - .map(SanitizedPath::from) + .map(|path| SanitizedPath::new_arc(&path)) .filter(|new_path| *new_path != root_path); if let Some(new_path) = new_path.as_ref() { @@ -4011,7 +4017,7 @@ impl BackgroundScanner { abs_paths.sort_unstable(); abs_paths.dedup_by(|a, b| a.starts_with(b)); abs_paths.retain(|abs_path| { - let abs_path = SanitizedPath::from(abs_path); + let abs_path = &SanitizedPath::new(abs_path); let snapshot = &self.state.lock().snapshot; { @@ -4054,7 +4060,7 @@ impl BackgroundScanner { return false; }; - if abs_path.0.file_name() == Some(*GITIGNORE) { + if abs_path.file_name() == Some(*GITIGNORE) { for (_, repo) in snapshot.git_repositories.iter().filter(|(_, repo)| repo.directory_contains(&relative_path)) { if !dot_git_abs_paths.iter().any(|dot_git_abs_path| dot_git_abs_path == repo.common_dir_abs_path.as_ref()) { dot_git_abs_paths.push(repo.common_dir_abs_path.to_path_buf()); @@ -4093,8 +4099,8 @@ impl BackgroundScanner { let (scan_job_tx, scan_job_rx) = channel::unbounded(); log::debug!("received fs events {:?}", relative_paths); self.reload_entries_for_paths( - root_path, - root_canonical_path, + &root_path, + &root_canonical_path, &relative_paths, abs_paths, Some(scan_job_tx.clone()), @@ -4441,8 +4447,8 @@ impl BackgroundScanner { /// All list arguments should be sorted before calling this function async fn reload_entries_for_paths( &self, - root_abs_path: SanitizedPath, - root_canonical_path: SanitizedPath, + root_abs_path: &SanitizedPath, + root_canonical_path: &SanitizedPath, relative_paths: &[Arc], abs_paths: Vec, scan_queue_tx: Option>, @@ -4470,7 +4476,7 @@ impl BackgroundScanner { } } - anyhow::Ok(Some((metadata, SanitizedPath::from(canonical_path)))) + anyhow::Ok(Some((metadata, SanitizedPath::new_arc(&canonical_path)))) } else { Ok(None) } From 8697b91ea09e612b5d5c088b5fe548fa8c1084c0 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 28 Aug 2025 15:33:00 -0400 Subject: [PATCH 420/744] acp: Automatically install gemini under Zed's data dir (#37054) Closes: https://github.com/zed-industries/zed/issues/37089 Instead of looking for the gemini command on `$PATH`, by default we'll install our own copy on demand under our data dir, as we already do for language servers and debug adapters. This also means we can handle keeping the binary up to date instead of prompting the user to upgrade. Notes: - The download is only triggered if you open a new Gemini thread - Custom commands from `agent_servers.gemini` in settings are respected as before - A new `agent_servers.gemini.ignore_system_version` setting is added, similar to the existing settings for language servers. It's `true` by default, and setting it to `false` disables the automatic download and makes Zed search `$PATH` as before. - If `agent_servers.gemini.ignore_system_version` is `false` and no binary is found on `$PATH`, we'll fall back to automatic installation. If it's `false` and a binary is found, but the version is older than v0.2.1, we'll show an error. Release Notes: - acp: By default, Zed will now download and use a private copy of the Gemini CLI binary, instead of searching your `$PATH`. To make Zed search your `$PATH` for Gemini CLI before attempting to download it, use the following setting: ``` { "agent_servers": { "gemini": { "ignore_system_version": false } } } ``` --- Cargo.lock | 1 + crates/acp_thread/src/acp_thread.rs | 13 +- crates/agent2/src/native_agent_server.rs | 19 +- crates/agent_servers/Cargo.toml | 5 +- crates/agent_servers/src/agent_servers.rs | 146 +++++++++++--- crates/agent_servers/src/claude.rs | 19 +- crates/agent_servers/src/custom.rs | 26 +-- crates/agent_servers/src/e2e_tests.rs | 14 +- crates/agent_servers/src/gemini.rs | 64 +++--- crates/agent_servers/src/settings.rs | 56 +++++- crates/agent_ui/src/acp/message_editor.rs | 5 +- crates/agent_ui/src/acp/thread_view.rs | 217 +++++++-------------- crates/agent_ui/src/agent_configuration.rs | 151 +++----------- crates/agent_ui/src/agent_panel.rs | 17 +- crates/agent_ui/src/agent_ui.rs | 8 +- 15 files changed, 331 insertions(+), 430 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ecd8b42c79dee10549a73edfafd302e981ff488..0e3bfd18c2991328de18149be6688fcfc303eb61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,6 +306,7 @@ dependencies = [ "libc", "log", "nix 0.29.0", + "node_runtime", "paths", "project", "rand 0.8.5", diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 0da4b43394b76125b2ea9a310ae5bfe9bf0fac9a..04ff032ad40c600c80fed7cff9f48139b2307931 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -789,11 +789,12 @@ pub enum ThreadStatus { #[derive(Debug, Clone)] pub enum LoadError { - NotInstalled, Unsupported { command: SharedString, current_version: SharedString, + minimum_version: SharedString, }, + FailedToInstall(SharedString), Exited { status: ExitStatus, }, @@ -803,15 +804,19 @@ pub enum LoadError { impl Display for LoadError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - LoadError::NotInstalled => write!(f, "not installed"), LoadError::Unsupported { command: path, current_version, + minimum_version, } => { - write!(f, "version {current_version} from {path} is not supported") + write!( + f, + "version {current_version} from {path} is not supported (need at least {minimum_version})" + ) } + LoadError::FailedToInstall(msg) => write!(f, "Failed to install: {msg}"), LoadError::Exited { status } => write!(f, "Server exited with status {status}"), - LoadError::Other(msg) => write!(f, "{}", msg), + LoadError::Other(msg) => write!(f, "{msg}"), } } } diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 0079dcc5724a77e02cd68be3deaee0735fcf56fa..030d2cce746970bd9c8a0c7f0f5e1516eb68fcaf 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -1,10 +1,9 @@ use std::{any::Any, path::Path, rc::Rc, sync::Arc}; -use agent_servers::AgentServer; +use agent_servers::{AgentServer, AgentServerDelegate}; use anyhow::Result; use fs::Fs; use gpui::{App, Entity, SharedString, Task}; -use project::Project; use prompt_store::PromptStore; use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates}; @@ -30,33 +29,21 @@ impl AgentServer for NativeAgentServer { "Zed Agent".into() } - fn empty_state_headline(&self) -> SharedString { - self.name() - } - - fn empty_state_message(&self) -> SharedString { - "".into() - } - fn logo(&self) -> ui::IconName { ui::IconName::ZedAgent } - fn install_command(&self) -> Option<&'static str> { - None - } - fn connect( &self, _root_dir: &Path, - project: &Entity, + delegate: AgentServerDelegate, cx: &mut App, ) -> Task>> { log::debug!( "NativeAgentServer::connect called for path: {:?}", _root_dir ); - let project = project.clone(); + let project = delegate.project().clone(); let fs = self.fs.clone(); let history = self.history.clone(); let prompt_store = PromptStore::global(cx); diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 9f90f3a78aed825c372cc8bffc67d194b7ec2027..3e6bae104ce339f371b2ea69afebecbc6c1cec27 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -6,7 +6,7 @@ publish.workspace = true license = "GPL-3.0-or-later" [features] -test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "fs", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"] +test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"] e2e = [] [lints] @@ -27,7 +27,7 @@ client = { workspace = true, optional = true } collections.workspace = true context_server.workspace = true env_logger = { workspace = true, optional = true } -fs = { workspace = true, optional = true } +fs.workspace = true futures.workspace = true gpui.workspace = true gpui_tokio = { workspace = true, optional = true } @@ -37,6 +37,7 @@ language.workspace = true language_model.workspace = true language_models.workspace = true log.workspace = true +node_runtime.workspace = true paths.workspace = true project.workspace = true rand.workspace = true diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index dc7d75c52d72928cfc3673bc1eb476f08206669f..e5d954b071a86f39a44e7c370dbc841c2f58d706 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -13,12 +13,19 @@ pub use gemini::*; pub use settings::*; use acp_thread::AgentConnection; +use acp_thread::LoadError; use anyhow::Result; +use anyhow::anyhow; +use anyhow::bail; use collections::HashMap; +use gpui::AppContext as _; use gpui::{App, AsyncApp, Entity, SharedString, Task}; +use node_runtime::VersionStrategy; use project::Project; use schemars::JsonSchema; +use semver::Version; use serde::{Deserialize, Serialize}; +use std::str::FromStr as _; use std::{ any::Any, path::{Path, PathBuf}, @@ -31,23 +38,118 @@ pub fn init(cx: &mut App) { settings::init(cx); } +pub struct AgentServerDelegate { + project: Entity, + status_tx: watch::Sender, +} + +impl AgentServerDelegate { + pub fn new(project: Entity, status_tx: watch::Sender) -> Self { + Self { project, status_tx } + } + + pub fn project(&self) -> &Entity { + &self.project + } + + fn get_or_npm_install_builtin_agent( + self, + binary_name: SharedString, + package_name: SharedString, + entrypoint_path: PathBuf, + settings: Option, + minimum_version: Option, + cx: &mut App, + ) -> Task> { + if let Some(settings) = &settings + && let Some(command) = settings.clone().custom_command() + { + return Task::ready(Ok(command)); + } + + let project = self.project; + let fs = project.read(cx).fs().clone(); + let Some(node_runtime) = project.read(cx).node_runtime().cloned() else { + return Task::ready(Err(anyhow!("Missing node runtime"))); + }; + let mut status_tx = self.status_tx; + + cx.spawn(async move |cx| { + if let Some(settings) = settings && !settings.ignore_system_version.unwrap_or(true) { + if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await { + return Ok(AgentServerCommand { path: bin, args: Vec::new(), env: Default::default() }) + } + } + + cx.background_spawn(async move { + let node_path = node_runtime.binary_path().await?; + let dir = paths::data_dir().join("external_agents").join(binary_name.as_str()); + fs.create_dir(&dir).await?; + let local_executable_path = dir.join(entrypoint_path); + let command = AgentServerCommand { + path: node_path, + args: vec![local_executable_path.to_string_lossy().to_string()], + env: Default::default(), + }; + + let installed_version = node_runtime + .npm_package_installed_version(&dir, &package_name) + .await? + .filter(|version| { + Version::from_str(&version) + .is_ok_and(|version| Some(version) >= minimum_version) + }); + + status_tx.send("Checking for latest version…".into())?; + let latest_version = match node_runtime.npm_package_latest_version(&package_name).await + { + Ok(latest_version) => latest_version, + Err(e) => { + if let Some(installed_version) = installed_version { + log::error!("{e}"); + log::warn!("failed to fetch latest version of {package_name}, falling back to cached version {installed_version}"); + return Ok(command); + } else { + bail!(e); + } + } + }; + + let should_install = node_runtime + .should_install_npm_package( + &package_name, + &local_executable_path, + &dir, + VersionStrategy::Latest(&latest_version), + ) + .await; + + if should_install { + status_tx.send("Installing latest version…".into())?; + node_runtime + .npm_install_packages(&dir, &[(&package_name, &latest_version)]) + .await?; + } + + Ok(command) + }).await.map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into()) + }) + } +} + pub trait AgentServer: Send { fn logo(&self) -> ui::IconName; fn name(&self) -> SharedString; - fn empty_state_headline(&self) -> SharedString; - fn empty_state_message(&self) -> SharedString; fn telemetry_id(&self) -> &'static str; fn connect( &self, root_dir: &Path, - project: &Entity, + delegate: AgentServerDelegate, cx: &mut App, ) -> Task>>; fn into_any(self: Rc) -> Rc; - - fn install_command(&self) -> Option<&'static str>; } impl dyn AgentServer { @@ -81,15 +183,6 @@ impl std::fmt::Debug for AgentServerCommand { } } -pub enum AgentServerVersion { - Supported, - Unsupported { - error_message: SharedString, - upgrade_message: SharedString, - upgrade_command: String, - }, -} - #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)] pub struct AgentServerCommand { #[serde(rename = "command")] @@ -104,23 +197,16 @@ impl AgentServerCommand { path_bin_name: &'static str, extra_args: &[&'static str], fallback_path: Option<&Path>, - settings: Option, + settings: Option, project: &Entity, cx: &mut AsyncApp, ) -> Option { - if let Some(agent_settings) = settings { - Some(Self { - path: agent_settings.command.path, - args: agent_settings - .command - .args - .into_iter() - .chain(extra_args.iter().map(|arg| arg.to_string())) - .collect(), - env: agent_settings.command.env, - }) + if let Some(settings) = settings + && let Some(command) = settings.custom_command() + { + Some(command) } else { - match find_bin_in_path(path_bin_name, project, cx).await { + match find_bin_in_path(path_bin_name.into(), project, cx).await { Some(path) => Some(Self { path, args: extra_args.iter().map(|arg| arg.to_string()).collect(), @@ -143,7 +229,7 @@ impl AgentServerCommand { } async fn find_bin_in_path( - bin_name: &'static str, + bin_name: SharedString, project: &Entity, cx: &mut AsyncApp, ) -> Option { @@ -173,11 +259,11 @@ async fn find_bin_in_path( cx.background_executor() .spawn(async move { let which_result = if cfg!(windows) { - which::which(bin_name) + which::which(bin_name.as_str()) } else { let env = env_task.await.unwrap_or_default(); let shell_path = env.get("PATH").cloned(); - which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref()) + which::which_in(bin_name.as_str(), shell_path.as_ref(), root_dir.as_ref()) }; if let Err(which::Error::CannotFindBinaryPath) = which_result { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 3a16b0601a0a85a2a66d170455af6b4cb9f4ae8f..b1832191480f112c7788e4e908c2e1594f08c0ad 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -36,7 +36,7 @@ use util::{ResultExt, debug_panic}; use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig}; use crate::claude::tools::ClaudeTool; -use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings}; +use crate::{AgentServer, AgentServerCommand, AgentServerDelegate, AllAgentServersSettings}; use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri}; #[derive(Clone)] @@ -51,26 +51,14 @@ impl AgentServer for ClaudeCode { "Claude Code".into() } - fn empty_state_headline(&self) -> SharedString { - self.name() - } - - fn empty_state_message(&self) -> SharedString { - "How can I help you today?".into() - } - fn logo(&self) -> ui::IconName { ui::IconName::AiClaude } - fn install_command(&self) -> Option<&'static str> { - Some("npm install -g @anthropic-ai/claude-code@latest") - } - fn connect( &self, _root_dir: &Path, - _project: &Entity, + _delegate: AgentServerDelegate, _cx: &mut App, ) -> Task>> { let connection = ClaudeAgentConnection { @@ -112,7 +100,7 @@ impl AgentConnection for ClaudeAgentConnection { ) .await else { - return Err(LoadError::NotInstalled.into()); + return Err(anyhow!("Failed to find Claude Code binary")); }; let api_key = @@ -232,6 +220,7 @@ impl AgentConnection for ClaudeAgentConnection { LoadError::Unsupported { command: command.path.to_string_lossy().to_string().into(), current_version: version.to_string().into(), + minimum_version: "1.0.0".into(), } } else { LoadError::Exited { status } diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index 0669e0a68ef019975fdbdcbe155fa3dc6aeb0b96..a481a850ff70018ff2e6b72446ca24e78732137a 100644 --- a/crates/agent_servers/src/custom.rs +++ b/crates/agent_servers/src/custom.rs @@ -1,9 +1,8 @@ -use crate::{AgentServerCommand, AgentServerSettings}; +use crate::{AgentServerCommand, AgentServerDelegate}; use acp_thread::AgentConnection; use anyhow::Result; -use gpui::{App, Entity, SharedString, Task}; +use gpui::{App, SharedString, Task}; use language_models::provider::anthropic::AnthropicLanguageModelProvider; -use project::Project; use std::{path::Path, rc::Rc}; use ui::IconName; @@ -14,11 +13,8 @@ pub struct CustomAgentServer { } impl CustomAgentServer { - pub fn new(name: SharedString, settings: &AgentServerSettings) -> Self { - Self { - name, - command: settings.command.clone(), - } + pub fn new(name: SharedString, command: AgentServerCommand) -> Self { + Self { name, command } } } @@ -35,18 +31,10 @@ impl crate::AgentServer for CustomAgentServer { IconName::Terminal } - fn empty_state_headline(&self) -> SharedString { - "No conversations yet".into() - } - - fn empty_state_message(&self) -> SharedString { - format!("Start a conversation with {}", self.name).into() - } - fn connect( &self, root_dir: &Path, - _project: &Entity, + _delegate: AgentServerDelegate, cx: &mut App, ) -> Task>> { let server_name = self.name(); @@ -70,10 +58,6 @@ impl crate::AgentServer for CustomAgentServer { }) } - fn install_command(&self) -> Option<&'static str> { - None - } - fn into_any(self: Rc) -> Rc { self } diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 42264b4b4f747e11cab11a21f7cde2ad0c43fee3..d310870c23c957900e3cdcdb8a88084f26520208 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -1,4 +1,4 @@ -use crate::AgentServer; +use crate::{AgentServer, AgentServerDelegate}; use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus}; use agent_client_protocol as acp; use futures::{FutureExt, StreamExt, channel::mpsc, select}; @@ -471,12 +471,8 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc { #[cfg(test)] crate::AllAgentServersSettings::override_global( crate::AllAgentServersSettings { - claude: Some(crate::AgentServerSettings { - command: crate::claude::tests::local_command(), - }), - gemini: Some(crate::AgentServerSettings { - command: crate::gemini::tests::local_command(), - }), + claude: Some(crate::claude::tests::local_command().into()), + gemini: Some(crate::gemini::tests::local_command().into()), custom: collections::HashMap::default(), }, cx, @@ -494,8 +490,10 @@ pub async fn new_test_thread( current_dir: impl AsRef, cx: &mut TestAppContext, ) -> Entity { + let delegate = AgentServerDelegate::new(project.clone(), watch::channel("".into()).0); + let connection = cx - .update(|cx| server.connect(current_dir.as_ref(), &project, cx)) + .update(|cx| server.connect(current_dir.as_ref(), delegate, cx)) .await .unwrap(); diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 6d17cc0512d382f9b1a550dcb978957318de0d74..84dc6750b1a8e74b509e467d811ec790cdc0dea9 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -2,12 +2,11 @@ use std::rc::Rc; use std::{any::Any, path::Path}; use crate::acp::AcpConnection; -use crate::{AgentServer, AgentServerCommand}; +use crate::{AgentServer, AgentServerDelegate}; use acp_thread::{AgentConnection, LoadError}; use anyhow::Result; -use gpui::{App, Entity, SharedString, Task}; +use gpui::{App, SharedString, Task}; use language_models::provider::google::GoogleLanguageModelProvider; -use project::Project; use settings::SettingsStore; use crate::AllAgentServersSettings; @@ -26,29 +25,16 @@ impl AgentServer for Gemini { "Gemini CLI".into() } - fn empty_state_headline(&self) -> SharedString { - self.name() - } - - fn empty_state_message(&self) -> SharedString { - "Ask questions, edit files, run commands".into() - } - fn logo(&self) -> ui::IconName { ui::IconName::AiGemini } - fn install_command(&self) -> Option<&'static str> { - Some("npm install --engine-strict -g @google/gemini-cli@latest") - } - fn connect( &self, root_dir: &Path, - project: &Entity, + delegate: AgentServerDelegate, cx: &mut App, ) -> Task>> { - let project = project.clone(); let root_dir = root_dir.to_path_buf(); let server_name = self.name(); cx.spawn(async move |cx| { @@ -56,12 +42,19 @@ impl AgentServer for Gemini { settings.get::(None).gemini.clone() })?; - let Some(mut command) = - AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx) - .await - else { - return Err(LoadError::NotInstalled.into()); - }; + let mut command = cx + .update(|cx| { + delegate.get_or_npm_install_builtin_agent( + Self::BINARY_NAME.into(), + Self::PACKAGE_NAME.into(), + format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(), + settings, + Some("0.2.1".parse().unwrap()), + cx, + ) + })? + .await?; + command.args.push("--experimental-acp".into()); if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() { command @@ -87,12 +80,8 @@ impl AgentServer for Gemini { if !connection.prompt_capabilities().image { return Err(LoadError::Unsupported { current_version: current_version.into(), - command: format!( - "{} {}", - command.path.to_string_lossy(), - command.args.join(" ") - ) - .into(), + command: command.path.to_string_lossy().to_string().into(), + minimum_version: Self::MINIMUM_VERSION.into(), } .into()); } @@ -114,13 +103,16 @@ impl AgentServer for Gemini { let (version_output, help_output) = futures::future::join(version_fut, help_fut).await; - let current_version = String::from_utf8(version_output?.stdout)?; + let current_version = std::str::from_utf8(&version_output?.stdout)? + .trim() + .to_string(); let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG); if !supported { return Err(LoadError::Unsupported { current_version: current_version.into(), command: command.path.to_string_lossy().to_string().into(), + minimum_version: Self::MINIMUM_VERSION.into(), } .into()); } @@ -136,17 +128,11 @@ impl AgentServer for Gemini { } impl Gemini { - pub fn binary_name() -> &'static str { - "gemini" - } + const PACKAGE_NAME: &str = "@google/gemini-cli"; - pub fn install_command() -> &'static str { - "npm install --engine-strict -g @google/gemini-cli@latest" - } + const MINIMUM_VERSION: &str = "0.2.1"; - pub fn upgrade_command() -> &'static str { - "npm install -g @google/gemini-cli@latest" - } + const BINARY_NAME: &str = "gemini"; } #[cfg(test)] diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 96ac6e3cbe7dcd8a03aef5c6ec79c884bf99ae67..59f3b4b54089a5598bc77d0ba127f2c54e9ec986 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use crate::AgentServerCommand; use anyhow::Result; use collections::HashMap; @@ -12,16 +14,62 @@ pub fn init(cx: &mut App) { #[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)] pub struct AllAgentServersSettings { - pub gemini: Option, - pub claude: Option, + pub gemini: Option, + pub claude: Option, /// Custom agent servers configured by the user #[serde(flatten)] - pub custom: HashMap, + pub custom: HashMap, +} + +#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] +pub struct BuiltinAgentServerSettings { + /// Absolute path to a binary to be used when launching this agent. + /// + /// This can be used to run a specific binary without automatic downloads or searching `$PATH`. + #[serde(rename = "command")] + pub path: Option, + /// If a binary is specified in `command`, it will be passed these arguments. + pub args: Option>, + /// If a binary is specified in `command`, it will be passed these environment variables. + pub env: Option>, + /// Whether to skip searching `$PATH` for an agent server binary when + /// launching this agent. + /// + /// This has no effect if a `command` is specified. Otherwise, when this is + /// `false`, Zed will search `$PATH` for an agent server binary and, if one + /// is found, use it for threads with this agent. If no agent binary is + /// found on `$PATH`, Zed will automatically install and use its own binary. + /// When this is `true`, Zed will not search `$PATH`, and will always use + /// its own binary. + /// + /// Default: true + pub ignore_system_version: Option, +} + +impl BuiltinAgentServerSettings { + pub(crate) fn custom_command(self) -> Option { + self.path.map(|path| AgentServerCommand { + path, + args: self.args.unwrap_or_default(), + env: self.env, + }) + } +} + +impl From for BuiltinAgentServerSettings { + fn from(value: AgentServerCommand) -> Self { + BuiltinAgentServerSettings { + path: Some(value.path), + args: Some(value.args), + env: value.env, + ..Default::default() + } + } } #[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] -pub struct AgentServerSettings { +pub struct CustomAgentServerSettings { #[serde(flatten)] pub command: AgentServerCommand, } diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 12ae893c317b9a5518b78fdc4c4d7ab7c315eba7..f4ce2652d60c76848827967f8a34a23376e7406f 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -4,7 +4,7 @@ use crate::{ }; use acp_thread::{MentionUri, selection_name}; use agent_client_protocol as acp; -use agent_servers::AgentServer; +use agent_servers::{AgentServer, AgentServerDelegate}; use agent2::HistoryStore; use anyhow::{Result, anyhow}; use assistant_slash_commands::codeblock_fence_for_path; @@ -645,7 +645,8 @@ impl MessageEditor { self.project.read(cx).fs().clone(), self.history_store.clone(), )); - let connection = server.connect(Path::new(""), &self.project, cx); + let delegate = AgentServerDelegate::new(self.project.clone(), watch::channel("".into()).0); + let connection = server.connect(Path::new(""), delegate, cx); cx.spawn(async move |_, cx| { let agent = connection.await?; let agent = agent.downcast::().unwrap(); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2b18ebcd1d72e721e57d91469c835cb70e7812f4..8069812729265c20c7758487cef87479b01dea02 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -6,7 +6,7 @@ use acp_thread::{ use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; use agent_client_protocol::{self as acp, PromptCapabilities}; -use agent_servers::{AgentServer, ClaudeCode}; +use agent_servers::{AgentServer, AgentServerDelegate, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore}; use anyhow::{Result, anyhow, bail}; @@ -46,7 +46,7 @@ use text::Anchor; use theme::ThemeSettings; use ui::{ Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, - Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*, + Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -285,15 +285,12 @@ pub struct AcpThreadView { editing_message: Option, prompt_capabilities: Rc>, is_loading_contents: bool, - install_command_markdown: Entity, _cancel_task: Option>, _subscriptions: [Subscription; 3], } enum ThreadState { - Loading { - _task: Task<()>, - }, + Loading(Entity), Ready { thread: Entity, title_editor: Option>, @@ -309,6 +306,12 @@ enum ThreadState { }, } +struct LoadingView { + title: SharedString, + _load_task: Task<()>, + _update_title_task: Task>, +} + impl AcpThreadView { pub fn new( agent: Rc, @@ -399,7 +402,6 @@ impl AcpThreadView { hovered_recent_history_item: None, prompt_capabilities, is_loading_contents: false, - install_command_markdown: cx.new(|cx| Markdown::new("".into(), None, None, cx)), _subscriptions: subscriptions, _cancel_task: None, focus_handle: cx.focus_handle(), @@ -420,8 +422,10 @@ impl AcpThreadView { .next() .map(|worktree| worktree.read(cx).abs_path()) .unwrap_or_else(|| paths::home_dir().as_path().into()); + let (tx, mut rx) = watch::channel("Loading…".into()); + let delegate = AgentServerDelegate::new(project.clone(), tx); - let connect_task = agent.connect(&root_dir, &project, cx); + let connect_task = agent.connect(&root_dir, delegate, cx); let load_task = cx.spawn_in(window, async move |this, cx| { let connection = match connect_task.await { Ok(connection) => connection, @@ -574,7 +578,25 @@ impl AcpThreadView { .log_err(); }); - ThreadState::Loading { _task: load_task } + let loading_view = cx.new(|cx| { + let update_title_task = cx.spawn(async move |this, cx| { + loop { + let status = rx.recv().await?; + this.update(cx, |this: &mut LoadingView, cx| { + this.title = status; + cx.notify(); + })?; + } + }); + + LoadingView { + title: "Loading…".into(), + _load_task: load_task, + _update_title_task: update_title_task, + } + }); + + ThreadState::Loading(loading_view) } fn handle_auth_required( @@ -674,13 +696,15 @@ impl AcpThreadView { } } - pub fn title(&self) -> SharedString { + pub fn title(&self, cx: &App) -> SharedString { match &self.thread_state { ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(), - ThreadState::Loading { .. } => "Loading…".into(), + ThreadState::Loading(loading_view) => loading_view.read(cx).title.clone(), ThreadState::LoadError(error) => match error { - LoadError::NotInstalled { .. } => format!("Install {}", self.agent.name()).into(), LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(), + LoadError::FailedToInstall(_) => { + format!("Failed to Install {}", self.agent.name()).into() + } LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(), LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(), }, @@ -2950,18 +2974,26 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) -> AnyElement { - let (message, action_slot): (SharedString, _) = match e { - LoadError::NotInstalled => { - return self.render_not_installed(None, window, cx); - } + let (title, message, action_slot): (_, SharedString, _) = match e { LoadError::Unsupported { command: path, current_version, + minimum_version, } => { - return self.render_not_installed(Some((path, current_version)), window, cx); + return self.render_unsupported(path, current_version, minimum_version, window, cx); } - LoadError::Exited { .. } => ("Server exited with status {status}".into(), None), + LoadError::FailedToInstall(msg) => ( + "Failed to Install", + msg.into(), + Some(self.create_copy_button(msg.to_string()).into_any_element()), + ), + LoadError::Exited { status } => ( + "Failed to Launch", + format!("Server exited with status {status}").into(), + None, + ), LoadError::Other(msg) => ( + "Failed to Launch", msg.into(), Some(self.create_copy_button(msg.to_string()).into_any_element()), ), @@ -2970,95 +3002,34 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) .icon(IconName::XCircleFilled) - .title("Failed to Launch") + .title(title) .description(message) .actions_slot(div().children(action_slot)) .into_any_element() } - fn install_agent(&self, window: &mut Window, cx: &mut Context) { - telemetry::event!("Agent Install CLI", agent = self.agent.telemetry_id()); - let Some(install_command) = self.agent.install_command().map(|s| s.to_owned()) else { - return; - }; - let task = self - .workspace - .update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - let cwd = project.first_project_directory(cx); - let shell = project.terminal_settings(&cwd, cx).shell.clone(); - let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId(install_command.clone()), - full_label: install_command.clone(), - label: install_command.clone(), - command: Some(install_command.clone()), - args: Vec::new(), - command_label: install_command.clone(), - cwd, - env: Default::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: Default::default(), - reveal_target: Default::default(), - hide: Default::default(), - shell, - show_summary: true, - show_command: true, - show_rerun: false, - }; - workspace.spawn_in_terminal(spawn_in_terminal, window, cx) - }) - .ok(); - let Some(task) = task else { return }; - cx.spawn_in(window, async move |this, cx| { - if let Some(Ok(_)) = task.await { - this.update_in(cx, |this, window, cx| { - this.reset(window, cx); - }) - .ok(); - } - }) - .detach() - } - - fn render_not_installed( + fn render_unsupported( &self, - existing_version: Option<(&SharedString, &SharedString)>, - window: &mut Window, + path: &SharedString, + version: &SharedString, + minimum_version: &SharedString, + _window: &mut Window, cx: &mut Context, ) -> AnyElement { - let install_command = self.agent.install_command().unwrap_or_default(); - - self.install_command_markdown.update(cx, |markdown, cx| { - if !markdown.source().contains(&install_command) { - markdown.replace(format!("```\n{}\n```", install_command), cx); - } - }); - - let (heading_label, description_label, button_label) = - if let Some((path, version)) = existing_version { - ( - format!("Upgrade {} to work with Zed", self.agent.name()), - if version.is_empty() { - format!( - "Currently using {}, which does not report a valid --version", - path, - ) - } else { - format!( - "Currently using {}, which is only version {}", - path, version - ) - }, - format!("Upgrade {}", self.agent.name()), + let (heading_label, description_label) = ( + format!("Upgrade {} to work with Zed", self.agent.name()), + if version.is_empty() { + format!( + "Currently using {}, which does not report a valid --version", + path, ) } else { - ( - format!("Get Started with {} in Zed", self.agent.name()), - "Use Google's new coding agent directly in Zed.".to_string(), - format!("Install {}", self.agent.name()), + format!( + "Currently using {}, which is only version {} (need at least {minimum_version})", + path, version ) - }; + }, + ); v_flex() .w_full() @@ -3078,34 +3049,6 @@ impl AcpThreadView { .color(Color::Muted), ), ) - .child( - Button::new("install_gemini", button_label) - .full_width() - .size(ButtonSize::Medium) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .label_size(LabelSize::Small) - .icon(IconName::TerminalGhost) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .on_click(cx.listener(|this, _, window, cx| this.install_agent(window, cx))), - ) - .child( - Label::new("Or, run the following command in your terminal:") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(MarkdownElement::new( - self.install_command_markdown.clone(), - default_markdown_style(false, false, window, cx), - )) - .when_some(existing_version, |el, (path, _)| { - el.child( - Label::new(format!("If this does not work you will need to upgrade manually, or uninstall your existing version from {}", path)) - .size(LabelSize::Small) - .color(Color::Muted), - ) - }) .into_any_element() } @@ -4994,18 +4937,6 @@ impl AcpThreadView { })) } - fn reset(&mut self, window: &mut Window, cx: &mut Context) { - self.thread_state = Self::initial_state( - self.agent.clone(), - None, - self.workspace.clone(), - self.project.clone(), - window, - cx, - ); - cx.notify(); - } - pub fn delete_history_entry(&mut self, entry: HistoryEntry, cx: &mut Context) { let task = match entry { HistoryEntry::AcpThread(thread) => self.history_store.update(cx, |history, cx| { @@ -5534,22 +5465,10 @@ pub(crate) mod tests { "Test".into() } - fn empty_state_headline(&self) -> SharedString { - "Test".into() - } - - fn empty_state_message(&self) -> SharedString { - "Test".into() - } - - fn install_command(&self) -> Option<&'static str> { - None - } - fn connect( &self, _root_dir: &Path, - _project: &Entity, + _delegate: AgentServerDelegate, _cx: &mut App, ) -> Task>> { Task::ready(Ok(Rc::new(self.connection.clone()))) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 224f49cc3e11f71a208a3f8f5b9f777b14478d23..23b6e69a56886ca2e5d7c4bdbd27ee8fb1307629 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -5,7 +5,7 @@ mod tool_picker; use std::{ops::Range, sync::Arc, time::Duration}; -use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini}; +use agent_servers::{AgentServerCommand, AllAgentServersSettings, CustomAgentServerSettings}; use agent_settings::AgentSettings; use anyhow::Result; use assistant_tool::{ToolSource, ToolWorkingSet}; @@ -27,7 +27,6 @@ use language_model::{ }; use notifications::status_toast::{StatusToast, ToastIcon}; use project::{ - Project, context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, project_settings::{ContextServerSettings, ProjectSettings}, }; @@ -52,7 +51,6 @@ pub struct AgentConfiguration { fs: Arc, language_registry: Arc, workspace: WeakEntity, - project: WeakEntity, focus_handle: FocusHandle, configuration_views_by_provider: HashMap, context_server_store: Entity, @@ -62,7 +60,6 @@ pub struct AgentConfiguration { _registry_subscription: Subscription, scroll_handle: ScrollHandle, scrollbar_state: ScrollbarState, - gemini_is_installed: bool, _check_for_gemini: Task<()>, } @@ -73,7 +70,6 @@ impl AgentConfiguration { tools: Entity, language_registry: Arc, workspace: WeakEntity, - project: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -98,11 +94,6 @@ impl AgentConfiguration { cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify()) .detach(); - cx.observe_global_in::(window, |this, _, cx| { - this.check_for_gemini(cx); - cx.notify(); - }) - .detach(); let scroll_handle = ScrollHandle::new(); let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); @@ -111,7 +102,6 @@ impl AgentConfiguration { fs, language_registry, workspace, - project, focus_handle, configuration_views_by_provider: HashMap::default(), context_server_store, @@ -121,11 +111,9 @@ impl AgentConfiguration { _registry_subscription: registry_subscription, scroll_handle, scrollbar_state, - gemini_is_installed: false, _check_for_gemini: Task::ready(()), }; this.build_provider_configuration_views(window, cx); - this.check_for_gemini(cx); this } @@ -155,34 +143,6 @@ impl AgentConfiguration { self.configuration_views_by_provider .insert(provider.id(), configuration_view); } - - fn check_for_gemini(&mut self, cx: &mut Context) { - let project = self.project.clone(); - let settings = AllAgentServersSettings::get_global(cx).clone(); - self._check_for_gemini = cx.spawn({ - async move |this, cx| { - let Some(project) = project.upgrade() else { - return; - }; - let gemini_is_installed = AgentServerCommand::resolve( - Gemini::binary_name(), - &[], - // TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here - None, - settings.gemini, - &project, - cx, - ) - .await - .is_some(); - this.update(cx, |this, cx| { - this.gemini_is_installed = gemini_is_installed; - cx.notify(); - }) - .ok(); - } - }); - } } impl Focusable for AgentConfiguration { @@ -1041,9 +1001,8 @@ impl AgentConfiguration { name.clone(), ExternalAgent::Custom { name: name.clone(), - settings: settings.clone(), + command: settings.command.clone(), }, - None, cx, ) .into_any_element() @@ -1102,7 +1061,6 @@ impl AgentConfiguration { IconName::AiGemini, "Gemini CLI", ExternalAgent::Gemini, - (!self.gemini_is_installed).then_some(Gemini::install_command().into()), cx, )) // TODO add CC @@ -1115,7 +1073,6 @@ impl AgentConfiguration { icon: IconName, name: impl Into, agent: ExternalAgent, - install_command: Option, cx: &mut Context, ) -> impl IntoElement { let name = name.into(); @@ -1135,88 +1092,28 @@ impl AgentConfiguration { .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) .child(Label::new(name.clone())), ) - .map(|this| { - if let Some(install_command) = install_command { - this.child( - Button::new( - SharedString::from(format!("install_external_agent-{name}")), - "Install Agent", - ) - .label_size(LabelSize::Small) - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .tooltip(Tooltip::text(install_command.clone())) - .on_click(cx.listener( - move |this, _, window, cx| { - let Some(project) = this.project.upgrade() else { - return; - }; - let Some(workspace) = this.workspace.upgrade() else { - return; - }; - let cwd = project.read(cx).first_project_directory(cx); - let shell = - project.read(cx).terminal_settings(&cwd, cx).shell.clone(); - let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId(install_command.to_string()), - full_label: install_command.to_string(), - label: install_command.to_string(), - command: Some(install_command.to_string()), - args: Vec::new(), - command_label: install_command.to_string(), - cwd, - env: Default::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: Default::default(), - reveal_target: Default::default(), - hide: Default::default(), - shell, - show_summary: true, - show_command: true, - show_rerun: false, - }; - let task = workspace.update(cx, |workspace, cx| { - workspace.spawn_in_terminal(spawn_in_terminal, window, cx) - }); - cx.spawn(async move |this, cx| { - task.await; - this.update(cx, |this, cx| { - this.check_for_gemini(cx); - }) - .ok(); - }) - .detach(); - }, - )), - ) - } else { - this.child( - h_flex().gap_1().child( - Button::new( - SharedString::from(format!("start_acp_thread-{name}")), - "Start New Thread", - ) - .label_size(LabelSize::Small) - .icon(IconName::Thread) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click(move |_, window, cx| { - window.dispatch_action( - NewExternalAgentThread { - agent: Some(agent.clone()), - } - .boxed_clone(), - cx, - ); - }), - ), + .child( + h_flex().gap_1().child( + Button::new( + SharedString::from(format!("start_acp_thread-{name}")), + "Start New Thread", ) - } - }) + .label_size(LabelSize::Small) + .icon(IconName::Thread) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .on_click(move |_, window, cx| { + window.dispatch_action( + NewExternalAgentThread { + agent: Some(agent.clone()), + } + .boxed_clone(), + cx, + ); + }), + ), + ) } } @@ -1393,7 +1290,7 @@ async fn open_new_agent_servers_entry_in_settings_editor( unique_server_name = Some(server_name.clone()); file.custom.insert( server_name, - AgentServerSettings { + CustomAgentServerSettings { command: AgentServerCommand { path: "path_to_executable".into(), args: vec![], diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 586a782bc35211fa77c744b229fb7bf9f1ef0057..232311c5b02cdaa9edad4c0e9053163f450378e8 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use std::time::Duration; use acp_thread::AcpThread; -use agent_servers::AgentServerSettings; +use agent_servers::AgentServerCommand; use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; @@ -259,7 +259,7 @@ pub enum AgentType { NativeAgent, Custom { name: SharedString, - settings: AgentServerSettings, + command: AgentServerCommand, }, } @@ -1479,7 +1479,6 @@ impl AgentPanel { tools, self.language_registry.clone(), self.workspace.clone(), - self.project.downgrade(), window, cx, ) @@ -1896,8 +1895,8 @@ impl AgentPanel { window, cx, ), - AgentType::Custom { name, settings } => self.external_thread( - Some(crate::ExternalAgent::Custom { name, settings }), + AgentType::Custom { name, command } => self.external_thread( + Some(crate::ExternalAgent::Custom { name, command }), None, None, window, @@ -2115,7 +2114,7 @@ impl AgentPanel { .child(title_editor) .into_any_element() } else { - Label::new(thread_view.read(cx).title()) + Label::new(thread_view.read(cx).title(cx)) .color(Color::Muted) .truncate() .into_any_element() @@ -2664,9 +2663,9 @@ impl AgentPanel { AgentType::Custom { name: agent_name .clone(), - settings: - agent_settings - .clone(), + command: agent_settings + .command + .clone(), }, window, cx, diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 110c432df3932f902b6ffcdac505a86e88550b28..93a4a8f748eefc933f809669af841f443888f7ed 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -28,7 +28,7 @@ use std::rc::Rc; use std::sync::Arc; use agent::{Thread, ThreadId}; -use agent_servers::AgentServerSettings; +use agent_servers::AgentServerCommand; use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection}; use assistant_slash_command::SlashCommandRegistry; use client::Client; @@ -170,7 +170,7 @@ enum ExternalAgent { NativeAgent, Custom { name: SharedString, - settings: AgentServerSettings, + command: AgentServerCommand, }, } @@ -193,9 +193,9 @@ impl ExternalAgent { Self::Gemini => Rc::new(agent_servers::Gemini), Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode), Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), - Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new( + Self::Custom { name, command } => Rc::new(agent_servers::CustomAgentServer::new( name.clone(), - settings, + command.clone(), )), } } From f2e62c98d151370eb138547586441b25594afd10 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Thu, 28 Aug 2025 21:48:35 +0200 Subject: [PATCH 421/744] docs: Fix broken link in `agent-panel.md` (#37113) This fixes a small typo I stumbled upon, which caused a 404 within the docs. Release Notes: - N/A --- docs/src/ai/agent-panel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/ai/agent-panel.md b/docs/src/ai/agent-panel.md index ff5fdf84ce354f0c28c1986de84e28fa1c3c7ada..002c7d64150d53f734b9f1bbce87567b7c05036a 100644 --- a/docs/src/ai/agent-panel.md +++ b/docs/src/ai/agent-panel.md @@ -25,7 +25,7 @@ You should start to see the responses stream in with indications of [which tools By default, the Agent Panel uses Zed's first-party agent. To change that, go to the plus button in the top-right of the Agent Panel and choose another option. -You choose to create a new [Text Thread](./text-threads.md) or, if you have [external agents](/.external-agents.md) connected, you can create new threads with them. +You choose to create a new [Text Thread](./text-threads.md) or, if you have [external agents](./external-agents.md) connected, you can create new threads with them. ### Editing Messages {#editing-messages} From 88e8f7af6861f56edddccd9a87790602043c48fb Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Thu, 28 Aug 2025 14:07:02 -0700 Subject: [PATCH 422/744] Activate preview for initially selected item (#37112) @JosephTLyons pointed out that it's a bit weird that we only show a preview for items selected after the initial one, so this does it for that too. It makes tab switching feel even faster! Release Notes: - N/A Co-authored-by: David Kleingeld --- crates/tab_switcher/src/tab_switcher.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 5f60bc03f2a74f06680007d26edf63168b9c256e..241642115a3025aba13fe1aa8788e2c382b24693 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -360,7 +360,12 @@ impl TabSwitcherDelegate { .detach(); } - fn update_all_pane_matches(&mut self, query: String, window: &mut Window, cx: &mut App) { + fn update_all_pane_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) { let Some(workspace) = self.workspace.upgrade() else { return; }; @@ -418,7 +423,7 @@ impl TabSwitcherDelegate { let selected_item_id = self.selected_item_id(); self.matches = matches; - self.selected_index = self.compute_selected_index(selected_item_id); + self.selected_index = self.compute_selected_index(selected_item_id, window, cx); } fn update_matches( @@ -477,7 +482,7 @@ impl TabSwitcherDelegate { a_score.cmp(&b_score) }); - self.selected_index = self.compute_selected_index(selected_item_id); + self.selected_index = self.compute_selected_index(selected_item_id, window, cx); } fn selected_item_id(&self) -> Option { @@ -486,7 +491,12 @@ impl TabSwitcherDelegate { .map(|tab_match| tab_match.item.item_id()) } - fn compute_selected_index(&mut self, prev_selected_item_id: Option) -> usize { + fn compute_selected_index( + &mut self, + prev_selected_item_id: Option, + window: &mut Window, + cx: &mut Context>, + ) -> usize { if self.matches.is_empty() { return 0; } @@ -508,8 +518,10 @@ impl TabSwitcherDelegate { return self.matches.len() - 1; } + // This only runs when initially opening the picker + // Index 0 is already active, so don't preselect it for switching. if self.matches.len() > 1 { - // Index 0 is active, so don't preselect it for switching. + self.set_selected_index(1, window, cx); return 1; } From 08c23c92ca78a669a166f779cfefd2781b5f3d90 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 28 Aug 2025 14:16:06 -0700 Subject: [PATCH 423/744] acp: Bump to 0.1.1 (#37119) No big changes, just tracking the latest version after the official release Release Notes: - N/A --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e3bfd18c2991328de18149be6688fcfc303eb61..a77d1e68c01131b6c135552010a913645610634e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,9 +191,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.31" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860" +checksum = "6b91e5ec3ce05e8effb2a7a3b7b1a587daa6699b9f98bbde6a35e44b8c6c773a" dependencies = [ "anyhow", "async-broadcast", diff --git a/Cargo.toml b/Cargo.toml index 209c312aec4061c97d547a6157dece5ed5402cf8..974796a5e5ff4a3093fc8b492628e1c6d33a616a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -426,7 +426,7 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agent-client-protocol = "0.0.31" +agent-client-protocol = "0.1" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" From 930189ed83a7ed641b38064a22f43d3955332ee3 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 28 Aug 2025 17:38:14 -0400 Subject: [PATCH 424/744] acp: Support automatic installation of Claude Code (#37120) Release Notes: - N/A --- Cargo.lock | 5 - crates/agent_servers/Cargo.toml | 5 - crates/agent_servers/src/agent_servers.rs | 10 +- crates/agent_servers/src/claude.rs | 1362 +---------------- crates/agent_servers/src/claude/edit_tool.rs | 178 --- crates/agent_servers/src/claude/mcp_server.rs | 99 -- .../src/claude/permission_tool.rs | 158 -- crates/agent_servers/src/claude/read_tool.rs | 59 - crates/agent_servers/src/claude/tools.rs | 688 --------- crates/agent_servers/src/claude/write_tool.rs | 59 - crates/agent_servers/src/custom.rs | 20 +- crates/agent_servers/src/e2e_tests.rs | 10 +- crates/agent_servers/src/gemini.rs | 29 +- crates/agent_servers/src/settings.rs | 2 +- 14 files changed, 76 insertions(+), 2608 deletions(-) delete mode 100644 crates/agent_servers/src/claude/edit_tool.rs delete mode 100644 crates/agent_servers/src/claude/mcp_server.rs delete mode 100644 crates/agent_servers/src/claude/permission_tool.rs delete mode 100644 crates/agent_servers/src/claude/read_tool.rs delete mode 100644 crates/agent_servers/src/claude/tools.rs delete mode 100644 crates/agent_servers/src/claude/write_tool.rs diff --git a/Cargo.lock b/Cargo.lock index a77d1e68c01131b6c135552010a913645610634e..aeacdd899685e46ebd1d38df7cd58b19810de9c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -292,14 +292,12 @@ dependencies = [ "anyhow", "client", "collections", - "context_server", "env_logger 0.11.8", "fs", "futures 0.3.31", "gpui", "gpui_tokio", "indoc", - "itertools 0.14.0", "language", "language_model", "language_models", @@ -309,7 +307,6 @@ dependencies = [ "node_runtime", "paths", "project", - "rand 0.8.5", "reqwest_client", "schemars", "semver", @@ -317,12 +314,10 @@ dependencies = [ "serde_json", "settings", "smol", - "strum 0.27.1", "tempfile", "thiserror 2.0.12", "ui", "util", - "uuid", "watch", "which 6.0.3", "workspace-hack", diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 3e6bae104ce339f371b2ea69afebecbc6c1cec27..222feb9aaa31a6ace1e13ba8943f416942e8918c 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -25,14 +25,12 @@ agent_settings.workspace = true anyhow.workspace = true client = { workspace = true, optional = true } collections.workspace = true -context_server.workspace = true env_logger = { workspace = true, optional = true } fs.workspace = true futures.workspace = true gpui.workspace = true gpui_tokio = { workspace = true, optional = true } indoc.workspace = true -itertools.workspace = true language.workspace = true language_model.workspace = true language_models.workspace = true @@ -40,7 +38,6 @@ log.workspace = true node_runtime.workspace = true paths.workspace = true project.workspace = true -rand.workspace = true reqwest_client = { workspace = true, optional = true } schemars.workspace = true semver.workspace = true @@ -48,12 +45,10 @@ serde.workspace = true serde_json.workspace = true 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 which.workspace = true workspace-hack.workspace = true diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index e5d954b071a86f39a44e7c370dbc841c2f58d706..e1b4057b71b0b4aee84548df74935d9b0598f598 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -57,16 +57,10 @@ impl AgentServerDelegate { binary_name: SharedString, package_name: SharedString, entrypoint_path: PathBuf, - settings: Option, + ignore_system_version: bool, minimum_version: Option, cx: &mut App, ) -> Task> { - if let Some(settings) = &settings - && let Some(command) = settings.clone().custom_command() - { - return Task::ready(Ok(command)); - } - let project = self.project; let fs = project.read(cx).fs().clone(); let Some(node_runtime) = project.read(cx).node_runtime().cloned() else { @@ -75,7 +69,7 @@ impl AgentServerDelegate { let mut status_tx = self.status_tx; cx.spawn(async move |cx| { - if let Some(settings) = settings && !settings.ignore_system_version.unwrap_or(true) { + if !ignore_system_version { if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await { return Ok(AgentServerCommand { path: bin, args: Vec::new(), env: Default::default() }) } diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index b1832191480f112c7788e4e908c2e1594f08c0ad..db8853695ec798a8b146666292cd29f2c1fc145c 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1,47 +1,23 @@ -mod edit_tool; -mod mcp_server; -mod permission_tool; -mod read_tool; -pub mod tools; -mod write_tool; - -use action_log::ActionLog; -use collections::HashMap; -use context_server::listener::McpServerTool; use language_models::provider::anthropic::AnthropicLanguageModelProvider; -use project::Project; use settings::SettingsStore; -use smol::process::Child; use std::any::Any; -use std::cell::RefCell; -use std::fmt::Display; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::rc::Rc; -use util::command::new_smol_command; -use uuid::Uuid; -use agent_client_protocol as acp; -use anyhow::{Context as _, Result, anyhow}; -use futures::channel::oneshot; -use futures::{AsyncBufReadExt, AsyncWriteExt}; -use futures::{ - AsyncRead, AsyncWrite, FutureExt, StreamExt, - channel::mpsc::{self, UnboundedReceiver, UnboundedSender}, - io::BufReader, - select_biased, -}; -use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity}; -use serde::{Deserialize, Serialize}; -use util::{ResultExt, debug_panic}; +use anyhow::Result; +use gpui::{App, AppContext as _, SharedString, Task}; -use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig}; -use crate::claude::tools::ClaudeTool; -use crate::{AgentServer, AgentServerCommand, AgentServerDelegate, AllAgentServersSettings}; -use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri}; +use crate::{AgentServer, AgentServerDelegate, AllAgentServersSettings}; +use acp_thread::AgentConnection; #[derive(Clone)] pub struct ClaudeCode; +impl ClaudeCode { + const BINARY_NAME: &'static str = "claude-code-acp"; + const PACKAGE_NAME: &'static str = "@zed-industries/claude-code-acp"; +} + impl AgentServer for ClaudeCode { fn telemetry_id(&self) -> &'static str { "claude-code" @@ -57,1301 +33,49 @@ impl AgentServer for ClaudeCode { fn connect( &self, - _root_dir: &Path, - _delegate: AgentServerDelegate, - _cx: &mut App, + root_dir: &Path, + delegate: AgentServerDelegate, + cx: &mut App, ) -> Task>> { - let connection = ClaudeAgentConnection { - sessions: Default::default(), - }; - - Task::ready(Ok(Rc::new(connection) as _)) - } - - fn into_any(self: Rc) -> Rc { - self - } -} - -struct ClaudeAgentConnection { - sessions: Rc>>, -} + let root_dir = root_dir.to_path_buf(); + let server_name = self.name(); + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).claude.clone() + }); -impl AgentConnection for ClaudeAgentConnection { - fn new_thread( - self: Rc, - project: Entity, - cwd: &Path, - cx: &mut App, - ) -> Task>> { - let cwd = cwd.to_owned(); cx.spawn(async move |cx| { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).claude.clone() - })?; - - let Some(command) = AgentServerCommand::resolve( - "claude", - &[], - Some(&util::paths::home_dir().join(".claude/local/claude")), - settings, - &project, - cx, - ) - .await - else { - return Err(anyhow!("Failed to find Claude Code binary")); + let mut command = if let Some(settings) = settings { + settings.command + } else { + cx.update(|cx| { + delegate.get_or_npm_install_builtin_agent( + Self::BINARY_NAME.into(), + Self::PACKAGE_NAME.into(), + format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(), + true, + None, + cx, + ) + })? + .await? }; - let api_key = - cx.update(AnthropicLanguageModelProvider::api_key)? - .await - .map_err(|err| { - if err.is::() { - anyhow!(AuthRequired::new().with_language_model_provider( - language_model::ANTHROPIC_PROVIDER_ID - )) - } else { - anyhow!(err) - } - })?; - - let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid()); - let fs = project.read_with(cx, |project, _cx| project.fs().clone())?; - let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), fs, cx).await?; - - let mut mcp_servers = HashMap::default(); - mcp_servers.insert( - mcp_server::SERVER_NAME.to_string(), - permission_mcp_server.server_config()?, - ); - let mcp_config = McpConfig { mcp_servers }; - - let mcp_config_file = tempfile::NamedTempFile::new()?; - let (mcp_config_file, mcp_config_path) = mcp_config_file.into_parts(); - - let mut mcp_config_file = smol::fs::File::from(mcp_config_file); - mcp_config_file - .write_all(serde_json::to_string(&mcp_config)?.as_bytes()) - .await?; - mcp_config_file.flush().await?; - - let (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded(); - let (outgoing_tx, outgoing_rx) = mpsc::unbounded(); - - let session_id = acp::SessionId(Uuid::new_v4().to_string().into()); - - log::trace!("Starting session with id: {}", session_id); - - let mut child = spawn_claude( - &command, - ClaudeSessionMode::Start, - session_id.clone(), - api_key, - &mcp_config_path, - &cwd, - )?; - - let stdout = child.stdout.take().context("Failed to take stdout")?; - let stdin = child.stdin.take().context("Failed to take stdin")?; - let stderr = child.stderr.take().context("Failed to take stderr")?; - - let pid = child.id(); - log::trace!("Spawned (pid: {})", pid); - - cx.background_spawn(async move { - let mut stderr = BufReader::new(stderr); - let mut line = String::new(); - while let Ok(n) = stderr.read_line(&mut line).await - && n > 0 - { - log::warn!("agent stderr: {}", &line); - line.clear(); - } - }) - .detach(); - - cx.background_spawn(async move { - let mut outgoing_rx = Some(outgoing_rx); - - ClaudeAgentSession::handle_io( - outgoing_rx.take().unwrap(), - incoming_message_tx.clone(), - stdin, - stdout, - ) - .await?; - - log::trace!("Stopped (pid: {})", pid); - - drop(mcp_config_path); - anyhow::Ok(()) - }) - .detach(); - - let turn_state = Rc::new(RefCell::new(TurnState::None)); - - let handler_task = cx.spawn({ - let turn_state = turn_state.clone(); - let mut thread_rx = thread_rx.clone(); - async move |cx| { - while let Some(message) = incoming_message_rx.next().await { - ClaudeAgentSession::handle_message( - thread_rx.clone(), - message, - turn_state.clone(), - cx, - ) - .await - } - - if let Some(status) = child.status().await.log_err() - && let Some(thread) = thread_rx.recv().await.ok() - { - let version = claude_version(command.path.clone(), cx).await.log_err(); - let help = claude_help(command.path.clone(), cx).await.log_err(); - thread - .update(cx, |thread, cx| { - let error = if let Some(version) = version - && let Some(help) = help - && (!help.contains("--input-format") - || !help.contains("--session-id")) - { - LoadError::Unsupported { - command: command.path.to_string_lossy().to_string().into(), - current_version: version.to_string().into(), - minimum_version: "1.0.0".into(), - } - } else { - LoadError::Exited { status } - }; - thread.emit_load_error(error, cx); - }) - .ok(); - } - } - }); - - let action_log = cx.new(|_| ActionLog::new(project.clone()))?; - let thread = cx.new(|cx| { - AcpThread::new( - "Claude Code", - self.clone(), - project, - action_log, - session_id.clone(), - watch::Receiver::constant(acp::PromptCapabilities { - image: true, - audio: false, - embedded_context: true, - }), - cx, - ) - })?; - - thread_tx.send(thread.downgrade())?; - - let session = ClaudeAgentSession { - outgoing_tx, - turn_state, - _handler_task: handler_task, - _mcp_server: Some(permission_mcp_server), - }; - - self.sessions.borrow_mut().insert(session_id, session); + if let Some(api_key) = cx + .update(AnthropicLanguageModelProvider::api_key)? + .await + .ok() + { + command + .env + .get_or_insert_default() + .insert("ANTHROPIC_API_KEY".to_owned(), api_key.key); + } - Ok(thread) + crate::acp::connect(server_name, command.clone(), &root_dir, cx).await }) } - 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, - _id: Option, - 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!( - "Attempted to send message to nonexistent session {}", - params.session_id - ))); - }; - - let (end_tx, end_rx) = oneshot::channel(); - session.turn_state.replace(TurnState::InProgress { end_tx }); - - let content = acp_content_to_claude(params.prompt); - - if let Err(err) = session.outgoing_tx.unbounded_send(SdkMessage::User { - message: Message { - role: Role::User, - content: Content::Chunks(content), - id: None, - model: None, - stop_reason: None, - stop_sequence: None, - usage: None, - }, - session_id: Some(params.session_id.to_string()), - }) { - return Task::ready(Err(anyhow!(err))); - } - - cx.foreground_executor().spawn(async move { end_rx.await? }) - } - - fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { - let sessions = self.sessions.borrow(); - let Some(session) = sessions.get(session_id) else { - log::warn!("Attempted to cancel nonexistent session {}", session_id); - return; - }; - - let request_id = new_request_id(); - - let turn_state = session.turn_state.take(); - let TurnState::InProgress { end_tx } = turn_state else { - // Already canceled or idle, put it back - session.turn_state.replace(turn_state); - return; - }; - - session.turn_state.replace(TurnState::CancelRequested { - end_tx, - request_id: request_id.clone(), - }); - - session - .outgoing_tx - .unbounded_send(SdkMessage::ControlRequest { - request_id, - request: ControlRequest::Interrupt, - }) - .log_err(); - } - fn into_any(self: Rc) -> Rc { self } } - -#[derive(Clone, Copy)] -enum ClaudeSessionMode { - Start, - #[expect(dead_code)] - Resume, -} - -fn spawn_claude( - command: &AgentServerCommand, - mode: ClaudeSessionMode, - session_id: acp::SessionId, - api_key: language_models::provider::anthropic::ApiKey, - mcp_config_path: &Path, - root_dir: &Path, -) -> Result { - let child = util::command::new_smol_command(&command.path) - .args([ - "--input-format", - "stream-json", - "--output-format", - "stream-json", - "--print", - "--verbose", - "--mcp-config", - mcp_config_path.to_string_lossy().as_ref(), - "--permission-prompt-tool", - &format!( - "mcp__{}__{}", - mcp_server::SERVER_NAME, - permission_tool::PermissionTool::NAME, - ), - "--allowedTools", - &format!( - "mcp__{}__{}", - mcp_server::SERVER_NAME, - read_tool::ReadTool::NAME - ), - "--disallowedTools", - "Read,Write,Edit,MultiEdit", - ]) - .args(match mode { - ClaudeSessionMode::Start => ["--session-id".to_string(), session_id.to_string()], - ClaudeSessionMode::Resume => ["--resume".to_string(), session_id.to_string()], - }) - .args(command.args.iter().map(|arg| arg.as_str())) - .envs(command.env.iter().flatten()) - .env("ANTHROPIC_API_KEY", api_key.key) - .current_dir(root_dir) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .kill_on_drop(true) - .spawn()?; - - Ok(child) -} - -fn claude_version(path: PathBuf, cx: &mut AsyncApp) -> Task> { - cx.background_spawn(async move { - let output = new_smol_command(path).arg("--version").output().await?; - let output = String::from_utf8(output.stdout)?; - let version = output - .trim() - .strip_suffix(" (Claude Code)") - .context("parsing Claude version")?; - let version = semver::Version::parse(version)?; - anyhow::Ok(version) - }) -} - -fn claude_help(path: PathBuf, cx: &mut AsyncApp) -> Task> { - cx.background_spawn(async move { - let output = new_smol_command(path).arg("--help").output().await?; - let output = String::from_utf8(output.stdout)?; - anyhow::Ok(output) - }) -} - -struct ClaudeAgentSession { - outgoing_tx: UnboundedSender, - turn_state: Rc>, - _mcp_server: Option, - _handler_task: Task<()>, -} - -#[derive(Debug, Default)] -enum TurnState { - #[default] - None, - InProgress { - end_tx: oneshot::Sender>, - }, - CancelRequested { - end_tx: oneshot::Sender>, - request_id: String, - }, - CancelConfirmed { - end_tx: oneshot::Sender>, - }, -} - -impl TurnState { - fn is_canceled(&self) -> bool { - matches!(self, TurnState::CancelConfirmed { .. }) - } - - fn end_tx(self) -> Option>> { - match self { - TurnState::None => None, - TurnState::InProgress { end_tx, .. } => Some(end_tx), - TurnState::CancelRequested { end_tx, .. } => Some(end_tx), - TurnState::CancelConfirmed { end_tx } => Some(end_tx), - } - } - - fn confirm_cancellation(self, id: &str) -> Self { - match self { - TurnState::CancelRequested { request_id, end_tx } if request_id == id => { - TurnState::CancelConfirmed { end_tx } - } - _ => self, - } - } -} - -impl ClaudeAgentSession { - async fn handle_message( - mut thread_rx: watch::Receiver>, - message: SdkMessage, - turn_state: Rc>, - cx: &mut AsyncApp, - ) { - match message { - // we should only be sending these out, they don't need to be in the thread - SdkMessage::ControlRequest { .. } => {} - SdkMessage::User { - message, - session_id: _, - } => { - let Some(thread) = thread_rx - .recv() - .await - .log_err() - .and_then(|entity| entity.upgrade()) - else { - log::error!("Received an SDK message but thread is gone"); - return; - }; - - for chunk in message.content.chunks() { - match chunk { - ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => { - if !turn_state.borrow().is_canceled() { - thread - .update(cx, |thread, cx| { - thread.push_user_content_block(None, text.into(), cx) - }) - .log_err(); - } - } - ContentChunk::ToolResult { - content, - tool_use_id, - } => { - let content = content.to_string(); - thread - .update(cx, |thread, cx| { - let id = acp::ToolCallId(tool_use_id.into()); - let set_new_content = !content.is_empty() - && thread.tool_call(&id).is_none_or(|(_, tool_call)| { - // preserve rich diff if we have one - tool_call.diffs().next().is_none() - }); - - thread.update_tool_call( - acp::ToolCallUpdate { - id, - fields: acp::ToolCallUpdateFields { - status: if turn_state.borrow().is_canceled() { - // Do not set to completed if turn was canceled - None - } else { - Some(acp::ToolCallStatus::Completed) - }, - content: set_new_content - .then(|| vec![content.into()]), - ..Default::default() - }, - }, - cx, - ) - }) - .log_err(); - } - ContentChunk::Thinking { .. } - | ContentChunk::RedactedThinking - | ContentChunk::ToolUse { .. } => { - debug_panic!( - "Should not get {:?} with role: assistant. should we handle this?", - chunk - ); - } - ContentChunk::Image { source } => { - if !turn_state.borrow().is_canceled() { - thread - .update(cx, |thread, cx| { - thread.push_user_content_block(None, source.into(), cx) - }) - .log_err(); - } - } - - ContentChunk::Document | ContentChunk::WebSearchToolResult => { - thread - .update(cx, |thread, cx| { - thread.push_assistant_content_block( - format!("Unsupported content: {:?}", chunk).into(), - false, - cx, - ) - }) - .log_err(); - } - } - } - } - SdkMessage::Assistant { - message, - session_id: _, - } => { - let Some(thread) = thread_rx - .recv() - .await - .log_err() - .and_then(|entity| entity.upgrade()) - else { - log::error!("Received an SDK message but thread is gone"); - return; - }; - - for chunk in message.content.chunks() { - match chunk { - ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => { - thread - .update(cx, |thread, cx| { - thread.push_assistant_content_block(text.into(), false, cx) - }) - .log_err(); - } - ContentChunk::Thinking { thinking } => { - thread - .update(cx, |thread, cx| { - thread.push_assistant_content_block(thinking.into(), true, cx) - }) - .log_err(); - } - ContentChunk::RedactedThinking => { - thread - .update(cx, |thread, cx| { - thread.push_assistant_content_block( - "[REDACTED]".into(), - true, - cx, - ) - }) - .log_err(); - } - ContentChunk::ToolUse { id, name, input } => { - let claude_tool = ClaudeTool::infer(&name, input); - - thread - .update(cx, |thread, cx| { - if let ClaudeTool::TodoWrite(Some(params)) = claude_tool { - thread.update_plan( - acp::Plan { - entries: params - .todos - .into_iter() - .map(Into::into) - .collect(), - }, - cx, - ) - } else { - thread.upsert_tool_call( - claude_tool.as_acp(acp::ToolCallId(id.into())), - cx, - )?; - } - anyhow::Ok(()) - }) - .log_err(); - } - ContentChunk::ToolResult { .. } | ContentChunk::WebSearchToolResult => { - debug_panic!( - "Should not get tool results with role: assistant. should we handle this?" - ); - } - ContentChunk::Image { source } => { - thread - .update(cx, |thread, cx| { - thread.push_assistant_content_block(source.into(), false, cx) - }) - .log_err(); - } - ContentChunk::Document => { - thread - .update(cx, |thread, cx| { - thread.push_assistant_content_block( - format!("Unsupported content: {:?}", chunk).into(), - false, - cx, - ) - }) - .log_err(); - } - } - } - } - SdkMessage::Result { - is_error, - subtype, - result, - .. - } => { - let turn_state = turn_state.take(); - let was_canceled = turn_state.is_canceled(); - let Some(end_turn_tx) = turn_state.end_tx() else { - debug_panic!("Received `SdkMessage::Result` but there wasn't an active turn"); - return; - }; - - if is_error || (!was_canceled && subtype == ResultErrorType::ErrorDuringExecution) { - end_turn_tx - .send(Err(anyhow!( - "Error: {}", - result.unwrap_or_else(|| subtype.to_string()) - ))) - .ok(); - } else { - let stop_reason = match subtype { - ResultErrorType::Success => acp::StopReason::EndTurn, - ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests, - ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled, - }; - end_turn_tx - .send(Ok(acp::PromptResponse { stop_reason })) - .ok(); - } - } - SdkMessage::ControlResponse { response } => { - if matches!(response.subtype, ResultErrorType::Success) { - let new_state = turn_state.take().confirm_cancellation(&response.request_id); - turn_state.replace(new_state); - } else { - log::error!("Control response error: {:?}", response); - } - } - SdkMessage::System { .. } => {} - } - } - - async fn handle_io( - mut outgoing_rx: UnboundedReceiver, - incoming_tx: UnboundedSender, - mut outgoing_bytes: impl Unpin + AsyncWrite, - incoming_bytes: impl Unpin + AsyncRead, - ) -> Result> { - let mut output_reader = BufReader::new(incoming_bytes); - let mut outgoing_line = Vec::new(); - let mut incoming_line = String::new(); - loop { - select_biased! { - message = outgoing_rx.next() => { - if let Some(message) = message { - outgoing_line.clear(); - serde_json::to_writer(&mut outgoing_line, &message)?; - log::trace!("send: {}", String::from_utf8_lossy(&outgoing_line)); - outgoing_line.push(b'\n'); - outgoing_bytes.write_all(&outgoing_line).await.ok(); - } else { - break; - } - } - bytes_read = output_reader.read_line(&mut incoming_line).fuse() => { - if bytes_read? == 0 { - break - } - log::trace!("recv: {}", &incoming_line); - match serde_json::from_str::(&incoming_line) { - Ok(message) => { - incoming_tx.unbounded_send(message).log_err(); - } - Err(error) => { - log::error!("failed to parse incoming message: {error}. Raw: {incoming_line}"); - } - } - incoming_line.clear(); - } - } - } - - Ok(outgoing_rx) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Message { - role: Role, - content: Content, - #[serde(skip_serializing_if = "Option::is_none")] - id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - model: Option, - #[serde(skip_serializing_if = "Option::is_none")] - stop_reason: Option, - #[serde(skip_serializing_if = "Option::is_none")] - stop_sequence: Option, - #[serde(skip_serializing_if = "Option::is_none")] - usage: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -enum Content { - UntaggedText(String), - Chunks(Vec), -} - -impl Content { - pub fn chunks(self) -> impl Iterator { - match self { - Self::Chunks(chunks) => chunks.into_iter(), - Self::UntaggedText(text) => vec![ContentChunk::Text { text }].into_iter(), - } - } -} - -impl Display for Content { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Content::UntaggedText(txt) => write!(f, "{}", txt), - Content::Chunks(chunks) => { - for chunk in chunks { - write!(f, "{}", chunk)?; - } - Ok(()) - } - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum ContentChunk { - Text { - text: String, - }, - ToolUse { - id: String, - name: String, - input: serde_json::Value, - }, - ToolResult { - content: Content, - tool_use_id: String, - }, - Thinking { - thinking: String, - }, - RedactedThinking, - Image { - source: ImageSource, - }, - // TODO - Document, - WebSearchToolResult, - #[serde(untagged)] - UntaggedText(String), -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum ImageSource { - Base64 { data: String, media_type: String }, - Url { url: String }, -} - -impl Into for ImageSource { - fn into(self) -> acp::ContentBlock { - match self { - ImageSource::Base64 { data, media_type } => { - acp::ContentBlock::Image(acp::ImageContent { - annotations: None, - data, - mime_type: media_type, - uri: None, - }) - } - ImageSource::Url { url } => acp::ContentBlock::Image(acp::ImageContent { - annotations: None, - data: "".to_string(), - mime_type: "".to_string(), - uri: Some(url), - }), - } - } -} - -impl Display for ContentChunk { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ContentChunk::Text { text } => write!(f, "{}", text), - ContentChunk::Thinking { thinking } => write!(f, "Thinking: {}", thinking), - ContentChunk::RedactedThinking => write!(f, "Thinking: [REDACTED]"), - ContentChunk::UntaggedText(text) => write!(f, "{}", text), - ContentChunk::ToolResult { content, .. } => write!(f, "{}", content), - ContentChunk::Image { .. } - | ContentChunk::Document - | ContentChunk::ToolUse { .. } - | ContentChunk::WebSearchToolResult => { - write!(f, "\n{:?}\n", &self) - } - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Usage { - input_tokens: u32, - cache_creation_input_tokens: u32, - cache_read_input_tokens: u32, - output_tokens: u32, - service_tier: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -enum Role { - System, - Assistant, - User, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct MessageParam { - role: Role, - content: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum SdkMessage { - // An assistant message - Assistant { - message: Message, // from Anthropic SDK - #[serde(skip_serializing_if = "Option::is_none")] - session_id: Option, - }, - // A user message - User { - message: Message, // from Anthropic SDK - #[serde(skip_serializing_if = "Option::is_none")] - session_id: Option, - }, - // Emitted as the last message in a conversation - Result { - subtype: ResultErrorType, - duration_ms: f64, - duration_api_ms: f64, - is_error: bool, - num_turns: i32, - #[serde(skip_serializing_if = "Option::is_none")] - result: Option, - session_id: String, - total_cost_usd: f64, - }, - // Emitted as the first message at the start of a conversation - System { - cwd: String, - session_id: String, - tools: Vec, - model: String, - mcp_servers: Vec, - #[serde(rename = "apiKeySource")] - api_key_source: String, - #[serde(rename = "permissionMode")] - permission_mode: PermissionMode, - }, - /// Messages used to control the conversation, outside of chat messages to the model - ControlRequest { - request_id: String, - request: ControlRequest, - }, - /// Response to a control request - ControlResponse { response: ControlResponse }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "subtype", rename_all = "snake_case")] -enum ControlRequest { - /// Cancel the current conversation - Interrupt, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ControlResponse { - request_id: String, - subtype: ResultErrorType, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "snake_case")] -enum ResultErrorType { - Success, - ErrorMaxTurns, - ErrorDuringExecution, -} - -impl Display for ResultErrorType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ResultErrorType::Success => write!(f, "success"), - ResultErrorType::ErrorMaxTurns => write!(f, "error_max_turns"), - ResultErrorType::ErrorDuringExecution => write!(f, "error_during_execution"), - } - } -} - -fn acp_content_to_claude(prompt: Vec) -> Vec { - let mut content = Vec::with_capacity(prompt.len()); - let mut context = Vec::with_capacity(prompt.len()); - - for chunk in prompt { - match chunk { - acp::ContentBlock::Text(text_content) => { - content.push(ContentChunk::Text { - text: text_content.text, - }); - } - acp::ContentBlock::ResourceLink(resource_link) => { - match MentionUri::parse(&resource_link.uri) { - Ok(uri) => { - content.push(ContentChunk::Text { - text: format!("{}", uri.as_link()), - }); - } - Err(_) => { - content.push(ContentChunk::Text { - text: resource_link.uri, - }); - } - } - } - acp::ContentBlock::Resource(resource) => match resource.resource { - acp::EmbeddedResourceResource::TextResourceContents(resource) => { - match MentionUri::parse(&resource.uri) { - Ok(uri) => { - content.push(ContentChunk::Text { - text: format!("{}", uri.as_link()), - }); - } - Err(_) => { - content.push(ContentChunk::Text { - text: resource.uri.clone(), - }); - } - } - - context.push(ContentChunk::Text { - text: format!( - "\n\n{}\n", - resource.uri, resource.text - ), - }); - } - acp::EmbeddedResourceResource::BlobResourceContents(_) => { - // Unsupported by SDK - } - }, - acp::ContentBlock::Image(acp::ImageContent { - data, mime_type, .. - }) => content.push(ContentChunk::Image { - source: ImageSource::Base64 { - data, - media_type: mime_type, - }, - }), - acp::ContentBlock::Audio(_) => { - // Unsupported by SDK - } - } - } - - content.extend(context); - content -} - -fn new_request_id() -> String { - use rand::Rng; - // In the Claude Code TS SDK they just generate a random 12 character string, - // `Math.random().toString(36).substring(2, 15)` - rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(12) - .map(char::from) - .collect() -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct McpServer { - name: String, - status: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -enum PermissionMode { - Default, - AcceptEdits, - BypassPermissions, - Plan, -} - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use crate::e2e_tests; - use gpui::TestAppContext; - use serde_json::json; - - crate::common_e2e_tests!(async |_, _, _| ClaudeCode, allow_option_id = "allow"); - - pub fn local_command() -> AgentServerCommand { - AgentServerCommand { - path: "claude".into(), - args: vec![], - env: None, - } - } - - #[gpui::test] - #[cfg_attr(not(feature = "e2e"), ignore)] - async fn test_todo_plan(cx: &mut TestAppContext) { - let fs = e2e_tests::init_test(cx).await; - let project = Project::test(fs, [], cx).await; - let thread = - e2e_tests::new_test_thread(ClaudeCode, project.clone(), "/private/tmp", cx).await; - - thread - .update(cx, |thread, cx| { - thread.send_raw( - "Create a todo plan for initializing a new React app. I'll follow it myself, do not execute on it.", - cx, - ) - }) - .await - .unwrap(); - - let mut entries_len = 0; - - thread.read_with(cx, |thread, _| { - entries_len = thread.plan().entries.len(); - assert!(!thread.plan().entries.is_empty(), "Empty plan"); - }); - - thread - .update(cx, |thread, cx| { - thread.send_raw( - "Mark the first entry status as in progress without acting on it.", - cx, - ) - }) - .await - .unwrap(); - - thread.read_with(cx, |thread, _| { - assert!(matches!( - thread.plan().entries[0].status, - acp::PlanEntryStatus::InProgress - )); - assert_eq!(thread.plan().entries.len(), entries_len); - }); - - thread - .update(cx, |thread, cx| { - thread.send_raw( - "Now mark the first entry as completed without acting on it.", - cx, - ) - }) - .await - .unwrap(); - - thread.read_with(cx, |thread, _| { - assert!(matches!( - thread.plan().entries[0].status, - acp::PlanEntryStatus::Completed - )); - assert_eq!(thread.plan().entries.len(), entries_len); - }); - } - - #[test] - fn test_deserialize_content_untagged_text() { - let json = json!("Hello, world!"); - let content: Content = serde_json::from_value(json).unwrap(); - match content { - Content::UntaggedText(text) => assert_eq!(text, "Hello, world!"), - _ => panic!("Expected UntaggedText variant"), - } - } - - #[test] - fn test_deserialize_content_chunks() { - let json = json!([ - { - "type": "text", - "text": "Hello" - }, - { - "type": "tool_use", - "id": "tool_123", - "name": "calculator", - "input": {"operation": "add", "a": 1, "b": 2} - } - ]); - let content: Content = serde_json::from_value(json).unwrap(); - match content { - Content::Chunks(chunks) => { - assert_eq!(chunks.len(), 2); - match &chunks[0] { - ContentChunk::Text { text } => assert_eq!(text, "Hello"), - _ => panic!("Expected Text chunk"), - } - match &chunks[1] { - ContentChunk::ToolUse { id, name, input } => { - assert_eq!(id, "tool_123"); - assert_eq!(name, "calculator"); - assert_eq!(input["operation"], "add"); - assert_eq!(input["a"], 1); - assert_eq!(input["b"], 2); - } - _ => panic!("Expected ToolUse chunk"), - } - } - _ => panic!("Expected Chunks variant"), - } - } - - #[test] - fn test_deserialize_tool_result_untagged_text() { - let json = json!({ - "type": "tool_result", - "content": "Result content", - "tool_use_id": "tool_456" - }); - let chunk: ContentChunk = serde_json::from_value(json).unwrap(); - match chunk { - ContentChunk::ToolResult { - content, - tool_use_id, - } => { - match content { - Content::UntaggedText(text) => assert_eq!(text, "Result content"), - _ => panic!("Expected UntaggedText content"), - } - assert_eq!(tool_use_id, "tool_456"); - } - _ => panic!("Expected ToolResult variant"), - } - } - - #[test] - fn test_deserialize_tool_result_chunks() { - let json = json!({ - "type": "tool_result", - "content": [ - { - "type": "text", - "text": "Processing complete" - }, - { - "type": "text", - "text": "Result: 42" - } - ], - "tool_use_id": "tool_789" - }); - let chunk: ContentChunk = serde_json::from_value(json).unwrap(); - match chunk { - ContentChunk::ToolResult { - content, - tool_use_id, - } => { - match content { - Content::Chunks(chunks) => { - assert_eq!(chunks.len(), 2); - match &chunks[0] { - ContentChunk::Text { text } => assert_eq!(text, "Processing complete"), - _ => panic!("Expected Text chunk"), - } - match &chunks[1] { - ContentChunk::Text { text } => assert_eq!(text, "Result: 42"), - _ => panic!("Expected Text chunk"), - } - } - _ => panic!("Expected Chunks content"), - } - assert_eq!(tool_use_id, "tool_789"); - } - _ => panic!("Expected ToolResult variant"), - } - } - - #[test] - fn test_acp_content_to_claude() { - let acp_content = vec![ - acp::ContentBlock::Text(acp::TextContent { - text: "Hello world".to_string(), - annotations: None, - }), - acp::ContentBlock::Image(acp::ImageContent { - data: "base64data".to_string(), - mime_type: "image/png".to_string(), - annotations: None, - uri: None, - }), - acp::ContentBlock::ResourceLink(acp::ResourceLink { - uri: "file:///path/to/example.rs".to_string(), - name: "example.rs".to_string(), - annotations: None, - description: None, - mime_type: None, - size: None, - title: None, - }), - acp::ContentBlock::Resource(acp::EmbeddedResource { - annotations: None, - resource: acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents { - mime_type: None, - text: "fn main() { println!(\"Hello!\"); }".to_string(), - uri: "file:///path/to/code.rs".to_string(), - }, - ), - }), - acp::ContentBlock::ResourceLink(acp::ResourceLink { - uri: "invalid_uri_format".to_string(), - name: "invalid.txt".to_string(), - annotations: None, - description: None, - mime_type: None, - size: None, - title: None, - }), - ]; - - let claude_content = acp_content_to_claude(acp_content); - - assert_eq!(claude_content.len(), 6); - - match &claude_content[0] { - ContentChunk::Text { text } => assert_eq!(text, "Hello world"), - _ => panic!("Expected Text chunk"), - } - - match &claude_content[1] { - ContentChunk::Image { source } => match source { - ImageSource::Base64 { data, media_type } => { - assert_eq!(data, "base64data"); - assert_eq!(media_type, "image/png"); - } - _ => panic!("Expected Base64 image source"), - }, - _ => panic!("Expected Image chunk"), - } - - match &claude_content[2] { - ContentChunk::Text { text } => { - assert!(text.contains("example.rs")); - assert!(text.contains("file:///path/to/example.rs")); - } - _ => panic!("Expected Text chunk for ResourceLink"), - } - - match &claude_content[3] { - ContentChunk::Text { text } => { - assert!(text.contains("code.rs")); - assert!(text.contains("file:///path/to/code.rs")); - } - _ => panic!("Expected Text chunk for Resource"), - } - - match &claude_content[4] { - ContentChunk::Text { text } => { - assert_eq!(text, "invalid_uri_format"); - } - _ => panic!("Expected Text chunk for invalid URI"), - } - - match &claude_content[5] { - ContentChunk::Text { text } => { - assert!(text.contains("")); - assert!(text.contains("fn main() { println!(\"Hello!\"); }")); - assert!(text.contains("")); - } - _ => panic!("Expected Text chunk for context"), - } - } -} diff --git a/crates/agent_servers/src/claude/edit_tool.rs b/crates/agent_servers/src/claude/edit_tool.rs deleted file mode 100644 index a8d93c3f3d5579173709b3ace6194059745885a7..0000000000000000000000000000000000000000 --- a/crates/agent_servers/src/claude/edit_tool.rs +++ /dev/null @@ -1,178 +0,0 @@ -use acp_thread::AcpThread; -use anyhow::Result; -use context_server::{ - listener::{McpServerTool, ToolResponse}, - types::{ToolAnnotations, ToolResponseContent}, -}; -use gpui::{AsyncApp, WeakEntity}; -use language::unified_diff; -use util::markdown::MarkdownCodeBlock; - -use crate::tools::EditToolParams; - -#[derive(Clone)] -pub struct EditTool { - thread_rx: watch::Receiver>, -} - -impl EditTool { - pub fn new(thread_rx: watch::Receiver>) -> Self { - Self { thread_rx } - } -} - -impl McpServerTool for EditTool { - type Input = EditToolParams; - type Output = (); - - const NAME: &'static str = "Edit"; - - fn annotations(&self) -> ToolAnnotations { - ToolAnnotations { - title: Some("Edit file".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(false), - open_world_hint: Some(false), - idempotent_hint: Some(false), - } - } - - 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.abs_path.clone(), None, None, true, cx) - })? - .await?; - - let (new_content, diff) = cx - .background_executor() - .spawn(async move { - let new_content = content.replace(&input.old_text, &input.new_text); - if new_content == content { - return Err(anyhow::anyhow!("Failed to find `old_text`",)); - } - let diff = unified_diff(&content, &new_content); - - Ok((new_content, diff)) - }) - .await?; - - thread - .update(cx, |thread, cx| { - thread.write_text_file(input.abs_path, new_content, cx) - })? - .await?; - - Ok(ToolResponse { - content: vec![ToolResponseContent::Text { - text: MarkdownCodeBlock { - tag: "diff", - text: diff.as_str().trim_end_matches('\n'), - } - .to_string(), - }], - structured_content: (), - }) - } -} - -#[cfg(test)] -mod tests { - use std::rc::Rc; - - use acp_thread::{AgentConnection, StubAgentConnection}; - use gpui::{Entity, TestAppContext}; - use indoc::indoc; - use project::{FakeFs, Project}; - use serde_json::json; - use settings::SettingsStore; - use util::path; - - use super::*; - - #[gpui::test] - async fn old_text_not_found(cx: &mut TestAppContext) { - let (_thread, tool) = init_test(cx).await; - - let result = tool - .run( - EditToolParams { - abs_path: path!("/root/file.txt").into(), - old_text: "hi".into(), - new_text: "bye".into(), - }, - &mut cx.to_async(), - ) - .await; - - assert_eq!(result.unwrap_err().to_string(), "Failed to find `old_text`"); - } - - #[gpui::test] - async fn found_and_replaced(cx: &mut TestAppContext) { - let (_thread, tool) = init_test(cx).await; - - let result = tool - .run( - EditToolParams { - abs_path: path!("/root/file.txt").into(), - old_text: "hello".into(), - new_text: "hi".into(), - }, - &mut cx.to_async(), - ) - .await; - - assert_eq!( - result.unwrap().content[0].text().unwrap(), - indoc! { - r" - ```diff - @@ -1,1 +1,1 @@ - -hello - +hi - ``` - " - } - ); - } - - async fn init_test(cx: &mut TestAppContext) -> (Entity, EditTool) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - Project::init_settings(cx); - }); - - let connection = Rc::new(StubAgentConnection::new()); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "file.txt": "hello" - }), - ) - .await; - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid()); - - let thread = cx - .update(|cx| connection.new_thread(project, path!("/test").as_ref(), cx)) - .await - .unwrap(); - - thread_tx.send(thread.downgrade()).unwrap(); - - (thread, EditTool::new(thread_rx)) - } -} diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs deleted file mode 100644 index 6442c784b59a655def92bcae108c27c503daa630..0000000000000000000000000000000000000000 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; - -use crate::claude::edit_tool::EditTool; -use crate::claude::permission_tool::PermissionTool; -use crate::claude::read_tool::ReadTool; -use crate::claude::write_tool::WriteTool; -use acp_thread::AcpThread; -#[cfg(not(test))] -use anyhow::Context as _; -use anyhow::Result; -use collections::HashMap; -use context_server::types::{ - Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities, - ToolsCapabilities, requests, -}; -use gpui::{App, AsyncApp, Task, WeakEntity}; -use project::Fs; -use serde::Serialize; - -pub struct ClaudeZedMcpServer { - server: context_server::listener::McpServer, -} - -pub const SERVER_NAME: &str = "zed"; - -impl ClaudeZedMcpServer { - pub async fn new( - thread_rx: watch::Receiver>, - fs: Arc, - 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(PermissionTool::new(fs.clone(), thread_rx.clone())); - mcp_server.add_tool(ReadTool::new(thread_rx.clone())); - mcp_server.add_tool(EditTool::new(thread_rx.clone())); - mcp_server.add_tool(WriteTool::new(thread_rx.clone())); - - Ok(Self { server: mcp_server }) - } - - pub fn server_config(&self) -> Result { - #[cfg(not(test))] - let zed_path = std::env::current_exe() - .context("finding current executable path for use in mcp_server")?; - - #[cfg(test)] - let zed_path = crate::e2e_tests::get_zed_path(); - - Ok(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, - }) - }) - } -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct McpConfig { - pub mcp_servers: HashMap, -} - -#[derive(Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct McpServerConfig { - pub command: PathBuf, - pub args: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub env: Option>, -} diff --git a/crates/agent_servers/src/claude/permission_tool.rs b/crates/agent_servers/src/claude/permission_tool.rs deleted file mode 100644 index 96a24105e87bd99b46ec16e39dc32df57557f882..0000000000000000000000000000000000000000 --- a/crates/agent_servers/src/claude/permission_tool.rs +++ /dev/null @@ -1,158 +0,0 @@ -use std::sync::Arc; - -use acp_thread::AcpThread; -use agent_client_protocol as acp; -use agent_settings::AgentSettings; -use anyhow::{Context as _, Result}; -use context_server::{ - listener::{McpServerTool, ToolResponse}, - types::ToolResponseContent, -}; -use gpui::{AsyncApp, WeakEntity}; -use project::Fs; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings as _, update_settings_file}; -use util::debug_panic; - -use crate::tools::ClaudeTool; - -#[derive(Clone)] -pub struct PermissionTool { - fs: Arc, - thread_rx: watch::Receiver>, -} - -/// Request permission for tool calls -#[derive(Deserialize, JsonSchema, Debug)] -pub struct PermissionToolParams { - tool_name: String, - input: serde_json::Value, - tool_use_id: Option, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct PermissionToolResponse { - behavior: PermissionToolBehavior, - updated_input: serde_json::Value, -} - -#[derive(Serialize)] -#[serde(rename_all = "snake_case")] -enum PermissionToolBehavior { - Allow, - Deny, -} - -impl PermissionTool { - pub fn new(fs: Arc, thread_rx: watch::Receiver>) -> Self { - Self { fs, thread_rx } - } -} - -impl McpServerTool for PermissionTool { - type Input = PermissionToolParams; - type Output = (); - - const NAME: &'static str = "Confirmation"; - - async fn run( - &self, - input: Self::Input, - cx: &mut AsyncApp, - ) -> Result> { - if agent_settings::AgentSettings::try_read_global(cx, |settings| { - settings.always_allow_tool_actions - }) - .unwrap_or(false) - { - let response = PermissionToolResponse { - behavior: PermissionToolBehavior::Allow, - updated_input: input.input, - }; - - return Ok(ToolResponse { - content: vec![ToolResponseContent::Text { - text: serde_json::to_string(&response)?, - }], - structured_content: (), - }); - } - - let mut thread_rx = self.thread_rx.clone(); - let Some(thread) = thread_rx.recv().await?.upgrade() else { - anyhow::bail!("Thread closed"); - }; - - let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone()); - let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into()); - - const ALWAYS_ALLOW: &str = "always_allow"; - const ALLOW: &str = "allow"; - const REJECT: &str = "reject"; - - let chosen_option = thread - .update(cx, |thread, cx| { - thread.request_tool_call_authorization( - claude_tool.as_acp(tool_call_id).into(), - vec![ - acp::PermissionOption { - id: acp::PermissionOptionId(ALWAYS_ALLOW.into()), - name: "Always Allow".into(), - kind: acp::PermissionOptionKind::AllowAlways, - }, - acp::PermissionOption { - id: acp::PermissionOptionId(ALLOW.into()), - name: "Allow".into(), - kind: acp::PermissionOptionKind::AllowOnce, - }, - acp::PermissionOption { - id: acp::PermissionOptionId(REJECT.into()), - name: "Reject".into(), - kind: acp::PermissionOptionKind::RejectOnce, - }, - ], - cx, - ) - })?? - .await?; - - let response = match chosen_option.0.as_ref() { - ALWAYS_ALLOW => { - cx.update(|cx| { - update_settings_file::(self.fs.clone(), cx, |settings, _| { - settings.set_always_allow_tool_actions(true); - }); - })?; - - PermissionToolResponse { - behavior: PermissionToolBehavior::Allow, - updated_input: input.input, - } - } - ALLOW => PermissionToolResponse { - behavior: PermissionToolBehavior::Allow, - updated_input: input.input, - }, - REJECT => PermissionToolResponse { - behavior: PermissionToolBehavior::Deny, - updated_input: input.input, - }, - opt => { - debug_panic!("Unexpected option: {}", opt); - PermissionToolResponse { - behavior: PermissionToolBehavior::Deny, - updated_input: input.input, - } - } - }; - - Ok(ToolResponse { - content: vec![ToolResponseContent::Text { - text: serde_json::to_string(&response)?, - }], - structured_content: (), - }) - } -} diff --git a/crates/agent_servers/src/claude/read_tool.rs b/crates/agent_servers/src/claude/read_tool.rs deleted file mode 100644 index cbe25876b3deabc32d1d30f7d8ad4de90fb21494..0000000000000000000000000000000000000000 --- a/crates/agent_servers/src/claude/read_tool.rs +++ /dev/null @@ -1,59 +0,0 @@ -use acp_thread::AcpThread; -use anyhow::Result; -use context_server::{ - listener::{McpServerTool, ToolResponse}, - types::{ToolAnnotations, ToolResponseContent}, -}; -use gpui::{AsyncApp, WeakEntity}; - -use crate::tools::ReadToolParams; - -#[derive(Clone)] -pub struct ReadTool { - thread_rx: watch::Receiver>, -} - -impl ReadTool { - pub fn new(thread_rx: watch::Receiver>) -> Self { - Self { thread_rx } - } -} - -impl McpServerTool for ReadTool { - type Input = ReadToolParams; - type Output = (); - - const NAME: &'static str = "Read"; - - fn annotations(&self) -> ToolAnnotations { - ToolAnnotations { - title: Some("Read file".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - open_world_hint: Some(false), - idempotent_hint: None, - } - } - - 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.abs_path, input.offset, input.limit, false, cx) - })? - .await?; - - Ok(ToolResponse { - content: vec![ToolResponseContent::Text { text: content }], - structured_content: (), - }) - } -} diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs deleted file mode 100644 index 323190300131c87a443b95e4bb18424cf034e667..0000000000000000000000000000000000000000 --- a/crates/agent_servers/src/claude/tools.rs +++ /dev/null @@ -1,688 +0,0 @@ -use std::path::PathBuf; - -use agent_client_protocol as acp; -use itertools::Itertools; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use util::ResultExt; - -pub enum ClaudeTool { - Task(Option), - NotebookRead(Option), - NotebookEdit(Option), - Edit(Option), - MultiEdit(Option), - ReadFile(Option), - Write(Option), - Ls(Option), - Glob(Option), - Grep(Option), - Terminal(Option), - WebFetch(Option), - WebSearch(Option), - TodoWrite(Option), - ExitPlanMode(Option), - Other { - name: String, - input: serde_json::Value, - }, -} - -impl ClaudeTool { - pub fn infer(tool_name: &str, input: serde_json::Value) -> Self { - match tool_name { - // Known tools - "mcp__zed__Read" => Self::ReadFile(serde_json::from_value(input).log_err()), - "mcp__zed__Edit" => Self::Edit(serde_json::from_value(input).log_err()), - "mcp__zed__Write" => Self::Write(serde_json::from_value(input).log_err()), - "MultiEdit" => Self::MultiEdit(serde_json::from_value(input).log_err()), - "Write" => Self::Write(serde_json::from_value(input).log_err()), - "LS" => Self::Ls(serde_json::from_value(input).log_err()), - "Glob" => Self::Glob(serde_json::from_value(input).log_err()), - "Grep" => Self::Grep(serde_json::from_value(input).log_err()), - "Bash" => Self::Terminal(serde_json::from_value(input).log_err()), - "WebFetch" => Self::WebFetch(serde_json::from_value(input).log_err()), - "WebSearch" => Self::WebSearch(serde_json::from_value(input).log_err()), - "TodoWrite" => Self::TodoWrite(serde_json::from_value(input).log_err()), - "exit_plan_mode" => Self::ExitPlanMode(serde_json::from_value(input).log_err()), - "Task" => Self::Task(serde_json::from_value(input).log_err()), - "NotebookRead" => Self::NotebookRead(serde_json::from_value(input).log_err()), - "NotebookEdit" => Self::NotebookEdit(serde_json::from_value(input).log_err()), - // Inferred from name - _ => { - let tool_name = tool_name.to_lowercase(); - - if tool_name.contains("edit") || tool_name.contains("write") { - Self::Edit(None) - } else if tool_name.contains("terminal") { - Self::Terminal(None) - } else { - Self::Other { - name: tool_name, - input, - } - } - } - } - } - - pub fn label(&self) -> String { - match &self { - Self::Task(Some(params)) => params.description.clone(), - Self::Task(None) => "Task".into(), - Self::NotebookRead(Some(params)) => { - format!("Read Notebook {}", params.notebook_path.display()) - } - Self::NotebookRead(None) => "Read Notebook".into(), - Self::NotebookEdit(Some(params)) => { - format!("Edit Notebook {}", params.notebook_path.display()) - } - Self::NotebookEdit(None) => "Edit Notebook".into(), - Self::Terminal(Some(params)) => format!("`{}`", params.command), - Self::Terminal(None) => "Terminal".into(), - Self::ReadFile(_) => "Read File".into(), - Self::Ls(Some(params)) => { - format!("List Directory {}", params.path.display()) - } - Self::Ls(None) => "List Directory".into(), - Self::Edit(Some(params)) => { - format!("Edit {}", params.abs_path.display()) - } - Self::Edit(None) => "Edit".into(), - Self::MultiEdit(Some(params)) => { - format!("Multi Edit {}", params.file_path.display()) - } - Self::MultiEdit(None) => "Multi Edit".into(), - Self::Write(Some(params)) => { - format!("Write {}", params.abs_path.display()) - } - Self::Write(None) => "Write".into(), - Self::Glob(Some(params)) => { - format!("Glob `{params}`") - } - Self::Glob(None) => "Glob".into(), - Self::Grep(Some(params)) => format!("`{params}`"), - Self::Grep(None) => "Grep".into(), - Self::WebFetch(Some(params)) => format!("Fetch {}", params.url), - Self::WebFetch(None) => "Fetch".into(), - Self::WebSearch(Some(params)) => format!("Web Search: {}", params), - Self::WebSearch(None) => "Web Search".into(), - Self::TodoWrite(Some(params)) => format!( - "Update TODOs: {}", - params.todos.iter().map(|todo| &todo.content).join(", ") - ), - Self::TodoWrite(None) => "Update TODOs".into(), - Self::ExitPlanMode(_) => "Exit Plan Mode".into(), - Self::Other { name, .. } => name.clone(), - } - } - pub fn content(&self) -> Vec { - match &self { - Self::Other { input, .. } => vec![ - format!( - "```json\n{}```", - serde_json::to_string_pretty(&input).unwrap_or("{}".to_string()) - ) - .into(), - ], - Self::Task(Some(params)) => vec![params.prompt.clone().into()], - Self::NotebookRead(Some(params)) => { - vec![params.notebook_path.display().to_string().into()] - } - Self::NotebookEdit(Some(params)) => vec![params.new_source.clone().into()], - Self::Terminal(Some(params)) => vec![ - format!( - "`{}`\n\n{}", - params.command, - params.description.as_deref().unwrap_or_default() - ) - .into(), - ], - Self::ReadFile(Some(params)) => vec![params.abs_path.display().to_string().into()], - Self::Ls(Some(params)) => vec![params.path.display().to_string().into()], - Self::Glob(Some(params)) => vec![params.to_string().into()], - Self::Grep(Some(params)) => vec![format!("`{params}`").into()], - Self::WebFetch(Some(params)) => vec![params.prompt.clone().into()], - Self::WebSearch(Some(params)) => vec![params.to_string().into()], - Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()], - Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff { - diff: acp::Diff { - path: params.abs_path.clone(), - old_text: Some(params.old_text.clone()), - new_text: params.new_text.clone(), - }, - }], - Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff { - diff: acp::Diff { - path: params.abs_path.clone(), - old_text: None, - new_text: params.content.clone(), - }, - }], - Self::MultiEdit(Some(params)) => { - // todo: show multiple edits in a multibuffer? - params - .edits - .first() - .map(|edit| { - vec![acp::ToolCallContent::Diff { - diff: acp::Diff { - path: params.file_path.clone(), - old_text: Some(edit.old_string.clone()), - new_text: edit.new_string.clone(), - }, - }] - }) - .unwrap_or_default() - } - Self::TodoWrite(Some(_)) => { - // These are mapped to plan updates later - vec![] - } - Self::Task(None) - | Self::NotebookRead(None) - | Self::NotebookEdit(None) - | Self::Terminal(None) - | Self::ReadFile(None) - | Self::Ls(None) - | Self::Glob(None) - | Self::Grep(None) - | Self::WebFetch(None) - | Self::WebSearch(None) - | Self::TodoWrite(None) - | Self::ExitPlanMode(None) - | Self::Edit(None) - | Self::Write(None) - | Self::MultiEdit(None) => vec![], - } - } - - pub fn kind(&self) -> acp::ToolKind { - match self { - Self::Task(_) => acp::ToolKind::Think, - Self::NotebookRead(_) => acp::ToolKind::Read, - Self::NotebookEdit(_) => acp::ToolKind::Edit, - Self::Edit(_) => acp::ToolKind::Edit, - Self::MultiEdit(_) => acp::ToolKind::Edit, - Self::Write(_) => acp::ToolKind::Edit, - Self::ReadFile(_) => acp::ToolKind::Read, - Self::Ls(_) => acp::ToolKind::Search, - Self::Glob(_) => acp::ToolKind::Search, - Self::Grep(_) => acp::ToolKind::Search, - Self::Terminal(_) => acp::ToolKind::Execute, - Self::WebSearch(_) => acp::ToolKind::Search, - Self::WebFetch(_) => acp::ToolKind::Fetch, - Self::TodoWrite(_) => acp::ToolKind::Think, - Self::ExitPlanMode(_) => acp::ToolKind::Think, - Self::Other { .. } => acp::ToolKind::Other, - } - } - - pub fn locations(&self) -> Vec { - match &self { - Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![acp::ToolCallLocation { - path: abs_path.clone(), - line: None, - }], - Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => { - vec![acp::ToolCallLocation { - path: file_path.clone(), - line: None, - }] - } - Self::Write(Some(WriteToolParams { - abs_path: file_path, - .. - })) => { - vec![acp::ToolCallLocation { - path: file_path.clone(), - line: None, - }] - } - Self::ReadFile(Some(ReadToolParams { - abs_path, offset, .. - })) => vec![acp::ToolCallLocation { - path: abs_path.clone(), - line: *offset, - }], - Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => { - vec![acp::ToolCallLocation { - path: notebook_path.clone(), - line: None, - }] - } - Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => { - vec![acp::ToolCallLocation { - path: notebook_path.clone(), - line: None, - }] - } - Self::Glob(Some(GlobToolParams { - path: Some(path), .. - })) => vec![acp::ToolCallLocation { - path: path.clone(), - line: None, - }], - Self::Ls(Some(LsToolParams { path, .. })) => vec![acp::ToolCallLocation { - path: path.clone(), - line: None, - }], - Self::Grep(Some(GrepToolParams { - path: Some(path), .. - })) => vec![acp::ToolCallLocation { - path: PathBuf::from(path), - line: None, - }], - Self::Task(_) - | Self::NotebookRead(None) - | Self::NotebookEdit(None) - | Self::Edit(None) - | Self::MultiEdit(None) - | Self::Write(None) - | Self::ReadFile(None) - | Self::Ls(None) - | Self::Glob(_) - | Self::Grep(_) - | Self::Terminal(_) - | Self::WebFetch(_) - | Self::WebSearch(_) - | Self::TodoWrite(_) - | Self::ExitPlanMode(_) - | Self::Other { .. } => vec![], - } - } - - pub fn as_acp(&self, id: acp::ToolCallId) -> acp::ToolCall { - acp::ToolCall { - id, - kind: self.kind(), - status: acp::ToolCallStatus::InProgress, - title: self.label(), - content: self.content(), - locations: self.locations(), - raw_input: None, - raw_output: None, - } - } -} - -/// Edit a file. -/// -/// In sessions with mcp__zed__Edit always use it instead of Edit as it will -/// allow the user to conveniently review changes. -/// -/// File editing instructions: -/// - The `old_text` param must match existing file content, including indentation. -/// - The `old_text` param must come from the actual file, not an outline. -/// - The `old_text` section must not be empty. -/// - Be minimal with replacements: -/// - For unique lines, include only those lines. -/// - For non-unique lines, include enough context to identify them. -/// - Do not escape quotes, newlines, or other characters. -/// - Only edit the specified file. -#[derive(Deserialize, JsonSchema, Debug)] -pub struct EditToolParams { - /// The absolute path to the file to read. - pub abs_path: PathBuf, - /// The old text to replace (must be unique in the file) - pub old_text: String, - /// The new text. - pub new_text: String, -} - -/// Reads the content of the given file in the project. -/// -/// Never attempt to read a path that hasn't been previously mentioned. -/// -/// In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents. -#[derive(Deserialize, JsonSchema, Debug)] -pub struct ReadToolParams { - /// The absolute path to the file to read. - pub abs_path: PathBuf, - /// Which line to start reading from. Omit to start from the beginning. - #[serde(skip_serializing_if = "Option::is_none")] - pub offset: Option, - /// How many lines to read. Omit for the whole file. - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, -} - -/// Writes content to the specified file in the project. -/// -/// In sessions with mcp__zed__Write always use it instead of Write as it will -/// allow the user to conveniently review changes. -#[derive(Deserialize, JsonSchema, Debug)] -pub struct WriteToolParams { - /// The absolute path of the file to write. - pub abs_path: PathBuf, - /// The full content to write. - pub content: String, -} - -#[derive(Deserialize, JsonSchema, Debug)] -pub struct BashToolParams { - /// Shell command to execute - pub command: String, - /// 5-10 word description of what command does - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - /// Timeout in ms (max 600000ms/10min, default 120000ms) - #[serde(skip_serializing_if = "Option::is_none")] - pub timeout: Option, -} - -#[derive(Deserialize, JsonSchema, Debug)] -pub struct GlobToolParams { - /// Glob pattern like **/*.js or src/**/*.ts - pub pattern: String, - /// Directory to search in (omit for current directory) - #[serde(skip_serializing_if = "Option::is_none")] - pub path: Option, -} - -impl std::fmt::Display for GlobToolParams { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(path) = &self.path { - write!(f, "{}", path.display())?; - } - write!(f, "{}", self.pattern) - } -} - -#[derive(Deserialize, JsonSchema, Debug)] -pub struct LsToolParams { - /// Absolute path to directory - pub path: PathBuf, - /// Array of glob patterns to ignore - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub ignore: Vec, -} - -#[derive(Deserialize, JsonSchema, Debug)] -pub struct GrepToolParams { - /// Regex pattern to search for - pub pattern: String, - /// File/directory to search (defaults to current directory) - #[serde(skip_serializing_if = "Option::is_none")] - pub path: Option, - /// "content" (shows lines), "files_with_matches" (default), "count" - #[serde(skip_serializing_if = "Option::is_none")] - pub output_mode: Option, - /// Filter files with glob pattern like "*.js" - #[serde(skip_serializing_if = "Option::is_none")] - pub glob: Option, - /// File type filter like "js", "py", "rust" - #[serde(rename = "type", skip_serializing_if = "Option::is_none")] - pub file_type: Option, - /// Case insensitive search - #[serde(rename = "-i", default, skip_serializing_if = "is_false")] - pub case_insensitive: bool, - /// Show line numbers (content mode only) - #[serde(rename = "-n", default, skip_serializing_if = "is_false")] - pub line_numbers: bool, - /// Lines after match (content mode only) - #[serde(rename = "-A", skip_serializing_if = "Option::is_none")] - pub after_context: Option, - /// Lines before match (content mode only) - #[serde(rename = "-B", skip_serializing_if = "Option::is_none")] - pub before_context: Option, - /// Lines before and after match (content mode only) - #[serde(rename = "-C", skip_serializing_if = "Option::is_none")] - pub context: Option, - /// Enable multiline/cross-line matching - #[serde(default, skip_serializing_if = "is_false")] - pub multiline: bool, - /// Limit output to first N results - #[serde(skip_serializing_if = "Option::is_none")] - pub head_limit: Option, -} - -impl std::fmt::Display for GrepToolParams { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "grep")?; - - // Boolean flags - if self.case_insensitive { - write!(f, " -i")?; - } - if self.line_numbers { - write!(f, " -n")?; - } - - // Context options - if let Some(after) = self.after_context { - write!(f, " -A {}", after)?; - } - if let Some(before) = self.before_context { - write!(f, " -B {}", before)?; - } - if let Some(context) = self.context { - write!(f, " -C {}", context)?; - } - - // Output mode - if let Some(mode) = &self.output_mode { - match mode { - GrepOutputMode::FilesWithMatches => write!(f, " -l")?, - GrepOutputMode::Count => write!(f, " -c")?, - GrepOutputMode::Content => {} // Default mode - } - } - - // Head limit - if let Some(limit) = self.head_limit { - write!(f, " | head -{}", limit)?; - } - - // Glob pattern - if let Some(glob) = &self.glob { - write!(f, " --include=\"{}\"", glob)?; - } - - // File type - if let Some(file_type) = &self.file_type { - write!(f, " --type={}", file_type)?; - } - - // Multiline - if self.multiline { - write!(f, " -P")?; // Perl-compatible regex for multiline - } - - // Pattern (escaped if contains special characters) - write!(f, " \"{}\"", self.pattern)?; - - // Path - if let Some(path) = &self.path { - write!(f, " {}", path)?; - } - - Ok(()) - } -} - -#[derive(Default, Deserialize, Serialize, JsonSchema, strum::Display, Debug)] -#[serde(rename_all = "snake_case")] -pub enum TodoPriority { - High, - #[default] - Medium, - Low, -} - -impl Into for TodoPriority { - fn into(self) -> acp::PlanEntryPriority { - match self { - TodoPriority::High => acp::PlanEntryPriority::High, - TodoPriority::Medium => acp::PlanEntryPriority::Medium, - TodoPriority::Low => acp::PlanEntryPriority::Low, - } - } -} - -#[derive(Deserialize, Serialize, JsonSchema, Debug)] -#[serde(rename_all = "snake_case")] -pub enum TodoStatus { - Pending, - InProgress, - Completed, -} - -impl Into for TodoStatus { - fn into(self) -> acp::PlanEntryStatus { - match self { - TodoStatus::Pending => acp::PlanEntryStatus::Pending, - TodoStatus::InProgress => acp::PlanEntryStatus::InProgress, - TodoStatus::Completed => acp::PlanEntryStatus::Completed, - } - } -} - -#[derive(Deserialize, Serialize, JsonSchema, Debug)] -pub struct Todo { - /// Task description - pub content: String, - /// Current status of the todo - pub status: TodoStatus, - /// Priority level of the todo - #[serde(default)] - pub priority: TodoPriority, -} - -impl Into for Todo { - fn into(self) -> acp::PlanEntry { - acp::PlanEntry { - content: self.content, - priority: self.priority.into(), - status: self.status.into(), - } - } -} - -#[derive(Deserialize, JsonSchema, Debug)] -pub struct TodoWriteToolParams { - pub todos: Vec, -} - -#[derive(Deserialize, JsonSchema, Debug)] -pub struct ExitPlanModeToolParams { - /// Implementation plan in markdown format - pub plan: String, -} - -#[derive(Deserialize, JsonSchema, Debug)] -pub struct TaskToolParams { - /// Short 3-5 word description of task - pub description: String, - /// Detailed task for agent to perform - pub prompt: String, -} - -#[derive(Deserialize, JsonSchema, Debug)] -pub struct NotebookReadToolParams { - /// Absolute path to .ipynb file - pub notebook_path: PathBuf, - /// Specific cell ID to read - #[serde(skip_serializing_if = "Option::is_none")] - pub cell_id: Option, -} - -#[derive(Deserialize, Serialize, JsonSchema, Debug)] -#[serde(rename_all = "snake_case")] -pub enum CellType { - Code, - Markdown, -} - -#[derive(Deserialize, Serialize, JsonSchema, Debug)] -#[serde(rename_all = "snake_case")] -pub enum EditMode { - Replace, - Insert, - Delete, -} - -#[derive(Deserialize, JsonSchema, Debug)] -pub struct NotebookEditToolParams { - /// Absolute path to .ipynb file - pub notebook_path: PathBuf, - /// New cell content - pub new_source: String, - /// Cell ID to edit - #[serde(skip_serializing_if = "Option::is_none")] - pub cell_id: Option, - /// Type of cell (code or markdown) - #[serde(skip_serializing_if = "Option::is_none")] - pub cell_type: Option, - /// Edit operation mode - #[serde(skip_serializing_if = "Option::is_none")] - pub edit_mode: Option, -} - -#[derive(Deserialize, Serialize, JsonSchema, Debug)] -pub struct MultiEditItem { - /// The text to search for and replace - pub old_string: String, - /// The replacement text - pub new_string: String, - /// Whether to replace all occurrences or just the first - #[serde(default, skip_serializing_if = "is_false")] - pub replace_all: bool, -} - -#[derive(Deserialize, JsonSchema, Debug)] -pub struct MultiEditToolParams { - /// Absolute path to file - pub file_path: PathBuf, - /// List of edits to apply - pub edits: Vec, -} - -fn is_false(v: &bool) -> bool { - !*v -} - -#[derive(Deserialize, JsonSchema, Debug)] -#[serde(rename_all = "snake_case")] -pub enum GrepOutputMode { - Content, - FilesWithMatches, - Count, -} - -#[derive(Deserialize, JsonSchema, Debug)] -pub struct WebFetchToolParams { - /// Valid URL to fetch - #[serde(rename = "url")] - pub url: String, - /// What to extract from content - pub prompt: String, -} - -#[derive(Deserialize, JsonSchema, Debug)] -pub struct WebSearchToolParams { - /// Search query (min 2 chars) - pub query: String, - /// Only include these domains - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub allowed_domains: Vec, - /// Exclude these domains - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub blocked_domains: Vec, -} - -impl std::fmt::Display for WebSearchToolParams { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "\"{}\"", self.query)?; - - if !self.allowed_domains.is_empty() { - write!(f, " (allowed: {})", self.allowed_domains.join(", "))?; - } - - if !self.blocked_domains.is_empty() { - write!(f, " (blocked: {})", self.blocked_domains.join(", "))?; - } - - Ok(()) - } -} diff --git a/crates/agent_servers/src/claude/write_tool.rs b/crates/agent_servers/src/claude/write_tool.rs deleted file mode 100644 index 39479a9c38ba616b3d0f2e4197c112bcbea68261..0000000000000000000000000000000000000000 --- a/crates/agent_servers/src/claude/write_tool.rs +++ /dev/null @@ -1,59 +0,0 @@ -use acp_thread::AcpThread; -use anyhow::Result; -use context_server::{ - listener::{McpServerTool, ToolResponse}, - types::ToolAnnotations, -}; -use gpui::{AsyncApp, WeakEntity}; - -use crate::tools::WriteToolParams; - -#[derive(Clone)] -pub struct WriteTool { - thread_rx: watch::Receiver>, -} - -impl WriteTool { - pub fn new(thread_rx: watch::Receiver>) -> Self { - Self { thread_rx } - } -} - -impl McpServerTool for WriteTool { - type Input = WriteToolParams; - type Output = (); - - const NAME: &'static str = "Write"; - - fn annotations(&self) -> ToolAnnotations { - ToolAnnotations { - title: Some("Write file".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(false), - open_world_hint: Some(false), - idempotent_hint: Some(false), - } - } - - 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.abs_path, input.content, cx) - })? - .await?; - - Ok(ToolResponse { - content: vec![], - structured_content: (), - }) - } -} diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index a481a850ff70018ff2e6b72446ca24e78732137a..8d9670473a619eea9dc6730d04a4c807937aa393 100644 --- a/crates/agent_servers/src/custom.rs +++ b/crates/agent_servers/src/custom.rs @@ -2,7 +2,6 @@ use crate::{AgentServerCommand, AgentServerDelegate}; use acp_thread::AgentConnection; use anyhow::Result; use gpui::{App, SharedString, Task}; -use language_models::provider::anthropic::AnthropicLanguageModelProvider; use std::{path::Path, rc::Rc}; use ui::IconName; @@ -38,24 +37,9 @@ impl crate::AgentServer for CustomAgentServer { cx: &mut App, ) -> Task>> { let server_name = self.name(); - let mut command = self.command.clone(); + let command = self.command.clone(); let root_dir = root_dir.to_path_buf(); - - // TODO: Remove this once we have Claude properly - cx.spawn(async move |mut cx| { - if let Some(api_key) = cx - .update(AnthropicLanguageModelProvider::api_key)? - .await - .ok() - { - command - .env - .get_or_insert_default() - .insert("ANTHROPIC_API_KEY".to_owned(), api_key.key); - } - - crate::acp::connect(server_name, command, &root_dir, &mut cx).await - }) + cx.spawn(async move |cx| crate::acp::connect(server_name, command, &root_dir, cx).await) } fn into_any(self: Rc) -> Rc { diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index d310870c23c957900e3cdcdb8a88084f26520208..5d2becf0ccc4b30cfeca27f4eb5ee08c2d0bb7d1 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -1,4 +1,6 @@ use crate::{AgentServer, AgentServerDelegate}; +#[cfg(test)] +use crate::{AgentServerCommand, CustomAgentServerSettings}; use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus}; use agent_client_protocol as acp; use futures::{FutureExt, StreamExt, channel::mpsc, select}; @@ -471,7 +473,13 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc { #[cfg(test)] crate::AllAgentServersSettings::override_global( crate::AllAgentServersSettings { - claude: Some(crate::claude::tests::local_command().into()), + claude: Some(CustomAgentServerSettings { + command: AgentServerCommand { + path: "claude-code-acp".into(), + args: vec![], + env: None, + }, + }), gemini: Some(crate::gemini::tests::local_command().into()), custom: collections::HashMap::default(), }, diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 84dc6750b1a8e74b509e467d811ec790cdc0dea9..5e958f686959d78e6ceaf8b8ea7d8404ffba166a 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -5,7 +5,7 @@ use crate::acp::AcpConnection; use crate::{AgentServer, AgentServerDelegate}; use acp_thread::{AgentConnection, LoadError}; use anyhow::Result; -use gpui::{App, SharedString, Task}; +use gpui::{App, AppContext as _, SharedString, Task}; use language_models::provider::google::GoogleLanguageModelProvider; use settings::SettingsStore; @@ -37,23 +37,32 @@ impl AgentServer for Gemini { ) -> Task>> { let root_dir = root_dir.to_path_buf(); let server_name = self.name(); - cx.spawn(async move |cx| { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).gemini.clone() - })?; + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).gemini.clone() + }); - let mut command = cx - .update(|cx| { + cx.spawn(async move |cx| { + let ignore_system_version = settings + .as_ref() + .and_then(|settings| settings.ignore_system_version) + .unwrap_or(true); + let mut command = if let Some(settings) = settings + && let Some(command) = settings.custom_command() + { + command + } else { + cx.update(|cx| { delegate.get_or_npm_install_builtin_agent( Self::BINARY_NAME.into(), Self::PACKAGE_NAME.into(), format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(), - settings, - Some("0.2.1".parse().unwrap()), + ignore_system_version, + Some(Self::MINIMUM_VERSION.parse().unwrap()), cx, ) })? - .await?; + .await? + }; command.args.push("--experimental-acp".into()); if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() { diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 59f3b4b54089a5598bc77d0ba127f2c54e9ec986..81f80a7d7d9581b8c1862ae3393c4a5d5e6706b6 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -15,7 +15,7 @@ pub fn init(cx: &mut App) { #[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)] pub struct AllAgentServersSettings { pub gemini: Option, - pub claude: Option, + pub claude: Option, /// Custom agent servers configured by the user #[serde(flatten)] From 8c18f059f195d099dfdf3fea70eac33703e6c9dd Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 28 Aug 2025 17:42:12 -0400 Subject: [PATCH 425/744] Always enable acp accept/reject buttons for now (#37121) We have a bug in our ACP implementation where sometimes the Accept/Reject buttons are disabled (and stay disabled even after the thread has finished). I haven't found a complete fix for this yet, so in the meantime I'm putting out the fire by making it so those buttons are always enabled. That way you're never blocked, and the only consequence of the bug is that sometimes they should be disabled but are enabled instead. Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 8069812729265c20c7758487cef87479b01dea02..c718540c217425c8987f4282d5990579d529779e 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -3071,7 +3071,12 @@ impl AcpThreadView { let active_color = cx.theme().colors().element_selected; let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3)); - let pending_edits = thread.has_pending_edit_tool_calls(); + // Temporarily always enable ACP edit controls. This is temporary, to lessen the + // impact of a nasty bug that causes them to sometimes be disabled when they shouldn't + // be, which blocks you from being able to accept or reject edits. This switches the + // bug to be that sometimes it's enabled when it shouldn't be, which at least doesn't + // block you from using the panel. + let pending_edits = false; v_flex() .mt_1() From 52d119b637e2c4d3d4849cd692d94ea855e97686 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 28 Aug 2025 18:45:09 -0400 Subject: [PATCH 426/744] docs: Add Expert to Elixir docs (#37127) This PR adds documentation for [Expert](https://github.com/elixir-lang/expert) to the Elixir docs. Also updated the examples for the other language servers to be representative of all the supported language servers. Release Notes: - N/A --- docs/src/languages/elixir.md | 43 +++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/docs/src/languages/elixir.md b/docs/src/languages/elixir.md index 175d0d2e8c48631fbb8862558f24c4e555eb8b01..c7b7e2287a0d772871bee331035944a5e7bab8a1 100644 --- a/docs/src/languages/elixir.md +++ b/docs/src/languages/elixir.md @@ -6,35 +6,72 @@ Elixir support is available through the [Elixir extension](https://github.com/ze - [elixir-lang/tree-sitter-elixir](https://github.com/elixir-lang/tree-sitter-elixir) - [phoenixframework/tree-sitter-heex](https://github.com/phoenixframework/tree-sitter-heex) - Language servers: + - [elixir-lang/expert](https://github.com/elixir-lang/expert) - [elixir-lsp/elixir-ls](https://github.com/elixir-lsp/elixir-ls) - [elixir-tools/next-ls](https://github.com/elixir-tools/next-ls) - [lexical-lsp/lexical](https://github.com/lexical-lsp/lexical) ## Choosing a language server -The Elixir extension offers language server support for `elixir-ls`, `next-ls`, and `lexical`. +The Elixir extension offers language server support for `expert`, `elixir-ls`, `next-ls`, and `lexical`. `elixir-ls` is enabled by default. +### Expert + +To switch to `expert`, add the following to your `settings.json`: + +```json +{ + "languages": { + "Elixir": { + "language_servers": [ + "expert", + "!elixir-ls", + "!next-ls", + "!lexical", + "..." + ] + } + } +} +``` + +### Next LS + To switch to `next-ls`, add the following to your `settings.json`: ```json { "languages": { "Elixir": { - "language_servers": ["next-ls", "!elixir-ls", "..."] + "language_servers": [ + "next-ls", + "!expert", + "!elixir-ls", + "!lexical", + "..." + ] } } } ``` +### Lexical + To switch to `lexical`, add the following to your `settings.json`: ```json { "languages": { "Elixir": { - "language_servers": ["lexical", "!elixir-ls", "..."] + "language_servers": [ + "lexical", + "!expert", + "!elixir-ls", + "!next-ls", + "..." + ] } } } From 960d9ce48c854cee70d118486a76af7bb13115f1 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 28 Aug 2025 18:50:27 -0400 Subject: [PATCH 427/744] Disable Expert language server by default for Elixir (#37126) This PR updates the language server configuration for Elixir and HEEx to not start the [Expert](https://github.com/elixir-lang/expert) language server by default. While Expert is the official Elixir language server, it is still early, so we don't want to make it the default just yet. Release Notes: - Updated the default Elixir and HEEx language server settings to not start the Expert language server. --- assets/settings/default.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index ef57412842c302571be6827119e4abe6505a43b3..57a5d13eab281d6bffec2f299fbb1e2d5a3a01c5 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1585,7 +1585,7 @@ "ensure_final_newline_on_save": false }, "Elixir": { - "language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."] + "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."] }, "Elm": { "tab_size": 4 @@ -1610,7 +1610,7 @@ } }, "HEEX": { - "language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."] + "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."] }, "HTML": { "prettier": { From cfd56a744d594e2cb239da47cebc498ca002d659 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Thu, 28 Aug 2025 18:22:56 -0600 Subject: [PATCH 428/744] zeta: Show update required notification on appropriate window(s) (#37130) To show these notifications, Zeta was being initialized with the initial workspace it's used on - which may not even still exist! This removes a confusing/misleading workspace field from Zeta. Release Notes: - N/A --- .../zed/src/zed/edit_prediction_registry.rs | 9 +-- crates/zeta/src/zeta.rs | 60 +++++++------------ 2 files changed, 22 insertions(+), 47 deletions(-) diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index bc2d757fd1900ef0c3ce015a33476fd2c9df9eec..7b8b98018e6d6c608574ab81e912e8a98e363046 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -8,7 +8,6 @@ use settings::SettingsStore; use std::{cell::RefCell, rc::Rc, sync::Arc}; use supermaven::{Supermaven, SupermavenCompletionProvider}; use ui::Window; -use workspace::Workspace; use zeta::{ProviderDataCollection, ZetaEditPredictionProvider}; pub fn init(client: Arc, user_store: Entity, cx: &mut App) { @@ -204,13 +203,7 @@ fn assign_edit_prediction_provider( } } - let workspace = window - .root::() - .flatten() - .map(|workspace| workspace.downgrade()); - - let zeta = - zeta::Zeta::register(workspace, worktree, client.clone(), user_store, cx); + let zeta = zeta::Zeta::register(worktree, client.clone(), user_store, cx); if let Some(buffer) = &singleton_buffer && buffer.read(cx).file().is_some() diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 7b14d1279604bc8915e552a879cb6406f4a3948c..e0cfd23dd26cd7ea49181b5aabc16f00f4fd826a 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -24,7 +24,7 @@ use collections::{HashMap, HashSet, VecDeque}; use futures::AsyncReadExt; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EntityId, Global, SemanticVersion, - Subscription, Task, WeakEntity, actions, + SharedString, Subscription, Task, actions, }; use http_client::{AsyncBody, HttpClient, Method, Request, Response}; use input_excerpt::excerpt_for_cursor_position; @@ -51,8 +51,7 @@ use telemetry_events::EditPredictionRating; use thiserror::Error; use util::ResultExt; use uuid::Uuid; -use workspace::Workspace; -use workspace::notifications::{ErrorMessagePrompt, NotificationId}; +use workspace::notifications::{ErrorMessagePrompt, NotificationId, show_app_notification}; use worktree::Worktree; const CURSOR_MARKER: &str = "<|user_cursor_is_here|>"; @@ -212,7 +211,6 @@ impl std::fmt::Debug for EditPrediction { } pub struct Zeta { - workspace: Option>, client: Arc, events: VecDeque, registered_buffers: HashMap, @@ -233,14 +231,13 @@ impl Zeta { } pub fn register( - workspace: Option>, worktree: Option>, client: Arc, user_store: Entity, cx: &mut App, ) -> Entity { let this = Self::global(cx).unwrap_or_else(|| { - let entity = cx.new(|cx| Self::new(workspace, client, user_store, cx)); + let entity = cx.new(|cx| Self::new(client, user_store, cx)); cx.set_global(ZetaGlobal(entity.clone())); entity }); @@ -265,19 +262,13 @@ impl Zeta { self.user_store.read(cx).edit_prediction_usage() } - fn new( - workspace: Option>, - client: Arc, - user_store: Entity, - cx: &mut Context, - ) -> Self { + fn new(client: Arc, user_store: Entity, cx: &mut Context) -> Self { let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx); let data_collection_choice = Self::load_data_collection_choices(); let data_collection_choice = cx.new(|_| data_collection_choice); Self { - workspace, client, events: VecDeque::new(), shown_completions: VecDeque::new(), @@ -370,7 +361,6 @@ impl Zeta { fn request_completion_impl( &mut self, - workspace: Option>, project: Option<&Entity>, buffer: &Entity, cursor: language::Anchor, @@ -453,23 +443,20 @@ impl Zeta { zeta.update_required = true; }); - if let Some(workspace) = workspace { - workspace.update(cx, |workspace, cx| { - workspace.show_notification( - NotificationId::unique::(), - cx, - |cx| { - cx.new(|cx| { - ErrorMessagePrompt::new(err.to_string(), cx) - .with_link_button( - "Update Zed", - "https://zed.dev/releases", - ) - }) - }, - ); - }); - } + let error_message: SharedString = err.to_string().into(); + show_app_notification( + NotificationId::unique::(), + cx, + move |cx| { + cx.new(|cx| { + ErrorMessagePrompt::new(error_message.clone(), cx) + .with_link_button( + "Update Zed", + "https://zed.dev/releases", + ) + }) + }, + ); }) .ok(); } @@ -689,7 +676,7 @@ and then another ) -> Task>> { use std::future::ready; - self.request_completion_impl(None, project, buffer, position, false, cx, |_params| { + self.request_completion_impl(project, buffer, position, false, cx, |_params| { ready(Ok((response, None))) }) } @@ -702,12 +689,7 @@ and then another can_collect_data: bool, cx: &mut Context, ) -> Task>> { - let workspace = self - .workspace - .as_ref() - .and_then(|workspace| workspace.upgrade()); self.request_completion_impl( - workspace, project, buffer, position, @@ -2029,7 +2011,7 @@ mod tests { // Construct the fake server to authenticate. let _server = FakeServer::for_client(42, &client, cx).await; let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let zeta = cx.new(|cx| Zeta::new(None, client, user_store.clone(), cx)); + let zeta = cx.new(|cx| Zeta::new(client, user_store.clone(), cx)); let buffer = cx.new(|cx| Buffer::local(buffer_content, cx)); let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0))); @@ -2093,7 +2075,7 @@ mod tests { // Construct the fake server to authenticate. let _server = FakeServer::for_client(42, &client, cx).await; let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let zeta = cx.new(|cx| Zeta::new(None, client, user_store.clone(), cx)); + let zeta = cx.new(|cx| Zeta::new(client, user_store.clone(), cx)); let buffer = cx.new(|cx| Buffer::local(buffer_content, cx)); let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); From e5cea54cbbdb37c4e047a344d5fd245860ccd529 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 28 Aug 2025 20:09:20 -0600 Subject: [PATCH 429/744] acp: Load agent panel even if serialized config is bogus (#37134) Closes #ISSUE Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 232311c5b02cdaa9edad4c0e9053163f450378e8..3eb171054a2c4d529bbc4b89063bf58f69ce5c45 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -86,7 +86,7 @@ use zed_actions::{ const AGENT_PANEL_KEY: &str = "agent_panel"; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] struct SerializedAgentPanel { width: Option, selected_agent: Option, @@ -592,7 +592,7 @@ impl AgentPanel { .log_err() .flatten() { - Some(serde_json::from_str::(&panel)?) + serde_json::from_str::(&panel).log_err() } else { None }; From c3ccdc0b4421d34d541d592b0184345c2ac08f7e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 28 Aug 2025 20:50:24 -0700 Subject: [PATCH 430/744] Add a setting to control the number of context lines in excerpts (#37138) Fixes https://github.com/zed-industries/zed/discussions/28739 Release Notes: - Added a setting, `excerpt_context_lines`, for setting the number of context lines shown in a multibuffer --- assets/settings/default.json | 2 ++ crates/acp_thread/src/diff.rs | 8 +++---- crates/agent_ui/src/agent_diff.rs | 3 ++- crates/assistant_tools/src/edit_file_tool.rs | 10 +++++---- crates/diagnostics/src/diagnostics.rs | 6 +++-- crates/editor/src/editor.rs | 12 +++++++--- crates/editor/src/editor_settings.rs | 6 +++++ crates/editor/src/editor_tests.rs | 2 +- crates/git_ui/src/commit_view.rs | 4 ++-- crates/git_ui/src/project_diff.rs | 3 ++- crates/search/src/project_search.rs | 3 ++- crates/settings/src/settings_store.rs | 23 ++++++++++++++++++++ docs/src/configuring-zed.md | 10 +++++++++ docs/src/development/releases.md | 2 -- docs/src/visual-customization.md | 4 +++- 15 files changed, 76 insertions(+), 22 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 57a5d13eab281d6bffec2f299fbb1e2d5a3a01c5..297c932e5b54ca75eb34b2399c0a1f427dcc9f77 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -279,6 +279,8 @@ "redact_private_values": false, // The default number of lines to expand excerpts in the multibuffer by. "expand_excerpt_lines": 5, + // The default number of context lines shown in multibuffer excerpts. + "excerpt_context_lines": 2, // Globs to match against file paths to determine if a file is private. "private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"], // Whether to use additional LSP queries to format (and amend) the code after diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index 0fec6809e01ff3f85acc7ad80effe95197200d60..f75af0543e373b47b0c6de36760ba18b5d9da318 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -1,6 +1,6 @@ use anyhow::Result; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; -use editor::{MultiBuffer, PathKey}; +use editor::{MultiBuffer, PathKey, multibuffer_context_lines}; use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task}; use itertools::Itertools; use language::{ @@ -64,7 +64,7 @@ impl Diff { PathKey::for_buffer(&buffer, cx), buffer.clone(), hunk_ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, + multibuffer_context_lines(cx), cx, ); multibuffer.add_diff(diff, cx); @@ -279,7 +279,7 @@ impl PendingDiff { path_key, buffer, ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, + multibuffer_context_lines(cx), cx, ); multibuffer.add_diff(buffer_diff.clone(), cx); @@ -305,7 +305,7 @@ impl PendingDiff { PathKey::for_buffer(&self.new_buffer, cx), self.new_buffer.clone(), ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, + multibuffer_context_lines(cx), cx, ); let end = multibuffer.len(cx); diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 1e1ff95178308e20988019305b0546a169acba8f..4bd525e9d0461a7a180cccc1748e7f8983c0b665 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -10,6 +10,7 @@ use editor::{ Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot, SelectionEffects, ToPoint, actions::{GoToHunk, GoToPreviousHunk}, + multibuffer_context_lines, scroll::Autoscroll, }; use gpui::{ @@ -257,7 +258,7 @@ impl AgentDiffPane { path_key.clone(), buffer.clone(), diff_hunk_ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, + multibuffer_context_lines(cx), cx, ); multibuffer.add_diff(diff_handle, cx); diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 95b01c40eb96472caf85f239b2212f25e06fe9e2..7b208ccc7768c9c0df2904573e2d47504a8eb61f 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -11,7 +11,9 @@ use assistant_tool::{ AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, }; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; -use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey}; +use editor::{ + Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, multibuffer_context_lines, +}; use futures::StreamExt; use gpui::{ Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task, @@ -474,7 +476,7 @@ impl Tool for EditFileTool { PathKey::for_buffer(&buffer, cx), buffer, diff_hunk_ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, + multibuffer_context_lines(cx), cx, ); multibuffer.add_diff(buffer_diff, cx); @@ -703,7 +705,7 @@ impl EditFileToolCard { PathKey::for_buffer(buffer, cx), buffer.clone(), ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, + multibuffer_context_lines(cx), cx, ); let end = multibuffer.len(cx); @@ -791,7 +793,7 @@ impl EditFileToolCard { path_key, buffer, ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, + multibuffer_context_lines(cx), cx, ); multibuffer.add_diff(buffer_diff.clone(), cx); diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 1c27e820a0d8afb64c5c67e66e125caf8720593d..53d03718475da1eeaf2b6b3faa22baabb1695f2d 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -10,8 +10,9 @@ use anyhow::Result; use collections::{BTreeSet, HashMap}; use diagnostic_renderer::DiagnosticBlock; use editor::{ - DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey, + Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey, display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, + multibuffer_context_lines, }; use gpui::{ AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, @@ -493,10 +494,11 @@ impl ProjectDiagnosticsEditor { } let mut excerpt_ranges: Vec> = Vec::new(); + let context_lines = cx.update(|_, cx| multibuffer_context_lines(cx))?; for b in blocks.iter() { let excerpt_range = context_range_for_entry( b.initial_range.clone(), - DEFAULT_MULTIBUFFER_CONTEXT, + context_lines, buffer_snapshot.clone(), cx, ) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ea7cce5d8b741268fe0d4182b66638c0495bb211..04780e79f84c6f762b246bfb662eb693675e5d38 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -219,7 +219,6 @@ use crate::{ pub const FILE_HEADER_HEIGHT: u32 = 2; pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1; -pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; @@ -6402,7 +6401,7 @@ impl Editor { PathKey::for_buffer(buffer_handle, cx), buffer_handle.clone(), edited_ranges, - DEFAULT_MULTIBUFFER_CONTEXT, + multibuffer_context_lines(cx), cx, ); @@ -16237,7 +16236,7 @@ impl Editor { PathKey::for_buffer(&location.buffer, cx), location.buffer.clone(), ranges_for_buffer, - DEFAULT_MULTIBUFFER_CONTEXT, + multibuffer_context_lines(cx), cx, ); ranges.extend(new_ranges) @@ -24078,3 +24077,10 @@ fn render_diff_hunk_controls( ) .into_any_element() } + +pub fn multibuffer_context_lines(cx: &App) -> u32 { + EditorSettings::try_get(cx) + .map(|settings| settings.excerpt_context_lines) + .unwrap_or(2) + .clamp(1, 32) +} diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 1d7e04cae021dd7b755f1f80e78fd3ea83197539..9b110d782a0bbcf789791240ef42a935b7ecd47b 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -37,6 +37,7 @@ pub struct EditorSettings { pub multi_cursor_modifier: MultiCursorModifier, pub redact_private_values: bool, pub expand_excerpt_lines: u32, + pub excerpt_context_lines: u32, pub middle_click_paste: bool, #[serde(default)] pub double_click_in_multibuffer: DoubleClickInMultibuffer, @@ -515,6 +516,11 @@ pub struct EditorSettingsContent { /// Default: 3 pub expand_excerpt_lines: Option, + /// How many lines of context to provide in multibuffer excerpts by default + /// + /// Default: 2 + pub excerpt_context_lines: Option, + /// Whether to enable middle-click paste on Linux /// /// Default: true diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 85471c7ce96e172f7bd5ade399ed0ba1cd6d4a02..dfef8a92f064e3c8785f92d26e058fc43519dca2 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -19867,7 +19867,7 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) { PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()), buffer.clone(), vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)], - DEFAULT_MULTIBUFFER_CONTEXT, + 2, cx, ); } diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index d428ccbb0509702ee2535fb8c8e95b059fa24499..ac51cee8e42567a607891dd242c2bf103ae7fc0e 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -1,6 +1,6 @@ use anyhow::{Context as _, Result}; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; -use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects}; +use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_context_lines}; use git::repository::{CommitDetails, CommitDiff, CommitSummary, RepoPath}; use gpui::{ AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, @@ -195,7 +195,7 @@ impl CommitView { PathKey::namespaced(FILE_NAMESPACE, path), buffer, diff_hunk_ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, + multibuffer_context_lines(cx), cx, ); multibuffer.add_diff(buffer_diff, cx); diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 524dbf13d30e4539dcc80ec37625333a37cc2206..69ebd83ea8c1a78f13f2218c020bd8654f2b4374 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -10,6 +10,7 @@ use collections::HashSet; use editor::{ Editor, EditorEvent, SelectionEffects, actions::{GoToHunk, GoToPreviousHunk}, + multibuffer_context_lines, scroll::Autoscroll, }; use futures::StreamExt; @@ -465,7 +466,7 @@ impl ProjectDiff { path_key.clone(), buffer, excerpt_ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, + multibuffer_context_lines(cx), cx, ); (was_empty, is_newly_added) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 1ee959f111bd5741a655551aa71030fd9d7c15c9..2668d270d7f008d49d6d067ba01d951d44a43a00 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -11,6 +11,7 @@ use editor::{ Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, SelectionEffects, actions::{Backtab, SelectAll, Tab}, items::active_match_index, + multibuffer_context_lines, }; use futures::{StreamExt, stream::FuturesOrdered}; use gpui::{ @@ -345,7 +346,7 @@ impl ProjectSearch { excerpts.set_anchored_excerpts_for_path( buffer, ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, + multibuffer_context_lines(cx), cx, ) }) diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 3deaed8b9d0b9cba46a955409f6013d133a08358..c83719141067c8271e4d64344c957454740febea 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -103,6 +103,18 @@ pub trait Settings: 'static + Send + Sync { cx.global::().get(None) } + #[track_caller] + fn try_get(cx: &App) -> Option<&Self> + where + Self: Sized, + { + if cx.has_global::() { + cx.global::().try_get(None) + } else { + None + } + } + #[track_caller] fn try_read_global(cx: &AsyncApp, f: impl FnOnce(&Self) -> R) -> Option where @@ -407,6 +419,17 @@ impl SettingsStore { .expect("no default value for setting type") } + /// Get the value of a setting. + /// + /// Panics if the given setting type has not been registered, or if there is no + /// value for this setting. + pub fn try_get(&self, path: Option) -> Option<&T> { + self.setting_values + .get(&TypeId::of::()) + .map(|value| value.value_for_path(path)) + .and_then(|value| value.downcast_ref::()) + } + /// Get all values from project specific settings pub fn get_all_locals(&self) -> Vec<(WorktreeId, Arc, &T)> { self.setting_values diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index fb9306acc5a4b21b709904618a6438e58c30039f..2b1d801f8010c8ad00f1295c38803bd80df1c282 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1461,6 +1461,16 @@ This setting enables integration with macOS’s native window tabbing feature. W Positive `integer` values +## Excerpt Context Lines + +- Description: The number of lines of context to provide when showing excerpts in the multibuffer. +- Setting: `excerpt_context_lines` +- Default: `2` + +**Options** + +Positive `integer` value between 1 and 32. Values outside of this range will be clamped to this range. + ## Extend Comment On Newline - Description: Whether to start a new line with a comment when a previous line is a comment as well. diff --git a/docs/src/development/releases.md b/docs/src/development/releases.md index d1f99401d6b78545c34a64b47a146cecacc7eec1..76432d93f002dc7dd9d9d119d24ed1348863c73e 100644 --- a/docs/src/development/releases.md +++ b/docs/src/development/releases.md @@ -51,7 +51,6 @@ Credentials for various services used in this process can be found in 1Password. - We sometimes correct things here and there that didn't translate from GitHub's renderer to Kit's. 1. Build social media posts based on the popular items in stable. - - You can use the [prior week's post chain](https://zed.dev/channel/tweets-23331) as your outline. - Stage the copy and assets using [Buffer](https://buffer.com), for both X and BlueSky. - Publish both, one at a time, ensuring both are posted to each respective platform. @@ -89,7 +88,6 @@ You will need write access to the Zed repository to do this: - Download the artifacts for each release draft and test that you can run them locally. 1. Publish stable / preview drafts, one at a time. - - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the progress of the website rebuild. The release will be public once the rebuild has completed. diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 4fc5a9ba8864bc3a721d4d7d101977d729082e59..1df76d17f026c9457b296230f93bec0e10c4aa19 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -334,7 +334,9 @@ TBD: Centered layout related settings ```json { // The default number of lines to expand excerpts in the multibuffer by. - "expand_excerpt_lines": 5 + "expand_excerpt_lines": 5, + // The default number of lines of context provided for excerpts in the multibuffer by. + "excerpt_context_lines": 2 } ``` From 384ffb883f472546609b3cc0513623b6bb223c01 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 28 Aug 2025 21:07:52 -0700 Subject: [PATCH 431/744] Fix method documentation (#37140) Release Notes: - N/A --- crates/settings/src/settings_store.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index c83719141067c8271e4d64344c957454740febea..fbd0f75aefc2173a3affbb7423d4ccc718679919 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -421,8 +421,7 @@ impl SettingsStore { /// Get the value of a setting. /// - /// Panics if the given setting type has not been registered, or if there is no - /// value for this setting. + /// Does not panic pub fn try_get(&self, path: Option) -> Option<&T> { self.setting_values .get(&TypeId::of::()) From 52da72d80af8a985db74e04c081fef0453e55e00 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 29 Aug 2025 00:16:49 -0400 Subject: [PATCH 432/744] acp: Install new versions of agent binaries in the background (#37141) Release Notes: - acp: New releases of external agents are now installed in the background. Co-authored-by: Conrad Irwin --- crates/agent_servers/src/agent_servers.rs | 186 ++++++++++++++++------ 1 file changed, 139 insertions(+), 47 deletions(-) diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index e1b4057b71b0b4aee84548df74935d9b0598f598..83b3be76ce709c9b8c4d9f13ca55632a79e7b677 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -7,20 +7,24 @@ mod settings; #[cfg(any(test, feature = "test-support"))] pub mod e2e_tests; +use anyhow::Context as _; pub use claude::*; pub use custom::*; +use fs::Fs; +use fs::RemoveOptions; +use fs::RenameOptions; +use futures::StreamExt as _; pub use gemini::*; +use gpui::AppContext; +use node_runtime::NodeRuntime; pub use settings::*; use acp_thread::AgentConnection; use acp_thread::LoadError; use anyhow::Result; use anyhow::anyhow; -use anyhow::bail; use collections::HashMap; -use gpui::AppContext as _; use gpui::{App, AsyncApp, Entity, SharedString, Task}; -use node_runtime::VersionStrategy; use project::Project; use schemars::JsonSchema; use semver::Version; @@ -64,70 +68,158 @@ impl AgentServerDelegate { let project = self.project; let fs = project.read(cx).fs().clone(); let Some(node_runtime) = project.read(cx).node_runtime().cloned() else { - return Task::ready(Err(anyhow!("Missing node runtime"))); + return Task::ready(Err(anyhow!( + "External agents are not yet available in remote projects." + ))); }; let mut status_tx = self.status_tx; cx.spawn(async move |cx| { if !ignore_system_version { if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await { - return Ok(AgentServerCommand { path: bin, args: Vec::new(), env: Default::default() }) + return Ok(AgentServerCommand { + path: bin, + args: Vec::new(), + env: Default::default(), + }); } } - cx.background_spawn(async move { + cx.spawn(async move |cx| { let node_path = node_runtime.binary_path().await?; - let dir = paths::data_dir().join("external_agents").join(binary_name.as_str()); + let dir = paths::data_dir() + .join("external_agents") + .join(binary_name.as_str()); fs.create_dir(&dir).await?; - let local_executable_path = dir.join(entrypoint_path); - let command = AgentServerCommand { - path: node_path, - args: vec![local_executable_path.to_string_lossy().to_string()], - env: Default::default(), - }; - let installed_version = node_runtime - .npm_package_installed_version(&dir, &package_name) - .await? - .filter(|version| { - Version::from_str(&version) - .is_ok_and(|version| Some(version) >= minimum_version) - }); + let mut stream = fs.read_dir(&dir).await?; + let mut versions = Vec::new(); + let mut to_delete = Vec::new(); + while let Some(entry) = stream.next().await { + let Ok(entry) = entry else { continue }; + let Some(file_name) = entry.file_name() else { + continue; + }; + + if let Some(version) = file_name + .to_str() + .and_then(|name| semver::Version::from_str(&name).ok()) + { + versions.push((file_name.to_owned(), version)); + } else { + to_delete.push(file_name.to_owned()) + } + } - status_tx.send("Checking for latest version…".into())?; - let latest_version = match node_runtime.npm_package_latest_version(&package_name).await + versions.sort(); + let newest_version = if let Some((file_name, version)) = versions.last().cloned() + && minimum_version.is_none_or(|minimum_version| version > minimum_version) { - Ok(latest_version) => latest_version, - Err(e) => { - if let Some(installed_version) = installed_version { - log::error!("{e}"); - log::warn!("failed to fetch latest version of {package_name}, falling back to cached version {installed_version}"); - return Ok(command); - } else { - bail!(e); + versions.pop(); + Some(file_name) + } else { + None + }; + to_delete.extend(versions.into_iter().map(|(file_name, _)| file_name)); + + cx.background_spawn({ + let fs = fs.clone(); + let dir = dir.clone(); + async move { + for file_name in to_delete { + fs.remove_dir( + &dir.join(file_name), + RemoveOptions { + recursive: true, + ignore_if_not_exists: false, + }, + ) + .await + .ok(); } } + }) + .detach(); + + let version = if let Some(file_name) = newest_version { + cx.background_spawn({ + let file_name = file_name.clone(); + let dir = dir.clone(); + async move { + let latest_version = + node_runtime.npm_package_latest_version(&package_name).await; + if let Ok(latest_version) = latest_version + && &latest_version != &file_name.to_string_lossy() + { + Self::download_latest_version( + fs, + dir.clone(), + node_runtime, + package_name, + ) + .await + .log_err(); + } + } + }) + .detach(); + file_name + } else { + status_tx.send("Installing…".into()).ok(); + let dir = dir.clone(); + cx.background_spawn(Self::download_latest_version( + fs, + dir.clone(), + node_runtime, + package_name, + )) + .await? + .into() }; + anyhow::Ok(AgentServerCommand { + path: node_path, + args: vec![ + dir.join(version) + .join(entrypoint_path) + .to_string_lossy() + .to_string(), + ], + env: Default::default(), + }) + }) + .await + .map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into()) + }) + } - let should_install = node_runtime - .should_install_npm_package( - &package_name, - &local_executable_path, - &dir, - VersionStrategy::Latest(&latest_version), - ) - .await; + async fn download_latest_version( + fs: Arc, + dir: PathBuf, + node_runtime: NodeRuntime, + package_name: SharedString, + ) -> Result { + let tmp_dir = tempfile::tempdir_in(&dir)?; - if should_install { - status_tx.send("Installing latest version…".into())?; - node_runtime - .npm_install_packages(&dir, &[(&package_name, &latest_version)]) - .await?; - } + node_runtime + .npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")]) + .await?; - Ok(command) - }).await.map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into()) - }) + let version = node_runtime + .npm_package_installed_version(tmp_dir.path(), &package_name) + .await? + .context("expected package to be installed")?; + + fs.rename( + &tmp_dir.keep(), + &dir.join(&version), + RenameOptions { + ignore_if_exists: true, + overwrite: false, + }, + ) + .await?; + + anyhow::Ok(version) } } From 7403a4ba17d05e8ea02f80b5f4ea25d1d3c1cb71 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 29 Aug 2025 12:19:27 +0200 Subject: [PATCH 433/744] Add basic PyEnv and pixi support for python environments (#37156) cc https://github.com/zed-industries/zed/issues/29807 Release Notes: - Fixed terminals and tasks not respecting python pyenv and pixi environments --- crates/language/src/toolchain.rs | 8 +- crates/languages/src/python.rs | 71 ++++++++++--- crates/project/src/debugger/dap_store.rs | 1 - crates/project/src/project_tests.rs | 6 +- crates/project/src/terminals.rs | 125 +++++++++++++---------- crates/remote/src/remote_client.rs | 12 +-- crates/remote/src/transport/ssh.rs | 12 +-- crates/terminal/src/terminal.rs | 20 +++- 8 files changed, 155 insertions(+), 100 deletions(-) diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 2a8dfd58418812b94c625845dce9724e145c7388..84b10c7961eddb130f88b24c9e3438ff2882f8d3 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -14,6 +14,7 @@ use collections::HashMap; use fs::Fs; use gpui::{AsyncApp, SharedString}; use settings::WorktreeId; +use task::ShellKind; use crate::{LanguageName, ManifestName}; @@ -68,7 +69,12 @@ pub trait ToolchainLister: Send + Sync { fn term(&self) -> SharedString; /// Returns the name of the manifest file for this toolchain. fn manifest_name(&self) -> ManifestName; - async fn activation_script(&self, toolchain: &Toolchain, fs: &dyn Fs) -> Option; + async fn activation_script( + &self, + toolchain: &Toolchain, + shell: ShellKind, + fs: &dyn Fs, + ) -> Vec; } #[async_trait(?Send)] diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 37d38de9dab6bb5968b446e7009a42c5f2e86e86..f76bd8e793d8e391654cb6391086ade528d56264 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -34,7 +34,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use task::{TaskTemplate, TaskTemplates, VariableName}; +use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName}; use util::ResultExt; pub(crate) struct PyprojectTomlManifestProvider; @@ -894,20 +894,65 @@ impl ToolchainLister for PythonToolchainProvider { fn term(&self) -> SharedString { self.term.clone() } - async fn activation_script(&self, toolchain: &Toolchain, fs: &dyn Fs) -> Option { - let toolchain = serde_json::from_value::( + async fn activation_script( + &self, + toolchain: &Toolchain, + shell: ShellKind, + fs: &dyn Fs, + ) -> Vec { + let Ok(toolchain) = serde_json::from_value::( toolchain.as_json.clone(), - ) - .ok()?; - let mut activation_script = None; - if let Some(prefix) = &toolchain.prefix { - #[cfg(not(target_os = "windows"))] - let path = prefix.join(BINARY_DIR).join("activate"); - #[cfg(target_os = "windows")] - let path = prefix.join(BINARY_DIR).join("activate.ps1"); - if fs.is_file(&path).await { - activation_script = Some(format!(". {}", path.display())); + ) else { + return vec![]; + }; + let mut activation_script = vec![]; + + match toolchain.kind { + Some(PythonEnvironmentKind::Pixi) => { + let env = toolchain.name.as_deref().unwrap_or("default"); + activation_script.push(format!("pixi shell -e {env}")) + } + Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => { + if let Some(prefix) = &toolchain.prefix { + let activate_keyword = match shell { + ShellKind::Cmd => ".", + ShellKind::Nushell => "overlay use", + ShellKind::Powershell => ".", + ShellKind::Fish => "source", + ShellKind::Csh => "source", + ShellKind::Posix => "source", + }; + let activate_script_name = match shell { + ShellKind::Posix => "activate", + ShellKind::Csh => "activate.csh", + ShellKind::Fish => "activate.fish", + ShellKind::Nushell => "activate.nu", + ShellKind::Powershell => "activate.ps1", + ShellKind::Cmd => "activate.bat", + }; + let path = prefix.join(BINARY_DIR).join(activate_script_name); + if fs.is_file(&path).await { + activation_script.push(format!("{activate_keyword} {}", path.display())); + } + } + } + Some(PythonEnvironmentKind::Pyenv) => { + let Some(manager) = toolchain.manager else { + return vec![]; + }; + let version = toolchain.version.as_deref().unwrap_or("system"); + let pyenv = manager.executable; + let pyenv = pyenv.display(); + activation_script.extend(match shell { + ShellKind::Fish => Some(format!("{pyenv} shell - fish {version}")), + ShellKind::Posix => Some(format!("{pyenv} shell - sh {version}")), + ShellKind::Nushell => Some(format!("{pyenv} shell - nu {version}")), + ShellKind::Powershell => None, + ShellKind::Csh => None, + ShellKind::Cmd => None, + }) } + _ => {} } activation_script } diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 859574c82a5b4470d477df555b314498cbfcd0e0..d8c6d3acc1116e9a97b2f6ca3fc54ec098029cbe 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -276,7 +276,6 @@ impl DapStore { &binary.arguments, &binary.envs, binary.cwd.map(|path| path.display().to_string()), - None, port_forwarding, ) })??; diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index c814d6207e92608c13502a4da3a0781836acce0e..96f891d9c380fe6feec490627cd782955c833eda 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -40,7 +40,7 @@ use serde_json::json; #[cfg(not(windows))] use std::os; use std::{env, mem, num::NonZeroU32, ops::Range, str::FromStr, sync::OnceLock, task::Poll}; -use task::{ResolvedTask, TaskContext}; +use task::{ResolvedTask, ShellKind, TaskContext}; use unindent::Unindent as _; use util::{ TryFutureExt as _, assert_set_eq, maybe, path, @@ -9222,8 +9222,8 @@ fn python_lang(fs: Arc) -> Arc { fn manifest_name(&self) -> ManifestName { SharedString::new_static("pyproject.toml").into() } - async fn activation_script(&self, _: &Toolchain, _: &dyn Fs) -> Option { - None + async fn activation_script(&self, _: &Toolchain, _: ShellKind, _: &dyn Fs) -> Vec { + vec![] } } Arc::new( diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index aad5ce941125c2c747df3a76473a9dbffba0b80e..c189242fadc2948593186edb5dcd2c56879f07af 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,7 +1,8 @@ use anyhow::Result; use collections::HashMap; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; -use itertools::Itertools; + +use itertools::Itertools as _; use language::LanguageName; use remote::RemoteClient; use settings::{Settings, SettingsLocation}; @@ -11,7 +12,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use task::{Shell, ShellBuilder, SpawnInTerminal}; +use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal}; use terminal::{ TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings, }; @@ -131,33 +132,62 @@ impl Project { cx.spawn(async move |project, cx| { let activation_script = maybe!(async { let toolchain = toolchain?.await?; - lang_registry - .language_for_name(&toolchain.language_name.0) - .await - .ok()? - .toolchain_lister()? - .activation_script(&toolchain, fs.as_ref()) - .await + Some( + lang_registry + .language_for_name(&toolchain.language_name.0) + .await + .ok()? + .toolchain_lister()? + .activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref()) + .await, + ) }) - .await; + .await + .unwrap_or_default(); project.update(cx, move |this, cx| { let shell = { env.extend(spawn_task.env); match remote_client { - Some(remote_client) => create_remote_shell( - spawn_task - .command - .as_ref() - .map(|command| (command, &spawn_task.args)), - &mut env, - path, - remote_client, - activation_script.clone(), - cx, - )?, + Some(remote_client) => match activation_script.clone() { + activation_script if !activation_script.is_empty() => { + let activation_script = activation_script.join("; "); + let to_run = if let Some(command) = spawn_task.command { + let command: Option> = shlex::try_quote(&command).ok(); + let args = spawn_task + .args + .iter() + .filter_map(|arg| shlex::try_quote(arg).ok()); + command.into_iter().chain(args).join(" ") + } else { + format!("exec {shell} -l") + }; + let args = vec![ + "-c".to_owned(), + format!("{activation_script}; {to_run}",), + ]; + create_remote_shell( + Some((&shell, &args)), + &mut env, + path, + remote_client, + cx, + )? + } + _ => create_remote_shell( + spawn_task + .command + .as_ref() + .map(|command| (command, &spawn_task.args)), + &mut env, + path, + remote_client, + cx, + )?, + }, None => match activation_script.clone() { - Some(activation_script) => { + activation_script if !activation_script.is_empty() => { + let activation_script = activation_script.join("; "); let to_run = if let Some(command) = spawn_task.command { let command: Option> = shlex::try_quote(&command).ok(); let args = spawn_task @@ -169,7 +199,7 @@ impl Project { format!("exec {shell} -l") }; Shell::WithArguments { - program: get_default_system_shell(), + program: shell, args: vec![ "-c".to_owned(), format!("{activation_script}; {to_run}",), @@ -177,7 +207,7 @@ impl Project { title_override: None, } } - None => { + _ => { if let Some(program) = spawn_task.command { Shell::WithArguments { program, @@ -302,31 +332,21 @@ impl Project { .await .ok(); let lister = language?.toolchain_lister(); - lister?.activation_script(&toolchain, fs.as_ref()).await + Some( + lister? + .activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref()) + .await, + ) }) - .await; + .await + .unwrap_or_default(); project.update(cx, move |this, cx| { let shell = { match remote_client { - Some(remote_client) => create_remote_shell( - None, - &mut env, - path, - remote_client, - activation_script.clone(), - cx, - )?, - None => match activation_script.clone() { - Some(activation_script) => Shell::WithArguments { - program: get_default_system_shell(), - args: vec![ - "-c".to_owned(), - format!("{activation_script}; exec {shell} -l",), - ], - title_override: Some(shell.into()), - }, - None => settings.shell, - }, + Some(remote_client) => { + create_remote_shell(None, &mut env, path, remote_client, cx)? + } + None => settings.shell, } }; TerminalBuilder::new( @@ -437,15 +457,10 @@ impl Project { match remote_client { Some(remote_client) => { - let command_template = remote_client.read(cx).build_command( - Some(command), - &args, - &env, - None, - // todo - None, - None, - )?; + let command_template = + remote_client + .read(cx) + .build_command(Some(command), &args, &env, None, None)?; let mut command = std::process::Command::new(command_template.program); command.args(command_template.args); command.envs(command_template.env); @@ -473,7 +488,6 @@ fn create_remote_shell( env: &mut HashMap, working_directory: Option>, remote_client: Entity, - activation_script: Option, cx: &mut App, ) -> Result { // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed @@ -493,7 +507,6 @@ fn create_remote_shell( args.as_slice(), env, working_directory.map(|path| path.display().to_string()), - activation_script, None, )?; *env = command.env; diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index 2b8d9e4a94fb9988e801c5ef9202ee603959d36b..dd529ca87499b0daf2061fd990f7149828e3fce4 100644 --- a/crates/remote/src/remote_client.rs +++ b/crates/remote/src/remote_client.rs @@ -757,7 +757,6 @@ impl RemoteClient { args: &[String], env: &HashMap, working_dir: Option, - activation_script: Option, port_forward: Option<(u16, String, u16)>, ) -> Result { let Some(connection) = self @@ -767,14 +766,7 @@ impl RemoteClient { else { return Err(anyhow!("no connection")); }; - connection.build_command( - program, - args, - env, - working_dir, - activation_script, - port_forward, - ) + connection.build_command(program, args, env, working_dir, port_forward) } pub fn upload_directory( @@ -1006,7 +998,6 @@ pub(crate) trait RemoteConnection: Send + Sync { args: &[String], env: &HashMap, working_dir: Option, - activation_script: Option, port_forward: Option<(u16, String, u16)>, ) -> Result; fn connection_options(&self) -> SshConnectionOptions; @@ -1373,7 +1364,6 @@ mod fake { args: &[String], env: &HashMap, _: Option, - _: Option, _: Option<(u16, String, u16)>, ) -> Result { let ssh_program = program.unwrap_or_else(|| "sh".to_string()); diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 0036a687a6f73b57723e8c3c9fcffc56cab626c2..b6698014024ab48d171631a190b421dcb614edae 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -30,10 +30,7 @@ use std::{ time::Instant, }; use tempfile::TempDir; -use util::{ - get_default_system_shell, - paths::{PathStyle, RemotePathBuf}, -}; +use util::paths::{PathStyle, RemotePathBuf}; pub(crate) struct SshRemoteConnection { socket: SshSocket, @@ -116,7 +113,6 @@ impl RemoteConnection for SshRemoteConnection { input_args: &[String], input_env: &HashMap, working_dir: Option, - activation_script: Option, port_forward: Option<(u16, String, u16)>, ) -> Result { use std::fmt::Write as _; @@ -138,9 +134,6 @@ impl RemoteConnection for SshRemoteConnection { } else { write!(&mut script, "cd; ").unwrap(); }; - if let Some(activation_script) = activation_script { - write!(&mut script, " {activation_script};").unwrap(); - } for (k, v) in input_env.iter() { if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) { @@ -162,8 +155,7 @@ impl RemoteConnection for SshRemoteConnection { write!(&mut script, "exec {shell} -l").unwrap(); }; - let sys_shell = get_default_system_shell(); - let shell_invocation = format!("{sys_shell} -c {}", shlex::try_quote(&script).unwrap()); + let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&script).unwrap()); let mut args = Vec::new(); args.extend(self.socket.ssh_args()); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index a5e0227533cf0e3ecbc9a8f2c6c55fa1254473e3..0f4f2ae97b67b9fd43a63b54088f66c74ca1c855 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -354,7 +354,7 @@ impl TerminalBuilder { window_id: u64, completion_tx: Option>>, cx: &App, - activation_script: Option, + activation_script: Vec, ) -> Result { // If the parent environment doesn't have a locale set // (As is the case when launched from a .app on MacOS), @@ -493,7 +493,9 @@ impl TerminalBuilder { let pty_tx = event_loop.channel(); let _io_thread = event_loop.spawn(); // DANGER - let terminal = Terminal { + let no_task = task.is_none(); + + let mut terminal = Terminal { task, pty_tx: Notifier(pty_tx), completion_tx, @@ -518,7 +520,7 @@ impl TerminalBuilder { last_hyperlink_search_position: None, #[cfg(windows)] shell_program, - activation_script, + activation_script: activation_script.clone(), template: CopyTemplate { shell, env, @@ -529,6 +531,14 @@ impl TerminalBuilder { }, }; + if !activation_script.is_empty() && no_task { + for activation_script in activation_script { + terminal.input(activation_script.into_bytes()); + terminal.write_to_pty(b"\n"); + } + terminal.clear(); + } + Ok(TerminalBuilder { terminal, events_rx, @@ -712,7 +722,7 @@ pub struct Terminal { #[cfg(windows)] shell_program: Option, template: CopyTemplate, - activation_script: Option, + activation_script: Vec, } struct CopyTemplate { @@ -2218,7 +2228,7 @@ mod tests { 0, Some(completion_tx), cx, - None, + vec![], ) .unwrap() .subscribe(cx) From d13ba0162ae5d6d200b3e4509e691b57e0a27dda Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 29 Aug 2025 12:44:47 +0200 Subject: [PATCH 434/744] Require authorization for MCP tools (#37155) Release Notes: - Fixed a regression that caused MCP tools to run without requesting authorization first. --- crates/agent2/src/tests/mod.rs | 1 + crates/agent2/src/tools/context_server_registry.rs | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index fbeee46a484a71742dd4ce52b537bebb5da91924..4527cdb056164efa8e3bc81c19969a3fa02d7036 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -950,6 +950,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) { paths::settings_file(), json!({ "agent": { + "always_allow_tool_actions": true, "profiles": { "test": { "name": "Test Profile", diff --git a/crates/agent2/src/tools/context_server_registry.rs b/crates/agent2/src/tools/context_server_registry.rs index c7963fa6e6e14ffa34d076dc2ca5dbdc23c78cab..e13f47fb2399d7408c5047ff6491ce2d2e76d948 100644 --- a/crates/agent2/src/tools/context_server_registry.rs +++ b/crates/agent2/src/tools/context_server_registry.rs @@ -169,15 +169,18 @@ impl AnyAgentTool for ContextServerTool { fn run( self: Arc, input: serde_json::Value, - _event_stream: ToolCallEventStream, + event_stream: ToolCallEventStream, cx: &mut App, ) -> Task> { let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else { return Task::ready(Err(anyhow!("Context server not found"))); }; let tool_name = self.tool.name.clone(); + let authorize = event_stream.authorize(self.initial_title(input.clone()), cx); cx.spawn(async move |_cx| { + authorize.await?; + let Some(protocol) = server.client() else { bail!("Context server not initialized"); }; From 4507f60b8d8b43be7770dfcd0ca52bcb655d5d66 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 29 Aug 2025 13:39:38 +0200 Subject: [PATCH 435/744] languages: Fix python activation scripts not being quoted (#37159) Release Notes: - N/A --- crates/languages/src/python.rs | 115 +++++++++++++---------------- crates/remote/src/transport/ssh.rs | 9 ++- 2 files changed, 57 insertions(+), 67 deletions(-) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index f76bd8e793d8e391654cb6391086ade528d56264..5bdc4aa0d94a7355c60ab8912d9a328a657ad77f 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -328,41 +328,35 @@ impl LspAdapter for PythonLspAdapter { .unwrap_or_default(); // If we have a detected toolchain, configure Pyright to use it - if let Some(toolchain) = toolchain { + if let Some(toolchain) = toolchain + && let Ok(env) = serde_json::from_value::< + pet_core::python_environment::PythonEnvironment, + >(toolchain.as_json.clone()) + { if user_settings.is_null() { user_settings = Value::Object(serde_json::Map::default()); } let object = user_settings.as_object_mut().unwrap(); let interpreter_path = toolchain.path.to_string(); + if let Some(venv_dir) = env.prefix { + // Set venvPath and venv at the root level + // This matches the format of a pyrightconfig.json file + if let Some(parent) = venv_dir.parent() { + // Use relative path if the venv is inside the workspace + let venv_path = if parent == adapter.worktree_root_path() { + ".".to_string() + } else { + parent.to_string_lossy().into_owned() + }; + object.insert("venvPath".to_string(), Value::String(venv_path)); + } - // Detect if this is a virtual environment - if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() - && let Some(venv_dir) = interpreter_dir.parent() - { - // Check if this looks like a virtual environment - if venv_dir.join("pyvenv.cfg").exists() - || venv_dir.join("bin/activate").exists() - || venv_dir.join("Scripts/activate.bat").exists() - { - // Set venvPath and venv at the root level - // This matches the format of a pyrightconfig.json file - if let Some(parent) = venv_dir.parent() { - // Use relative path if the venv is inside the workspace - let venv_path = if parent == adapter.worktree_root_path() { - ".".to_string() - } else { - parent.to_string_lossy().into_owned() - }; - object.insert("venvPath".to_string(), Value::String(venv_path)); - } - - if let Some(venv_name) = venv_dir.file_name() { - object.insert( - "venv".to_owned(), - Value::String(venv_name.to_string_lossy().into_owned()), - ); - } + if let Some(venv_name) = venv_dir.file_name() { + object.insert( + "venv".to_owned(), + Value::String(venv_name.to_string_lossy().into_owned()), + ); } } @@ -932,7 +926,8 @@ impl ToolchainLister for PythonToolchainProvider { }; let path = prefix.join(BINARY_DIR).join(activate_script_name); if fs.is_file(&path).await { - activation_script.push(format!("{activate_keyword} {}", path.display())); + activation_script + .push(format!("{activate_keyword} \"{}\"", path.display())); } } } @@ -944,9 +939,9 @@ impl ToolchainLister for PythonToolchainProvider { let pyenv = manager.executable; let pyenv = pyenv.display(); activation_script.extend(match shell { - ShellKind::Fish => Some(format!("{pyenv} shell - fish {version}")), - ShellKind::Posix => Some(format!("{pyenv} shell - sh {version}")), - ShellKind::Nushell => Some(format!("{pyenv} shell - nu {version}")), + ShellKind::Fish => Some(format!("\"{pyenv}\" shell - fish {version}")), + ShellKind::Posix => Some(format!("\"{pyenv}\" shell - sh {version}")), + ShellKind::Nushell => Some(format!("\"{pyenv}\" shell - nu {version}")), ShellKind::Powershell => None, ShellKind::Csh => None, ShellKind::Cmd => None, @@ -1108,10 +1103,10 @@ impl LspAdapter for PyLspAdapter { arguments: vec![], }) } else { - let venv = toolchain?; - let pylsp_path = Path::new(venv.path.as_ref()).parent()?.join("pylsp"); + let toolchain = toolchain?; + let pylsp_path = Path::new(toolchain.path.as_ref()).parent()?.join("pylsp"); pylsp_path.exists().then(|| LanguageServerBinary { - path: venv.path.to_string().into(), + path: toolchain.path.to_string().into(), arguments: vec![pylsp_path.into()], env: None, }) @@ -1575,41 +1570,35 @@ impl LspAdapter for BasedPyrightLspAdapter { .unwrap_or_default(); // If we have a detected toolchain, configure Pyright to use it - if let Some(toolchain) = toolchain { + if let Some(toolchain) = toolchain + && let Ok(env) = serde_json::from_value::< + pet_core::python_environment::PythonEnvironment, + >(toolchain.as_json.clone()) + { if user_settings.is_null() { user_settings = Value::Object(serde_json::Map::default()); } let object = user_settings.as_object_mut().unwrap(); let interpreter_path = toolchain.path.to_string(); + if let Some(venv_dir) = env.prefix { + // Set venvPath and venv at the root level + // This matches the format of a pyrightconfig.json file + if let Some(parent) = venv_dir.parent() { + // Use relative path if the venv is inside the workspace + let venv_path = if parent == adapter.worktree_root_path() { + ".".to_string() + } else { + parent.to_string_lossy().into_owned() + }; + object.insert("venvPath".to_string(), Value::String(venv_path)); + } - // Detect if this is a virtual environment - if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() - && let Some(venv_dir) = interpreter_dir.parent() - { - // Check if this looks like a virtual environment - if venv_dir.join("pyvenv.cfg").exists() - || venv_dir.join("bin/activate").exists() - || venv_dir.join("Scripts/activate.bat").exists() - { - // Set venvPath and venv at the root level - // This matches the format of a pyrightconfig.json file - if let Some(parent) = venv_dir.parent() { - // Use relative path if the venv is inside the workspace - let venv_path = if parent == adapter.worktree_root_path() { - ".".to_string() - } else { - parent.to_string_lossy().into_owned() - }; - object.insert("venvPath".to_string(), Value::String(venv_path)); - } - - if let Some(venv_name) = venv_dir.file_name() { - object.insert( - "venv".to_owned(), - Value::String(venv_name.to_string_lossy().into_owned()), - ); - } + if let Some(venv_name) = venv_dir.file_name() { + object.insert( + "venv".to_owned(), + Value::String(venv_name.to_string_lossy().into_owned()), + ); } } diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index b6698014024ab48d171631a190b421dcb614edae..34f1ebf71c278538b57e486856f9b3315a41cf91 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -125,12 +125,13 @@ impl RemoteConnection for SshRemoteConnection { // shlex will wrap the command in single quotes (''), disabling ~ expansion, // replace ith with something that works const TILDE_PREFIX: &'static str = "~/"; - if working_dir.starts_with(TILDE_PREFIX) { + let working_dir = if working_dir.starts_with(TILDE_PREFIX) { let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/"); - write!(&mut script, "cd \"$HOME/{working_dir}\"; ").unwrap(); + format!("$HOME/{working_dir}") } else { - write!(&mut script, "cd \"{working_dir}\"; ").unwrap(); - } + working_dir + }; + write!(&mut script, "cd \"{working_dir}\"; ",).unwrap(); } else { write!(&mut script, "cd; ").unwrap(); }; From 01266d10d60269723c6b8d41bbcbe6363bc38ca0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 29 Aug 2025 15:23:45 +0300 Subject: [PATCH 436/744] Do not send any LSP logs by default to collab clients (#37163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up https://github.com/zed-industries/zed/pull/37083 Noisy RPC LSP logs were functioning this way already, but to keep Collab loaded even less, do not send any kind of logs to the client if the client has a corresponding log tab not opened. This change is pretty raw and does not fully cover scenarious with multiple clients: if one client has a log tab open and another opens tab with another kind of log, the 2nd kind of logs will be streamed only. Also, it should be possible to forward the host logs to the client on enabling — that is not done to keep the change smaller. Release Notes: - N/A --- crates/language_tools/src/language_tools.rs | 2 +- crates/language_tools/src/lsp_log_view.rs | 96 ++++++++++++++----- .../language_tools/src/lsp_log_view_tests.rs | 2 +- crates/project/src/lsp_store/log_store.rs | 66 ++++++++----- crates/project/src/project.rs | 12 ++- crates/remote_server/src/headless_project.rs | 6 +- 6 files changed, 135 insertions(+), 49 deletions(-) diff --git a/crates/language_tools/src/language_tools.rs b/crates/language_tools/src/language_tools.rs index c784a67313a904df34c9f2ae071ed5b0e4c11751..aa1672806417493c0c5a877a28fc7906f3da6ff8 100644 --- a/crates/language_tools/src/language_tools.rs +++ b/crates/language_tools/src/language_tools.rs @@ -14,7 +14,7 @@ use ui::{Context, Window}; use workspace::{Item, ItemHandle, SplitDirection, Workspace}; pub fn init(cx: &mut App) { - lsp_log_view::init(true, cx); + lsp_log_view::init(false, cx); syntax_tree_view::init(cx); key_context_view::init(cx); } diff --git a/crates/language_tools/src/lsp_log_view.rs b/crates/language_tools/src/lsp_log_view.rs index e54411f1d43a6e99352b8ef4dfc48cca423badb6..b1f1e5c4f62b4c14b88cdd3de27a1624c7c7158f 100644 --- a/crates/language_tools/src/lsp_log_view.rs +++ b/crates/language_tools/src/lsp_log_view.rs @@ -16,6 +16,7 @@ use project::{ lsp_store::log_store::{self, Event, LanguageServerKind, LogKind, LogStore, Message}, search::SearchQuery, }; +use proto::toggle_lsp_logs::LogType; use std::{any::TypeId, borrow::Cow, sync::Arc}; use ui::{Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState, prelude::*}; use util::ResultExt as _; @@ -111,8 +112,8 @@ actions!( ] ); -pub fn init(store_logs: bool, cx: &mut App) { - let log_store = log_store::init(store_logs, cx); +pub fn init(on_headless_host: bool, cx: &mut App) { + let log_store = log_store::init(on_headless_host, cx); log_store.update(cx, |_, cx| { Copilot::global(cx).map(|copilot| { @@ -266,6 +267,19 @@ impl LspLogView { window.focus(&log_view.editor.focus_handle(cx)); }); + cx.on_release(|log_view, cx| { + log_view.log_store.update(cx, |log_store, cx| { + for (server_id, state) in &log_store.language_servers { + if let Some(log_kind) = state.toggled_log_kind { + if let Some(log_type) = log_type(log_kind) { + send_toggle_log_message(state, *server_id, false, log_type, cx); + } + } + } + }); + }) + .detach(); + let mut lsp_log_view = Self { focus_handle, editor, @@ -436,6 +450,12 @@ impl LspLogView { cx.notify(); } self.editor.read(cx).focus_handle(cx).focus(window); + self.log_store.update(cx, |log_store, cx| { + let state = log_store.get_language_server_state(server_id)?; + state.toggled_log_kind = Some(LogKind::Logs); + send_toggle_log_message(state, server_id, true, LogType::Log, cx); + Some(()) + }); } fn update_log_level( @@ -472,8 +492,8 @@ impl LspLogView { ) { let trace_level = self .log_store - .update(cx, |this, _| { - Some(this.get_language_server_state(server_id)?.trace_level) + .update(cx, |log_store, _| { + Some(log_store.get_language_server_state(server_id)?.trace_level) }) .unwrap_or(TraceValue::Messages); let log_contents = self @@ -487,6 +507,12 @@ impl LspLogView { let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx); self.editor = editor; self.editor_subscriptions = editor_subscriptions; + self.log_store.update(cx, |log_store, cx| { + let state = log_store.get_language_server_state(server_id)?; + state.toggled_log_kind = Some(LogKind::Trace); + send_toggle_log_message(state, server_id, true, LogType::Trace, cx); + Some(()) + }); cx.notify(); } self.editor.read(cx).focus_handle(cx).focus(window); @@ -551,24 +577,7 @@ impl LspLogView { } if let Some(server_state) = log_store.language_servers.get(&server_id) { - if let LanguageServerKind::Remote { project } = &server_state.kind { - project - .update(cx, |project, cx| { - if let Some((client, project_id)) = - project.lsp_store().read(cx).upstream_client() - { - client - .send(proto::ToggleLspLogs { - project_id, - log_type: proto::toggle_lsp_logs::LogType::Rpc as i32, - server_id: server_id.to_proto(), - enabled, - }) - .log_err(); - } - }) - .ok(); - } + send_toggle_log_message(server_state, server_id, enabled, LogType::Rpc, cx); }; }); if !enabled && Some(server_id) == self.current_server_id { @@ -644,6 +653,49 @@ impl LspLogView { self.editor_subscriptions = editor_subscriptions; cx.notify(); self.editor.read(cx).focus_handle(cx).focus(window); + self.log_store.update(cx, |log_store, cx| { + let state = log_store.get_language_server_state(server_id)?; + if let Some(log_kind) = state.toggled_log_kind.take() { + if let Some(log_type) = log_type(log_kind) { + send_toggle_log_message(state, server_id, false, log_type, cx); + } + }; + Some(()) + }); + } +} + +fn log_type(log_kind: LogKind) -> Option { + match log_kind { + LogKind::Rpc => Some(LogType::Rpc), + LogKind::Trace => Some(LogType::Trace), + LogKind::Logs => Some(LogType::Log), + LogKind::ServerInfo => None, + } +} + +fn send_toggle_log_message( + server_state: &log_store::LanguageServerState, + server_id: LanguageServerId, + enabled: bool, + log_type: LogType, + cx: &mut App, +) { + if let LanguageServerKind::Remote { project } = &server_state.kind { + project + .update(cx, |project, cx| { + if let Some((client, project_id)) = project.lsp_store().read(cx).upstream_client() { + client + .send(proto::ToggleLspLogs { + project_id, + log_type: log_type as i32, + server_id: server_id.to_proto(), + enabled, + }) + .log_err(); + } + }) + .ok(); } } diff --git a/crates/language_tools/src/lsp_log_view_tests.rs b/crates/language_tools/src/lsp_log_view_tests.rs index bfd093e3db1c1bc0dc04b111d2072339f1314b8e..d572c4375ed09997dc57d6c58e6c90f3e55775b6 100644 --- a/crates/language_tools/src/lsp_log_view_tests.rs +++ b/crates/language_tools/src/lsp_log_view_tests.rs @@ -53,7 +53,7 @@ async fn test_lsp_log_view(cx: &mut TestAppContext) { }, ); - let log_store = cx.new(|cx| LogStore::new(true, cx)); + let log_store = cx.new(|cx| LogStore::new(false, cx)); log_store.update(cx, |store, cx| store.add_project(&project, cx)); let _rust_buffer = project diff --git a/crates/project/src/lsp_store/log_store.rs b/crates/project/src/lsp_store/log_store.rs index 1fbdb494a303b47bea181c5046e51f3c0b21c5c1..67a20dd6cd8b2f5d6ca48d7790fc0b2e60aff370 100644 --- a/crates/project/src/lsp_store/log_store.rs +++ b/crates/project/src/lsp_store/log_store.rs @@ -21,8 +21,8 @@ const SERVER_LOGS: &str = "Server Logs"; const SERVER_TRACE: &str = "Server Trace"; const SERVER_INFO: &str = "Server Info"; -pub fn init(store_logs: bool, cx: &mut App) -> Entity { - let log_store = cx.new(|cx| LogStore::new(store_logs, cx)); +pub fn init(on_headless_host: bool, cx: &mut App) -> Entity { + let log_store = cx.new(|cx| LogStore::new(on_headless_host, cx)); cx.set_global(GlobalLogStore(log_store.clone())); log_store } @@ -43,7 +43,7 @@ pub enum Event { impl EventEmitter for LogStore {} pub struct LogStore { - store_logs: bool, + on_headless_host: bool, projects: HashMap, ProjectState>, pub copilot_log_subscription: Option, pub language_servers: HashMap, @@ -138,6 +138,7 @@ pub struct LanguageServerState { pub trace_level: TraceValue, pub log_level: MessageType, io_logs_subscription: Option, + pub toggled_log_kind: Option, } impl std::fmt::Debug for LanguageServerState { @@ -151,6 +152,7 @@ impl std::fmt::Debug for LanguageServerState { .field("rpc_state", &self.rpc_state) .field("trace_level", &self.trace_level) .field("log_level", &self.log_level) + .field("toggled_log_kind", &self.toggled_log_kind) .finish_non_exhaustive() } } @@ -226,14 +228,14 @@ impl LogKind { } impl LogStore { - pub fn new(store_logs: bool, cx: &mut Context) -> Self { + pub fn new(on_headless_host: bool, cx: &mut Context) -> Self { let (io_tx, mut io_rx) = mpsc::unbounded(); let log_store = Self { projects: HashMap::default(), language_servers: HashMap::default(), copilot_log_subscription: None, - store_logs, + on_headless_host, io_tx, }; cx.spawn(async move |log_store, cx| { @@ -351,12 +353,26 @@ impl LogStore { } } } - crate::Event::ToggleLspLogs { server_id, enabled } => { - // we do not support any other log toggling yet - if *enabled { - log_store.enable_rpc_trace_for_language_server(*server_id); - } else { - log_store.disable_rpc_trace_for_language_server(*server_id); + crate::Event::ToggleLspLogs { + server_id, + enabled, + toggled_log_kind, + } => { + if let Some(server_state) = + log_store.get_language_server_state(*server_id) + { + if *enabled { + server_state.toggled_log_kind = Some(*toggled_log_kind); + } else { + server_state.toggled_log_kind = None; + } + } + if LogKind::Rpc == *toggled_log_kind { + if *enabled { + log_store.enable_rpc_trace_for_language_server(*server_id); + } else { + log_store.disable_rpc_trace_for_language_server(*server_id); + } } } _ => {} @@ -395,6 +411,7 @@ impl LogStore { trace_level: TraceValue::Off, log_level: MessageType::LOG, io_logs_subscription: None, + toggled_log_kind: None, } }); @@ -425,7 +442,7 @@ impl LogStore { message: &str, cx: &mut Context, ) -> Option<()> { - let store_logs = self.store_logs; + let store_logs = !self.on_headless_host; let language_server_state = self.get_language_server_state(id)?; let log_lines = &mut language_server_state.log_messages; @@ -464,7 +481,7 @@ impl LogStore { verbose_info: Option, cx: &mut Context, ) -> Option<()> { - let store_logs = self.store_logs; + let store_logs = !self.on_headless_host; let language_server_state = self.get_language_server_state(id)?; let log_lines = &mut language_server_state.trace_messages; @@ -530,7 +547,7 @@ impl LogStore { message: &str, cx: &mut Context<'_, Self>, ) { - let store_logs = self.store_logs; + let store_logs = !self.on_headless_host; let Some(state) = self .get_language_server_state(language_server_id) .and_then(|state| state.rpc_state.as_mut()) @@ -673,6 +690,7 @@ impl LogStore { } fn emit_event(&mut self, e: Event, cx: &mut Context) { + let on_headless_host = self.on_headless_host; match &e { Event::NewServerLogEntry { id, kind, text } => { if let Some(state) = self.get_language_server_state(*id) { @@ -686,14 +704,18 @@ impl LogStore { } .and_then(|lsp_store| lsp_store.read(cx).downstream_client()); if let Some((client, project_id)) = downstream_client { - client - .send(proto::LanguageServerLog { - project_id, - language_server_id: id.to_proto(), - message: text.clone(), - log_type: Some(kind.to_proto()), - }) - .ok(); + if on_headless_host + || Some(LogKind::from_server_log_type(kind)) == state.toggled_log_kind + { + client + .send(proto::LanguageServerLog { + project_id, + language_server_id: id.to_proto(), + message: text.clone(), + log_type: Some(kind.to_proto()), + }) + .ok(); + } } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 68e04cfd3bec25638964e8fccd675279c450795d..8c289c935cd2bc4ebb919d171f0a9e4f0334b334 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -33,7 +33,7 @@ mod yarn; use dap::inline_value::{InlineValueLocation, VariableLookupKind, VariableScope}; -use crate::git_store::GitStore; +use crate::{git_store::GitStore, lsp_store::log_store::LogKind}; pub use git_store::{ ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate, git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal}, @@ -285,6 +285,7 @@ pub enum Event { ToggleLspLogs { server_id: LanguageServerId, enabled: bool, + toggled_log_kind: LogKind, }, Toast { notification_id: SharedString, @@ -4719,10 +4720,19 @@ impl Project { envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result<()> { + let toggled_log_kind = + match proto::toggle_lsp_logs::LogType::from_i32(envelope.payload.log_type) + .context("invalid log type")? + { + proto::toggle_lsp_logs::LogType::Log => LogKind::Logs, + proto::toggle_lsp_logs::LogType::Trace => LogKind::Trace, + proto::toggle_lsp_logs::LogType::Rpc => LogKind::Rpc, + }; project.update(&mut cx, |_, cx| { cx.emit(Event::ToggleLspLogs { server_id: LanguageServerId::from_proto(envelope.payload.server_id), enabled: envelope.payload.enabled, + toggled_log_kind, }) })?; Ok(()) diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index c81a69c2b308d2623ada78b1f38df80f96f8fe14..f55826631b46b4f9eaaa17d8a9f4b0603a07fcc3 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -67,7 +67,7 @@ impl HeadlessProject { settings::init(cx); language::init(cx); project::Project::init_settings(cx); - log_store::init(false, cx); + log_store::init(true, cx); } pub fn new( @@ -546,7 +546,9 @@ impl HeadlessProject { .context("lsp logs store is missing")?; lsp_logs.update(&mut cx, |lsp_logs, _| { - // we do not support any other log toggling yet + // RPC logs are very noisy and we need to toggle it on the headless server too. + // The rest of the logs for the ssh project are very important to have toggled always, + // to e.g. send language server error logs to the client before anything is toggled. if envelope.payload.enabled { lsp_logs.enable_rpc_trace_for_language_server(server_id); } else { From ff035e8a22fb40ea29af97f974351151af226198 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 29 Aug 2025 09:26:52 -0300 Subject: [PATCH 437/744] agent: Add CC item in the settings view (#37164) Release Notes: - N/A --- crates/agent_ui/src/agent_configuration.rs | 49 ++++++++++++---------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 23b6e69a56886ca2e5d7c4bdbd27ee8fb1307629..5f0b6f33c38b0b064fcb8b287a901a33e9e7186b 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -331,6 +331,7 @@ impl AgentConfiguration { .gap_0p5() .child( h_flex() + .pr_1() .w_full() .gap_2() .justify_between() @@ -1022,6 +1023,7 @@ impl AgentConfiguration { .gap_0p5() .child( h_flex() + .pr_1() .w_full() .gap_2() .justify_between() @@ -1052,7 +1054,7 @@ impl AgentConfiguration { ) .child( Label::new( - "Bring the agent of your choice to Zed via our new Agent Client Protocol.", + "All agents connected through the Agent Client Protocol.", ) .color(Color::Muted), ), @@ -1063,7 +1065,12 @@ impl AgentConfiguration { ExternalAgent::Gemini, cx, )) - // TODO add CC + .child(self.render_agent_server( + IconName::AiClaude, + "Claude Code", + ExternalAgent::ClaudeCode, + cx, + )) .children(user_defined_agents), ) } @@ -1093,26 +1100,24 @@ impl AgentConfiguration { .child(Label::new(name.clone())), ) .child( - h_flex().gap_1().child( - Button::new( - SharedString::from(format!("start_acp_thread-{name}")), - "Start New Thread", - ) - .label_size(LabelSize::Small) - .icon(IconName::Thread) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click(move |_, window, cx| { - window.dispatch_action( - NewExternalAgentThread { - agent: Some(agent.clone()), - } - .boxed_clone(), - cx, - ); - }), - ), + Button::new( + SharedString::from(format!("start_acp_thread-{name}")), + "Start New Thread", + ) + .label_size(LabelSize::Small) + .icon(IconName::Thread) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .on_click(move |_, window, cx| { + window.dispatch_action( + NewExternalAgentThread { + agent: Some(agent.clone()), + } + .boxed_clone(), + cx, + ); + }), ) } } From 20d32d111c15dfef49a1c9c1267d33250d58b67b Mon Sep 17 00:00:00 2001 From: Wouter Kayser Date: Fri, 29 Aug 2025 16:08:42 +0200 Subject: [PATCH 438/744] Update lsp-types to properly handle brackets (#37166) Closes #21062 See also this pull request: https://github.com/zed-industries/lsp-types/pull/6. Release Notes: - Fixed incorrect URL encoding of file paths with `[` `]` in them --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/agent_ui/src/acp/message_editor.rs | 2 +- crates/collab/src/tests/editor_tests.rs | 52 ++++++------ crates/collab/src/tests/integration_tests.rs | 26 +++--- .../random_project_collaboration_tests.rs | 2 +- crates/copilot/src/copilot.rs | 12 +-- crates/copilot/src/request.rs | 2 +- crates/diagnostics/src/diagnostics_tests.rs | 45 +++++----- crates/editor/src/editor_tests.rs | 36 ++++---- crates/editor/src/inlay_hint_cache.rs | 28 +++--- .../src/test/editor_lsp_test_context.rs | 6 +- crates/language/src/buffer.rs | 2 +- crates/language/src/proto.rs | 2 +- crates/languages/src/rust.rs | 2 +- crates/lsp/src/lsp.rs | 38 ++++----- crates/project/src/lsp_command.rs | 30 +++---- crates/project/src/lsp_store.rs | 54 +++++++----- .../project/src/lsp_store/lsp_ext_command.rs | 2 +- crates/project/src/project.rs | 4 +- crates/project/src/project_tests.rs | 85 +++++++++---------- crates/project_symbols/src/project_symbols.rs | 2 +- .../remote_server/src/remote_editing_tests.rs | 2 +- 23 files changed, 223 insertions(+), 215 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aeacdd899685e46ebd1d38df7cd58b19810de9c5..e493c99a2fc0f9514503b7cee8ef41cca582c387 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9770,7 +9770,7 @@ dependencies = [ [[package]] name = "lsp-types" version = "0.95.1" -source = "git+https://github.com/zed-industries/lsp-types?rev=39f629bdd03d59abd786ed9fc27e8bca02c0c0ec#39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" +source = "git+https://github.com/zed-industries/lsp-types?rev=0874f8742fe55b4dc94308c1e3c0069710d8eeaf#0874f8742fe55b4dc94308c1e3c0069710d8eeaf" dependencies = [ "bitflags 1.3.2", "serde", diff --git a/Cargo.toml b/Cargo.toml index 974796a5e5ff4a3093fc8b492628e1c6d33a616a..d346043c0ef64b3cce0827c2553c5b3c254d66f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -519,7 +519,7 @@ libc = "0.2" libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } -lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" } +lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "0874f8742fe55b4dc94308c1e3c0069710d8eeaf" } mach2 = "0.5" markup5ever_rcdom = "0.3.0" metal = "0.29" diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index f4ce2652d60c76848827967f8a34a23376e7406f..bd5e4faf7aedba4644206794a1c7a837517c52d6 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -2128,7 +2128,7 @@ mod tests { lsp::SymbolInformation { name: "MySymbol".into(), location: lsp::Location { - uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(), range: lsp::Range::new( lsp::Position::new(0, 0), lsp::Position::new(0, 1), diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 59d66f1821e60ecbf3a7550c1385fa6de7ae047d..bfea497e9b57d806af1f13bb3af7e88521d03816 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -369,7 +369,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu .set_request_handler::(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); assert_eq!( params.text_document_position.position, @@ -488,7 +488,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu .set_request_handler::(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); assert_eq!( params.text_document_position.position, @@ -615,7 +615,7 @@ async fn test_collaborating_with_code_actions( .set_request_handler::(|params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); assert_eq!(params.range.start, lsp::Position::new(0, 0)); assert_eq!(params.range.end, lsp::Position::new(0, 0)); @@ -637,7 +637,7 @@ async fn test_collaborating_with_code_actions( .set_request_handler::(|params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); assert_eq!(params.range.start, lsp::Position::new(1, 31)); assert_eq!(params.range.end, lsp::Position::new(1, 31)); @@ -649,7 +649,7 @@ async fn test_collaborating_with_code_actions( changes: Some( [ ( - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), vec![lsp::TextEdit::new( lsp::Range::new( lsp::Position::new(1, 22), @@ -659,7 +659,7 @@ async fn test_collaborating_with_code_actions( )], ), ( - lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap(), vec![lsp::TextEdit::new( lsp::Range::new( lsp::Position::new(0, 0), @@ -721,7 +721,7 @@ async fn test_collaborating_with_code_actions( changes: Some( [ ( - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), vec![lsp::TextEdit::new( lsp::Range::new( lsp::Position::new(1, 22), @@ -731,7 +731,7 @@ async fn test_collaborating_with_code_actions( )], ), ( - lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap(), vec![lsp::TextEdit::new( lsp::Range::new( lsp::Position::new(0, 0), @@ -949,14 +949,14 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T changes: Some( [ ( - lsp::Url::from_file_path(path!("/dir/one.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/dir/one.rs")).unwrap(), vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), "THREE".to_string(), )], ), ( - lsp::Url::from_file_path(path!("/dir/two.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/dir/two.rs")).unwrap(), vec![ lsp::TextEdit::new( lsp::Range::new( @@ -1574,7 +1574,7 @@ async fn test_on_input_format_from_host_to_guest( |params, _| async move { assert_eq!( params.text_document_position.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); assert_eq!( params.text_document_position.position, @@ -1717,7 +1717,7 @@ async fn test_on_input_format_from_guest_to_host( .set_request_handler::(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); assert_eq!( params.text_document_position.position, @@ -1901,7 +1901,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); let edits_made = task_edits_made.load(atomic::Ordering::Acquire); Ok(Some(vec![lsp::InlayHint { @@ -2151,7 +2151,7 @@ async fn test_inlay_hint_refresh_is_forwarded( async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); let other_hints = task_other_hints.load(atomic::Ordering::Acquire); let character = if other_hints { 0 } else { 2 }; @@ -2332,7 +2332,7 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); requests_made.fetch_add(1, atomic::Ordering::Release); Ok(vec![lsp::ColorInformation { @@ -2621,11 +2621,11 @@ async fn test_lsp_pull_diagnostics( let requests_made = closure_diagnostics_pulls_made.clone(); let diagnostics_pulls_result_ids = closure_diagnostics_pulls_result_ids.clone(); async move { - let message = if lsp::Url::from_file_path(path!("/a/main.rs")).unwrap() + let message = if lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap() == params.text_document.uri { expected_pull_diagnostic_main_message.to_string() - } else if lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap() + } else if lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap() == params.text_document.uri { expected_pull_diagnostic_lib_message.to_string() @@ -2717,7 +2717,7 @@ async fn test_lsp_pull_diagnostics( items: vec![ lsp::WorkspaceDocumentDiagnosticReport::Full( lsp::WorkspaceFullDocumentDiagnosticReport { - uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), version: None, full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport { @@ -2746,7 +2746,7 @@ async fn test_lsp_pull_diagnostics( ), lsp::WorkspaceDocumentDiagnosticReport::Full( lsp::WorkspaceFullDocumentDiagnosticReport { - uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(), version: None, full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport { @@ -2821,7 +2821,7 @@ async fn test_lsp_pull_diagnostics( fake_language_server.notify::( &lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), diagnostics: vec![lsp::Diagnostic { range: lsp::Range { start: lsp::Position { @@ -2842,7 +2842,7 @@ async fn test_lsp_pull_diagnostics( ); fake_language_server.notify::( &lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(), diagnostics: vec![lsp::Diagnostic { range: lsp::Range { start: lsp::Position { @@ -2870,7 +2870,7 @@ async fn test_lsp_pull_diagnostics( items: vec![ lsp::WorkspaceDocumentDiagnosticReport::Full( lsp::WorkspaceFullDocumentDiagnosticReport { - uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), version: None, full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport { @@ -2902,7 +2902,7 @@ async fn test_lsp_pull_diagnostics( ), lsp::WorkspaceDocumentDiagnosticReport::Full( lsp::WorkspaceFullDocumentDiagnosticReport { - uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(), version: None, full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport { @@ -3051,7 +3051,7 @@ async fn test_lsp_pull_diagnostics( lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport { items: vec![lsp::WorkspaceDocumentDiagnosticReport::Full( lsp::WorkspaceFullDocumentDiagnosticReport { - uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(), version: None, full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport { result_id: Some(format!( @@ -4040,7 +4040,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); assert_eq!(params.position, lsp::Position::new(0, 0)); Ok(Some(ExpandedMacro { @@ -4075,7 +4075,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); assert_eq!( params.position, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 5c732530480a14ab28e231aa0fae1b79ef2703fb..6bb2db05201ea464053a758b390e84ccdfc6527a 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4075,7 +4075,7 @@ async fn test_collaborating_with_diagnostics( .await; fake_language_server.notify::( &lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { severity: Some(lsp::DiagnosticSeverity::WARNING), @@ -4095,7 +4095,7 @@ async fn test_collaborating_with_diagnostics( .unwrap(); fake_language_server.notify::( &lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { severity: Some(lsp::DiagnosticSeverity::ERROR), @@ -4169,7 +4169,7 @@ async fn test_collaborating_with_diagnostics( // Simulate a language server reporting more errors for a file. fake_language_server.notify::( &lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(), version: None, diagnostics: vec![ lsp::Diagnostic { @@ -4265,7 +4265,7 @@ async fn test_collaborating_with_diagnostics( // Simulate a language server reporting no errors for a file. fake_language_server.notify::( &lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(), version: None, diagnostics: Vec::new(), }, @@ -4372,7 +4372,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering( for file_name in file_names { fake_language_server.notify::( &lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(), + uri: lsp::Uri::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { severity: Some(lsp::DiagnosticSeverity::WARNING), @@ -4838,7 +4838,7 @@ async fn test_definition( |_, _| async move { Ok(Some(lsp::GotoDefinitionResponse::Scalar( lsp::Location::new( - lsp::Url::from_file_path(path!("/root/dir-2/b.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/root/dir-2/b.rs")).unwrap(), lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), ), ))) @@ -4876,7 +4876,7 @@ async fn test_definition( |_, _| async move { Ok(Some(lsp::GotoDefinitionResponse::Scalar( lsp::Location::new( - lsp::Url::from_file_path(path!("/root/dir-2/b.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/root/dir-2/b.rs")).unwrap(), lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)), ), ))) @@ -4914,7 +4914,7 @@ async fn test_definition( ); Ok(Some(lsp::GotoDefinitionResponse::Scalar( lsp::Location::new( - lsp::Url::from_file_path(path!("/root/dir-2/c.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/root/dir-2/c.rs")).unwrap(), lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)), ), ))) @@ -5049,15 +5049,15 @@ async fn test_references( lsp_response_tx .unbounded_send(Ok(Some(vec![ lsp::Location { - uri: lsp::Url::from_file_path(path!("/root/dir-1/two.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/root/dir-1/two.rs")).unwrap(), range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)), }, lsp::Location { - uri: lsp::Url::from_file_path(path!("/root/dir-1/two.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/root/dir-1/two.rs")).unwrap(), range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)), }, lsp::Location { - uri: lsp::Url::from_file_path(path!("/root/dir-2/three.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/root/dir-2/three.rs")).unwrap(), range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)), }, ]))) @@ -5625,7 +5625,7 @@ async fn test_project_symbols( lsp::SymbolInformation { name: "TWO".into(), location: lsp::Location { - uri: lsp::Url::from_file_path(path!("/code/crate-2/two.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/code/crate-2/two.rs")).unwrap(), range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), }, kind: lsp::SymbolKind::CONSTANT, @@ -5737,7 +5737,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( |_, _| async move { Ok(Some(lsp::GotoDefinitionResponse::Scalar( lsp::Location::new( - lsp::Url::from_file_path(path!("/root/b.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/root/b.rs")).unwrap(), lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), ), ))) diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index ac5c4c54ca570bf5545505419cb20a021ca97202..bfe05c4a1d600bb280d3821350204d0b2d0d6e08 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -1101,7 +1101,7 @@ impl RandomizedTest for ProjectCollaborationTest { files .into_iter() .map(|file| lsp::Location { - uri: lsp::Url::from_file_path(file).unwrap(), + uri: lsp::Uri::from_file_path(file).unwrap(), range: Default::default(), }) .collect(), diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index b7d8423fd7d4d601250172a5789cbe83620849af..d0a57735ab5a0342b245aa8db72e6b021b3943de 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -197,7 +197,7 @@ impl Status { } struct RegisteredBuffer { - uri: lsp::Url, + uri: lsp::Uri, language_id: String, snapshot: BufferSnapshot, snapshot_version: i32, @@ -1108,9 +1108,9 @@ fn id_for_language(language: Option<&Arc>) -> String { .unwrap_or_else(|| "plaintext".to_string()) } -fn uri_for_buffer(buffer: &Entity, cx: &App) -> Result { +fn uri_for_buffer(buffer: &Entity, cx: &App) -> Result { if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { - lsp::Url::from_file_path(file.abs_path(cx)) + lsp::Uri::from_file_path(file.abs_path(cx)) } else { format!("buffer://{}", buffer.entity_id()) .parse() @@ -1201,7 +1201,7 @@ mod tests { let (copilot, mut lsp) = Copilot::fake(cx); let buffer_1 = cx.new(|cx| Buffer::local("Hello", cx)); - let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64()) + let buffer_1_uri: lsp::Uri = format!("buffer://{}", buffer_1.entity_id().as_u64()) .parse() .unwrap(); copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx)); @@ -1219,7 +1219,7 @@ mod tests { ); let buffer_2 = cx.new(|cx| Buffer::local("Goodbye", cx)); - let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64()) + let buffer_2_uri: lsp::Uri = format!("buffer://{}", buffer_2.entity_id().as_u64()) .parse() .unwrap(); copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx)); @@ -1270,7 +1270,7 @@ mod tests { text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri), } ); - let buffer_1_uri = lsp::Url::from_file_path(path!("/root/child/buffer-1")).unwrap(); + let buffer_1_uri = lsp::Uri::from_file_path(path!("/root/child/buffer-1")).unwrap(); assert_eq!( lsp.receive_notification::() .await, diff --git a/crates/copilot/src/request.rs b/crates/copilot/src/request.rs index 0deabe16d15c4a502b278c4a631720094ad18af7..85d6254dc060824a9b2686e8f53090fccb39980e 100644 --- a/crates/copilot/src/request.rs +++ b/crates/copilot/src/request.rs @@ -102,7 +102,7 @@ pub struct GetCompletionsDocument { pub tab_size: u32, pub indent_size: u32, pub insert_spaces: bool, - pub uri: lsp::Url, + pub uri: lsp::Uri, pub relative_path: String, pub position: lsp::Position, pub version: usize, diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 4a544f9ea718f0df037fb3012c48efec1c804b43..fdca32520d1e08d562ac6f533968c146b5ec0673 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -24,6 +24,7 @@ use settings::SettingsStore; use std::{ env, path::{Path, PathBuf}, + str::FromStr, }; use unindent::Unindent as _; use util::{RandomCharIter, path, post_inc}; @@ -70,7 +71,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) { let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = &mut VisualTestContext::from_window(*window, cx); let workspace = window.root(cx).unwrap(); - let uri = lsp::Url::from_file_path(path!("/test/main.rs")).unwrap(); + let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap(); // Create some diagnostics lsp_store.update(cx, |lsp_store, cx| { @@ -167,7 +168,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) { .update_diagnostics( language_server_id, lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/test/consts.rs")).unwrap(), diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new( lsp::Position::new(0, 15), @@ -243,7 +244,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) { .update_diagnostics( language_server_id, lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/test/consts.rs")).unwrap(), diagnostics: vec![ lsp::Diagnostic { range: lsp::Range::new( @@ -356,14 +357,14 @@ async fn test_diagnostics_with_folds(cx: &mut TestAppContext) { .update_diagnostics( server_id_1, lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(), diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(4, 0), lsp::Position::new(4, 4)), severity: Some(lsp::DiagnosticSeverity::WARNING), message: "no method `tset`".to_string(), related_information: Some(vec![lsp::DiagnosticRelatedInformation { location: lsp::Location::new( - lsp::Url::from_file_path(path!("/test/main.js")).unwrap(), + lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(), lsp::Range::new( lsp::Position::new(0, 9), lsp::Position::new(0, 13), @@ -465,7 +466,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { .update_diagnostics( server_id_1, lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(), diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)), severity: Some(lsp::DiagnosticSeverity::WARNING), @@ -509,7 +510,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { .update_diagnostics( server_id_2, lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(), diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 1)), severity: Some(lsp::DiagnosticSeverity::ERROR), @@ -552,7 +553,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { .update_diagnostics( server_id_1, lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(), diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 1)), severity: Some(lsp::DiagnosticSeverity::WARNING), @@ -571,7 +572,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { .update_diagnostics( server_id_2, lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/test/main.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap(), diagnostics: vec![], version: None, }, @@ -608,7 +609,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { .update_diagnostics( server_id_2, lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(), diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(3, 0), lsp::Position::new(3, 1)), severity: Some(lsp::DiagnosticSeverity::WARNING), @@ -745,8 +746,8 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng .update_diagnostics( server_id, lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| { - lsp::Url::parse("file:///test/fallback.rs").unwrap() + uri: lsp::Uri::from_file_path(&path).unwrap_or_else(|_| { + lsp::Uri::from_str("file:///test/fallback.rs").unwrap() }), diagnostics: diagnostics.clone(), version: None, @@ -934,8 +935,8 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S .update_diagnostics( server_id, lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| { - lsp::Url::parse("file:///test/fallback.rs").unwrap() + uri: lsp::Uri::from_file_path(&path).unwrap_or_else(|_| { + lsp::Uri::from_str("file:///test/fallback.rs").unwrap() }), diagnostics: diagnostics.clone(), version: None, @@ -985,7 +986,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) .update_diagnostics( LanguageServerId(0), lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new( @@ -1028,7 +1029,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) .update_diagnostics( LanguageServerId(0), lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(), version: None, diagnostics: Vec::new(), }, @@ -1078,7 +1079,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { .update_diagnostics( LanguageServerId(0), lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(), version: None, diagnostics: vec![ lsp::Diagnostic { @@ -1246,7 +1247,7 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) { lsp_store.update_diagnostics( LanguageServerId(0), lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)), @@ -1299,7 +1300,7 @@ async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) lsp_store.update_diagnostics( LanguageServerId(0), lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/root/dir/file.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range, @@ -1376,7 +1377,7 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) { let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = &mut VisualTestContext::from_window(*window, cx); let workspace = window.root(cx).unwrap(); - let uri = lsp::Url::from_file_path(path!("/root/main.js")).unwrap(); + let uri = lsp::Uri::from_file_path(path!("/root/main.js")).unwrap(); // Create diagnostics with code fields lsp_store.update(cx, |lsp_store, cx| { @@ -1460,7 +1461,7 @@ async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) { .update_diagnostics( LanguageServerId(0), lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(), version: None, diagnostics: vec![ lsp::Diagnostic { @@ -1673,7 +1674,7 @@ fn random_lsp_diagnostic( ); related_info.push(lsp::DiagnosticRelatedInformation { - location: lsp::Location::new(lsp::Url::from_file_path(path).unwrap(), info_range), + location: lsp::Location::new(lsp::Uri::from_file_path(path).unwrap(), info_range), message: format!("related info {i} for diagnostic {unique_id}"), }); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index dfef8a92f064e3c8785f92d26e058fc43519dca2..10ebae8e27a07115de1e202187f491026bd7f503 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -9909,7 +9909,7 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/file.rs")).unwrap() + lsp::Uri::from_file_path(path!("/file.rs")).unwrap() ); assert_eq!(params.options.tab_size, 4); Ok(Some(vec![lsp::TextEdit::new( @@ -9952,7 +9952,7 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/file.rs")).unwrap() + lsp::Uri::from_file_path(path!("/file.rs")).unwrap() ); futures::future::pending::<()>().await; unreachable!() @@ -10000,7 +10000,7 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { .set_request_handler::(move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/file.rs")).unwrap() + lsp::Uri::from_file_path(path!("/file.rs")).unwrap() ); assert_eq!(params.options.tab_size, 8); Ok(Some(vec![])) @@ -10548,7 +10548,7 @@ async fn test_range_format_on_save_success(cx: &mut TestAppContext) { .set_request_handler::(move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/file.rs")).unwrap() + lsp::Uri::from_file_path(path!("/file.rs")).unwrap() ); assert_eq!(params.options.tab_size, 4); Ok(Some(vec![lsp::TextEdit::new( @@ -10581,7 +10581,7 @@ async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) { move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/file.rs")).unwrap() + lsp::Uri::from_file_path(path!("/file.rs")).unwrap() ); futures::future::pending::<()>().await; unreachable!() @@ -10674,7 +10674,7 @@ async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppC .set_request_handler::(move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/file.rs")).unwrap() + lsp::Uri::from_file_path(path!("/file.rs")).unwrap() ); assert_eq!(params.options.tab_size, 8); Ok(Some(Vec::new())) @@ -10761,7 +10761,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { .set_request_handler::(move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/file.rs")).unwrap() + lsp::Uri::from_file_path(path!("/file.rs")).unwrap() ); assert_eq!(params.options.tab_size, 4); Ok(Some(vec![lsp::TextEdit::new( @@ -10786,7 +10786,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/file.rs")).unwrap() + lsp::Uri::from_file_path(path!("/file.rs")).unwrap() ); futures::future::pending::<()>().await; unreachable!() @@ -10882,7 +10882,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { params.context.only, Some(vec!["code-action-1".into(), "code-action-2".into()]) ); - let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap(); + let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap(); Ok(Some(vec![ lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { kind: Some("code-action-1".into()), @@ -10942,7 +10942,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { edit: lsp::WorkspaceEdit { changes: Some( [( - lsp::Url::from_file_path(path!("/file.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/file.rs")).unwrap(), vec![lsp::TextEdit { range: lsp::Range::new( lsp::Position::new(0, 0), @@ -11153,7 +11153,7 @@ async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) { .set_request_handler::(move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/file.ts")).unwrap() + lsp::Uri::from_file_path(path!("/file.ts")).unwrap() ); Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( lsp::CodeAction { @@ -11201,7 +11201,7 @@ async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) { move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/file.ts")).unwrap() + lsp::Uri::from_file_path(path!("/file.ts")).unwrap() ); futures::future::pending::<()>().await; unreachable!() @@ -15478,7 +15478,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu .update_diagnostics( LanguageServerId(0), lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(), version: None, diagnostics: vec![ lsp::Diagnostic { @@ -15874,7 +15874,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) { |params, _| async move { assert_eq!( params.text_document_position.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); assert_eq!( params.text_document_position.position, @@ -16399,7 +16399,7 @@ async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) { edit: Some(lsp::WorkspaceEdit { changes: Some( [( - lsp::Url::from_file_path(path!("/file.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/file.rs")).unwrap(), vec![lsp::TextEdit { range: lsp::Range::new( lsp::Position::new(5, 4), @@ -22067,7 +22067,7 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex edit: lsp::WorkspaceEdit { changes: Some( [( - lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(), + lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(), vec![lsp::TextEdit { range: lsp::Range::new( lsp::Position::new(0, 0), @@ -24039,7 +24039,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) { let result_id = Some(new_result_id.to_string()); assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/a/first.rs")).unwrap() + lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap() ); async move { Ok(lsp::DocumentDiagnosticReportResult::Report( @@ -24254,7 +24254,7 @@ async fn test_document_colors(cx: &mut TestAppContext) { async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/a/first.rs")).unwrap() + lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap() ); requests_made.fetch_add(1, atomic::Ordering::Release); Ok(vec![ diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index dbf5ac95b78433c9a67da110e804a8973e51dee1..c1b0a7640c155fff02f0b778e8996a9b68ea452e 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -1339,7 +1339,7 @@ pub mod tests { let i = task_lsp_request_count.fetch_add(1, Ordering::Release) + 1; assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(file_with_hints).unwrap(), + lsp::Uri::from_file_path(file_with_hints).unwrap(), ); Ok(Some(vec![lsp::InlayHint { position: lsp::Position::new(0, i), @@ -1449,7 +1449,7 @@ pub mod tests { async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(file_with_hints).unwrap(), + lsp::Uri::from_file_path(file_with_hints).unwrap(), ); let current_call_id = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); @@ -1594,7 +1594,7 @@ pub mod tests { "Rust" => { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")) + lsp::Uri::from_file_path(path!("/a/main.rs")) .unwrap(), ); rs_lsp_request_count.fetch_add(1, Ordering::Release) @@ -1603,7 +1603,7 @@ pub mod tests { "Markdown" => { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/a/other.md")) + lsp::Uri::from_file_path(path!("/a/other.md")) .unwrap(), ); md_lsp_request_count.fetch_add(1, Ordering::Release) @@ -1789,7 +1789,7 @@ pub mod tests { async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(file_with_hints).unwrap(), + lsp::Uri::from_file_path(file_with_hints).unwrap(), ); Ok(Some(vec![ lsp::InlayHint { @@ -2127,7 +2127,7 @@ pub mod tests { let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1; assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(file_with_hints).unwrap(), + lsp::Uri::from_file_path(file_with_hints).unwrap(), ); Ok(Some(vec![lsp::InlayHint { position: lsp::Position::new(0, i), @@ -2290,7 +2290,7 @@ pub mod tests { async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); task_lsp_request_ranges.lock().push(params.range); @@ -2633,11 +2633,11 @@ pub mod tests { let task_editor_edited = Arc::clone(&closure_editor_edited); async move { let hint_text = if params.text_document.uri - == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap() + == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap() { "main hint" } else if params.text_document.uri - == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap() + == lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap() { "other hint" } else { @@ -2944,11 +2944,11 @@ pub mod tests { let task_editor_edited = Arc::clone(&closure_editor_edited); async move { let hint_text = if params.text_document.uri - == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap() + == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap() { "main hint" } else if params.text_document.uri - == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap() + == lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap() { "other hint" } else { @@ -3116,7 +3116,7 @@ pub mod tests { async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); let query_start = params.range.start; Ok(Some(vec![lsp::InlayHint { @@ -3188,7 +3188,7 @@ pub mod tests { async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(file_with_hints).unwrap(), + lsp::Uri::from_file_path(file_with_hints).unwrap(), ); let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1; @@ -3351,7 +3351,7 @@ pub mod tests { move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), ); Ok(Some( serde_json::from_value(json!([ diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 3f78fa2f3e9bcd592ba5d2a9f29c42967a27c126..79935340358662350dbbc640d96f5d60ec8aaf6b 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -29,7 +29,7 @@ pub struct EditorLspTestContext { pub cx: EditorTestContext, pub lsp: lsp::FakeLanguageServer, pub workspace: Entity, - pub buffer_lsp_url: lsp::Url, + pub buffer_lsp_url: lsp::Uri, } pub(crate) fn rust_lang() -> Arc { @@ -189,7 +189,7 @@ impl EditorLspTestContext { }, lsp, workspace, - buffer_lsp_url: lsp::Url::from_file_path(root.join("dir").join(file_name)).unwrap(), + buffer_lsp_url: lsp::Uri::from_file_path(root.join("dir").join(file_name)).unwrap(), } } @@ -358,7 +358,7 @@ impl EditorLspTestContext { where T: 'static + request::Request, T::Params: 'static + Send, - F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncApp) -> Fut, + F: 'static + Send + FnMut(lsp::Uri, T::Params, gpui::AsyncApp) -> Fut, Fut: 'static + Future>, { let url = self.buffer_lsp_url.clone(); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4ddc2b3018614f592beeb55aaa2cc9ed46b5522c..1a1d9fb4a7dc3a3d2a847cee3661361343a6871e 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -202,7 +202,7 @@ pub struct Diagnostic { pub source: Option, /// A machine-readable code that identifies this diagnostic. pub code: Option, - pub code_description: Option, + pub code_description: Option, /// Whether this diagnostic is a hint, warning, or error. pub severity: DiagnosticSeverity, /// The human-readable message associated with this diagnostic. diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 3be189cea08f247f97d05e6b9714f07d17289a8a..0d5a8e916c8712733dcc7a26faa984453cdd30fd 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -431,7 +431,7 @@ pub fn deserialize_diagnostics( code: diagnostic.code.map(lsp::NumberOrString::from_string), code_description: diagnostic .code_description - .and_then(|s| lsp::Url::parse(&s).ok()), + .and_then(|s| lsp::Uri::from_str(&s).ok()), is_primary: diagnostic.is_primary, is_disk_based: diagnostic.is_disk_based, is_unnecessary: diagnostic.is_unnecessary, diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 3e8dce756be42ca59d88d86404518e65cf54ff7e..a5acc0043298cab49264dac75d51e6a69e5149fe 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -1058,7 +1058,7 @@ mod tests { #[gpui::test] async fn test_process_rust_diagnostics() { let mut params = lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/a")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/a")).unwrap(), version: None, diagnostics: vec![ // no newlines diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 1ad89db017bc9a0c6f9009cba8ad22f94a31c65d..943bdab5ff817da7819590679d19bbe522b47835 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -100,8 +100,8 @@ pub struct LanguageServer { io_tasks: Mutex>, Task>)>>, output_done_rx: Mutex>, server: Arc>>, - workspace_folders: Option>>>, - root_uri: Url, + workspace_folders: Option>>>, + root_uri: Uri, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -310,7 +310,7 @@ impl LanguageServer { binary: LanguageServerBinary, root_path: &Path, code_action_kinds: Option>, - workspace_folders: Option>>>, + workspace_folders: Option>>>, cx: &mut AsyncApp, ) -> Result { let working_dir = if root_path.is_dir() { @@ -318,7 +318,7 @@ impl LanguageServer { } else { root_path.parent().unwrap_or_else(|| Path::new("/")) }; - let root_uri = Url::from_file_path(&working_dir) + let root_uri = Uri::from_file_path(&working_dir) .map_err(|()| anyhow!("{working_dir:?} is not a valid URI"))?; log::info!( @@ -384,8 +384,8 @@ impl LanguageServer { server: Option, code_action_kinds: Option>, binary: LanguageServerBinary, - root_uri: Url, - workspace_folders: Option>>>, + root_uri: Uri, + workspace_folders: Option>>>, cx: &mut AsyncApp, on_unhandled_notification: F, ) -> Self @@ -1350,7 +1350,7 @@ impl LanguageServer { } /// Add new workspace folder to the list. - pub fn add_workspace_folder(&self, uri: Url) { + pub fn add_workspace_folder(&self, uri: Uri) { if self .capabilities() .workspace @@ -1385,7 +1385,7 @@ impl LanguageServer { } /// Remove existing workspace folder from the list. - pub fn remove_workspace_folder(&self, uri: Url) { + pub fn remove_workspace_folder(&self, uri: Uri) { if self .capabilities() .workspace @@ -1417,7 +1417,7 @@ impl LanguageServer { self.notify::(¶ms).ok(); } } - pub fn set_workspace_folders(&self, folders: BTreeSet) { + pub fn set_workspace_folders(&self, folders: BTreeSet) { let Some(workspace_folders) = self.workspace_folders.as_ref() else { return; }; @@ -1450,7 +1450,7 @@ impl LanguageServer { } } - pub fn workspace_folders(&self) -> BTreeSet { + pub fn workspace_folders(&self) -> BTreeSet { self.workspace_folders.as_ref().map_or_else( || BTreeSet::from_iter([self.root_uri.clone()]), |folders| folders.lock().clone(), @@ -1459,7 +1459,7 @@ impl LanguageServer { pub fn register_buffer( &self, - uri: Url, + uri: Uri, language_id: String, version: i32, initial_text: String, @@ -1470,7 +1470,7 @@ impl LanguageServer { .ok(); } - pub fn unregister_buffer(&self, uri: Url) { + pub fn unregister_buffer(&self, uri: Uri) { self.notify::(&DidCloseTextDocumentParams { text_document: TextDocumentIdentifier::new(uri), }) @@ -1587,7 +1587,7 @@ impl FakeLanguageServer { let server_name = LanguageServerName(name.clone().into()); let process_name = Arc::from(name.as_str()); let root = Self::root_path(); - let workspace_folders: Arc>> = Default::default(); + let workspace_folders: Arc>> = Default::default(); let mut server = LanguageServer::new_internal( server_id, server_name.clone(), @@ -1657,13 +1657,13 @@ impl FakeLanguageServer { (server, fake) } #[cfg(target_os = "windows")] - fn root_path() -> Url { - Url::from_file_path("C:/").unwrap() + fn root_path() -> Uri { + Uri::from_file_path("C:/").unwrap() } #[cfg(not(target_os = "windows"))] - fn root_path() -> Url { - Url::from_file_path("/").unwrap() + fn root_path() -> Uri { + Uri::from_file_path("/").unwrap() } } @@ -1865,7 +1865,7 @@ mod tests { server .notify::(&DidOpenTextDocumentParams { text_document: TextDocumentItem::new( - Url::from_str("file://a/b").unwrap(), + Uri::from_str("file://a/b").unwrap(), "rust".to_string(), 0, "".to_string(), @@ -1886,7 +1886,7 @@ mod tests { message: "ok".to_string(), }); fake.notify::(&PublishDiagnosticsParams { - uri: Url::from_str("file://b/c").unwrap(), + uri: Uri::from_str("file://b/c").unwrap(), version: Some(5), diagnostics: vec![], }); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index ce7a871d1a63107ab4908dccea68dd41d73a319f..a960e1183dd46537ef3aee829cd9753b28001480 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -50,8 +50,8 @@ pub fn lsp_formatting_options(settings: &LanguageSettings) -> lsp::FormattingOpt } } -pub fn file_path_to_lsp_url(path: &Path) -> Result { - match lsp::Url::from_file_path(path) { +pub fn file_path_to_lsp_url(path: &Path) -> Result { + match lsp::Uri::from_file_path(path) { Ok(url) => Ok(url), Err(()) => anyhow::bail!("Invalid file path provided to LSP request: {path:?}"), } @@ -3135,7 +3135,7 @@ impl InlayHints { Some(((uri, range), server_id)) => Some(( LanguageServerId(server_id as usize), lsp::Location { - uri: lsp::Url::parse(&uri) + uri: lsp::Uri::from_str(&uri) .context("invalid uri in hint part {part:?}")?, range: lsp::Range::new( point_to_lsp(PointUtf16::new( @@ -3733,7 +3733,7 @@ impl GetDocumentDiagnostics { .filter_map(|diagnostics| { Some(LspPullDiagnostics::Response { server_id: LanguageServerId::from_proto(diagnostics.server_id), - uri: lsp::Url::from_str(diagnostics.uri.as_str()).log_err()?, + uri: lsp::Uri::from_str(diagnostics.uri.as_str()).log_err()?, diagnostics: if diagnostics.changed { PulledDiagnostics::Unchanged { result_id: diagnostics.result_id?, @@ -3788,7 +3788,7 @@ impl GetDocumentDiagnostics { start: point_to_lsp(PointUtf16::new(start.row, start.column)), end: point_to_lsp(PointUtf16::new(end.row, end.column)), }, - uri: lsp::Url::parse(&info.location_url.unwrap()).unwrap(), + uri: lsp::Uri::from_str(&info.location_url.unwrap()).unwrap(), }, message: info.message, } @@ -3821,7 +3821,7 @@ impl GetDocumentDiagnostics { code_description: diagnostic .code_description .map(|code_description| CodeDescription { - href: Some(lsp::Url::parse(&code_description).unwrap()), + href: Some(lsp::Uri::from_str(&code_description).unwrap()), }), related_information: Some(related_information), tags: Some(tags), @@ -3961,7 +3961,7 @@ pub struct WorkspaceLspPullDiagnostics { } fn process_full_workspace_diagnostics_report( - diagnostics: &mut HashMap, + diagnostics: &mut HashMap, server_id: LanguageServerId, report: lsp::WorkspaceFullDocumentDiagnosticReport, ) { @@ -3984,7 +3984,7 @@ fn process_full_workspace_diagnostics_report( } fn process_unchanged_workspace_diagnostics_report( - diagnostics: &mut HashMap, + diagnostics: &mut HashMap, server_id: LanguageServerId, report: lsp::WorkspaceUnchangedDocumentDiagnosticReport, ) { @@ -4343,9 +4343,9 @@ impl LspCommand for GetDocumentColor { } fn process_related_documents( - diagnostics: &mut HashMap, + diagnostics: &mut HashMap, server_id: LanguageServerId, - documents: impl IntoIterator, + documents: impl IntoIterator, ) { for (url, report_kind) in documents { match report_kind { @@ -4360,9 +4360,9 @@ fn process_related_documents( } fn process_unchanged_diagnostics_report( - diagnostics: &mut HashMap, + diagnostics: &mut HashMap, server_id: LanguageServerId, - uri: lsp::Url, + uri: lsp::Uri, report: lsp::UnchangedDocumentDiagnosticReport, ) { let result_id = report.result_id; @@ -4404,9 +4404,9 @@ fn process_unchanged_diagnostics_report( } fn process_full_diagnostics_report( - diagnostics: &mut HashMap, + diagnostics: &mut HashMap, server_id: LanguageServerId, - uri: lsp::Url, + uri: lsp::Uri, report: lsp::FullDocumentDiagnosticReport, ) { let result_id = report.result_id; @@ -4540,7 +4540,7 @@ mod tests { fn test_related_information() { let related_info = lsp::DiagnosticRelatedInformation { location: lsp::Location { - uri: lsp::Url::parse("file:///test.rs").unwrap(), + uri: lsp::Uri::from_str("file:///test.rs").unwrap(), range: lsp::Range { start: lsp::Position::new(1, 1), end: lsp::Position::new(1, 5), diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index b4c7c0bc37fc0409570ece3c5e3df00b1b1cd89f..3f04f38607415e9678944c5546aa84abf4446597 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -79,7 +79,7 @@ use lsp::{ LSP_REQUEST_TIMEOUT, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind, - TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, + TextDocumentSyncSaveOptions, TextEdit, Uri, WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; @@ -114,7 +114,7 @@ use std::{ }; use sum_tree::Dimensions; use text::{Anchor, BufferId, LineEnding, OffsetRangeExt}; -use url::Url; + use util::{ ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into, paths::{PathExt, SanitizedPath}, @@ -314,7 +314,7 @@ impl LocalLspStore { true, cx, ); - let pending_workspace_folders: Arc>> = Default::default(); + let pending_workspace_folders: Arc>> = Default::default(); let pending_server = cx.spawn({ let adapter = adapter.clone(); @@ -2405,7 +2405,7 @@ impl LocalLspStore { { let uri = - Url::from_file_path(worktree.read(cx).abs_path().join(&path.path)); + Uri::from_file_path(worktree.read(cx).abs_path().join(&path.path)); let server_id = self.get_or_insert_language_server( &worktree, @@ -2565,7 +2565,7 @@ impl LocalLspStore { None => return, }; - let Ok(file_url) = lsp::Url::from_file_path(old_path.as_path()) else { + let Ok(file_url) = lsp::Uri::from_file_path(old_path.as_path()) else { debug_panic!( "`{}` is not parseable as an URI", old_path.to_string_lossy() @@ -2578,7 +2578,7 @@ impl LocalLspStore { pub(crate) fn unregister_buffer_from_language_servers( &mut self, buffer: &Entity, - file_url: &lsp::Url, + file_url: &lsp::Uri, cx: &mut App, ) { buffer.update(cx, |buffer, cx| { @@ -4694,7 +4694,7 @@ impl LspStore { for node in nodes { let server_id = node.server_id_or_init(|disposition| { let path = &disposition.path; - let uri = Url::from_file_path(worktree_root.join(&path.path)); + let uri = Uri::from_file_path(worktree_root.join(&path.path)); let key = LanguageServerSeed { worktree_id, name: disposition.server_name.clone(), @@ -6578,7 +6578,7 @@ impl LspStore { File::from_dyn(buffer.file()) .and_then(|file| { let abs_path = file.as_local()?.abs_path(cx); - lsp::Url::from_file_path(abs_path).ok() + lsp::Uri::from_file_path(abs_path).ok() }) .is_none_or(|buffer_uri| { unchanged_buffers.contains(&buffer_uri) @@ -7179,7 +7179,7 @@ impl LspStore { let buffer = buffer.read(cx); let file = File::from_dyn(buffer.file())?; let abs_path = file.as_local()?.abs_path(cx); - let uri = lsp::Url::from_file_path(abs_path).unwrap(); + let uri = lsp::Uri::from_file_path(abs_path).unwrap(); let next_snapshot = buffer.text_snapshot(); for language_server in language_servers { let language_server = language_server.clone(); @@ -7816,7 +7816,7 @@ impl LspStore { }; let symbol_abs_path = resolve_path(&worktree_abs_path, &symbol.path.path); - let symbol_uri = if let Ok(uri) = lsp::Url::from_file_path(symbol_abs_path) { + let symbol_uri = if let Ok(uri) = lsp::Uri::from_file_path(symbol_abs_path) { uri } else { return Task::ready(Err(anyhow!("invalid symbol path"))); @@ -7830,14 +7830,14 @@ impl LspStore { pub(crate) fn open_local_buffer_via_lsp( &mut self, - mut abs_path: lsp::Url, + abs_path: lsp::Uri, language_server_id: LanguageServerId, cx: &mut Context, ) -> Task>> { cx.spawn(async move |lsp_store, cx| { // Escape percent-encoded string. let current_scheme = abs_path.scheme().to_owned(); - let _ = abs_path.set_scheme("file"); + // Uri is immutable, so we can't modify the scheme let abs_path = abs_path .to_file_path() @@ -9230,8 +9230,12 @@ impl LspStore { maybe!({ let local_store = self.as_local()?; - let old_uri = lsp::Url::from_file_path(old_path).ok().map(String::from)?; - let new_uri = lsp::Url::from_file_path(new_path).ok().map(String::from)?; + let old_uri = lsp::Uri::from_file_path(old_path) + .ok() + .map(|uri| uri.to_string())?; + let new_uri = lsp::Uri::from_file_path(new_path) + .ok() + .map(|uri| uri.to_string())?; for language_server in local_store.language_servers_for_worktree(worktree_id) { let Some(filter) = local_store @@ -9264,8 +9268,12 @@ impl LspStore { is_dir: bool, cx: AsyncApp, ) -> Task { - let old_uri = lsp::Url::from_file_path(old_path).ok().map(String::from); - let new_uri = lsp::Url::from_file_path(new_path).ok().map(String::from); + let old_uri = lsp::Uri::from_file_path(old_path) + .ok() + .map(|uri| uri.to_string()); + let new_uri = lsp::Uri::from_file_path(new_path) + .ok() + .map(|uri| uri.to_string()); cx.spawn(async move |cx| { let mut tasks = vec![]; this.update(cx, |this, cx| { @@ -10878,7 +10886,7 @@ impl LspStore { language_server: Arc, server_id: LanguageServerId, key: LanguageServerSeed, - workspace_folders: Arc>>, + workspace_folders: Arc>>, cx: &mut Context, ) { let Some(local) = self.as_local_mut() else { @@ -11038,7 +11046,7 @@ impl LspStore { let snapshot = versions.last().unwrap(); let version = snapshot.version; let initial_snapshot = &snapshot.snapshot; - let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + let uri = lsp::Uri::from_file_path(file.abs_path(cx)).unwrap(); language_server.register_buffer( uri, adapter.language_id(&language.name()), @@ -11277,7 +11285,7 @@ impl LspStore { PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED, }; Some(lsp::FileEvent { - uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(), + uri: lsp::Uri::from_file_path(abs_path.join(path)).unwrap(), typ, }) }) @@ -11689,7 +11697,7 @@ impl LspStore { File::from_dyn(buffer.file()) .and_then(|file| { let abs_path = file.as_local()?.abs_path(cx); - lsp::Url::from_file_path(abs_path).ok() + lsp::Uri::from_file_path(abs_path).ok() }) .is_none_or(|buffer_uri| { unchanged_buffers.contains(&buffer_uri) @@ -12821,7 +12829,7 @@ pub enum LanguageServerState { Starting { startup: Task>>, /// List of language servers that will be added to the workspace once it's initialization completes. - pending_workspace_folders: Arc>>, + pending_workspace_folders: Arc>>, }, Running { @@ -12833,7 +12841,7 @@ pub enum LanguageServerState { } impl LanguageServerState { - fn add_workspace_folder(&self, uri: Url) { + fn add_workspace_folder(&self, uri: Uri) { match self { LanguageServerState::Starting { pending_workspace_folders, @@ -12846,7 +12854,7 @@ impl LanguageServerState { } } } - fn _remove_workspace_folder(&self, uri: Url) { + fn _remove_workspace_folder(&self, uri: Uri) { match self { LanguageServerState::Starting { pending_workspace_folders, diff --git a/crates/project/src/lsp_store/lsp_ext_command.rs b/crates/project/src/lsp_store/lsp_ext_command.rs index 1c969f8114eb7647e7c109baf2a7b70339997b41..0263946b25ed58969a3a7a98a9f537ce81d86ab1 100644 --- a/crates/project/src/lsp_store/lsp_ext_command.rs +++ b/crates/project/src/lsp_store/lsp_ext_command.rs @@ -213,7 +213,7 @@ impl LspCommand for OpenDocs { ) -> Result { Ok(OpenDocsParams { text_document: lsp::TextDocumentIdentifier { - uri: lsp::Url::from_file_path(path).unwrap(), + uri: lsp::Uri::from_file_path(path).unwrap(), }, position: point_to_lsp(self.position), }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8c289c935cd2bc4ebb919d171f0a9e4f0334b334..74ad08570a996a2dc9fc07bfb616f0edc0085b9f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -930,7 +930,7 @@ pub enum LspPullDiagnostics { /// The id of the language server that produced diagnostics. server_id: LanguageServerId, /// URI of the resource, - uri: lsp::Url, + uri: lsp::Uri, /// The diagnostics produced by this language server. diagnostics: PulledDiagnostics, }, @@ -3599,7 +3599,7 @@ impl Project { pub fn open_local_buffer_via_lsp( &mut self, - abs_path: lsp::Url, + abs_path: lsp::Uri, language_server_id: LanguageServerId, cx: &mut Context, ) -> Task>> { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 96f891d9c380fe6feec490627cd782955c833eda..a07f94fb737745b22bf6eaf685e1a4f2874a4dae 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -18,7 +18,6 @@ use git::{ }; use git2::RepositoryInitOptions; use gpui::{App, BackgroundExecutor, SemanticVersion, UpdateGlobal}; -use http_client::Url; use itertools::Itertools; use language::{ Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter, @@ -29,7 +28,7 @@ use language::{ }; use lsp::{ DiagnosticSeverity, DocumentChanges, FileOperationFilter, NumberOrString, TextDocumentEdit, - WillRenameFiles, notification::DidRenameFiles, + Uri, WillRenameFiles, notification::DidRenameFiles, }; use parking_lot::Mutex; use paths::{config_dir, tasks_file}; @@ -701,7 +700,7 @@ async fn test_running_multiple_instances_of_a_single_server_in_one_worktree( assert_eq!( server.workspace_folders(), BTreeSet::from_iter( - [Url::from_file_path(path!("/the-root/project-a")).unwrap()].into_iter() + [Uri::from_file_path(path!("/the-root/project-a")).unwrap()].into_iter() ) ); @@ -891,7 +890,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::TextDocumentItem { - uri: lsp::Url::from_file_path(path!("/dir/test.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/dir/test.rs")).unwrap(), version: 0, text: "const A: i32 = 1;".to_string(), language_id: "rust".to_string(), @@ -921,7 +920,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::VersionedTextDocumentIdentifier::new( - lsp::Url::from_file_path(path!("/dir/test.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/dir/test.rs")).unwrap(), 1 ) ); @@ -942,7 +941,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::TextDocumentItem { - uri: lsp::Url::from_file_path(path!("/dir/package.json")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/dir/package.json")).unwrap(), version: 0, text: "{\"a\": 1}".to_string(), language_id: "json".to_string(), @@ -992,7 +991,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::VersionedTextDocumentIdentifier::new( - lsp::Url::from_file_path(path!("/dir/test2.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/dir/test2.rs")).unwrap(), 1 ) ); @@ -1008,7 +1007,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path(path!("/dir/Cargo.toml")).unwrap() + lsp::Uri::from_file_path(path!("/dir/Cargo.toml")).unwrap() ) ); assert_eq!( @@ -1017,7 +1016,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path(path!("/dir/Cargo.toml")).unwrap() + lsp::Uri::from_file_path(path!("/dir/Cargo.toml")).unwrap() ) ); @@ -1034,7 +1033,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .receive_notification::() .await .text_document, - lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path!("/dir/test2.rs")).unwrap()), + lsp::TextDocumentIdentifier::new(lsp::Uri::from_file_path(path!("/dir/test2.rs")).unwrap()), ); assert_eq!( fake_rust_server @@ -1042,7 +1041,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::TextDocumentItem { - uri: lsp::Url::from_file_path(path!("/dir/test3.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/dir/test3.rs")).unwrap(), version: 0, text: rust_buffer2.update(cx, |buffer, _| buffer.text()), language_id: "rust".to_string(), @@ -1084,7 +1083,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .receive_notification::() .await .text_document, - lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path!("/dir/test3.rs")).unwrap()), + lsp::TextDocumentIdentifier::new(lsp::Uri::from_file_path(path!("/dir/test3.rs")).unwrap()), ); assert_eq!( fake_json_server @@ -1092,7 +1091,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::TextDocumentItem { - uri: lsp::Url::from_file_path(path!("/dir/test3.json")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/dir/test3.json")).unwrap(), version: 0, text: rust_buffer2.update(cx, |buffer, _| buffer.text()), language_id: "json".to_string(), @@ -1118,7 +1117,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::VersionedTextDocumentIdentifier::new( - lsp::Url::from_file_path(path!("/dir/test3.json")).unwrap(), + lsp::Uri::from_file_path(path!("/dir/test3.json")).unwrap(), 1 ) ); @@ -1148,7 +1147,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::TextDocumentItem { - uri: lsp::Url::from_file_path(path!("/dir/test.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/dir/test.rs")).unwrap(), version: 0, text: rust_buffer.update(cx, |buffer, _| buffer.text()), language_id: "rust".to_string(), @@ -1169,13 +1168,13 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { ], [ lsp::TextDocumentItem { - uri: lsp::Url::from_file_path(path!("/dir/package.json")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/dir/package.json")).unwrap(), version: 0, text: json_buffer.update(cx, |buffer, _| buffer.text()), language_id: "json".to_string(), }, lsp::TextDocumentItem { - uri: lsp::Url::from_file_path(path!("/dir/test3.json")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/dir/test3.json")).unwrap(), version: 0, text: rust_buffer2.update(cx, |buffer, _| buffer.text()), language_id: "json".to_string(), @@ -1187,7 +1186,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { cx.update(|_| drop(_json_handle)); let close_message = lsp::DidCloseTextDocumentParams { text_document: lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path(path!("/dir/package.json")).unwrap(), + lsp::Uri::from_file_path(path!("/dir/package.json")).unwrap(), ), }; assert_eq!( @@ -1316,7 +1315,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon let _out_of_worktree_buffer = project .update(cx, |project, cx| { project.open_local_buffer_via_lsp( - lsp::Url::from_file_path(path!("/the-registry/dep1/src/dep1.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/the-registry/dep1/src/dep1.rs")).unwrap(), server_id, cx, ) @@ -1476,23 +1475,23 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon &*file_changes.lock(), &[ lsp::FileEvent { - uri: lsp::Url::from_file_path(path!("/the-root/Cargo.lock")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/the-root/Cargo.lock")).unwrap(), typ: lsp::FileChangeType::CHANGED, }, lsp::FileEvent { - uri: lsp::Url::from_file_path(path!("/the-root/src/b.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/the-root/src/b.rs")).unwrap(), typ: lsp::FileChangeType::DELETED, }, lsp::FileEvent { - uri: lsp::Url::from_file_path(path!("/the-root/src/c.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/the-root/src/c.rs")).unwrap(), typ: lsp::FileChangeType::CREATED, }, lsp::FileEvent { - uri: lsp::Url::from_file_path(path!("/the-root/target/y/out/y2.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/the-root/target/y/out/y2.rs")).unwrap(), typ: lsp::FileChangeType::CREATED, }, lsp::FileEvent { - uri: lsp::Url::from_file_path(path!("/the/stdlib/src/string.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/the/stdlib/src/string.rs")).unwrap(), typ: lsp::FileChangeType::CHANGED, }, ] @@ -1539,7 +1538,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { .update_diagnostics( LanguageServerId(0), lsp::PublishDiagnosticsParams { - uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(), + uri: Uri::from_file_path(path!("/dir/a.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)), @@ -1558,7 +1557,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { .update_diagnostics( LanguageServerId(0), lsp::PublishDiagnosticsParams { - uri: Url::from_file_path(path!("/dir/b.rs")).unwrap(), + uri: Uri::from_file_path(path!("/dir/b.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)), @@ -1650,7 +1649,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) { .update_diagnostics( server_id, lsp::PublishDiagnosticsParams { - uri: Url::from_file_path(path!("/root/dir/b.rs")).unwrap(), + uri: Uri::from_file_path(path!("/root/dir/b.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)), @@ -1669,7 +1668,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) { .update_diagnostics( server_id, lsp::PublishDiagnosticsParams { - uri: Url::from_file_path(path!("/root/other.rs")).unwrap(), + uri: Uri::from_file_path(path!("/root/other.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 9)), @@ -1813,7 +1812,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { ); fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(), + uri: Uri::from_file_path(path!("/dir/a.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), @@ -1866,7 +1865,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { // Ensure publishing empty diagnostics twice only results in one update event. fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(), + uri: Uri::from_file_path(path!("/dir/a.rs")).unwrap(), version: None, diagnostics: Default::default(), }); @@ -1879,7 +1878,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { ); fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(), + uri: Uri::from_file_path(path!("/dir/a.rs")).unwrap(), version: None, diagnostics: Default::default(), }); @@ -2011,7 +2010,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp // Publish diagnostics let fake_server = fake_servers.next().await.unwrap(); fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(), + uri: Uri::from_file_path(path!("/dir/a.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), @@ -2092,7 +2091,7 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T // Before restarting the server, report diagnostics with an unknown buffer version. let fake_server = fake_servers.next().await.unwrap(); fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/dir/a.rs")).unwrap(), version: Some(10000), diagnostics: Vec::new(), }); @@ -2343,7 +2342,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { // Report some diagnostics for the initial version of the buffer fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/dir/a.rs")).unwrap(), version: Some(open_notification.text_document.version), diagnostics: vec![ lsp::Diagnostic { @@ -2431,7 +2430,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { // Ensure overlapping diagnostics are highlighted correctly. fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/dir/a.rs")).unwrap(), version: Some(open_notification.text_document.version), diagnostics: vec![ lsp::Diagnostic { @@ -2525,7 +2524,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { // Handle out-of-order diagnostics fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/dir/a.rs")).unwrap(), version: Some(change_notification_2.text_document.version), diagnostics: vec![ lsp::Diagnostic { @@ -3206,7 +3205,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { Ok(Some(lsp::GotoDefinitionResponse::Scalar( lsp::Location::new( - lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/dir/a.rs")).unwrap(), lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), ), ))) @@ -3765,7 +3764,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { edit: lsp::WorkspaceEdit { changes: Some( [( - lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(), + lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(), vec![lsp::TextEdit { range: lsp::Range::new( lsp::Position::new(0, 0), @@ -3904,7 +3903,7 @@ async fn test_save_file_spawns_language_server(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::TextDocumentItem { - uri: lsp::Url::from_file_path(path!("/dir/file.rs")).unwrap(), + uri: lsp::Uri::from_file_path(path!("/dir/file.rs")).unwrap(), version: 0, text: "".to_string(), language_id: "rust".to_string(), @@ -4742,7 +4741,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { .await .unwrap(); - let buffer_uri = Url::from_file_path(path!("/dir/a.rs")).unwrap(); + let buffer_uri = Uri::from_file_path(path!("/dir/a.rs")).unwrap(); let message = lsp::PublishDiagnosticsParams { uri: buffer_uri.clone(), diagnostics: vec![ @@ -5064,7 +5063,7 @@ async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) { new_text: "This is not a drill".to_owned(), })], text_document: lsp::OptionalVersionedTextDocumentIdentifier { - uri: Url::from_str(uri!("file:///dir/two/two.rs")).unwrap(), + uri: Uri::from_str(uri!("file:///dir/two/two.rs")).unwrap(), version: Some(1337), }, }] @@ -5189,14 +5188,14 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { changes: Some( [ ( - lsp::Url::from_file_path(path!("/dir/one.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/dir/one.rs")).unwrap(), vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), "THREE".to_string(), )], ), ( - lsp::Url::from_file_path(path!("/dir/two.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/dir/two.rs")).unwrap(), vec![ lsp::TextEdit::new( lsp::Range::new( diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 72029e55a0dad7e2070f4660b86b4b4d1eb4ffba..7f42f9e8efbda74bae52318d7353896e296ababc 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -437,7 +437,7 @@ mod tests { deprecated: None, container_name: None, location: lsp::Location::new( - lsp::Url::from_file_path(path.as_ref()).unwrap(), + lsp::Uri::from_file_path(path.as_ref()).unwrap(), lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), ), } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index e106a5ef18d59ebeb942564f24600635f78f89c7..353857f5871551a20315f638aa3d9653b3ed2848 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -533,7 +533,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext Ok(Some(lsp::WorkspaceEdit { changes: Some( [( - lsp::Url::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(), + lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(), vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)), "two".to_string(), From 5001c037116386cb3f3316d5e4459fe78a4bd3fc Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 29 Aug 2025 17:14:27 +0300 Subject: [PATCH 439/744] Properly process files that cannot be open for a reason (#37170) Follow-up of https://github.com/zed-industries/zed/pull/36764 * Fix `anyhow!({e})` conversion lossing Collab error codes context when opening a buffer remotely * Use this context to only allow opening files that had not specific Collab error code Release Notes: - N/A --- crates/client/src/client.rs | 15 ++---------- crates/project/src/buffer_store.rs | 22 ++++++++++++++--- crates/remote/src/remote_client.rs | 2 +- crates/workspace/src/workspace.rs | 39 ++++++++++++++++++------------ 4 files changed, 45 insertions(+), 33 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index bdbf049b75ef1e0de351c65be7382a94d73448e6..1e735b0025f1e8a15809b096c5a462361d4ed8f3 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1696,21 +1696,10 @@ impl Client { ); cx.spawn(async move |_| match future.await { Ok(()) => { - log::debug!( - "rpc message handled. client_id:{}, sender_id:{:?}, type:{}", - client_id, - original_sender_id, - type_name - ); + log::debug!("rpc message handled. client_id:{client_id}, sender_id:{original_sender_id:?}, type:{type_name}"); } Err(error) => { - log::error!( - "error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}", - client_id, - original_sender_id, - type_name, - error - ); + log::error!("error handling message. client_id:{client_id}, sender_id:{original_sender_id:?}, type:{type_name}, error:{error:#}"); } }) .detach(); diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 295bad6e596252cbbeecb36b587b696ccbab32a0..89bd4b27c9c47470a781e0ff322f5ef4a29b4927 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -20,7 +20,7 @@ use language::{ }, }; use rpc::{ - AnyProtoClient, ErrorExt as _, TypedEnvelope, + AnyProtoClient, ErrorCode, ErrorExt as _, TypedEnvelope, proto::{self, ToProto}, }; use smol::channel::Receiver; @@ -837,7 +837,15 @@ impl BufferStore { } }; - cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) }) + cx.background_spawn(async move { + task.await.map_err(|e| { + if e.error_code() != ErrorCode::Internal { + anyhow!(e.error_code()) + } else { + anyhow!("{e}") + } + }) + }) } pub fn create_buffer(&mut self, cx: &mut Context) -> Task>> { @@ -944,7 +952,15 @@ impl BufferStore { ) -> impl Iterator>>)> { self.loading_buffers.iter().map(|(path, task)| { let task = task.clone(); - (path, async move { task.await.map_err(|e| anyhow!("{e}")) }) + (path, async move { + task.await.map_err(|e| { + if e.error_code() != ErrorCode::Internal { + anyhow!(e.error_code()) + } else { + anyhow!("{e}") + } + }) + }) }) } diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index dd529ca87499b0daf2061fd990f7149828e3fce4..7e231e622cb2336a113799f7087fc0e30a5f79ff 100644 --- a/crates/remote/src/remote_client.rs +++ b/crates/remote/src/remote_client.rs @@ -1117,7 +1117,7 @@ impl ChannelClient { } Err(error) => { log::error!( - "{}:error handling message. type:{}, error:{}", + "{}:error handling message. type:{}, error:{:#}", this.name, type_name, format!("{error:#}").lines().fold( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0f119c14003d0f54f2f3a5323cb5e9106716a24d..61442eb6348e6152a4ad8ba4d3f93c24d1887346 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -648,23 +648,30 @@ impl ProjectItemRegistry { ) as Box<_>; Ok((project_entry_id, build_workspace_item)) } - Err(e) => match entry_abs_path.as_deref().filter(|_| is_file) { - Some(abs_path) => match cx.update(|window, cx| { - T::for_broken_project_item(abs_path, is_local, &e, window, cx) - })? { - Some(broken_project_item_view) => { - let build_workspace_item = Box::new( - move |_: &mut Pane, _: &mut Window, cx: &mut Context| { - cx.new(|_| broken_project_item_view).boxed_clone() - }, - ) - as Box<_>; - Ok((None, build_workspace_item)) + Err(e) => { + if e.error_code() == ErrorCode::Internal { + if let Some(abs_path) = + entry_abs_path.as_deref().filter(|_| is_file) + { + if let Some(broken_project_item_view) = + cx.update(|window, cx| { + T::for_broken_project_item( + abs_path, is_local, &e, window, cx, + ) + })? + { + let build_workspace_item = Box::new( + move |_: &mut Pane, _: &mut Window, cx: &mut Context| { + cx.new(|_| broken_project_item_view).boxed_clone() + }, + ) + as Box<_>; + return Ok((None, build_workspace_item)); + } } - None => Err(e)?, - }, - None => Err(e)?, - }, + } + Err(e) + } } })) }); From 11fb57a6d96f2133c492c6da18b6a976cb2429b2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 29 Aug 2025 16:16:02 +0200 Subject: [PATCH 440/744] acp: Use the custom claude installation to perform login (#37169) Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner Co-authored-by: Agus Zubiaga Co-authored-by: Nathan Sobo Co-authored-by: Cole Miller Co-authored-by: morgankrey --- Cargo.lock | 1 + crates/agent_servers/src/agent_servers.rs | 10 ++-- crates/agent_servers/src/claude.rs | 40 ++++++++++++- crates/agent_servers/src/e2e_tests.rs | 2 +- crates/agent_ui/Cargo.toml | 1 + crates/agent_ui/src/acp/message_editor.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 69 +++++++++++++++-------- 7 files changed, 94 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e493c99a2fc0f9514503b7cee8ef41cca582c387..aa1bcab9a68294baa4264916ef5a35adbeb20802 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -414,6 +414,7 @@ dependencies = [ "serde_json", "serde_json_lenient", "settings", + "shlex", "smol", "streaming_diff", "task", diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 83b3be76ce709c9b8c4d9f13ca55632a79e7b677..c1fc7b91ae862a25eac8da998f4b848327a3dd3e 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -44,11 +44,11 @@ pub fn init(cx: &mut App) { pub struct AgentServerDelegate { project: Entity, - status_tx: watch::Sender, + status_tx: Option>, } impl AgentServerDelegate { - pub fn new(project: Entity, status_tx: watch::Sender) -> Self { + pub fn new(project: Entity, status_tx: Option>) -> Self { Self { project, status_tx } } @@ -72,7 +72,7 @@ impl AgentServerDelegate { "External agents are not yet available in remote projects." ))); }; - let mut status_tx = self.status_tx; + let status_tx = self.status_tx; cx.spawn(async move |cx| { if !ignore_system_version { @@ -165,7 +165,9 @@ impl AgentServerDelegate { .detach(); file_name } else { - status_tx.send("Installing…".into()).ok(); + if let Some(mut status_tx) = status_tx { + status_tx.send("Installing…".into()).ok(); + } let dir = dir.clone(); cx.background_spawn(Self::download_latest_version( fs, diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index db8853695ec798a8b146666292cd29f2c1fc145c..0a4f152e8afd991fed90af12aa5bbff909c8aa2d 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1,8 +1,8 @@ use language_models::provider::anthropic::AnthropicLanguageModelProvider; use settings::SettingsStore; -use std::any::Any; use std::path::Path; use std::rc::Rc; +use std::{any::Any, path::PathBuf}; use anyhow::Result; use gpui::{App, AppContext as _, SharedString, Task}; @@ -13,9 +13,47 @@ use acp_thread::AgentConnection; #[derive(Clone)] pub struct ClaudeCode; +pub struct ClaudeCodeLoginCommand { + pub path: PathBuf, + pub arguments: Vec, +} + impl ClaudeCode { const BINARY_NAME: &'static str = "claude-code-acp"; const PACKAGE_NAME: &'static str = "@zed-industries/claude-code-acp"; + + pub fn login_command( + delegate: AgentServerDelegate, + cx: &mut App, + ) -> Task> { + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).claude.clone() + }); + + cx.spawn(async move |cx| { + let mut command = if let Some(settings) = settings { + settings.command + } else { + cx.update(|cx| { + delegate.get_or_npm_install_builtin_agent( + Self::BINARY_NAME.into(), + Self::PACKAGE_NAME.into(), + "node_modules/@anthropic-ai/claude-code/cli.js".into(), + true, + None, + cx, + ) + })? + .await? + }; + command.args.push("/login".into()); + + Ok(ClaudeCodeLoginCommand { + path: command.path, + arguments: command.args, + }) + }) + } } impl AgentServer for ClaudeCode { diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 5d2becf0ccc4b30cfeca27f4eb5ee08c2d0bb7d1..7988b86081351b29c8a19b676498db26d0b83fc3 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -498,7 +498,7 @@ pub async fn new_test_thread( current_dir: impl AsRef, cx: &mut TestAppContext, ) -> Entity { - let delegate = AgentServerDelegate::new(project.clone(), watch::channel("".into()).0); + let delegate = AgentServerDelegate::new(project.clone(), None); let connection = cx .update(|cx| server.connect(current_dir.as_ref(), delegate, cx)) diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 6b0979ee696571841a7ec620ca48de2880f66492..6c8b9528800041d8920d935a8f75867d03719a9d 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -80,6 +80,7 @@ serde.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true settings.workspace = true +shlex.workspace = true smol.workspace = true streaming_diff.workspace = true task.workspace = true diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index bd5e4faf7aedba4644206794a1c7a837517c52d6..b9e85e0ee34b3dccd0dcd4a22c1fbaa05031e2d9 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -645,7 +645,7 @@ impl MessageEditor { self.project.read(cx).fs().clone(), self.history_store.clone(), )); - let delegate = AgentServerDelegate::new(self.project.clone(), watch::channel("".into()).0); + let delegate = AgentServerDelegate::new(self.project.clone(), None); let connection = server.connect(Path::new(""), delegate, cx); cx.spawn(async move |_, cx| { let agent = connection.await?; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c718540c217425c8987f4282d5990579d529779e..eff9ceedd433ea8beb833108fb9fea1eb3f706da 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -9,7 +9,7 @@ use agent_client_protocol::{self as acp, PromptCapabilities}; use agent_servers::{AgentServer, AgentServerDelegate, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore}; -use anyhow::{Result, anyhow, bail}; +use anyhow::{Context as _, Result, anyhow, bail}; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; use client::zed_urls; @@ -423,7 +423,7 @@ impl AcpThreadView { .map(|worktree| worktree.read(cx).abs_path()) .unwrap_or_else(|| paths::home_dir().as_path().into()); let (tx, mut rx) = watch::channel("Loading…".into()); - let delegate = AgentServerDelegate::new(project.clone(), tx); + let delegate = AgentServerDelegate::new(project.clone(), Some(tx)); let connect_task = agent.connect(&root_dir, delegate, cx); let load_task = cx.spawn_in(window, async move |this, cx| { @@ -1386,31 +1386,52 @@ impl AcpThreadView { let Some(terminal_panel) = workspace.read(cx).panel::(cx) else { return Task::ready(Ok(())); }; - let project = workspace.read(cx).project().read(cx); + let project_entity = workspace.read(cx).project(); + let project = project_entity.read(cx); let cwd = project.first_project_directory(cx); let shell = project.terminal_settings(&cwd, cx).shell.clone(); - let terminal = terminal_panel.update(cx, |terminal_panel, cx| { - terminal_panel.spawn_task( - &SpawnInTerminal { - id: task::TaskId("claude-login".into()), - full_label: "claude /login".to_owned(), - label: "claude /login".to_owned(), - command: Some("claude".to_owned()), - args: vec!["/login".to_owned()], - command_label: "claude /login".to_owned(), - cwd, - use_new_terminal: true, - allow_concurrent_runs: true, - hide: task::HideStrategy::Always, - shell, - ..Default::default() - }, - window, - cx, - ) - }); - cx.spawn(async move |cx| { + let delegate = AgentServerDelegate::new(project_entity.clone(), None); + let command = ClaudeCode::login_command(delegate, cx); + + window.spawn(cx, async move |cx| { + let login_command = command.await?; + let command = login_command + .path + .to_str() + .with_context(|| format!("invalid login command: {:?}", login_command.path))?; + let command = shlex::try_quote(command)?; + let args = login_command + .arguments + .iter() + .map(|arg| { + Ok(shlex::try_quote(arg) + .context("Failed to quote argument")? + .to_string()) + }) + .collect::>>()?; + + let terminal = terminal_panel.update_in(cx, |terminal_panel, window, cx| { + terminal_panel.spawn_task( + &SpawnInTerminal { + id: task::TaskId("claude-login".into()), + full_label: "claude /login".to_owned(), + label: "claude /login".to_owned(), + command: Some(command.into()), + args, + command_label: "claude /login".to_owned(), + cwd, + use_new_terminal: true, + allow_concurrent_runs: true, + hide: task::HideStrategy::Always, + shell, + ..Default::default() + }, + window, + cx, + ) + })?; + let terminal = terminal.await?; let mut exit_status = terminal .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))? From a13881746a5ac5f5693f867016f2908b081090c3 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Fri, 29 Aug 2025 22:22:43 +0530 Subject: [PATCH 441/744] editor: APCA contrast (#37165) Closes #35787 Closes #17890 Closes #28789 Closes #36495 How it works: For highlights (and selections) within the visible rows of the editor, we split them row by row. This is efficient since the number of visible rows is constant. For each row, all highlights and selections, which may overlap, are flattened using a line sweep. This produces non-overlapping consecutive segments for each row, each with a blended background color. Next, for each row, we split text runs into smaller runs to adjust its color using APCA contrast. Since both text runs and segment are non-overlapping and consecutive, we can use two-pointer on them to do this. For example, a text run for the variable red might be split into two runs if a highlight partially covers it. As a result, one part may appear as red, while the other appears as a lighter red, depending on the background behind it. Result: image image image Release Notes: - Improved text contrast when selected or highlighted in the editor. --- assets/settings/default.json | 14 + crates/editor/src/editor_settings.rs | 7 + crates/editor/src/element.rs | 538 ++++++++++++++++++++++++++- 3 files changed, 557 insertions(+), 2 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 297c932e5b54ca75eb34b2399c0a1f427dcc9f77..572193be4eecbeb63a19eab1811bff126638162b 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -226,6 +226,20 @@ // The debounce delay before querying highlights from the language // server based on the current cursor location. "lsp_highlight_debounce": 75, + // The minimum APCA perceptual contrast between foreground and background colors. + // APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x, + // especially for dark mode. Values range from 0 to 106. + // + // Based on APCA Readability Criterion (ARC) Bronze Simple Mode: + // https://readtech.org/ARC/tests/bronze-simple-mode/ + // - 0: No contrast adjustment + // - 45: Minimum for large fluent text (36px+) + // - 60: Minimum for other content text + // - 75: Minimum for body text + // - 90: Preferred for body text + // + // This only affects text drawn over highlight backgrounds in the editor. + "minimum_contrast_for_highlights": 45, // Whether to pop the completions menu while typing in an editor without // explicitly requesting it. "show_completions_on_input": true, diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 9b110d782a0bbcf789791240ef42a935b7ecd47b..55c040428d7e73d9e6e9bf6cc66cc20d301038f2 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -56,6 +56,7 @@ pub struct EditorSettings { pub inline_code_actions: bool, pub drag_and_drop_selection: DragAndDropSelection, pub lsp_document_colors: DocumentColorsRenderMode, + pub minimum_contrast_for_highlights: f32, } /// How to render LSP `textDocument/documentColor` colors in the editor. @@ -550,6 +551,12 @@ pub struct EditorSettingsContent { /// /// Default: false pub show_signature_help_after_edits: Option, + /// The minimum APCA perceptual contrast to maintain when + /// rendering text over highlight backgrounds in the editor. + /// + /// Values range from 0 to 106. Set to 0 to disable adjustments. + /// Default: 45 + pub minimum_contrast_for_highlights: Option, /// Whether to follow-up empty go to definition responses from the language server or not. /// `FindAllReferences` allows to look up references of the same symbol instead. diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a63c18e003907f16a1383bbfb12085e1044d9eb9..ca6eac080e6121880eae63b4dc60ca6d32c6da5d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -82,6 +82,7 @@ use std::{ use sum_tree::Bias; use text::{BufferId, SelectionGoal}; use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor}; +use ui::utils::ensure_minimum_contrast; use ui::{ ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*, right_click_menu, @@ -3260,12 +3261,161 @@ impl EditorElement { .collect() } + fn bg_segments_per_row( + rows: Range, + selections: &[(PlayerColor, Vec)], + highlight_ranges: &[(Range, Hsla)], + base_background: Hsla, + ) -> Vec, Hsla)>> { + if rows.start >= rows.end { + return Vec::new(); + } + let highlight_iter = highlight_ranges.iter().cloned(); + let selection_iter = selections.iter().flat_map(|(player_color, layouts)| { + let color = player_color.selection; + layouts.iter().filter_map(move |selection_layout| { + if selection_layout.range.start != selection_layout.range.end { + Some((selection_layout.range.clone(), color)) + } else { + None + } + }) + }); + let mut per_row_map = vec![Vec::new(); rows.len()]; + for (range, color) in highlight_iter.chain(selection_iter) { + let covered_rows = if range.end.column() == 0 { + cmp::max(range.start.row(), rows.start)..cmp::min(range.end.row(), rows.end) + } else { + cmp::max(range.start.row(), rows.start) + ..cmp::min(range.end.row().next_row(), rows.end) + }; + for row in covered_rows.iter_rows() { + let seg_start = if row == range.start.row() { + range.start + } else { + DisplayPoint::new(row, 0) + }; + let seg_end = if row == range.end.row() && range.end.column() != 0 { + range.end + } else { + DisplayPoint::new(row, u32::MAX) + }; + let ix = row.minus(rows.start) as usize; + debug_assert!(row >= rows.start && row < rows.end); + debug_assert!(ix < per_row_map.len()); + per_row_map[ix].push((seg_start..seg_end, color)); + } + } + for row_segments in per_row_map.iter_mut() { + if row_segments.is_empty() { + continue; + } + let segments = mem::take(row_segments); + let merged = Self::merge_overlapping_ranges(segments, base_background); + *row_segments = merged; + } + per_row_map + } + + /// Merge overlapping ranges by splitting at all range boundaries and blending colors where + /// multiple ranges overlap. The result contains non-overlapping ranges ordered from left to right. + /// + /// Expects `start.row() == end.row()` for each range. + fn merge_overlapping_ranges( + ranges: Vec<(Range, Hsla)>, + base_background: Hsla, + ) -> Vec<(Range, Hsla)> { + struct Boundary { + pos: DisplayPoint, + is_start: bool, + index: usize, + color: Hsla, + } + + let mut boundaries: SmallVec<[Boundary; 16]> = SmallVec::with_capacity(ranges.len() * 2); + for (index, (range, color)) in ranges.iter().enumerate() { + debug_assert!( + range.start.row() == range.end.row(), + "expects single-row ranges" + ); + if range.start < range.end { + boundaries.push(Boundary { + pos: range.start, + is_start: true, + index, + color: *color, + }); + boundaries.push(Boundary { + pos: range.end, + is_start: false, + index, + color: *color, + }); + } + } + + if boundaries.is_empty() { + return Vec::new(); + } + + boundaries + .sort_unstable_by(|a, b| a.pos.cmp(&b.pos).then_with(|| a.is_start.cmp(&b.is_start))); + + let mut processed_ranges: Vec<(Range, Hsla)> = Vec::new(); + let mut active_ranges: SmallVec<[(usize, Hsla); 8]> = SmallVec::new(); + + let mut i = 0; + let mut start_pos = boundaries[0].pos; + + let boundaries_len = boundaries.len(); + while i < boundaries_len { + let current_boundary_pos = boundaries[i].pos; + if start_pos < current_boundary_pos { + if !active_ranges.is_empty() { + let mut color = base_background; + for &(_, c) in &active_ranges { + color = Hsla::blend(color, c); + } + if let Some((last_range, last_color)) = processed_ranges.last_mut() { + if *last_color == color && last_range.end == start_pos { + last_range.end = current_boundary_pos; + } else { + processed_ranges.push((start_pos..current_boundary_pos, color)); + } + } else { + processed_ranges.push((start_pos..current_boundary_pos, color)); + } + } + } + while i < boundaries_len && boundaries[i].pos == current_boundary_pos { + let active_range = &boundaries[i]; + if active_range.is_start { + let idx = active_range.index; + let pos = active_ranges + .binary_search_by_key(&idx, |(i, _)| *i) + .unwrap_or_else(|p| p); + active_ranges.insert(pos, (idx, active_range.color)); + } else { + let idx = active_range.index; + if let Ok(pos) = active_ranges.binary_search_by_key(&idx, |(i, _)| *i) { + active_ranges.remove(pos); + } + } + i += 1; + } + start_pos = current_boundary_pos; + } + + processed_ranges + } + fn layout_lines( rows: Range, snapshot: &EditorSnapshot, style: &EditorStyle, editor_width: Pixels, is_row_soft_wrapped: impl Copy + Fn(usize) -> bool, + bg_segments_per_row: &[Vec<(Range, Hsla)>], window: &mut Window, cx: &mut App, ) -> Vec { @@ -3321,6 +3471,7 @@ impl EditorElement { &snapshot.mode, editor_width, is_row_soft_wrapped, + bg_segments_per_row, window, cx, ) @@ -7340,6 +7491,7 @@ impl LineWithInvisibles { editor_mode: &EditorMode, text_width: Pixels, is_row_soft_wrapped: impl Copy + Fn(usize) -> bool, + bg_segments_per_row: &[Vec<(Range, Hsla)>], window: &mut Window, cx: &mut App, ) -> Vec { @@ -7355,6 +7507,7 @@ impl LineWithInvisibles { let mut row = 0; let mut line_exceeded_max_len = false; let font_size = text_style.font_size.to_pixels(window.rem_size()); + let min_contrast = EditorSettings::get_global(cx).minimum_contrast_for_highlights; let ellipsis = SharedString::from("⋯"); @@ -7367,10 +7520,16 @@ impl LineWithInvisibles { }]) { if let Some(replacement) = highlighted_chunk.replacement { if !line.is_empty() { + let segments = bg_segments_per_row.get(row).map(|v| &v[..]).unwrap_or(&[]); + let text_runs: &[TextRun] = if segments.is_empty() { + &styles + } else { + &Self::split_runs_by_bg_segments(&styles, segments, min_contrast) + }; let shaped_line = window.text_system().shape_line( line.clone().into(), font_size, - &styles, + text_runs, None, ); width += shaped_line.width; @@ -7448,10 +7607,16 @@ impl LineWithInvisibles { } else { for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() { if ix > 0 { + let segments = bg_segments_per_row.get(row).map(|v| &v[..]).unwrap_or(&[]); + let text_runs = if segments.is_empty() { + &styles + } else { + &Self::split_runs_by_bg_segments(&styles, segments, min_contrast) + }; let shaped_line = window.text_system().shape_line( line.clone().into(), font_size, - &styles, + text_runs, None, ); width += shaped_line.width; @@ -7539,6 +7704,81 @@ impl LineWithInvisibles { layouts } + /// Takes text runs and non-overlapping left-to-right background ranges with color. + /// Returns new text runs with adjusted contrast as per background ranges. + fn split_runs_by_bg_segments( + text_runs: &[TextRun], + bg_segments: &[(Range, Hsla)], + min_contrast: f32, + ) -> Vec { + let mut output_runs: Vec = Vec::with_capacity(text_runs.len()); + let mut line_col = 0usize; + let mut segment_ix = 0usize; + + for text_run in text_runs.iter() { + let run_start_col = line_col; + let run_end_col = run_start_col + text_run.len; + while segment_ix < bg_segments.len() + && (bg_segments[segment_ix].0.end.column() as usize) <= run_start_col + { + segment_ix += 1; + } + let mut cursor_col = run_start_col; + let mut local_segment_ix = segment_ix; + while local_segment_ix < bg_segments.len() { + let (range, segment_color) = &bg_segments[local_segment_ix]; + let segment_start_col = range.start.column() as usize; + let segment_end_col = range.end.column() as usize; + if segment_start_col >= run_end_col { + break; + } + if segment_start_col > cursor_col { + let span_len = segment_start_col - cursor_col; + output_runs.push(TextRun { + len: span_len, + font: text_run.font.clone(), + color: text_run.color, + background_color: text_run.background_color, + underline: text_run.underline, + strikethrough: text_run.strikethrough, + }); + cursor_col = segment_start_col; + } + let segment_slice_end_col = segment_end_col.min(run_end_col); + if segment_slice_end_col > cursor_col { + let new_text_color = + ensure_minimum_contrast(text_run.color, *segment_color, min_contrast); + output_runs.push(TextRun { + len: segment_slice_end_col - cursor_col, + font: text_run.font.clone(), + color: new_text_color, + background_color: text_run.background_color, + underline: text_run.underline, + strikethrough: text_run.strikethrough, + }); + cursor_col = segment_slice_end_col; + } + if segment_end_col >= run_end_col { + break; + } + local_segment_ix += 1; + } + if cursor_col < run_end_col { + output_runs.push(TextRun { + len: run_end_col - cursor_col, + font: text_run.font.clone(), + color: text_run.color, + background_color: text_run.background_color, + underline: text_run.underline, + strikethrough: text_run.strikethrough, + }); + } + line_col = run_end_col; + segment_ix = local_segment_ix; + } + output_runs + } + fn prepaint( &mut self, line_height: Pixels, @@ -8452,12 +8692,20 @@ impl Element for EditorElement { cx, ); + let bg_segments_per_row = Self::bg_segments_per_row( + start_row..end_row, + &selections, + &highlighted_ranges, + self.style.background, + ); + let mut line_layouts = Self::layout_lines( start_row..end_row, &snapshot, &self.style, editor_width, is_row_soft_wrapped, + &bg_segments_per_row, window, cx, ); @@ -9817,6 +10065,7 @@ pub fn layout_line( &snapshot.mode, text_width, is_row_soft_wrapped, + &[], window, cx, ) @@ -10717,4 +10966,289 @@ mod tests { .cloned() .collect() } + + #[gpui::test] + fn test_merge_overlapping_ranges() { + let base_bg = Hsla::default(); + let color1 = Hsla { + h: 0.0, + s: 0.5, + l: 0.5, + a: 0.5, + }; + let color2 = Hsla { + h: 120.0, + s: 0.5, + l: 0.5, + a: 0.5, + }; + + let display_point = |col| DisplayPoint::new(DisplayRow(0), col); + let cols = |v: &Vec<(Range, Hsla)>| -> Vec<(u32, u32)> { + v.iter() + .map(|(r, _)| (r.start.column(), r.end.column())) + .collect() + }; + + // Test overlapping ranges blend colors + let overlapping = vec![ + (display_point(5)..display_point(15), color1), + (display_point(10)..display_point(20), color2), + ]; + let result = EditorElement::merge_overlapping_ranges(overlapping, base_bg); + assert_eq!(cols(&result), vec![(5, 10), (10, 15), (15, 20)]); + + // Test middle segment should have blended color + let blended = Hsla::blend(Hsla::blend(base_bg, color1), color2); + assert_eq!(result[1].1, blended); + + // Test adjacent same-color ranges merge + let adjacent_same = vec![ + (display_point(5)..display_point(10), color1), + (display_point(10)..display_point(15), color1), + ]; + let result = EditorElement::merge_overlapping_ranges(adjacent_same, base_bg); + assert_eq!(cols(&result), vec![(5, 15)]); + + // Test contained range splits + let contained = vec![ + (display_point(5)..display_point(20), color1), + (display_point(10)..display_point(15), color2), + ]; + let result = EditorElement::merge_overlapping_ranges(contained, base_bg); + assert_eq!(cols(&result), vec![(5, 10), (10, 15), (15, 20)]); + + // Test multiple overlaps split at every boundary + let color3 = Hsla { + h: 240.0, + s: 0.5, + l: 0.5, + a: 0.5, + }; + let complex = vec![ + (display_point(5)..display_point(12), color1), + (display_point(8)..display_point(16), color2), + (display_point(10)..display_point(14), color3), + ]; + let result = EditorElement::merge_overlapping_ranges(complex, base_bg); + assert_eq!( + cols(&result), + vec![(5, 8), (8, 10), (10, 12), (12, 14), (14, 16)] + ); + } + + #[gpui::test] + fn test_bg_segments_per_row() { + let base_bg = Hsla::default(); + + // Case A: selection spans three display rows: row 1 [5, end), full row 2, row 3 [0, 7) + { + let selection_color = Hsla { + h: 200.0, + s: 0.5, + l: 0.5, + a: 0.5, + }; + let player_color = PlayerColor { + cursor: selection_color, + background: selection_color, + selection: selection_color, + }; + + let spanning_selection = SelectionLayout { + head: DisplayPoint::new(DisplayRow(3), 7), + cursor_shape: CursorShape::Bar, + is_newest: true, + is_local: true, + range: DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(3), 7), + active_rows: DisplayRow(1)..DisplayRow(4), + user_name: None, + }; + + let selections = vec![(player_color, vec![spanning_selection])]; + let result = EditorElement::bg_segments_per_row( + DisplayRow(0)..DisplayRow(5), + &selections, + &[], + base_bg, + ); + + assert_eq!(result.len(), 5); + assert!(result[0].is_empty()); + assert_eq!(result[1].len(), 1); + assert_eq!(result[2].len(), 1); + assert_eq!(result[3].len(), 1); + assert!(result[4].is_empty()); + + assert_eq!(result[1][0].0.start, DisplayPoint::new(DisplayRow(1), 5)); + assert_eq!(result[1][0].0.end.row(), DisplayRow(1)); + assert_eq!(result[1][0].0.end.column(), u32::MAX); + assert_eq!(result[2][0].0.start, DisplayPoint::new(DisplayRow(2), 0)); + assert_eq!(result[2][0].0.end.row(), DisplayRow(2)); + assert_eq!(result[2][0].0.end.column(), u32::MAX); + assert_eq!(result[3][0].0.start, DisplayPoint::new(DisplayRow(3), 0)); + assert_eq!(result[3][0].0.end, DisplayPoint::new(DisplayRow(3), 7)); + } + + // Case B: selection ends exactly at the start of row 3, excluding row 3 + { + let selection_color = Hsla { + h: 120.0, + s: 0.5, + l: 0.5, + a: 0.5, + }; + let player_color = PlayerColor { + cursor: selection_color, + background: selection_color, + selection: selection_color, + }; + + let selection = SelectionLayout { + head: DisplayPoint::new(DisplayRow(2), 0), + cursor_shape: CursorShape::Bar, + is_newest: true, + is_local: true, + range: DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(3), 0), + active_rows: DisplayRow(1)..DisplayRow(3), + user_name: None, + }; + + let selections = vec![(player_color, vec![selection])]; + let result = EditorElement::bg_segments_per_row( + DisplayRow(0)..DisplayRow(4), + &selections, + &[], + base_bg, + ); + + assert_eq!(result.len(), 4); + assert!(result[0].is_empty()); + assert_eq!(result[1].len(), 1); + assert_eq!(result[2].len(), 1); + assert!(result[3].is_empty()); + + assert_eq!(result[1][0].0.start, DisplayPoint::new(DisplayRow(1), 5)); + assert_eq!(result[1][0].0.end.row(), DisplayRow(1)); + assert_eq!(result[1][0].0.end.column(), u32::MAX); + assert_eq!(result[2][0].0.start, DisplayPoint::new(DisplayRow(2), 0)); + assert_eq!(result[2][0].0.end.row(), DisplayRow(2)); + assert_eq!(result[2][0].0.end.column(), u32::MAX); + } + } + + #[cfg(test)] + fn generate_test_run(len: usize, color: Hsla) -> TextRun { + TextRun { + len, + font: gpui::font(".SystemUIFont"), + color, + background_color: None, + underline: None, + strikethrough: None, + } + } + + #[gpui::test] + fn test_split_runs_by_bg_segments(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let text_color = Hsla { + h: 210.0, + s: 0.1, + l: 0.4, + a: 1.0, + }; + let bg1 = Hsla { + h: 30.0, + s: 0.6, + l: 0.8, + a: 1.0, + }; + let bg2 = Hsla { + h: 200.0, + s: 0.6, + l: 0.2, + a: 1.0, + }; + let min_contrast = 45.0; + + // Case A: single run; disjoint segments inside the run + let runs = vec![generate_test_run(20, text_color)]; + let segs = vec![ + ( + DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10), + bg1, + ), + ( + DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 16), + bg2, + ), + ]; + let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast); + // Expected slices: [0,5) [5,10) [10,12) [12,16) [16,20) + assert_eq!( + out.iter().map(|r| r.len).collect::>(), + vec![5, 5, 2, 4, 4] + ); + assert_eq!(out[0].color, text_color); + assert_eq!( + out[1].color, + ensure_minimum_contrast(text_color, bg1, min_contrast) + ); + assert_eq!(out[2].color, text_color); + assert_eq!( + out[3].color, + ensure_minimum_contrast(text_color, bg2, min_contrast) + ); + assert_eq!(out[4].color, text_color); + + // Case B: multiple runs; segment extends to end of line (u32::MAX) + let runs = vec![ + generate_test_run(8, text_color), + generate_test_run(7, text_color), + ]; + let segs = vec![( + DisplayPoint::new(DisplayRow(0), 6)..DisplayPoint::new(DisplayRow(0), u32::MAX), + bg1, + )]; + let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast); + // Expected slices across runs: [0,6) [6,8) | [0,7) + assert_eq!(out.iter().map(|r| r.len).collect::>(), vec![6, 2, 7]); + let adjusted = ensure_minimum_contrast(text_color, bg1, min_contrast); + assert_eq!(out[0].color, text_color); + assert_eq!(out[1].color, adjusted); + assert_eq!(out[2].color, adjusted); + + // Case C: multi-byte characters + // for text: "Hello 🌍 世界!" + let runs = vec![ + generate_test_run(5, text_color), // "Hello" + generate_test_run(6, text_color), // " 🌍 " + generate_test_run(6, text_color), // "世界" + generate_test_run(1, text_color), // "!" + ]; + // selecting "🌍 世" + let segs = vec![( + DisplayPoint::new(DisplayRow(0), 6)..DisplayPoint::new(DisplayRow(0), 14), + bg1, + )]; + let out = LineWithInvisibles::split_runs_by_bg_segments(&runs, &segs, min_contrast); + // "Hello" | " " | "🌍 " | "世" | "界" | "!" + assert_eq!( + out.iter().map(|r| r.len).collect::>(), + vec![5, 1, 5, 3, 3, 1] + ); + assert_eq!(out[0].color, text_color); // "Hello" + assert_eq!( + out[2].color, + ensure_minimum_contrast(text_color, bg1, min_contrast) + ); // "🌍 " + assert_eq!( + out[3].color, + ensure_minimum_contrast(text_color, bg1, min_contrast) + ); // "世" + assert_eq!(out[4].color, text_color); // "界" + assert_eq!(out[5].color, text_color); // "!" + } } From 3d4f9172040eacc6cc8787524588506abef07c0a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 29 Aug 2025 14:07:27 -0300 Subject: [PATCH 442/744] Make project symbols picker entry consistent with outline picker (#37176) Closes https://github.com/zed-industries/zed/issues/36383 The project symbols modal didn't use the buffer font and highlighted matches through modifying the font weight, which is inconsistent with the outline picker, which presents code in list items in a similar way, as well as project _and_ buffer search highlighting design. Release Notes: - N/A --- crates/project_symbols/src/project_symbols.rs | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 7f42f9e8efbda74bae52318d7353896e296ababc..ea67499acb07fc7517028dcd43282b051d52c3eb 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -1,18 +1,19 @@ use editor::{Bias, Editor, SelectionEffects, scroll::Autoscroll, styled_runs_for_code_label}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - App, Context, DismissEvent, Entity, FontWeight, ParentElement, StyledText, Task, WeakEntity, - Window, rems, + App, Context, DismissEvent, Entity, HighlightStyle, ParentElement, StyledText, Task, TextStyle, + WeakEntity, Window, relative, rems, }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; use project::{Project, Symbol}; +use settings::Settings; use std::{borrow::Cow, cmp::Reverse, sync::Arc}; -use theme::ActiveTheme; +use theme::{ActiveTheme, ThemeSettings}; use util::ResultExt; use workspace::{ Workspace, - ui::{Color, Label, LabelCommon, LabelLike, ListItem, ListItemSpacing, Toggleable, v_flex}, + ui::{LabelLike, ListItem, ListItemSpacing, prelude::*}, }; pub fn init(cx: &mut App) { @@ -213,7 +214,7 @@ impl PickerDelegate for ProjectSymbolsDelegate { &self, ix: usize, selected: bool, - window: &mut Window, + _window: &mut Window, cx: &mut Context>, ) -> Option { let string_match = &self.matches[ix]; @@ -235,18 +236,29 @@ impl PickerDelegate for ProjectSymbolsDelegate { let label = symbol.label.text.clone(); let path = path.to_string(); - let highlights = gpui::combine_highlights( - string_match - .positions - .iter() - .map(|pos| (*pos..pos + 1, FontWeight::BOLD.into())), - syntax_runs.map(|(range, mut highlight)| { - // Ignore font weight for syntax highlighting, as we'll use it - // for fuzzy matches. - highlight.font_weight = None; - (range, highlight) - }), - ); + let settings = ThemeSettings::get_global(cx); + + let text_style = TextStyle { + color: cx.theme().colors().text, + font_family: settings.buffer_font.family.clone(), + font_features: settings.buffer_font.features.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), + font_size: settings.buffer_font_size(cx).into(), + font_weight: settings.buffer_font.weight, + line_height: relative(1.), + ..Default::default() + }; + + let highlight_style = HighlightStyle { + background_color: Some(cx.theme().colors().text_accent.alpha(0.3)), + ..Default::default() + }; + let custom_highlights = string_match + .positions + .iter() + .map(|pos| (*pos..pos + 1, highlight_style)); + + let highlights = gpui::combine_highlights(custom_highlights, syntax_runs); Some( ListItem::new(ix) @@ -255,13 +267,10 @@ impl PickerDelegate for ProjectSymbolsDelegate { .toggle_state(selected) .child( v_flex() - .child( - LabelLike::new().child( - StyledText::new(label) - .with_default_highlights(&window.text_style(), highlights), - ), - ) - .child(Label::new(path).color(Color::Muted)), + .child(LabelLike::new().child( + StyledText::new(label).with_default_highlights(&text_style, highlights), + )) + .child(Label::new(path).size(LabelSize::Small).color(Color::Muted)), ), ) } From 92f739dbb9f2b46a1d825b39c0ea2c521dae0dbc Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 29 Aug 2025 13:40:39 -0400 Subject: [PATCH 443/744] acp: Improve error reporting and log more information when failing to launch gemini (#37178) In the case where we fail to create an ACP connection to Gemini, only report the "unsupported version" error if the version for the found binary is at least our minimum version. That means we'll surface the real error in this situation. This also fixes incorrect sorting of downloaded Gemini versions--as @kpe pointed out we were effectively using the version string as a key. Now we'll correctly use the parsed semver::Version instead. Release Notes: - N/A --- crates/agent_servers/src/agent_servers.rs | 11 ++++--- crates/agent_servers/src/gemini.rs | 39 ++++++++++++++--------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index c1fc7b91ae862a25eac8da998f4b848327a3dd3e..c610c53ea8d61d24ece6d3c80ec15505d259ea3b 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -105,22 +105,23 @@ impl AgentServerDelegate { .to_str() .and_then(|name| semver::Version::from_str(&name).ok()) { - versions.push((file_name.to_owned(), version)); + versions.push((version, file_name.to_owned())); } else { to_delete.push(file_name.to_owned()) } } versions.sort(); - let newest_version = if let Some((file_name, version)) = versions.last().cloned() - && minimum_version.is_none_or(|minimum_version| version > minimum_version) + let newest_version = if let Some((version, file_name)) = versions.last().cloned() + && minimum_version.is_none_or(|minimum_version| version >= minimum_version) { versions.pop(); Some(file_name) } else { None }; - to_delete.extend(versions.into_iter().map(|(file_name, _)| file_name)); + log::debug!("existing version of {package_name}: {newest_version:?}"); + to_delete.extend(versions.into_iter().map(|(_, file_name)| file_name)); cx.background_spawn({ let fs = fs.clone(); @@ -200,6 +201,8 @@ impl AgentServerDelegate { node_runtime: NodeRuntime, package_name: SharedString, ) -> Result { + log::debug!("downloading latest version of {package_name}"); + let tmp_dir = tempfile::tempdir_in(&dir)?; node_runtime diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 5e958f686959d78e6ceaf8b8ea7d8404ffba166a..a1553d288ab44d96bdfe08723a092ce231ba005b 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -63,7 +63,9 @@ impl AgentServer for Gemini { })? .await? }; - command.args.push("--experimental-acp".into()); + if !command.args.contains(&ACP_ARG.into()) { + command.args.push(ACP_ARG.into()); + } if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() { command @@ -86,17 +88,17 @@ impl AgentServer for Gemini { .await; let current_version = String::from_utf8(version_output?.stdout)?.trim().to_owned(); - if !connection.prompt_capabilities().image { - return Err(LoadError::Unsupported { - current_version: current_version.into(), - command: command.path.to_string_lossy().to_string().into(), - minimum_version: Self::MINIMUM_VERSION.into(), - } - .into()); + + log::error!("connected to gemini, but missing prompt_capabilities.image (version is {current_version})"); + return Err(LoadError::Unsupported { + current_version: current_version.into(), + command: command.path.to_string_lossy().to_string().into(), + minimum_version: Self::MINIMUM_VERSION.into(), } + .into()); } } - Err(_) => { + Err(e) => { let version_fut = util::command::new_smol_command(&command.path) .args(command.args.iter()) .arg("--version") @@ -111,12 +113,19 @@ impl AgentServer for Gemini { let (version_output, help_output) = futures::future::join(version_fut, help_fut).await; - - let current_version = std::str::from_utf8(&version_output?.stdout)? - .trim() - .to_string(); - let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG); - + let Some(version_output) = version_output.ok().and_then(|output| String::from_utf8(output.stdout).ok()) else { + return result; + }; + let Some((help_stdout, help_stderr)) = help_output.ok().and_then(|output| String::from_utf8(output.stdout).ok().zip(String::from_utf8(output.stderr).ok())) else { + return result; + }; + + let current_version = version_output.trim().to_string(); + let supported = help_stdout.contains(ACP_ARG) || current_version.parse::().is_ok_and(|version| version >= Self::MINIMUM_VERSION.parse::().unwrap()); + + log::error!("failed to create ACP connection to gemini (version is {current_version}, supported: {supported}): {e}"); + log::debug!("gemini --help stdout: {help_stdout:?}"); + log::debug!("gemini --help stderr: {help_stderr:?}"); if !supported { return Err(LoadError::Unsupported { current_version: current_version.into(), From a790e514af4d6957aa1a14cc8190b2ff24a0484c Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Fri, 29 Aug 2025 14:58:54 -0300 Subject: [PATCH 444/744] Fix ACP permission request with new tool calls (#37182) Release Notes: - Gemini integration: Fixed a bug with permission requests when `always_allow_tool_calls` is enabled --- Cargo.lock | 1 + crates/acp_thread/Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 35 ++++++++++++++++++++++++++-- crates/acp_thread/src/connection.rs | 17 +++++++------- crates/agent2/src/agent.rs | 13 ++++------- crates/agent_servers/src/acp.rs | 36 ++++------------------------- 6 files changed, 53 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa1bcab9a68294baa4264916ef5a35adbeb20802..e201b4af804b0be95f100c34f93652b6ecf6f8e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,7 @@ version = "0.1.0" dependencies = [ "action_log", "agent-client-protocol", + "agent_settings", "anyhow", "buffer_diff", "collections", diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index eab756db51885b8b2e2797bbf0303937f19fefb9..196614f731c6e330328e46eb75ba58cf928cf6cc 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -19,6 +19,7 @@ test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"] action_log.workspace = true agent-client-protocol.workspace = true anyhow.workspace = true +agent_settings.workspace = true buffer_diff.workspace = true collections.workspace = true editor.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 04ff032ad40c600c80fed7cff9f48139b2307931..394619732a72c205b6c5c940cc8b2b7d3a6d3d38 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -3,6 +3,7 @@ mod diff; mod mention; mod terminal; +use agent_settings::AgentSettings; use collections::HashSet; pub use connection::*; pub use diff::*; @@ -10,6 +11,7 @@ use language::language_settings::FormatOnSave; pub use mention::*; use project::lsp_store::{FormatTrigger, LspFormatTarget}; use serde::{Deserialize, Serialize}; +use settings::Settings as _; pub use terminal::*; use action_log::ActionLog; @@ -1230,9 +1232,29 @@ impl AcpThread { tool_call: acp::ToolCallUpdate, options: Vec, cx: &mut Context, - ) -> Result, acp::Error> { + ) -> Result> { let (tx, rx) = oneshot::channel(); + if AgentSettings::get_global(cx).always_allow_tool_actions { + // Don't use AllowAlways, because then if you were to turn off always_allow_tool_actions, + // some tools would (incorrectly) continue to auto-accept. + if let Some(allow_once_option) = options.iter().find_map(|option| { + if matches!(option.kind, acp::PermissionOptionKind::AllowOnce) { + Some(option.id.clone()) + } else { + None + } + }) { + self.upsert_tool_call_inner(tool_call, ToolCallStatus::Pending, cx)?; + return Ok(async { + acp::RequestPermissionOutcome::Selected { + option_id: allow_once_option, + } + } + .boxed()); + } + } + let status = ToolCallStatus::WaitingForConfirmation { options, respond_tx: tx, @@ -1240,7 +1262,16 @@ impl AcpThread { self.upsert_tool_call_inner(tool_call, status, cx)?; cx.emit(AcpThreadEvent::ToolAuthorizationRequired); - Ok(rx) + + let fut = async { + match rx.await { + Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, + Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, + } + } + .boxed(); + + Ok(fut) } pub fn authorize_tool_call( diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index af229b7545651c2f19f361afc7ea0abadcb5cc76..96abd1d2b4cf92698e7046cd4b7e24e6043280ff 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -393,14 +393,15 @@ mod test_support { }; 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_authorization( - tool_call.clone().into(), - options.clone(), - cx, - ) - })?; - permission?.await?; + thread + .update(cx, |thread, cx| { + thread.request_tool_call_authorization( + tool_call.clone().into(), + options.clone(), + cx, + ) + })?? + .await; } thread.update(cx, |thread, cx| { thread.handle_session_update(update.clone(), cx).unwrap(); diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index ea80df8fb52cffab80c8c64307b75de7f0954a56..bb6a3c097ca27d6103c1072986f6d3255bc6c69f 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -762,18 +762,15 @@ impl NativeAgentConnection { options, response, }) => { - let recv = acp_thread.update(cx, |thread, cx| { + let outcome_task = acp_thread.update(cx, |thread, cx| { thread.request_tool_call_authorization(tool_call, options, cx) - })?; + })??; cx.background_spawn(async move { - if let Some(recv) = recv.log_err() - && let Some(option) = recv - .await - .context("authorization sender was dropped") - .log_err() + if let acp::RequestPermissionOutcome::Selected { option_id } = + outcome_task.await { response - .send(option) + .send(option_id) .map(|_| anyhow!("authorization receiver was dropped")) .log_err(); } diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index d929d1fc501fb2093f47f8bdeb4d3695b7b87ebf..b1d4bea5c35c113277847690906dd2f21e12050c 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -3,15 +3,13 @@ use acp_thread::AgentConnection; use acp_tools::AcpConnectionRegistry; use action_log::ActionLog; use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; -use agent_settings::AgentSettings; use anyhow::anyhow; use collections::HashMap; use futures::AsyncBufReadExt as _; -use futures::channel::oneshot; use futures::io::BufReader; use project::Project; use serde::Deserialize; -use settings::Settings as _; + use std::{any::Any, cell::RefCell}; use std::{path::Path, rc::Rc}; use thiserror::Error; @@ -345,28 +343,7 @@ impl acp::Client for ClientDelegate { ) -> Result { let cx = &mut self.cx.clone(); - // If always_allow_tool_actions is enabled, then auto-choose the first "Allow" button - if AgentSettings::try_read_global(cx, |settings| settings.always_allow_tool_actions) - .unwrap_or(false) - { - // Don't use AllowAlways, because then if you were to turn off always_allow_tool_actions, - // some tools would (incorrectly) continue to auto-accept. - if let Some(allow_once_option) = arguments.options.iter().find_map(|option| { - if matches!(option.kind, acp::PermissionOptionKind::AllowOnce) { - Some(option.id.clone()) - } else { - None - } - }) { - return Ok(acp::RequestPermissionResponse { - outcome: acp::RequestPermissionOutcome::Selected { - option_id: allow_once_option, - }, - }); - } - } - - let rx = self + let task = self .sessions .borrow() .get(&arguments.session_id) @@ -374,14 +351,9 @@ impl acp::Client for ClientDelegate { .thread .update(cx, |thread, cx| { thread.request_tool_call_authorization(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, - }; + let outcome = task.await; Ok(acp::RequestPermissionResponse { outcome }) } From fcc3d1092fc2c0323d6f06e93e06c5ed8fad2c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raphael=20L=C3=BCthy?= Date: Fri, 29 Aug 2025 22:17:22 +0200 Subject: [PATCH 445/744] supermaven: Improve completion caching and position validation (#37047) Closes #36981 - Add completion text and position caching to reduce redundant API calls - Only trigger new completion requests on text changes, not cursor movement - Validate cursor position to ensure completions show at correct location - Improve end-of-line range calculation for more accurate deletions - Extract reset_completion_cache helper for cleaner code organization - Update completion diff algorithm documentation for clarity Edit: Sorry this is the 2nd PR, I forgot that the forks history was messy; I cherrypicked and cleaned it properly with this PR Release Notes: - supermaven: Improved caching of predictions - supermaven: Fixed an issue where changing cursor position would incorrectly trigger new completions --- .../src/supermaven_completion_provider.rs | 93 +++++++++++++++---- 1 file changed, 77 insertions(+), 16 deletions(-) diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index eb54c83f8126002a19728e51b282b98191707717..89c5129822d94229cd1644587f15f4a4de2bf86a 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -19,8 +19,10 @@ pub struct SupermavenCompletionProvider { supermaven: Entity, buffer_id: Option, completion_id: Option, + completion_text: Option, file_extension: Option, pending_refresh: Option>>, + completion_position: Option, } impl SupermavenCompletionProvider { @@ -29,16 +31,19 @@ impl SupermavenCompletionProvider { supermaven, buffer_id: None, completion_id: None, + completion_text: None, file_extension: None, pending_refresh: None, + completion_position: None, } } } // Computes the edit prediction from the difference between the completion text. -// this is defined by greedily matching the buffer text against the completion text, with any leftover buffer placed at the end. -// for example, given the completion text "moo cows are cool" and the buffer text "cowsre pool", the completion state would be -// the inlays "moo ", " a", and "cool" which will render as "[moo ]cows[ a]re [cool]pool" in the editor. +// This is defined by greedily matching the buffer text against the completion text. +// Inlays are inserted for parts of the completion text that are not present in the buffer text. +// For example, given the completion text "axbyc" and the buffer text "xy", the rendered output in the editor would be "[a]x[b]y[c]". +// The parts in brackets are the inlays. fn completion_from_diff( snapshot: BufferSnapshot, completion_text: &str, @@ -133,6 +138,14 @@ impl EditPredictionProvider for SupermavenCompletionProvider { debounce: bool, cx: &mut Context, ) { + // Only make new completion requests when debounce is true (i.e., when text is typed) + // When debounce is false (i.e., cursor movement), we should not make new requests + if !debounce { + return; + } + + reset_completion_cache(self, cx); + let Some(mut completion) = self.supermaven.update(cx, |supermaven, cx| { supermaven.complete(&buffer_handle, cursor_position, cx) }) else { @@ -146,6 +159,17 @@ impl EditPredictionProvider for SupermavenCompletionProvider { while let Some(()) = completion.updates.next().await { this.update(cx, |this, cx| { + // Get the completion text and cache it + if let Some(text) = + this.supermaven + .read(cx) + .completion(&buffer_handle, cursor_position, cx) + { + this.completion_text = Some(text.to_string()); + + this.completion_position = Some(cursor_position); + } + this.completion_id = Some(completion.id); this.buffer_id = Some(buffer_handle.entity_id()); this.file_extension = buffer_handle.read(cx).file().and_then(|file| { @@ -156,7 +180,6 @@ impl EditPredictionProvider for SupermavenCompletionProvider { .to_string(), ) }); - this.pending_refresh = None; cx.notify(); })?; } @@ -174,13 +197,11 @@ impl EditPredictionProvider for SupermavenCompletionProvider { } fn accept(&mut self, _cx: &mut Context) { - self.pending_refresh = None; - self.completion_id = None; + reset_completion_cache(self, _cx); } fn discard(&mut self, _cx: &mut Context) { - self.pending_refresh = None; - self.completion_id = None; + reset_completion_cache(self, _cx); } fn suggest( @@ -189,10 +210,34 @@ impl EditPredictionProvider for SupermavenCompletionProvider { cursor_position: Anchor, cx: &mut Context, ) -> Option { - let completion_text = self - .supermaven - .read(cx) - .completion(buffer, cursor_position, cx)?; + if self.buffer_id != Some(buffer.entity_id()) { + return None; + } + + if self.completion_id.is_none() { + return None; + } + + let completion_text = if let Some(cached_text) = &self.completion_text { + cached_text.as_str() + } else { + let text = self + .supermaven + .read(cx) + .completion(buffer, cursor_position, cx)?; + self.completion_text = Some(text.to_string()); + text + }; + + // Check if the cursor is still at the same position as the completion request + // If we don't have a completion position stored, don't show the completion + if let Some(completion_position) = self.completion_position { + if cursor_position != completion_position { + return None; + } + } else { + return None; + } let completion_text = trim_to_end_of_line_unless_leading_newline(completion_text); @@ -200,15 +245,20 @@ impl EditPredictionProvider for SupermavenCompletionProvider { if !completion_text.trim().is_empty() { let snapshot = buffer.read(cx).snapshot(); - let mut point = cursor_position.to_point(&snapshot); - point.column = snapshot.line_len(point.row); - let range = cursor_position..snapshot.anchor_after(point); + + // Calculate the range from cursor to end of line correctly + let cursor_point = cursor_position.to_point(&snapshot); + let end_of_line = snapshot.anchor_after(language::Point::new( + cursor_point.row, + snapshot.line_len(cursor_point.row), + )); + let delete_range = cursor_position..end_of_line; Some(completion_from_diff( snapshot, completion_text, cursor_position, - range, + delete_range, )) } else { None @@ -216,6 +266,17 @@ impl EditPredictionProvider for SupermavenCompletionProvider { } } +fn reset_completion_cache( + provider: &mut SupermavenCompletionProvider, + _cx: &mut Context, +) { + provider.pending_refresh = None; + provider.completion_id = None; + provider.completion_text = None; + provider.completion_position = None; + provider.buffer_id = None; +} + fn trim_to_end_of_line_unless_leading_newline(text: &str) -> &str { if has_leading_newline(text) { text From e9252a7a74b6af4002639405b32b6167da810fe6 Mon Sep 17 00:00:00 2001 From: Dino Date: Fri, 29 Aug 2025 21:23:44 +0100 Subject: [PATCH 446/744] editor: Context menu aside scrolling (#35985) Add support for scrolling the contents rendered aside an `editor::code_context_menus::CodeContextMenu` by introducing the `scroll_aside` method. For now this method is only implemented for the `CodeContextMenu::Completions` variant, which will scroll the aside contents for an `editor::code_context_menus::CompletionsMenu` element, as a `ScrollHandle` is added to the aside content that is rendered. In order to be possible to trigger this via keybindings, a new editor action is introduced, `ContextMenuScrollAside`, which accepts a number of lines or pages to scroll the content by. Lastly, the default keymaps for both MacOS and Linux, as well as for Zed's vim mode, are updated to ensure that the following keybindings are supported when a completion menu is open and the completion item's documentation is rendered aside: - `ctrl-e` - `ctrl-y` - `ctrl-d` - `ctrl-u` ### Recording https://github.com/user-attachments/assets/02043763-87ea-46f5-9768-00e907127b69 --- Closes #13194 Release Notes: - Added support for scrolling the documentation panel shown alongside the completion menu in the editor with `cltr-d`, `ctrl-u`, `ctrl-e` and `ctrl-y` --------- Co-authored-by: Conrad Irwin Co-authored-by: MrSubidubi --- assets/keymaps/vim.json | 11 ++- crates/editor/src/code_context_menus.rs | 43 ++++++++++- crates/editor/src/hover_links.rs | 18 +++-- crates/editor/src/hover_popover.rs | 2 +- crates/editor/src/scroll/scroll_amount.rs | 2 +- crates/vim/src/normal/scroll.rs | 6 +- crates/vim/src/test.rs | 87 ++++++++++++++++++++++- crates/vim/src/test/vim_test_context.rs | 4 ++ 8 files changed, 156 insertions(+), 17 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 0a88baee027a3ae4d72409f5f142ceda3f4d9717..bd6eb3982cd9860b2635a3390d47484f1a6dbe55 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -324,7 +324,7 @@ } }, { - "context": "vim_mode == insert", + "context": "vim_mode == insert && !menu", "bindings": { "ctrl-c": "vim::NormalBefore", "ctrl-[": "vim::NormalBefore", @@ -354,6 +354,15 @@ "ctrl-s": "editor::ShowSignatureHelp" } }, + { + "context": "showing_completions", + "bindings": { + "ctrl-d": "vim::ScrollDown", + "ctrl-u": "vim::ScrollUp", + "ctrl-e": "vim::LineDown", + "ctrl-y": "vim::LineUp" + } + }, { "context": "(vim_mode == normal || vim_mode == helix_normal) && !menu", "bindings": { diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 96809d68777ca8d84623c308bb8b06eec493a5be..01e74284eff4cb140efe43202ef5dda9a002f94d 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1,7 +1,9 @@ +use crate::scroll::ScrollAmount; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString, - Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px, uniform_list, + AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollHandle, ScrollStrategy, + SharedString, Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px, + uniform_list, }; use itertools::Itertools; use language::CodeLabel; @@ -184,6 +186,20 @@ impl CodeContextMenu { CodeContextMenu::CodeActions(_) => false, } } + + pub fn scroll_aside( + &mut self, + scroll_amount: ScrollAmount, + window: &mut Window, + cx: &mut Context, + ) { + match self { + CodeContextMenu::Completions(completions_menu) => { + completions_menu.scroll_aside(scroll_amount, window, cx) + } + CodeContextMenu::CodeActions(_) => (), + } + } } pub enum ContextMenuOrigin { @@ -207,6 +223,9 @@ pub struct CompletionsMenu { filter_task: Task<()>, cancel_filter: Arc, scroll_handle: UniformListScrollHandle, + // The `ScrollHandle` used on the Markdown documentation rendered on the + // side of the completions menu. + pub scroll_handle_aside: ScrollHandle, resolve_completions: bool, show_completion_documentation: bool, last_rendered_range: Rc>>>, @@ -279,6 +298,7 @@ impl CompletionsMenu { filter_task: Task::ready(()), cancel_filter: Arc::new(AtomicBool::new(false)), scroll_handle: UniformListScrollHandle::new(), + scroll_handle_aside: ScrollHandle::new(), resolve_completions: true, last_rendered_range: RefCell::new(None).into(), markdown_cache: RefCell::new(VecDeque::new()).into(), @@ -348,6 +368,7 @@ impl CompletionsMenu { filter_task: Task::ready(()), cancel_filter: Arc::new(AtomicBool::new(false)), scroll_handle: UniformListScrollHandle::new(), + scroll_handle_aside: ScrollHandle::new(), resolve_completions: false, show_completion_documentation: false, last_rendered_range: RefCell::new(None).into(), @@ -911,6 +932,7 @@ impl CompletionsMenu { .max_w(max_size.width) .max_h(max_size.height) .overflow_y_scroll() + .track_scroll(&self.scroll_handle_aside) .occlude(), ) .into_any_element(), @@ -1175,6 +1197,23 @@ impl CompletionsMenu { } }); } + + pub fn scroll_aside( + &mut self, + amount: ScrollAmount, + window: &mut Window, + cx: &mut Context, + ) { + let mut offset = self.scroll_handle_aside.offset(); + + offset.y -= amount.pixels( + window.line_height(), + self.scroll_handle_aside.bounds().size.height - px(16.), + ) / 2.0; + + cx.notify(); + self.scroll_handle_aside.set_offset(offset); + } } #[derive(Clone)] diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 94f49f601a101cd8ca2556df9ec1568b5e7337fa..ba0b6f88683969aca3818a2795aa6b8454de3bb8 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -188,22 +188,26 @@ impl Editor { pub fn scroll_hover( &mut self, - amount: &ScrollAmount, + amount: ScrollAmount, window: &mut Window, cx: &mut Context, ) -> bool { let selection = self.selections.newest_anchor().head(); let snapshot = self.snapshot(window, cx); - let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| { + if let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| { popover .symbol_range .point_within_range(&TriggerPoint::Text(selection), &snapshot) - }) else { - return false; - }; - popover.scroll(amount, window, cx); - true + }) { + popover.scroll(amount, window, cx); + true + } else if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { + context_menu.scroll_aside(amount, window, cx); + true + } else { + false + } } fn cmd_click_reveal_task( diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index fab53457876866223be6b7d32f964cd1abd1dd28..6541f76a56e671fb414e28d83adc6b0459e288a8 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -896,7 +896,7 @@ impl InfoPopover { .into_any_element() } - pub fn scroll(&self, amount: &ScrollAmount, window: &mut Window, cx: &mut Context) { + pub fn scroll(&self, amount: ScrollAmount, window: &mut Window, cx: &mut Context) { let mut current = self.scroll_handle.offset(); current.y -= amount.pixels( window.line_height(), diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs index 5992c9023c1f9d6eb7e7eb201099c6eef17a33d8..43f1aa128548597ee07cbb297ab5aaf0e8f79b6e 100644 --- a/crates/editor/src/scroll/scroll_amount.rs +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -15,7 +15,7 @@ impl ScrollDirection { } } -#[derive(Debug, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] pub enum ScrollAmount { // Scroll N lines (positive is towards the end of the document) Line(f32), diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 9eb8367f57ade6a7ccf090f0e16d87e73f4a9f25..eeb98692bc30c5c8c39c0be23ba17b3276b708df 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -98,7 +98,7 @@ impl Vim { Vim::take_forced_motion(cx); self.exit_temporary_normal(window, cx); self.update_editor(cx, |_, editor, cx| { - scroll_editor(editor, move_cursor, &amount, window, cx) + scroll_editor(editor, move_cursor, amount, window, cx) }); } } @@ -106,7 +106,7 @@ impl Vim { fn scroll_editor( editor: &mut Editor, preserve_cursor_position: bool, - amount: &ScrollAmount, + amount: ScrollAmount, window: &mut Window, cx: &mut Context, ) { @@ -126,7 +126,7 @@ fn scroll_editor( ScrollAmount::Line(amount.lines(visible_line_count) - 1.0) } } - _ => amount.clone(), + _ => amount, }; editor.scroll_screen(&amount, window, cx); diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index ce04b621cb91c7b6b7da57bd1e1b74e9c0e00bbc..84376719d141fa4862a3e7a1b0f6116dd809bfe5 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -8,13 +8,15 @@ use collections::HashMap; use command_palette::CommandPalette; use editor::{ AnchorRangeExt, DisplayPoint, Editor, EditorMode, MultiBuffer, actions::DeleteLine, - display_map::DisplayRow, test::editor_test_context::EditorTestContext, + code_context_menus::CodeContextMenu, display_map::DisplayRow, + test::editor_test_context::EditorTestContext, }; use futures::StreamExt; -use gpui::{KeyBinding, Modifiers, MouseButton, TestAppContext}; +use gpui::{KeyBinding, Modifiers, MouseButton, TestAppContext, px}; use language::Point; pub use neovim_backed_test_context::*; use settings::SettingsStore; +use ui::Pixels; use util::test::marked_text_ranges; pub use vim_test_context::*; @@ -971,6 +973,87 @@ async fn test_comma_w(cx: &mut gpui::TestAppContext) { .assert_eq("hellˇo hello\nhello hello"); } +#[gpui::test] +async fn test_completion_menu_scroll_aside(cx: &mut TestAppContext) { + let mut cx = VimTestContext::new_typescript(cx).await; + + cx.lsp + .set_request_handler::(move |_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "Test Item".to_string(), + documentation: Some(lsp::Documentation::String( + "This is some very long documentation content that will be displayed in the aside panel for scrolling.\n".repeat(50) + )), + ..Default::default() + }, + ]))) + }); + + cx.set_state("variableˇ", Mode::Insert); + cx.simulate_keystroke("."); + cx.executor().run_until_parked(); + + let mut initial_offset: Pixels = px(0.0); + + cx.update_editor(|editor, _, _| { + let binding = editor.context_menu().borrow(); + let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { + panic!("Should have completions menu open"); + }; + + initial_offset = menu.scroll_handle_aside.offset().y; + }); + + // The `ctrl-e` shortcut should scroll the completion menu's aside content + // down, so the updated offset should be lower than the initial offset. + cx.simulate_keystroke("ctrl-e"); + cx.update_editor(|editor, _, _| { + let binding = editor.context_menu().borrow(); + let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { + panic!("Should have completions menu open"); + }; + + assert!(menu.scroll_handle_aside.offset().y < initial_offset); + }); + + // The `ctrl-y` shortcut should do the inverse scrolling as `ctrl-e`, so the + // offset should now be the same as the initial offset. + cx.simulate_keystroke("ctrl-y"); + cx.update_editor(|editor, _, _| { + let binding = editor.context_menu().borrow(); + let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { + panic!("Should have completions menu open"); + }; + + assert_eq!(menu.scroll_handle_aside.offset().y, initial_offset); + }); + + // The `ctrl-d` shortcut should scroll the completion menu's aside content + // down, so the updated offset should be lower than the initial offset. + cx.simulate_keystroke("ctrl-d"); + cx.update_editor(|editor, _, _| { + let binding = editor.context_menu().borrow(); + let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { + panic!("Should have completions menu open"); + }; + + assert!(menu.scroll_handle_aside.offset().y < initial_offset); + }); + + // The `ctrl-u` shortcut should do the inverse scrolling as `ctrl-u`, so the + // offset should now be the same as the initial offset. + cx.simulate_keystroke("ctrl-u"); + cx.update_editor(|editor, _, _| { + let binding = editor.context_menu().borrow(); + let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { + panic!("Should have completions menu open"); + }; + + assert_eq!(menu.scroll_handle_aside.offset().y, initial_offset); + }); +} + #[gpui::test] async fn test_rename(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new_typescript(cx).await; diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index e7ac692df14cb482656d930efa2313e85c27a4bc..ef9588acae181bad2b079d7c89458458bb851a64 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -49,6 +49,10 @@ impl VimTestContext { Self::new_with_lsp( EditorLspTestContext::new_typescript( lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }), rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { prepare_provider: Some(true), work_done_progress_options: Default::default(), From f2c3f3b168bab7c808e3ce2c3392b0c692919f81 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:56:10 -0400 Subject: [PATCH 447/744] settings ui: Start work on creating the initial structure (#36904) ## Goal This PR creates the initial settings ui structure with the primary goal of making a settings UI that is - Comprehensive: All settings are available through the UI - Correct: Easy to understand the underlying JSON file from the UI - Intuitive - Easy to implement per setting so that UI is not a hindrance to future settings changes ### Structure The overall structure is settings layer -> data layer -> ui layer. The settings layer is the pre-existing settings definitions, that implement the `Settings` trait. The data layer is constructed from settings primarily through the `SettingsUi` trait, and it's associated derive macro. The data layer tracks the grouping of the settings, the json path of the settings, and a data representation of how to render the controls for the setting in the UI, that is either a marker value for the component to use (avoiding a dependency on the `ui` crate) or a custom render function. Abstracting the data layer from the ui layer allows crates depending on `settings` to implement their own UI without having to add additional UI dependencies, thus avoiding circular dependencies. In cases where custom UI is desired, and a creating a custom render function in the same crate is infeasible due to circular dependencies, the current solution is to implement a marker for the component in the `settings` crate, and then handle the rendering of that component in `settings_ui`. ### Foundation This PR creates a macro and a trait both called `SettingsUi`. The `SettingsUi` trait is added as a new trait bound on the `Settings` trait, this allows the type system to guarantee that all settings implement UI functionality. The macro is used to derived the trait for most types, and can be modified through attributes for unique cases as well. A derive-macro is used to generate the settings UI trait impl, allowing it the UI generation to be generated from the static information in our code base (`default.json`, Struct/Enum names, field names, `serde` attributes, etc). This allows the UI to be auto-generated for the most part, and ensures consistency across the UI. #### Immediate Follow ups - Add a new `SettingsPath` trait that will be a trait bound on `SettingsUi` and `Settings` - This trait will replace the `Settings::key` value to enable `SettingsUi` to infer the json path of it's derived type - Figure out how to render `Option where T: SettingsUi` correctly - Handle `serde` attributes in the `SettingsUi` proc macro to correctly get json path from a type's field and identity Release Notes: - N/A --------- Co-authored-by: Ben Kunkle --- Cargo.lock | 75 ++- Cargo.toml | 10 +- assets/settings/default.json | 3 + crates/agent_servers/src/settings.rs | 4 +- crates/agent_settings/src/agent_settings.rs | 4 +- crates/agent_ui/src/slash_command_settings.rs | 4 +- crates/audio/src/audio_settings.rs | 4 +- crates/auto_update/src/auto_update.rs | 3 +- crates/call/src/call_settings.rs | 4 +- crates/client/src/client.rs | 8 +- crates/collab_ui/src/panel_settings.rs | 10 +- crates/dap/src/debugger_settings.rs | 8 +- crates/editor/src/editor_settings.rs | 4 +- .../extension_host/src/extension_settings.rs | 4 +- .../file_finder/src/file_finder_settings.rs | 4 +- crates/git_hosting_providers/src/settings.rs | 4 +- crates/git_ui/src/git_panel_settings.rs | 4 +- crates/go_to_line/src/cursor_position.rs | 4 +- crates/gpui_macros/src/derive_action.rs | 7 + .../image_viewer/src/image_viewer_settings.rs | 4 +- crates/journal/src/journal.rs | 4 +- crates/keymap_editor/Cargo.toml | 53 ++ crates/keymap_editor/LICENSE-GPL | 1 + .../src/keymap_editor.rs} | 6 +- .../src/ui_components/keystroke_input.rs | 0 .../src/ui_components/mod.rs | 0 .../src/ui_components/table.rs | 0 crates/language/src/language_settings.rs | 4 +- crates/language_models/src/settings.rs | 4 +- .../src/outline_panel_settings.rs | 4 +- crates/project/src/project.rs | 2 +- crates/project/src/project_settings.rs | 4 +- .../src/project_panel_settings.rs | 4 +- crates/recent_projects/src/ssh_connections.rs | 4 +- crates/repl/src/jupyter_settings.rs | 4 +- crates/settings/Cargo.toml | 1 + crates/settings/src/base_keymap_setting.rs | 8 +- crates/settings/src/settings.rs | 4 + crates/settings/src/settings_json.rs | 12 +- crates/settings/src/settings_store.rs | 144 +++-- crates/settings/src/settings_ui.rs | 118 +++++ crates/settings/src/vscode_import.rs | 4 +- crates/settings_ui/Cargo.toml | 41 +- crates/settings_ui/src/settings_ui.rs | 500 +++++++++++++++++- crates/settings_ui_macros/Cargo.toml | 22 + crates/settings_ui_macros/LICENSE-GPL | 1 + .../src/settings_ui_macros.rs | 201 +++++++ crates/terminal/src/terminal_settings.rs | 4 +- crates/theme/src/settings.rs | 4 +- crates/title_bar/Cargo.toml | 2 +- crates/title_bar/src/title_bar.rs | 6 +- crates/title_bar/src/title_bar_settings.rs | 5 +- crates/vim/src/vim.rs | 4 +- .../vim_mode_setting/src/vim_mode_setting.rs | 4 +- crates/workspace/src/item.rs | 6 +- crates/workspace/src/workspace_settings.rs | 6 +- crates/worktree/src/worktree_settings.rs | 4 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + crates/zed/src/zed.rs | 2 +- crates/zed/src/zed/app_menus.rs | 3 +- crates/zlog_settings/src/zlog_settings.rs | 4 +- 62 files changed, 1149 insertions(+), 229 deletions(-) create mode 100644 crates/keymap_editor/Cargo.toml create mode 120000 crates/keymap_editor/LICENSE-GPL rename crates/{settings_ui/src/keybindings.rs => keymap_editor/src/keymap_editor.rs} (99%) rename crates/{settings_ui => keymap_editor}/src/ui_components/keystroke_input.rs (100%) rename crates/{settings_ui => keymap_editor}/src/ui_components/mod.rs (100%) rename crates/{settings_ui => keymap_editor}/src/ui_components/table.rs (100%) create mode 100644 crates/settings/src/settings_ui.rs create mode 100644 crates/settings_ui_macros/Cargo.toml create mode 120000 crates/settings_ui_macros/LICENSE-GPL create mode 100644 crates/settings_ui_macros/src/settings_ui_macros.rs diff --git a/Cargo.lock b/Cargo.lock index e201b4af804b0be95f100c34f93652b6ecf6f8e6..4c68280de25b878187b3a5627362f6373808734b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8951,6 +8951,44 @@ dependencies = [ "uuid", ] +[[package]] +name = "keymap_editor" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "command_palette", + "component", + "db", + "editor", + "fs", + "fuzzy", + "gpui", + "itertools 0.14.0", + "language", + "log", + "menu", + "notifications", + "paths", + "project", + "search", + "serde", + "serde_json", + "settings", + "telemetry", + "tempfile", + "theme", + "tree-sitter-json", + "tree-sitter-rust", + "ui", + "ui_input", + "util", + "vim", + "workspace", + "workspace-hack", + "zed_actions", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -14856,6 +14894,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_json_lenient", + "settings_ui_macros", "smallvec", "tree-sitter", "tree-sitter-json", @@ -14891,39 +14930,28 @@ name = "settings_ui" version = "0.1.0" dependencies = [ "anyhow", - "collections", - "command_palette", "command_palette_hooks", - "component", - "db", "editor", "feature_flags", - "fs", - "fuzzy", "gpui", - "itertools 0.14.0", - "language", - "log", - "menu", - "notifications", - "paths", - "project", - "search", "serde", "serde_json", "settings", - "telemetry", - "tempfile", + "smallvec", "theme", - "tree-sitter-json", - "tree-sitter-rust", "ui", - "ui_input", - "util", - "vim", "workspace", "workspace-hack", - "zed_actions", +] + +[[package]] +name = "settings_ui_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "workspace-hack", ] [[package]] @@ -16739,6 +16767,7 @@ dependencies = [ "db", "gpui", "http_client", + "keymap_editor", "notifications", "pretty_assertions", "project", @@ -16747,7 +16776,6 @@ dependencies = [ "schemars", "serde", "settings", - "settings_ui", "smallvec", "story", "telemetry", @@ -20458,6 +20486,7 @@ dependencies = [ "itertools 0.14.0", "jj_ui", "journal", + "keymap_editor", "language", "language_extension", "language_model", diff --git a/Cargo.toml b/Cargo.toml index d346043c0ef64b3cce0827c2553c5b3c254d66f7..b64113311adb2662562cc4ae488054f54d569c3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,8 @@ members = [ "crates/deepseek", "crates/diagnostics", "crates/docs_preprocessor", + "crates/edit_prediction", + "crates/edit_prediction_button", "crates/editor", "crates/eval", "crates/explorer_command_injector", @@ -82,13 +84,12 @@ members = [ "crates/http_client_tls", "crates/icons", "crates/image_viewer", - "crates/edit_prediction", - "crates/edit_prediction_button", "crates/inspector_ui", "crates/install_cli", "crates/jj", "crates/jj_ui", "crates/journal", + "crates/keymap_editor", "crates/language", "crates/language_extension", "crates/language_model", @@ -146,6 +147,7 @@ members = [ "crates/settings", "crates/settings_profile_selector", "crates/settings_ui", + "crates/settings_ui_macros", "crates/snippet", "crates/snippet_provider", "crates/snippets_ui", @@ -156,9 +158,9 @@ members = [ "crates/streaming_diff", "crates/sum_tree", "crates/supermaven", - "crates/system_specs", "crates/supermaven_api", "crates/svg_preview", + "crates/system_specs", "crates/tab_switcher", "crates/task", "crates/tasks_ui", @@ -314,6 +316,7 @@ install_cli = { path = "crates/install_cli" } jj = { path = "crates/jj" } jj_ui = { path = "crates/jj_ui" } journal = { path = "crates/journal" } +keymap_editor = { path = "crates/keymap_editor" } language = { path = "crates/language" } language_extension = { path = "crates/language_extension" } language_model = { path = "crates/language_model" } @@ -373,6 +376,7 @@ semantic_version = { path = "crates/semantic_version" } session = { path = "crates/session" } settings = { path = "crates/settings" } settings_ui = { path = "crates/settings_ui" } +settings_ui_macros = { path = "crates/settings_ui_macros" } snippet = { path = "crates/snippet" } snippet_provider = { path = "crates/snippet_provider" } snippets_ui = { path = "crates/snippets_ui" } diff --git a/assets/settings/default.json b/assets/settings/default.json index 572193be4eecbeb63a19eab1811bff126638162b..b15eb6e5ce8de85bb088108f065a31494b9087a1 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1922,7 +1922,10 @@ "debugger": { "stepping_granularity": "line", "save_breakpoints": true, + "timeout": 2000, "dock": "bottom", + "log_dap_communications": true, + "format_dap_log_messages": true, "button": true }, // Configures any number of settings profiles that are temporarily applied on diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 81f80a7d7d9581b8c1862ae3393c4a5d5e6706b6..693d7d7b7014b3abbecfbe592bac67210b336872 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -6,13 +6,13 @@ use collections::HashMap; use gpui::{App, SharedString}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; pub fn init(cx: &mut App) { AllAgentServersSettings::register(cx); } -#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)] +#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi)] pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index ed1ed2b89879c18eceaab22843390a766e4f6c77..3808cc510f7941107f6e4ab90c9a5f8a2c3d920a 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -8,7 +8,7 @@ use gpui::{App, Pixels, SharedString}; use language_model::LanguageModel; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use std::borrow::Cow; pub use crate::agent_profile::*; @@ -48,7 +48,7 @@ pub enum NotifyWhenAgentWaiting { Never, } -#[derive(Default, Clone, Debug)] +#[derive(Default, Clone, Debug, SettingsUi)] pub struct AgentSettings { pub enabled: bool, pub button: bool, diff --git a/crates/agent_ui/src/slash_command_settings.rs b/crates/agent_ui/src/slash_command_settings.rs index 73e5622aa921ccf03a3813717446e830c21079b8..c54a10ed49a77d395c4968e551b1cd30ad1c6e07 100644 --- a/crates/agent_ui/src/slash_command_settings.rs +++ b/crates/agent_ui/src/slash_command_settings.rs @@ -2,10 +2,10 @@ use anyhow::Result; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; /// Settings for slash commands. -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] +#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)] pub struct SlashCommandSettings { /// Settings for the `/cargo-workspace` slash command. #[serde(default)] diff --git a/crates/audio/src/audio_settings.rs b/crates/audio/src/audio_settings.rs index 807179881c7c3b27aad2e3142a84c730951eb709..e42918825cd3a25bb18d6f0b357801949520833f 100644 --- a/crates/audio/src/audio_settings.rs +++ b/crates/audio/src/audio_settings.rs @@ -2,9 +2,9 @@ use anyhow::Result; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Deserialize, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] pub struct AudioSettings { /// Opt into the new audio system. #[serde(rename = "experimental.rodio_audio", default)] diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 2150873cadd0a84b4a2894ebbe373d9bd0e007f0..71dcf25aeea9d8ebd4feb01db9161dc177fcdd26 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -10,7 +10,7 @@ use paths::remote_servers_dir; use release_channel::{AppCommitSha, ReleaseChannel}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsStore}; +use settings::{Settings, SettingsSources, SettingsStore, SettingsUi}; use smol::{fs, io::AsyncReadExt}; use smol::{fs::File, process::Command}; use std::{ @@ -113,6 +113,7 @@ impl Drop for MacOsUnmounter { } } +#[derive(SettingsUi)] struct AutoUpdateSetting(bool); /// Whether or not to automatically check for updates. diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs index c8f51e0c1a2019dd2c266210e469989946ed8a35..64d11d0df64eedbbc29f06b8205f0318d999ea30 100644 --- a/crates/call/src/call_settings.rs +++ b/crates/call/src/call_settings.rs @@ -2,9 +2,9 @@ use anyhow::Result; use gpui::App; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, SettingsUi)] pub struct CallSettings { pub mute_on_join: bool, pub share_on_join: bool, diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 1e735b0025f1e8a15809b096c5a462361d4ed8f3..c5bb1af0d7605cfcfc28d86bc389189d653e28ae 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -31,7 +31,7 @@ use release_channel::{AppVersion, ReleaseChannel}; use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use std::{ any::TypeId, convert::TryFrom, @@ -101,7 +101,7 @@ pub struct ClientSettingsContent { server_url: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, SettingsUi)] pub struct ClientSettings { pub server_url: String, } @@ -127,7 +127,7 @@ pub struct ProxySettingsContent { proxy: Option, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, SettingsUi)] pub struct ProxySettings { pub proxy: Option, } @@ -520,7 +520,7 @@ impl Drop for PendingEntitySubscription { } } -#[derive(Copy, Clone, Deserialize, Debug)] +#[derive(Copy, Clone, Deserialize, Debug, SettingsUi)] pub struct TelemetrySettings { pub diagnostics: bool, pub metrics: bool, diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index 652d9eb67f6ce1f0ab583e20e4feab05cfb743e3..4e5c8ad8f005d00a8802ab0a1f79ff7fbb3d0861 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/crates/collab_ui/src/panel_settings.rs @@ -1,10 +1,10 @@ use gpui::Pixels; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use workspace::dock::DockPosition; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, SettingsUi)] pub struct CollaborationPanelSettings { pub button: bool, pub dock: DockPosition, @@ -20,7 +20,7 @@ pub enum ChatPanelButton { WhenInCall, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, SettingsUi)] pub struct ChatPanelSettings { pub button: ChatPanelButton, pub dock: DockPosition, @@ -43,7 +43,7 @@ pub struct ChatPanelSettingsContent { pub default_width: Option, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, SettingsUi)] pub struct NotificationPanelSettings { pub button: bool, pub dock: DockPosition, @@ -66,7 +66,7 @@ pub struct PanelSettingsContent { pub default_width: Option, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] pub struct MessageEditorSettings { /// Whether to automatically replace emoji shortcodes with emoji characters. /// For example: typing `:wave:` gets replaced with `👋`. diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs index e1176633e5403116c2789161d654912337150e9a..6843f19e3811967084cc61a3874ec86451ab6faf 100644 --- a/crates/dap/src/debugger_settings.rs +++ b/crates/dap/src/debugger_settings.rs @@ -2,9 +2,9 @@ use dap_types::SteppingGranularity; use gpui::{App, Global}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)] #[serde(rename_all = "snake_case")] pub enum DebugPanelDockPosition { Left, @@ -12,12 +12,14 @@ pub enum DebugPanelDockPosition { Right, } -#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)] +#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi)] #[serde(default)] +#[settings_ui(group = "Debugger", path = "debugger")] pub struct DebuggerSettings { /// Determines the stepping granularity. /// /// Default: line + #[settings_ui(skip)] pub stepping_granularity: SteppingGranularity, /// Whether the breakpoints should be reused across Zed sessions. /// diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 55c040428d7e73d9e6e9bf6cc66cc20d301038f2..c2baa9de024b1988f9acb77a529936f947103f56 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -6,12 +6,12 @@ use language::CursorShape; use project::project_settings::DiagnosticSeverity; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, VsCodeSettings}; +use settings::{Settings, SettingsSources, SettingsUi, VsCodeSettings}; use util::serde::default_true; /// Imports from the VSCode settings at /// https://code.visualstudio.com/docs/reference/default-settings -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, SettingsUi)] pub struct EditorSettings { pub cursor_blink: bool, pub cursor_shape: Option, diff --git a/crates/extension_host/src/extension_settings.rs b/crates/extension_host/src/extension_settings.rs index cfa67990b09de9fda5bf0e26229a9b1b1410de46..6bd760795cec6d1c4208770f1355e8ac7a34eb95 100644 --- a/crates/extension_host/src/extension_settings.rs +++ b/crates/extension_host/src/extension_settings.rs @@ -3,10 +3,10 @@ use collections::HashMap; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use std::sync::Arc; -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] +#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)] pub struct ExtensionSettings { /// The extensions that should be automatically installed by Zed. /// diff --git a/crates/file_finder/src/file_finder_settings.rs b/crates/file_finder/src/file_finder_settings.rs index 350e1de3b36c9073d137993ce4fbc50aa43bb36e..20057417a2ddbce7acd7fd5a8e09e54aab779638 100644 --- a/crates/file_finder/src/file_finder_settings.rs +++ b/crates/file_finder/src/file_finder_settings.rs @@ -1,9 +1,9 @@ use anyhow::Result; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, SettingsUi)] pub struct FileFinderSettings { pub file_icons: bool, pub modal_max_width: Option, diff --git a/crates/git_hosting_providers/src/settings.rs b/crates/git_hosting_providers/src/settings.rs index 91179fea392bc38cfc2a513bfc391dd3eec6137d..34e3805a39ea8a13a6a2f79552a6a917c4597692 100644 --- a/crates/git_hosting_providers/src/settings.rs +++ b/crates/git_hosting_providers/src/settings.rs @@ -5,7 +5,7 @@ use git::GitHostingProviderRegistry; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; +use settings::{Settings, SettingsStore, SettingsUi}; use url::Url; use util::ResultExt as _; @@ -78,7 +78,7 @@ pub struct GitHostingProviderConfig { pub name: String, } -#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct GitHostingProviderSettings { /// The list of custom Git hosting providers. #[serde(default)] diff --git a/crates/git_ui/src/git_panel_settings.rs b/crates/git_ui/src/git_panel_settings.rs index b6891c7d256794b5b457669a20b17e6e41e4fd23..576949220405e408df1b23d189e661405c4c39e4 100644 --- a/crates/git_ui/src/git_panel_settings.rs +++ b/crates/git_ui/src/git_panel_settings.rs @@ -2,7 +2,7 @@ use editor::ShowScrollbar; use gpui::Pixels; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use workspace::dock::DockPosition; #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -77,7 +77,7 @@ pub struct GitPanelSettingsContent { pub collapse_untracked_diff: Option, } -#[derive(Deserialize, Debug, Clone, PartialEq)] +#[derive(Deserialize, Debug, Clone, PartialEq, SettingsUi)] pub struct GitPanelSettings { pub button: bool, pub dock: DockPosition, diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index e60a3651aae3f062b16fdfa7aa01a28e5c845e85..345af8a867c6ff6c1790450d2b28cd275c04ebbb 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -2,7 +2,7 @@ use editor::{Editor, EditorSettings, MultiBufferSnapshot}; use gpui::{App, Entity, FocusHandle, Focusable, Subscription, Task, WeakEntity}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use std::{fmt::Write, num::NonZeroU32, time::Duration}; use text::{Point, Selection}; use ui::{ @@ -293,7 +293,7 @@ impl StatusItemView for CursorPosition { } } -#[derive(Clone, Copy, Default, PartialEq, JsonSchema, Deserialize, Serialize)] +#[derive(Clone, Copy, Default, PartialEq, JsonSchema, Deserialize, Serialize, SettingsUi)] #[serde(rename_all = "snake_case")] pub(crate) enum LineIndicatorFormat { Short, diff --git a/crates/gpui_macros/src/derive_action.rs b/crates/gpui_macros/src/derive_action.rs index 9c7f97371d86eecc29dc16902ba9e392d53b8660..4e6c6277e452189657b4725b4027780a54cfed1d 100644 --- a/crates/gpui_macros/src/derive_action.rs +++ b/crates/gpui_macros/src/derive_action.rs @@ -16,6 +16,13 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream { let mut deprecated = None; let mut doc_str: Option = None; + /* + * + * #[action()] + * Struct Foo { + * bar: bool // is bar considered an attribute + } + */ for attr in &input.attrs { if attr.path().is_ident("action") { attr.parse_nested_meta(|meta| { diff --git a/crates/image_viewer/src/image_viewer_settings.rs b/crates/image_viewer/src/image_viewer_settings.rs index 1dcf99c0afcb3f69f48e2e1a82351852a4bf1c22..4949b266b4e03c7089d4bc25e2a223a0ce64a081 100644 --- a/crates/image_viewer/src/image_viewer_settings.rs +++ b/crates/image_viewer/src/image_viewer_settings.rs @@ -1,10 +1,10 @@ use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; /// The settings for the image viewer. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, SettingsUi)] pub struct ImageViewerSettings { /// The unit to use for displaying image file sizes. /// diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index c09ab6f764893589945f2c3cc00d71df84b8f77a..ffa24571c88a0f0e06252565261b1a6d285d098c 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -5,7 +5,7 @@ use editor::{Editor, SelectionEffects}; use gpui::{App, AppContext as _, Context, Window, actions}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use std::{ fs::OpenOptions, path::{Path, PathBuf}, @@ -22,7 +22,7 @@ actions!( ); /// Settings specific to journaling -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct JournalSettings { /// The path of the directory where journal entries are stored. /// diff --git a/crates/keymap_editor/Cargo.toml b/crates/keymap_editor/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..ae3af21239f22a8d01ec9e792a3ab0daed6080bb --- /dev/null +++ b/crates/keymap_editor/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "keymap_editor" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/keymap_editor.rs" + +[dependencies] +anyhow.workspace = true +collections.workspace = true +command_palette.workspace = true +component.workspace = true +db.workspace = true +editor.workspace = true +fs.workspace = true +fuzzy.workspace = true +gpui.workspace = true +itertools.workspace = true +language.workspace = true +log.workspace = true +menu.workspace = true +notifications.workspace = true +paths.workspace = true +project.workspace = true +search.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +telemetry.workspace = true +tempfile.workspace = true +theme.workspace = true +tree-sitter-json.workspace = true +tree-sitter-rust.workspace = true +ui.workspace = true +ui_input.workspace = true +util.workspace = true +vim.workspace = true +workspace-hack.workspace = true +workspace.workspace = true +zed_actions.workspace = true + +[dev-dependencies] +db = {"workspace"= true, "features" = ["test-support"]} +fs = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } +project = { workspace = true, features = ["test-support"] } +workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/keymap_editor/LICENSE-GPL b/crates/keymap_editor/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/keymap_editor/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings_ui/src/keybindings.rs b/crates/keymap_editor/src/keymap_editor.rs similarity index 99% rename from crates/settings_ui/src/keybindings.rs rename to crates/keymap_editor/src/keymap_editor.rs index 161e1e768ddd8a111e001198d8aad352169d1cef..12149061124d2b3144a32b7f54a65ce5af70d492 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/keymap_editor/src/keymap_editor.rs @@ -5,6 +5,8 @@ use std::{ time::Duration, }; +mod ui_components; + use anyhow::{Context as _, anyhow}; use collections::{HashMap, HashSet}; use editor::{CompletionProvider, Editor, EditorEvent}; @@ -34,8 +36,10 @@ use workspace::{ register_serializable_item, }; +pub use ui_components::*; + use crate::{ - keybindings::persistence::KEYBINDING_EDITORS, + persistence::KEYBINDING_EDITORS, ui_components::{ keystroke_input::{ClearKeystrokes, KeystrokeInput, StartRecording, StopRecording}, table::{ColumnWidths, ResizeBehavior, Table, TableInteractionState}, diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/keymap_editor/src/ui_components/keystroke_input.rs similarity index 100% rename from crates/settings_ui/src/ui_components/keystroke_input.rs rename to crates/keymap_editor/src/ui_components/keystroke_input.rs diff --git a/crates/settings_ui/src/ui_components/mod.rs b/crates/keymap_editor/src/ui_components/mod.rs similarity index 100% rename from crates/settings_ui/src/ui_components/mod.rs rename to crates/keymap_editor/src/ui_components/mod.rs diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/keymap_editor/src/ui_components/table.rs similarity index 100% rename from crates/settings_ui/src/ui_components/table.rs rename to crates/keymap_editor/src/ui_components/table.rs diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 0f82d3997f981286c81dc18c29f8763b0402ddd2..a44df4993af5f29cbfce337d2c90dd8f840d97a6 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -17,7 +17,7 @@ use serde::{ }; use settings::{ - ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore, + ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore, SettingsUi, }; use shellexpand; use std::{borrow::Cow, num::NonZeroU32, path::Path, slice, sync::Arc}; @@ -55,7 +55,7 @@ pub fn all_language_settings<'a>( } /// The settings for all languages. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, SettingsUi)] pub struct AllLanguageSettings { /// The edit prediction settings. pub edit_predictions: EditPredictionSettings, diff --git a/crates/language_models/src/settings.rs b/crates/language_models/src/settings.rs index b163585aa7b745447381aa62f710e8c5dbdf469c..1d03ab48f7de3ab9a20c1a099803e6b759b8ea81 100644 --- a/crates/language_models/src/settings.rs +++ b/crates/language_models/src/settings.rs @@ -5,7 +5,7 @@ use collections::HashMap; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use crate::provider::{ self, @@ -29,7 +29,7 @@ pub fn init_settings(cx: &mut App) { AllLanguageModelSettings::register(cx); } -#[derive(Default)] +#[derive(Default, SettingsUi)] pub struct AllLanguageModelSettings { pub anthropic: AnthropicSettings, pub bedrock: AmazonBedrockSettings, diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index 133d28b748d2978e07a540b3c8c7517b03dc4767..c33125654f043022bfaa7a31200d43d1d6326607 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -2,7 +2,7 @@ use editor::ShowScrollbar; use gpui::Pixels; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[serde(rename_all = "snake_case")] @@ -18,7 +18,7 @@ pub enum ShowIndentGuides { Never, } -#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, SettingsUi)] pub struct OutlinePanelSettings { pub button: bool, pub default_width: Pixels, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 74ad08570a996a2dc9fc07bfb616f0edc0085b9f..b32e95741f522650e5d20f80a6ba18c423805234 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -952,7 +952,7 @@ pub enum PulledDiagnostics { /// Whether to disable all AI features in Zed. /// /// Default: false -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, settings::SettingsUi)] pub struct DisableAiSettings { pub disable_ai: bool, } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 4447c2512943257b27a91fb1ac051bccde6e3f7f..30a71c4caeb676509239151a4766beb590fdb47e 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -19,7 +19,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, - SettingsStore, parse_json_with_comments, watch_config_file, + SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file, }; use std::{ collections::BTreeMap, @@ -36,7 +36,7 @@ use crate::{ worktree_store::{WorktreeStore, WorktreeStoreEvent}, }; -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct ProjectSettings { /// Configuration for language servers. /// diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index fc399d66a7b78e75a9e43a3e7bf0404624123685..9c7bd4fd66e9e5b884867bf13f88856c126974b6 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -2,7 +2,7 @@ use editor::ShowScrollbar; use gpui::Pixels; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[serde(rename_all = "snake_case")] @@ -28,7 +28,7 @@ pub enum EntrySpacing { Standard, } -#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, SettingsUi)] pub struct ProjectPanelSettings { pub button: bool, pub hide_gitignore: bool, diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index e3fb249d1632a35d888996da2665d00ea98b2c26..29f6e75bbdebf72b36295b20295f0705b636214e 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -20,7 +20,7 @@ use remote::{ }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use theme::ThemeSettings; use ui::{ ActiveTheme, Color, Context, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, @@ -29,7 +29,7 @@ use ui::{ use util::serde::default_true; use workspace::{AppState, ModalView, Workspace}; -#[derive(Deserialize)] +#[derive(Deserialize, SettingsUi)] pub struct SshSettings { pub ssh_connections: Option>, /// Whether to read ~/.ssh/config for ssh connection sources. diff --git a/crates/repl/src/jupyter_settings.rs b/crates/repl/src/jupyter_settings.rs index 8b00e0f75722e54766b3d7447894e73dfeb441f8..c3bfd2079dfae21c9b990b15faec4cf7d4ffaa68 100644 --- a/crates/repl/src/jupyter_settings.rs +++ b/crates/repl/src/jupyter_settings.rs @@ -4,9 +4,9 @@ use editor::EditorSettings; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Debug, Default)] +#[derive(Debug, Default, SettingsUi)] pub struct JupyterSettings { pub kernel_selections: HashMap, } diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 892d4dea8b2daac7395bcbe273635fbb535a0e53..8768b4073602461a5031b8d70d3a1e930ad2a41e 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -31,6 +31,7 @@ schemars.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true +settings_ui_macros.workspace = true serde_json_lenient.workspace = true smallvec.workspace = true tree-sitter-json.workspace = true diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs index 91dda03d00ca282e5ccacde2c07f5359be1ebb16..087f25185a99cb927892e3ada22d92c1c319a390 100644 --- a/crates/settings/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -1,13 +1,17 @@ use std::fmt::{Display, Formatter}; -use crate::{Settings, SettingsSources, VsCodeSettings}; +use crate as settings; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsSources, VsCodeSettings}; +use settings_ui_macros::SettingsUi; /// Base key bindings scheme. Base keymaps can be overridden with user keymaps. /// /// Default: VSCode -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +#[derive( + Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default, SettingsUi, +)] pub enum BaseKeymap { #[default] VSCode, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index a0717333159e508ea42a1b95bd9f2226e6392871..983cd31dd31d6b9c2cd017568fffe0812f9ae4e5 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -4,6 +4,7 @@ mod keymap_file; mod settings_file; mod settings_json; mod settings_store; +mod settings_ui; mod vscode_import; use gpui::{App, Global}; @@ -23,6 +24,9 @@ pub use settings_store::{ InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, SettingsStore, }; +pub use settings_ui::*; +// Re-export the derive macro +pub use settings_ui_macros::SettingsUi; pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource}; #[derive(Clone, Debug, PartialEq)] diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index f112ec811d2828350d41eeab63161c8e345d4d77..b916df6e5c205c7fc2c0c920d0ac8343cb986a5c 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -87,9 +87,9 @@ pub fn update_value_in_json_text<'a>( } /// * `replace_key` - When an exact key match according to `key_path` is found, replace the key with `replace_key` if `Some`. -fn replace_value_in_json_text( +pub fn replace_value_in_json_text>( text: &str, - key_path: &[&str], + key_path: &[T], tab_size: usize, new_value: Option<&Value>, replace_key: Option<&str>, @@ -141,7 +141,7 @@ fn replace_value_in_json_text( let found_key = text .get(key_range.clone()) .map(|key_text| { - depth < key_path.len() && key_text == format!("\"{}\"", key_path[depth]) + depth < key_path.len() && key_text == format!("\"{}\"", key_path[depth].as_ref()) }) .unwrap_or(false); @@ -226,13 +226,13 @@ fn replace_value_in_json_text( } } else { // We have key paths, construct the sub objects - let new_key = key_path[depth]; + let new_key = key_path[depth].as_ref(); // We don't have the key, construct the nested objects let mut new_value = serde_json::to_value(new_value.unwrap_or(&serde_json::Value::Null)).unwrap(); for key in key_path[(depth + 1)..].iter().rev() { - new_value = serde_json::json!({ key.to_string(): new_value }); + new_value = serde_json::json!({ key.as_ref().to_string(): new_value }); } if let Some(first_key_start) = first_key_start { @@ -465,7 +465,7 @@ pub fn append_top_level_array_value_in_json_text( } let (mut replace_range, mut replace_value) = - replace_value_in_json_text("", &[], tab_size, Some(new_value), None); + replace_value_in_json_text::<&str>("", &[], tab_size, Some(new_value), None); replace_range.start = close_bracket_start; replace_range.end = close_bracket_start; diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index fbd0f75aefc2173a3affbb7423d4ccc718679919..09ac6f9766e32e7a0d8765b09919cd0f8c09866c 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -7,7 +7,7 @@ use futures::{ channel::{mpsc, oneshot}, future::LocalBoxFuture, }; -use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal}; +use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGlobal}; use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; use schemars::JsonSchema; @@ -31,14 +31,15 @@ use util::{ pub type EditorconfigProperties = ec4rs::Properties; use crate::{ - ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings, - WorktreeId, parse_json_with_comments, update_value_in_json_text, + ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, SettingsUiEntry, + VsCodeSettings, WorktreeId, parse_json_with_comments, replace_value_in_json_text, + settings_ui::SettingsUi, update_value_in_json_text, }; /// A value that can be defined as a user setting. /// /// Settings can be loaded from a combination of multiple JSON files. -pub trait Settings: 'static + Send + Sync { +pub trait Settings: SettingsUi + 'static + Send + Sync { /// The name of a key within the JSON file from which this setting should /// be deserialized. If this is `None`, then the setting will be deserialized /// from the root object. @@ -284,6 +285,7 @@ trait AnySettingValue: 'static + Send + Sync { text: &mut String, edits: &mut Vec<(Range, String)>, ); + fn settings_ui_item(&self) -> SettingsUiEntry; } struct DeserializedSetting(Box); @@ -480,6 +482,11 @@ impl SettingsStore { self.raw_global_settings.as_ref() } + /// Access the raw JSON value of the default settings. + pub fn raw_default_settings(&self) -> &Value { + &self.raw_default_settings + } + #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut App) -> Self { let mut this = Self::new(cx); @@ -532,49 +539,10 @@ impl SettingsStore { } } - pub fn update_settings_file( + fn update_settings_file_inner( &self, fs: Arc, - update: impl 'static + Send + FnOnce(&mut T::FileContent, &App), - ) { - 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.new_text_for_update::(old_text, |content| update(content, cx)) - })?; - 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) - })?; - } - - anyhow::Ok(()) - } - .boxed_local() - })) - .ok(); - } - - pub fn import_vscode_settings( - &self, - fs: Arc, - vscode_settings: VsCodeSettings, + update: impl 'static + Send + FnOnce(String, AsyncApp) -> Result, ) -> oneshot::Receiver> { let (tx, rx) = oneshot::channel::>(); self.setting_file_updates_tx @@ -582,9 +550,7 @@ impl SettingsStore { async move { 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 new_text = update(old_text, cx)?; let settings_path = paths::settings_file().as_path(); if fs.is_file(settings_path).await { let resolved_path = @@ -607,7 +573,6 @@ impl SettingsStore { format!("Failed to write settings to file {:?}", settings_path) })?; } - anyhow::Ok(()) } .await; @@ -622,9 +587,67 @@ impl SettingsStore { } .boxed_local() })) - .ok(); + .map_err(|err| anyhow::format_err!("Failed to update settings file: {}", err)) + .log_with_level(log::Level::Warn); + return rx; + } + + pub fn update_settings_file_at_path( + &self, + fs: Arc, + path: &[&str], + new_value: serde_json::Value, + ) -> oneshot::Receiver> { + let key_path = path + .into_iter() + .cloned() + .map(SharedString::new) + .collect::>(); + let update = move |mut old_text: String, cx: AsyncApp| { + cx.read_global(|store: &SettingsStore, _cx| { + // todo(settings_ui) use `update_value_in_json_text` for merging new and old objects with comment preservation, needs old value though... + let (range, replacement) = replace_value_in_json_text( + &old_text, + key_path.as_slice(), + store.json_tab_size(), + Some(&new_value), + None, + ); + old_text.replace_range(range, &replacement); + old_text + }) + }; + self.update_settings_file_inner(fs, update) + } - rx + pub fn update_settings_file( + &self, + fs: Arc, + update: impl 'static + Send + FnOnce(&mut T::FileContent, &App), + ) { + _ = self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| { + cx.read_global(|store: &SettingsStore, cx| { + store.new_text_for_update::(old_text, |content| update(content, cx)) + }) + }); + } + + pub fn import_vscode_settings( + &self, + fs: Arc, + vscode_settings: VsCodeSettings, + ) -> oneshot::Receiver> { + self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| { + cx.read_global(|store: &SettingsStore, _cx| { + store.get_vscode_edits(old_text, &vscode_settings) + }) + }) + } + + pub fn settings_ui_items(&self) -> impl IntoIterator { + self.setting_values + .values() + .map(|item| item.settings_ui_item()) } } @@ -1520,6 +1543,10 @@ impl AnySettingValue for SettingValue { edits, ); } + + fn settings_ui_item(&self) -> SettingsUiEntry { + ::settings_ui_entry() + } } #[cfg(test)] @@ -1527,7 +1554,10 @@ mod tests { use crate::VsCodeSettingsSource; use super::*; + // This is so the SettingsUi macro can still work properly + use crate as settings; use serde_derive::Deserialize; + use settings_ui_macros::SettingsUi; use unindent::Unindent; #[gpui::test] @@ -2070,14 +2100,14 @@ mod tests { pretty_assertions::assert_eq!(new, expected); } - #[derive(Debug, PartialEq, Deserialize)] + #[derive(Debug, PartialEq, Deserialize, SettingsUi)] struct UserSettings { name: String, age: u32, staff: bool, } - #[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] + #[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)] struct UserSettingsContent { name: Option, age: Option, @@ -2097,7 +2127,7 @@ mod tests { } } - #[derive(Debug, Deserialize, PartialEq)] + #[derive(Debug, Deserialize, PartialEq, SettingsUi)] struct TurboSetting(bool); impl Settings for TurboSetting { @@ -2111,7 +2141,7 @@ mod tests { fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut Self::FileContent) {} } - #[derive(Clone, Debug, PartialEq, Deserialize)] + #[derive(Clone, Debug, PartialEq, Deserialize, SettingsUi)] struct MultiKeySettings { #[serde(default)] key1: String, @@ -2144,7 +2174,7 @@ mod tests { } } - #[derive(Debug, Deserialize)] + #[derive(Debug, Deserialize, SettingsUi)] struct JournalSettings { pub path: String, pub hour_format: HourFormat, @@ -2245,7 +2275,7 @@ mod tests { ); } - #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] + #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] struct LanguageSettings { #[serde(default)] languages: HashMap, diff --git a/crates/settings/src/settings_ui.rs b/crates/settings/src/settings_ui.rs new file mode 100644 index 0000000000000000000000000000000000000000..8b30ebc9d5968943d3814f7569d1367d389e386a --- /dev/null +++ b/crates/settings/src/settings_ui.rs @@ -0,0 +1,118 @@ +use anyhow::Context as _; +use fs::Fs; +use gpui::{AnyElement, App, AppContext as _, ReadGlobal as _, Window}; +use smallvec::SmallVec; + +use crate::SettingsStore; + +pub trait SettingsUi { + fn settings_ui_item() -> SettingsUiItem { + SettingsUiItem::None + } + fn settings_ui_entry() -> SettingsUiEntry; +} + +pub struct SettingsUiEntry { + // todo(settings_ui): move this back here once there isn't a None variant + // pub path: &'static str, + // pub title: &'static str, + pub item: SettingsUiEntryVariant, +} + +pub enum SettingsUiEntryVariant { + Group { + path: &'static str, + title: &'static str, + items: Vec, + }, + Item { + path: &'static str, + item: SettingsUiItemSingle, + }, + // todo(settings_ui): remove + None, +} + +pub enum SettingsUiItemSingle { + SwitchField, + NumericStepper, + ToggleGroup(&'static [&'static str]), + /// This should be used when toggle group size > 6 + DropDown(&'static [&'static str]), + Custom(Box, &mut Window, &mut App) -> AnyElement>), +} + +pub struct SettingsValue { + pub title: &'static str, + pub path: SmallVec<[&'static str; 1]>, + pub value: Option, + pub default_value: T, +} + +impl SettingsValue { + pub fn read(&self) -> &T { + match &self.value { + Some(value) => value, + None => &self.default_value, + } + } +} + +impl SettingsValue { + pub fn write_value(path: &SmallVec<[&'static str; 1]>, value: serde_json::Value, cx: &mut App) { + let settings_store = SettingsStore::global(cx); + let fs = ::global(cx); + + let rx = settings_store.update_settings_file_at_path(fs.clone(), path.as_slice(), value); + let path = path.clone(); + cx.background_spawn(async move { + rx.await? + .with_context(|| format!("Failed to update setting at path `{:?}`", path.join("."))) + }) + .detach_and_log_err(cx); + } +} + +impl SettingsValue { + pub fn write( + path: &SmallVec<[&'static str; 1]>, + value: T, + cx: &mut App, + ) -> Result<(), serde_json::Error> { + SettingsValue::write_value(path, serde_json::to_value(value)?, cx); + Ok(()) + } +} + +pub enum SettingsUiItem { + Group { + title: &'static str, + items: Vec, + }, + Single(SettingsUiItemSingle), + None, +} + +impl SettingsUi for bool { + fn settings_ui_item() -> SettingsUiItem { + SettingsUiItem::Single(SettingsUiItemSingle::SwitchField) + } + + fn settings_ui_entry() -> SettingsUiEntry { + SettingsUiEntry { + item: SettingsUiEntryVariant::None, + } + } +} + +impl SettingsUi for u64 { + fn settings_ui_item() -> SettingsUiItem { + SettingsUiItem::Single(SettingsUiItemSingle::NumericStepper) + } + + fn settings_ui_entry() -> SettingsUiEntry { + SettingsUiEntry { + item: SettingsUiEntryVariant::None, + } + } +} diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 4a48c18f7c2cbd19538257d51e8342c15c69f587..53fbf797c3d9e56e49b1d96e7dabcac19ddde8e2 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -2,7 +2,7 @@ use anyhow::{Context as _, Result, anyhow}; use fs::Fs; use paths::{cursor_settings_file_paths, vscode_settings_file_paths}; use serde_json::{Map, Value}; -use std::{path::Path, rc::Rc, sync::Arc}; +use std::{path::Path, sync::Arc}; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum VsCodeSettingsSource { @@ -21,7 +21,7 @@ impl std::fmt::Display for VsCodeSettingsSource { pub struct VsCodeSettings { pub source: VsCodeSettingsSource, - pub path: Rc, + pub path: Arc, content: Map, } diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 8a151359ec4bb246e23c4a09fdbe63c23c69a98a..7c2b81aee0ecf48afb7131adf5ddb19a165ca351 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -11,45 +11,26 @@ workspace = true [lib] path = "src/settings_ui.rs" +[features] +default = [] + [dependencies] anyhow.workspace = true -collections.workspace = true -command_palette.workspace = true command_palette_hooks.workspace = true -component.workspace = true -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 -log.workspace = true -menu.workspace = true -notifications.workspace = true -paths.workspace = true -project.workspace = true -search.workspace = true -serde.workspace = true serde_json.workspace = true +serde.workspace = true settings.workspace = true -telemetry.workspace = true -tempfile.workspace = true +smallvec.workspace = true theme.workspace = true -tree-sitter-json.workspace = true -tree-sitter-rust.workspace = true ui.workspace = true -ui_input.workspace = true -util.workspace = true -vim.workspace = true -workspace-hack.workspace = true workspace.workspace = true -zed_actions.workspace = true +workspace-hack.workspace = true -[dev-dependencies] -db = {"workspace"= true, "features" = ["test-support"]} -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } +# Uncomment other workspace dependencies as needed +# assistant.workspace = true +# client.workspace = true +# project.workspace = true +# settings.workspace = true diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 3022cc714268f641b7b6f30021b5e86d6072b7b6..ae03170a1a9a2cb3e53c67402c95c8e79e739ab9 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -1,20 +1,24 @@ mod appearance_settings_controls; use std::any::TypeId; +use std::ops::{Not, Range}; +use anyhow::Context as _; use command_palette_hooks::CommandPaletteFilter; use editor::EditorSettingsControls; use feature_flags::{FeatureFlag, FeatureFlagViewExt}; -use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, actions}; -use ui::prelude::*; -use workspace::item::{Item, ItemEvent}; -use workspace::{Workspace, with_active_or_new_workspace}; +use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, actions}; +use settings::{SettingsStore, SettingsUiEntryVariant, SettingsUiItemSingle, SettingsValue}; +use smallvec::SmallVec; +use ui::{NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple, prelude::*}; +use workspace::{ + Workspace, + item::{Item, ItemEvent}, + with_active_or_new_workspace, +}; use crate::appearance_settings_controls::AppearanceSettingsControls; -pub mod keybindings; -pub mod ui_components; - pub struct SettingsUiFeatureFlag; impl FeatureFlag for SettingsUiFeatureFlag { @@ -75,18 +79,18 @@ pub fn init(cx: &mut App) { .detach(); }) .detach(); - - keybindings::init(cx); } pub struct SettingsPage { focus_handle: FocusHandle, + settings_tree: SettingsUiTree, } impl SettingsPage { pub fn new(_workspace: &Workspace, cx: &mut Context) -> Entity { cx.new(|cx| Self { focus_handle: cx.focus_handle(), + settings_tree: SettingsUiTree::new(cx), }) } } @@ -119,26 +123,472 @@ impl Item for SettingsPage { } } +// We want to iterate over the side bar with root groups +// - this is a loop over top level groups, and if any are expanded, recursively displaying their items +// - Should be able to get all items from a group (flatten a group) +// - Should be able to toggle/untoggle groups in UI (at least in sidebar) +// - Search should be available +// - there should be an index of text -> item mappings, for using fuzzy::match +// - Do we want to show the parent groups when a item is matched? + +struct UIEntry { + title: &'static str, + path: &'static str, + _depth: usize, + // a + // b < a descendant range < a total descendant range + // f | | + // g | | + // c < | + // d | + // e < + descendant_range: Range, + total_descendant_range: Range, + next_sibling: Option, + // expanded: bool, + render: Option, +} + +struct SettingsUiTree { + root_entry_indices: Vec, + entries: Vec, + active_entry_index: usize, +} + +fn build_tree_item( + tree: &mut Vec, + group: SettingsUiEntryVariant, + depth: usize, + prev_index: Option, +) { + let index = tree.len(); + tree.push(UIEntry { + title: "", + path: "", + _depth: depth, + descendant_range: index + 1..index + 1, + total_descendant_range: index + 1..index + 1, + render: None, + next_sibling: None, + }); + if let Some(prev_index) = prev_index { + tree[prev_index].next_sibling = Some(index); + } + match group { + SettingsUiEntryVariant::Group { + path, + title, + items: group_items, + } => { + tree[index].path = path; + tree[index].title = title; + for group_item in group_items { + let prev_index = tree[index] + .descendant_range + .is_empty() + .not() + .then_some(tree[index].descendant_range.end - 1); + tree[index].descendant_range.end = tree.len() + 1; + build_tree_item(tree, group_item.item, depth + 1, prev_index); + tree[index].total_descendant_range.end = tree.len(); + } + } + SettingsUiEntryVariant::Item { path, item } => { + tree[index].path = path; + // todo(settings_ui) create title from path in macro, and use here + tree[index].title = path; + tree[index].render = Some(item); + } + SettingsUiEntryVariant::None => { + return; + } + } +} + +impl SettingsUiTree { + fn new(cx: &App) -> Self { + let settings_store = SettingsStore::global(cx); + let mut tree = vec![]; + let mut root_entry_indices = vec![]; + for item in settings_store.settings_ui_items() { + if matches!(item.item, SettingsUiEntryVariant::None) { + continue; + } + + assert!( + matches!(item.item, SettingsUiEntryVariant::Group { .. }), + "top level items must be groups: {:?}", + match item.item { + SettingsUiEntryVariant::Item { path, .. } => path, + _ => unreachable!(), + } + ); + let prev_root_entry_index = root_entry_indices.last().copied(); + root_entry_indices.push(tree.len()); + build_tree_item(&mut tree, item.item, 0, prev_root_entry_index); + } + + root_entry_indices.sort_by_key(|i| tree[*i].title); + + let active_entry_index = root_entry_indices[0]; + Self { + entries: tree, + root_entry_indices, + active_entry_index, + } + } +} + +fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context) -> Div { + let mut nav = v_flex().p_4().gap_2(); + for &index in &tree.root_entry_indices { + nav = nav.child( + div() + .id(index) + .on_click(cx.listener(move |settings, _, _, _| { + settings.settings_tree.active_entry_index = index; + })) + .child( + Label::new(SharedString::new_static(tree.entries[index].title)) + .size(LabelSize::Large) + .when(tree.active_entry_index == index, |this| { + this.color(Color::Selected) + }), + ), + ); + } + nav +} + +fn render_content( + tree: &SettingsUiTree, + window: &mut Window, + cx: &mut Context, +) -> impl IntoElement { + let Some(entry) = tree.entries.get(tree.active_entry_index) else { + return div() + .size_full() + .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error)); + }; + let mut content = v_flex().size_full().gap_4(); + + let mut child_index = entry + .descendant_range + .is_empty() + .not() + .then_some(entry.descendant_range.start); + let mut path = smallvec::smallvec![entry.path]; + + while let Some(index) = child_index { + let child = &tree.entries[index]; + child_index = child.next_sibling; + if child.render.is_none() { + // todo(settings_ui): subgroups? + continue; + } + path.push(child.path); + let settings_value = settings_value_from_settings_and_path( + path.clone(), + // PERF: how to structure this better? There feels like there's a way to avoid the clone + // and every value lookup + SettingsStore::global(cx).raw_user_settings(), + SettingsStore::global(cx).raw_default_settings(), + ); + content = content.child( + div() + .child( + Label::new(SharedString::new_static(tree.entries[index].title)) + .size(LabelSize::Large) + .when(tree.active_entry_index == index, |this| { + this.color(Color::Selected) + }), + ) + .child(render_item_single( + settings_value, + child.render.as_ref().unwrap(), + window, + cx, + )), + ); + + path.pop(); + } + + return content; +} + impl Render for SettingsPage { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .grid() + .grid_cols(16) .p_4() + .bg(cx.theme().colors().editor_background) .size_full() - .gap_4() - .child(Label::new("Settings").size(LabelSize::Large)) - .child( - v_flex().gap_1().child(Label::new("Appearance")).child( - v_flex() - .elevation_2(cx) - .child(AppearanceSettingsControls::new()), - ), - ) .child( - v_flex().gap_1().child(Label::new("Editor")).child( - v_flex() - .elevation_2(cx) - .child(EditorSettingsControls::new()), - ), + div() + .col_span(2) + .h_full() + .child(render_nav(&self.settings_tree, window, cx)), ) + .child(div().col_span(4).h_full().child(render_content( + &self.settings_tree, + window, + cx, + ))) } } + +// todo(settings_ui): remove, only here as inspiration +#[allow(dead_code)] +fn render_old_appearance_settings(cx: &mut App) -> impl IntoElement { + v_flex() + .p_4() + .size_full() + .gap_4() + .child(Label::new("Settings").size(LabelSize::Large)) + .child( + v_flex().gap_1().child(Label::new("Appearance")).child( + v_flex() + .elevation_2(cx) + .child(AppearanceSettingsControls::new()), + ), + ) + .child( + v_flex().gap_1().child(Label::new("Editor")).child( + v_flex() + .elevation_2(cx) + .child(EditorSettingsControls::new()), + ), + ) +} + +fn element_id_from_path(path: &[&'static str]) -> ElementId { + if path.len() == 0 { + panic!("Path length must not be zero"); + } else if path.len() == 1 { + ElementId::Name(SharedString::new_static(path[0])) + } else { + ElementId::from(( + ElementId::from(SharedString::new_static(path[path.len() - 2])), + SharedString::new_static(path[path.len() - 1]), + )) + } +} + +fn render_item_single( + settings_value: SettingsValue, + item: &SettingsUiItemSingle, + window: &mut Window, + cx: &mut App, +) -> AnyElement { + match item { + SettingsUiItemSingle::Custom(_) => div() + .child(format!("Item: {}", settings_value.path.join("."))) + .into_any_element(), + SettingsUiItemSingle::SwitchField => { + render_any_item(settings_value, render_switch_field, window, cx) + } + SettingsUiItemSingle::NumericStepper => { + render_any_item(settings_value, render_numeric_stepper, window, cx) + } + SettingsUiItemSingle::ToggleGroup(variants) => { + render_toggle_button_group(settings_value, variants, window, cx) + } + SettingsUiItemSingle::DropDown(_) => { + unimplemented!("This") + } + } +} + +fn read_settings_value_from_path<'a>( + settings_contents: &'a serde_json::Value, + path: &[&'static str], +) -> Option<&'a serde_json::Value> { + let Some((key, remaining)) = path.split_first() else { + return Some(settings_contents); + }; + let Some(value) = settings_contents.get(key) else { + return None; + }; + + read_settings_value_from_path(value, remaining) +} + +fn downcast_any_item( + settings_value: SettingsValue, +) -> SettingsValue { + let value = settings_value + .value + .map(|value| serde_json::from_value::(value).expect("value is not a T")); + // todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values + let default_value = serde_json::from_value::(settings_value.default_value) + .expect("default value is not an Option"); + let deserialized_setting_value = SettingsValue { + title: settings_value.title, + path: settings_value.path, + value, + default_value, + }; + deserialized_setting_value +} + +fn render_any_item( + settings_value: SettingsValue, + render_fn: impl Fn(SettingsValue, &mut Window, &mut App) -> AnyElement + 'static, + window: &mut Window, + cx: &mut App, +) -> AnyElement { + let deserialized_setting_value = downcast_any_item(settings_value); + render_fn(deserialized_setting_value, window, cx) +} + +fn render_numeric_stepper( + value: SettingsValue, + _window: &mut Window, + _cx: &mut App, +) -> AnyElement { + let id = element_id_from_path(&value.path); + let path = value.path.clone(); + let num = value.value.unwrap_or_else(|| value.default_value); + + NumericStepper::new( + id, + num.to_string(), + { + let path = value.path.clone(); + move |_, _, cx| { + let Some(number) = serde_json::Number::from_u128(num.saturating_sub(1) as u128) + else { + return; + }; + let new_value = serde_json::Value::Number(number); + SettingsValue::write_value(&path, new_value, cx); + } + }, + move |_, _, cx| { + let Some(number) = serde_json::Number::from_u128(num.saturating_add(1) as u128) else { + return; + }; + + let new_value = serde_json::Value::Number(number); + + SettingsValue::write_value(&path, new_value, cx); + }, + ) + .style(ui::NumericStepperStyle::Outlined) + .into_any_element() +} + +fn render_switch_field( + value: SettingsValue, + _window: &mut Window, + _cx: &mut App, +) -> AnyElement { + let id = element_id_from_path(&value.path); + let path = value.path.clone(); + SwitchField::new( + id, + SharedString::new_static(value.title), + None, + match value.read() { + true => ToggleState::Selected, + false => ToggleState::Unselected, + }, + move |toggle_state, _, cx| { + let new_value = serde_json::Value::Bool(match toggle_state { + ToggleState::Indeterminate => { + return; + } + ToggleState::Selected => true, + ToggleState::Unselected => false, + }); + + SettingsValue::write_value(&path, new_value, cx); + }, + ) + .into_any_element() +} + +fn render_toggle_button_group( + value: SettingsValue, + variants: &'static [&'static str], + _: &mut Window, + _: &mut App, +) -> AnyElement { + let value = downcast_any_item::(value); + + fn make_toggle_group( + group_name: &'static str, + value: SettingsValue, + variants: &'static [&'static str], + ) -> AnyElement { + let mut variants_array: [&'static str; LEN] = ["default"; LEN]; + variants_array.copy_from_slice(variants); + let active_value = value.read(); + + let selected_idx = variants_array + .iter() + .enumerate() + .find_map(|(idx, variant)| { + if variant == &active_value { + Some(idx) + } else { + None + } + }); + + ToggleButtonGroup::single_row( + group_name, + variants_array.map(|variant| { + let path = value.path.clone(); + ToggleButtonSimple::new(variant, move |_, _, cx| { + SettingsValue::write_value( + &path, + serde_json::Value::String(variant.to_string()), + cx, + ); + }) + }), + ) + .when_some(selected_idx, |this, ix| this.selected_index(ix)) + .style(ui::ToggleButtonGroupStyle::Filled) + .into_any_element() + } + + macro_rules! templ_toggl_with_const_param { + ($len:expr) => { + if variants.len() == $len { + return make_toggle_group::<$len>(value.title, value, variants); + } + }; + } + templ_toggl_with_const_param!(1); + templ_toggl_with_const_param!(2); + templ_toggl_with_const_param!(3); + templ_toggl_with_const_param!(4); + templ_toggl_with_const_param!(5); + templ_toggl_with_const_param!(6); + unreachable!("Too many variants"); +} + +fn settings_value_from_settings_and_path( + path: SmallVec<[&'static str; 1]>, + user_settings: &serde_json::Value, + default_settings: &serde_json::Value, +) -> SettingsValue { + let default_value = read_settings_value_from_path(default_settings, &path) + .with_context(|| format!("No default value for item at path {:?}", path.join("."))) + .expect("Default value set for item") + .clone(); + + let value = read_settings_value_from_path(user_settings, &path).cloned(); + let settings_value = SettingsValue { + default_value, + value, + path: path.clone(), + // todo(settings_ui) title for items + title: path.last().expect("path non empty"), + }; + return settings_value; +} diff --git a/crates/settings_ui_macros/Cargo.toml b/crates/settings_ui_macros/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..e242e7546d1527632dba6eece9b17ccea27295f4 --- /dev/null +++ b/crates/settings_ui_macros/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "settings_ui_macros" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lib] +path = "src/settings_ui_macros.rs" +proc-macro = true + +[lints] +workspace = true + +[features] +default = [] + +[dependencies] +proc-macro2.workspace = true +quote.workspace = true +syn.workspace = true +workspace-hack.workspace = true diff --git a/crates/settings_ui_macros/LICENSE-GPL b/crates/settings_ui_macros/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/settings_ui_macros/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings_ui_macros/src/settings_ui_macros.rs b/crates/settings_ui_macros/src/settings_ui_macros.rs new file mode 100644 index 0000000000000000000000000000000000000000..6e37745a7c24155de631e47ffc8c265209ee24e8 --- /dev/null +++ b/crates/settings_ui_macros/src/settings_ui_macros.rs @@ -0,0 +1,201 @@ +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use syn::{Data, DeriveInput, LitStr, Token, parse_macro_input}; + +/// Derive macro for the `SettingsUi` marker trait. +/// +/// This macro automatically implements the `SettingsUi` trait for the annotated type. +/// The `SettingsUi` trait is a marker trait used to indicate that a type can be +/// displayed in the settings UI. +/// +/// # Example +/// +/// ``` +/// use settings::SettingsUi; +/// use settings_ui_macros::SettingsUi; +/// +/// #[derive(SettingsUi)] +/// #[settings_ui(group = "Standard")] +/// struct MySettings { +/// enabled: bool, +/// count: usize, +/// } +/// ``` +#[proc_macro_derive(SettingsUi, attributes(settings_ui))] +pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + // Handle generic parameters if present + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let mut group_name = Option::::None; + let mut path_name = Option::::None; + + for attr in &input.attrs { + if attr.path().is_ident("settings_ui") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("group") { + if group_name.is_some() { + return Err(meta.error("Only one 'group' path can be specified")); + } + meta.input.parse::()?; + let lit: LitStr = meta.input.parse()?; + group_name = Some(lit.value()); + } else if meta.path.is_ident("path") { + // todo(settings_ui) try get KEY from Settings if possible, and once we do, + // if can get key from settings, throw error if path also passed + if path_name.is_some() { + return Err(meta.error("Only one 'path' can be specified")); + } + meta.input.parse::()?; + let lit: LitStr = meta.input.parse()?; + path_name = Some(lit.value()); + } + Ok(()) + }) + .unwrap_or_else(|e| panic!("in #[settings_ui] attribute: {}", e)); + } + } + + if path_name.is_none() && group_name.is_some() { + // todo(settings_ui) derive path from settings + panic!("path is required when group is specified"); + } + + let ui_render_fn_body = generate_ui_item_body(group_name.as_ref(), path_name.as_ref(), &input); + + let settings_ui_item_fn_body = path_name + .as_ref() + .map(|path_name| map_ui_item_to_render(path_name, quote! { Self })) + .unwrap_or(quote! { + settings::SettingsUiEntry { + item: settings::SettingsUiEntryVariant::None + } + }); + + let expanded = quote! { + impl #impl_generics settings::SettingsUi for #name #ty_generics #where_clause { + fn settings_ui_item() -> settings::SettingsUiItem { + #ui_render_fn_body + } + + fn settings_ui_entry() -> settings::SettingsUiEntry { + #settings_ui_item_fn_body + } + } + }; + + proc_macro::TokenStream::from(expanded) +} + +fn map_ui_item_to_render(path: &str, ty: TokenStream) -> TokenStream { + quote! { + settings::SettingsUiEntry { + item: match #ty::settings_ui_item() { + settings::SettingsUiItem::Group{title, items} => settings::SettingsUiEntryVariant::Group { + title, + path: #path, + items, + }, + settings::SettingsUiItem::Single(item) => settings::SettingsUiEntryVariant::Item { + path: #path, + item, + }, + settings::SettingsUiItem::None => settings::SettingsUiEntryVariant::None, + } + } + } +} + +fn generate_ui_item_body( + group_name: Option<&String>, + path_name: Option<&String>, + input: &syn::DeriveInput, +) -> TokenStream { + match (group_name, path_name, &input.data) { + (_, _, Data::Union(_)) => unimplemented!("Derive SettingsUi for Unions"), + (None, None, Data::Struct(_)) => quote! { + settings::SettingsUiItem::None + }, + (Some(_), None, Data::Struct(_)) => quote! { + settings::SettingsUiItem::None + }, + (None, Some(_), Data::Struct(_)) => quote! { + settings::SettingsUiItem::None + }, + (Some(group_name), _, Data::Struct(data_struct)) => { + let fields = data_struct + .fields + .iter() + .filter(|field| { + !field.attrs.iter().any(|attr| { + let mut has_skip = false; + if attr.path().is_ident("settings_ui") { + let _ = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("skip") { + has_skip = true; + } + Ok(()) + }); + } + + has_skip + }) + }) + .map(|field| { + ( + field.ident.clone().expect("tuple fields").to_string(), + field.ty.to_token_stream(), + ) + }) + .map(|(name, ty)| map_ui_item_to_render(&name, ty)); + + quote! { + settings::SettingsUiItem::Group{ title: #group_name, items: vec![#(#fields),*] } + } + } + (None, _, Data::Enum(data_enum)) => { + let mut lowercase = false; + for attr in &input.attrs { + if attr.path().is_ident("serde") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("rename_all") { + meta.input.parse::()?; + let lit = meta.input.parse::()?.value(); + // todo(settings_ui) snake case + lowercase = lit == "lowercase" || lit == "snake_case"; + } + Ok(()) + }) + .ok(); + } + } + let length = data_enum.variants.len(); + + let variants = data_enum.variants.iter().map(|variant| { + let string = variant.ident.clone().to_string(); + + if lowercase { + string.to_lowercase() + } else { + string + } + }); + + if length > 6 { + quote! { + settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::DropDown(&[#(#variants),*])) + } + } else { + quote! { + settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::ToggleGroup(&[#(#variants),*])) + } + } + } + // todo(settings_ui) discriminated unions + (_, _, Data::Enum(_)) => quote! { + settings::SettingsUiItem::None + }, + } +} diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 635e3e2ca5895562c7981d89169bf6f0632a223f..01f2d85f09e416b6c8ac40d7fa283d1f1e296cd5 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -6,7 +6,7 @@ use gpui::{AbsoluteLength, App, FontFallbacks, FontFeatures, FontWeight, Pixels, use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::SettingsSources; +use settings::{SettingsSources, SettingsUi}; use std::path::PathBuf; use task::Shell; use theme::FontFamilyName; @@ -24,7 +24,7 @@ pub struct Toolbar { pub breadcrumbs: bool, } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, SettingsUi)] pub struct TerminalSettings { pub shell: Shell, pub working_directory: WorkingDirectory, diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index df147cfe92377962b135fed309ef0a7df68adcd8..61b41eba0642f10312a4c78df447ac7344f7e2dc 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -13,7 +13,7 @@ use gpui::{ use refineable::Refineable; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; -use settings::{ParameterizedJsonSchema, Settings, SettingsSources}; +use settings::{ParameterizedJsonSchema, Settings, SettingsSources, SettingsUi}; use std::sync::Arc; use util::ResultExt as _; use util::schemars::replace_subschema; @@ -87,7 +87,7 @@ impl From for String { } /// Customizable settings for the UI and theme system. -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, SettingsUi)] pub struct ThemeSettings { /// The UI font size. Determines the size of text in the UI, /// as well as the size of a [gpui::Rems] unit. diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index cf178e2850397c5a2398033a02addb73ab615ec9..f60ac7c301359d0bb0d3d8ee1d4115c5d815cf69 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -42,7 +42,7 @@ rpc.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true -settings_ui.workspace = true +keymap_editor.workspace = true smallvec.workspace = true story = { workspace = true, optional = true } telemetry.workspace = true diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index ac5e9201b3be083fef43e58c2e717cb59a0ba185..075b9fcd86276244d154be1aebe904fbfb4a7b6c 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -29,10 +29,10 @@ use gpui::{ IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, actions, div, }; +use keymap_editor; use onboarding_banner::OnboardingBanner; use project::Project; use settings::Settings as _; -use settings_ui::keybindings; use std::sync::Arc; use theme::ActiveTheme; use title_bar_settings::TitleBarSettings; @@ -684,7 +684,7 @@ impl TitleBar { "Settings Profiles", zed_actions::settings_profile_selector::Toggle.boxed_clone(), ) - .action("Key Bindings", Box::new(keybindings::OpenKeymapEditor)) + .action("Key Bindings", Box::new(keymap_editor::OpenKeymapEditor)) .action( "Themes…", zed_actions::theme_selector::Toggle::default().boxed_clone(), @@ -732,7 +732,7 @@ impl TitleBar { "Settings Profiles", zed_actions::settings_profile_selector::Toggle.boxed_clone(), ) - .action("Key Bindings", Box::new(keybindings::OpenKeymapEditor)) + .action("Key Bindings", Box::new(keymap_editor::OpenKeymapEditor)) .action( "Themes…", zed_actions::theme_selector::Toggle::default().boxed_clone(), diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs index a98e984d80e1dbf0c016b8d8e4c6dc609106c081..29d74c8590a63cd8aa75bdaa3655111d76fcf757 100644 --- a/crates/title_bar/src/title_bar_settings.rs +++ b/crates/title_bar/src/title_bar_settings.rs @@ -1,9 +1,10 @@ use db::anyhow; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Copy, Clone, Deserialize, Debug)] +#[derive(Copy, Clone, Deserialize, Debug, SettingsUi)] +#[settings_ui(group = "Title Bar", path = "title_bar")] pub struct TitleBarSettings { pub show_branch_icon: bool, pub show_onboarding_banner: bool, diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 9da01e6f444d2284814282f9bf6eecfb0814953d..a5cd909d5b53079d1da49591a5eca21416ba415a 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -39,7 +39,7 @@ use object::Object; use schemars::JsonSchema; use serde::Deserialize; use serde_derive::Serialize; -use settings::{Settings, SettingsSources, SettingsStore, update_settings_file}; +use settings::{Settings, SettingsSources, SettingsStore, SettingsUi, update_settings_file}; use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; use std::{mem, ops::Range, sync::Arc}; use surrounds::SurroundsType; @@ -1774,7 +1774,7 @@ struct CursorShapeSettings { pub insert: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, SettingsUi)] struct VimSettings { pub default_mode: Mode, pub toggle_relative_line_numbers: bool, diff --git a/crates/vim_mode_setting/src/vim_mode_setting.rs b/crates/vim_mode_setting/src/vim_mode_setting.rs index 6f60d3f21fc707abd981c34d7617f4e9bb563477..7fb39ef4f6f10370f1a0fb2cf83dcb3a88b80d81 100644 --- a/crates/vim_mode_setting/src/vim_mode_setting.rs +++ b/crates/vim_mode_setting/src/vim_mode_setting.rs @@ -6,7 +6,7 @@ use anyhow::Result; use gpui::App; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; /// Initializes the `vim_mode_setting` crate. pub fn init(cx: &mut App) { @@ -17,6 +17,7 @@ pub fn init(cx: &mut App) { /// Whether or not to enable Vim mode. /// /// Default: false +#[derive(SettingsUi)] pub struct VimModeSetting(pub bool); impl Settings for VimModeSetting { @@ -43,6 +44,7 @@ impl Settings for VimModeSetting { /// Whether or not to enable Helix mode. /// /// Default: false +#[derive(SettingsUi)] pub struct HelixModeSetting(pub bool); impl Settings for HelixModeSetting { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index db91bd82b904b40d0eaf2466689156f03d3723f3..a513f8c9317645469e5d5ca54c3b5351383c1ca3 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -17,7 +17,7 @@ use gpui::{ use project::{Project, ProjectEntryId, ProjectPath}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsLocation, SettingsSources}; +use settings::{Settings, SettingsLocation, SettingsSources, SettingsUi}; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -49,7 +49,7 @@ impl Default for SaveOptions { } } -#[derive(Deserialize)] +#[derive(Deserialize, SettingsUi)] pub struct ItemSettings { pub git_status: bool, pub close_position: ClosePosition, @@ -59,7 +59,7 @@ pub struct ItemSettings { pub show_close_button: ShowCloseButton, } -#[derive(Deserialize)] +#[derive(Deserialize, SettingsUi)] pub struct PreviewTabsSettings { pub enabled: bool, pub enable_preview_from_file_finder: bool, diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 0d7fb9bb9c1ae6f8ff4a6644132c4a347da4117d..419e33e54435779012207a024ea49e44a8acb1c2 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -6,9 +6,9 @@ use collections::HashMap; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Deserialize)] +#[derive(Deserialize, SettingsUi)] pub struct WorkspaceSettings { pub active_pane_modifiers: ActivePanelModifiers, pub bottom_dock_layout: BottomDockLayout, @@ -216,7 +216,7 @@ pub struct WorkspaceSettingsContent { pub zoomed_padding: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, SettingsUi)] pub struct TabBarSettings { pub show: bool, pub show_nav_history_buttons: bool, diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index b18d3509beb408c37beaf246a747248d2f17438a..df3a4d35570ad21b80f968539afbe681c58e2a06 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -4,10 +4,10 @@ use anyhow::Context as _; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use util::paths::PathMatcher; -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq, SettingsUi)] pub struct WorktreeSettings { pub file_scan_inclusions: PathMatcher, pub file_scan_exclusions: PathMatcher, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 0ddfe3dde1b57de8f6fb5ae83d1bb3ccef8b12ff..bb46a5a4f65ac76a7cff2a5bc43525db30ed0930 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -131,6 +131,7 @@ serde_json.workspace = true session.workspace = true settings.workspace = true settings_ui.workspace = true +keymap_editor.workspace = true shellexpand.workspace = true smol.workspace = true snippet_provider.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 5e7934c3094755b39535ef054f077dbc9fb180af..e4438792045617498e5c8cd3b52117b1d0b752ef 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -632,6 +632,7 @@ pub fn main() { svg_preview::init(cx); onboarding::init(cx); settings_ui::init(cx); + keymap_editor::init(cx); extensions_ui::init(cx); zeta::init(cx); inspector_ui::init(app_state.clone(), cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5a180e4b42705332bd51dffe43943d131a42907f..5797070a39c8a60dc760ac3b82341842bc11d63e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1491,7 +1491,7 @@ fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec) { workspace::NewWindow, )]); // todo: nicer api here? - settings_ui::keybindings::KeymapEventChannel::trigger_keymap_changed(cx); + keymap_editor::KeymapEventChannel::trigger_keymap_changed(cx); } pub fn load_default_keymap(cx: &mut App) { diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 6c7ab0b37403ae941660da853a83e6c147e5869f..342fd26cb77aa08dcbc346609b3185f3263f0f1d 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -1,6 +1,5 @@ use collab_ui::collab_panel; use gpui::{Menu, MenuItem, OsAction}; -use settings_ui::keybindings; use terminal_view::terminal_panel; pub fn app_menus() -> Vec { @@ -17,7 +16,7 @@ pub fn app_menus() -> Vec { name: "Settings".into(), items: vec![ MenuItem::action("Open Settings", super::OpenSettings), - MenuItem::action("Open Key Bindings", keybindings::OpenKeymapEditor), + MenuItem::action("Open Key Bindings", keymap_editor::OpenKeymapEditor), MenuItem::action("Open Default Settings", super::OpenDefaultSettings), MenuItem::action( "Open Default Key Bindings", diff --git a/crates/zlog_settings/src/zlog_settings.rs b/crates/zlog_settings/src/zlog_settings.rs index b58cbcc1433d01ce865580111d1f92c98987bbea..0cdc784489b47d89388edc9ed20aed6f3c2f9959 100644 --- a/crates/zlog_settings/src/zlog_settings.rs +++ b/crates/zlog_settings/src/zlog_settings.rs @@ -3,7 +3,7 @@ use anyhow::Result; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; +use settings::{Settings, SettingsStore, SettingsUi}; pub fn init(cx: &mut App) { ZlogSettings::register(cx); @@ -15,7 +15,7 @@ pub fn init(cx: &mut App) { .detach(); } -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)] pub struct ZlogSettings { #[serde(default, flatten)] pub scopes: std::collections::HashMap, From 515282d719416a2b95bfb1461e5796f41e96eae4 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Fri, 29 Aug 2025 15:16:42 -0600 Subject: [PATCH 448/744] zeta: Add detection of BSD licenses + efficiency improvements + more lenient whitespace handling (#37194) Closes #36564 Release Notes: - Edit Prediction: Added various BSD licenses to open-source licenses eligible for data collection. --- Cargo.lock | 1 + crates/zeta/Cargo.toml | 1 + crates/zeta/src/license_detection.rs | 450 ++++++++---------- crates/zeta/src/license_detection/0bsd.regex | 12 + crates/zeta/src/license_detection/0bsd.txt | 13 + .../{apache.regex => apache-2.0.regex} | 12 +- .../{apache-text => apache-2.0.txt} | 0 .../src/license_detection/bsd-1-clause.regex | 17 + .../src/license_detection/bsd-1-clause.txt | 20 + .../src/license_detection/bsd-2-clause.regex | 22 + .../src/license_detection/bsd-2-clause.txt | 26 + .../src/license_detection/bsd-3-clause.regex | 26 + .../src/license_detection/bsd-3-clause.txt | 29 ++ crates/zeta/src/license_detection/isc.regex | 4 +- crates/zeta/src/license_detection/isc.txt | 15 + crates/zeta/src/license_detection/mit.regex | 4 +- .../license_detection/{mit-text => mit.txt} | 0 .../{upl.regex => upl-1.0.regex} | 4 +- crates/zeta/src/license_detection/upl-1.0.txt | 35 ++ 19 files changed, 436 insertions(+), 255 deletions(-) create mode 100644 crates/zeta/src/license_detection/0bsd.regex create mode 100644 crates/zeta/src/license_detection/0bsd.txt rename crates/zeta/src/license_detection/{apache.regex => apache-2.0.regex} (98%) rename crates/zeta/src/license_detection/{apache-text => apache-2.0.txt} (100%) create mode 100644 crates/zeta/src/license_detection/bsd-1-clause.regex create mode 100644 crates/zeta/src/license_detection/bsd-1-clause.txt create mode 100644 crates/zeta/src/license_detection/bsd-2-clause.regex create mode 100644 crates/zeta/src/license_detection/bsd-2-clause.txt create mode 100644 crates/zeta/src/license_detection/bsd-3-clause.regex create mode 100644 crates/zeta/src/license_detection/bsd-3-clause.txt create mode 100644 crates/zeta/src/license_detection/isc.txt rename crates/zeta/src/license_detection/{mit-text => mit.txt} (100%) rename crates/zeta/src/license_detection/{upl.regex => upl-1.0.regex} (96%) create mode 100644 crates/zeta/src/license_detection/upl-1.0.txt diff --git a/Cargo.lock b/Cargo.lock index 4c68280de25b878187b3a5627362f6373808734b..84d633dd6f126f1ce86cd73b83f9d1aac23c591e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20830,6 +20830,7 @@ dependencies = [ "serde", "serde_json", "settings", + "strum 0.27.1", "telemetry", "telemetry_events", "theme", diff --git a/crates/zeta/Cargo.toml b/crates/zeta/Cargo.toml index ee76308ff38089b9553f9a6ba87998ce74480181..05eedd6015d47e0c020266f27da8d63850d162e3 100644 --- a/crates/zeta/Cargo.toml +++ b/crates/zeta/Cargo.toml @@ -46,6 +46,7 @@ release_channel.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +strum.workspace = true telemetry.workspace = true telemetry_events.workspace = true theme.workspace = true diff --git a/crates/zeta/src/license_detection.rs b/crates/zeta/src/license_detection.rs index 022b2d19de433e9087454fec0874c0d1b31ae6c3..d6b8ef10a3363f49f92607e30c6059ffee573a65 100644 --- a/crates/zeta/src/license_detection.rs +++ b/crates/zeta/src/license_detection.rs @@ -1,5 +1,6 @@ use std::{ collections::BTreeSet, + fmt::{Display, Formatter}, path::{Path, PathBuf}, sync::{Arc, LazyLock}, }; @@ -10,6 +11,7 @@ use gpui::{App, AppContext as _, Entity, Subscription, Task}; use postage::watch; use project::Worktree; use regex::Regex; +use strum::VariantArray; use util::ResultExt as _; use worktree::ChildEntriesOptions; @@ -17,8 +19,14 @@ use worktree::ChildEntriesOptions; static LICENSE_FILE_NAME_REGEX: LazyLock = LazyLock::new(|| { regex::bytes::RegexBuilder::new( "^ \ - (?: license | licence) \ - (?: [\\-._] (?: apache | isc | mit | upl))? \ + (?: license | licence)? \ + (?: [\\-._]? \ + (?: apache (?: [\\-._] (?: 2.0 | 2 ))? | \ + 0? bsd (?: [\\-._] [0123])? (?: [\\-._] clause)? | \ + isc | \ + mit | \ + upl))? \ + (?: [\\-._]? (?: license | licence))? \ (?: \\.txt | \\.md)? \ $", ) @@ -28,40 +36,93 @@ static LICENSE_FILE_NAME_REGEX: LazyLock = LazyLock::new(|| .unwrap() }); -fn is_license_eligible_for_data_collection(license: &str) -> bool { - static LICENSE_REGEXES: LazyLock> = LazyLock::new(|| { - [ - include_str!("license_detection/apache.regex"), - include_str!("license_detection/isc.regex"), - include_str!("license_detection/mit.regex"), - include_str!("license_detection/upl.regex"), - ] - .into_iter() - .map(|pattern| Regex::new(&canonicalize_license_text(pattern)).unwrap()) - .collect() +#[derive(Debug, Clone, Copy, Eq, PartialEq, VariantArray)] +pub enum OpenSourceLicense { + Apache2_0, + BSD0Clause, + BSD1Clause, + BSD2Clause, + BSD3Clause, + ISC, + MIT, + UPL1_0, +} + +impl Display for OpenSourceLicense { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.spdx_identifier()) + } +} + +impl OpenSourceLicense { + pub fn spdx_identifier(&self) -> &'static str { + match self { + OpenSourceLicense::Apache2_0 => "apache-2.0", + OpenSourceLicense::BSD0Clause => "0bsd", + OpenSourceLicense::BSD1Clause => "bsd-1-clause", + OpenSourceLicense::BSD2Clause => "bsd-2-clause", + OpenSourceLicense::BSD3Clause => "bsd-3-clause", + OpenSourceLicense::ISC => "isc", + OpenSourceLicense::MIT => "mit", + OpenSourceLicense::UPL1_0 => "upl-1.0", + } + } + + pub fn regex(&self) -> &'static str { + match self { + OpenSourceLicense::Apache2_0 => include_str!("license_detection/apache-2.0.regex"), + OpenSourceLicense::BSD0Clause => include_str!("license_detection/0bsd.regex"), + OpenSourceLicense::BSD1Clause => include_str!("license_detection/bsd-1-clause.regex"), + OpenSourceLicense::BSD2Clause => include_str!("license_detection/bsd-2-clause.regex"), + OpenSourceLicense::BSD3Clause => include_str!("license_detection/bsd-3-clause.regex"), + OpenSourceLicense::ISC => include_str!("license_detection/isc.regex"), + OpenSourceLicense::MIT => include_str!("license_detection/mit.regex"), + OpenSourceLicense::UPL1_0 => include_str!("license_detection/upl-1.0.regex"), + } + } +} + +fn detect_license(license: &str) -> Option { + static LICENSE_REGEX: LazyLock = LazyLock::new(|| { + let mut regex_string = String::new(); + let mut is_first = true; + for license in OpenSourceLicense::VARIANTS { + if is_first { + regex_string.push_str("^(?:("); + is_first = false; + } else { + regex_string.push_str(")|("); + } + regex_string.push_str(&canonicalize_license_text(license.regex())); + } + regex_string.push_str("))$"); + let regex = Regex::new(®ex_string).unwrap(); + assert_eq!(regex.captures_len(), OpenSourceLicense::VARIANTS.len() + 1); + regex }); - let license = canonicalize_license_text(license); - LICENSE_REGEXES.iter().any(|regex| regex.is_match(&license)) + LICENSE_REGEX + .captures(&canonicalize_license_text(license)) + .and_then(|captures| { + let license = OpenSourceLicense::VARIANTS + .iter() + .enumerate() + .find(|(index, _)| captures.get(index + 1).is_some()) + .map(|(_, license)| *license); + if license.is_none() { + log::error!("bug: open source license regex matched without any capture groups"); + } + license + }) } /// Canonicalizes the whitespace of license text and license regexes. fn canonicalize_license_text(license: &str) -> String { - static PARAGRAPH_SEPARATOR_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"\s*\n\s*\n\s*").unwrap()); - - PARAGRAPH_SEPARATOR_REGEX - .split(license) - .filter(|paragraph| !paragraph.trim().is_empty()) - .map(|paragraph| { - paragraph - .trim() - .split_whitespace() - .collect::>() - .join(" ") - }) + license + .split_ascii_whitespace() .collect::>() - .join("\n\n") + .join(" ") + .to_ascii_lowercase() } pub enum LicenseDetectionWatcher { @@ -157,7 +218,7 @@ impl LicenseDetectionWatcher { return None; } let text = fs.load(&abs_path).await.log_err()?; - let is_eligible = is_license_eligible_for_data_collection(&text); + let is_eligible = detect_license(&text).is_some(); if is_eligible { log::debug!( "`{abs_path:?}` matches a license that is eligible for data collection (if enabled)" @@ -193,193 +254,47 @@ mod tests { use super::*; - const MIT_LICENSE: &str = include_str!("license_detection/mit-text"); - const APACHE_LICENSE: &str = include_str!("license_detection/apache-text"); - - #[test] - fn test_mit_positive_detection() { - assert!(is_license_eligible_for_data_collection(MIT_LICENSE)); - } - - #[test] - fn test_mit_negative_detection() { - let example_license = format!( - r#"{MIT_LICENSE} - - This project is dual licensed under the MIT License and the Apache License, Version 2.0."# - ); - assert!(!is_license_eligible_for_data_collection(&example_license)); - } - - #[test] - fn test_isc_positive_detection() { - let example_license = unindent( - r#" - ISC License - - Copyright (c) 2024, John Doe - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - "# - .trim(), - ); - - assert!(is_license_eligible_for_data_collection(&example_license)); - } - - #[test] - fn test_isc_negative_detection() { - let example_license = unindent( - r#" - ISC License - - Copyright (c) 2024, John Doe - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - This project is dual licensed under the ISC License and the MIT License. - "# - .trim(), - ); - - assert!(!is_license_eligible_for_data_collection(&example_license)); - } - - #[test] - fn test_upl_positive_detection() { - let example_license = unindent( - r#" - Copyright (c) 2025, John Doe - - The Universal Permissive License (UPL), Version 1.0 - - Subject to the condition set forth below, permission is hereby granted to any person - obtaining a copy of this software, associated documentation and/or data (collectively - the "Software"), free of charge and under any and all copyright rights in the - Software, and any and all patent rights owned or freely licensable by each licensor - hereunder covering either (i) the unmodified Software as contributed to or provided - by such licensor, or (ii) the Larger Works (as defined below), to deal in both - - (a) the Software, and - - (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is - included with the Software (each a "Larger Work" to which the Software is - contributed by such licensors), - - without restriction, including without limitation the rights to copy, create - derivative works of, display, perform, and distribute the Software and make, use, - sell, offer for sale, import, export, have made, and have sold the Software and the - Larger Work(s), and to sublicense the foregoing rights on either these or other - terms. - - This license is subject to the following condition: - - The above copyright notice and either this complete permission notice or at a minimum - a reference to the UPL must be included in all copies or substantial portions of the - Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE - OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - "# - .trim(), - ); - - assert!(is_license_eligible_for_data_collection(&example_license)); + const APACHE_2_0_TXT: &str = include_str!("license_detection/apache-2.0.txt"); + const ISC_TXT: &str = include_str!("license_detection/isc.txt"); + const MIT_TXT: &str = include_str!("license_detection/mit.txt"); + const UPL_1_0_TXT: &str = include_str!("license_detection/upl-1.0.txt"); + const BSD_0_CLAUSE_TXT: &str = include_str!("license_detection/0bsd.txt"); + const BSD_1_CLAUSE_TXT: &str = include_str!("license_detection/bsd-1-clause.txt"); + const BSD_2_CLAUSE_TXT: &str = include_str!("license_detection/bsd-2-clause.txt"); + const BSD_3_CLAUSE_TXT: &str = include_str!("license_detection/bsd-3-clause.txt"); + + #[track_caller] + fn assert_matches_license(text: &str, license: OpenSourceLicense) { + let license_regex = + Regex::new(&format!("^{}$", canonicalize_license_text(license.regex()))).unwrap(); + assert!(license_regex.is_match(&canonicalize_license_text(text))); + assert_eq!(detect_license(text), Some(license)); } #[test] - fn test_upl_negative_detection() { - let example_license = unindent( - r#" - UPL License - - Copyright (c) 2024, John Doe - - The Universal Permissive License (UPL), Version 1.0 - - Subject to the condition set forth below, permission is hereby granted to any person - obtaining a copy of this software, associated documentation and/or data (collectively - the "Software"), free of charge and under any and all copyright rights in the - Software, and any and all patent rights owned or freely licensable by each licensor - hereunder covering either (i) the unmodified Software as contributed to or provided - by such licensor, or (ii) the Larger Works (as defined below), to deal in both - - (a) the Software, and - - (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is - included with the Software (each a "Larger Work" to which the Software is - contributed by such licensors), - - without restriction, including without limitation the rights to copy, create - derivative works of, display, perform, and distribute the Software and make, use, - sell, offer for sale, import, export, have made, and have sold the Software and the - Larger Work(s), and to sublicense the foregoing rights on either these or other - terms. - - This license is subject to the following condition: - - The above copyright notice and either this complete permission notice or at a minimum - a reference to the UPL must be included in all copies or substantial portions of the - Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE - OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - This project is dual licensed under the ISC License and the MIT License. - "# - .trim(), - ); - - assert!(!is_license_eligible_for_data_collection(&example_license)); + fn test_0bsd_positive_detection() { + assert_matches_license(BSD_0_CLAUSE_TXT, OpenSourceLicense::BSD0Clause); } #[test] fn test_apache_positive_detection() { - assert!(is_license_eligible_for_data_collection(APACHE_LICENSE)); + assert_matches_license(APACHE_2_0_TXT, OpenSourceLicense::Apache2_0); let license_with_appendix = format!( - r#"{APACHE_LICENSE} + r#"{APACHE_2_0_TXT} END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -395,9 +310,7 @@ mod tests { See the License for the specific language governing permissions and limitations under the License."# ); - assert!(is_license_eligible_for_data_collection( - &license_with_appendix - )); + assert_matches_license(&license_with_appendix, OpenSourceLicense::Apache2_0); // Sometimes people fill in the appendix with copyright info. let license_with_copyright = license_with_appendix.replace( @@ -405,16 +318,79 @@ mod tests { "Copyright 2025 John Doe", ); assert!(license_with_copyright != license_with_appendix); - assert!(is_license_eligible_for_data_collection( - &license_with_copyright - )); + assert_matches_license(&license_with_copyright, OpenSourceLicense::Apache2_0); } #[test] fn test_apache_negative_detection() { - assert!(!is_license_eligible_for_data_collection(&format!( - "{APACHE_LICENSE}\n\nThe terms in this license are void if P=NP." - ))); + assert!( + detect_license(&format!( + "{APACHE_2_0_TXT}\n\nThe terms in this license are void if P=NP." + )) + .is_none() + ); + } + + #[test] + fn test_bsd_1_clause_positive_detection() { + assert_matches_license(BSD_1_CLAUSE_TXT, OpenSourceLicense::BSD1Clause); + } + + #[test] + fn test_bsd_2_clause_positive_detection() { + assert_matches_license(BSD_2_CLAUSE_TXT, OpenSourceLicense::BSD2Clause); + } + + #[test] + fn test_bsd_3_clause_positive_detection() { + assert_matches_license(BSD_3_CLAUSE_TXT, OpenSourceLicense::BSD3Clause); + } + + #[test] + fn test_isc_positive_detection() { + assert_matches_license(ISC_TXT, OpenSourceLicense::ISC); + } + + #[test] + fn test_isc_negative_detection() { + let license_text = format!( + r#"{ISC_TXT} + + This project is dual licensed under the ISC License and the MIT License."# + ); + + assert!(detect_license(&license_text).is_none()); + } + + #[test] + fn test_mit_positive_detection() { + assert_matches_license(MIT_TXT, OpenSourceLicense::MIT); + } + + #[test] + fn test_mit_negative_detection() { + let license_text = format!( + r#"{MIT_TXT} + + This project is dual licensed under the MIT License and the Apache License, Version 2.0."# + ); + assert!(detect_license(&license_text).is_none()); + } + + #[test] + fn test_upl_positive_detection() { + assert_matches_license(UPL_1_0_TXT, OpenSourceLicense::UPL1_0); + } + + #[test] + fn test_upl_negative_detection() { + let license_text = format!( + r#"{UPL_1_0_TXT} + + This project is dual licensed under the UPL License and the MIT License."# + ); + + assert!(detect_license(&license_text).is_none()); } #[test] @@ -439,10 +415,22 @@ mod tests { assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-ISC")); assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-UPL")); + // Test with "license" coming after + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"APACHE-LICENSE")); + + // Test version numbers + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"APACHE-2")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"APACHE-2.0")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"BSD-1")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"BSD-2")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"BSD-3")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"BSD-3-CLAUSE")); + // Test combinations assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-MIT.txt")); assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE.ISC.md")); assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license_upl")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.APACHE.2.0")); // Test case insensitive assert!(LICENSE_FILE_NAME_REGEX.is_match(b"License")); @@ -461,39 +449,17 @@ mod tests { assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.old")); assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-GPL")); assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSEABC")); - assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"")); } #[test] fn test_canonicalize_license_text() { - // Test basic whitespace normalization - let input = "Line 1\n Line 2 \n\n\n Line 3 "; - let expected = "Line 1 Line 2\n\nLine 3"; - assert_eq!(canonicalize_license_text(input), expected); - - // Test paragraph separation - let input = "Paragraph 1\nwith multiple lines\n\n\n\nParagraph 2\nwith more lines"; - let expected = "Paragraph 1 with multiple lines\n\nParagraph 2 with more lines"; - assert_eq!(canonicalize_license_text(input), expected); - - // Test empty paragraphs are filtered out - let input = "\n\n\nParagraph 1\n\n\n \n\n\nParagraph 2\n\n\n"; - let expected = "Paragraph 1\n\nParagraph 2"; - assert_eq!(canonicalize_license_text(input), expected); - - // Test single line - let input = " Single line with spaces "; - let expected = "Single line with spaces"; - assert_eq!(canonicalize_license_text(input), expected); - - // Test multiple consecutive spaces within lines - let input = "Word1 Word2\n\nWord3 Word4"; - let expected = "Word1 Word2\n\nWord3 Word4"; + let input = " Paragraph 1\nwith multiple lines\n\n\n\nParagraph 2\nwith more lines\n "; + let expected = "paragraph 1 with multiple lines paragraph 2 with more lines"; assert_eq!(canonicalize_license_text(input), expected); // Test tabs and mixed whitespace let input = "Word1\t\tWord2\n\n Word3\r\n\r\n\r\nWord4 "; - let expected = "Word1 Word2\n\nWord3\n\nWord4"; + let expected = "word1 word2 word3 word4"; assert_eq!(canonicalize_license_text(input), expected); } @@ -532,9 +498,7 @@ mod tests { .trim(), ); - assert!(is_license_eligible_for_data_collection( - &mit_with_weird_spacing - )); + assert_matches_license(&mit_with_weird_spacing, OpenSourceLicense::MIT); } fn init_test(cx: &mut TestAppContext) { @@ -590,14 +554,14 @@ mod tests { assert!(matches!(watcher, LicenseDetectionWatcher::Local { .. })); assert!(!watcher.is_project_open_source()); - fs.write(Path::new("/root/LICENSE-MIT"), MIT_LICENSE.as_bytes()) + fs.write(Path::new("/root/LICENSE-MIT"), MIT_TXT.as_bytes()) .await .unwrap(); cx.background_executor.run_until_parked(); assert!(watcher.is_project_open_source()); - fs.write(Path::new("/root/LICENSE-APACHE"), APACHE_LICENSE.as_bytes()) + fs.write(Path::new("/root/LICENSE-APACHE"), APACHE_2_0_TXT.as_bytes()) .await .unwrap(); @@ -630,7 +594,7 @@ mod tests { let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/root", - json!({ "main.rs": "fn main() {}", "LICENSE-MIT": MIT_LICENSE }), + json!({ "main.rs": "fn main() {}", "LICENSE-MIT": MIT_TXT }), ) .await; diff --git a/crates/zeta/src/license_detection/0bsd.regex b/crates/zeta/src/license_detection/0bsd.regex new file mode 100644 index 0000000000000000000000000000000000000000..7928a8d181a48ad54bb825ac120aaa4ef53ba8ef --- /dev/null +++ b/crates/zeta/src/license_detection/0bsd.regex @@ -0,0 +1,12 @@ +.* + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted\. + +THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS\. IN NO EVENT SHALL THE AUTHOR BE LIABLE +FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE\. diff --git a/crates/zeta/src/license_detection/0bsd.txt b/crates/zeta/src/license_detection/0bsd.txt new file mode 100644 index 0000000000000000000000000000000000000000..d3061a372fda562b5a1d0a85bc56c67fc0d7d3fb --- /dev/null +++ b/crates/zeta/src/license_detection/0bsd.txt @@ -0,0 +1,13 @@ +Zero-Clause BSD +============= + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE +FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/crates/zeta/src/license_detection/apache.regex b/crates/zeta/src/license_detection/apache-2.0.regex similarity index 98% rename from crates/zeta/src/license_detection/apache.regex rename to crates/zeta/src/license_detection/apache-2.0.regex index e200e063c9d35f6e56d6f808190fc4206e7ea02c..dcf12fe28915f94e1f5d8de81285ea49dcc10f8e 100644 --- a/crates/zeta/src/license_detection/apache.regex +++ b/crates/zeta/src/license_detection/apache-2.0.regex @@ -1,4 +1,4 @@ - ^Apache License + Apache License Version 2\.0, January 2004 http://www\.apache\.org/licenses/ @@ -171,9 +171,9 @@ of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability\.(:? + of your accepting any such warranty or additional liability\.(?: - END OF TERMS AND CONDITIONS)?(:? + END OF TERMS AND CONDITIONS)?(?: APPENDIX: How to apply the Apache License to your work\. @@ -184,9 +184,9 @@ comment syntax for the file format\. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier - identification within third\-party archives\.)?(:? + identification within third\-party archives\.)?(?: - Copyright .*)?(:? + Copyright .*)?(?: Licensed under the Apache License, Version 2\.0 \(the "License"\); you may not use this file except in compliance with the License\. @@ -198,4 +198,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\. See the License for the specific language governing permissions and - limitations under the License\.)?$ + limitations under the License\.)? diff --git a/crates/zeta/src/license_detection/apache-text b/crates/zeta/src/license_detection/apache-2.0.txt similarity index 100% rename from crates/zeta/src/license_detection/apache-text rename to crates/zeta/src/license_detection/apache-2.0.txt diff --git a/crates/zeta/src/license_detection/bsd-1-clause.regex b/crates/zeta/src/license_detection/bsd-1-clause.regex new file mode 100644 index 0000000000000000000000000000000000000000..5e73e5c6d0e67cd9e4899e1a44bd064f11f3e3dc --- /dev/null +++ b/crates/zeta/src/license_detection/bsd-1-clause.regex @@ -0,0 +1,17 @@ +.*Copyright.* + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +(?:1\.|\*)? Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer\. + +THIS SOFTWARE IS PROVIDED BY .* “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL .* BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES \(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION\) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT \(INCLUDING NEGLIGENCE OR OTHERWISE\) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE\. diff --git a/crates/zeta/src/license_detection/bsd-1-clause.txt b/crates/zeta/src/license_detection/bsd-1-clause.txt new file mode 100644 index 0000000000000000000000000000000000000000..1ae6f9d5ff16f1783ac1d62f438dc8e566414cd3 --- /dev/null +++ b/crates/zeta/src/license_detection/bsd-1-clause.txt @@ -0,0 +1,20 @@ +Copyright (c) 2024 John Doe +Some Organization +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +THIS SOFTWARE IS PROVIDED BY [Name of Organization] “AS IS” AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL [Name of Organisation] BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY +OF SUCH DAMAGE. diff --git a/crates/zeta/src/license_detection/bsd-2-clause.regex b/crates/zeta/src/license_detection/bsd-2-clause.regex new file mode 100644 index 0000000000000000000000000000000000000000..93d22652fb11ba81d55e7d2d38e1b42bdce243b6 --- /dev/null +++ b/crates/zeta/src/license_detection/bsd-2-clause.regex @@ -0,0 +1,22 @@ +.*Copyright.* + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +(?:1\.|\*)? Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer\. + +(?:2\.|\*)? Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution\. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED\. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES \(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION\) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT \(INCLUDING NEGLIGENCE OR OTHERWISE\) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE\. diff --git a/crates/zeta/src/license_detection/bsd-2-clause.txt b/crates/zeta/src/license_detection/bsd-2-clause.txt new file mode 100644 index 0000000000000000000000000000000000000000..bbf946465e7f0f7f24b73bbf944bbd699f1b8e14 --- /dev/null +++ b/crates/zeta/src/license_detection/bsd-2-clause.txt @@ -0,0 +1,26 @@ +Copyright (c) 2024 + +John Doe (john.doe@gmail.com) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/crates/zeta/src/license_detection/bsd-3-clause.regex b/crates/zeta/src/license_detection/bsd-3-clause.regex new file mode 100644 index 0000000000000000000000000000000000000000..b31443de64283d0d66135b73e57eaf9bd19b88a3 --- /dev/null +++ b/crates/zeta/src/license_detection/bsd-3-clause.regex @@ -0,0 +1,26 @@ +.*Copyright.* + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +(?:1\.|\*)? Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer\. + +(?:2\.|\*)? Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution\. + +(?:3\.|\*)? Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission\. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED\. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES \(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION\) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT \(INCLUDING NEGLIGENCE OR OTHERWISE\) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE\. diff --git a/crates/zeta/src/license_detection/bsd-3-clause.txt b/crates/zeta/src/license_detection/bsd-3-clause.txt new file mode 100644 index 0000000000000000000000000000000000000000..0edcde7462648aaee95c558e1eec94dba303de16 --- /dev/null +++ b/crates/zeta/src/license_detection/bsd-3-clause.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2025, John Doe +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/crates/zeta/src/license_detection/isc.regex b/crates/zeta/src/license_detection/isc.regex index 63c6126bcea79e5103656788cb28a5b2b6faec22..ddaece5375fc17455e8640bb47a807d5cd347f5b 100644 --- a/crates/zeta/src/license_detection/isc.regex +++ b/crates/zeta/src/license_detection/isc.regex @@ -1,4 +1,4 @@ -^.*ISC License.* +.*ISC License.* Copyright.* @@ -12,4 +12,4 @@ MERCHANTABILITY AND FITNESS\. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE\.$ +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE\. diff --git a/crates/zeta/src/license_detection/isc.txt b/crates/zeta/src/license_detection/isc.txt new file mode 100644 index 0000000000000000000000000000000000000000..97fda7f97515bf3f2010eaf5f93f07cda371a14c --- /dev/null +++ b/crates/zeta/src/license_detection/isc.txt @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2024, John Doe + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/crates/zeta/src/license_detection/mit.regex b/crates/zeta/src/license_detection/mit.regex index deda8f0352270bb09ab4ad3631fd35246c89aa9a..43130424c5fe5f73d11ddda5d5c821bc6cb86afe 100644 --- a/crates/zeta/src/license_detection/mit.regex +++ b/crates/zeta/src/license_detection/mit.regex @@ -1,4 +1,4 @@ -^.*MIT License.* +.*MIT License.* Copyright.* @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE\.$ +SOFTWARE\. diff --git a/crates/zeta/src/license_detection/mit-text b/crates/zeta/src/license_detection/mit.txt similarity index 100% rename from crates/zeta/src/license_detection/mit-text rename to crates/zeta/src/license_detection/mit.txt diff --git a/crates/zeta/src/license_detection/upl.regex b/crates/zeta/src/license_detection/upl-1.0.regex similarity index 96% rename from crates/zeta/src/license_detection/upl.regex rename to crates/zeta/src/license_detection/upl-1.0.regex index 34ba2a64c66abb553ca1721d52c3e1d2752f5076..0959f729716af4714ae9f41c92e1480d276cdeab 100644 --- a/crates/zeta/src/license_detection/upl.regex +++ b/crates/zeta/src/license_detection/upl-1.0.regex @@ -1,4 +1,4 @@ -^Copyright.* +Copyright.* The Universal Permissive License.* @@ -32,4 +32,4 @@ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE\.$ +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE\. diff --git a/crates/zeta/src/license_detection/upl-1.0.txt b/crates/zeta/src/license_detection/upl-1.0.txt new file mode 100644 index 0000000000000000000000000000000000000000..6193e80270967eee149b9ae7b3c392ed1d45cf15 --- /dev/null +++ b/crates/zeta/src/license_detection/upl-1.0.txt @@ -0,0 +1,35 @@ +Copyright (c) 2025, John Doe + +The Universal Permissive License (UPL), Version 1.0 + +Subject to the condition set forth below, permission is hereby granted to any person +obtaining a copy of this software, associated documentation and/or data (collectively +the "Software"), free of charge and under any and all copyright rights in the +Software, and any and all patent rights owned or freely licensable by each licensor +hereunder covering either (i) the unmodified Software as contributed to or provided +by such licensor, or (ii) the Larger Works (as defined below), to deal in both + +(a) the Software, and + +(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is + included with the Software (each a "Larger Work" to which the Software is + contributed by such licensors), + +without restriction, including without limitation the rights to copy, create +derivative works of, display, perform, and distribute the Software and make, use, +sell, offer for sale, import, export, have made, and have sold the Software and the +Larger Work(s), and to sublicense the foregoing rights on either these or other +terms. + +This license is subject to the following condition: + +The above copyright notice and either this complete permission notice or at a minimum +a reference to the UPL must be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From db508bbbe2fe41507b2930d19effbecd25ea84c4 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 29 Aug 2025 17:29:58 -0400 Subject: [PATCH 449/744] docs: Remove MSYS2 instructions --- docs/src/development/windows.md | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/docs/src/development/windows.md b/docs/src/development/windows.md index 551d5f9f21129d7fbe774c8265e385150ab0f0b0..45e8ea911bb2add362d2f53bc46e9674535663e2 100644 --- a/docs/src/development/windows.md +++ b/docs/src/development/windows.md @@ -114,20 +114,7 @@ cargo test --workspace ## Installing from msys2 -[MSYS2](https://msys2.org/) distribution provides Zed as a package [mingw-w64-zed](https://packages.msys2.org/base/mingw-w64-zed). The package is available for UCRT64, CLANG64 and CLANGARM64 repositories. To download it, run - -```sh -pacman -Syu -pacman -S $MINGW_PACKAGE_PREFIX-zed -``` - -You can see the [build script](https://github.com/msys2/MINGW-packages/blob/master/mingw-w64-zed/PKGBUILD) for more details on build process. - -> Please, report any issue in [msys2/MINGW-packages/issues](https://github.com/msys2/MINGW-packages/issues?q=is%3Aissue+is%3Aopen+zed) first. - -See also MSYS2 [documentation page](https://www.msys2.org/docs/ides-editors). - -Note that `collab` is not supported for MSYS2. +Zed does not support unofficial MSYS2 Zed packages built for Mingw-w64. Please report any issues you may have with [mingw-w64-zed](https://packages.msys2.org/base/mingw-w64-zed) to [msys2/MINGW-packages/issues](https://github.com/msys2/MINGW-packages/issues?q=is%3Aissue+is%3Aopen+zed). ## Troubleshooting From bdedb18c300e71086a63dae1cacf3fe87c885fcf Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 29 Aug 2025 17:36:22 -0400 Subject: [PATCH 450/744] docs: Fix msys2 (#37199) I accidentally pushed https://github.com/zed-industries/zed/commit/db508bbbe2fe41507b2930d19effbecd25ea84c4 to main instead of to a branch. That broke tests. Release Notes: - N/A --- docs/src/development/windows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/development/windows.md b/docs/src/development/windows.md index 45e8ea911bb2add362d2f53bc46e9674535663e2..a4ad220bcc859d7d49edec7f967537ee4de2418a 100644 --- a/docs/src/development/windows.md +++ b/docs/src/development/windows.md @@ -114,7 +114,7 @@ cargo test --workspace ## Installing from msys2 -Zed does not support unofficial MSYS2 Zed packages built for Mingw-w64. Please report any issues you may have with [mingw-w64-zed](https://packages.msys2.org/base/mingw-w64-zed) to [msys2/MINGW-packages/issues](https://github.com/msys2/MINGW-packages/issues?q=is%3Aissue+is%3Aopen+zed). +Zed does not support unofficial MSYS2 Zed packages built for Mingw-w64. Please report any issues you may have with [mingw-w64-zed](https://packages.msys2.org/base/mingw-w64-zed) to [msys2/MINGW-packages/issues](https://github.com/msys2/MINGW-packages/issues?q=is%3Aissue+is%3Aopen+zed). ## Troubleshooting From a70cf3f1d432462f164fbc4b4de187bc7b52e31d Mon Sep 17 00:00:00 2001 From: Shardul Vaidya <31039336+5herlocked@users.noreply.github.com> Date: Fri, 29 Aug 2025 18:13:06 -0400 Subject: [PATCH 451/744] bedrock: Inference Config updates (#35808) Fixes #36866 - Updated internal naming for Claude 4 models to be consistent. - Corrected max output tokens for Anthropic Bedrock models to match docs Shoutout to @tlehn for noticing the bug, and finding the resolution. Release Notes: - bedrock: Fixed inference config errors causing Opus 4 Thinking and Opus 4.1 Thinking to fail (thanks [@tlehn](https://github.com/tlehn) and [@5herlocked](https://github.com/5herlocked]) - bedrock: Fixed an issue which prevented Rules / System prompts not functioning with Bedrock models (thanks [@tlehn](https://github.com/tlehn) and [@5herlocked](https://github.com/5herlocked]) --- crates/bedrock/src/bedrock.rs | 18 +++++++++++++++++- crates/bedrock/src/models.rs | 28 +++++++++++++--------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/crates/bedrock/src/bedrock.rs b/crates/bedrock/src/bedrock.rs index c8315d4201a46d5ac47825ff40aed3829f191d87..ec0b4070906fdfd31195668312b3e7b425cd28ee 100644 --- a/crates/bedrock/src/bedrock.rs +++ b/crates/bedrock/src/bedrock.rs @@ -3,6 +3,7 @@ mod models; use anyhow::{Context, Error, Result, anyhow}; use aws_sdk_bedrockruntime as bedrock; pub use aws_sdk_bedrockruntime as bedrock_client; +use aws_sdk_bedrockruntime::types::InferenceConfiguration; pub use aws_sdk_bedrockruntime::types::{ AnyToolChoice as BedrockAnyToolChoice, AutoToolChoice as BedrockAutoToolChoice, ContentBlock as BedrockInnerContent, Tool as BedrockTool, ToolChoice as BedrockToolChoice, @@ -17,7 +18,8 @@ pub use bedrock::types::{ ConverseOutput as BedrockResponse, ConverseStreamOutput as BedrockStreamingResponse, ImageBlock as BedrockImageBlock, Message as BedrockMessage, ReasoningContentBlock as BedrockThinkingBlock, ReasoningTextBlock as BedrockThinkingTextBlock, - ResponseStream as BedrockResponseStream, ToolResultBlock as BedrockToolResultBlock, + ResponseStream as BedrockResponseStream, SystemContentBlock as BedrockSystemContentBlock, + ToolResultBlock as BedrockToolResultBlock, ToolResultContentBlock as BedrockToolResultContentBlock, ToolResultStatus as BedrockToolResultStatus, ToolUseBlock as BedrockToolUseBlock, }; @@ -58,6 +60,20 @@ pub async fn stream_completion( response = response.set_tool_config(request.tools); } + let inference_config = InferenceConfiguration::builder() + .max_tokens(request.max_tokens as i32) + .set_temperature(request.temperature) + .set_top_p(request.top_p) + .build(); + + response = response.inference_config(inference_config); + + if let Some(system) = request.system { + if !system.is_empty() { + response = response.system(BedrockSystemContentBlock::Text(system)); + } + } + let output = response .send() .await diff --git a/crates/bedrock/src/models.rs b/crates/bedrock/src/models.rs index 69d2ffb84569ef848f88de47f5394a6b25b18e02..c3a793d69d086a8a8c607d34debc5a7034f33f32 100644 --- a/crates/bedrock/src/models.rs +++ b/crates/bedrock/src/models.rs @@ -151,12 +151,12 @@ impl Model { pub fn id(&self) -> &str { match self { - 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::ClaudeSonnet4 => "claude-sonnet-4", + Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking", + Model::ClaudeOpus4 => "claude-opus-4", + Model::ClaudeOpus4_1 => "claude-opus-4-1", + Model::ClaudeOpus4Thinking => "claude-opus-4-thinking", + Model::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking", Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2", Model::Claude3_5Sonnet => "claude-3-5-sonnet", Model::Claude3Opus => "claude-3-opus", @@ -359,14 +359,12 @@ impl Model { pub fn max_output_tokens(&self) -> u64 { match self { Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096, - Self::Claude3_7Sonnet - | Self::Claude3_7SonnetThinking - | Self::ClaudeSonnet4 - | Self::ClaudeSonnet4Thinking - | Self::ClaudeOpus4 - | Model::ClaudeOpus4Thinking + Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000, + Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 64_000, + Self::ClaudeOpus4 + | Self::ClaudeOpus4Thinking | Self::ClaudeOpus4_1 - | Model::ClaudeOpus4_1Thinking => 128_000, + | Self::ClaudeOpus4_1Thinking => 32_000, Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192, Self::Custom { max_output_tokens, .. @@ -784,10 +782,10 @@ mod tests { ); // Test thinking models have different friendly IDs but same request IDs - assert_eq!(Model::ClaudeSonnet4.id(), "claude-4-sonnet"); + assert_eq!(Model::ClaudeSonnet4.id(), "claude-sonnet-4"); assert_eq!( Model::ClaudeSonnet4Thinking.id(), - "claude-4-sonnet-thinking" + "claude-sonnet-4-thinking" ); assert_eq!( Model::ClaudeSonnet4.request_id(), From 1c2e2a00fe87d8a9820d5d23f4828482f94c57f9 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 29 Aug 2025 18:26:11 -0400 Subject: [PATCH 452/744] agent: Re-add workaround for language model behavior with empty tool result (#37196) This is just copying over the same workaround here: https://github.com/zed-industries/zed/blob/a790e514af4d6957aa1a14cc8190b2ff24a0484c/crates/agent/src/thread.rs#L1455-L1459 Into the agent2 code. Release Notes: - agent: Fixed an issue where some tool calls in the Zed agent could return an error like "`tool_use` ids were found without `tool_result` blocks immediately after" --- crates/agent2/src/thread.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 97ea1caf1d766be0314a16cc0f518ad701564569..8ff5b845066c8af90eb713aef2a0c87e6d114a85 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -484,11 +484,15 @@ impl AgentMessage { }; for tool_result in self.tool_results.values() { + let mut tool_result = tool_result.clone(); + // Surprisingly, the API fails if we return an empty string here. + // It thinks we are sending a tool use without a tool result. + if tool_result.content.is_empty() { + tool_result.content = "".into(); + } user_message .content - .push(language_model::MessageContent::ToolResult( - tool_result.clone(), - )); + .push(language_model::MessageContent::ToolResult(tool_result)); } let mut messages = Vec::new(); From f78f3e7729b6e505685ba20ef207c709f0229149 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Aug 2025 17:18:52 -0700 Subject: [PATCH 453/744] Add initial support for WSL (#37035) Closes #36188 ## Todo * [x] CLI * [x] terminals * [x] tasks ## For future PRs * debugging * UI for opening WSL projects * fixing workspace state restoration Release Notes: - Windows alpha: Zed now supports editing folders in WSL. --------- Co-authored-by: Junkui Zhang <364772080@qq.com> --- crates/auto_update_helper/src/updater.rs | 26 +- crates/cli/src/cli.rs | 1 + crates/cli/src/main.rs | 74 ++- crates/extension_host/src/extension_host.rs | 11 +- crates/paths/src/paths.rs | 5 + crates/project/src/debugger/dap_store.rs | 10 +- crates/project/src/project.rs | 4 +- crates/project/src/terminals.rs | 2 +- .../src/disconnected_overlay.rs | 31 +- crates/recent_projects/src/recent_projects.rs | 33 +- ...h_connections.rs => remote_connections.rs} | 138 ++--- crates/recent_projects/src/remote_servers.rs | 123 +++-- crates/remote/src/remote.rs | 3 +- crates/remote/src/remote_client.rs | 117 ++++- crates/remote/src/transport.rs | 335 ++++++++++++ crates/remote/src/transport/ssh.rs | 341 +----------- crates/remote/src/transport/wsl.rs | 494 ++++++++++++++++++ crates/title_bar/src/title_bar.rs | 13 +- crates/workspace/src/persistence.rs | 438 +++++++++++----- crates/workspace/src/persistence/model.rs | 32 +- crates/workspace/src/workspace.rs | 59 +-- crates/zed/resources/windows/zed-wsl | 25 + crates/zed/src/main.rs | 75 +-- crates/zed/src/zed.rs | 4 +- crates/zed/src/zed/open_listener.rs | 91 ++-- crates/zed/src/zed/windows_only_instance.rs | 1 + script/bundle-windows.ps1 | 1 + 27 files changed, 1701 insertions(+), 786 deletions(-) rename crates/recent_projects/src/{ssh_connections.rs => remote_connections.rs} (85%) create mode 100644 crates/remote/src/transport/wsl.rs create mode 100644 crates/zed/resources/windows/zed-wsl diff --git a/crates/auto_update_helper/src/updater.rs b/crates/auto_update_helper/src/updater.rs index 762771617609e63996685d3d96fae69135355249..a48bbccec304a1b49bb0496c21b299f5dd176076 100644 --- a/crates/auto_update_helper/src/updater.rs +++ b/crates/auto_update_helper/src/updater.rs @@ -16,7 +16,7 @@ use crate::windows_impl::WM_JOB_UPDATED; type Job = fn(&Path) -> Result<()>; #[cfg(not(test))] -pub(crate) const JOBS: [Job; 6] = [ +pub(crate) const JOBS: &[Job] = &[ // Delete old files |app_dir| { let zed_executable = app_dir.join("Zed.exe"); @@ -32,6 +32,12 @@ pub(crate) const JOBS: [Job; 6] = [ std::fs::remove_file(&zed_cli) .context(format!("Failed to remove old file {}", zed_cli.display())) }, + |app_dir| { + let zed_wsl = app_dir.join("bin\\zed"); + log::info!("Removing old file: {}", zed_wsl.display()); + std::fs::remove_file(&zed_wsl) + .context(format!("Failed to remove old file {}", zed_wsl.display())) + }, // Copy new files |app_dir| { let zed_executable_source = app_dir.join("install\\Zed.exe"); @@ -65,6 +71,22 @@ pub(crate) const JOBS: [Job; 6] = [ zed_cli_dest.display() )) }, + |app_dir| { + let zed_wsl_source = app_dir.join("install\\bin\\zed"); + let zed_wsl_dest = app_dir.join("bin\\zed"); + log::info!( + "Copying new file {} to {}", + zed_wsl_source.display(), + zed_wsl_dest.display() + ); + std::fs::copy(&zed_wsl_source, &zed_wsl_dest) + .map(|_| ()) + .context(format!( + "Failed to copy new file {} to {}", + zed_wsl_source.display(), + zed_wsl_dest.display() + )) + }, // Clean up installer folder and updates folder |app_dir| { let updates_folder = app_dir.join("updates"); @@ -85,7 +107,7 @@ pub(crate) const JOBS: [Job; 6] = [ ]; #[cfg(test)] -pub(crate) const JOBS: [Job; 2] = [ +pub(crate) const JOBS: &[Job] = &[ |_| { std::thread::sleep(Duration::from_millis(1000)); if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") { diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 6274f69035a02bed20d1a85608371744395c951a..79a10fa2b0936b44d9500fd9990ffa4c6ac62e85 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -14,6 +14,7 @@ pub enum CliRequest { paths: Vec, urls: Vec, diff_paths: Vec<[String; 2]>, + wsl: Option, wait: bool, open_new_workspace: Option, env: Option>, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index b84e7a9f7a53a471bd854a15377c79f45003aaf4..151e96e3cf68ab94295a8386d2842539e6a986a2 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -6,7 +6,6 @@ use anyhow::{Context as _, Result}; use clap::Parser; use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer}; -use collections::HashMap; use parking_lot::Mutex; use std::{ env, fs, io, @@ -85,6 +84,15 @@ struct Args { /// Run zed in dev-server mode #[arg(long)] dev_server_token: Option, + /// The username and WSL distribution to use when opening paths. ,If not specified, + /// Zed will attempt to open the paths directly. + /// + /// The username is optional, and if not specified, the default user for the distribution + /// will be used. + /// + /// Example: `me@Ubuntu` or `Ubuntu` for default distribution. + #[arg(long, value_name = "USER@DISTRO")] + wsl: Option, /// Not supported in Zed CLI, only supported on Zed binary /// Will attempt to give the correct command to run #[arg(long)] @@ -129,14 +137,41 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result { Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string())) } -fn main() -> Result<()> { - #[cfg(all(not(debug_assertions), target_os = "windows"))] - unsafe { - use ::windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole}; +fn parse_path_in_wsl(source: &str, wsl: &str) -> Result { + let mut command = util::command::new_std_command("wsl.exe"); - let _ = AttachConsole(ATTACH_PARENT_PROCESS); + let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') { + if user.is_empty() { + anyhow::bail!("user is empty in wsl argument"); + } + (Some(user), distro) + } else { + (None, wsl) + }; + + if let Some(user) = user { + command.arg("--user").arg(user); } + let output = command + .arg("--distribution") + .arg(distro_name) + .arg("wslpath") + .arg("-m") + .arg(source) + .output()?; + + let result = String::from_utf8_lossy(&output.stdout); + let prefix = format!("//wsl.localhost/{}", distro_name); + + Ok(result + .trim() + .strip_prefix(&prefix) + .unwrap_or(&result) + .to_string()) +} + +fn main() -> Result<()> { #[cfg(unix)] util::prevent_root_execution(); @@ -223,6 +258,8 @@ fn main() -> Result<()> { let env = { #[cfg(any(target_os = "linux", target_os = "freebsd"))] { + use collections::HashMap; + // On Linux, the desktop entry uses `cli` to spawn `zed`. // We need to handle env vars correctly since std::env::vars() may not contain // project-specific vars (e.g. those set by direnv). @@ -235,8 +272,19 @@ fn main() -> Result<()> { } } - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - Some(std::env::vars().collect::>()) + #[cfg(target_os = "windows")] + { + // On Windows, by default, a child process inherits a copy of the environment block of the parent process. + // So we don't need to pass env vars explicitly. + None + } + + #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))] + { + use collections::HashMap; + + Some(std::env::vars().collect::>()) + } }; let exit_status = Arc::new(Mutex::new(None)); @@ -271,8 +319,10 @@ fn main() -> Result<()> { paths.push(tmp_file.path().to_string_lossy().to_string()); let (tmp_file, _) = tmp_file.keep()?; anonymous_fd_tmp_files.push((file, tmp_file)); + } else if let Some(wsl) = &args.wsl { + urls.push(format!("file://{}", parse_path_in_wsl(path, wsl)?)); } else { - paths.push(parse_path_with_position(path)?) + paths.push(parse_path_with_position(path)?); } } @@ -292,6 +342,7 @@ fn main() -> Result<()> { paths, urls, diff_paths, + wsl: args.wsl, wait: args.wait, open_new_workspace, env, @@ -644,15 +695,15 @@ mod windows { Storage::FileSystem::{ CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, WriteFile, }, - System::Threading::CreateMutexW, + System::Threading::{CREATE_NEW_PROCESS_GROUP, CreateMutexW}, }, core::HSTRING, }; use crate::{Detect, InstalledApp}; - use std::io; use std::path::{Path, PathBuf}; use std::process::ExitStatus; + use std::{io, os::windows::process::CommandExt}; fn check_single_instance() -> bool { let mutex = unsafe { @@ -691,6 +742,7 @@ mod windows { fn launch(&self, ipc_url: String) -> anyhow::Result<()> { if check_single_instance() { std::process::Command::new(self.0.clone()) + .creation_flags(CREATE_NEW_PROCESS_GROUP.0) .arg(ipc_url) .spawn()?; } else { diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index b8189c36511a03f136e5e215549453947e888bb1..b114ad9f4c526f9c270681c55626455531becc2f 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -43,7 +43,7 @@ use language::{ use node_runtime::NodeRuntime; use project::ContextProviderWithTasks; use release_channel::ReleaseChannel; -use remote::RemoteClient; +use remote::{RemoteClient, RemoteConnectionOptions}; use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -117,7 +117,7 @@ pub struct ExtensionStore { pub wasm_host: Arc, pub wasm_extensions: Vec<(Arc, WasmExtension)>, pub tasks: Vec>, - pub remote_clients: HashMap>, + pub remote_clients: HashMap>, pub ssh_registered_tx: UnboundedSender<()>, } @@ -1779,16 +1779,15 @@ impl ExtensionStore { } pub fn register_remote_client(&mut self, client: Entity, cx: &mut Context) { - let connection_options = client.read(cx).connection_options(); - let ssh_url = connection_options.ssh_url(); + let options = client.read(cx).connection_options(); - if let Some(existing_client) = self.remote_clients.get(&ssh_url) + if let Some(existing_client) = self.remote_clients.get(&options) && existing_client.upgrade().is_some() { return; } - self.remote_clients.insert(ssh_url, client.downgrade()); + self.remote_clients.insert(options, client.downgrade()); self.ssh_registered_tx.unbounded_send(()).ok(); } } diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index c2c3c89305939bc32c635549c23d64d565f8fbb0..ede42af0272902892afd2e9dfdafb5c5eae2f8f5 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -33,6 +33,11 @@ pub fn remote_server_dir_relative() -> &'static Path { Path::new(".zed_server") } +/// Returns the relative path to the zed_wsl_server directory on the wsl host. +pub fn remote_wsl_server_dir_relative() -> &'static Path { + Path::new(".zed_wsl_server") +} + /// Sets a custom directory for all user data, overriding the default data directory. /// This function must be called before any other path operations that depend on the data directory. /// The directory's path will be canonicalized to an absolute path by a blocking FS operation. diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index d8c6d3acc1116e9a97b2f6ca3fc54ec098029cbe..6c1449b728d3ee5b8c8b019d5e527e9adfb3bf25 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -258,8 +258,14 @@ impl DapStore { let connection; if let Some(c) = binary.connection { let host = Ipv4Addr::LOCALHOST; - let port = dap::transport::TcpTransport::unused_port(host).await?; - port_forwarding = Some((port, c.host.to_string(), c.port)); + let port; + if remote.read_with(cx, |remote, _cx| remote.shares_network_interface())? { + port = c.port; + port_forwarding = None; + } else { + port = dap::transport::TcpTransport::unused_port(host).await?; + port_forwarding = Some((port, c.host.to_string(), c.port)); + } connection = Some(TcpArguments { port, host, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b32e95741f522650e5d20f80a6ba18c423805234..557367edf522a103ee1a8b55f5264be561d1698e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -87,7 +87,7 @@ use node_runtime::NodeRuntime; use parking_lot::Mutex; pub use prettier_store::PrettierStore; use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent}; -use remote::{RemoteClient, SshConnectionOptions}; +use remote::{RemoteClient, RemoteConnectionOptions}; use rpc::{ AnyProtoClient, ErrorCode, proto::{FromProto, LanguageServerPromptResponse, REMOTE_SERVER_PROJECT_ID, ToProto}, @@ -1916,7 +1916,7 @@ impl Project { .map(|remote| remote.read(cx).connection_state()) } - pub fn remote_connection_options(&self, cx: &App) -> Option { + pub fn remote_connection_options(&self, cx: &App) -> Option { self.remote_client .as_ref() .map(|remote| remote.read(cx).connection_options()) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index c189242fadc2948593186edb5dcd2c56879f07af..597da04617e9670e623196ef21f02c366e49d392 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -512,7 +512,7 @@ fn create_remote_shell( *env = command.env; log::debug!("Connecting to a remote server: {:?}", command.program); - let host = remote_client.read(cx).connection_options().host; + let host = remote_client.read(cx).connection_options().display_name(); Ok(Shell::WithArguments { program: command.program, diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index 36da6897b92e4bc183aa7c0f51d5100e8836931e..c97f7062a8206052e7c63f6bec909dd5823dbedf 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -1,6 +1,6 @@ use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, WeakEntity}; use project::project_settings::ProjectSettings; -use remote::SshConnectionOptions; +use remote::RemoteConnectionOptions; use settings::Settings; use ui::{ Button, ButtonCommon, ButtonStyle, Clickable, Context, ElevationIndex, FluentBuilder, Headline, @@ -9,11 +9,11 @@ use ui::{ }; use workspace::{ModalView, OpenOptions, Workspace, notifications::DetachAndPromptErr}; -use crate::open_ssh_project; +use crate::open_remote_project; enum Host { - RemoteProject, - SshRemoteProject(SshConnectionOptions), + CollabGuestProject, + RemoteServerProject(RemoteConnectionOptions), } pub struct DisconnectedOverlay { @@ -66,9 +66,9 @@ impl DisconnectedOverlay { let remote_connection_options = project.read(cx).remote_connection_options(cx); let host = if let Some(ssh_connection_options) = remote_connection_options { - Host::SshRemoteProject(ssh_connection_options) + Host::RemoteServerProject(ssh_connection_options) } else { - Host::RemoteProject + Host::CollabGuestProject }; workspace.toggle_modal(window, cx, |_, cx| DisconnectedOverlay { @@ -86,14 +86,14 @@ impl DisconnectedOverlay { self.finished = true; cx.emit(DismissEvent); - if let Host::SshRemoteProject(ssh_connection_options) = &self.host { - self.reconnect_to_ssh_remote(ssh_connection_options.clone(), window, cx); + if let Host::RemoteServerProject(ssh_connection_options) = &self.host { + self.reconnect_to_remote_project(ssh_connection_options.clone(), window, cx); } } - fn reconnect_to_ssh_remote( + fn reconnect_to_remote_project( &self, - connection_options: SshConnectionOptions, + connection_options: RemoteConnectionOptions, window: &mut Window, cx: &mut Context, ) { @@ -114,7 +114,7 @@ impl DisconnectedOverlay { .collect(); cx.spawn_in(window, async move |_, cx| { - open_ssh_project( + open_remote_project( connection_options, paths, app_state, @@ -138,13 +138,13 @@ impl DisconnectedOverlay { impl Render for DisconnectedOverlay { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let can_reconnect = matches!(self.host, Host::SshRemoteProject(_)); + let can_reconnect = matches!(self.host, Host::RemoteServerProject(_)); let message = match &self.host { - Host::RemoteProject => { + Host::CollabGuestProject => { "Your connection to the remote project has been lost.".to_string() } - Host::SshRemoteProject(options) => { + Host::RemoteServerProject(options) => { let autosave = if ProjectSettings::get_global(cx) .session .restore_unsaved_buffers @@ -155,7 +155,8 @@ impl Render for DisconnectedOverlay { }; format!( "Your connection to {} has been lost.{}", - options.host, autosave + options.display_name(), + autosave ) } }; diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index fa57b588cd8457788adc0226264a4871c3305b85..aa0ce7661b29123c25fdf20cbde5f53e6525d2d6 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -1,9 +1,10 @@ pub mod disconnected_overlay; +mod remote_connections; mod remote_servers; mod ssh_config; -mod ssh_connections; -pub use ssh_connections::{is_connecting_over_ssh, open_ssh_project}; +use remote::RemoteConnectionOptions; +pub use remote_connections::open_remote_project; use disconnected_overlay::DisconnectedOverlay; use fuzzy::{StringMatch, StringMatchCandidate}; @@ -16,9 +17,9 @@ use picker::{ Picker, PickerDelegate, highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths}, }; +pub use remote_connections::SshSettings; pub use remote_servers::RemoteServerProjects; use settings::Settings; -pub use ssh_connections::SshSettings; use std::{path::Path, sync::Arc}; use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container}; use util::{ResultExt, paths::PathExt}; @@ -290,7 +291,7 @@ impl PickerDelegate for RecentProjectsDelegate { if workspace.database_id() == Some(*candidate_workspace_id) { Task::ready(Ok(())) } else { - match candidate_workspace_location { + match candidate_workspace_location.clone() { SerializedWorkspaceLocation::Local => { let paths = candidate_workspace_paths.paths().to_vec(); if replace_current_window { @@ -320,7 +321,7 @@ impl PickerDelegate for RecentProjectsDelegate { workspace.open_workspace_for_paths(false, paths, window, cx) } } - SerializedWorkspaceLocation::Ssh(connection) => { + SerializedWorkspaceLocation::Remote(mut connection) => { let app_state = workspace.app_state().clone(); let replace_window = if replace_current_window { @@ -334,18 +335,16 @@ impl PickerDelegate for RecentProjectsDelegate { ..Default::default() }; - let connection_options = SshSettings::get_global(cx) - .connection_options_for( - connection.host.clone(), - connection.port, - connection.user.clone(), - ); + if let RemoteConnectionOptions::Ssh(connection) = &mut connection { + SshSettings::get_global(cx) + .fill_connection_options_from_settings(connection); + }; let paths = candidate_workspace_paths.paths().to_vec(); cx.spawn_in(window, async move |_, cx| { - open_ssh_project( - connection_options, + open_remote_project( + connection.clone(), paths, app_state, open_options, @@ -418,9 +417,11 @@ impl PickerDelegate for RecentProjectsDelegate { SerializedWorkspaceLocation::Local => Icon::new(IconName::Screen) .color(Color::Muted) .into_any_element(), - SerializedWorkspaceLocation::Ssh(_) => Icon::new(IconName::Server) - .color(Color::Muted) - .into_any_element(), + SerializedWorkspaceLocation::Remote(_) => { + Icon::new(IconName::Server) + .color(Color::Muted) + .into_any_element() + } }) }) .child({ diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/remote_connections.rs similarity index 85% rename from crates/recent_projects/src/ssh_connections.rs rename to crates/recent_projects/src/remote_connections.rs index 29f6e75bbdebf72b36295b20295f0705b636214e..47607813b547e28b9b4a37449f8daaa6ec022764 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -16,7 +16,8 @@ use language::CursorShape; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use release_channel::ReleaseChannel; use remote::{ - ConnectionIdentifier, RemoteClient, RemotePlatform, SshConnectionOptions, SshPortForwardOption, + ConnectionIdentifier, RemoteClient, RemoteConnectionOptions, RemotePlatform, + SshConnectionOptions, SshPortForwardOption, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -42,32 +43,35 @@ impl SshSettings { self.ssh_connections.clone().into_iter().flatten() } + pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) { + for conn in self.ssh_connections() { + if conn.host == options.host + && conn.username == options.username + && conn.port == options.port + { + options.nickname = conn.nickname; + options.upload_binary_over_ssh = conn.upload_binary_over_ssh.unwrap_or_default(); + options.args = Some(conn.args); + options.port_forwards = conn.port_forwards; + break; + } + } + } + pub fn connection_options_for( &self, host: String, port: Option, username: Option, ) -> SshConnectionOptions { - for conn in self.ssh_connections() { - if conn.host == host && conn.username == username && conn.port == port { - return SshConnectionOptions { - nickname: conn.nickname, - upload_binary_over_ssh: conn.upload_binary_over_ssh.unwrap_or_default(), - args: Some(conn.args), - host, - port, - username, - port_forwards: conn.port_forwards, - password: None, - }; - } - } - SshConnectionOptions { + let mut options = SshConnectionOptions { host, port, username, ..Default::default() - } + }; + self.fill_connection_options_from_settings(&mut options); + options } } @@ -135,7 +139,7 @@ impl Settings for SshSettings { fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } -pub struct SshPrompt { +pub struct RemoteConnectionPrompt { connection_string: SharedString, nickname: Option, status_message: Option, @@ -144,7 +148,7 @@ pub struct SshPrompt { editor: Entity, } -impl Drop for SshPrompt { +impl Drop for RemoteConnectionPrompt { fn drop(&mut self) { if let Some(cancel) = self.cancellation.take() { cancel.send(()).ok(); @@ -152,24 +156,22 @@ impl Drop for SshPrompt { } } -pub struct SshConnectionModal { - pub(crate) prompt: Entity, +pub struct RemoteConnectionModal { + pub(crate) prompt: Entity, paths: Vec, finished: bool, } -impl SshPrompt { +impl RemoteConnectionPrompt { pub(crate) fn new( - connection_options: &SshConnectionOptions, + connection_string: String, + nickname: Option, window: &mut Window, cx: &mut Context, ) -> Self { - let connection_string = connection_options.connection_string().into(); - let nickname = connection_options.nickname.clone().map(|s| s.into()); - Self { - connection_string, - nickname, + connection_string: connection_string.into(), + nickname: nickname.map(|nickname| nickname.into()), editor: cx.new(|cx| Editor::single_line(window, cx)), status_message: None, cancellation: None, @@ -232,7 +234,7 @@ impl SshPrompt { } } -impl Render for SshPrompt { +impl Render for RemoteConnectionPrompt { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let theme = ThemeSettings::get_global(cx); @@ -297,15 +299,22 @@ impl Render for SshPrompt { } } -impl SshConnectionModal { +impl RemoteConnectionModal { pub(crate) fn new( - connection_options: &SshConnectionOptions, + connection_options: &RemoteConnectionOptions, paths: Vec, window: &mut Window, cx: &mut Context, ) -> Self { + let (connection_string, nickname) = match connection_options { + RemoteConnectionOptions::Ssh(options) => { + (options.connection_string(), options.nickname.clone()) + } + RemoteConnectionOptions::Wsl(options) => (options.distro_name.clone(), None), + }; Self { - prompt: cx.new(|cx| SshPrompt::new(connection_options, window, cx)), + prompt: cx + .new(|cx| RemoteConnectionPrompt::new(connection_string, nickname, window, cx)), finished: false, paths, } @@ -386,7 +395,7 @@ impl RenderOnce for SshConnectionHeader { } } -impl Render for SshConnectionModal { +impl Render for RemoteConnectionModal { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { let nickname = self.prompt.read(cx).nickname.clone(); let connection_string = self.prompt.read(cx).connection_string.clone(); @@ -423,15 +432,15 @@ impl Render for SshConnectionModal { } } -impl Focusable for SshConnectionModal { +impl Focusable for RemoteConnectionModal { fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { self.prompt.read(cx).editor.focus_handle(cx) } } -impl EventEmitter for SshConnectionModal {} +impl EventEmitter for RemoteConnectionModal {} -impl ModalView for SshConnectionModal { +impl ModalView for RemoteConnectionModal { fn on_before_dismiss( &mut self, _window: &mut Window, @@ -446,13 +455,13 @@ impl ModalView for SshConnectionModal { } #[derive(Clone)] -pub struct SshClientDelegate { +pub struct RemoteClientDelegate { window: AnyWindowHandle, - ui: WeakEntity, + ui: WeakEntity, known_password: Option, } -impl remote::RemoteClientDelegate for SshClientDelegate { +impl remote::RemoteClientDelegate for RemoteClientDelegate { fn ask_password(&self, prompt: String, tx: oneshot::Sender, cx: &mut AsyncApp) { let mut known_password = self.known_password.clone(); if let Some(password) = known_password.take() { @@ -522,7 +531,7 @@ impl remote::RemoteClientDelegate for SshClientDelegate { } } -impl SshClientDelegate { +impl RemoteClientDelegate { fn update_status(&self, status: Option<&str>, cx: &mut AsyncApp) { self.window .update(cx, |_, _, cx| { @@ -534,14 +543,10 @@ impl SshClientDelegate { } } -pub fn is_connecting_over_ssh(workspace: &Workspace, cx: &App) -> bool { - workspace.active_modal::(cx).is_some() -} - pub fn connect_over_ssh( unique_identifier: ConnectionIdentifier, connection_options: SshConnectionOptions, - ui: Entity, + ui: Entity, window: &mut Window, cx: &mut App, ) -> Task>>> { @@ -554,7 +559,7 @@ pub fn connect_over_ssh( unique_identifier, connection_options, rx, - Arc::new(SshClientDelegate { + Arc::new(RemoteClientDelegate { window, ui: ui.downgrade(), known_password, @@ -563,8 +568,8 @@ pub fn connect_over_ssh( ) } -pub async fn open_ssh_project( - connection_options: SshConnectionOptions, +pub async fn open_remote_project( + connection_options: RemoteConnectionOptions, paths: Vec, app_state: Arc, open_options: workspace::OpenOptions, @@ -575,13 +580,7 @@ pub async fn open_ssh_project( } else { let workspace_position = cx .update(|cx| { - workspace::ssh_workspace_position_from_db( - connection_options.host.clone(), - connection_options.port, - connection_options.username.clone(), - &paths, - cx, - ) + workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx) })? .await .context("fetching ssh workspace position from db")?; @@ -611,16 +610,16 @@ pub async fn open_ssh_project( loop { let (cancel_tx, cancel_rx) = oneshot::channel(); let delegate = window.update(cx, { - let connection_options = connection_options.clone(); let paths = paths.clone(); + let connection_options = connection_options.clone(); move |workspace, window, cx| { window.activate_window(); workspace.toggle_modal(window, cx, |window, cx| { - SshConnectionModal::new(&connection_options, paths, window, cx) + RemoteConnectionModal::new(&connection_options, paths, window, cx) }); let ui = workspace - .active_modal::(cx)? + .active_modal::(cx)? .read(cx) .prompt .clone(); @@ -629,19 +628,25 @@ pub async fn open_ssh_project( ui.set_cancellation_tx(cancel_tx); }); - Some(Arc::new(SshClientDelegate { + Some(Arc::new(RemoteClientDelegate { window: window.window_handle(), ui: ui.downgrade(), - known_password: connection_options.password.clone(), + known_password: if let RemoteConnectionOptions::Ssh(options) = + &connection_options + { + options.password.clone() + } else { + None + }, })) } })?; let Some(delegate) = delegate else { break }; - let did_open_ssh_project = cx + let did_open_project = cx .update(|cx| { - workspace::open_ssh_project_with_new_connection( + workspace::open_remote_project_with_new_connection( window, connection_options.clone(), cancel_rx, @@ -655,19 +660,22 @@ pub async fn open_ssh_project( window .update(cx, |workspace, _, cx| { - if let Some(ui) = workspace.active_modal::(cx) { + if let Some(ui) = workspace.active_modal::(cx) { ui.update(cx, |modal, cx| modal.finished(cx)) } }) .ok(); - if let Err(e) = did_open_ssh_project { + if let Err(e) = did_open_project { log::error!("Failed to open project: {e:?}"); let response = window .update(cx, |_, window, cx| { window.prompt( PromptLevel::Critical, - "Failed to connect over SSH", + match connection_options { + RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH", + RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL", + }, Some(&e.to_string()), &["Retry", "Ok"], cx, diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index f4fd1f1c1bbb12e2fbf11088baf859b08bfbf310..3cf084bef76a56cf85973f67bb5713aee59fb1bc 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1,70 +1,52 @@ -use std::any::Any; -use std::borrow::Cow; -use std::collections::BTreeSet; -use std::path::PathBuf; -use std::rc::Rc; -use std::sync::Arc; -use std::sync::atomic; -use std::sync::atomic::AtomicUsize; - +use crate::{ + remote_connections::{ + RemoteConnectionModal, RemoteConnectionPrompt, RemoteSettingsContent, SshConnection, + SshConnectionHeader, SshProject, SshSettings, connect_over_ssh, open_remote_project, + }, + ssh_config::parse_ssh_config_hosts, +}; use editor::Editor; use file_finder::OpenPathDelegate; -use futures::FutureExt; -use futures::channel::oneshot; -use futures::future::Shared; -use futures::select; -use gpui::ClickEvent; -use gpui::ClipboardItem; -use gpui::Subscription; -use gpui::Task; -use gpui::WeakEntity; -use gpui::canvas; +use futures::{FutureExt, channel::oneshot, future::Shared, select}; use gpui::{ - AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, - PromptLevel, ScrollHandle, Window, + AnyElement, App, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, EventEmitter, + FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task, WeakEntity, Window, + canvas, }; -use paths::global_ssh_config_file; -use paths::user_ssh_config_file; +use paths::{global_ssh_config_file, user_ssh_config_file}; use picker::Picker; -use project::Fs; -use project::Project; -use remote::remote_client::ConnectionIdentifier; -use remote::{RemoteClient, SshConnectionOptions}; -use settings::Settings; -use settings::SettingsStore; -use settings::update_settings_file; -use settings::watch_config_file; +use project::{Fs, Project}; +use remote::{ + RemoteClient, RemoteConnectionOptions, SshConnectionOptions, + remote_client::ConnectionIdentifier, +}; +use settings::{Settings, SettingsStore, update_settings_file, watch_config_file}; use smol::stream::StreamExt as _; -use ui::Navigable; -use ui::NavigableEntry; +use std::{ + any::Any, + borrow::Cow, + collections::BTreeSet, + path::PathBuf, + rc::Rc, + sync::{ + Arc, + atomic::{self, AtomicUsize}, + }, +}; use ui::{ - IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState, - Section, Tooltip, prelude::*, + IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Navigable, NavigableEntry, + Scrollbar, ScrollbarState, Section, Tooltip, prelude::*, }; use util::{ ResultExt, paths::{PathStyle, RemotePathBuf}, }; -use workspace::OpenOptions; -use workspace::Toast; -use workspace::notifications::NotificationId; use workspace::{ - ModalView, Workspace, notifications::DetachAndPromptErr, - open_ssh_project_with_existing_connection, + ModalView, OpenOptions, Toast, Workspace, + notifications::{DetachAndPromptErr, NotificationId}, + open_remote_project_with_existing_connection, }; -use crate::ssh_config::parse_ssh_config_hosts; -use crate::ssh_connections::RemoteSettingsContent; -use crate::ssh_connections::SshConnection; -use crate::ssh_connections::SshConnectionHeader; -use crate::ssh_connections::SshConnectionModal; -use crate::ssh_connections::SshProject; -use crate::ssh_connections::SshPrompt; -use crate::ssh_connections::SshSettings; -use crate::ssh_connections::connect_over_ssh; -use crate::ssh_connections::open_ssh_project; - -mod navigation_base {} pub struct RemoteServerProjects { mode: Mode, focus_handle: FocusHandle, @@ -79,7 +61,7 @@ pub struct RemoteServerProjects { struct CreateRemoteServer { address_editor: Entity, address_error: Option, - ssh_prompt: Option>, + ssh_prompt: Option>, _creating: Option>>, } @@ -222,8 +204,13 @@ impl ProjectPicker { }) .log_err()?; - open_ssh_project_with_existing_connection( - connection, project, paths, app_state, window, cx, + open_remote_project_with_existing_connection( + RemoteConnectionOptions::Ssh(connection), + project, + paths, + app_state, + window, + cx, ) .await .log_err(); @@ -472,7 +459,14 @@ impl RemoteServerProjects { return; } }; - let ssh_prompt = cx.new(|cx| SshPrompt::new(&connection_options, window, cx)); + let ssh_prompt = cx.new(|cx| { + RemoteConnectionPrompt::new( + connection_options.connection_string(), + connection_options.nickname.clone(), + window, + cx, + ) + }); let connection = connect_over_ssh( ConnectionIdentifier::setup(), @@ -552,15 +546,20 @@ impl RemoteServerProjects { }; let create_new_window = self.create_new_window; - let connection_options = ssh_connection.into(); + let connection_options: SshConnectionOptions = ssh_connection.into(); workspace.update(cx, |_, cx| { cx.defer_in(window, move |workspace, window, cx| { let app_state = workspace.app_state().clone(); workspace.toggle_modal(window, cx, |window, cx| { - SshConnectionModal::new(&connection_options, Vec::new(), window, cx) + RemoteConnectionModal::new( + &RemoteConnectionOptions::Ssh(connection_options.clone()), + Vec::new(), + window, + cx, + ) }); let prompt = workspace - .active_modal::(cx) + .active_modal::(cx) .unwrap() .read(cx) .prompt @@ -579,7 +578,7 @@ impl RemoteServerProjects { let session = connect.await; workspace.update(cx, |workspace, cx| { - if let Some(prompt) = workspace.active_modal::(cx) { + if let Some(prompt) = workspace.active_modal::(cx) { prompt.update(cx, |prompt, cx| prompt.finished(cx)) } })?; @@ -898,8 +897,8 @@ impl RemoteServerProjects { }; cx.spawn_in(window, async move |_, cx| { - let result = open_ssh_project( - server.into(), + let result = open_remote_project( + RemoteConnectionOptions::Ssh(server.into()), project.paths.into_iter().map(PathBuf::from).collect(), app_state, OpenOptions { diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs index c698353d9edfc0d48c7039f321a2c88890e8c098..74d45b1a696ff1a02a9f2b4d9afc3844f82196cd 100644 --- a/crates/remote/src/remote.rs +++ b/crates/remote/src/remote.rs @@ -6,6 +6,7 @@ mod transport; pub use remote_client::{ ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent, - RemotePlatform, + RemoteConnectionOptions, RemotePlatform, }; pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption}; +pub use transport::wsl::WslConnectionOptions; diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index 7e231e622cb2336a113799f7087fc0e30a5f79ff..501c6a8dd639630b1930cb32e804f8cca658a9ca 100644 --- a/crates/remote/src/remote_client.rs +++ b/crates/remote/src/remote_client.rs @@ -1,6 +1,11 @@ use crate::{ - SshConnectionOptions, protocol::MessageId, proxy::ProxyLaunchError, - transport::ssh::SshRemoteConnection, + SshConnectionOptions, + protocol::MessageId, + proxy::ProxyLaunchError, + transport::{ + ssh::SshRemoteConnection, + wsl::{WslConnectionOptions, WslRemoteConnection}, + }, }; use anyhow::{Context as _, Result, anyhow}; use async_trait::async_trait; @@ -237,7 +242,7 @@ impl From<&State> for ConnectionState { pub struct RemoteClient { client: Arc, unique_identifier: String, - connection_options: SshConnectionOptions, + connection_options: RemoteConnectionOptions, path_style: PathStyle, state: Option, } @@ -290,6 +295,22 @@ impl RemoteClient { cancellation: oneshot::Receiver<()>, delegate: Arc, cx: &mut App, + ) -> Task>>> { + Self::new( + unique_identifier, + RemoteConnectionOptions::Ssh(connection_options), + cancellation, + delegate, + cx, + ) + } + + pub fn new( + unique_identifier: ConnectionIdentifier, + connection_options: RemoteConnectionOptions, + cancellation: oneshot::Receiver<()>, + delegate: Arc, + cx: &mut App, ) -> Task>>> { let unique_identifier = unique_identifier.to_string(cx); cx.spawn(async move |cx| { @@ -424,7 +445,7 @@ impl RemoteClient { } let state = self.state.take().unwrap(); - let (attempts, ssh_connection, delegate) = match state { + let (attempts, remote_connection, delegate) = match state { State::Connected { ssh_connection, delegate, @@ -482,15 +503,15 @@ impl RemoteClient { }; } - if let Err(error) = ssh_connection + if let Err(error) = remote_connection .kill() .await .context("Failed to kill ssh process") { - failed!(error, attempts, ssh_connection, delegate); + failed!(error, attempts, remote_connection, delegate); }; - let connection_options = ssh_connection.connection_options(); + let connection_options = remote_connection.connection_options(); let (outgoing_tx, outgoing_rx) = mpsc::unbounded::(); let (incoming_tx, incoming_rx) = mpsc::unbounded::(); @@ -519,7 +540,7 @@ impl RemoteClient { { Ok((ssh_connection, io_task)) => (ssh_connection, io_task), Err(error) => { - failed!(error, attempts, ssh_connection, delegate); + failed!(error, attempts, remote_connection, delegate); } }; @@ -751,6 +772,13 @@ impl RemoteClient { Some(self.state.as_ref()?.remote_connection()?.shell()) } + pub fn shares_network_interface(&self) -> bool { + self.state + .as_ref() + .and_then(|state| state.remote_connection()) + .map_or(false, |connection| connection.shares_network_interface()) + } + pub fn build_command( &self, program: Option, @@ -789,11 +817,7 @@ impl RemoteClient { self.client.clone().into() } - pub fn host(&self) -> String { - self.connection_options.host.clone() - } - - pub fn connection_options(&self) -> SshConnectionOptions { + pub fn connection_options(&self) -> RemoteConnectionOptions { self.connection_options.clone() } @@ -836,14 +860,14 @@ impl RemoteClient { pub fn fake_server( client_cx: &mut gpui::TestAppContext, server_cx: &mut gpui::TestAppContext, - ) -> (SshConnectionOptions, AnyProtoClient) { + ) -> (RemoteConnectionOptions, AnyProtoClient) { let port = client_cx .update(|cx| cx.default_global::().connections.len() as u16 + 1); - let opts = SshConnectionOptions { + let opts = RemoteConnectionOptions::Ssh(SshConnectionOptions { host: "".to_string(), port: Some(port), ..Default::default() - }; + }); let (outgoing_tx, _) = mpsc::unbounded::(); let (_, incoming_rx) = mpsc::unbounded::(); let server_client = @@ -874,13 +898,13 @@ impl RemoteClient { #[cfg(any(test, feature = "test-support"))] pub async fn fake_client( - opts: SshConnectionOptions, + opts: RemoteConnectionOptions, client_cx: &mut gpui::TestAppContext, ) -> Entity { let (_tx, rx) = oneshot::channel(); client_cx .update(|cx| { - Self::ssh( + Self::new( ConnectionIdentifier::setup(), opts, rx, @@ -901,7 +925,7 @@ enum ConnectionPoolEntry { #[derive(Default)] struct ConnectionPool { - connections: HashMap, + connections: HashMap, } impl Global for ConnectionPool {} @@ -909,7 +933,7 @@ impl Global for ConnectionPool {} impl ConnectionPool { pub fn connect( &mut self, - opts: SshConnectionOptions, + opts: RemoteConnectionOptions, delegate: &Arc, cx: &mut App, ) -> Shared, Arc>>> { @@ -939,9 +963,18 @@ impl ConnectionPool { let opts = opts.clone(); let delegate = delegate.clone(); async move |cx| { - let connection = SshRemoteConnection::new(opts.clone(), delegate, cx) - .await - .map(|connection| Arc::new(connection) as Arc); + let connection = match opts.clone() { + RemoteConnectionOptions::Ssh(opts) => { + SshRemoteConnection::new(opts, delegate, cx) + .await + .map(|connection| Arc::new(connection) as Arc) + } + RemoteConnectionOptions::Wsl(opts) => { + WslRemoteConnection::new(opts, delegate, cx) + .await + .map(|connection| Arc::new(connection) as Arc) + } + }; cx.update_global(|pool: &mut Self, _| { debug_assert!(matches!( @@ -972,6 +1005,33 @@ impl ConnectionPool { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum RemoteConnectionOptions { + Ssh(SshConnectionOptions), + Wsl(WslConnectionOptions), +} + +impl RemoteConnectionOptions { + pub fn display_name(&self) -> String { + match self { + RemoteConnectionOptions::Ssh(opts) => opts.host.clone(), + RemoteConnectionOptions::Wsl(opts) => opts.distro_name.clone(), + } + } +} + +impl From for RemoteConnectionOptions { + fn from(opts: SshConnectionOptions) -> Self { + RemoteConnectionOptions::Ssh(opts) + } +} + +impl From for RemoteConnectionOptions { + fn from(opts: WslConnectionOptions) -> Self { + RemoteConnectionOptions::Wsl(opts) + } +} + #[async_trait(?Send)] pub(crate) trait RemoteConnection: Send + Sync { fn start_proxy( @@ -992,6 +1052,9 @@ pub(crate) trait RemoteConnection: Send + Sync { ) -> Task>; async fn kill(&self) -> Result<()>; fn has_been_killed(&self) -> bool; + fn shares_network_interface(&self) -> bool { + false + } fn build_command( &self, program: Option, @@ -1000,7 +1063,7 @@ pub(crate) trait RemoteConnection: Send + Sync { working_dir: Option, port_forward: Option<(u16, String, u16)>, ) -> Result; - fn connection_options(&self) -> SshConnectionOptions; + fn connection_options(&self) -> RemoteConnectionOptions; fn path_style(&self) -> PathStyle; fn shell(&self) -> String; @@ -1307,7 +1370,7 @@ impl ProtoClient for ChannelClient { #[cfg(any(test, feature = "test-support"))] mod fake { use super::{ChannelClient, RemoteClientDelegate, RemoteConnection, RemotePlatform}; - use crate::{SshConnectionOptions, remote_client::CommandTemplate}; + use crate::remote_client::{CommandTemplate, RemoteConnectionOptions}; use anyhow::Result; use async_trait::async_trait; use collections::HashMap; @@ -1326,7 +1389,7 @@ mod fake { use util::paths::{PathStyle, RemotePathBuf}; pub(super) struct FakeRemoteConnection { - pub(super) connection_options: SshConnectionOptions, + pub(super) connection_options: RemoteConnectionOptions, pub(super) server_channel: Arc, pub(super) server_cx: SendableCx, } @@ -1386,7 +1449,7 @@ mod fake { unreachable!() } - fn connection_options(&self) -> SshConnectionOptions { + fn connection_options(&self) -> RemoteConnectionOptions { self.connection_options.clone() } diff --git a/crates/remote/src/transport.rs b/crates/remote/src/transport.rs index aa086fd3f56196e71224ef346c9810e8638c5c47..36525b7fcc1d91f106cffb6592a1ffd8e5e96fa9 100644 --- a/crates/remote/src/transport.rs +++ b/crates/remote/src/transport.rs @@ -1 +1,336 @@ +use crate::{ + json_log::LogRecord, + protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message}, +}; +use anyhow::{Context as _, Result}; +use futures::{ + AsyncReadExt as _, FutureExt as _, StreamExt as _, + channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}, +}; +use gpui::{AppContext as _, AsyncApp, Task}; +use rpc::proto::Envelope; +use smol::process::Child; + pub mod ssh; +pub mod wsl; + +fn handle_rpc_messages_over_child_process_stdio( + mut ssh_proxy_process: Child, + incoming_tx: UnboundedSender, + mut outgoing_rx: UnboundedReceiver, + mut connection_activity_tx: Sender<()>, + cx: &AsyncApp, +) -> Task> { + let mut child_stderr = ssh_proxy_process.stderr.take().unwrap(); + let mut child_stdout = ssh_proxy_process.stdout.take().unwrap(); + let mut child_stdin = ssh_proxy_process.stdin.take().unwrap(); + + let mut stdin_buffer = Vec::new(); + let mut stdout_buffer = Vec::new(); + let mut stderr_buffer = Vec::new(); + let mut stderr_offset = 0; + + let stdin_task = cx.background_spawn(async move { + while let Some(outgoing) = outgoing_rx.next().await { + write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?; + } + anyhow::Ok(()) + }); + + let stdout_task = cx.background_spawn({ + let mut connection_activity_tx = connection_activity_tx.clone(); + async move { + loop { + stdout_buffer.resize(MESSAGE_LEN_SIZE, 0); + let len = child_stdout.read(&mut stdout_buffer).await?; + + if len == 0 { + return anyhow::Ok(()); + } + + if len < MESSAGE_LEN_SIZE { + child_stdout.read_exact(&mut stdout_buffer[len..]).await?; + } + + let message_len = message_len_from_buffer(&stdout_buffer); + let envelope = + read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len) + .await?; + connection_activity_tx.try_send(()).ok(); + incoming_tx.unbounded_send(envelope).ok(); + } + } + }); + + let stderr_task: Task> = cx.background_spawn(async move { + loop { + stderr_buffer.resize(stderr_offset + 1024, 0); + + let len = child_stderr + .read(&mut stderr_buffer[stderr_offset..]) + .await?; + if len == 0 { + return anyhow::Ok(()); + } + + stderr_offset += len; + let mut start_ix = 0; + while let Some(ix) = stderr_buffer[start_ix..stderr_offset] + .iter() + .position(|b| b == &b'\n') + { + let line_ix = start_ix + ix; + let content = &stderr_buffer[start_ix..line_ix]; + start_ix = line_ix + 1; + if let Ok(record) = serde_json::from_slice::(content) { + record.log(log::logger()) + } else { + eprintln!("(remote) {}", String::from_utf8_lossy(content)); + } + } + stderr_buffer.drain(0..start_ix); + stderr_offset -= start_ix; + + connection_activity_tx.try_send(()).ok(); + } + }); + + cx.background_spawn(async move { + let result = futures::select! { + result = stdin_task.fuse() => { + result.context("stdin") + } + result = stdout_task.fuse() => { + result.context("stdout") + } + result = stderr_task.fuse() => { + result.context("stderr") + } + }; + + let status = ssh_proxy_process.status().await?.code().unwrap_or(1); + match result { + Ok(_) => Ok(status), + Err(error) => Err(error), + } + }) +} + +#[cfg(debug_assertions)] +async fn build_remote_server_from_source( + platform: &crate::RemotePlatform, + delegate: &dyn crate::RemoteClientDelegate, + cx: &mut AsyncApp, +) -> Result> { + use std::path::Path; + + let Some(build_remote_server) = std::env::var("ZED_BUILD_REMOTE_SERVER").ok() else { + return Ok(None); + }; + + use smol::process::{Command, Stdio}; + use std::env::VarError; + + async fn run_cmd(command: &mut Command) -> Result<()> { + let output = command + .kill_on_drop(true) + .stderr(Stdio::inherit()) + .output() + .await?; + anyhow::ensure!( + output.status.success(), + "Failed to run command: {command:?}" + ); + Ok(()) + } + + let use_musl = !build_remote_server.contains("nomusl"); + let triple = format!( + "{}-{}", + platform.arch, + match platform.os { + "linux" => + if use_musl { + "unknown-linux-musl" + } else { + "unknown-linux-gnu" + }, + "macos" => "apple-darwin", + _ => anyhow::bail!("can't cross compile for: {:?}", platform), + } + ); + let mut rust_flags = match std::env::var("RUSTFLAGS") { + Ok(val) => val, + Err(VarError::NotPresent) => String::new(), + Err(e) => { + log::error!("Failed to get env var `RUSTFLAGS` value: {e}"); + String::new() + } + }; + if platform.os == "linux" && use_musl { + rust_flags.push_str(" -C target-feature=+crt-static"); + } + if build_remote_server.contains("mold") { + rust_flags.push_str(" -C link-arg=-fuse-ld=mold"); + } + + if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS { + delegate.set_status(Some("Building remote server binary from source"), cx); + log::info!("building remote server binary from source"); + run_cmd( + Command::new("cargo") + .args([ + "build", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; + } else if build_remote_server.contains("cross") { + #[cfg(target_os = "windows")] + use util::paths::SanitizedPath; + + delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx); + log::info!("installing cross"); + run_cmd(Command::new("cargo").args([ + "install", + "cross", + "--git", + "https://github.com/cross-rs/cross", + ])) + .await?; + + delegate.set_status( + Some(&format!( + "Building remote server binary from source for {} with Docker", + &triple + )), + cx, + ); + log::info!("building remote server binary from source for {}", &triple); + + // On Windows, the binding needs to be set to the canonical path + #[cfg(target_os = "windows")] + let src = SanitizedPath::new(&smol::fs::canonicalize("./target").await?).to_glob_string(); + #[cfg(not(target_os = "windows"))] + let src = "./target"; + + run_cmd( + Command::new("cross") + .args([ + "build", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env( + "CROSS_CONTAINER_OPTS", + format!("--mount type=bind,src={src},dst=/app/target"), + ) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; + } else { + let which = cx + .background_spawn(async move { which::which("zig") }) + .await; + + if which.is_err() { + #[cfg(not(target_os = "windows"))] + { + anyhow::bail!( + "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" + ) + } + #[cfg(target_os = "windows")] + { + anyhow::bail!( + "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" + ) + } + } + + delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); + log::info!("adding rustup target"); + run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?; + + delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx); + log::info!("installing cargo-zigbuild"); + run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?; + + delegate.set_status( + Some(&format!( + "Building remote binary from source for {triple} with Zig" + )), + cx, + ); + log::info!("building remote binary from source for {triple} with Zig"); + run_cmd( + Command::new("cargo") + .args([ + "zigbuild", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; + }; + let bin_path = Path::new("target") + .join("remote_server") + .join(&triple) + .join("debug") + .join("remote_server"); + + let path = if !build_remote_server.contains("nocompress") { + delegate.set_status(Some("Compressing binary"), cx); + + #[cfg(not(target_os = "windows"))] + { + run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?; + } + + #[cfg(target_os = "windows")] + { + // On Windows, we use 7z to compress the binary + let seven_zip = which::which("7z.exe").context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?; + let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple); + if smol::fs::metadata(&gz_path).await.is_ok() { + smol::fs::remove_file(&gz_path).await?; + } + run_cmd(Command::new(seven_zip).args([ + "a", + "-tgzip", + &gz_path, + &bin_path.to_string_lossy(), + ])) + .await?; + } + + let mut archive_path = bin_path; + archive_path.set_extension("gz"); + std::env::current_dir()?.join(archive_path) + } else { + bin_path + }; + + Ok(Some(path)) +} diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 34f1ebf71c278538b57e486856f9b3315a41cf91..0995e0dd611ae667cc2e68638773c8b80bf2f22b 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -1,14 +1,12 @@ use crate::{ RemoteClientDelegate, RemotePlatform, - json_log::LogRecord, - protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message}, - remote_client::{CommandTemplate, RemoteConnection}, + remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions}, }; use anyhow::{Context as _, Result, anyhow}; use async_trait::async_trait; use collections::HashMap; use futures::{ - AsyncReadExt as _, FutureExt as _, StreamExt as _, + AsyncReadExt as _, FutureExt as _, channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}, select_biased, }; @@ -99,8 +97,8 @@ impl RemoteConnection for SshRemoteConnection { self.master_process.lock().is_none() } - fn connection_options(&self) -> SshConnectionOptions { - self.socket.connection_options.clone() + fn connection_options(&self) -> RemoteConnectionOptions { + RemoteConnectionOptions::Ssh(self.socket.connection_options.clone()) } fn shell(&self) -> String { @@ -267,7 +265,7 @@ impl RemoteConnection for SshRemoteConnection { } }; - Self::multiplex( + super::handle_rpc_messages_over_child_process_stdio( ssh_proxy_process, incoming_tx, outgoing_rx, @@ -415,109 +413,6 @@ impl SshRemoteConnection { Ok(this) } - fn multiplex( - mut ssh_proxy_process: Child, - incoming_tx: UnboundedSender, - mut outgoing_rx: UnboundedReceiver, - mut connection_activity_tx: Sender<()>, - cx: &AsyncApp, - ) -> Task> { - let mut child_stderr = ssh_proxy_process.stderr.take().unwrap(); - let mut child_stdout = ssh_proxy_process.stdout.take().unwrap(); - let mut child_stdin = ssh_proxy_process.stdin.take().unwrap(); - - let mut stdin_buffer = Vec::new(); - let mut stdout_buffer = Vec::new(); - let mut stderr_buffer = Vec::new(); - let mut stderr_offset = 0; - - let stdin_task = cx.background_spawn(async move { - while let Some(outgoing) = outgoing_rx.next().await { - write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?; - } - anyhow::Ok(()) - }); - - let stdout_task = cx.background_spawn({ - let mut connection_activity_tx = connection_activity_tx.clone(); - async move { - loop { - stdout_buffer.resize(MESSAGE_LEN_SIZE, 0); - let len = child_stdout.read(&mut stdout_buffer).await?; - - if len == 0 { - return anyhow::Ok(()); - } - - if len < MESSAGE_LEN_SIZE { - child_stdout.read_exact(&mut stdout_buffer[len..]).await?; - } - - let message_len = message_len_from_buffer(&stdout_buffer); - let envelope = - read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len) - .await?; - connection_activity_tx.try_send(()).ok(); - incoming_tx.unbounded_send(envelope).ok(); - } - } - }); - - let stderr_task: Task> = cx.background_spawn(async move { - loop { - stderr_buffer.resize(stderr_offset + 1024, 0); - - let len = child_stderr - .read(&mut stderr_buffer[stderr_offset..]) - .await?; - if len == 0 { - return anyhow::Ok(()); - } - - stderr_offset += len; - let mut start_ix = 0; - while let Some(ix) = stderr_buffer[start_ix..stderr_offset] - .iter() - .position(|b| b == &b'\n') - { - let line_ix = start_ix + ix; - let content = &stderr_buffer[start_ix..line_ix]; - start_ix = line_ix + 1; - if let Ok(record) = serde_json::from_slice::(content) { - record.log(log::logger()) - } else { - eprintln!("(remote) {}", String::from_utf8_lossy(content)); - } - } - stderr_buffer.drain(0..start_ix); - stderr_offset -= start_ix; - - connection_activity_tx.try_send(()).ok(); - } - }); - - cx.background_spawn(async move { - let result = futures::select! { - result = stdin_task.fuse() => { - result.context("stdin") - } - result = stdout_task.fuse() => { - result.context("stdout") - } - result = stderr_task.fuse() => { - result.context("stderr") - } - }; - - let status = ssh_proxy_process.status().await?.code().unwrap_or(1); - match result { - Ok(_) => Ok(status), - Err(error) => Err(error), - } - }) - } - - #[allow(unused)] async fn ensure_server_binary( &self, delegate: &Arc, @@ -544,19 +439,20 @@ impl SshRemoteConnection { self.ssh_path_style, ); - let build_remote_server = std::env::var("ZED_BUILD_REMOTE_SERVER").ok(); #[cfg(debug_assertions)] - if let Some(build_remote_server) = build_remote_server { - let src_path = self.build_local(build_remote_server, delegate, cx).await?; + if let Some(remote_server_path) = + super::build_remote_server_from_source(&self.ssh_platform, delegate.as_ref(), cx) + .await? + { let tmp_path = RemotePathBuf::new( paths::remote_server_dir_relative().join(format!( "download-{}-{}", std::process::id(), - src_path.file_name().unwrap().to_string_lossy() + remote_server_path.file_name().unwrap().to_string_lossy() )), self.ssh_path_style, ); - self.upload_local_server_binary(&src_path, &tmp_path, delegate, cx) + self.upload_local_server_binary(&remote_server_path, &tmp_path, delegate, cx) .await?; self.extract_server_binary(&dst_path, &tmp_path, delegate, cx) .await?; @@ -794,221 +690,6 @@ impl SshRemoteConnection { ); Ok(()) } - - #[cfg(debug_assertions)] - async fn build_local( - &self, - build_remote_server: String, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result { - use smol::process::{Command, Stdio}; - use std::env::VarError; - - async fn run_cmd(command: &mut Command) -> Result<()> { - let output = command - .kill_on_drop(true) - .stderr(Stdio::inherit()) - .output() - .await?; - anyhow::ensure!( - output.status.success(), - "Failed to run command: {command:?}" - ); - Ok(()) - } - - let use_musl = !build_remote_server.contains("nomusl"); - let triple = format!( - "{}-{}", - self.ssh_platform.arch, - match self.ssh_platform.os { - "linux" => - if use_musl { - "unknown-linux-musl" - } else { - "unknown-linux-gnu" - }, - "macos" => "apple-darwin", - _ => anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform), - } - ); - let mut rust_flags = match std::env::var("RUSTFLAGS") { - Ok(val) => val, - Err(VarError::NotPresent) => String::new(), - Err(e) => { - log::error!("Failed to get env var `RUSTFLAGS` value: {e}"); - String::new() - } - }; - if self.ssh_platform.os == "linux" && use_musl { - rust_flags.push_str(" -C target-feature=+crt-static"); - } - if build_remote_server.contains("mold") { - rust_flags.push_str(" -C link-arg=-fuse-ld=mold"); - } - - if self.ssh_platform.arch == std::env::consts::ARCH - && self.ssh_platform.os == std::env::consts::OS - { - delegate.set_status(Some("Building remote server binary from source"), cx); - log::info!("building remote server binary from source"); - run_cmd( - Command::new("cargo") - .args([ - "build", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - } else if build_remote_server.contains("cross") { - #[cfg(target_os = "windows")] - use util::paths::SanitizedPath; - - delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx); - log::info!("installing cross"); - run_cmd(Command::new("cargo").args([ - "install", - "cross", - "--git", - "https://github.com/cross-rs/cross", - ])) - .await?; - - delegate.set_status( - Some(&format!( - "Building remote server binary from source for {} with Docker", - &triple - )), - cx, - ); - log::info!("building remote server binary from source for {}", &triple); - - // On Windows, the binding needs to be set to the canonical path - #[cfg(target_os = "windows")] - let src = - SanitizedPath::new(&smol::fs::canonicalize("./target").await?).to_glob_string(); - #[cfg(not(target_os = "windows"))] - let src = "./target"; - run_cmd( - Command::new("cross") - .args([ - "build", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env( - "CROSS_CONTAINER_OPTS", - format!("--mount type=bind,src={src},dst=/app/target"), - ) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - } else { - let which = cx - .background_spawn(async move { which::which("zig") }) - .await; - - if which.is_err() { - #[cfg(not(target_os = "windows"))] - { - anyhow::bail!( - "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" - ) - } - #[cfg(target_os = "windows")] - { - anyhow::bail!( - "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" - ) - } - } - - delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); - log::info!("adding rustup target"); - run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?; - - delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx); - log::info!("installing cargo-zigbuild"); - run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?; - - delegate.set_status( - Some(&format!( - "Building remote binary from source for {triple} with Zig" - )), - cx, - ); - log::info!("building remote binary from source for {triple} with Zig"); - run_cmd( - Command::new("cargo") - .args([ - "zigbuild", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - }; - let bin_path = Path::new("target") - .join("remote_server") - .join(&triple) - .join("debug") - .join("remote_server"); - - let path = if !build_remote_server.contains("nocompress") { - delegate.set_status(Some("Compressing binary"), cx); - - #[cfg(not(target_os = "windows"))] - { - run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?; - } - #[cfg(target_os = "windows")] - { - // On Windows, we use 7z to compress the binary - let seven_zip = which::which("7z.exe").context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?; - let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple); - if smol::fs::metadata(&gz_path).await.is_ok() { - smol::fs::remove_file(&gz_path).await?; - } - run_cmd(Command::new(seven_zip).args([ - "a", - "-tgzip", - &gz_path, - &bin_path.to_string_lossy(), - ])) - .await?; - } - - let mut archive_path = bin_path; - archive_path.set_extension("gz"); - std::env::current_dir()?.join(archive_path) - } else { - bin_path - }; - - Ok(path) - } } impl SshSocket { diff --git a/crates/remote/src/transport/wsl.rs b/crates/remote/src/transport/wsl.rs new file mode 100644 index 0000000000000000000000000000000000000000..ea8f2443d9a674492674bdc2fb19f2a021b03dcc --- /dev/null +++ b/crates/remote/src/transport/wsl.rs @@ -0,0 +1,494 @@ +use crate::{ + RemoteClientDelegate, RemotePlatform, + remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions}, +}; +use anyhow::{Result, anyhow, bail}; +use async_trait::async_trait; +use collections::HashMap; +use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}; +use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task}; +use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; +use rpc::proto::Envelope; +use smol::{fs, process}; +use std::{ + fmt::Write as _, + path::{Path, PathBuf}, + process::Stdio, + sync::Arc, + time::Instant, +}; +use util::paths::{PathStyle, RemotePathBuf}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct WslConnectionOptions { + pub distro_name: String, + pub user: Option, +} + +pub(crate) struct WslRemoteConnection { + remote_binary_path: Option, + platform: RemotePlatform, + shell: String, + connection_options: WslConnectionOptions, +} + +impl WslRemoteConnection { + pub(crate) async fn new( + connection_options: WslConnectionOptions, + delegate: Arc, + cx: &mut AsyncApp, + ) -> Result { + log::info!( + "Connecting to WSL distro {} with user {:?}", + connection_options.distro_name, + connection_options.user + ); + let (release_channel, version, commit) = cx.update(|cx| { + ( + ReleaseChannel::global(cx), + AppVersion::global(cx), + AppCommitSha::try_global(cx), + ) + })?; + + let mut this = Self { + connection_options, + remote_binary_path: None, + platform: RemotePlatform { os: "", arch: "" }, + shell: String::new(), + }; + delegate.set_status(Some("Detecting WSL environment"), cx); + this.platform = this.detect_platform().await?; + this.shell = this.detect_shell().await?; + this.remote_binary_path = Some( + this.ensure_server_binary(&delegate, release_channel, version, commit, cx) + .await?, + ); + + Ok(this) + } + + async fn detect_platform(&self) -> Result { + let arch_str = self.run_wsl_command("uname", &["-m"]).await?; + let arch_str = arch_str.trim().to_string(); + let arch = match arch_str.as_str() { + "x86_64" => "x86_64", + "aarch64" | "arm64" => "aarch64", + _ => "x86_64", + }; + Ok(RemotePlatform { os: "linux", arch }) + } + + async fn detect_shell(&self) -> Result { + Ok(self + .run_wsl_command("sh", &["-c", "echo $SHELL"]) + .await + .ok() + .and_then(|shell_path| shell_path.trim().split('/').next_back().map(str::to_string)) + .unwrap_or_else(|| "bash".to_string())) + } + + async fn windows_path_to_wsl_path(&self, source: &Path) -> Result { + windows_path_to_wsl_path_impl(&self.connection_options, source).await + } + + fn wsl_command(&self, program: &str, args: &[&str]) -> process::Command { + wsl_command_impl(&self.connection_options, program, args) + } + + async fn run_wsl_command(&self, program: &str, args: &[&str]) -> Result { + run_wsl_command_impl(&self.connection_options, program, args).await + } + + async fn ensure_server_binary( + &self, + delegate: &Arc, + release_channel: ReleaseChannel, + version: SemanticVersion, + commit: Option, + cx: &mut AsyncApp, + ) -> Result { + let version_str = match release_channel { + ReleaseChannel::Nightly => { + let commit = commit.map(|s| s.full()).unwrap_or_default(); + format!("{}-{}", version, commit) + } + ReleaseChannel::Dev => "build".to_string(), + _ => version.to_string(), + }; + + let binary_name = format!( + "zed-remote-server-{}-{}", + release_channel.dev_name(), + version_str + ); + + let dst_path = RemotePathBuf::new( + paths::remote_wsl_server_dir_relative().join(binary_name), + PathStyle::Posix, + ); + + if let Some(parent) = dst_path.parent() { + self.run_wsl_command("mkdir", &["-p", &parent.to_string()]) + .await + .map_err(|e| anyhow!("Failed to create directory: {}", e))?; + } + + #[cfg(debug_assertions)] + if let Some(remote_server_path) = + super::build_remote_server_from_source(&self.platform, delegate.as_ref(), cx).await? + { + let tmp_path = RemotePathBuf::new( + paths::remote_wsl_server_dir_relative().join(format!( + "download-{}-{}", + std::process::id(), + remote_server_path.file_name().unwrap().to_string_lossy() + )), + PathStyle::Posix, + ); + self.upload_file(&remote_server_path, &tmp_path, delegate, cx) + .await?; + self.extract_and_install(&tmp_path, &dst_path, delegate, cx) + .await?; + return Ok(dst_path); + } + + if self + .run_wsl_command(&dst_path.to_string(), &["version"]) + .await + .is_ok() + { + return Ok(dst_path); + } + + delegate.set_status(Some("Installing remote server"), cx); + + let wanted_version = match release_channel { + ReleaseChannel::Nightly => None, + ReleaseChannel::Dev => { + return Err(anyhow!("Dev builds require manual installation")); + } + _ => Some(cx.update(|cx| AppVersion::global(cx))?), + }; + + let src_path = delegate + .download_server_binary_locally(self.platform, release_channel, wanted_version, cx) + .await?; + + let tmp_path = RemotePathBuf::new( + PathBuf::from(format!("{}.{}.tmp", dst_path, std::process::id())), + PathStyle::Posix, + ); + + self.upload_file(&src_path, &tmp_path, delegate, cx).await?; + self.extract_and_install(&tmp_path, &dst_path, delegate, cx) + .await?; + + Ok(dst_path) + } + + async fn upload_file( + &self, + src_path: &Path, + dst_path: &RemotePathBuf, + delegate: &Arc, + cx: &mut AsyncApp, + ) -> Result<()> { + delegate.set_status(Some("Uploading remote server to WSL"), cx); + + if let Some(parent) = dst_path.parent() { + self.run_wsl_command("mkdir", &["-p", &parent.to_string()]) + .await + .map_err(|e| anyhow!("Failed to create directory when uploading file: {}", e))?; + } + + let t0 = Instant::now(); + let src_stat = fs::metadata(&src_path).await?; + let size = src_stat.len(); + log::info!( + "uploading remote server to WSL {:?} ({}kb)", + dst_path, + size / 1024 + ); + + let src_path_in_wsl = self.windows_path_to_wsl_path(src_path).await?; + self.run_wsl_command("cp", &["-f", &src_path_in_wsl, &dst_path.to_string()]) + .await + .map_err(|e| { + anyhow!( + "Failed to copy file {}({}) to WSL {:?}: {}", + src_path.display(), + src_path_in_wsl, + dst_path, + e + ) + })?; + + log::info!("uploaded remote server in {:?}", t0.elapsed()); + Ok(()) + } + + async fn extract_and_install( + &self, + tmp_path: &RemotePathBuf, + dst_path: &RemotePathBuf, + delegate: &Arc, + cx: &mut AsyncApp, + ) -> Result<()> { + delegate.set_status(Some("Extracting remote server"), cx); + + let tmp_path_str = tmp_path.to_string(); + let dst_path_str = dst_path.to_string(); + + // Build extraction script with proper error handling + let script = if tmp_path_str.ends_with(".gz") { + let uncompressed = tmp_path_str.trim_end_matches(".gz"); + format!( + "set -e; gunzip -f '{}' && chmod 755 '{}' && mv -f '{}' '{}'", + tmp_path_str, uncompressed, uncompressed, dst_path_str + ) + } else { + format!( + "set -e; chmod 755 '{}' && mv -f '{}' '{}'", + tmp_path_str, tmp_path_str, dst_path_str + ) + }; + + self.run_wsl_command("sh", &["-c", &script]) + .await + .map_err(|e| anyhow!("Failed to extract server binary: {}", e))?; + Ok(()) + } +} + +#[async_trait(?Send)] +impl RemoteConnection for WslRemoteConnection { + fn start_proxy( + &self, + unique_identifier: String, + reconnect: bool, + incoming_tx: UnboundedSender, + outgoing_rx: UnboundedReceiver, + connection_activity_tx: Sender<()>, + delegate: Arc, + cx: &mut AsyncApp, + ) -> Task> { + delegate.set_status(Some("Starting proxy"), cx); + + let Some(remote_binary_path) = &self.remote_binary_path else { + return Task::ready(Err(anyhow!("Remote binary path not set"))); + }; + + let mut proxy_command = format!( + "exec {} proxy --identifier {}", + remote_binary_path, unique_identifier + ); + + if reconnect { + proxy_command.push_str(" --reconnect"); + } + + for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { + if let Some(value) = std::env::var(env_var).ok() { + proxy_command = format!("{}='{}' {}", env_var, value, proxy_command); + } + } + let proxy_process = match self + .wsl_command("sh", &["-lc", &proxy_command]) + .kill_on_drop(true) + .spawn() + { + Ok(process) => process, + Err(error) => { + return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error))); + } + }; + + super::handle_rpc_messages_over_child_process_stdio( + proxy_process, + incoming_tx, + outgoing_rx, + connection_activity_tx, + cx, + ) + } + + fn upload_directory( + &self, + src_path: PathBuf, + dest_path: RemotePathBuf, + cx: &App, + ) -> Task> { + cx.background_spawn({ + let options = self.connection_options.clone(); + async move { + let wsl_src = windows_path_to_wsl_path_impl(&options, &src_path).await?; + + run_wsl_command_impl(&options, "cp", &["-r", &wsl_src, &dest_path.to_string()]) + .await + .map_err(|e| { + anyhow!( + "failed to upload directory {} -> {}: {}", + src_path.display(), + dest_path.to_string(), + e + ) + })?; + + Ok(()) + } + }) + } + + async fn kill(&self) -> Result<()> { + Ok(()) + } + + fn has_been_killed(&self) -> bool { + false + } + + fn shares_network_interface(&self) -> bool { + true + } + + fn build_command( + &self, + program: Option, + args: &[String], + env: &HashMap, + working_dir: Option, + port_forward: Option<(u16, String, u16)>, + ) -> Result { + if port_forward.is_some() { + bail!("WSL shares the network interface with the host system"); + } + + let working_dir = working_dir + .map(|working_dir| RemotePathBuf::new(working_dir.into(), PathStyle::Posix).to_string()) + .unwrap_or("~".to_string()); + + let mut script = String::new(); + + for (k, v) in env.iter() { + write!(&mut script, "{}='{}' ", k, v).unwrap(); + } + + if let Some(program) = program { + let command = shlex::try_quote(&program)?; + script.push_str(&command); + for arg in args { + let arg = shlex::try_quote(&arg)?; + script.push_str(" "); + script.push_str(&arg); + } + } else { + write!(&mut script, "exec {} -l", self.shell).unwrap(); + } + + let wsl_args = if let Some(user) = &self.connection_options.user { + vec![ + "--distribution".to_string(), + self.connection_options.distro_name.clone(), + "--user".to_string(), + user.clone(), + "--cd".to_string(), + working_dir, + "--".to_string(), + self.shell.clone(), + "-c".to_string(), + shlex::try_quote(&script)?.to_string(), + ] + } else { + vec![ + "--distribution".to_string(), + self.connection_options.distro_name.clone(), + "--cd".to_string(), + working_dir, + "--".to_string(), + self.shell.clone(), + "-c".to_string(), + shlex::try_quote(&script)?.to_string(), + ] + }; + + Ok(CommandTemplate { + program: "wsl.exe".to_string(), + args: wsl_args, + env: HashMap::default(), + }) + } + + fn connection_options(&self) -> RemoteConnectionOptions { + RemoteConnectionOptions::Wsl(self.connection_options.clone()) + } + + fn path_style(&self) -> PathStyle { + PathStyle::Posix + } + + fn shell(&self) -> String { + self.shell.clone() + } +} + +/// `wslpath` is a executable available in WSL, it's a linux binary. +/// So it doesn't support Windows style paths. +async fn sanitize_path(path: &Path) -> Result { + let path = smol::fs::canonicalize(path).await?; + let path_str = path.to_string_lossy(); + + let sanitized = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str); + Ok(sanitized.replace('\\', "/")) +} + +async fn windows_path_to_wsl_path_impl( + options: &WslConnectionOptions, + source: &Path, +) -> Result { + let source = sanitize_path(source).await?; + run_wsl_command_impl(options, "wslpath", &["-u", &source]).await +} + +fn wsl_command_impl( + options: &WslConnectionOptions, + program: &str, + args: &[&str], +) -> process::Command { + let mut command = util::command::new_smol_command("wsl.exe"); + + if let Some(user) = &options.user { + command.arg("--user").arg(user); + } + + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .arg("--distribution") + .arg(&options.distro_name) + .arg("--cd") + .arg("~") + .arg(program) + .args(args); + + command +} + +async fn run_wsl_command_impl( + options: &WslConnectionOptions, + program: &str, + args: &[&str], +) -> Result { + let output = wsl_command_impl(options, program, args).output().await?; + + if !output.status.success() { + return Err(anyhow!( + "Command '{}' failed: {}", + program, + String::from_utf8_lossy(&output.stderr).trim() + )); + } + + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 075b9fcd86276244d154be1aebe904fbfb4a7b6c..2b13ef58c3a8707b81d6870590efe5337ffef048 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -32,6 +32,7 @@ use gpui::{ use keymap_editor; use onboarding_banner::OnboardingBanner; use project::Project; +use remote::RemoteConnectionOptions; use settings::Settings as _; use std::sync::Arc; use theme::ActiveTheme; @@ -304,12 +305,14 @@ impl TitleBar { fn render_remote_project_connection(&self, cx: &mut Context) -> Option { let options = self.project.read(cx).remote_connection_options(cx)?; - let host: SharedString = options.connection_string().into(); + let host: SharedString = options.display_name().into(); - let nickname = options - .nickname - .map(|nick| nick.into()) - .unwrap_or_else(|| host.clone()); + let nickname = if let RemoteConnectionOptions::Ssh(options) = options { + options.nickname.map(|nick| nick.into()) + } else { + None + }; + let nickname = nickname.unwrap_or_else(|| host.clone()); let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? { remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")), diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 3ef9ff65eb0fe5aedfd5e72aa18f1481a011fce7..160823f547f3ab0019d4a631550aec70f1ca101e 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -20,6 +20,7 @@ use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; use language::{LanguageName, Toolchain}; use project::WorktreeId; +use remote::{RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions}; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::{SqlType, Statement}, @@ -33,11 +34,12 @@ use uuid::Uuid; use crate::{ WorkspaceId, path_list::{PathList, SerializedPathList}, + persistence::model::RemoteConnectionKind, }; use model::{ - GroupId, ItemId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, - SerializedSshConnection, SerializedWorkspace, SshConnectionId, + GroupId, ItemId, PaneId, RemoteConnectionId, SerializedItem, SerializedPane, + SerializedPaneGroup, SerializedWorkspace, }; use self::model::{DockStructure, SerializedWorkspaceLocation}; @@ -627,6 +629,88 @@ impl Domain for WorkspaceDb { END WHERE paths IS NOT NULL ), + sql!( + CREATE TABLE remote_connections( + id INTEGER PRIMARY KEY, + kind TEXT NOT NULL, + host TEXT, + port INTEGER, + user TEXT, + distro TEXT + ); + + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + paths TEXT, + paths_order TEXT, + remote_connection_id INTEGER REFERENCES remote_connections(id), + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB, + left_dock_visible INTEGER, + left_dock_active_panel TEXT, + right_dock_visible INTEGER, + right_dock_active_panel TEXT, + bottom_dock_visible INTEGER, + bottom_dock_active_panel TEXT, + left_dock_zoom INTEGER, + right_dock_zoom INTEGER, + bottom_dock_zoom INTEGER, + fullscreen INTEGER, + centered_layout INTEGER, + session_id TEXT, + window_id INTEGER + ) STRICT; + + INSERT INTO remote_connections + SELECT + id, + "ssh" as kind, + host, + port, + user, + NULL as distro + FROM ssh_connections; + + INSERT + INTO workspaces_2 + SELECT + workspace_id, + paths, + paths_order, + ssh_connection_id as remote_connection_id, + timestamp, + window_state, + window_x, + window_y, + window_width, + window_height, + display, + left_dock_visible, + left_dock_active_panel, + right_dock_visible, + right_dock_active_panel, + bottom_dock_visible, + bottom_dock_active_panel, + left_dock_zoom, + right_dock_zoom, + bottom_dock_zoom, + fullscreen, + centered_layout, + session_id, + window_id + FROM + workspaces; + + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + + CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(remote_connection_id, paths); + ), ]; // Allow recovering from bad migration that was initially shipped to nightly @@ -650,10 +734,10 @@ impl WorkspaceDb { self.workspace_for_roots_internal(worktree_roots, None) } - pub(crate) fn ssh_workspace_for_roots>( + pub(crate) fn remote_workspace_for_roots>( &self, worktree_roots: &[P], - ssh_project_id: SshConnectionId, + ssh_project_id: RemoteConnectionId, ) -> Option { self.workspace_for_roots_internal(worktree_roots, Some(ssh_project_id)) } @@ -661,7 +745,7 @@ impl WorkspaceDb { pub(crate) fn workspace_for_roots_internal>( &self, worktree_roots: &[P], - ssh_connection_id: Option, + remote_connection_id: Option, ) -> Option { // paths are sorted before db interactions to ensure that the order of the paths // doesn't affect the workspace selection for existing workspaces @@ -713,13 +797,13 @@ impl WorkspaceDb { FROM workspaces WHERE paths IS ? AND - ssh_connection_id IS ? + remote_connection_id IS ? LIMIT 1 }) .map(|mut prepared_statement| { (prepared_statement)(( root_paths.serialize().paths, - ssh_connection_id.map(|id| id.0 as i32), + remote_connection_id.map(|id| id.0 as i32), )) .unwrap() }) @@ -803,14 +887,12 @@ impl WorkspaceDb { log::debug!("Saving workspace at location: {:?}", workspace.location); self.write(move |conn| { conn.with_savepoint("update_worktrees", || { - let ssh_connection_id = match &workspace.location { + let remote_connection_id = match workspace.location.clone() { SerializedWorkspaceLocation::Local => None, - SerializedWorkspaceLocation::Ssh(connection) => { - Some(Self::get_or_create_ssh_connection_query( + SerializedWorkspaceLocation::Remote(connection_options) => { + Some(Self::get_or_create_remote_connection_internal( conn, - connection.host.clone(), - connection.port, - connection.user.clone(), + connection_options )?.0) } }; @@ -860,11 +942,11 @@ impl WorkspaceDb { WHERE workspace_id != ?1 AND paths IS ?2 AND - ssh_connection_id IS ?3 + remote_connection_id IS ?3 ))?(( workspace.id, paths.paths.clone(), - ssh_connection_id, + remote_connection_id, )) .context("clearing out old locations")?; @@ -874,7 +956,7 @@ impl WorkspaceDb { workspace_id, paths, paths_order, - ssh_connection_id, + remote_connection_id, left_dock_visible, left_dock_active_panel, left_dock_zoom, @@ -893,7 +975,7 @@ impl WorkspaceDb { UPDATE SET paths = ?2, paths_order = ?3, - ssh_connection_id = ?4, + remote_connection_id = ?4, left_dock_visible = ?5, left_dock_active_panel = ?6, left_dock_zoom = ?7, @@ -912,7 +994,7 @@ impl WorkspaceDb { workspace.id, paths.paths.clone(), paths.order.clone(), - ssh_connection_id, + remote_connection_id, workspace.docks, workspace.session_id, workspace.window_id, @@ -931,39 +1013,78 @@ impl WorkspaceDb { .await; } - pub(crate) async fn get_or_create_ssh_connection( + pub(crate) async fn get_or_create_remote_connection( &self, - host: String, - port: Option, - user: Option, - ) -> Result { - self.write(move |conn| Self::get_or_create_ssh_connection_query(conn, host, port, user)) + options: RemoteConnectionOptions, + ) -> Result { + self.write(move |conn| Self::get_or_create_remote_connection_internal(conn, options)) .await } - fn get_or_create_ssh_connection_query( + fn get_or_create_remote_connection_internal( + this: &Connection, + options: RemoteConnectionOptions, + ) -> Result { + let kind; + let user; + let mut host = None; + let mut port = None; + let mut distro = None; + match options { + RemoteConnectionOptions::Ssh(options) => { + kind = RemoteConnectionKind::Ssh; + host = Some(options.host); + port = options.port; + user = options.username; + } + RemoteConnectionOptions::Wsl(options) => { + kind = RemoteConnectionKind::Wsl; + distro = Some(options.distro_name); + user = options.user; + } + } + Self::get_or_create_remote_connection_query(this, kind, host, port, user, distro) + } + + fn get_or_create_remote_connection_query( this: &Connection, - host: String, + kind: RemoteConnectionKind, + host: Option, port: Option, user: Option, - ) -> Result { + distro: Option, + ) -> Result { if let Some(id) = this.select_row_bound(sql!( - SELECT id FROM ssh_connections WHERE host IS ? AND port IS ? AND user IS ? LIMIT 1 - ))?((host.clone(), port, user.clone()))? - { - Ok(SshConnectionId(id)) + SELECT id + FROM remote_connections + WHERE + kind IS ? AND + host IS ? AND + port IS ? AND + user IS ? AND + distro IS ? + LIMIT 1 + ))?(( + kind.serialize(), + host.clone(), + port, + user.clone(), + distro.clone(), + ))? { + Ok(RemoteConnectionId(id)) } else { - log::debug!("Inserting SSH project at host {host}"); let id = this.select_row_bound(sql!( - INSERT INTO ssh_connections ( + INSERT INTO remote_connections ( + kind, host, port, - user - ) VALUES (?1, ?2, ?3) + user, + distro + ) VALUES (?1, ?2, ?3, ?4, ?5) RETURNING id - ))?((host, port, user))? - .context("failed to insert ssh project")?; - Ok(SshConnectionId(id)) + ))?((kind.serialize(), host, port, user, distro))? + .context("failed to insert remote project")?; + Ok(RemoteConnectionId(id)) } } @@ -973,15 +1094,17 @@ impl WorkspaceDb { } } - fn recent_workspaces(&self) -> Result)>> { + fn recent_workspaces( + &self, + ) -> Result)>> { Ok(self .recent_workspaces_query()? .into_iter() - .map(|(id, paths, order, ssh_connection_id)| { + .map(|(id, paths, order, remote_connection_id)| { ( id, PathList::deserialize(&SerializedPathList { paths, order }), - ssh_connection_id, + remote_connection_id.map(RemoteConnectionId), ) }) .collect()) @@ -1001,7 +1124,7 @@ impl WorkspaceDb { fn session_workspaces( &self, session_id: String, - ) -> Result, Option)>> { + ) -> Result, Option)>> { Ok(self .session_workspaces_query(session_id)? .into_iter() @@ -1009,7 +1132,7 @@ impl WorkspaceDb { ( PathList::deserialize(&SerializedPathList { paths, order }), window_id, - ssh_connection_id.map(SshConnectionId), + ssh_connection_id.map(RemoteConnectionId), ) }) .collect()) @@ -1017,7 +1140,7 @@ impl WorkspaceDb { query! { fn session_workspaces_query(session_id: String) -> Result, Option)>> { - SELECT paths, paths_order, window_id, ssh_connection_id + SELECT paths, paths_order, window_id, remote_connection_id FROM workspaces WHERE session_id = ?1 ORDER BY timestamp DESC @@ -1039,40 +1162,55 @@ impl WorkspaceDb { } } - fn ssh_connections(&self) -> Result> { - Ok(self - .ssh_connections_query()? - .into_iter() - .map(|(id, host, port, user)| { - ( - SshConnectionId(id), - SerializedSshConnection { host, port, user }, - ) - }) - .collect()) - } - - query! { - pub fn ssh_connections_query() -> Result, Option)>> { - SELECT id, host, port, user - FROM ssh_connections - } - } - - pub(crate) fn ssh_connection(&self, id: SshConnectionId) -> Result { - let row = self.ssh_connection_query(id.0)?; - Ok(SerializedSshConnection { - host: row.0, - port: row.1, - user: row.2, + fn remote_connections(&self) -> Result> { + Ok(self.select(sql!( + SELECT + id, kind, host, port, user, distro + FROM + remote_connections + ))?()? + .into_iter() + .filter_map(|(id, kind, host, port, user, distro)| { + Some(( + RemoteConnectionId(id), + Self::remote_connection_from_row(kind, host, port, user, distro)?, + )) }) + .collect()) } - query! { - fn ssh_connection_query(id: u64) -> Result<(String, Option, Option)> { - SELECT host, port, user - FROM ssh_connections + pub(crate) fn remote_connection( + &self, + id: RemoteConnectionId, + ) -> Result { + let (kind, host, port, user, distro) = self.select_row_bound(sql!( + SELECT kind, host, port, user, distro + FROM remote_connections WHERE id = ? + ))?(id.0)? + .context("no such remote connection")?; + Self::remote_connection_from_row(kind, host, port, user, distro) + .context("invalid remote_connection row") + } + + fn remote_connection_from_row( + kind: String, + host: Option, + port: Option, + user: Option, + distro: Option, + ) -> Option { + match RemoteConnectionKind::deserialize(&kind)? { + RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions { + distro_name: distro?, + user: user, + })), + RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: host?, + port, + username: user, + ..Default::default() + })), } } @@ -1108,14 +1246,14 @@ impl WorkspaceDb { ) -> Result> { let mut result = Vec::new(); let mut delete_tasks = Vec::new(); - let ssh_connections = self.ssh_connections()?; + let remote_connections = self.remote_connections()?; - for (id, paths, ssh_connection_id) in self.recent_workspaces()? { - if let Some(ssh_connection_id) = ssh_connection_id.map(SshConnectionId) { - if let Some(ssh_connection) = ssh_connections.get(&ssh_connection_id) { + for (id, paths, remote_connection_id) in self.recent_workspaces()? { + if let Some(remote_connection_id) = remote_connection_id { + if let Some(connection_options) = remote_connections.get(&remote_connection_id) { result.push(( id, - SerializedWorkspaceLocation::Ssh(ssh_connection.clone()), + SerializedWorkspaceLocation::Remote(connection_options.clone()), paths, )); } else { @@ -1157,12 +1295,14 @@ impl WorkspaceDb { ) -> Result> { let mut workspaces = Vec::new(); - for (paths, window_id, ssh_connection_id) in + for (paths, window_id, remote_connection_id) in self.session_workspaces(last_session_id.to_owned())? { - if let Some(ssh_connection_id) = ssh_connection_id { + if let Some(remote_connection_id) = remote_connection_id { workspaces.push(( - SerializedWorkspaceLocation::Ssh(self.ssh_connection(ssh_connection_id)?), + SerializedWorkspaceLocation::Remote( + self.remote_connection(remote_connection_id)?, + ), paths, window_id.map(WindowId::from), )); @@ -1545,6 +1685,7 @@ mod tests { }; use gpui; use pretty_assertions::assert_eq; + use remote::SshConnectionOptions; use std::{thread, time::Duration}; #[gpui::test] @@ -2196,14 +2337,20 @@ mod tests { }; let connection_id = db - .get_or_create_ssh_connection("my-host".to_string(), Some(1234), None) + .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: "my-host".to_string(), + port: Some(1234), + ..Default::default() + })) .await .unwrap(); let workspace_5 = SerializedWorkspace { id: WorkspaceId(5), paths: PathList::default(), - location: SerializedWorkspaceLocation::Ssh(db.ssh_connection(connection_id).unwrap()), + location: SerializedWorkspaceLocation::Remote( + db.remote_connection(connection_id).unwrap(), + ), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2362,13 +2509,12 @@ mod tests { } #[gpui::test] - async fn test_last_session_workspace_locations_ssh_projects() { - let db = WorkspaceDb::open_test_db( - "test_serializing_workspaces_last_session_workspaces_ssh_projects", - ) - .await; + async fn test_last_session_workspace_locations_remote() { + let db = + WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote") + .await; - let ssh_connections = [ + let remote_connections = [ ("host-1", "my-user-1"), ("host-2", "my-user-2"), ("host-3", "my-user-3"), @@ -2376,30 +2522,31 @@ mod tests { ] .into_iter() .map(|(host, user)| async { - db.get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string())) + let options = RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: host.to_string(), + username: Some(user.to_string()), + ..Default::default() + }); + db.get_or_create_remote_connection(options.clone()) .await .unwrap(); - SerializedSshConnection { - host: host.into(), - port: None, - user: Some(user.into()), - } + options }) .collect::>(); - let ssh_connections = futures::future::join_all(ssh_connections).await; + let remote_connections = futures::future::join_all(remote_connections).await; let workspaces = [ - (1, ssh_connections[0].clone(), 9), - (2, ssh_connections[1].clone(), 5), - (3, ssh_connections[2].clone(), 8), - (4, ssh_connections[3].clone(), 2), + (1, remote_connections[0].clone(), 9), + (2, remote_connections[1].clone(), 5), + (3, remote_connections[2].clone(), 8), + (4, remote_connections[3].clone(), 2), ] .into_iter() - .map(|(id, ssh_connection, window_id)| SerializedWorkspace { + .map(|(id, remote_connection, window_id)| SerializedWorkspace { id: WorkspaceId(id), paths: PathList::default(), - location: SerializedWorkspaceLocation::Ssh(ssh_connection), + location: SerializedWorkspaceLocation::Remote(remote_connection), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2429,28 +2576,28 @@ mod tests { assert_eq!( have[0], ( - SerializedWorkspaceLocation::Ssh(ssh_connections[3].clone()), + SerializedWorkspaceLocation::Remote(remote_connections[3].clone()), PathList::default() ) ); assert_eq!( have[1], ( - SerializedWorkspaceLocation::Ssh(ssh_connections[2].clone()), + SerializedWorkspaceLocation::Remote(remote_connections[2].clone()), PathList::default() ) ); assert_eq!( have[2], ( - SerializedWorkspaceLocation::Ssh(ssh_connections[1].clone()), + SerializedWorkspaceLocation::Remote(remote_connections[1].clone()), PathList::default() ) ); assert_eq!( have[3], ( - SerializedWorkspaceLocation::Ssh(ssh_connections[0].clone()), + SerializedWorkspaceLocation::Remote(remote_connections[0].clone()), PathList::default() ) ); @@ -2465,13 +2612,23 @@ mod tests { let user = Some("user".to_string()); let connection_id = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: host.clone(), + port, + username: user.clone(), + ..Default::default() + })) .await .unwrap(); // Test that calling the function again with the same parameters returns the same project let same_connection = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: host.clone(), + port, + username: user.clone(), + ..Default::default() + })) .await .unwrap(); @@ -2483,7 +2640,12 @@ mod tests { let user2 = Some("otheruser".to_string()); let different_connection = db - .get_or_create_ssh_connection(host2.clone(), port2, user2.clone()) + .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: host2.clone(), + port: port2, + username: user2.clone(), + ..Default::default() + })) .await .unwrap(); @@ -2497,12 +2659,22 @@ mod tests { let (host, port, user) = ("example.com".to_string(), None, None); let connection_id = db - .get_or_create_ssh_connection(host.clone(), port, None) + .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: host.clone(), + port, + username: None, + ..Default::default() + })) .await .unwrap(); let same_connection_id = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { + host: host.clone(), + port, + username: user.clone(), + ..Default::default() + })) .await .unwrap(); @@ -2510,8 +2682,8 @@ mod tests { } #[gpui::test] - async fn test_get_ssh_connections() { - let db = WorkspaceDb::open_test_db("test_get_ssh_connections").await; + async fn test_get_remote_connections() { + let db = WorkspaceDb::open_test_db("test_get_remote_connections").await; let connections = [ ("example.com".to_string(), None, None), @@ -2526,39 +2698,49 @@ mod tests { let mut ids = Vec::new(); for (host, port, user) in connections.iter() { ids.push( - db.get_or_create_ssh_connection(host.clone(), *port, user.clone()) - .await - .unwrap(), + db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh( + SshConnectionOptions { + host: host.clone(), + port: *port, + username: user.clone(), + ..Default::default() + }, + )) + .await + .unwrap(), ); } - let stored_projects = db.ssh_connections().unwrap(); + let stored_connections = db.remote_connections().unwrap(); assert_eq!( - stored_projects, + stored_connections, [ ( ids[0], - SerializedSshConnection { + RemoteConnectionOptions::Ssh(SshConnectionOptions { host: "example.com".into(), port: None, - user: None, - } + username: None, + ..Default::default() + }), ), ( ids[1], - SerializedSshConnection { + RemoteConnectionOptions::Ssh(SshConnectionOptions { host: "anotherexample.com".into(), port: Some(123), - user: Some("user2".into()), - } + username: Some("user2".into()), + ..Default::default() + }), ), ( ids[2], - SerializedSshConnection { + RemoteConnectionOptions::Ssh(SshConnectionOptions { host: "yetanother.com".into(), port: Some(345), - user: None, - } + username: None, + ..Default::default() + }), ), ] .into_iter() diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 04757d04950ac1ca200096d7b46d04abb18ce8f9..005a1ba2347f8ac3847199ad4564d8ca45420f4a 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -12,7 +12,7 @@ use db::sqlez::{ use gpui::{AsyncWindowContext, Entity, WeakEntity}; use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; -use serde::{Deserialize, Serialize}; +use remote::RemoteConnectionOptions; use std::{ collections::BTreeMap, path::{Path, PathBuf}, @@ -24,19 +24,18 @@ use uuid::Uuid; #[derive( Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, )] -pub(crate) struct SshConnectionId(pub u64); +pub(crate) struct RemoteConnectionId(pub u64); -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct SerializedSshConnection { - pub host: String, - pub port: Option, - pub user: Option, +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub(crate) enum RemoteConnectionKind { + Ssh, + Wsl, } #[derive(Debug, PartialEq, Clone)] pub enum SerializedWorkspaceLocation { Local, - Ssh(SerializedSshConnection), + Remote(RemoteConnectionOptions), } impl SerializedWorkspaceLocation { @@ -68,6 +67,23 @@ pub struct DockStructure { pub(crate) bottom: DockData, } +impl RemoteConnectionKind { + pub(crate) fn serialize(&self) -> &'static str { + match self { + RemoteConnectionKind::Ssh => "ssh", + RemoteConnectionKind::Wsl => "wsl", + } + } + + pub(crate) fn deserialize(text: &str) -> Option { + match text { + "ssh" => Some(Self::Ssh), + "wsl" => Some(Self::Wsl), + _ => None, + } + } +} + impl Column for DockStructure { fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { let (left, next_index) = DockData::column(statement, start_index)?; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 61442eb6348e6152a4ad8ba4d3f93c24d1887346..bd19f37c1e0fd8653f5d73dea365f1148fd2e91d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -67,14 +67,14 @@ pub use pane_group::*; use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace}; pub use persistence::{ DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items, - model::{ItemId, SerializedSshConnection, SerializedWorkspaceLocation}, + model::{ItemId, SerializedWorkspaceLocation}, }; use postage::stream::Stream; use project::{ DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId, debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, }; -use remote::{RemoteClientDelegate, SshConnectionOptions, remote_client::ConnectionIdentifier}; +use remote::{RemoteClientDelegate, RemoteConnectionOptions, remote_client::ConnectionIdentifier}; use schemars::JsonSchema; use serde::Deserialize; use session::AppSession; @@ -5262,14 +5262,7 @@ impl Workspace { fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { let paths = PathList::new(&self.root_paths(cx)); if let Some(connection) = self.project.read(cx).remote_connection_options(cx) { - WorkspaceLocation::Location( - SerializedWorkspaceLocation::Ssh(SerializedSshConnection { - host: connection.host, - port: connection.port, - user: connection.username, - }), - paths, - ) + WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths) } else if self.project.read(cx).is_local() { if !paths.is_empty() { WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths) @@ -7282,9 +7275,9 @@ pub fn create_and_open_local_file( }) } -pub fn open_ssh_project_with_new_connection( +pub fn open_remote_project_with_new_connection( window: WindowHandle, - connection_options: SshConnectionOptions, + connection_options: RemoteConnectionOptions, cancel_rx: oneshot::Receiver<()>, delegate: Arc, app_state: Arc, @@ -7293,11 +7286,11 @@ pub fn open_ssh_project_with_new_connection( ) -> Task> { cx.spawn(async move |cx| { let (workspace_id, serialized_workspace) = - serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; + serialize_remote_project(connection_options.clone(), paths.clone(), cx).await?; let session = match cx .update(|cx| { - remote::RemoteClient::ssh( + remote::RemoteClient::new( ConnectionIdentifier::Workspace(workspace_id.0), connection_options, cancel_rx, @@ -7323,7 +7316,7 @@ pub fn open_ssh_project_with_new_connection( ) })?; - open_ssh_project_inner( + open_remote_project_inner( project, paths, workspace_id, @@ -7336,8 +7329,8 @@ pub fn open_ssh_project_with_new_connection( }) } -pub fn open_ssh_project_with_existing_connection( - connection_options: SshConnectionOptions, +pub fn open_remote_project_with_existing_connection( + connection_options: RemoteConnectionOptions, project: Entity, paths: Vec, app_state: Arc, @@ -7346,9 +7339,9 @@ pub fn open_ssh_project_with_existing_connection( ) -> Task> { cx.spawn(async move |cx| { let (workspace_id, serialized_workspace) = - serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; + serialize_remote_project(connection_options.clone(), paths.clone(), cx).await?; - open_ssh_project_inner( + open_remote_project_inner( project, paths, workspace_id, @@ -7361,7 +7354,7 @@ pub fn open_ssh_project_with_existing_connection( }) } -async fn open_ssh_project_inner( +async fn open_remote_project_inner( project: Entity, paths: Vec, workspace_id: WorkspaceId, @@ -7448,22 +7441,18 @@ async fn open_ssh_project_inner( Ok(()) } -fn serialize_ssh_project( - connection_options: SshConnectionOptions, +fn serialize_remote_project( + connection_options: RemoteConnectionOptions, paths: Vec, cx: &AsyncApp, ) -> Task)>> { cx.background_spawn(async move { - let ssh_connection_id = persistence::DB - .get_or_create_ssh_connection( - connection_options.host.clone(), - connection_options.port, - connection_options.username.clone(), - ) + let remote_connection_id = persistence::DB + .get_or_create_remote_connection(connection_options) .await?; let serialized_workspace = - persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id); + persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id); let workspace_id = if let Some(workspace_id) = serialized_workspace.as_ref().map(|workspace| workspace.id) @@ -8013,22 +8002,20 @@ pub struct WorkspacePosition { pub centered_layout: bool, } -pub fn ssh_workspace_position_from_db( - host: String, - port: Option, - user: Option, +pub fn remote_workspace_position_from_db( + connection_options: RemoteConnectionOptions, paths_to_open: &[PathBuf], cx: &App, ) -> Task> { let paths = paths_to_open.to_vec(); cx.background_spawn(async move { - let ssh_connection_id = persistence::DB - .get_or_create_ssh_connection(host, port, user) + let remote_connection_id = persistence::DB + .get_or_create_remote_connection(connection_options) .await .context("fetching serialized ssh project")?; let serialized_workspace = - persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id); + persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id); let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() { (Some(WindowBounds::Windowed(bounds)), None) diff --git a/crates/zed/resources/windows/zed-wsl b/crates/zed/resources/windows/zed-wsl new file mode 100644 index 0000000000000000000000000000000000000000..d3cbb93af6f5979508229656deadeab0dbf21661 --- /dev/null +++ b/crates/zed/resources/windows/zed-wsl @@ -0,0 +1,25 @@ +#!/usr/bin/env sh + +if [ "$ZED_WSL_DEBUG_INFO" = true ]; then + set -x +fi + +ZED_PATH="$(dirname "$(realpath "$0")")" + +IN_WSL=false +if [ -n "$WSL_DISTRO_NAME" ]; then + # $WSL_DISTRO_NAME is available since WSL builds 18362, also for WSL2 + IN_WSL=true +fi + +if [ $IN_WSL = true ]; then + WSL_USER="$USER" + if [ -z "$WSL_USER" ]; then + WSL_USER="$USERNAME" + fi + "$ZED_PATH/zed.exe" --wsl "$WSL_USER@$WSL_DISTRO_NAME" "$@" + exit $? +else + echo "Only WSL is supported for now" >&2 + exit 1 +fi diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e4438792045617498e5c8cd3b52117b1d0b752ef..79cf2bfa66fb217680dea86720eb46402f116958 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -23,13 +23,14 @@ use http_client::{Url, read_proxy_from_env}; use language::LanguageRegistry; use onboarding::{FIRST_OPEN, show_onboarding_view}; use prompt_store::PromptBuilder; +use remote::RemoteConnectionOptions; use reqwest_client::ReqwestClient; use assets::Assets; use node_runtime::{NodeBinaryOptions, NodeRuntime}; use parking_lot::Mutex; use project::project_settings::ProjectSettings; -use recent_projects::{SshSettings, open_ssh_project}; +use recent_projects::{SshSettings, open_remote_project}; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use session::{AppSession, Session}; use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file}; @@ -360,6 +361,7 @@ pub fn main() { open_listener.open(RawOpenRequest { urls, diff_paths: Vec::new(), + ..Default::default() }) } }); @@ -696,7 +698,7 @@ pub fn main() { let urls: Vec<_> = args .paths_or_urls .iter() - .filter_map(|arg| parse_url_arg(arg, cx).log_err()) + .map(|arg| parse_url_arg(arg, cx)) .collect(); let diff_paths: Vec<[String; 2]> = args @@ -706,7 +708,11 @@ pub fn main() { .collect(); if !urls.is_empty() || !diff_paths.is_empty() { - open_listener.open(RawOpenRequest { urls, diff_paths }) + open_listener.open(RawOpenRequest { + urls, + diff_paths, + wsl: args.wsl, + }) } match open_rx @@ -792,10 +798,10 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut return; } - if let Some(connection_options) = request.ssh_connection { + if let Some(connection_options) = request.remote_connection { cx.spawn(async move |cx| { let paths: Vec = request.open_paths.into_iter().map(PathBuf::from).collect(); - open_ssh_project( + open_remote_project( connection_options, paths, app_state, @@ -978,31 +984,24 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp tasks.push(task); } } - SerializedWorkspaceLocation::Ssh(ssh) => { + SerializedWorkspaceLocation::Remote(mut connection_options) => { let app_state = app_state.clone(); - let ssh_host = ssh.host.clone(); - let task = cx.spawn(async move |cx| { - let connection_options = cx.update(|cx| { + if let RemoteConnectionOptions::Ssh(options) = &mut connection_options { + cx.update(|cx| { SshSettings::get_global(cx) - .connection_options_for(ssh.host, ssh.port, ssh.user) - }); - - match connection_options { - Ok(connection_options) => recent_projects::open_ssh_project( - connection_options, - paths.paths().into_iter().map(PathBuf::from).collect(), - app_state, - workspace::OpenOptions::default(), - cx, - ) - .await - .map_err(|e| anyhow::anyhow!(e)), - Err(e) => Err(anyhow::anyhow!( - "Failed to get SSH connection options for {}: {}", - ssh_host, - e - )), - } + .fill_connection_options_from_settings(options) + })?; + } + let task = cx.spawn(async move |cx| { + recent_projects::open_remote_project( + connection_options, + paths.paths().into_iter().map(PathBuf::from).collect(), + app_state, + workspace::OpenOptions::default(), + cx, + ) + .await + .map_err(|e| anyhow::anyhow!(e)) }); tasks.push(task); } @@ -1184,6 +1183,16 @@ struct Args { #[arg(long, value_name = "DIR")] user_data_dir: Option, + /// The username and WSL distribution to use when opening paths. ,If not specified, + /// Zed will attempt to open the paths directly. + /// + /// The username is optional, and if not specified, the default user for the distribution + /// will be used. + /// + /// Example: `me@Ubuntu` or `Ubuntu` for default distribution. + #[arg(long, value_name = "USER@DISTRO")] + wsl: Option, + /// Instructs zed to run as a dev server on this machine. (not implemented) #[arg(long)] dev_server_token: Option, @@ -1242,18 +1251,18 @@ impl ToString for IdType { } } -fn parse_url_arg(arg: &str, cx: &App) -> Result { +fn parse_url_arg(arg: &str, cx: &App) -> String { match std::fs::canonicalize(Path::new(&arg)) { - Ok(path) => Ok(format!("file://{}", path.display())), - Err(error) => { + Ok(path) => format!("file://{}", path.display()), + Err(_) => { if arg.starts_with("file://") || arg.starts_with("zed-cli://") || arg.starts_with("ssh://") || parse_zed_link(arg, cx).is_some() { - Ok(arg.into()) + arg.into() } else { - anyhow::bail!("error parsing path argument: {error}") + format!("file://{arg}") } } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5797070a39c8a60dc760ac3b82341842bc11d63e..d0e4687a132a85645cdbfe52e67ebb6afd894c0e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -48,7 +48,7 @@ use project::{DirectoryLister, ProjectItem}; use project_panel::ProjectPanel; use prompt_store::PromptBuilder; use quick_action_bar::QuickActionBar; -use recent_projects::open_ssh_project; +use recent_projects::open_remote_project; use release_channel::{AppCommitSha, ReleaseChannel}; use rope::Rope; use search::project_search::ProjectSearchBar; @@ -1557,7 +1557,7 @@ pub fn open_new_ssh_project_from_project( }; let connection_options = ssh_client.read(cx).connection_options(); cx.spawn_in(window, async move |_, cx| { - open_ssh_project( + open_remote_project( connection_options, paths, app_state, diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 2194fb7af5d48577a4316b99418df7dbce0a0375..f2d8cd46c301c0f688d36e17ed1b7d0dcd31ec00 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -17,8 +17,8 @@ use gpui::{App, AsyncApp, Global, WindowHandle}; use language::Point; use onboarding::FIRST_OPEN; use onboarding::show_onboarding_view; -use recent_projects::{SshSettings, open_ssh_project}; -use remote::SshConnectionOptions; +use recent_projects::{SshSettings, open_remote_project}; +use remote::{RemoteConnectionOptions, WslConnectionOptions}; use settings::Settings; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -37,7 +37,7 @@ pub struct OpenRequest { pub diff_paths: Vec<[String; 2]>, pub open_channel_notes: Vec<(u64, Option)>, pub join_channel: Option, - pub ssh_connection: Option, + pub remote_connection: Option, } #[derive(Debug)] @@ -51,6 +51,23 @@ pub enum OpenRequestKind { impl OpenRequest { pub fn parse(request: RawOpenRequest, cx: &App) -> Result { let mut this = Self::default(); + + this.diff_paths = request.diff_paths; + if let Some(wsl) = request.wsl { + let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') { + if user.is_empty() { + anyhow::bail!("user is empty in wsl argument"); + } + (Some(user.to_string()), distro.to_string()) + } else { + (None, wsl) + }; + this.remote_connection = Some(RemoteConnectionOptions::Wsl(WslConnectionOptions { + distro_name, + user, + })); + } + for url in request.urls { if let Some(server_name) = url.strip_prefix("zed-cli://") { this.kind = Some(OpenRequestKind::CliConnection(connect_to_cli(server_name)?)); @@ -80,8 +97,6 @@ impl OpenRequest { } } - this.diff_paths = request.diff_paths; - Ok(this) } @@ -108,13 +123,15 @@ impl OpenRequest { if let Some(password) = url.password() { connection_options.password = Some(password.to_string()); } - if let Some(ssh_connection) = &self.ssh_connection { + + let connection_options = RemoteConnectionOptions::Ssh(connection_options); + if let Some(ssh_connection) = &self.remote_connection { anyhow::ensure!( *ssh_connection == connection_options, - "cannot open multiple ssh connections" + "cannot open multiple different remote connections" ); } - self.ssh_connection = Some(connection_options); + self.remote_connection = Some(connection_options); self.parse_file_path(url.path()); Ok(()) } @@ -152,6 +169,7 @@ pub struct OpenListener(UnboundedSender); pub struct RawOpenRequest { pub urls: Vec, pub diff_paths: Vec<[String; 2]>, + pub wsl: Option, } impl Global for OpenListener {} @@ -303,13 +321,21 @@ pub async fn handle_cli_connection( paths, diff_paths, wait, + wsl, open_new_workspace, env, user_data_dir: _, } => { if !urls.is_empty() { cx.update(|cx| { - match OpenRequest::parse(RawOpenRequest { urls, diff_paths }, cx) { + match OpenRequest::parse( + RawOpenRequest { + urls, + diff_paths, + wsl, + }, + cx, + ) { Ok(open_request) => { handle_open_request(open_request, app_state.clone(), cx); responses.send(CliResponse::Exit { status: 0 }).log_err(); @@ -422,30 +448,26 @@ async fn open_workspaces( errored = true } } - SerializedWorkspaceLocation::Ssh(ssh) => { + SerializedWorkspaceLocation::Remote(mut connection) => { let app_state = app_state.clone(); - let connection_options = cx.update(|cx| { - SshSettings::get_global(cx) - .connection_options_for(ssh.host, ssh.port, ssh.user) - }); - if let Ok(connection_options) = connection_options { - cx.spawn(async move |cx| { - open_ssh_project( - connection_options, - workspace_paths.paths().to_vec(), - app_state, - OpenOptions::default(), - cx, - ) - .await - .log_err(); - }) - .detach(); - // We don't set `errored` here if `open_ssh_project` fails, because for ssh projects, the - // error is displayed in the window. - } else { - errored = false; + if let RemoteConnectionOptions::Ssh(options) = &mut connection { + cx.update(|cx| { + SshSettings::get_global(cx) + .fill_connection_options_from_settings(options) + })?; } + cx.spawn(async move |cx| { + open_remote_project( + connection, + workspace_paths.paths().to_vec(), + app_state, + OpenOptions::default(), + cx, + ) + .await + .log_err(); + }) + .detach(); } } } @@ -587,6 +609,7 @@ mod tests { }; use editor::Editor; use gpui::TestAppContext; + use remote::SshConnectionOptions; use serde_json::json; use std::sync::Arc; use util::path; @@ -609,8 +632,8 @@ mod tests { .unwrap() }); assert_eq!( - request.ssh_connection.unwrap(), - SshConnectionOptions { + request.remote_connection.unwrap(), + RemoteConnectionOptions::Ssh(SshConnectionOptions { host: "localhost".into(), username: Some("me".into()), port: None, @@ -619,7 +642,7 @@ mod tests { port_forwards: None, nickname: None, upload_binary_over_ssh: false, - } + }) ); assert_eq!(request.open_paths, vec!["/"]); } diff --git a/crates/zed/src/zed/windows_only_instance.rs b/crates/zed/src/zed/windows_only_instance.rs index bd62dea75aac5ad2c4b01c4b17d8d6219b9110db..1dd51b5ffbd7c11cce0346142834581c022f512d 100644 --- a/crates/zed/src/zed/windows_only_instance.rs +++ b/crates/zed/src/zed/windows_only_instance.rs @@ -153,6 +153,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> { urls, diff_paths, wait: false, + wsl: args.wsl.clone(), open_new_workspace: None, env: None, user_data_dir: args.user_data_dir.clone(), diff --git a/script/bundle-windows.ps1 b/script/bundle-windows.ps1 index 8ae02124918a2f7f47a1c6204f5199f6eb4e6056..84ad39fb706f9d3e0e4af73a68b468e0bea33ee1 100644 --- a/script/bundle-windows.ps1 +++ b/script/bundle-windows.ps1 @@ -150,6 +150,7 @@ function CollectFiles { Move-Item -Path "$innoDir\zed_explorer_command_injector.appx" -Destination "$innoDir\appx\zed_explorer_command_injector.appx" -Force Move-Item -Path "$innoDir\zed_explorer_command_injector.dll" -Destination "$innoDir\appx\zed_explorer_command_injector.dll" -Force Move-Item -Path "$innoDir\cli.exe" -Destination "$innoDir\bin\zed.exe" -Force + Move-Item -Path "$innoDir\zed-wsl" -Destination "$innoDir\bin\zed" -Force Move-Item -Path "$innoDir\auto_update_helper.exe" -Destination "$innoDir\tools\auto_update_helper.exe" -Force Move-Item -Path ".\AGS_SDK-6.3.0\ags_lib\lib\amd_ags_x64.dll" -Destination "$innoDir\amd_ags_x64.dll" -Force } From 7d0a303785fd73677c255fb15657e6af8dc1e3e8 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 29 Aug 2025 23:03:47 -0400 Subject: [PATCH 454/744] Add xAI to supported language model providers (#37206) After setting a `grok` model via the agent panel, the settings complains that it doesn't recognize the language model provider: SCR-20250829-tqqd Also, sorted the list, in the follow-up commit. Release Notes: - N/A --- crates/agent_settings/src/agent_settings.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 3808cc510f7941107f6e4ab90c9a5f8a2c3d920a..3e21e18a11ba68726f15d88bec93b95f01f89500 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -352,18 +352,19 @@ impl JsonSchema for LanguageModelProviderSetting { fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { json_schema!({ "enum": [ - "anthropic", "amazon-bedrock", + "anthropic", + "copilot_chat", + "deepseek", "google", "lmstudio", + "mistral", "ollama", "openai", - "zed.dev", - "copilot_chat", - "deepseek", "openrouter", - "mistral", - "vercel" + "vercel", + "x_ai", + "zed.dev" ] }) } From b473f4a1304bc1a4b2e911cc6063167ede3a281c Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Sat, 30 Aug 2025 15:13:23 +0200 Subject: [PATCH 455/744] Fix SQL error in recent projects query (#37220) Follow-up to https://github.com/zed-industries/zed/pull/37035 In the WSL PR, `ssh_connection_id` was renamed to `remote_connection_id`. However, that was not accounted for within the `recent_workspaces_query`. This caused a query fail: ``` 2025-08-30T14:45:44+02:00 ERROR [recent_projects] Prepare call failed for query: SELECT workspace_id, paths, paths_order, ssh_connection_id FROM workspaces WHERE paths IS NOT NULL OR ssh_connection_id IS NOT NULL ORDER BY timestamp DESC Caused by: Sqlite call failed with code 1 and message: Some("no such column: ssh_connection_id") ``` and resulted in no recent workspaces being shown within the recent projects picker. This change updates the column name to the new name and thus fixes the error. Release Notes: - N/A --- crates/workspace/src/persistence.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 160823f547f3ab0019d4a631550aec70f1ca101e..ef5a86a2762510fbea6f6a1a5172953a0ea20f7d 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -1112,11 +1112,11 @@ impl WorkspaceDb { query! { fn recent_workspaces_query() -> Result)>> { - SELECT workspace_id, paths, paths_order, ssh_connection_id + SELECT workspace_id, paths, paths_order, remote_connection_id FROM workspaces WHERE paths IS NOT NULL OR - ssh_connection_id IS NOT NULL + remote_connection_id IS NOT NULL ORDER BY timestamp DESC } } @@ -1128,11 +1128,11 @@ impl WorkspaceDb { Ok(self .session_workspaces_query(session_id)? .into_iter() - .map(|(paths, order, window_id, ssh_connection_id)| { + .map(|(paths, order, window_id, remote_connection_id)| { ( PathList::deserialize(&SerializedPathList { paths, order }), window_id, - ssh_connection_id.map(RemoteConnectionId), + remote_connection_id.map(RemoteConnectionId), ) }) .collect()) From 0a32aa8db1c4bbd4ae8977b0923ece0f32537074 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Sat, 30 Aug 2025 20:12:15 +0530 Subject: [PATCH 456/744] language_models: Fix GitHub Copilot thread summary by removing unnecessary noop tool logic (#37152) Closes #37025 This PR fixes GitHub Copilot thread summary failures by removing the unnecessary `noop` tool insertion logic. The code was originally added as a workaround in https://github.com/zed-industries/zed/pull/30007 for supposed GitHub Copilot API issues when tools were used previously in a conversation but no tools are provided in the current request. However, testing revealed that this scenario works fine without the workaround, and the `noop` tool insertion was actually causing "Invalid schema for function 'noop'" errors that prevented thread summarization from working. Removing this logic eliminates the errors and allows thread summarization to function correctly with GitHub Copilot models. The best way to see if removing that part of code works is just triggering thread summarisation. Error Log: ``` 2025-08-27T13:47:50-04:00 ERROR [workspace::notifications] "Failed to connect to API: 400 Bad Request {"error":{"message":"Invalid schema for function 'noop': In context=(), object schema missing properties.","code":"invalid_function_parameters"}}\n" ``` Release Notes: - Fixed GitHub Copilot thread summary failures by removing unnecessary noop tool insertion logic. --- .../src/provider/copilot_chat.rs | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index eb12c0056f871a4d9eb053c51455081572868aef..d48c12aa4b5de713c0130320f7c9e61a733dc33e 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -475,7 +475,6 @@ fn into_copilot_chat( } } - let mut tool_called = false; let mut messages: Vec = Vec::new(); for message in request_messages { match message.role { @@ -545,7 +544,6 @@ fn into_copilot_chat( let mut tool_calls = Vec::new(); for content in &message.content { if let MessageContent::ToolUse(tool_use) = content { - tool_called = true; tool_calls.push(ToolCall { id: tool_use.id.to_string(), content: copilot::copilot_chat::ToolCallContent::Function { @@ -590,7 +588,7 @@ fn into_copilot_chat( } } - let mut tools = request + let tools = request .tools .iter() .map(|tool| Tool::Function { @@ -602,22 +600,6 @@ fn into_copilot_chat( }) .collect::>(); - // The API will return a Bad Request (with no error message) when tools - // were used previously in the conversation but no tools are provided as - // part of this request. Inserting a dummy tool seems to circumvent this - // error. - if tool_called && tools.is_empty() { - tools.push(Tool::Function { - function: copilot::copilot_chat::Function { - name: "noop".to_string(), - description: "No operation".to_string(), - parameters: serde_json::json!({ - "type": "object" - }), - }, - }); - } - Ok(CopilotChatRequest { intent: true, n: 1, From af26b627bf8540edc6eea4146acc081183f6241e Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Sat, 30 Aug 2025 12:59:04 -0500 Subject: [PATCH 457/744] settings: Improve parse errors (#37234) Closes #ISSUE Adds a dependency on `serde_path_to_error` to the workspace allowing us to include the path to the setting that failed to parse on settings parse failure. Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 1 + Cargo.toml | 1 + crates/settings/Cargo.toml | 1 + crates/settings/src/settings_json.rs | 3 ++- crates/settings/src/settings_store.rs | 26 +++++++++++++++++++++++--- docs/src/visual-customization.md | 2 +- 6 files changed, 29 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84d633dd6f126f1ce86cd73b83f9d1aac23c591e..a80809461e5135541b1223e7482310effa6cb50b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14894,6 +14894,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_json_lenient", + "serde_path_to_error", "settings_ui_macros", "smallvec", "tree-sitter", diff --git a/Cargo.toml b/Cargo.toml index b64113311adb2662562cc4ae488054f54d569c3e..48017d9c6b4858fb7e5415b92bd993e534d1fabb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -592,6 +592,7 @@ serde_json_lenient = { version = "0.2", features = [ "preserve_order", "raw_value", ] } +serde_path_to_error = "0.1.17" serde_repr = "0.1" serde_urlencoded = "0.7" sha2 = "0.10" diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 8768b4073602461a5031b8d70d3a1e930ad2a41e..d9b8d7275f9abdd60df85443988595f025bf26c0 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -33,6 +33,7 @@ serde_derive.workspace = true serde_json.workspace = true settings_ui_macros.workspace = true serde_json_lenient.workspace = true +serde_path_to_error.workspace = true smallvec.workspace = true tree-sitter-json.workspace = true tree-sitter.workspace = true diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index b916df6e5c205c7fc2c0c920d0ac8343cb986a5c..480fe057eacb8d96255a3bf2d7b5f96208f87ced 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -563,7 +563,8 @@ pub fn to_pretty_json( } pub fn parse_json_with_comments(content: &str) -> Result { - Ok(serde_json_lenient::from_str(content)?) + let mut deserializer = serde_json_lenient::Deserializer::from_str(content); + Ok(serde_path_to_error::deserialize(&mut deserializer)?) } #[cfg(test)] diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 09ac6f9766e32e7a0d8765b09919cd0f8c09866c..023f8cbfba3d96b0a6cad2e1c6ebb930f0bcdf9e 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -11,7 +11,7 @@ use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGl use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; use schemars::JsonSchema; -use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde::{Serialize, de::DeserializeOwned}; use serde_json::{Value, json}; use smallvec::SmallVec; use std::{ @@ -1464,9 +1464,29 @@ impl AnySettingValue for SettingValue { return (T::KEY, Ok(DeserializedSetting(Box::new(value)))); } } - let value = T::FileContent::deserialize(json) + let value = serde_path_to_error::deserialize::<_, T::FileContent>(json) .map(|value| DeserializedSetting(Box::new(value))) - .map_err(anyhow::Error::from); + .map_err(|err| { + // construct a path using the key and reported error path if possible. + // Unfortunately, serde_path_to_error does not expose the necessary + // methods and data to simply add the key to the path + let mut path = String::new(); + if let Some(key) = key { + path.push_str(key); + } + let err_path = err.path().to_string(); + // when the path is empty, serde_path_to_error stringifies the path as ".", + // when the path is unknown, serde_path_to_error stringifies the path as an empty string + if !err_path.is_empty() && !err_path.starts_with(".") { + path.push('.'); + path.push_str(&err_path); + } + if path.is_empty() { + anyhow::Error::from(err.into_inner()) + } else { + anyhow::anyhow!("'{}': {}", err.into_inner(), path) + } + }); (key, value) } diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 1df76d17f026c9457b296230f93bec0e10c4aa19..47c72e80f5ea0ca6ce8576e29c51ff9e44041eb5 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -8,7 +8,7 @@ See [Configuring Zed](./configuring-zed.md) for additional information and other Use may install zed extensions providing [Themes](./themes.md) and [Icon Themes](./icon-themes.md) via {#action zed::Extensions} from the command palette or menu. -You can preview/choose amongsts your installed themes and icon themes with {#action theme_selector::Toggle} ({#kb theme_selector::Toggle}) and ({#action icon_theme_selector::Toggle}) which will modify the following settings: +You can preview/choose amongst your installed themes and icon themes with {#action theme_selector::Toggle} ({#kb theme_selector::Toggle}) and ({#action icon_theme_selector::Toggle}) which will modify the following settings: ```json { From de576bd1b81cef5a8bc41506806ea44c92d9d9a5 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sat, 30 Aug 2025 15:51:08 -0400 Subject: [PATCH 458/744] agent: Fix agent panel header not updating when opening a history entry (#37189) Closes #37171 Release Notes: - agent: Fixed a bug that caused the agent information in the panel header to be incorrect when opening a thread from history. --- crates/agent_ui/src/agent_panel.rs | 33 +++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 3eb171054a2c4d529bbc4b89063bf58f69ce5c45..fac880b783271ffd8c9524464a8f0a178f276895 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -284,6 +284,17 @@ impl AgentType { } } +impl From for AgentType { + fn from(value: ExternalAgent) -> Self { + match value { + ExternalAgent::Gemini => Self::Gemini, + ExternalAgent::ClaudeCode => Self::ClaudeCode, + ExternalAgent::Custom { name, command } => Self::Custom { name, command }, + ExternalAgent::NativeAgent => Self::NativeAgent, + } + } +} + impl ActiveView { pub fn which_font_size_used(&self) -> WhichFontSize { match self { @@ -1049,6 +1060,11 @@ impl AgentPanel { editor }); + if self.selected_agent != AgentType::TextThread { + self.selected_agent = AgentType::TextThread; + self.serialize(cx); + } + self.set_active_view( ActiveView::prompt_editor( context_editor.clone(), @@ -1140,6 +1156,12 @@ impl AgentPanel { } } + let selected_agent = ext_agent.into(); + if this.selected_agent != selected_agent { + this.selected_agent = selected_agent; + this.serialize(cx); + } + let thread_view = cx.new(|cx| { crate::acp::AcpThreadView::new( server, @@ -1235,6 +1257,12 @@ impl AgentPanel { cx, ) }); + + if self.selected_agent != AgentType::TextThread { + self.selected_agent = AgentType::TextThread; + self.serialize(cx); + } + self.set_active_view( ActiveView::prompt_editor( editor, @@ -1860,11 +1888,6 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) { - if self.selected_agent != agent { - self.selected_agent = agent.clone(); - self.serialize(cx); - } - match agent { AgentType::Zed => { window.dispatch_action( From ad746f25f268ef0ad3aeddc41bc05287c1d0d006 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Sat, 30 Aug 2025 14:13:39 -0600 Subject: [PATCH 459/744] zeta: Add zlib to license detection + ignore symbol differences (#37238) See discussion on #36564. Makes the license regexes a less fragile by not matching on symbols, while also excluding cases where a long file ends with a valid license. Also adds Zlib license, a commented out test to check all license-like files discovered in the homedir, and more testcases. Not too happy with the efficiency here, on my quite good computer it takes ~120ms to compile the regex and allocates ~8mb for it. This is just not a great use of regexes, I think something using eager substring matching would be much more efficient - hoping to followup with that. Release Notes: - Edit Prediction: Added Zlib license to open-source licenses eligible for data collection. --- Cargo.lock | 1 + crates/zeta/Cargo.toml | 1 + .../0bsd.txt | 0 .../apache-2.0-ex0.txt} | 0 .../zeta/license_examples/apache-2.0-ex1.txt | 55 +++++ .../apache-2.0-ex2.txt} | 150 ++++++------ .../zeta/license_examples/apache-2.0-ex3.txt | 13 + .../bsd-1-clause.txt | 0 .../bsd-2-clause-ex0.txt} | 0 .../bsd-3-clause-ex0.txt} | 0 .../license_examples/bsd-3-clause-ex1.txt | 27 +++ .../license_examples/bsd-3-clause-ex2.txt | 31 +++ .../license_examples/bsd-3-clause-ex3.txt | 30 +++ .../license_examples/bsd-3-clause-ex4.txt | 27 +++ .../isc.txt | 0 .../mit.txt => license_examples/mit-ex0.txt} | 0 crates/zeta/license_examples/mit-ex1.txt | 26 ++ crates/zeta/license_examples/mit-ex2.txt | 22 ++ crates/zeta/license_examples/mit-ex3.txt | 21 ++ .../upl-1.0.txt | 0 crates/zeta/license_examples/zlib-ex0.txt | 19 ++ crates/zeta/license_regexes/0bsd.regex | 10 + crates/zeta/license_regexes/apache-2.0.regex | 223 +++++++++++++++++ crates/zeta/license_regexes/bsd.regex | 23 ++ crates/zeta/license_regexes/isc.regex | 12 + crates/zeta/license_regexes/mit.regex | 17 ++ crates/zeta/license_regexes/upl-1.0.regex | 32 +++ crates/zeta/license_regexes/zlib.regex | 18 ++ crates/zeta/src/license_detection.rs | 229 +++++++++++++++--- crates/zeta/src/license_detection/0bsd.regex | 12 - .../src/license_detection/bsd-1-clause.regex | 17 -- .../src/license_detection/bsd-2-clause.regex | 22 -- .../src/license_detection/bsd-3-clause.regex | 26 -- crates/zeta/src/license_detection/isc.regex | 15 -- crates/zeta/src/license_detection/mit.regex | 21 -- .../zeta/src/license_detection/upl-1.0.regex | 35 --- 36 files changed, 867 insertions(+), 268 deletions(-) rename crates/zeta/{src/license_detection => license_examples}/0bsd.txt (100%) rename crates/zeta/{src/license_detection/apache-2.0.txt => license_examples/apache-2.0-ex0.txt} (100%) create mode 100644 crates/zeta/license_examples/apache-2.0-ex1.txt rename crates/zeta/{src/license_detection/apache-2.0.regex => license_examples/apache-2.0-ex2.txt} (59%) create mode 100644 crates/zeta/license_examples/apache-2.0-ex3.txt rename crates/zeta/{src/license_detection => license_examples}/bsd-1-clause.txt (100%) rename crates/zeta/{src/license_detection/bsd-2-clause.txt => license_examples/bsd-2-clause-ex0.txt} (100%) rename crates/zeta/{src/license_detection/bsd-3-clause.txt => license_examples/bsd-3-clause-ex0.txt} (100%) create mode 100644 crates/zeta/license_examples/bsd-3-clause-ex1.txt create mode 100644 crates/zeta/license_examples/bsd-3-clause-ex2.txt create mode 100644 crates/zeta/license_examples/bsd-3-clause-ex3.txt create mode 100644 crates/zeta/license_examples/bsd-3-clause-ex4.txt rename crates/zeta/{src/license_detection => license_examples}/isc.txt (100%) rename crates/zeta/{src/license_detection/mit.txt => license_examples/mit-ex0.txt} (100%) create mode 100644 crates/zeta/license_examples/mit-ex1.txt create mode 100644 crates/zeta/license_examples/mit-ex2.txt create mode 100644 crates/zeta/license_examples/mit-ex3.txt rename crates/zeta/{src/license_detection => license_examples}/upl-1.0.txt (100%) create mode 100644 crates/zeta/license_examples/zlib-ex0.txt create mode 100644 crates/zeta/license_regexes/0bsd.regex create mode 100644 crates/zeta/license_regexes/apache-2.0.regex create mode 100644 crates/zeta/license_regexes/bsd.regex create mode 100644 crates/zeta/license_regexes/isc.regex create mode 100644 crates/zeta/license_regexes/mit.regex create mode 100644 crates/zeta/license_regexes/upl-1.0.regex create mode 100644 crates/zeta/license_regexes/zlib.regex delete mode 100644 crates/zeta/src/license_detection/0bsd.regex delete mode 100644 crates/zeta/src/license_detection/bsd-1-clause.regex delete mode 100644 crates/zeta/src/license_detection/bsd-2-clause.regex delete mode 100644 crates/zeta/src/license_detection/bsd-3-clause.regex delete mode 100644 crates/zeta/src/license_detection/isc.regex delete mode 100644 crates/zeta/src/license_detection/mit.regex delete mode 100644 crates/zeta/src/license_detection/upl-1.0.regex diff --git a/Cargo.lock b/Cargo.lock index a80809461e5135541b1223e7482310effa6cb50b..4ca45445e2ed26819c612381b682aa9d1bf35d07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20817,6 +20817,7 @@ dependencies = [ "gpui", "http_client", "indoc", + "itertools 0.14.0", "language", "language_model", "log", diff --git a/crates/zeta/Cargo.toml b/crates/zeta/Cargo.toml index 05eedd6015d47e0c020266f27da8d63850d162e3..a57781ee8ee4b97805935efc7943df9eff1a8958 100644 --- a/crates/zeta/Cargo.toml +++ b/crates/zeta/Cargo.toml @@ -34,6 +34,7 @@ futures.workspace = true gpui.workspace = true http_client.workspace = true indoc.workspace = true +itertools.workspace = true language.workspace = true language_model.workspace = true log.workspace = true diff --git a/crates/zeta/src/license_detection/0bsd.txt b/crates/zeta/license_examples/0bsd.txt similarity index 100% rename from crates/zeta/src/license_detection/0bsd.txt rename to crates/zeta/license_examples/0bsd.txt diff --git a/crates/zeta/src/license_detection/apache-2.0.txt b/crates/zeta/license_examples/apache-2.0-ex0.txt similarity index 100% rename from crates/zeta/src/license_detection/apache-2.0.txt rename to crates/zeta/license_examples/apache-2.0-ex0.txt diff --git a/crates/zeta/license_examples/apache-2.0-ex1.txt b/crates/zeta/license_examples/apache-2.0-ex1.txt new file mode 100644 index 0000000000000000000000000000000000000000..2df8c87fda4e00fd552766016915a97205de740d --- /dev/null +++ b/crates/zeta/license_examples/apache-2.0-ex1.txt @@ -0,0 +1,55 @@ +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/zeta/src/license_detection/apache-2.0.regex b/crates/zeta/license_examples/apache-2.0-ex2.txt similarity index 59% rename from crates/zeta/src/license_detection/apache-2.0.regex rename to crates/zeta/license_examples/apache-2.0-ex2.txt index dcf12fe28915f94e1f5d8de81285ea49dcc10f8e..016b1bc2e6136a367c59b13eecec27e605d13664 100644 --- a/crates/zeta/src/license_detection/apache-2.0.regex +++ b/crates/zeta/license_examples/apache-2.0-ex2.txt @@ -1,109 +1,110 @@ + Apache License - Version 2\.0, January 2004 - http://www\.apache\.org/licenses/ + Version 2.0, January 2004 + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - 1\. Definitions\. + 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document\. + and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License\. + the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common - control with that entity\. For the purposes of this definition, - "control" means \(i\) the power, direct or indirect, to cause the + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or - otherwise, or \(ii\) ownership of fifty percent \(50%\) or more of the - outstanding shares, or \(iii\) beneficial ownership of such entity\. + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "You" \(or "Your"\) shall mean an individual or Legal Entity - exercising permissions granted by this License\. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation - source, and configuration files\. + source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, - and conversions to other media types\. + and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work - \(an example is provided in the Appendix below\)\. + (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on \(or derived from\) the Work and for which the + form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship\. For the purposes + represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain - separable from, or merely link \(or bind by name\) to the interfaces of, - the Work and Derivative Works thereof\. + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner\. For the purposes of this definition, "submitted" + the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution\." + designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work\. + subsequently incorporated within the Work. - 2\. Grant of Copyright License\. Subject to the terms and conditions of + 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, - worldwide, non\-exclusive, no\-charge, royalty\-free, irrevocable + worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form\. + Work and such Derivative Works in Source or Object form. - 3\. Grant of Patent License\. Subject to the terms and conditions of + 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, - worldwide, non\-exclusive, no\-charge, royalty\-free, irrevocable - \(except as stated in this section\) patent license to make, have made, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their - Contribution\(s\) alone or by combination of their Contribution\(s\) - with the Work to which such Contribution\(s\) was submitted\. If You - institute patent litigation against any entity \(including a - cross\-claim or counterclaim in a lawsuit\) alleging that the Work + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate - as of the date such litigation is filed\. + as of the date such litigation is filed. - 4\. Redistribution\. You may reproduce and distribute copies of the + 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - \(a\) You must give any other recipients of the Work or + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - \(b\) You must cause any modified files to carry prominent notices + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - \(c\) You must retain, in the Source form of any Derivative Works + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - \(d\) If the Work includes a "NOTICE" text file as part of its + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not @@ -112,90 +113,77 @@ as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and - wherever such third\-party notices normally appear\. The contents + wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and - do not modify the License\. You may add Your own attribution + do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed - as modifying the License\. + as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License\. + the conditions stated in this License. - 5\. Submission of Contributions\. Unless You explicitly state otherwise, + 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions\. + this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions\. + with Licensor regarding such Contributions. - 6\. Trademarks\. This License does not grant permission to use the trade + 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file\. + origin of the Work and reproducing the content of the NOTICE file. - 7\. Disclaimer of Warranty\. Unless required by applicable law or - agreed to in writing, Licensor provides the Work \(and each - Contributor provides its Contributions\) on an "AS IS" BASIS, + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions - of TITLE, NON\-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE\. You are solely responsible for determining the + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License\. + risks associated with Your exercise of permissions under this License. - 8\. Limitation of Liability\. In no event and under no legal theory, - whether in tort \(including negligence\), contract, or otherwise, - unless required by applicable law \(such as deliberate and grossly - negligent acts\) or agreed to in writing, shall any Contributor be + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the - Work \(including but not limited to damages for loss of goodwill, + Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses\), even if such Contributor - has been advised of the possibility of such damages\. + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. - 9\. Accepting Warranty or Additional Liability\. While redistributing + 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this - License\. However, in accepting such obligations, You may act only + License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability\.(?: - - END OF TERMS AND CONDITIONS)?(?: - - APPENDIX: How to apply the Apache License to your work\. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "\[\]" - replaced with your own identifying information\. \(Don't include - the brackets!\) The text should be enclosed in the appropriate - comment syntax for the file format\. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third\-party archives\.)?(?: + of your accepting any such warranty or additional liability. - Copyright .*)?(?: + END OF TERMS AND CONDITIONS - Licensed under the Apache License, Version 2\.0 \(the "License"\); - you may not use this file except in compliance with the License\. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www\.apache\.org/licenses/LICENSE\-2\.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\. + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License\.)? + limitations under the License. diff --git a/crates/zeta/license_examples/apache-2.0-ex3.txt b/crates/zeta/license_examples/apache-2.0-ex3.txt new file mode 100644 index 0000000000000000000000000000000000000000..243448ceb5d4909dab3740d54d2541a4370d459f --- /dev/null +++ b/crates/zeta/license_examples/apache-2.0-ex3.txt @@ -0,0 +1,13 @@ +Copyright 2011 Someone + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/crates/zeta/src/license_detection/bsd-1-clause.txt b/crates/zeta/license_examples/bsd-1-clause.txt similarity index 100% rename from crates/zeta/src/license_detection/bsd-1-clause.txt rename to crates/zeta/license_examples/bsd-1-clause.txt diff --git a/crates/zeta/src/license_detection/bsd-2-clause.txt b/crates/zeta/license_examples/bsd-2-clause-ex0.txt similarity index 100% rename from crates/zeta/src/license_detection/bsd-2-clause.txt rename to crates/zeta/license_examples/bsd-2-clause-ex0.txt diff --git a/crates/zeta/src/license_detection/bsd-3-clause.txt b/crates/zeta/license_examples/bsd-3-clause-ex0.txt similarity index 100% rename from crates/zeta/src/license_detection/bsd-3-clause.txt rename to crates/zeta/license_examples/bsd-3-clause-ex0.txt diff --git a/crates/zeta/license_examples/bsd-3-clause-ex1.txt b/crates/zeta/license_examples/bsd-3-clause-ex1.txt new file mode 100644 index 0000000000000000000000000000000000000000..d460f673756539fb8f16db9a63968059db892027 --- /dev/null +++ b/crates/zeta/license_examples/bsd-3-clause-ex1.txt @@ -0,0 +1,27 @@ +// Copyright 2024 (this is copy modified from chromium) +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of da company nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/crates/zeta/license_examples/bsd-3-clause-ex2.txt b/crates/zeta/license_examples/bsd-3-clause-ex2.txt new file mode 100644 index 0000000000000000000000000000000000000000..99fa52679d8ceebed7ffbc0d75d37ca7cb1da41c --- /dev/null +++ b/crates/zeta/license_examples/bsd-3-clause-ex2.txt @@ -0,0 +1,31 @@ +The Glasgow Haskell Compiler License + +Copyright 2002, The University Court of the University of Glasgow. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/crates/zeta/license_examples/bsd-3-clause-ex3.txt b/crates/zeta/license_examples/bsd-3-clause-ex3.txt new file mode 100644 index 0000000000000000000000000000000000000000..68a181b5a71e991a80b8030d8a5094266092a432 --- /dev/null +++ b/crates/zeta/license_examples/bsd-3-clause-ex3.txt @@ -0,0 +1,30 @@ +Copyright (c) 2019 Someone + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Someone nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/crates/zeta/license_examples/bsd-3-clause-ex4.txt b/crates/zeta/license_examples/bsd-3-clause-ex4.txt new file mode 100644 index 0000000000000000000000000000000000000000..259c59ff9062bbc208044264afb3fa83e0773968 --- /dev/null +++ b/crates/zeta/license_examples/bsd-3-clause-ex4.txt @@ -0,0 +1,27 @@ +Copyright (c) 2009-2011, Mozilla Foundation and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the names of the Mozilla Foundation nor the names of project + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/crates/zeta/src/license_detection/isc.txt b/crates/zeta/license_examples/isc.txt similarity index 100% rename from crates/zeta/src/license_detection/isc.txt rename to crates/zeta/license_examples/isc.txt diff --git a/crates/zeta/src/license_detection/mit.txt b/crates/zeta/license_examples/mit-ex0.txt similarity index 100% rename from crates/zeta/src/license_detection/mit.txt rename to crates/zeta/license_examples/mit-ex0.txt diff --git a/crates/zeta/license_examples/mit-ex1.txt b/crates/zeta/license_examples/mit-ex1.txt new file mode 100644 index 0000000000000000000000000000000000000000..d3642458dc5c970492f7b021c0881b39654220b9 --- /dev/null +++ b/crates/zeta/license_examples/mit-ex1.txt @@ -0,0 +1,26 @@ +Copyright (c) 2006-2009 Someone +Copyright (c) 2009-2013 Some organization + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/crates/zeta/license_examples/mit-ex2.txt b/crates/zeta/license_examples/mit-ex2.txt new file mode 100644 index 0000000000000000000000000000000000000000..31ec7bf0e8b1e5c57ebebe3c93f2ed2b0d24ed04 --- /dev/null +++ b/crates/zeta/license_examples/mit-ex2.txt @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) someone + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/crates/zeta/license_examples/mit-ex3.txt b/crates/zeta/license_examples/mit-ex3.txt new file mode 100644 index 0000000000000000000000000000000000000000..ed5c99140214039ed83a7a0179c587b63805c918 --- /dev/null +++ b/crates/zeta/license_examples/mit-ex3.txt @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Someone. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/crates/zeta/src/license_detection/upl-1.0.txt b/crates/zeta/license_examples/upl-1.0.txt similarity index 100% rename from crates/zeta/src/license_detection/upl-1.0.txt rename to crates/zeta/license_examples/upl-1.0.txt diff --git a/crates/zeta/license_examples/zlib-ex0.txt b/crates/zeta/license_examples/zlib-ex0.txt new file mode 100644 index 0000000000000000000000000000000000000000..84a3048c69a2e1ae4aa93b26945f1aff4b6888fe --- /dev/null +++ b/crates/zeta/license_examples/zlib-ex0.txt @@ -0,0 +1,19 @@ +Copyright (c) 2021 Someone + +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a product, + an acknowledgment in the product documentation would be appreciated but is + not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source distribution. diff --git a/crates/zeta/license_regexes/0bsd.regex b/crates/zeta/license_regexes/0bsd.regex new file mode 100644 index 0000000000000000000000000000000000000000..15725f206a905fb0de1c2f03ec40dde25a1f01c4 --- /dev/null +++ b/crates/zeta/license_regexes/0bsd.regex @@ -0,0 +1,10 @@ +.{0,512}Permission to use copy modify andor distribute this software for any +purpose with or without fee is hereby granted + +THE SOFTWARE IS PROVIDED AS IS AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS IN NO EVENT SHALL THE AUTHOR BE LIABLE +FOR ANY SPECIAL DIRECT INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY +DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE DATA OR PROFITS WHETHER IN +AN ACTION OF CONTRACT NEGLIGENCE OR OTHER TORTIOUS ACTION ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE diff --git a/crates/zeta/license_regexes/apache-2.0.regex b/crates/zeta/license_regexes/apache-2.0.regex new file mode 100644 index 0000000000000000000000000000000000000000..26cbecf2ee299e957e18d9da5c467f7788874358 --- /dev/null +++ b/crates/zeta/license_regexes/apache-2.0.regex @@ -0,0 +1,223 @@ +.{0,512}Licensed under the Apache License Version 20 the License +you may not use this file except in compliance with the License +You may obtain a copy of the License at + + https?wwwapacheorglicensesLICENSE20 + +Unless required by applicable law or agreed to in writing software +distributed under the License is distributed on an AS IS BASIS +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied +See the License for the specific language governing permissions and +limitations under the License|.{0,512}(?:Licensed under the Apache License Version 20 the License +you may not use this file except in compliance with the License +You may obtain a copy of the License at + + https?wwwapacheorglicensesLICENSE20 + +Unless required by applicable law or agreed to in writing software +distributed under the License is distributed on an AS IS BASIS +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied +See the License for the specific language governing permissions and +limitations under the License)? + + ?Apache License + Version 20 January 2004 + https?wwwapacheorglicenses + + TERMS AND CONDITIONS FOR USE REPRODUCTION AND DISTRIBUTION + + 1 Definitions + + License shall mean the terms and conditions for use reproduction + and distribution as defined by Sections 1 through 9 of this document + + Licensor shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License + + Legal Entity shall mean the union of the acting entity and all + other entities that control are controlled by or are under common + control with that entity For the purposes of this definition + control means i the power direct or indirect to cause the + direction or management of such entity whether by contract or + otherwise or ii ownership of fifty percent 50 or more of the + outstanding shares or iii beneficial ownership of such entity + + You or Your shall mean an individual or Legal Entity + exercising permissions granted by this License + + Source form shall mean the preferred form for making modifications + including but not limited to software source code documentation + source and configuration files + + Object form shall mean any form resulting from mechanical + transformation or translation of a Source form including but + not limited to compiled object code generated documentation + and conversions to other media types + + Work shall mean the work of authorship whether in Source or + Object form made available under the License as indicated by a + copyright notice that is included in or attached to the work + an example is provided in the Appendix below + + Derivative Works shall mean any work whether in Source or Object + form that is based on or derived from the Work and for which the + editorial revisions annotations elaborations or other modifications + represent as a whole an original work of authorship For the purposes + of this License Derivative Works shall not include works that remain + separable from or merely link or bind by name to the interfaces of + the Work and Derivative Works thereof + + Contribution shall mean any work of authorship including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner For the purposes of this definition submitted + means any form of electronic verbal or written communication sent + to the Licensor or its representatives including but not limited to + communication on electronic mailing lists source code control systems + and issue tracking systems that are managed by or on behalf of the + Licensor for the purpose of discussing and improving the Work but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as Not a Contribution + + Contributor shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work + + 2 Grant of Copyright License Subject to the terms and conditions of + this License each Contributor hereby grants to You a perpetual + worldwide nonexclusive nocharge royaltyfree irrevocable + copyright license to reproduce prepare Derivative Works of + publicly display publicly perform sublicense and distribute the + Work and such Derivative Works in Source or Object form + + 3 Grant of Patent License Subject to the terms and conditions of + this License each Contributor hereby grants to You a perpetual + worldwide nonexclusive nocharge royaltyfree irrevocable + except as stated in this section patent license to make have made + use offer to sell sell import and otherwise transfer the Work + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contributions alone or by combination of their Contributions + with the Work to which such Contributions was submitted If You + institute patent litigation against any entity including a + crossclaim or counterclaim in a lawsuit alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed + + 4 Redistribution You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium with or without + modifications and in Source or Object form provided that You + meet the following conditions + + (?:a )?You must give any other recipients of the Work or + Derivative Works a copy of this License and + + (?:b )?You must cause any modified files to carry prominent notices + stating that You changed the files and + + (?:c )?You must retain in the Source form of any Derivative Works + that You distribute all copyright patent trademark and + attribution notices from the Source form of the Work + excluding those notices that do not pertain to any part of + the Derivative Works and + + (?:d )?If the Work includes a NOTICE text file as part of its + distribution then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file excluding those notices that do not + pertain to any part of the Derivative Works in at least one + of the following places within a NOTICE text file distributed + as part of the Derivative Works within the Source form or + documentation if provided along with the Derivative Works or + within a display generated by the Derivative Works if and + wherever such thirdparty notices normally appear The contents + of the NOTICE file are for informational purposes only and + do not modify the License You may add Your own attribution + notices within Derivative Works that You distribute alongside + or as an addendum to the NOTICE text from the Work provided + that such additional attribution notices cannot be construed + as modifying the License + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use reproduction or distribution of Your modifications or + for any such Derivative Works as a whole provided Your use + reproduction and distribution of the Work otherwise complies with + the conditions stated in this License + + 5 Submission of Contributions Unless You explicitly state otherwise + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License without any additional terms or conditions + Notwithstanding the above nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions + + 6 Trademarks This License does not grant permission to use the trade + names trademarks service marks or product names of the Licensor + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file + + 7 Disclaimer of Warranty Unless required by applicable law or + agreed to in writing Licensor provides the Work and each + Contributor provides its Contributions on an AS IS BASIS + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or + implied including without limitation any warranties or conditions + of TITLE NONINFRINGEMENT MERCHANTABILITY or FITNESS FOR A + PARTICULAR PURPOSE You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License + + 8 Limitation of Liability In no event and under no legal theory + whether in tort including negligence contract or otherwise + unless required by applicable law such as deliberate and grossly + negligent acts or agreed to in writing shall any Contributor be + liable to You for damages including any direct indirect special + incidental or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work including but not limited to damages for loss of goodwill + work stoppage computer failure or malfunction or any and all + other commercial damages or losses even if such Contributor + has been advised of the possibility of such damages + + 9 Accepting Warranty or Additional Liability While redistributing + the Work or Derivative Works thereof You may choose to offer + and charge a fee for acceptance of support warranty indemnity + or other liability obligations andor rights consistent with this + License However in accepting such obligations You may act only + on Your own behalf and on Your sole responsibility not on behalf + of any other Contributor and only if You agree to indemnify + defend and hold each Contributor harmless for any liability + incurred by or claims asserted against such Contributor by reason + of your accepting any such warranty or additional liability(?: + + END OF TERMS AND CONDITIONS)?(?: + + APPENDIX How to apply the Apache License to your work + + To apply the Apache License to your work attach the following + boilerplate notice with the fields enclosed by brackets + replaced with your own identifying information Dont include + the brackets The text should be enclosed in the appropriate + comment syntax for the file format We also recommend that a + file or class name and description of purpose be included on the + same printed page as the copyright notice for easier + identification within thirdparty archives)?(?: + + Copyright.{0,512})?(?: + + Licensed under the Apache License Version 20 the License + you may not use this file except in compliance with the License + You may obtain a copy of the License at + + https?wwwapacheorglicensesLICENSE20 + + Unless required by applicable law or agreed to in writing software + distributed under the License is distributed on an AS IS BASIS + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied + See the License for the specific language governing permissions and + limitations under the License)? diff --git a/crates/zeta/license_regexes/bsd.regex b/crates/zeta/license_regexes/bsd.regex new file mode 100644 index 0000000000000000000000000000000000000000..655e38fa4336a9e3580ec8a0fc29bba4e5bd68ab --- /dev/null +++ b/crates/zeta/license_regexes/bsd.regex @@ -0,0 +1,23 @@ +.{0,512}Redistribution and use in source and binary forms with or without +modification are permitted provided that the following conditions are met + +(?:1 )?Redistributions of source code must retain the above copyright +notice this list of conditions and the following disclaimer(?: + +(?:2 )?Redistributions in binary form must reproduce the above copyright +notice this list of conditions and the following disclaimer in the +documentation andor other materials provided with the distribution(?: + +(?:3 )?.{0,128} may be used to endorse or +promote products derived from this software without specific prior written +permission)?)? + +THIS SOFTWARE IS PROVIDED BY .{0,128}AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES +INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED IN NO EVENT SHALL .{0,128}BE LIABLE +FOR ANY DIRECT INDIRECT INCIDENTAL SPECIAL EXEMPLARY OR CONSEQUENTIAL +DAMAGES INCLUDING BUT NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES LOSS OF USE DATA OR PROFITS OR BUSINESS INTERRUPTION HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY WHETHER IN CONTRACT STRICT LIABILITY OR +TORT INCLUDING NEGLIGENCE OR OTHERWISE ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE diff --git a/crates/zeta/license_regexes/isc.regex b/crates/zeta/license_regexes/isc.regex new file mode 100644 index 0000000000000000000000000000000000000000..ba3e3c9cbf8d1a5711485066c496fa89fb8f4c66 --- /dev/null +++ b/crates/zeta/license_regexes/isc.regex @@ -0,0 +1,12 @@ +.{0,512}Permission to use copy modify andor distribute +this software for any purpose with or without fee is hereby granted provided +that the above copyright notice and this permission notice appear in all +copies + +THE SOFTWARE IS PROVIDED AS IS AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL DIRECT INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE DATA OR PROFITS WHETHER IN AN +ACTION OF CONTRACT NEGLIGENCE OR OTHER TORTIOUS ACTION ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE diff --git a/crates/zeta/license_regexes/mit.regex b/crates/zeta/license_regexes/mit.regex new file mode 100644 index 0000000000000000000000000000000000000000..a8fa7b3ee7a5656d74012790344e1204e75cefa4 --- /dev/null +++ b/crates/zeta/license_regexes/mit.regex @@ -0,0 +1,17 @@ +.{0,512}Permission is hereby granted free of charge to any +person obtaining a copy of this software and associated documentation files +the Software to deal in the Software without restriction including +without limitation the rights to use copy modify merge publish distribute +sublicense andor sell copies of the Software and to permit persons to whom +the Software is furnished to do so subject to the following conditions + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software + +THE SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND EXPRESS OR +IMPLIED INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM DAMAGES OR OTHER +LIABILITY WHETHER IN AN ACTION OF CONTRACT TORT OR OTHERWISE ARISING FROM +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE diff --git a/crates/zeta/license_regexes/upl-1.0.regex b/crates/zeta/license_regexes/upl-1.0.regex new file mode 100644 index 0000000000000000000000000000000000000000..f86f5fa3ab76d4f99177923d16b0b5fe8d0f18c0 --- /dev/null +++ b/crates/zeta/license_regexes/upl-1.0.regex @@ -0,0 +1,32 @@ +.{0,512}Subject to the condition set forth below permission is hereby granted to any +person obtaining a copy of this software associated documentation andor data +collectively the Software free of charge and under any and all copyright +rights in the Software and any and all patent rights owned or freely licensable +by each licensor hereunder covering either i the unmodified Software as +contributed to or provided by such licensor or ii the Larger Works as +defined below to deal in both + +a the Software and + +b any piece of software andor hardware listed in the lrgrwrkstxt file if one is + included with the Software each a Larger Work to which the Software is + contributed by such licensors + +without restriction including without limitation the rights to copy create +derivative works of display perform and distribute the Software and make use +sell offer for sale import export have made and have sold the Software and the +Larger Works and to sublicense the foregoing rights on either these or other +terms + +This license is subject to the following condition + +The above copyright notice and either this complete permission notice or at a minimum +a reference to the UPL must be included in all copies or substantial portions of the +Software + +THE SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND EXPRESS OR IMPLIED +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM DAMAGES OR OTHER LIABILITY WHETHER IN AN ACTION OF +CONTRACT TORT OR OTHERWISE ARISING FROM OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE diff --git a/crates/zeta/license_regexes/zlib.regex b/crates/zeta/license_regexes/zlib.regex new file mode 100644 index 0000000000000000000000000000000000000000..63a688d08c0fbc5a24783c1f8a2462979927ca6c --- /dev/null +++ b/crates/zeta/license_regexes/zlib.regex @@ -0,0 +1,18 @@ +.{0,512}This software is provided asis without any express or implied +warranty In no event will the authors be held liable for any damages +arising from the use of this software + +Permission is granted to anyone to use this software for any purpose +including commercial applications and to alter it and redistribute it +freely subject to the following restrictions + +1? The origin of this software must not be misrepresented you must not +claim that you wrote the original software If you use this software +in a product an acknowledgment in the product documentation would be +appreciated but is not required + +2? Altered source versions must be plainly marked as such and must not be +misrepresented as being the original software + +3? This notice may not be removed or altered from any source +distribution diff --git a/crates/zeta/src/license_detection.rs b/crates/zeta/src/license_detection.rs index d6b8ef10a3363f49f92607e30c6059ffee573a65..81314477e5383450c089be5291e02ba3f8478ac4 100644 --- a/crates/zeta/src/license_detection.rs +++ b/crates/zeta/src/license_detection.rs @@ -8,6 +8,7 @@ use std::{ use fs::Fs; use futures::StreamExt as _; use gpui::{App, AppContext as _, Entity, Subscription, Task}; +use itertools::Itertools; use postage::watch; use project::Worktree; use regex::Regex; @@ -25,7 +26,8 @@ static LICENSE_FILE_NAME_REGEX: LazyLock = LazyLock::new(|| 0? bsd (?: [\\-._] [0123])? (?: [\\-._] clause)? | \ isc | \ mit | \ - upl))? \ + upl | \ + zlib))? \ (?: [\\-._]? (?: license | licence))? \ (?: \\.txt | \\.md)? \ $", @@ -36,16 +38,15 @@ static LICENSE_FILE_NAME_REGEX: LazyLock = LazyLock::new(|| .unwrap() }); -#[derive(Debug, Clone, Copy, Eq, PartialEq, VariantArray)] +#[derive(Debug, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, VariantArray)] pub enum OpenSourceLicense { Apache2_0, - BSD0Clause, - BSD1Clause, - BSD2Clause, - BSD3Clause, + BSDZero, + BSD, ISC, MIT, UPL1_0, + Zlib, } impl Display for OpenSourceLicense { @@ -55,29 +56,31 @@ impl Display for OpenSourceLicense { } impl OpenSourceLicense { + /// These are SPDX identifiers for the licenses, except for BSD, where the variants are not + /// distinguished. pub fn spdx_identifier(&self) -> &'static str { match self { OpenSourceLicense::Apache2_0 => "apache-2.0", - OpenSourceLicense::BSD0Clause => "0bsd", - OpenSourceLicense::BSD1Clause => "bsd-1-clause", - OpenSourceLicense::BSD2Clause => "bsd-2-clause", - OpenSourceLicense::BSD3Clause => "bsd-3-clause", + OpenSourceLicense::BSDZero => "0bsd", + OpenSourceLicense::BSD => "bsd", OpenSourceLicense::ISC => "isc", OpenSourceLicense::MIT => "mit", OpenSourceLicense::UPL1_0 => "upl-1.0", + OpenSourceLicense::Zlib => "zlib", } } + /// Regexes to match the license text. These regexes are expected to match the entire file. Also + /// note that `canonicalize_license_text` removes everything but alphanumeric ascii characters. pub fn regex(&self) -> &'static str { match self { - OpenSourceLicense::Apache2_0 => include_str!("license_detection/apache-2.0.regex"), - OpenSourceLicense::BSD0Clause => include_str!("license_detection/0bsd.regex"), - OpenSourceLicense::BSD1Clause => include_str!("license_detection/bsd-1-clause.regex"), - OpenSourceLicense::BSD2Clause => include_str!("license_detection/bsd-2-clause.regex"), - OpenSourceLicense::BSD3Clause => include_str!("license_detection/bsd-3-clause.regex"), - OpenSourceLicense::ISC => include_str!("license_detection/isc.regex"), - OpenSourceLicense::MIT => include_str!("license_detection/mit.regex"), - OpenSourceLicense::UPL1_0 => include_str!("license_detection/upl-1.0.regex"), + OpenSourceLicense::Apache2_0 => include_str!("../license_regexes/apache-2.0.regex"), + OpenSourceLicense::BSDZero => include_str!("../license_regexes/0bsd.regex"), + OpenSourceLicense::BSD => include_str!("../license_regexes/bsd.regex"), + OpenSourceLicense::ISC => include_str!("../license_regexes/isc.regex"), + OpenSourceLicense::MIT => include_str!("../license_regexes/mit.regex"), + OpenSourceLicense::UPL1_0 => include_str!("../license_regexes/upl-1.0.regex"), + OpenSourceLicense::Zlib => include_str!("../license_regexes/zlib.regex"), } } } @@ -93,7 +96,7 @@ fn detect_license(license: &str) -> Option { } else { regex_string.push_str(")|("); } - regex_string.push_str(&canonicalize_license_text(license.regex())); + regex_string.push_str(&canonicalize_license_regex(license.regex())); } regex_string.push_str("))$"); let regex = Regex::new(®ex_string).unwrap(); @@ -116,15 +119,25 @@ fn detect_license(license: &str) -> Option { }) } -/// Canonicalizes the whitespace of license text and license regexes. -fn canonicalize_license_text(license: &str) -> String { +/// Canonicalizes the whitespace of license text. +fn canonicalize_license_regex(license: &str) -> String { license .split_ascii_whitespace() - .collect::>() .join(" ") .to_ascii_lowercase() } +/// Canonicalizes the whitespace of license text. +fn canonicalize_license_text(license: &str) -> String { + license + .chars() + .filter(|c| c.is_ascii_alphanumeric() || c.is_ascii_whitespace()) + .map(|c| c.to_ascii_lowercase()) + .collect::() + .split_ascii_whitespace() + .join(" ") +} + pub enum LicenseDetectionWatcher { Local { is_open_source_rx: watch::Receiver, @@ -254,26 +267,96 @@ mod tests { use super::*; - const APACHE_2_0_TXT: &str = include_str!("license_detection/apache-2.0.txt"); - const ISC_TXT: &str = include_str!("license_detection/isc.txt"); - const MIT_TXT: &str = include_str!("license_detection/mit.txt"); - const UPL_1_0_TXT: &str = include_str!("license_detection/upl-1.0.txt"); - const BSD_0_CLAUSE_TXT: &str = include_str!("license_detection/0bsd.txt"); - const BSD_1_CLAUSE_TXT: &str = include_str!("license_detection/bsd-1-clause.txt"); - const BSD_2_CLAUSE_TXT: &str = include_str!("license_detection/bsd-2-clause.txt"); - const BSD_3_CLAUSE_TXT: &str = include_str!("license_detection/bsd-3-clause.txt"); + const APACHE_2_0_TXT: &str = include_str!("../license_examples/apache-2.0-ex0.txt"); + const ISC_TXT: &str = include_str!("../license_examples/isc.txt"); + const MIT_TXT: &str = include_str!("../license_examples/mit-ex0.txt"); + const UPL_1_0_TXT: &str = include_str!("../license_examples/upl-1.0.txt"); + const BSD_0_TXT: &str = include_str!("../license_examples/0bsd.txt"); #[track_caller] fn assert_matches_license(text: &str, license: OpenSourceLicense) { - let license_regex = - Regex::new(&format!("^{}$", canonicalize_license_text(license.regex()))).unwrap(); - assert!(license_regex.is_match(&canonicalize_license_text(text))); - assert_eq!(detect_license(text), Some(license)); + if detect_license(text) != Some(license) { + let license_regex_text = canonicalize_license_regex(license.regex()); + let license_regex = Regex::new(&format!("^{}$", license_regex_text)).unwrap(); + let text = canonicalize_license_text(text); + let matched_regex = license_regex.is_match(&text); + if matched_regex { + panic!( + "The following text matches the individual regex for {}, \ + but not the combined one:\n```license-text\n{}\n```\n", + license, text + ); + } else { + panic!( + "The following text doesn't match the regex for {}:\n\ + ```license-text\n{}\n```\n\n```regex\n{}\n```\n", + license, text, license_regex_text + ); + } + } } + /* + // Uncomment this and run with `cargo test -p zeta -- --no-capture &> licenses-output` to + // traverse your entire home directory and run license detection on every file that has a + // license-like name. #[test] - fn test_0bsd_positive_detection() { - assert_matches_license(BSD_0_CLAUSE_TXT, OpenSourceLicense::BSD0Clause); + fn test_check_all_licenses_in_home_dir() { + let mut detected = Vec::new(); + let mut unrecognized = Vec::new(); + let mut walked_entries = 0; + let homedir = std::env::home_dir().unwrap(); + for entry in walkdir::WalkDir::new(&homedir) { + walked_entries += 1; + if walked_entries % 10000 == 0 { + println!( + "So far visited {} files in {}", + walked_entries, + homedir.display() + ); + } + let Ok(entry) = entry else { + continue; + }; + if !LICENSE_FILE_NAME_REGEX.is_match(entry.file_name().as_encoded_bytes()) { + continue; + } + let Ok(contents) = std::fs::read_to_string(entry.path()) else { + continue; + }; + let path_string = entry.path().to_string_lossy().to_string(); + match detect_license(&contents) { + Some(license) => detected.push((license, path_string)), + None => unrecognized.push(path_string), + } + } + println!("\nDetected licenses:\n"); + detected.sort(); + for (license, path) in &detected { + println!("{}: {}", license.spdx_identifier(), path); + } + println!("\nUnrecognized licenses:\n"); + for path in &unrecognized { + println!("{}", path); + } + panic!( + "{} licenses detected, {} unrecognized", + detected.len(), + unrecognized.len() + ); + println!("This line has a warning to make sure this test is always commented out"); + } + */ + + #[test] + fn test_no_unicode_in_regexes() { + for license in OpenSourceLicense::VARIANTS { + assert!( + !license.regex().contains(|c: char| !c.is_ascii()), + "{}.regex contains unicode", + license.spdx_identifier() + ); + } } #[test] @@ -319,6 +402,24 @@ mod tests { ); assert!(license_with_copyright != license_with_appendix); assert_matches_license(&license_with_copyright, OpenSourceLicense::Apache2_0); + + assert_matches_license( + include_str!("../../../LICENSE-APACHE"), + OpenSourceLicense::Apache2_0, + ); + + assert_matches_license( + include_str!("../license_examples/apache-2.0-ex1.txt"), + OpenSourceLicense::Apache2_0, + ); + assert_matches_license( + include_str!("../license_examples/apache-2.0-ex2.txt"), + OpenSourceLicense::Apache2_0, + ); + assert_matches_license( + include_str!("../license_examples/apache-2.0-ex3.txt"), + OpenSourceLicense::Apache2_0, + ); } #[test] @@ -333,17 +434,47 @@ mod tests { #[test] fn test_bsd_1_clause_positive_detection() { - assert_matches_license(BSD_1_CLAUSE_TXT, OpenSourceLicense::BSD1Clause); + assert_matches_license( + include_str!("../license_examples/bsd-1-clause.txt"), + OpenSourceLicense::BSD, + ); } #[test] fn test_bsd_2_clause_positive_detection() { - assert_matches_license(BSD_2_CLAUSE_TXT, OpenSourceLicense::BSD2Clause); + assert_matches_license( + include_str!("../license_examples/bsd-2-clause-ex0.txt"), + OpenSourceLicense::BSD, + ); } #[test] fn test_bsd_3_clause_positive_detection() { - assert_matches_license(BSD_3_CLAUSE_TXT, OpenSourceLicense::BSD3Clause); + assert_matches_license( + include_str!("../license_examples/bsd-3-clause-ex0.txt"), + OpenSourceLicense::BSD, + ); + assert_matches_license( + include_str!("../license_examples/bsd-3-clause-ex1.txt"), + OpenSourceLicense::BSD, + ); + assert_matches_license( + include_str!("../license_examples/bsd-3-clause-ex2.txt"), + OpenSourceLicense::BSD, + ); + assert_matches_license( + include_str!("../license_examples/bsd-3-clause-ex3.txt"), + OpenSourceLicense::BSD, + ); + assert_matches_license( + include_str!("../license_examples/bsd-3-clause-ex4.txt"), + OpenSourceLicense::BSD, + ); + } + + #[test] + fn test_bsd_0_positive_detection() { + assert_matches_license(BSD_0_TXT, OpenSourceLicense::BSDZero); } #[test] @@ -365,6 +496,18 @@ mod tests { #[test] fn test_mit_positive_detection() { assert_matches_license(MIT_TXT, OpenSourceLicense::MIT); + assert_matches_license( + include_str!("../license_examples/mit-ex1.txt"), + OpenSourceLicense::MIT, + ); + assert_matches_license( + include_str!("../license_examples/mit-ex2.txt"), + OpenSourceLicense::MIT, + ); + assert_matches_license( + include_str!("../license_examples/mit-ex3.txt"), + OpenSourceLicense::MIT, + ); } #[test] @@ -393,6 +536,14 @@ mod tests { assert!(detect_license(&license_text).is_none()); } + #[test] + fn test_zlib_positive_detection() { + assert_matches_license( + include_str!("../license_examples/zlib-ex0.txt"), + OpenSourceLicense::Zlib, + ); + } + #[test] fn test_license_file_name_regex() { // Test basic license file names diff --git a/crates/zeta/src/license_detection/0bsd.regex b/crates/zeta/src/license_detection/0bsd.regex deleted file mode 100644 index 7928a8d181a48ad54bb825ac120aaa4ef53ba8ef..0000000000000000000000000000000000000000 --- a/crates/zeta/src/license_detection/0bsd.regex +++ /dev/null @@ -1,12 +0,0 @@ -.* - -Permission to use, copy, modify, and/or distribute this software for -any purpose with or without fee is hereby granted\. - -THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL -WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS\. IN NO EVENT SHALL THE AUTHOR BE LIABLE -FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY -DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN -AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT -OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE\. diff --git a/crates/zeta/src/license_detection/bsd-1-clause.regex b/crates/zeta/src/license_detection/bsd-1-clause.regex deleted file mode 100644 index 5e73e5c6d0e67cd9e4899e1a44bd064f11f3e3dc..0000000000000000000000000000000000000000 --- a/crates/zeta/src/license_detection/bsd-1-clause.regex +++ /dev/null @@ -1,17 +0,0 @@ -.*Copyright.* - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -(?:1\.|\*)? Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer\. - -THIS SOFTWARE IS PROVIDED BY .* “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, -INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL .* BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES \(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION\) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -TORT \(INCLUDING NEGLIGENCE OR OTHERWISE\) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE\. diff --git a/crates/zeta/src/license_detection/bsd-2-clause.regex b/crates/zeta/src/license_detection/bsd-2-clause.regex deleted file mode 100644 index 93d22652fb11ba81d55e7d2d38e1b42bdce243b6..0000000000000000000000000000000000000000 --- a/crates/zeta/src/license_detection/bsd-2-clause.regex +++ /dev/null @@ -1,22 +0,0 @@ -.*Copyright.* - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -(?:1\.|\*)? Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer\. - -(?:2\.|\*)? Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution\. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED\. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES \(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION\) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -TORT \(INCLUDING NEGLIGENCE OR OTHERWISE\) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE\. diff --git a/crates/zeta/src/license_detection/bsd-3-clause.regex b/crates/zeta/src/license_detection/bsd-3-clause.regex deleted file mode 100644 index b31443de64283d0d66135b73e57eaf9bd19b88a3..0000000000000000000000000000000000000000 --- a/crates/zeta/src/license_detection/bsd-3-clause.regex +++ /dev/null @@ -1,26 +0,0 @@ -.*Copyright.* - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -(?:1\.|\*)? Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer\. - -(?:2\.|\*)? Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution\. - -(?:3\.|\*)? Neither the name of the copyright holder nor the names of its -contributors may be used to endorse or promote products derived from this -software without specific prior written permission\. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED\. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES \(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION\) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -TORT \(INCLUDING NEGLIGENCE OR OTHERWISE\) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE\. diff --git a/crates/zeta/src/license_detection/isc.regex b/crates/zeta/src/license_detection/isc.regex deleted file mode 100644 index ddaece5375fc17455e8640bb47a807d5cd347f5b..0000000000000000000000000000000000000000 --- a/crates/zeta/src/license_detection/isc.regex +++ /dev/null @@ -1,15 +0,0 @@ -.*ISC License.* - -Copyright.* - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies\. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS\. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE\. diff --git a/crates/zeta/src/license_detection/mit.regex b/crates/zeta/src/license_detection/mit.regex deleted file mode 100644 index 43130424c5fe5f73d11ddda5d5c821bc6cb86afe..0000000000000000000000000000000000000000 --- a/crates/zeta/src/license_detection/mit.regex +++ /dev/null @@ -1,21 +0,0 @@ -.*MIT License.* - -Copyright.* - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files \(the "Software"\), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software\. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE\. diff --git a/crates/zeta/src/license_detection/upl-1.0.regex b/crates/zeta/src/license_detection/upl-1.0.regex deleted file mode 100644 index 0959f729716af4714ae9f41c92e1480d276cdeab..0000000000000000000000000000000000000000 --- a/crates/zeta/src/license_detection/upl-1.0.regex +++ /dev/null @@ -1,35 +0,0 @@ -Copyright.* - -The Universal Permissive License.* - -Subject to the condition set forth below, permission is hereby granted to any person -obtaining a copy of this software, associated documentation and/or data \(collectively -the "Software"\), free of charge and under any and all copyright rights in the -Software, and any and all patent rights owned or freely licensable by each licensor -hereunder covering either \(i\) the unmodified Software as contributed to or provided -by such licensor, or \(ii\) the Larger Works \(as defined below\), to deal in both - -\(a\) the Software, and - -\(b\) any piece of software and/or hardware listed in the lrgrwrks\.txt file if one is - included with the Software \(each a "Larger Work" to which the Software is - contributed by such licensors\), - -without restriction, including without limitation the rights to copy, create -derivative works of, display, perform, and distribute the Software and make, use, -sell, offer for sale, import, export, have made, and have sold the Software and the -Larger Work\(s\), and to sublicense the foregoing rights on either these or other -terms\. - -This license is subject to the following condition: - -The above copyright notice and either this complete permission notice or at a minimum -a reference to the UPL must be included in all copies or substantial portions of the -Software\. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT\. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE\. From 253765aaa1f15d45e2effe098222d07550fdf0e9 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Sun, 31 Aug 2025 01:23:21 -0600 Subject: [PATCH 460/744] zeta: Improve efficiency and clarity of license detection patterns (#37242) See discussion on #36564 Adds a simple ad-hoc substring matching pattern language which allows skipping a bounded number of chars between matched substrings. Before this change compiling the regex was taking ~120ms on a fast machine and ~8mb of memory. This new version is way faster and uses minimal memory. Checked the behavior of this vs by running it against 10k licenses that happened to be in my home dir. There were only 4 differences of behavior with the regex implementation, and these were false negatives for the regex implementation that are true positives with the new one. Of the ~10k licenses in my home dir, ~1k do not match one of these licenses, usually because it's GPL/MPL/etc. Release Notes: - N/A --- Cargo.lock | 1 - crates/zeta/Cargo.toml | 1 - .../zeta/license_examples/apache-2.0-ex4.txt | 187 ++++++++++ crates/zeta/license_patterns/0bsd-pattern | 11 + .../zeta/license_patterns/apache-2.0-pattern | 109 ++++++ .../apache-2.0-reference-pattern | 14 + crates/zeta/license_patterns/bsd-pattern | 32 ++ crates/zeta/license_patterns/isc-pattern | 12 + crates/zeta/license_patterns/mit-pattern | 18 + crates/zeta/license_patterns/upl-1.0-pattern | 32 ++ crates/zeta/license_patterns/zlib-pattern | 21 ++ crates/zeta/license_regexes/0bsd.regex | 10 - crates/zeta/license_regexes/apache-2.0.regex | 223 ----------- crates/zeta/license_regexes/bsd.regex | 23 -- crates/zeta/license_regexes/isc.regex | 12 - crates/zeta/license_regexes/mit.regex | 17 - crates/zeta/license_regexes/upl-1.0.regex | 32 -- crates/zeta/license_regexes/zlib.regex | 18 - crates/zeta/src/license_detection.rs | 352 +++++++++--------- 19 files changed, 614 insertions(+), 511 deletions(-) create mode 100644 crates/zeta/license_examples/apache-2.0-ex4.txt create mode 100644 crates/zeta/license_patterns/0bsd-pattern create mode 100644 crates/zeta/license_patterns/apache-2.0-pattern create mode 100644 crates/zeta/license_patterns/apache-2.0-reference-pattern create mode 100644 crates/zeta/license_patterns/bsd-pattern create mode 100644 crates/zeta/license_patterns/isc-pattern create mode 100644 crates/zeta/license_patterns/mit-pattern create mode 100644 crates/zeta/license_patterns/upl-1.0-pattern create mode 100644 crates/zeta/license_patterns/zlib-pattern delete mode 100644 crates/zeta/license_regexes/0bsd.regex delete mode 100644 crates/zeta/license_regexes/apache-2.0.regex delete mode 100644 crates/zeta/license_regexes/bsd.regex delete mode 100644 crates/zeta/license_regexes/isc.regex delete mode 100644 crates/zeta/license_regexes/mit.regex delete mode 100644 crates/zeta/license_regexes/upl-1.0.regex delete mode 100644 crates/zeta/license_regexes/zlib.regex diff --git a/Cargo.lock b/Cargo.lock index 4ca45445e2ed26819c612381b682aa9d1bf35d07..ab3b713a113a95183e5f394bae0f1a31301da3f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20840,7 +20840,6 @@ dependencies = [ "tree-sitter-go", "tree-sitter-rust", "ui", - "unindent", "util", "uuid", "workspace", diff --git a/crates/zeta/Cargo.toml b/crates/zeta/Cargo.toml index a57781ee8ee4b97805935efc7943df9eff1a8958..a9c2a7619f4db22e51c014672aa2100b30a2539a 100644 --- a/crates/zeta/Cargo.toml +++ b/crates/zeta/Cargo.toml @@ -78,7 +78,6 @@ settings = { workspace = true, features = ["test-support"] } theme = { workspace = true, features = ["test-support"] } tree-sitter-go.workspace = true tree-sitter-rust.workspace = true -unindent.workspace = true workspace = { workspace = true, features = ["test-support"] } worktree = { workspace = true, features = ["test-support"] } zlog.workspace = true diff --git a/crates/zeta/license_examples/apache-2.0-ex4.txt b/crates/zeta/license_examples/apache-2.0-ex4.txt new file mode 100644 index 0000000000000000000000000000000000000000..8c004949ee1e3da9b0ba26cd35c57e61c243c3d1 --- /dev/null +++ b/crates/zeta/license_examples/apache-2.0-ex4.txt @@ -0,0 +1,187 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright (c) 2017, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/zeta/license_patterns/0bsd-pattern b/crates/zeta/license_patterns/0bsd-pattern new file mode 100644 index 0000000000000000000000000000000000000000..8b7f6100424931fa957615c5326d347eb1825942 --- /dev/null +++ b/crates/zeta/license_patterns/0bsd-pattern @@ -0,0 +1,11 @@ +-- 0..512 +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE +FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/crates/zeta/license_patterns/apache-2.0-pattern b/crates/zeta/license_patterns/apache-2.0-pattern new file mode 100644 index 0000000000000000000000000000000000000000..39e2d10c25800c1be612a3d43408025bcad9e74b --- /dev/null +++ b/crates/zeta/license_patterns/apache-2.0-pattern @@ -0,0 +1,109 @@ +-- 0..512 +-- 0..0 optional: +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http +-- 0..1 optional: +://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-- 0..5 +Apache License + +Version 2.0, January 2004 + +http +-- 0..1 +://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +-- 1..5 +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +-- 1..5 +You must cause any modified files to carry prominent notices stating that You changed the files; and + +-- 1..5 +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +-- 1..5 +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +-- 1..1 optional: +END OF TERMS AND CONDITIONS + +-- 1..1 optional: +APPENDIX How to apply the Apache License to your work + +To apply the Apache License to your work attach the following +boilerplate notice with the fields enclosed by brackets +replaced with your own identifying information Dont include +the brackets The text should be enclosed in the appropriate +comment syntax for the file format We also recommend that a +file or class name and description of purpose be included on the +same printed page as the copyright notice for easier +identification within thirdparty archives + +-- 1..512 optional: +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. + +-- 1..5 optional: +You may obtain a copy of the License at + +http +-- 0..1 optional: +://www.apache.org/licenses/LICENSE-2.0 + +-- 1..5 optional: +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/crates/zeta/license_patterns/apache-2.0-reference-pattern b/crates/zeta/license_patterns/apache-2.0-reference-pattern new file mode 100644 index 0000000000000000000000000000000000000000..192148fc7a774f563e421b84eb9dbdd39fe3f8cc --- /dev/null +++ b/crates/zeta/license_patterns/apache-2.0-reference-pattern @@ -0,0 +1,14 @@ +-- 0..512 +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http +-- 0..1 +://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/crates/zeta/license_patterns/bsd-pattern b/crates/zeta/license_patterns/bsd-pattern new file mode 100644 index 0000000000000000000000000000000000000000..917b7e3c443db4354f33c647be3a875eb9b6e9fa --- /dev/null +++ b/crates/zeta/license_patterns/bsd-pattern @@ -0,0 +1,32 @@ +-- 0..512 +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +-- 1..5 +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +-- 1..5 optional: +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +-- 1..128 optional: +may be used to endorse or promote products derived from this software without +specific prior written permission. + +-- 1..5 +THIS SOFTWARE IS PROVIDED +-- 1..128 +“AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL +-- 1..128 +BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/crates/zeta/license_patterns/isc-pattern b/crates/zeta/license_patterns/isc-pattern new file mode 100644 index 0000000000000000000000000000000000000000..8a47a1339f5c8462b96e5c77312451e2b324dcf0 --- /dev/null +++ b/crates/zeta/license_patterns/isc-pattern @@ -0,0 +1,12 @@ +-- 0..512 +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/crates/zeta/license_patterns/mit-pattern b/crates/zeta/license_patterns/mit-pattern new file mode 100644 index 0000000000000000000000000000000000000000..6e21baa00cbeb24d24e0da6eac3dac61a5355000 --- /dev/null +++ b/crates/zeta/license_patterns/mit-pattern @@ -0,0 +1,18 @@ +-- 0..512 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/zeta/license_patterns/upl-1.0-pattern b/crates/zeta/license_patterns/upl-1.0-pattern new file mode 100644 index 0000000000000000000000000000000000000000..da9d26ca3faf51654b7d42a734c88c8958a9ad8e --- /dev/null +++ b/crates/zeta/license_patterns/upl-1.0-pattern @@ -0,0 +1,32 @@ +-- 0..512 +Subject to the condition set forth below, permission is hereby granted to any person +obtaining a copy of this software, associated documentation and/or data (collectively +the "Software"), free of charge and under any and all copyright rights in the +Software, and any and all patent rights owned or freely licensable by each licensor +hereunder covering either (i) the unmodified Software as contributed to or provided +by such licensor, or (ii) the Larger Works (as defined below), to deal in both + +(a) the Software, and + +(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is + included with the Software (each a "Larger Work" to which the Software is + contributed by such licensors), + +without restriction, including without limitation the rights to copy, create +derivative works of, display, perform, and distribute the Software and make, use, +sell, offer for sale, import, export, have made, and have sold the Software and the +Larger Work(s), and to sublicense the foregoing rights on either these or other +terms. + +This license is subject to the following condition: + +The above copyright notice and either this complete permission notice or at a minimum +a reference to the UPL must be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/crates/zeta/license_patterns/zlib-pattern b/crates/zeta/license_patterns/zlib-pattern new file mode 100644 index 0000000000000000000000000000000000000000..121b5409ccd9be87eaed17673ee3ded74fcac76c --- /dev/null +++ b/crates/zeta/license_patterns/zlib-pattern @@ -0,0 +1,21 @@ +-- 0..512 +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +-- 1..5 +The origin of this software must not be misrepresented; you must not claim +that you wrote the original software. If you use this software in a product, +an acknowledgment in the product documentation would be appreciated but is +not required. + +-- 1..5 +Altered source versions must be plainly marked as such, and must not be +misrepresented as being the original software. + +-- 1..5 +This notice may not be removed or altered from any source distribution. diff --git a/crates/zeta/license_regexes/0bsd.regex b/crates/zeta/license_regexes/0bsd.regex deleted file mode 100644 index 15725f206a905fb0de1c2f03ec40dde25a1f01c4..0000000000000000000000000000000000000000 --- a/crates/zeta/license_regexes/0bsd.regex +++ /dev/null @@ -1,10 +0,0 @@ -.{0,512}Permission to use copy modify andor distribute this software for any -purpose with or without fee is hereby granted - -THE SOFTWARE IS PROVIDED AS IS AND THE AUTHOR DISCLAIMS ALL -WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS IN NO EVENT SHALL THE AUTHOR BE LIABLE -FOR ANY SPECIAL DIRECT INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY -DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE DATA OR PROFITS WHETHER IN -AN ACTION OF CONTRACT NEGLIGENCE OR OTHER TORTIOUS ACTION ARISING OUT -OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE diff --git a/crates/zeta/license_regexes/apache-2.0.regex b/crates/zeta/license_regexes/apache-2.0.regex deleted file mode 100644 index 26cbecf2ee299e957e18d9da5c467f7788874358..0000000000000000000000000000000000000000 --- a/crates/zeta/license_regexes/apache-2.0.regex +++ /dev/null @@ -1,223 +0,0 @@ -.{0,512}Licensed under the Apache License Version 20 the License -you may not use this file except in compliance with the License -You may obtain a copy of the License at - - https?wwwapacheorglicensesLICENSE20 - -Unless required by applicable law or agreed to in writing software -distributed under the License is distributed on an AS IS BASIS -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied -See the License for the specific language governing permissions and -limitations under the License|.{0,512}(?:Licensed under the Apache License Version 20 the License -you may not use this file except in compliance with the License -You may obtain a copy of the License at - - https?wwwapacheorglicensesLICENSE20 - -Unless required by applicable law or agreed to in writing software -distributed under the License is distributed on an AS IS BASIS -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied -See the License for the specific language governing permissions and -limitations under the License)? - - ?Apache License - Version 20 January 2004 - https?wwwapacheorglicenses - - TERMS AND CONDITIONS FOR USE REPRODUCTION AND DISTRIBUTION - - 1 Definitions - - License shall mean the terms and conditions for use reproduction - and distribution as defined by Sections 1 through 9 of this document - - Licensor shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License - - Legal Entity shall mean the union of the acting entity and all - other entities that control are controlled by or are under common - control with that entity For the purposes of this definition - control means i the power direct or indirect to cause the - direction or management of such entity whether by contract or - otherwise or ii ownership of fifty percent 50 or more of the - outstanding shares or iii beneficial ownership of such entity - - You or Your shall mean an individual or Legal Entity - exercising permissions granted by this License - - Source form shall mean the preferred form for making modifications - including but not limited to software source code documentation - source and configuration files - - Object form shall mean any form resulting from mechanical - transformation or translation of a Source form including but - not limited to compiled object code generated documentation - and conversions to other media types - - Work shall mean the work of authorship whether in Source or - Object form made available under the License as indicated by a - copyright notice that is included in or attached to the work - an example is provided in the Appendix below - - Derivative Works shall mean any work whether in Source or Object - form that is based on or derived from the Work and for which the - editorial revisions annotations elaborations or other modifications - represent as a whole an original work of authorship For the purposes - of this License Derivative Works shall not include works that remain - separable from or merely link or bind by name to the interfaces of - the Work and Derivative Works thereof - - Contribution shall mean any work of authorship including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner For the purposes of this definition submitted - means any form of electronic verbal or written communication sent - to the Licensor or its representatives including but not limited to - communication on electronic mailing lists source code control systems - and issue tracking systems that are managed by or on behalf of the - Licensor for the purpose of discussing and improving the Work but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as Not a Contribution - - Contributor shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work - - 2 Grant of Copyright License Subject to the terms and conditions of - this License each Contributor hereby grants to You a perpetual - worldwide nonexclusive nocharge royaltyfree irrevocable - copyright license to reproduce prepare Derivative Works of - publicly display publicly perform sublicense and distribute the - Work and such Derivative Works in Source or Object form - - 3 Grant of Patent License Subject to the terms and conditions of - this License each Contributor hereby grants to You a perpetual - worldwide nonexclusive nocharge royaltyfree irrevocable - except as stated in this section patent license to make have made - use offer to sell sell import and otherwise transfer the Work - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contributions alone or by combination of their Contributions - with the Work to which such Contributions was submitted If You - institute patent litigation against any entity including a - crossclaim or counterclaim in a lawsuit alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed - - 4 Redistribution You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium with or without - modifications and in Source or Object form provided that You - meet the following conditions - - (?:a )?You must give any other recipients of the Work or - Derivative Works a copy of this License and - - (?:b )?You must cause any modified files to carry prominent notices - stating that You changed the files and - - (?:c )?You must retain in the Source form of any Derivative Works - that You distribute all copyright patent trademark and - attribution notices from the Source form of the Work - excluding those notices that do not pertain to any part of - the Derivative Works and - - (?:d )?If the Work includes a NOTICE text file as part of its - distribution then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file excluding those notices that do not - pertain to any part of the Derivative Works in at least one - of the following places within a NOTICE text file distributed - as part of the Derivative Works within the Source form or - documentation if provided along with the Derivative Works or - within a display generated by the Derivative Works if and - wherever such thirdparty notices normally appear The contents - of the NOTICE file are for informational purposes only and - do not modify the License You may add Your own attribution - notices within Derivative Works that You distribute alongside - or as an addendum to the NOTICE text from the Work provided - that such additional attribution notices cannot be construed - as modifying the License - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use reproduction or distribution of Your modifications or - for any such Derivative Works as a whole provided Your use - reproduction and distribution of the Work otherwise complies with - the conditions stated in this License - - 5 Submission of Contributions Unless You explicitly state otherwise - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License without any additional terms or conditions - Notwithstanding the above nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions - - 6 Trademarks This License does not grant permission to use the trade - names trademarks service marks or product names of the Licensor - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file - - 7 Disclaimer of Warranty Unless required by applicable law or - agreed to in writing Licensor provides the Work and each - Contributor provides its Contributions on an AS IS BASIS - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or - implied including without limitation any warranties or conditions - of TITLE NONINFRINGEMENT MERCHANTABILITY or FITNESS FOR A - PARTICULAR PURPOSE You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License - - 8 Limitation of Liability In no event and under no legal theory - whether in tort including negligence contract or otherwise - unless required by applicable law such as deliberate and grossly - negligent acts or agreed to in writing shall any Contributor be - liable to You for damages including any direct indirect special - incidental or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work including but not limited to damages for loss of goodwill - work stoppage computer failure or malfunction or any and all - other commercial damages or losses even if such Contributor - has been advised of the possibility of such damages - - 9 Accepting Warranty or Additional Liability While redistributing - the Work or Derivative Works thereof You may choose to offer - and charge a fee for acceptance of support warranty indemnity - or other liability obligations andor rights consistent with this - License However in accepting such obligations You may act only - on Your own behalf and on Your sole responsibility not on behalf - of any other Contributor and only if You agree to indemnify - defend and hold each Contributor harmless for any liability - incurred by or claims asserted against such Contributor by reason - of your accepting any such warranty or additional liability(?: - - END OF TERMS AND CONDITIONS)?(?: - - APPENDIX How to apply the Apache License to your work - - To apply the Apache License to your work attach the following - boilerplate notice with the fields enclosed by brackets - replaced with your own identifying information Dont include - the brackets The text should be enclosed in the appropriate - comment syntax for the file format We also recommend that a - file or class name and description of purpose be included on the - same printed page as the copyright notice for easier - identification within thirdparty archives)?(?: - - Copyright.{0,512})?(?: - - Licensed under the Apache License Version 20 the License - you may not use this file except in compliance with the License - You may obtain a copy of the License at - - https?wwwapacheorglicensesLICENSE20 - - Unless required by applicable law or agreed to in writing software - distributed under the License is distributed on an AS IS BASIS - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND either express or implied - See the License for the specific language governing permissions and - limitations under the License)? diff --git a/crates/zeta/license_regexes/bsd.regex b/crates/zeta/license_regexes/bsd.regex deleted file mode 100644 index 655e38fa4336a9e3580ec8a0fc29bba4e5bd68ab..0000000000000000000000000000000000000000 --- a/crates/zeta/license_regexes/bsd.regex +++ /dev/null @@ -1,23 +0,0 @@ -.{0,512}Redistribution and use in source and binary forms with or without -modification are permitted provided that the following conditions are met - -(?:1 )?Redistributions of source code must retain the above copyright -notice this list of conditions and the following disclaimer(?: - -(?:2 )?Redistributions in binary form must reproduce the above copyright -notice this list of conditions and the following disclaimer in the -documentation andor other materials provided with the distribution(?: - -(?:3 )?.{0,128} may be used to endorse or -promote products derived from this software without specific prior written -permission)?)? - -THIS SOFTWARE IS PROVIDED BY .{0,128}AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES -INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED IN NO EVENT SHALL .{0,128}BE LIABLE -FOR ANY DIRECT INDIRECT INCIDENTAL SPECIAL EXEMPLARY OR CONSEQUENTIAL -DAMAGES INCLUDING BUT NOT LIMITED TO PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES LOSS OF USE DATA OR PROFITS OR BUSINESS INTERRUPTION HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY WHETHER IN CONTRACT STRICT LIABILITY OR -TORT INCLUDING NEGLIGENCE OR OTHERWISE ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE diff --git a/crates/zeta/license_regexes/isc.regex b/crates/zeta/license_regexes/isc.regex deleted file mode 100644 index ba3e3c9cbf8d1a5711485066c496fa89fb8f4c66..0000000000000000000000000000000000000000 --- a/crates/zeta/license_regexes/isc.regex +++ /dev/null @@ -1,12 +0,0 @@ -.{0,512}Permission to use copy modify andor distribute -this software for any purpose with or without fee is hereby granted provided -that the above copyright notice and this permission notice appear in all -copies - -THE SOFTWARE IS PROVIDED AS IS AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL DIRECT INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE DATA OR PROFITS WHETHER IN AN -ACTION OF CONTRACT NEGLIGENCE OR OTHER TORTIOUS ACTION ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE diff --git a/crates/zeta/license_regexes/mit.regex b/crates/zeta/license_regexes/mit.regex deleted file mode 100644 index a8fa7b3ee7a5656d74012790344e1204e75cefa4..0000000000000000000000000000000000000000 --- a/crates/zeta/license_regexes/mit.regex +++ /dev/null @@ -1,17 +0,0 @@ -.{0,512}Permission is hereby granted free of charge to any -person obtaining a copy of this software and associated documentation files -the Software to deal in the Software without restriction including -without limitation the rights to use copy modify merge publish distribute -sublicense andor sell copies of the Software and to permit persons to whom -the Software is furnished to do so subject to the following conditions - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software - -THE SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND EXPRESS OR -IMPLIED INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM DAMAGES OR OTHER -LIABILITY WHETHER IN AN ACTION OF CONTRACT TORT OR OTHERWISE ARISING FROM -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE diff --git a/crates/zeta/license_regexes/upl-1.0.regex b/crates/zeta/license_regexes/upl-1.0.regex deleted file mode 100644 index f86f5fa3ab76d4f99177923d16b0b5fe8d0f18c0..0000000000000000000000000000000000000000 --- a/crates/zeta/license_regexes/upl-1.0.regex +++ /dev/null @@ -1,32 +0,0 @@ -.{0,512}Subject to the condition set forth below permission is hereby granted to any -person obtaining a copy of this software associated documentation andor data -collectively the Software free of charge and under any and all copyright -rights in the Software and any and all patent rights owned or freely licensable -by each licensor hereunder covering either i the unmodified Software as -contributed to or provided by such licensor or ii the Larger Works as -defined below to deal in both - -a the Software and - -b any piece of software andor hardware listed in the lrgrwrkstxt file if one is - included with the Software each a Larger Work to which the Software is - contributed by such licensors - -without restriction including without limitation the rights to copy create -derivative works of display perform and distribute the Software and make use -sell offer for sale import export have made and have sold the Software and the -Larger Works and to sublicense the foregoing rights on either these or other -terms - -This license is subject to the following condition - -The above copyright notice and either this complete permission notice or at a minimum -a reference to the UPL must be included in all copies or substantial portions of the -Software - -THE SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND EXPRESS OR IMPLIED -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM DAMAGES OR OTHER LIABILITY WHETHER IN AN ACTION OF -CONTRACT TORT OR OTHERWISE ARISING FROM OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE diff --git a/crates/zeta/license_regexes/zlib.regex b/crates/zeta/license_regexes/zlib.regex deleted file mode 100644 index 63a688d08c0fbc5a24783c1f8a2462979927ca6c..0000000000000000000000000000000000000000 --- a/crates/zeta/license_regexes/zlib.regex +++ /dev/null @@ -1,18 +0,0 @@ -.{0,512}This software is provided asis without any express or implied -warranty In no event will the authors be held liable for any damages -arising from the use of this software - -Permission is granted to anyone to use this software for any purpose -including commercial applications and to alter it and redistribute it -freely subject to the following restrictions - -1? The origin of this software must not be misrepresented you must not -claim that you wrote the original software If you use this software -in a product an acknowledgment in the product documentation would be -appreciated but is not required - -2? Altered source versions must be plainly marked as such and must not be -misrepresented as being the original software - -3? This notice may not be removed or altered from any source -distribution diff --git a/crates/zeta/src/license_detection.rs b/crates/zeta/src/license_detection.rs index 81314477e5383450c089be5291e02ba3f8478ac4..2939f8a0c491422099e14ae7cc76997a9031e7a0 100644 --- a/crates/zeta/src/license_detection.rs +++ b/crates/zeta/src/license_detection.rs @@ -1,19 +1,20 @@ use std::{ collections::BTreeSet, fmt::{Display, Formatter}, + ops::Range, path::{Path, PathBuf}, sync::{Arc, LazyLock}, }; +use anyhow::{Result, anyhow}; use fs::Fs; use futures::StreamExt as _; use gpui::{App, AppContext as _, Entity, Subscription, Task}; use itertools::Itertools; use postage::watch; use project::Worktree; -use regex::Regex; use strum::VariantArray; -use util::ResultExt as _; +use util::{ResultExt as _, maybe}; use worktree::ChildEntriesOptions; /// Matches the most common license locations, with US and UK English spelling. @@ -70,68 +71,170 @@ impl OpenSourceLicense { } } - /// Regexes to match the license text. These regexes are expected to match the entire file. Also - /// note that `canonicalize_license_text` removes everything but alphanumeric ascii characters. - pub fn regex(&self) -> &'static str { + pub fn patterns(&self) -> &'static [&'static str] { match self { - OpenSourceLicense::Apache2_0 => include_str!("../license_regexes/apache-2.0.regex"), - OpenSourceLicense::BSDZero => include_str!("../license_regexes/0bsd.regex"), - OpenSourceLicense::BSD => include_str!("../license_regexes/bsd.regex"), - OpenSourceLicense::ISC => include_str!("../license_regexes/isc.regex"), - OpenSourceLicense::MIT => include_str!("../license_regexes/mit.regex"), - OpenSourceLicense::UPL1_0 => include_str!("../license_regexes/upl-1.0.regex"), - OpenSourceLicense::Zlib => include_str!("../license_regexes/zlib.regex"), + OpenSourceLicense::Apache2_0 => &[ + include_str!("../license_patterns/apache-2.0-pattern"), + include_str!("../license_patterns/apache-2.0-reference-pattern"), + ], + OpenSourceLicense::BSDZero => &[include_str!("../license_patterns/0bsd-pattern")], + OpenSourceLicense::BSD => &[include_str!("../license_patterns/bsd-pattern")], + OpenSourceLicense::ISC => &[include_str!("../license_patterns/isc-pattern")], + OpenSourceLicense::MIT => &[include_str!("../license_patterns/mit-pattern")], + OpenSourceLicense::UPL1_0 => &[include_str!("../license_patterns/upl-1.0-pattern")], + OpenSourceLicense::Zlib => &[include_str!("../license_patterns/zlib-pattern")], } } } -fn detect_license(license: &str) -> Option { - static LICENSE_REGEX: LazyLock = LazyLock::new(|| { - let mut regex_string = String::new(); - let mut is_first = true; - for license in OpenSourceLicense::VARIANTS { - if is_first { - regex_string.push_str("^(?:("); - is_first = false; - } else { - regex_string.push_str(")|("); - } - regex_string.push_str(&canonicalize_license_regex(license.regex())); +// TODO: Consider using databake or similar to not parse at runtime. +static LICENSE_PATTERNS: LazyLock = LazyLock::new(|| { + let mut approximate_max_length = 0; + let mut patterns = Vec::new(); + for license in OpenSourceLicense::VARIANTS { + for pattern in license.patterns() { + let (pattern, length) = parse_pattern(pattern).unwrap(); + patterns.push((*license, pattern)); + approximate_max_length = approximate_max_length.max(length); + } + } + LicensePatterns { + patterns, + approximate_max_length, + } +}); + +fn detect_license(text: &str) -> Option { + let text = canonicalize_license_text(text); + for (license, pattern) in LICENSE_PATTERNS.patterns.iter() { + log::trace!("Checking if license is {}", license); + if check_pattern(&pattern, &text) { + return Some(*license); } - regex_string.push_str("))$"); - let regex = Regex::new(®ex_string).unwrap(); - assert_eq!(regex.captures_len(), OpenSourceLicense::VARIANTS.len() + 1); - regex - }); - - LICENSE_REGEX - .captures(&canonicalize_license_text(license)) - .and_then(|captures| { - let license = OpenSourceLicense::VARIANTS - .iter() - .enumerate() - .find(|(index, _)| captures.get(index + 1).is_some()) - .map(|(_, license)| *license); - if license.is_none() { - log::error!("bug: open source license regex matched without any capture groups"); + } + + None +} + +struct LicensePatterns { + patterns: Vec<(OpenSourceLicense, Vec)>, + approximate_max_length: usize, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +struct PatternPart { + /// Indicates that matching `text` is optional. Skipping `match_any_chars` is conditional on + /// matching `text`. + optional: bool, + /// Indicates the number of characters that can be skipped before matching `text`. + match_any_chars: Range, + /// The text to match, may be empty. + text: String, +} + +/// Lines that start with "-- " begin a `PatternPart`. `-- 1..10` specifies `match_any_chars: +/// 1..10`. `-- 1..10 optional:` additionally specifies `optional: true`. It's a parse error for a +/// line to start with `--` without matching this format. +/// +/// Text that does not have `--` prefixes participate in the `text` field and are canonicalized by +/// lowercasing, replacing all runs of whitespace with a single space, and otherwise only keeping +/// ascii alphanumeric characters. +fn parse_pattern(pattern_source: &str) -> Result<(Vec, usize)> { + let mut pattern = Vec::new(); + let mut part = PatternPart::default(); + let mut approximate_max_length = 0; + for line in pattern_source.lines() { + if let Some(directive) = line.trim().strip_prefix("--") { + if part != PatternPart::default() { + pattern.push(part); + part = PatternPart::default(); + } + let valid = maybe!({ + let directive_chunks = directive.split_whitespace().collect::>(); + if !(1..=2).contains(&directive_chunks.len()) { + return None; + } + if directive_chunks.len() == 2 { + part.optional = true; + } + let range_chunks = directive_chunks[0].split("..").collect::>(); + if range_chunks.len() != 2 { + return None; + } + part.match_any_chars.start = range_chunks[0].parse::().ok()?; + part.match_any_chars.end = range_chunks[1].parse::().ok()?; + if part.match_any_chars.start > part.match_any_chars.end { + return None; + } + approximate_max_length += part.match_any_chars.end; + Some(()) + }); + if valid.is_none() { + return Err(anyhow!("Invalid pattern directive: {}", line)); } - license - }) + continue; + } + approximate_max_length += line.len() + 1; + let line = canonicalize_license_text(line); + if line.is_empty() { + continue; + } + if !part.text.is_empty() { + part.text.push(' '); + } + part.text.push_str(&line); + } + if part != PatternPart::default() { + pattern.push(part); + } + Ok((pattern, approximate_max_length)) } -/// Canonicalizes the whitespace of license text. -fn canonicalize_license_regex(license: &str) -> String { - license - .split_ascii_whitespace() - .join(" ") - .to_ascii_lowercase() +/// Checks a pattern against text by iterating over the pattern parts in reverse order, and checking +/// matches with the end of a prefix of the input. Assumes that `canonicalize_license_text` has +/// already been applied to the input. +fn check_pattern(pattern: &[PatternPart], input: &str) -> bool { + let mut input_ix = input.len(); + let mut match_any_chars = 0..0; + for part in pattern.iter().rev() { + if part.text.is_empty() { + match_any_chars.start += part.match_any_chars.start; + match_any_chars.end += part.match_any_chars.end; + continue; + } + let mut matched = false; + for skip_count in match_any_chars.start..=match_any_chars.end { + let end_ix = input_ix.saturating_sub(skip_count); + if end_ix < part.text.len() { + break; + } + if input[..end_ix].ends_with(&part.text) { + matched = true; + input_ix = end_ix - part.text.len(); + match_any_chars = part.match_any_chars.clone(); + break; + } + } + if !matched && !part.optional { + log::trace!( + "Failed to match pattern `...{}` against input `...{}`", + &part.text[part.text.len().saturating_sub(128)..], + &input[input_ix.saturating_sub(128)..] + ); + return false; + } + } + match_any_chars.contains(&input_ix) } -/// Canonicalizes the whitespace of license text. +/// Canonicalizes license text by removing all non-alphanumeric characters, lowercasing, and turning +/// runs of whitespace into a single space. Unicode alphanumeric characters are intentionally +/// preserved since these should cause license mismatch when not within a portion of the license +/// where arbitrary text is allowed. fn canonicalize_license_text(license: &str) -> String { license .chars() - .filter(|c| c.is_ascii_alphanumeric() || c.is_ascii_whitespace()) + .filter(|c| c.is_ascii_whitespace() || c.is_alphanumeric()) .map(|c| c.to_ascii_lowercase()) .collect::() .split_ascii_whitespace() @@ -218,7 +321,7 @@ impl LicenseDetectionWatcher { async fn is_path_eligible(fs: &Arc, abs_path: PathBuf) -> Option { log::debug!("checking if `{abs_path:?}` is an open source license"); - // Resolve symlinks so that the file size from metadata is correct. + // resolve symlinks so that the file size from metadata is correct let Some(abs_path) = fs.canonicalize(&abs_path).await.ok() else { log::debug!( "`{abs_path:?}` license file probably deleted (error canonicalizing the path)" @@ -226,8 +329,13 @@ impl LicenseDetectionWatcher { return None; }; let metadata = fs.metadata(&abs_path).await.log_err()??; - // If the license file is >32kb it's unlikely to legitimately match any eligible license. - if metadata.len > 32768 { + if metadata.len > LICENSE_PATTERNS.approximate_max_length as u64 { + log::debug!( + "`{abs_path:?}` license file was skipped \ + because its size of {} bytes was larger than the max size of {} bytes", + metadata.len, + LICENSE_PATTERNS.approximate_max_length + ); return None; } let text = fs.load(&abs_path).await.log_err()?; @@ -262,7 +370,6 @@ mod tests { use gpui::TestAppContext; use serde_json::json; use settings::{Settings as _, SettingsStore}; - use unindent::unindent; use worktree::WorktreeSettings; use super::*; @@ -275,25 +382,8 @@ mod tests { #[track_caller] fn assert_matches_license(text: &str, license: OpenSourceLicense) { - if detect_license(text) != Some(license) { - let license_regex_text = canonicalize_license_regex(license.regex()); - let license_regex = Regex::new(&format!("^{}$", license_regex_text)).unwrap(); - let text = canonicalize_license_text(text); - let matched_regex = license_regex.is_match(&text); - if matched_regex { - panic!( - "The following text matches the individual regex for {}, \ - but not the combined one:\n```license-text\n{}\n```\n", - license, text - ); - } else { - panic!( - "The following text doesn't match the regex for {}:\n\ - ```license-text\n{}\n```\n\n```regex\n{}\n```\n", - license, text, license_regex_text - ); - } - } + assert_eq!(detect_license(text), Some(license)); + assert!(text.len() < LICENSE_PATTERNS.approximate_max_length); } /* @@ -325,7 +415,8 @@ mod tests { continue; }; let path_string = entry.path().to_string_lossy().to_string(); - match detect_license(&contents) { + let license = detect_license(&contents); + match license { Some(license) => detected.push((license, path_string)), None => unrecognized.push(path_string), } @@ -348,87 +439,38 @@ mod tests { } */ - #[test] - fn test_no_unicode_in_regexes() { - for license in OpenSourceLicense::VARIANTS { - assert!( - !license.regex().contains(|c: char| !c.is_ascii()), - "{}.regex contains unicode", - license.spdx_identifier() - ); - } - } - #[test] fn test_apache_positive_detection() { assert_matches_license(APACHE_2_0_TXT, OpenSourceLicense::Apache2_0); - - let license_with_appendix = format!( - r#"{APACHE_2_0_TXT} - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License."# - ); - assert_matches_license(&license_with_appendix, OpenSourceLicense::Apache2_0); - - // Sometimes people fill in the appendix with copyright info. - let license_with_copyright = license_with_appendix.replace( - "Copyright [yyyy] [name of copyright owner]", - "Copyright 2025 John Doe", + assert_matches_license( + include_str!("../license_examples/apache-2.0-ex1.txt"), + OpenSourceLicense::Apache2_0, ); - assert!(license_with_copyright != license_with_appendix); - assert_matches_license(&license_with_copyright, OpenSourceLicense::Apache2_0); - assert_matches_license( - include_str!("../../../LICENSE-APACHE"), + include_str!("../license_examples/apache-2.0-ex2.txt"), OpenSourceLicense::Apache2_0, ); - assert_matches_license( - include_str!("../license_examples/apache-2.0-ex1.txt"), + include_str!("../license_examples/apache-2.0-ex3.txt"), OpenSourceLicense::Apache2_0, ); assert_matches_license( - include_str!("../license_examples/apache-2.0-ex2.txt"), + include_str!("../license_examples/apache-2.0-ex4.txt"), OpenSourceLicense::Apache2_0, ); assert_matches_license( - include_str!("../license_examples/apache-2.0-ex3.txt"), + include_str!("../../../LICENSE-APACHE"), OpenSourceLicense::Apache2_0, ); } #[test] fn test_apache_negative_detection() { - assert!( + assert_eq!( detect_license(&format!( "{APACHE_2_0_TXT}\n\nThe terms in this license are void if P=NP." - )) - .is_none() + )), + None ); } @@ -490,7 +532,7 @@ mod tests { This project is dual licensed under the ISC License and the MIT License."# ); - assert!(detect_license(&license_text).is_none()); + assert_eq!(detect_license(&license_text), None); } #[test] @@ -517,7 +559,7 @@ mod tests { This project is dual licensed under the MIT License and the Apache License, Version 2.0."# ); - assert!(detect_license(&license_text).is_none()); + assert_eq!(detect_license(&license_text), None); } #[test] @@ -533,7 +575,7 @@ mod tests { This project is dual licensed under the UPL License and the MIT License."# ); - assert!(detect_license(&license_text).is_none()); + assert_eq!(detect_license(&license_text), None); } #[test] @@ -614,44 +656,6 @@ mod tests { assert_eq!(canonicalize_license_text(input), expected); } - #[test] - fn test_license_detection_canonicalizes_whitespace() { - let mit_with_weird_spacing = unindent( - r#" - MIT License - - - Copyright (c) 2024 John Doe - - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - "# - .trim(), - ); - - assert_matches_license(&mit_with_weird_spacing, OpenSourceLicense::MIT); - } - fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); From fe0ab30e8fcdd66402131925c05fd38472d877ee Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Sun, 31 Aug 2025 16:14:57 +0800 Subject: [PATCH 461/744] Fix auto size rendering of SVG images in Markdown (#36663) Release Notes: - Fixed auto size rendering of SVG images in Markdown. ## Before image image ## After image image For GPUI example ``` cargo run -p gpui --example image ``` SCR-20250821-ojoy --- crates/gpui/examples/image/image.rs | 88 +++++++++++++++-------------- crates/gpui/src/assets.rs | 11 +++- crates/gpui/src/elements/img.rs | 23 ++++---- 3 files changed, 68 insertions(+), 54 deletions(-) diff --git a/crates/gpui/examples/image/image.rs b/crates/gpui/examples/image/image.rs index bd1708e8c453656b2b7047b428f3dc63409eddec..34a510f76db396a91a225dffe21fcec986a62e20 100644 --- a/crates/gpui/examples/image/image.rs +++ b/crates/gpui/examples/image/image.rs @@ -75,65 +75,71 @@ impl Render for ImageShowcase { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div() .id("main") + .bg(gpui::white()) .overflow_y_scroll() .p_5() .size_full() - .flex() - .flex_col() - .justify_center() - .items_center() - .gap_8() - .bg(rgb(0xffffff)) .child( div() .flex() - .flex_row() + .flex_col() .justify_center() .items_center() .gap_8() - .child(ImageContainer::new( - "Image loaded from a local file", - self.local_resource.clone(), - )) - .child(ImageContainer::new( - "Image loaded from a remote resource", - self.remote_resource.clone(), + .child(img( + "https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg", )) - .child(ImageContainer::new( - "Image loaded from an asset", - self.asset_resource.clone(), - )), - ) - .child( - div() - .flex() - .flex_row() - .gap_8() .child( div() - .flex_col() - .child("Auto Width") - .child(img("https://picsum.photos/800/400").h(px(180.))), + .flex() + .flex_row() + .justify_center() + .items_center() + .gap_8() + .child(ImageContainer::new( + "Image loaded from a local file", + self.local_resource.clone(), + )) + .child(ImageContainer::new( + "Image loaded from a remote resource", + self.remote_resource.clone(), + )) + .child(ImageContainer::new( + "Image loaded from an asset", + self.asset_resource.clone(), + )), + ) + .child( + div() + .flex() + .flex_row() + .gap_8() + .child( + div() + .flex_col() + .child("Auto Width") + .child(img("https://picsum.photos/800/400").h(px(180.))), + ) + .child( + div() + .flex_col() + .child("Auto Height") + .child(img("https://picsum.photos/800/400").w(px(180.))), + ), ) .child( div() + .flex() .flex_col() - .child("Auto Height") - .child(img("https://picsum.photos/800/400").w(px(180.))), + .justify_center() + .items_center() + .w_full() + .border_1() + .border_color(rgb(0xC0C0C0)) + .child("image with max width 100%") + .child(img("https://picsum.photos/800/400").max_w_full()), ), ) - .child( - div() - .flex() - .flex_col() - .justify_center() - .items_center() - .w_full() - .border_1() - .border_color(rgb(0xC0C0C0)) - .child("image with max width 100%") - .child(img("https://picsum.photos/800/400").max_w_full()), - ) } } diff --git a/crates/gpui/src/assets.rs b/crates/gpui/src/assets.rs index 70a07c11e9239c048f9eaede8cae31a79acf779c..8930b58f8d4fc0423b7d6f41755189a03d8b8b84 100644 --- a/crates/gpui/src/assets.rs +++ b/crates/gpui/src/assets.rs @@ -1,4 +1,4 @@ -use crate::{DevicePixels, Result, SharedString, Size, size}; +use crate::{DevicePixels, Pixels, Result, SharedString, Size, size}; use smallvec::SmallVec; use image::{Delay, Frame}; @@ -42,6 +42,8 @@ pub(crate) struct RenderImageParams { pub struct RenderImage { /// The ID associated with this image pub id: ImageId, + /// The scale factor of this image on render. + pub(crate) scale_factor: f32, data: SmallVec<[Frame; 1]>, } @@ -60,6 +62,7 @@ impl RenderImage { Self { id: ImageId(NEXT_ID.fetch_add(1, SeqCst)), + scale_factor: 1.0, data: data.into(), } } @@ -77,6 +80,12 @@ impl RenderImage { size(width.into(), height.into()) } + /// Get the size of this image, in pixels for display, adjusted for the scale factor. + pub(crate) fn render_size(&self, frame_index: usize) -> Size { + self.size(frame_index) + .map(|v| (v.0 as f32 / self.scale_factor).into()) + } + /// Get the delay of this frame from the previous pub fn delay(&self, frame_index: usize) -> Delay { self.data[frame_index].delay() diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 893860d7e1b781144b2d8de06ae2135420854ed7..40d1b5e44981b7cfd0de92ddbb10f2f715008c70 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -332,20 +332,18 @@ impl Element for Img { state.started_loading = None; } - let image_size = data.size(frame_index); - style.aspect_ratio = - Some(image_size.width.0 as f32 / image_size.height.0 as f32); + let image_size = data.render_size(frame_index); + style.aspect_ratio = Some(image_size.width / image_size.height); if let Length::Auto = style.size.width { style.size.width = match style.size.height { Length::Definite(DefiniteLength::Absolute( AbsoluteLength::Pixels(height), )) => Length::Definite( - px(image_size.width.0 as f32 * height.0 - / image_size.height.0 as f32) - .into(), + px(image_size.width.0 * height.0 / image_size.height.0) + .into(), ), - _ => Length::Definite(px(image_size.width.0 as f32).into()), + _ => Length::Definite(image_size.width.into()), }; } @@ -354,11 +352,10 @@ impl Element for Img { Length::Definite(DefiniteLength::Absolute( AbsoluteLength::Pixels(width), )) => Length::Definite( - px(image_size.height.0 as f32 * width.0 - / image_size.width.0 as f32) - .into(), + px(image_size.height.0 * width.0 / image_size.width.0) + .into(), ), - _ => Length::Definite(px(image_size.height.0 as f32).into()), + _ => Length::Definite(image_size.height.into()), }; } @@ -701,7 +698,9 @@ impl Asset for ImageAssetLoader { swap_rgba_pa_to_bgra(pixel); } - RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1)) + let mut image = RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1)); + image.scale_factor = SMOOTH_SVG_SCALE_FACTOR; + image }; Ok(Arc::new(data)) From e1155848962bcd04ea3ab7b2493a63e468d0ac6f Mon Sep 17 00:00:00 2001 From: Dan Dascalescu Date: Sun, 31 Aug 2025 11:19:25 +0300 Subject: [PATCH 462/744] docs: Copyedit debugger.md and clarify settings location (#36996) Release Notes: - N/A --- docs/src/debugger.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/src/debugger.md b/docs/src/debugger.md index 7cfbf63cd8266f7865e948d7da1997c1d81a1f95..b018ea904b2c480bfd5ae6b405d65fe355a5ec2e 100644 --- a/docs/src/debugger.md +++ b/docs/src/debugger.md @@ -78,11 +78,10 @@ While configuration fields are debug adapter-dependent, most adapters support th // The debug adapter that Zed should use to debug the program "adapter": "Example adapter name", // Request: - // - launch: Zed will launch the program if specified or shows a debug terminal with the right configuration - // - attach: Zed will attach to a running program to debug it or when the process_id is not specified we will show a process picker (only supported for node currently) + // - launch: Zed will launch the program if specified, or show a debug terminal with the right configuration + // - attach: Zed will attach to a running program to debug it, or when the process_id is not specified, will show a process picker (only supported for node currently) "request": "launch", - // program: The program that you want to debug - // This field supports path resolution with ~ or . symbols + // The program to debug. This field supports path resolution with ~ or . symbols. "program": "path_to_program", // cwd: defaults to the current working directory of your project ($ZED_WORKTREE_ROOT) "cwd": "$ZED_WORKTREE_ROOT" @@ -148,6 +147,8 @@ The debug adapter will then stop whenever an exception of a given kind occurs. W ## Settings +The settings for the debugger are grouped under the `debugger` key in `settings.json`: + - `dock`: Determines the position of the debug panel in the UI. - `stepping_granularity`: Determines the stepping granularity. - `save_breakpoints`: Whether the breakpoints should be reused across Zed sessions. From d80f13242b410af03e54a0bdbe9c6007b9feb6d8 Mon Sep 17 00:00:00 2001 From: Gerd Augsburg Date: Sun, 31 Aug 2025 10:26:28 +0200 Subject: [PATCH 463/744] Support for "Insert" from character key location (#37219) Release Notes: - Added support for the Insert-Key from a character key location for keyboard layouts like neo2 --- crates/gpui/src/platform/linux/platform.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 8bd89fc399cb8215748467090b973f3f4ee00759..196e5b65d04125ca90c588212c140d3a63345c2e 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -848,6 +848,7 @@ impl crate::Keystroke { Keysym::Down => "down".to_owned(), Keysym::Home => "home".to_owned(), Keysym::End => "end".to_owned(), + Keysym::Insert => "insert".to_owned(), _ => { let name = xkb::keysym_get_name(key_sym).to_lowercase(); From 1ca5e84019a1541943863f281f9b1804bba7dee1 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 31 Aug 2025 10:43:24 +0200 Subject: [PATCH 464/744] markdown: Add HTML `img` tag support (#36700) Closes #21992 Screenshot 2025-08-21 at 18 09 24 Code example: ```markdown # Html Tag Description of image # Html Tag with width and height Description of image # Html Tag with style attribute with width and height Description of image # Normal Tag ![alt text](https://picsum.photos/200/300) ``` Release Notes: - Markdown: Added HTML `` tag support --- Cargo.lock | 2 + crates/markdown_preview/Cargo.toml | 6 +- .../markdown_preview/src/markdown_elements.rs | 17 +- .../markdown_preview/src/markdown_parser.rs | 372 +++++++++++++++++- .../markdown_preview/src/markdown_renderer.rs | 137 ++++--- 5 files changed, 456 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab3b713a113a95183e5f394bae0f1a31301da3f1..6fc771894f589d98dec459bf877e8fa35f56f2aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9953,9 +9953,11 @@ dependencies = [ "editor", "fs", "gpui", + "html5ever 0.27.0", "language", "linkify", "log", + "markup5ever_rcdom", "pretty_assertions", "pulldown-cmark 0.12.2", "settings", diff --git a/crates/markdown_preview/Cargo.toml b/crates/markdown_preview/Cargo.toml index ebdd8a9eb6c0ffbe99f7c14d1e97b13b3a95d8a3..55646cdcf43617223665e9dc48f13c55f966d99d 100644 --- a/crates/markdown_preview/Cargo.toml +++ b/crates/markdown_preview/Cargo.toml @@ -19,19 +19,21 @@ anyhow.workspace = true async-recursion.workspace = true collections.workspace = true editor.workspace = true +fs.workspace = true gpui.workspace = true +html5ever.workspace = true language.workspace = true linkify.workspace = true log.workspace = true +markup5ever_rcdom.workspace = true pretty_assertions.workspace = true pulldown-cmark.workspace = true settings.workspace = true theme.workspace = true ui.workspace = true util.workspace = true -workspace.workspace = true workspace-hack.workspace = true -fs.workspace = true +workspace.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index a570e79f5344d0f35693072f82f947004e24ac65..560e468439efce22aa72d91054d68d491e125b23 100644 --- a/crates/markdown_preview/src/markdown_elements.rs +++ b/crates/markdown_preview/src/markdown_elements.rs @@ -1,5 +1,6 @@ use gpui::{ - FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle, UnderlineStyle, px, + DefiniteLength, FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle, + UnderlineStyle, px, }; use language::HighlightId; use std::{fmt::Display, ops::Range, path::PathBuf}; @@ -15,6 +16,7 @@ pub enum ParsedMarkdownElement { /// A paragraph of text and other inline elements. Paragraph(MarkdownParagraph), HorizontalRule(Range), + Image(Image), } impl ParsedMarkdownElement { @@ -30,6 +32,7 @@ impl ParsedMarkdownElement { MarkdownParagraphChunk::Image(image) => image.source_range.clone(), }, Self::HorizontalRule(range) => range.clone(), + Self::Image(image) => image.source_range.clone(), }) } @@ -290,6 +293,8 @@ pub struct Image { pub link: Link, pub source_range: Range, pub alt_text: Option, + pub width: Option, + pub height: Option, } impl Image { @@ -303,10 +308,20 @@ impl Image { source_range, link, alt_text: None, + width: None, + height: None, }) } pub fn set_alt_text(&mut self, alt_text: SharedString) { self.alt_text = Some(alt_text); } + + pub fn set_width(&mut self, width: DefiniteLength) { + self.width = Some(width); + } + + pub fn set_height(&mut self, height: DefiniteLength) { + self.height = Some(height); + } } diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index b51b98a2ed64c72d76a8ca6e7316b6866bdcd9fe..1b116c50d9820dc4fea9d6b2e5816543d75e7d52 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -1,10 +1,12 @@ use crate::markdown_elements::*; use async_recursion::async_recursion; use collections::FxHashMap; -use gpui::FontWeight; +use gpui::{DefiniteLength, FontWeight, px, relative}; +use html5ever::{ParseOpts, local_name, parse_document, tendril::TendrilSink}; use language::LanguageRegistry; +use markup5ever_rcdom::RcDom; use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd}; -use std::{ops::Range, path::PathBuf, sync::Arc, vec}; +use std::{cell::RefCell, collections::HashMap, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec}; pub async fn parse_markdown( markdown_input: &str, @@ -172,9 +174,14 @@ impl<'a> MarkdownParser<'a> { self.cursor += 1; - let code_block = self.parse_code_block(language).await; + let code_block = self.parse_code_block(language).await?; Some(vec![ParsedMarkdownElement::CodeBlock(code_block)]) } + Tag::HtmlBlock => { + self.cursor += 1; + + Some(self.parse_html_block().await) + } _ => None, }, Event::Rule => { @@ -378,7 +385,7 @@ impl<'a> MarkdownParser<'a> { TagEnd::Image => { if let Some(mut image) = image.take() { if !text.is_empty() { - image.alt_text = Some(std::mem::take(&mut text).into()); + image.set_alt_text(std::mem::take(&mut text).into()); } markdown_text_like.push(MarkdownParagraphChunk::Image(image)); } @@ -695,13 +702,22 @@ impl<'a> MarkdownParser<'a> { } } - async fn parse_code_block(&mut self, language: Option) -> ParsedMarkdownCodeBlock { - let (_event, source_range) = self.previous().unwrap(); + async fn parse_code_block( + &mut self, + language: Option, + ) -> Option { + let Some((_event, source_range)) = self.previous() else { + return None; + }; + let source_range = source_range.clone(); let mut code = String::new(); while !self.eof() { - let (current, _source_range) = self.current().unwrap(); + let Some((current, _source_range)) = self.current() else { + break; + }; + match current { Event::Text(text) => { code.push_str(text); @@ -734,23 +750,190 @@ impl<'a> MarkdownParser<'a> { None }; - ParsedMarkdownCodeBlock { + Some(ParsedMarkdownCodeBlock { source_range, contents: code.into(), language, highlights, + }) + } + + async fn parse_html_block(&mut self) -> Vec { + let mut elements = Vec::new(); + let Some((_event, _source_range)) = self.previous() else { + return elements; + }; + + while !self.eof() { + let Some((current, source_range)) = self.current() else { + break; + }; + let source_range = source_range.clone(); + match current { + Event::Html(html) => { + let mut cursor = std::io::Cursor::new(html.as_bytes()); + let Some(dom) = parse_document(RcDom::default(), ParseOpts::default()) + .from_utf8() + .read_from(&mut cursor) + .ok() + else { + self.cursor += 1; + continue; + }; + + self.cursor += 1; + + self.parse_html_node(source_range, &dom.document, &mut elements); + } + Event::End(TagEnd::CodeBlock) => { + self.cursor += 1; + break; + } + _ => { + break; + } + } + } + + elements + } + + fn parse_html_node( + &self, + source_range: Range, + node: &Rc, + elements: &mut Vec, + ) { + match &node.data { + markup5ever_rcdom::NodeData::Document => { + self.consume_children(source_range, node, elements); + } + markup5ever_rcdom::NodeData::Doctype { .. } => {} + markup5ever_rcdom::NodeData::Text { contents } => { + elements.push(ParsedMarkdownElement::Paragraph(vec![ + MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range, + contents: contents.borrow().to_string(), + highlights: Vec::default(), + region_ranges: Vec::default(), + regions: Vec::default(), + }), + ])); + } + markup5ever_rcdom::NodeData::Comment { .. } => {} + markup5ever_rcdom::NodeData::Element { name, attrs, .. } => { + if local_name!("img") == name.local { + if let Some(image) = self.extract_image(source_range, attrs) { + elements.push(ParsedMarkdownElement::Image(image)); + } + } else { + self.consume_children(source_range, node, elements); + } + } + markup5ever_rcdom::NodeData::ProcessingInstruction { .. } => {} + } + } + + fn consume_children( + &self, + source_range: Range, + node: &Rc, + elements: &mut Vec, + ) { + for node in node.children.borrow().iter() { + self.parse_html_node(source_range.clone(), node, elements); + } + } + + fn attr_value( + attrs: &RefCell>, + name: html5ever::LocalName, + ) -> Option { + attrs.borrow().iter().find_map(|attr| { + if attr.name.local == name { + Some(attr.value.to_string()) + } else { + None + } + }) + } + + fn extract_styles_from_attributes( + attrs: &RefCell>, + ) -> HashMap { + let mut styles = HashMap::new(); + + if let Some(style) = Self::attr_value(attrs, local_name!("style")) { + for decl in style.split(';') { + let mut parts = decl.splitn(2, ':'); + if let Some((key, value)) = parts.next().zip(parts.next()) { + styles.insert( + key.trim().to_lowercase().to_string(), + value.trim().to_string(), + ); + } + } + } + + styles + } + + fn extract_image( + &self, + source_range: Range, + attrs: &RefCell>, + ) -> Option { + let src = Self::attr_value(attrs, local_name!("src"))?; + + let mut image = Image::identify(src, source_range, self.file_location_directory.clone())?; + + if let Some(alt) = Self::attr_value(attrs, local_name!("alt")) { + image.set_alt_text(alt.into()); + } + + let styles = Self::extract_styles_from_attributes(attrs); + + if let Some(width) = Self::attr_value(attrs, local_name!("width")) + .or_else(|| styles.get("width").cloned()) + .and_then(|width| Self::parse_length(&width)) + { + image.set_width(width); + } + + if let Some(height) = Self::attr_value(attrs, local_name!("height")) + .or_else(|| styles.get("height").cloned()) + .and_then(|height| Self::parse_length(&height)) + { + image.set_height(height); + } + + Some(image) + } + + /// Parses the width/height attribute value of an html element (e.g. img element) + fn parse_length(value: &str) -> Option { + if value.ends_with("%") { + value + .trim_end_matches("%") + .parse::() + .ok() + .map(|value| relative(value / 100.)) + } else { + value + .trim_end_matches("px") + .parse() + .ok() + .map(|value| px(value).into()) } } } #[cfg(test)] mod tests { - use core::panic; - use super::*; - use ParsedMarkdownListItemType::*; - use gpui::BackgroundExecutor; + use core::panic; + use gpui::{AbsoluteLength, BackgroundExecutor, DefiniteLength}; use language::{ HighlightId, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, tree_sitter_rust, }; @@ -925,6 +1108,8 @@ mod tests { url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(), }, alt_text: Some("test".into()), + height: None, + width: None, },) ); } @@ -946,6 +1131,8 @@ mod tests { url: "http://example.com/foo.png".to_string(), }, alt_text: None, + height: None, + width: None, },) ); } @@ -965,6 +1152,8 @@ mod tests { url: "http://example.com/foo.png".to_string(), }, alt_text: Some("foo bar baz".into()), + height: None, + width: None, }),], ); } @@ -990,6 +1179,8 @@ mod tests { url: "http://example.com/foo.png".to_string(), }, alt_text: Some("foo".into()), + height: None, + width: None, }), MarkdownParagraphChunk::Text(ParsedMarkdownText { source_range: 0..81, @@ -1004,11 +1195,168 @@ mod tests { url: "http://example.com/bar.png".to_string(), }, alt_text: Some("bar".into()), + height: None, + width: None, }) ] ); } + #[test] + fn test_parse_length() { + // Test percentage values + assert_eq!( + MarkdownParser::parse_length("50%"), + Some(DefiniteLength::Fraction(0.5)) + ); + assert_eq!( + MarkdownParser::parse_length("100%"), + Some(DefiniteLength::Fraction(1.0)) + ); + assert_eq!( + MarkdownParser::parse_length("25%"), + Some(DefiniteLength::Fraction(0.25)) + ); + assert_eq!( + MarkdownParser::parse_length("0%"), + Some(DefiniteLength::Fraction(0.0)) + ); + + // Test pixel values + assert_eq!( + MarkdownParser::parse_length("100px"), + Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0)))) + ); + assert_eq!( + MarkdownParser::parse_length("50px"), + Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(50.0)))) + ); + assert_eq!( + MarkdownParser::parse_length("0px"), + Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(0.0)))) + ); + + // Test values without units (should be treated as pixels) + assert_eq!( + MarkdownParser::parse_length("100"), + Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0)))) + ); + assert_eq!( + MarkdownParser::parse_length("42"), + Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0)))) + ); + + // Test invalid values + assert_eq!(MarkdownParser::parse_length("invalid"), None); + assert_eq!(MarkdownParser::parse_length("px"), None); + assert_eq!(MarkdownParser::parse_length("%"), None); + assert_eq!(MarkdownParser::parse_length(""), None); + assert_eq!(MarkdownParser::parse_length("abc%"), None); + assert_eq!(MarkdownParser::parse_length("abcpx"), None); + + // Test decimal values + assert_eq!( + MarkdownParser::parse_length("50.5%"), + Some(DefiniteLength::Fraction(0.505)) + ); + assert_eq!( + MarkdownParser::parse_length("100.25px"), + Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.25)))) + ); + assert_eq!( + MarkdownParser::parse_length("42.0"), + Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0)))) + ); + } + + #[gpui::test] + async fn test_html_image_tag() { + let parsed = parse("").await; + + let ParsedMarkdownElement::Image(image) = &parsed.children[0] else { + panic!("Expected a image element"); + }; + assert_eq!( + image.clone(), + Image { + source_range: 0..40, + link: Link::Web { + url: "http://example.com/foo.png".to_string(), + }, + alt_text: None, + height: None, + width: None, + }, + ); + } + + #[gpui::test] + async fn test_html_image_tag_with_alt_text() { + let parsed = parse("\"Foo\"").await; + + let ParsedMarkdownElement::Image(image) = &parsed.children[0] else { + panic!("Expected a image element"); + }; + assert_eq!( + image.clone(), + Image { + source_range: 0..50, + link: Link::Web { + url: "http://example.com/foo.png".to_string(), + }, + alt_text: Some("Foo".into()), + height: None, + width: None, + }, + ); + } + + #[gpui::test] + async fn test_html_image_tag_with_height_and_width() { + let parsed = + parse("").await; + + let ParsedMarkdownElement::Image(image) = &parsed.children[0] else { + panic!("Expected a image element"); + }; + assert_eq!( + image.clone(), + Image { + source_range: 0..65, + link: Link::Web { + url: "http://example.com/foo.png".to_string(), + }, + alt_text: None, + height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))), + width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))), + }, + ); + } + + #[gpui::test] + async fn test_html_image_style_tag_with_height_and_width() { + let parsed = parse( + "", + ) + .await; + + let ParsedMarkdownElement::Image(image) = &parsed.children[0] else { + panic!("Expected a image element"); + }; + assert_eq!( + image.clone(), + Image { + source_range: 0..75, + link: Link::Web { + url: "http://example.com/foo.png".to_string(), + }, + alt_text: None, + height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))), + width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))), + }, + ); + } + #[gpui::test] async fn test_header_only_table() { let markdown = "\ diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index b0b10e927cb3bbc4f0b8366cc77b091c9df773d2..b07b4686a4eaebdfaef804ba903b6575f56ae479 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -1,5 +1,5 @@ use crate::markdown_elements::{ - HeadingLevel, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown, + HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable, ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, @@ -164,6 +164,7 @@ pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderConte BlockQuote(block_quote) => render_markdown_block_quote(block_quote, cx), CodeBlock(code_block) => render_markdown_code_block(code_block, cx), HorizontalRule(_) => render_markdown_rule(cx), + Image(image) => render_markdown_image(image, cx), } } @@ -722,65 +723,7 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) } MarkdownParagraphChunk::Image(image) => { - let image_resource = match image.link.clone() { - Link::Web { url } => Resource::Uri(url.into()), - Link::Path { path, .. } => Resource::Path(Arc::from(path)), - }; - - let element_id = cx.next_id(&image.source_range); - - let image_element = div() - .id(element_id) - .cursor_pointer() - .child( - img(ImageSource::Resource(image_resource)) - .max_w_full() - .with_fallback({ - let alt_text = image.alt_text.clone(); - move || div().children(alt_text.clone()).into_any_element() - }), - ) - .tooltip({ - let link = image.link.clone(); - move |_, cx| { - InteractiveMarkdownElementTooltip::new( - Some(link.to_string()), - "open image", - cx, - ) - .into() - } - }) - .on_click({ - let workspace = workspace_clone.clone(); - let link = image.link.clone(); - move |_, window, cx| { - if window.modifiers().secondary() { - match &link { - Link::Web { url } => cx.open_url(url), - Link::Path { path, .. } => { - if let Some(workspace) = &workspace { - _ = workspace.update(cx, |workspace, cx| { - workspace - .open_abs_path( - path.clone(), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ) - .detach(); - }); - } - } - } - } - } - }) - .into_any(); - any_element.push(image_element); + any_element.push(render_markdown_image(image, cx)); } } } @@ -793,18 +736,86 @@ fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement { div().py(cx.scaled_rems(0.5)).child(rule).into_any() } +fn render_markdown_image(image: &Image, cx: &mut RenderContext) -> AnyElement { + let image_resource = match image.link.clone() { + Link::Web { url } => Resource::Uri(url.into()), + Link::Path { path, .. } => Resource::Path(Arc::from(path)), + }; + + let element_id = cx.next_id(&image.source_range); + let workspace = cx.workspace.clone(); + + div() + .id(element_id) + .cursor_pointer() + .child( + img(ImageSource::Resource(image_resource)) + .max_w_full() + .with_fallback({ + let alt_text = image.alt_text.clone(); + move || div().children(alt_text.clone()).into_any_element() + }) + .when_some(image.height, |this, height| this.h(height)) + .when_some(image.width, |this, width| this.w(width)), + ) + .tooltip({ + let link = image.link.clone(); + let alt_text = image.alt_text.clone(); + move |_, cx| { + InteractiveMarkdownElementTooltip::new( + Some(alt_text.clone().unwrap_or(link.to_string().into())), + "open image", + cx, + ) + .into() + } + }) + .on_click({ + let link = image.link.clone(); + move |_, window, cx| { + if window.modifiers().secondary() { + match &link { + Link::Web { url } => cx.open_url(url), + Link::Path { path, .. } => { + if let Some(workspace) = &workspace { + _ = workspace.update(cx, |workspace, cx| { + workspace + .open_abs_path( + path.clone(), + OpenOptions { + visible: Some(OpenVisible::None), + ..Default::default() + }, + window, + cx, + ) + .detach(); + }); + } + } + } + } + } + }) + .into_any() +} + struct InteractiveMarkdownElementTooltip { tooltip_text: Option, - action_text: String, + action_text: SharedString, } impl InteractiveMarkdownElementTooltip { - pub fn new(tooltip_text: Option, action_text: &str, cx: &mut App) -> Entity { + pub fn new( + tooltip_text: Option, + action_text: impl Into, + cx: &mut App, + ) -> Entity { let tooltip_text = tooltip_text.map(|t| util::truncate_and_trailoff(&t, 50).into()); cx.new(|_cx| Self { tooltip_text, - action_text: action_text.to_string(), + action_text: action_text.into(), }) } } From f348737e8cfac9da2b6579ee7ce86ae788cc09c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 31 Aug 2025 08:54:22 +0000 Subject: [PATCH 465/744] Update Rust crate tracing-subscriber to v0.3.20 [SECURITY] (#37195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [tracing-subscriber](https://tokio.rs) ([source](https://redirect.github.com/tokio-rs/tracing)) | dependencies | patch | `0.3.19` -> `0.3.20` | ### GitHub Vulnerability Alerts #### [CVE-2025-58160](https://redirect.github.com/tokio-rs/tracing/security/advisories/GHSA-xwfj-jgwm-7wp5) ### Impact Previous versions of tracing-subscriber were vulnerable to ANSI escape sequence injection attacks. Untrusted user input containing ANSI escape sequences could be injected into terminal output when logged, potentially allowing attackers to: - Manipulate terminal title bars - Clear screens or modify terminal display - Potentially mislead users through terminal manipulation In isolation, impact is minimal, however security issues have been found in terminal emulators that enabled an attacker to use ANSI escape sequences via logs to exploit vulnerabilities in the terminal emulator. ### Patches `tracing-subscriber` version 0.3.20 fixes this vulnerability by escaping ANSI control characters in when writing events to destinations that may be printed to the terminal. ### Workarounds Avoid printing logs to terminal emulators without escaping ANSI control sequences. ### References https://www.packetlabs.net/posts/weaponizing-ansi-escape-sequences/ ### Acknowledgments We would like to thank [zefr0x](http://github.com/zefr0x) who responsibly reported the issue at `security@tokio.rs`. If you believe you have found a security vulnerability in any tokio-rs project, please email us at `security@tokio.rs`. --- ### Release Notes
tokio-rs/tracing (tracing-subscriber) ### [`v0.3.20`](https://redirect.github.com/tokio-rs/tracing/releases/tag/tracing-subscriber-0.3.20): tracing-subscriber 0.3.20 [Compare Source](https://redirect.github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20) **Security Fix**: ANSI Escape Sequence Injection (CVE-TBD) #### Impact Previous versions of tracing-subscriber were vulnerable to ANSI escape sequence injection attacks. Untrusted user input containing ANSI escape sequences could be injected into terminal output when logged, potentially allowing attackers to: - Manipulate terminal title bars - Clear screens or modify terminal display - Potentially mislead users through terminal manipulation In isolation, impact is minimal, however security issues have been found in terminal emulators that enabled an attacker to use ANSI escape sequences via logs to exploit vulnerabilities in the terminal emulator. #### Solution Version 0.3.20 fixes this vulnerability by escaping ANSI control characters in when writing events to destinations that may be printed to the terminal. #### Affected Versions All versions of tracing-subscriber prior to 0.3.20 are affected by this vulnerability. #### Recommendations Immediate Action Required: We recommend upgrading to tracing-subscriber 0.3.20 immediately, especially if your application: - Logs user-provided input (form data, HTTP headers, query parameters, etc.) - Runs in environments where terminal output is displayed to users #### Migration This is a patch release with no breaking API changes. Simply update your Cargo.toml: ```toml [dependencies] tracing-subscriber = "0.3.20" ``` #### Acknowledgments We would like to thank [zefr0x](http://github.com/zefr0x) who responsibly reported the issue at `security@tokio.rs`. If you believe you have found a security vulnerability in any tokio-rs project, please email us at `security@tokio.rs`.
--- ### Configuration 📅 **Schedule**: Branch creation - "" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- Release Notes: - N/A --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Kirill Bulatov --- Cargo.lock | 81 ++++++++++--------------------- tooling/workspace-hack/Cargo.toml | 4 +- 2 files changed, 27 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6fc771894f589d98dec459bf877e8fa35f56f2aa..fed7077281333f53f4a9ce7b746227e3369d663b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,7 +507,7 @@ dependencies = [ "parking_lot", "piper", "polling", - "regex-automata 0.4.9", + "regex-automata", "rustix-openpty", "serde", "signal-hook", @@ -2457,7 +2457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", - "regex-automata 0.4.9", + "regex-automata", "serde", ] @@ -4732,7 +4732,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291" dependencies = [ - "nu-ansi-term 0.50.1", + "nu-ansi-term", ] [[package]] @@ -5631,8 +5631,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" dependencies = [ "bit-set 0.5.3", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] @@ -5642,8 +5642,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" dependencies = [ "bit-set 0.8.0", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] @@ -7293,8 +7293,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] @@ -8299,7 +8299,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata 0.4.9", + "regex-automata", "same-file", "walkdir", "winapi-util", @@ -8898,7 +8898,7 @@ dependencies = [ "percent-encoding", "referencing", "regex", - "regex-syntax 0.8.5", + "regex-syntax", "reqwest 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", @@ -9738,7 +9738,7 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", - "regex-syntax 0.8.5", + "regex-syntax", "rustc_version", "syn 2.0.101", ] @@ -10018,11 +10018,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -10723,16 +10723,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "nu-ansi-term" version = "0.50.1" @@ -11426,12 +11416,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.11.1" @@ -13422,17 +13406,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -13443,7 +13418,7 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] @@ -13452,12 +13427,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -17147,14 +17116,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", - "nu-ansi-term 0.46.0", + "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "serde", "serde_json", "sharded-slab", @@ -17185,7 +17154,7 @@ checksum = "a7cf18d43cbf0bfca51f657132cc616a5097edc4424d538bae6fa60142eaf9f0" dependencies = [ "cc", "regex", - "regex-syntax 0.8.5", + "regex-syntax", "serde_json", "streaming-iterator", "tree-sitter-language", @@ -19983,8 +19952,8 @@ dependencies = [ "rand_core 0.6.4", "regalloc2", "regex", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", "ring", "rust_decimal", "rustc-hash 1.1.0", diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 2f9a963abc8b09d2255a5229dd2e44e06b2e8c9f..9bcaabb8cc942818fab9b3a454a0858f70be6bf2 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -568,7 +568,7 @@ tokio-rustls = { version = "0.26", default-features = false, features = ["loggin tokio-socks = { version = "0.5", features = ["futures-io"] } tokio-stream = { version = "0.1", features = ["fs"] } tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } -winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "errhandlingapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] } +winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] } windows-core = { version = "0.61" } windows-numerics = { version = "0.2" } windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } @@ -592,7 +592,7 @@ tokio-rustls = { version = "0.26", default-features = false, features = ["loggin tokio-socks = { version = "0.5", features = ["futures-io"] } tokio-stream = { version = "0.1", features = ["fs"] } tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } -winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "errhandlingapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] } +winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] } windows-core = { version = "0.61" } windows-numerics = { version = "0.2" } windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } From b69ebbd7b797117ffcd36d45a856cf4c1705d197 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 31 Aug 2025 13:19:12 +0300 Subject: [PATCH 466/744] Bump pnpm dependencies (#37258) Takes care of https://github.com/zed-industries/zed/security/dependabot/64 Release Notes: - N/A --- script/danger/pnpm-lock.yaml | 64 ++-- script/issue_response/package.json | 10 +- script/issue_response/pnpm-lock.yaml | 418 +++++++++++++++------------ 3 files changed, 264 insertions(+), 228 deletions(-) diff --git a/script/danger/pnpm-lock.yaml b/script/danger/pnpm-lock.yaml index f2739779e2bb1ea71b2cf14cf8e0940458745330..fd6b3f66acb627d57520e4ca928cc8ce2793b4b9 100644 --- a/script/danger/pnpm-lock.yaml +++ b/script/danger/pnpm-lock.yaml @@ -33,8 +33,8 @@ packages: resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} engines: {node: '>= 18'} - '@octokit/core@5.2.1': - resolution: {integrity: sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==} + '@octokit/core@5.2.2': + resolution: {integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==} engines: {node: '>= 18'} '@octokit/endpoint@9.0.6': @@ -131,8 +131,8 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - core-js@3.41.0: - resolution: {integrity: sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==} + core-js@3.45.1: + resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==} danger-plugin-pr-hygiene@0.6.1: resolution: {integrity: sha512-nb+iUQvirE3BlKXI1WoOND6sujyGzHar590mJm5tt4RLi65HXFaU5hqONxgDoWFujJNHYnXse9yaZdxnxEi4QA==} @@ -142,8 +142,8 @@ packages: engines: {node: '>=18'} hasBin: true - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -252,8 +252,8 @@ packages: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} - jwa@1.4.1: - resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} jws@3.2.2: resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} @@ -385,8 +385,8 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} hasBin: true @@ -460,7 +460,7 @@ snapshots: '@octokit/auth-token@4.0.0': {} - '@octokit/core@5.2.1': + '@octokit/core@5.2.2': dependencies: '@octokit/auth-token': 4.0.0 '@octokit/graphql': 7.1.1 @@ -483,18 +483,18 @@ snapshots: '@octokit/openapi-types@24.2.0': {} - '@octokit/plugin-paginate-rest@11.4.4-cjs.2(@octokit/core@5.2.1)': + '@octokit/plugin-paginate-rest@11.4.4-cjs.2(@octokit/core@5.2.2)': dependencies: - '@octokit/core': 5.2.1 + '@octokit/core': 5.2.2 '@octokit/types': 13.10.0 - '@octokit/plugin-request-log@4.0.1(@octokit/core@5.2.1)': + '@octokit/plugin-request-log@4.0.1(@octokit/core@5.2.2)': dependencies: - '@octokit/core': 5.2.1 + '@octokit/core': 5.2.2 - '@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1(@octokit/core@5.2.1)': + '@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1(@octokit/core@5.2.2)': dependencies: - '@octokit/core': 5.2.1 + '@octokit/core': 5.2.2 '@octokit/types': 13.10.0 '@octokit/request-error@5.1.1': @@ -512,10 +512,10 @@ snapshots: '@octokit/rest@20.1.2': dependencies: - '@octokit/core': 5.2.1 - '@octokit/plugin-paginate-rest': 11.4.4-cjs.2(@octokit/core@5.2.1) - '@octokit/plugin-request-log': 4.0.1(@octokit/core@5.2.1) - '@octokit/plugin-rest-endpoint-methods': 13.3.2-cjs.1(@octokit/core@5.2.1) + '@octokit/core': 5.2.2 + '@octokit/plugin-paginate-rest': 11.4.4-cjs.2(@octokit/core@5.2.2) + '@octokit/plugin-request-log': 4.0.1(@octokit/core@5.2.2) + '@octokit/plugin-rest-endpoint-methods': 13.3.2-cjs.1(@octokit/core@5.2.2) '@octokit/types@13.10.0': dependencies: @@ -525,7 +525,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -571,7 +571,7 @@ snapshots: commander@2.20.3: {} - core-js@3.41.0: {} + core-js@3.45.1: {} danger-plugin-pr-hygiene@0.6.1: {} @@ -582,8 +582,8 @@ snapshots: async-retry: 1.2.3 chalk: 2.4.2 commander: 2.20.3 - core-js: 3.41.0 - debug: 4.4.0 + core-js: 3.45.1 + debug: 4.4.1 fast-json-patch: 3.1.1 get-stdin: 6.0.0 http-proxy-agent: 5.0.0 @@ -618,7 +618,7 @@ snapshots: - encoding - supports-color - debug@4.4.0: + debug@4.4.1: dependencies: ms: 2.1.3 @@ -688,14 +688,14 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -720,9 +720,9 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.7.1 + semver: 7.7.2 - jwa@1.4.1: + jwa@1.4.2: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 @@ -730,7 +730,7 @@ snapshots: jws@3.2.2: dependencies: - jwa: 1.4.1 + jwa: 1.4.2 safe-buffer: 5.2.1 lodash.find@4.6.0: {} @@ -823,7 +823,7 @@ snapshots: safe-buffer@5.2.1: {} - semver@7.7.1: {} + semver@7.7.2: {} side-channel-list@1.0.0: dependencies: diff --git a/script/issue_response/package.json b/script/issue_response/package.json index 0f3715ef27eda3ff7a3e35b84e42adc7fbbf5e16..70696bc4b808868143ca2ffd94f782d24da7d05b 100644 --- a/script/issue_response/package.json +++ b/script/issue_response/package.json @@ -9,14 +9,14 @@ "start": "node main.js" }, "dependencies": { - "@octokit/rest": "^21.1.0", - "@slack/webhook": "^7.0.4", + "@octokit/rest": "^21.1.1", + "@slack/webhook": "^7.0.6", "date-fns": "^4.1.0", - "octokit": "^4.1.1" + "octokit": "^4.1.4" }, "devDependencies": { - "@octokit/types": "^13.8.0", - "@slack/types": "^2.14.0", + "@octokit/types": "^13.10.0", + "@slack/types": "^2.16.0", "@tsconfig/node20": "20.1.5", "@tsconfig/strictest": "2.0.5", "typescript": "5.7.3" diff --git a/script/issue_response/pnpm-lock.yaml b/script/issue_response/pnpm-lock.yaml index 7286c36467cc0e02441925337c8b849102dd6a83..a42e2460758b4c28b68c24065916140edf2c8404 100644 --- a/script/issue_response/pnpm-lock.yaml +++ b/script/issue_response/pnpm-lock.yaml @@ -9,24 +9,24 @@ importers: .: dependencies: '@octokit/rest': - specifier: ^21.1.0 + specifier: ^21.1.1 version: 21.1.1 '@slack/webhook': - specifier: ^7.0.4 - version: 7.0.5 + specifier: ^7.0.6 + version: 7.0.6 date-fns: specifier: ^4.1.0 version: 4.1.0 octokit: - specifier: ^4.1.1 - version: 4.1.2 + specifier: ^4.1.4 + version: 4.1.4 devDependencies: '@octokit/types': - specifier: ^13.8.0 - version: 13.8.0 + specifier: ^13.10.0 + version: 13.10.0 '@slack/types': - specifier: ^2.14.0 - version: 2.14.0 + specifier: ^2.16.0 + version: 2.16.0 '@tsconfig/node20': specifier: 20.1.5 version: 20.1.5 @@ -39,44 +39,44 @@ importers: packages: - '@octokit/app@15.1.4': - resolution: {integrity: sha512-PM1MqlPAnItjQIKWRmSoJu02+m7Eif4Am3w5C+Ctkw0//QETWMbW2ejBZhcw3aS7wRcFSbS+lH3NoYm614aZVQ==} + '@octokit/app@15.1.6': + resolution: {integrity: sha512-WELCamoCJo9SN0lf3SWZccf68CF0sBNPQuLYmZ/n87p5qvBJDe9aBtr5dHkh7T9nxWZ608pizwsUbypSzZAiUw==} engines: {node: '>= 18'} - '@octokit/auth-app@7.1.5': - resolution: {integrity: sha512-boklS4E6LpbA3nRx+SU2fRKRGZJdOGoSZne/i3Y0B5rfHOcGwFgcXrwDLdtbv4igfDSnAkZaoNBv1GYjPDKRNw==} + '@octokit/auth-app@7.2.2': + resolution: {integrity: sha512-p6hJtEyQDCJEPN9ijjhEC/kpFHMHN4Gca9r+8S0S8EJi7NaWftaEmexjxxpT1DFBeJpN4u/5RE22ArnyypupJw==} engines: {node: '>= 18'} - '@octokit/auth-oauth-app@8.1.3': - resolution: {integrity: sha512-4e6OjVe5rZ8yBe8w7byBjpKtSXFuro7gqeGAAZc7QYltOF8wB93rJl2FE0a4U1Mt88xxPv/mS+25/0DuLk0Ewg==} + '@octokit/auth-oauth-app@8.1.4': + resolution: {integrity: sha512-71iBa5SflSXcclk/OL3lJzdt4iFs56OJdpBGEBl1wULp7C58uiswZLV6TdRaiAzHP1LT8ezpbHlKuxADb+4NkQ==} engines: {node: '>= 18'} - '@octokit/auth-oauth-device@7.1.3': - resolution: {integrity: sha512-BECO/N4B/Uikj0w3GCvjf/odMujtYTP3q82BJSjxC2J3rxTEiZIJ+z2xnRlDb0IE9dQSaTgRqUPVOieSbFcVzg==} + '@octokit/auth-oauth-device@7.1.5': + resolution: {integrity: sha512-lR00+k7+N6xeECj0JuXeULQ2TSBB/zjTAmNF2+vyGPDEFx1dgk1hTDmL13MjbSmzusuAmuJD8Pu39rjp9jH6yw==} engines: {node: '>= 18'} - '@octokit/auth-oauth-user@5.1.3': - resolution: {integrity: sha512-zNPByPn9K7TC+OOHKGxU+MxrE9SZAN11UHYEFLsK2NRn3akJN2LHRl85q+Eypr3tuB2GrKx3rfj2phJdkYCvzw==} + '@octokit/auth-oauth-user@5.1.6': + resolution: {integrity: sha512-/R8vgeoulp7rJs+wfJ2LtXEVC7pjQTIqDab7wPKwVG6+2v/lUnCOub6vaHmysQBbb45FknM3tbHW8TOVqYHxCw==} engines: {node: '>= 18'} '@octokit/auth-token@5.1.2': resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==} engines: {node: '>= 18'} - '@octokit/auth-unauthenticated@6.1.2': - resolution: {integrity: sha512-07DlUGcz/AAVdzu3EYfi/dOyMSHp9YsOxPl/MPmtlVXWiD//GlV8HgZsPhud94DEyx+RfrW0wSl46Lx+AWbOlg==} + '@octokit/auth-unauthenticated@6.1.3': + resolution: {integrity: sha512-d5gWJla3WdSl1yjbfMpET+hUSFCE15qM0KVSB0H1shyuJihf/RL1KqWoZMIaonHvlNojkL9XtLFp8QeLe+1iwA==} engines: {node: '>= 18'} - '@octokit/core@6.1.4': - resolution: {integrity: sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==} + '@octokit/core@6.1.6': + resolution: {integrity: sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==} engines: {node: '>= 18'} - '@octokit/endpoint@10.1.3': - resolution: {integrity: sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==} + '@octokit/endpoint@10.1.4': + resolution: {integrity: sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==} engines: {node: '>= 18'} - '@octokit/graphql@8.2.1': - resolution: {integrity: sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw==} + '@octokit/graphql@8.2.2': + resolution: {integrity: sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==} engines: {node: '>= 18'} '@octokit/oauth-app@7.1.6': @@ -87,15 +87,18 @@ packages: resolution: {integrity: sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==} engines: {node: '>= 18'} - '@octokit/oauth-methods@5.1.4': - resolution: {integrity: sha512-Jc/ycnePClOvO1WL7tlC+TRxOFtyJBGuTDsL4dzXNiVZvzZdrPuNw7zHI3qJSUX2n6RLXE5L0SkFmYyNaVUFoQ==} + '@octokit/oauth-methods@5.1.5': + resolution: {integrity: sha512-Ev7K8bkYrYLhoOSZGVAGsLEscZQyq7XQONCBBAl2JdMg7IT3PQn/y8P0KjloPoYpI5UylqYrLeUcScaYWXwDvw==} engines: {node: '>= 18'} - '@octokit/openapi-types@23.0.1': - resolution: {integrity: sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==} + '@octokit/openapi-types@24.2.0': + resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} - '@octokit/openapi-webhooks-types@9.1.0': - resolution: {integrity: sha512-bO1D2jLdU8qEvqmbWjNxJzDYSFT4wesiYKIKP6f4LaM0XUGtn/0LBv/20hu9YqcnpdX38X5o/xANTMtIAqdwYw==} + '@octokit/openapi-types@25.1.0': + resolution: {integrity: sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==} + + '@octokit/openapi-webhooks-types@11.0.0': + resolution: {integrity: sha512-ZBzCFj98v3SuRM7oBas6BHZMJRadlnDoeFfvm1olVxZnYeU6Vh97FhPxyS5aLh5pN51GYv2I51l/hVUAVkGBlA==} '@octokit/plugin-paginate-graphql@5.2.4': resolution: {integrity: sha512-pLZES1jWaOynXKHOqdnwZ5ULeVR6tVVCMm+AUbp0htdcyXDU95WbkYdU4R2ej1wKj5Tu94Mee2Ne0PjPO9cCyA==} @@ -103,8 +106,14 @@ packages: peerDependencies: '@octokit/core': '>=6' - '@octokit/plugin-paginate-rest@11.4.2': - resolution: {integrity: sha512-BXJ7XPCTDXFF+wxcg/zscfgw2O/iDPtNSkwwR1W1W5c4Mb3zav/M2XvxQ23nVmKj7jpweB4g8viMeCQdm7LMVA==} + '@octokit/plugin-paginate-rest@11.6.0': + resolution: {integrity: sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-paginate-rest@12.0.0': + resolution: {integrity: sha512-MPd6WK1VtZ52lFrgZ0R2FlaoiWllzgqFHaSZxvp72NmoDeZ0m8GeJdg4oB6ctqMTYyrnDYp592Xma21mrgiyDA==} engines: {node: '>= 18'} peerDependencies: '@octokit/core': '>=6' @@ -115,53 +124,62 @@ packages: peerDependencies: '@octokit/core': '>=6' - '@octokit/plugin-rest-endpoint-methods@13.3.1': - resolution: {integrity: sha512-o8uOBdsyR+WR8MK9Cco8dCgvG13H1RlM1nWnK/W7TEACQBFux/vPREgKucxUfuDQ5yi1T3hGf4C5ZmZXAERgwQ==} + '@octokit/plugin-rest-endpoint-methods@13.5.0': + resolution: {integrity: sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@14.0.0': + resolution: {integrity: sha512-iQt6ovem4b7zZYZQtdv+PwgbL5VPq37th1m2x2TdkgimIDJpsi2A6Q/OI/23i/hR6z5mL0EgisNR4dcbmckSZQ==} engines: {node: '>= 18'} peerDependencies: '@octokit/core': '>=6' - '@octokit/plugin-retry@7.1.4': - resolution: {integrity: sha512-7AIP4p9TttKN7ctygG4BtR7rrB0anZqoU9ThXFk8nETqIfvgPUANTSYHqWYknK7W3isw59LpZeLI8pcEwiJdRg==} + '@octokit/plugin-retry@7.2.1': + resolution: {integrity: sha512-wUc3gv0D6vNHpGxSaR3FlqJpTXGWgqmk607N9L3LvPL4QjaxDgX/1nY2mGpT37Khn+nlIXdljczkRnNdTTV3/A==} engines: {node: '>= 18'} peerDependencies: '@octokit/core': '>=6' - '@octokit/plugin-throttling@9.4.0': - resolution: {integrity: sha512-IOlXxXhZA4Z3m0EEYtrrACkuHiArHLZ3CvqWwOez/pURNqRuwfoFlTPbN5Muf28pzFuztxPyiUiNwz8KctdZaQ==} + '@octokit/plugin-throttling@10.0.0': + resolution: {integrity: sha512-Kuq5/qs0DVYTHZuBAzCZStCzo2nKvVRo/TDNhCcpC2TKiOGz/DisXMCvjt3/b5kr6SCI1Y8eeeJTHBxxpFvZEg==} engines: {node: '>= 18'} peerDependencies: '@octokit/core': ^6.1.3 - '@octokit/request-error@6.1.7': - resolution: {integrity: sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==} + '@octokit/request-error@6.1.8': + resolution: {integrity: sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==} engines: {node: '>= 18'} - '@octokit/request@9.2.2': - resolution: {integrity: sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==} + '@octokit/request@9.2.4': + resolution: {integrity: sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==} engines: {node: '>= 18'} '@octokit/rest@21.1.1': resolution: {integrity: sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==} engines: {node: '>= 18'} - '@octokit/types@13.8.0': - resolution: {integrity: sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==} + '@octokit/types@13.10.0': + resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + + '@octokit/types@14.1.0': + resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} '@octokit/webhooks-methods@5.1.1': resolution: {integrity: sha512-NGlEHZDseJTCj8TMMFehzwa9g7On4KJMPVHDSrHxCQumL6uSQR8wIkP/qesv52fXqV1BPf4pTxwtS31ldAt9Xg==} engines: {node: '>= 18'} - '@octokit/webhooks@13.6.1': - resolution: {integrity: sha512-vk0jnc5k0/mLMUI4IA9LfSYkLs3OHtfa7B3h4aRG6to912V3wIG8lS/wKwatwYxRkAug4oE8is0ERRI8pzoYTw==} + '@octokit/webhooks@13.9.1': + resolution: {integrity: sha512-Nss2b4Jyn4wB3EAqAPJypGuCJFalz/ZujKBQQ5934To7Xw9xjf4hkr/EAByxQY7hp7MKd790bWGz7XYSTsHmaw==} engines: {node: '>= 18'} - '@slack/types@2.14.0': - resolution: {integrity: sha512-n0EGm7ENQRxlXbgKSrQZL69grzg1gHLAVd+GlRVQJ1NSORo0FrApR7wql/gaKdu2n4TO83Sq/AmeUOqD60aXUA==} + '@slack/types@2.16.0': + resolution: {integrity: sha512-bICnyukvdklXhwxprR3uF1+ZFkTvWTZge4evlCS4G1H1HU6QLY68AcjqzQRymf7/5gNt6Y4OBb4NdviheyZcAg==} engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} - '@slack/webhook@7.0.5': - resolution: {integrity: sha512-PmbZx89+SmH4zt78FUwe4If8hWX2MAIRmGXjmlF0A8PwyJb/H7CWaQYV6DDlZn1+7Zs6CEytKH0ejEE/idVSDw==} + '@slack/webhook@7.0.6': + resolution: {integrity: sha512-RvNCcOjNbzl5uQ2TZsbTJ+A+5ptoWMwnyd/W4lKzeXFToIwebeaZiuntcP0usmhZHj1LH9H1T9WN6Bt1B/DLyg==} engines: {node: '>= 18', npm: '>= 8.6.0'} '@tsconfig/node20@20.1.5': @@ -170,17 +188,17 @@ packages: '@tsconfig/strictest@2.0.5': resolution: {integrity: sha512-ec4tjL2Rr0pkZ5hww65c+EEPYwxOi4Ryv+0MtjeaSQRJyq322Q27eOQiFbuNgw2hpL4hB1/W/HBGk3VKS43osg==} - '@types/aws-lambda@8.10.147': - resolution: {integrity: sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew==} + '@types/aws-lambda@8.10.152': + resolution: {integrity: sha512-soT/c2gYBnT5ygwiHPmd9a1bftj462NWVk2tKCc1PYHSIacB2UwbTS2zYG4jzag1mRDuzg/OjtxQjQ2NKRB6Rw==} - '@types/node@22.13.13': - resolution: {integrity: sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==} + '@types/node@24.3.0': + resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axios@1.8.4: - resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} + axios@1.11.0: + resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} before-after-hook@3.0.2: resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} @@ -226,8 +244,8 @@ packages: fast-content-type-parse@2.0.1: resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -235,8 +253,8 @@ packages: debug: optional: true - form-data@4.0.2: - resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} function-bind@1.1.2: @@ -278,8 +296,8 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - octokit@4.1.2: - resolution: {integrity: sha512-0kcTxJOK3yQrJsRb8wKa28hlTze4QOz4sLuUnfXXnhboDhFKgv8LxS86tFwbsafDW9JZ08ByuVAE8kQbYJIZkA==} + octokit@4.1.4: + resolution: {integrity: sha512-cRvxRte6FU3vAHRC9+PMSY3D+mRAs2Rd9emMoqp70UGRvJRM3sbAoim2IXRZNNsf8wVfn4sGxVBHRAP+JBVX/g==} engines: {node: '>= 18'} proxy-from-env@1.1.0: @@ -294,182 +312,198 @@ packages: engines: {node: '>=14.17'} hasBin: true - undici-types@6.20.0: - resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + undici-types@7.10.0: + resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} - universal-github-app-jwt@2.2.0: - resolution: {integrity: sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ==} + universal-github-app-jwt@2.2.2: + resolution: {integrity: sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==} - universal-user-agent@7.0.2: - resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} snapshots: - '@octokit/app@15.1.4': + '@octokit/app@15.1.6': dependencies: - '@octokit/auth-app': 7.1.5 - '@octokit/auth-unauthenticated': 6.1.2 - '@octokit/core': 6.1.4 + '@octokit/auth-app': 7.2.2 + '@octokit/auth-unauthenticated': 6.1.3 + '@octokit/core': 6.1.6 '@octokit/oauth-app': 7.1.6 - '@octokit/plugin-paginate-rest': 11.4.2(@octokit/core@6.1.4) - '@octokit/types': 13.8.0 - '@octokit/webhooks': 13.6.1 + '@octokit/plugin-paginate-rest': 12.0.0(@octokit/core@6.1.6) + '@octokit/types': 14.1.0 + '@octokit/webhooks': 13.9.1 - '@octokit/auth-app@7.1.5': + '@octokit/auth-app@7.2.2': dependencies: - '@octokit/auth-oauth-app': 8.1.3 - '@octokit/auth-oauth-user': 5.1.3 - '@octokit/request': 9.2.2 - '@octokit/request-error': 6.1.7 - '@octokit/types': 13.8.0 + '@octokit/auth-oauth-app': 8.1.4 + '@octokit/auth-oauth-user': 5.1.6 + '@octokit/request': 9.2.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 toad-cache: 3.7.0 - universal-github-app-jwt: 2.2.0 - universal-user-agent: 7.0.2 + universal-github-app-jwt: 2.2.2 + universal-user-agent: 7.0.3 - '@octokit/auth-oauth-app@8.1.3': + '@octokit/auth-oauth-app@8.1.4': dependencies: - '@octokit/auth-oauth-device': 7.1.3 - '@octokit/auth-oauth-user': 5.1.3 - '@octokit/request': 9.2.2 - '@octokit/types': 13.8.0 - universal-user-agent: 7.0.2 + '@octokit/auth-oauth-device': 7.1.5 + '@octokit/auth-oauth-user': 5.1.6 + '@octokit/request': 9.2.4 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 - '@octokit/auth-oauth-device@7.1.3': + '@octokit/auth-oauth-device@7.1.5': dependencies: - '@octokit/oauth-methods': 5.1.4 - '@octokit/request': 9.2.2 - '@octokit/types': 13.8.0 - universal-user-agent: 7.0.2 + '@octokit/oauth-methods': 5.1.5 + '@octokit/request': 9.2.4 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 - '@octokit/auth-oauth-user@5.1.3': + '@octokit/auth-oauth-user@5.1.6': dependencies: - '@octokit/auth-oauth-device': 7.1.3 - '@octokit/oauth-methods': 5.1.4 - '@octokit/request': 9.2.2 - '@octokit/types': 13.8.0 - universal-user-agent: 7.0.2 + '@octokit/auth-oauth-device': 7.1.5 + '@octokit/oauth-methods': 5.1.5 + '@octokit/request': 9.2.4 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 '@octokit/auth-token@5.1.2': {} - '@octokit/auth-unauthenticated@6.1.2': + '@octokit/auth-unauthenticated@6.1.3': dependencies: - '@octokit/request-error': 6.1.7 - '@octokit/types': 13.8.0 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 - '@octokit/core@6.1.4': + '@octokit/core@6.1.6': dependencies: '@octokit/auth-token': 5.1.2 - '@octokit/graphql': 8.2.1 - '@octokit/request': 9.2.2 - '@octokit/request-error': 6.1.7 - '@octokit/types': 13.8.0 + '@octokit/graphql': 8.2.2 + '@octokit/request': 9.2.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 before-after-hook: 3.0.2 - universal-user-agent: 7.0.2 + universal-user-agent: 7.0.3 - '@octokit/endpoint@10.1.3': + '@octokit/endpoint@10.1.4': dependencies: - '@octokit/types': 13.8.0 - universal-user-agent: 7.0.2 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 - '@octokit/graphql@8.2.1': + '@octokit/graphql@8.2.2': dependencies: - '@octokit/request': 9.2.2 - '@octokit/types': 13.8.0 - universal-user-agent: 7.0.2 + '@octokit/request': 9.2.4 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 '@octokit/oauth-app@7.1.6': dependencies: - '@octokit/auth-oauth-app': 8.1.3 - '@octokit/auth-oauth-user': 5.1.3 - '@octokit/auth-unauthenticated': 6.1.2 - '@octokit/core': 6.1.4 + '@octokit/auth-oauth-app': 8.1.4 + '@octokit/auth-oauth-user': 5.1.6 + '@octokit/auth-unauthenticated': 6.1.3 + '@octokit/core': 6.1.6 '@octokit/oauth-authorization-url': 7.1.1 - '@octokit/oauth-methods': 5.1.4 - '@types/aws-lambda': 8.10.147 - universal-user-agent: 7.0.2 + '@octokit/oauth-methods': 5.1.5 + '@types/aws-lambda': 8.10.152 + universal-user-agent: 7.0.3 '@octokit/oauth-authorization-url@7.1.1': {} - '@octokit/oauth-methods@5.1.4': + '@octokit/oauth-methods@5.1.5': dependencies: '@octokit/oauth-authorization-url': 7.1.1 - '@octokit/request': 9.2.2 - '@octokit/request-error': 6.1.7 - '@octokit/types': 13.8.0 + '@octokit/request': 9.2.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + + '@octokit/openapi-types@24.2.0': {} - '@octokit/openapi-types@23.0.1': {} + '@octokit/openapi-types@25.1.0': {} - '@octokit/openapi-webhooks-types@9.1.0': {} + '@octokit/openapi-webhooks-types@11.0.0': {} - '@octokit/plugin-paginate-graphql@5.2.4(@octokit/core@6.1.4)': + '@octokit/plugin-paginate-graphql@5.2.4(@octokit/core@6.1.6)': dependencies: - '@octokit/core': 6.1.4 + '@octokit/core': 6.1.6 - '@octokit/plugin-paginate-rest@11.4.2(@octokit/core@6.1.4)': + '@octokit/plugin-paginate-rest@11.6.0(@octokit/core@6.1.6)': dependencies: - '@octokit/core': 6.1.4 - '@octokit/types': 13.8.0 + '@octokit/core': 6.1.6 + '@octokit/types': 13.10.0 - '@octokit/plugin-request-log@5.3.1(@octokit/core@6.1.4)': + '@octokit/plugin-paginate-rest@12.0.0(@octokit/core@6.1.6)': dependencies: - '@octokit/core': 6.1.4 + '@octokit/core': 6.1.6 + '@octokit/types': 14.1.0 - '@octokit/plugin-rest-endpoint-methods@13.3.1(@octokit/core@6.1.4)': + '@octokit/plugin-request-log@5.3.1(@octokit/core@6.1.6)': dependencies: - '@octokit/core': 6.1.4 - '@octokit/types': 13.8.0 + '@octokit/core': 6.1.6 - '@octokit/plugin-retry@7.1.4(@octokit/core@6.1.4)': + '@octokit/plugin-rest-endpoint-methods@13.5.0(@octokit/core@6.1.6)': dependencies: - '@octokit/core': 6.1.4 - '@octokit/request-error': 6.1.7 - '@octokit/types': 13.8.0 + '@octokit/core': 6.1.6 + '@octokit/types': 13.10.0 + + '@octokit/plugin-rest-endpoint-methods@14.0.0(@octokit/core@6.1.6)': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/types': 14.1.0 + + '@octokit/plugin-retry@7.2.1(@octokit/core@6.1.6)': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 bottleneck: 2.19.5 - '@octokit/plugin-throttling@9.4.0(@octokit/core@6.1.4)': + '@octokit/plugin-throttling@10.0.0(@octokit/core@6.1.6)': dependencies: - '@octokit/core': 6.1.4 - '@octokit/types': 13.8.0 + '@octokit/core': 6.1.6 + '@octokit/types': 14.1.0 bottleneck: 2.19.5 - '@octokit/request-error@6.1.7': + '@octokit/request-error@6.1.8': dependencies: - '@octokit/types': 13.8.0 + '@octokit/types': 14.1.0 - '@octokit/request@9.2.2': + '@octokit/request@9.2.4': dependencies: - '@octokit/endpoint': 10.1.3 - '@octokit/request-error': 6.1.7 - '@octokit/types': 13.8.0 + '@octokit/endpoint': 10.1.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 fast-content-type-parse: 2.0.1 - universal-user-agent: 7.0.2 + universal-user-agent: 7.0.3 '@octokit/rest@21.1.1': dependencies: - '@octokit/core': 6.1.4 - '@octokit/plugin-paginate-rest': 11.4.2(@octokit/core@6.1.4) - '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.4) - '@octokit/plugin-rest-endpoint-methods': 13.3.1(@octokit/core@6.1.4) + '@octokit/core': 6.1.6 + '@octokit/plugin-paginate-rest': 11.6.0(@octokit/core@6.1.6) + '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.6) + '@octokit/plugin-rest-endpoint-methods': 13.5.0(@octokit/core@6.1.6) + + '@octokit/types@13.10.0': + dependencies: + '@octokit/openapi-types': 24.2.0 - '@octokit/types@13.8.0': + '@octokit/types@14.1.0': dependencies: - '@octokit/openapi-types': 23.0.1 + '@octokit/openapi-types': 25.1.0 '@octokit/webhooks-methods@5.1.1': {} - '@octokit/webhooks@13.6.1': + '@octokit/webhooks@13.9.1': dependencies: - '@octokit/openapi-webhooks-types': 9.1.0 - '@octokit/request-error': 6.1.7 + '@octokit/openapi-webhooks-types': 11.0.0 + '@octokit/request-error': 6.1.8 '@octokit/webhooks-methods': 5.1.1 - '@slack/types@2.14.0': {} + '@slack/types@2.16.0': {} - '@slack/webhook@7.0.5': + '@slack/webhook@7.0.6': dependencies: - '@slack/types': 2.14.0 - '@types/node': 22.13.13 - axios: 1.8.4 + '@slack/types': 2.16.0 + '@types/node': 24.3.0 + axios: 1.11.0 transitivePeerDependencies: - debug @@ -477,18 +511,18 @@ snapshots: '@tsconfig/strictest@2.0.5': {} - '@types/aws-lambda@8.10.147': {} + '@types/aws-lambda@8.10.152': {} - '@types/node@22.13.13': + '@types/node@24.3.0': dependencies: - undici-types: 6.20.0 + undici-types: 7.10.0 asynckit@0.4.0: {} - axios@1.8.4: + axios@1.11.0: dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.2 + follow-redirects: 1.15.11 + form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -533,13 +567,14 @@ snapshots: fast-content-type-parse@2.0.1: {} - follow-redirects@1.15.9: {} + follow-redirects@1.15.11: {} - form-data@4.0.2: + form-data@4.0.4: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 + hasown: 2.0.2 mime-types: 2.1.35 function-bind@1.1.2: {} @@ -582,18 +617,19 @@ snapshots: dependencies: mime-db: 1.52.0 - octokit@4.1.2: + octokit@4.1.4: dependencies: - '@octokit/app': 15.1.4 - '@octokit/core': 6.1.4 + '@octokit/app': 15.1.6 + '@octokit/core': 6.1.6 '@octokit/oauth-app': 7.1.6 - '@octokit/plugin-paginate-graphql': 5.2.4(@octokit/core@6.1.4) - '@octokit/plugin-paginate-rest': 11.4.2(@octokit/core@6.1.4) - '@octokit/plugin-rest-endpoint-methods': 13.3.1(@octokit/core@6.1.4) - '@octokit/plugin-retry': 7.1.4(@octokit/core@6.1.4) - '@octokit/plugin-throttling': 9.4.0(@octokit/core@6.1.4) - '@octokit/request-error': 6.1.7 - '@octokit/types': 13.8.0 + '@octokit/plugin-paginate-graphql': 5.2.4(@octokit/core@6.1.6) + '@octokit/plugin-paginate-rest': 12.0.0(@octokit/core@6.1.6) + '@octokit/plugin-rest-endpoint-methods': 14.0.0(@octokit/core@6.1.6) + '@octokit/plugin-retry': 7.2.1(@octokit/core@6.1.6) + '@octokit/plugin-throttling': 10.0.0(@octokit/core@6.1.6) + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + '@octokit/webhooks': 13.9.1 proxy-from-env@1.1.0: {} @@ -601,8 +637,8 @@ snapshots: typescript@5.7.3: {} - undici-types@6.20.0: {} + undici-types@7.10.0: {} - universal-github-app-jwt@2.2.0: {} + universal-github-app-jwt@2.2.2: {} - universal-user-agent@7.0.2: {} + universal-user-agent@7.0.3: {} From 39d41ed822af014ef28a1499f8313939a641b724 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 31 Aug 2025 13:29:29 +0300 Subject: [PATCH 467/744] Add another entry to show how to hide the Sign In button from the interface (#37260) Release Notes: - N/A --- docs/src/accounts.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/accounts.md b/docs/src/accounts.md index 1ce23cf902dc558de4163621d4ec886d2b719e15..af4c4c172f76ba1d491ddb4031714f60f848c3b6 100644 --- a/docs/src/accounts.md +++ b/docs/src/accounts.md @@ -30,3 +30,8 @@ To sign out of Zed, you can use either of these methods: Your Zed account's email address is the address provided by GitHub OAuth. If you have a public email address then it will be used, otherwise your primary GitHub email address will be used. Changes to your email address on GitHub can be synced to your Zed account by [signing in to zed.dev](https://zed.dev/sign_in). Stripe is used for billing, and will use your Zed account's email address when starting a subscription. Changes to your Zed account email address do not currently update the email address used in Stripe. See [Updating Billing Information](./ai/billing.md#updating-billing-info) for how to change this email address. + +## Hiding Sign In button from the interface + +In case the Sign In feature is not used, it's possible to hide that from the interface by using `show_sign_in` settings property. +Refer to [Visual Customization page](./visual-customization.md) for more details. From babc0c09f0f54c9b5d2df93de4430b0b4cac9e07 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 31 Aug 2025 20:56:23 +0300 Subject: [PATCH 468/744] Add a "mandatory PR contents" section in the contribution docs (#37259) The LLM part is inspired by (and paraphrased from) https://github.com/ghostty-org/ghostty?tab=contributing-ov-file#ai-assistance-notice Release Notes: - N/A --- CONTRIBUTING.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91b1b75f8292f37b122c152d71fe1e38eeccf817..dd5bbdc2e1d7f7a98e42fdaba21a6189eb92c638 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,22 @@ By effectively engaging with the Zed team and community early in your process, w We plan to set aside time each week to pair program with contributors on promising pull requests in Zed. This will be an experiment. We tend to prefer pairing over async code review on our team, and we'd like to see how well it works in an open source setting. If we're finding it difficult to get on the same page with async review, we may ask you to pair with us if you're open to it. The closer a contribution is to the goals outlined in our roadmap, the more likely we'll be to spend time pairing on it. +## Mandatory PR contents + +Please ensure the PR contains + +- Before & after screenshots, if there are visual adjustments introduced. + +Examples of visual adjustments: tree-sitter query updates, UI changes, etc. + +- A disclosure of the AI assistance usage, if any was used. + +Any kind of AI assistance must be disclosed in the PR, along with the extent to which AI assistance was used (e.g. docs only vs. code generation). + +If the PR responses are being generated by an AI, disclose that as well. + +As a small exception, trivial tab-completion doesn't need to be disclosed, as long as it's limited to single keywords or short phrases. + ## Tips to improve the chances of your PR getting reviewed and merged - Discuss your plans ahead of time with the team From e48be30266a836640d74fbda086041863e35cc47 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Sun, 31 Aug 2025 20:39:26 +0200 Subject: [PATCH 469/744] vim: Fix `NormalBefore` with completions shown (#37272) Follow-up to https://github.com/zed-industries/zed/pull/35985 The `!menu` is actually not needed and breaks other keybinds from that context. Release Notes: - N/A --- assets/keymaps/vim.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index bd6eb3982cd9860b2635a3390d47484f1a6dbe55..fd33b888b742bff8ba6a3c1b1ff15b8dbe0c11f8 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -324,7 +324,7 @@ } }, { - "context": "vim_mode == insert && !menu", + "context": "vim_mode == insert", "bindings": { "ctrl-c": "vim::NormalBefore", "ctrl-[": "vim::NormalBefore", From 9c8c3966dfc2089d7ff340f2b5c0842e638b7344 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Sun, 31 Aug 2025 15:57:24 -0400 Subject: [PATCH 470/744] linux: Support ctrl-insert in markdown previews (#37273) Closes: https://github.com/zed-industries/zed/issues/37240 Release Notes: - Added support for copying in Markdown preview using `ctrl-insert` on Linux/Windows --- assets/keymaps/default-linux.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 2610f9b7051cbce74ce6df13d49699c74e870395..a60dc92844b337409e717b56975789073eb964fb 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -170,6 +170,7 @@ "context": "Markdown", "bindings": { "copy": "markdown::Copy", + "ctrl-insert": "markdown::Copy", "ctrl-c": "markdown::Copy" } }, @@ -258,6 +259,7 @@ "context": "AgentPanel > Markdown", "bindings": { "copy": "markdown::CopyAsMarkdown", + "ctrl-insert": "markdown::CopyAsMarkdown", "ctrl-c": "markdown::CopyAsMarkdown" } }, From 5abc398a0a1f486ba16743919a45955e721c9221 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Sun, 31 Aug 2025 23:09:09 +0200 Subject: [PATCH 471/744] nix: Update flake, remove legacy Darwin SDK usage (#37254) `darwin.apple_sdk.frameworks` has been obsoleted and is no longer required to be specified explicitly as per [Nixpkgs Reference Manual](https://nixos.org/manual/nixpkgs/stable/#sec-darwin-legacy-frameworks). @P1n3appl3 not sure what the process for updating Nix is, so lemme know if this is desired/acceptable! Release Notes: - N/A --- flake.lock | 18 +++++++++--------- nix/build.nix | 1 - 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 80022f7b555900ad78dca230d37faeb04dd09c7d..d96f0a998ff47958a7b605d61e1bf539929555f5 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1754269165, - "narHash": "sha256-0tcS8FHd4QjbCVoxN9jI+PjHgA4vc/IjkUSp+N3zy0U=", + "lastModified": 1755993354, + "narHash": "sha256-FCRRAzSaL/+umLIm3RU3O/+fJ2ssaPHseI2SSFL8yZU=", "owner": "ipetkov", "repo": "crane", - "rev": "444e81206df3f7d92780680e45858e31d2f07a08", + "rev": "25bd41b24426c7734278c2ff02e53258851db914", "type": "github" }, "original": { @@ -33,10 +33,10 @@ "nixpkgs": { "locked": { "lastModified": 315532800, - "narHash": "sha256-5VYevX3GccubYeccRGAXvCPA1ktrGmIX1IFC0icX07g=", - "rev": "a683adc19ff5228af548c6539dbc3440509bfed3", + "narHash": "sha256-E8CyvVDZuIsF7puIw+OLkrFmhj3qUV+iwPcNbBhdcxM=", + "rev": "a918bb3594dd243c2f8534b3be01b3cb4ed35fd1", "type": "tarball", - "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre840248.a683adc19ff5/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre854010.a918bb3594dd/nixexprs.tar.xz" }, "original": { "type": "tarball", @@ -58,11 +58,11 @@ ] }, "locked": { - "lastModified": 1754575663, - "narHash": "sha256-afOx8AG0KYtw7mlt6s6ahBBy7eEHZwws3iCRoiuRQS4=", + "lastModified": 1756607787, + "narHash": "sha256-ciwAdgtlAN1PCaidWK6RuWsTBL8DVuyDCGM+X3ein5Q=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "6db0fb0e9cec2e9729dc52bf4898e6c135bb8a0f", + "rev": "f46d294b87ebb9f7124f1ce13aa2a5f5acc0f3eb", "type": "github" }, "original": { diff --git a/nix/build.nix b/nix/build.nix index 03403cc1c97f2dca0a42d7fb09bc5936d67e7cab..9012a47c1fa1a1874ec4283bb73eae96087d4529 100644 --- a/nix/build.nix +++ b/nix/build.nix @@ -145,7 +145,6 @@ let ] ++ lib.optionals stdenv'.hostPlatform.isDarwin [ apple-sdk_15 - darwin.apple_sdk.frameworks.System (darwinMinVersionHook "10.15") ]; From d74384f6e2a1d0b04f7788883d1b599e7e0b85fa Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Mon, 1 Sep 2025 00:42:57 +0300 Subject: [PATCH 472/744] anthropic: Remove logging when no credentials are available (#37276) Removes excess log which got through on each start of Zed ``` ERROR [agent_ui::language_model_selector] Failed to authenticate provider: Anthropic: credentials not found ``` The `AnthropicLanguageModelProvider::api_key` method returned a `anyhow::Result` which would convert `AuthenticateError::CredentialsNotFound` into a generic error because of the implicit `Into` when using the `?` operator. This would then get converted into a `AuthenticateError::Other` later. By specifying the error type as `AuthenticateError`, we remove this implicit conversion and the log gets removed. Release Notes: - N/A --- crates/language_models/src/provider/anthropic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index c492edeaf569fe5eeedadd840bd6338c073b48dd..6c003c4c3919a9f553024c6b1b56d03d410d984b 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -197,7 +197,7 @@ impl AnthropicLanguageModelProvider { }) } - pub fn api_key(cx: &mut App) -> Task> { + pub fn api_key(cx: &mut App) -> Task> { let credentials_provider = ::global(cx); let api_url = AllLanguageModelSettings::get_global(cx) .anthropic From c833f8905bbe63955b34d69c2fb3eca42aa6c17e Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Mon, 1 Sep 2025 04:21:17 +0530 Subject: [PATCH 473/744] language_models: Fix `grok-code-fast-1` support for Copilot (#37116) This PR fixes a deserialization issue in GitHub Copilot Chat that was causing warnings when encountering xAI models from the GitHub Copilot API and skipping the Grok model from model selector. Release Notes: - Fixed support for xAI models that are now available through GitHub Copilot Chat. --- crates/copilot/src/copilot_chat.rs | 2 ++ crates/language_models/src/provider/copilot_chat.rs | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/copilot/src/copilot_chat.rs b/crates/copilot/src/copilot_chat.rs index bfddba0e2f8a41e3ed234b21ee52454d104c9dd2..9b9d6e19b8de86fd0ee7e6fe6bf57d6d91da19da 100644 --- a/crates/copilot/src/copilot_chat.rs +++ b/crates/copilot/src/copilot_chat.rs @@ -164,6 +164,8 @@ pub enum ModelVendor { OpenAI, Google, Anthropic, + #[serde(rename = "xAI")] + XAI, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index d48c12aa4b5de713c0130320f7c9e61a733dc33e..bd284eb72b207dee90048f06dc44a8e21ae8d34f 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -32,6 +32,8 @@ use std::time::Duration; use ui::prelude::*; use util::debug_panic; +use crate::provider::x_ai::count_xai_tokens; + use super::anthropic::count_anthropic_tokens; use super::google::count_google_tokens; use super::open_ai::count_open_ai_tokens; @@ -228,7 +230,9 @@ impl LanguageModel for CopilotChatLanguageModel { ModelVendor::OpenAI | ModelVendor::Anthropic => { LanguageModelToolSchemaFormat::JsonSchema } - ModelVendor::Google => LanguageModelToolSchemaFormat::JsonSchemaSubset, + ModelVendor::Google | ModelVendor::XAI => { + LanguageModelToolSchemaFormat::JsonSchemaSubset + } } } @@ -256,6 +260,10 @@ impl LanguageModel for CopilotChatLanguageModel { match self.model.vendor() { ModelVendor::Anthropic => count_anthropic_tokens(request, cx), ModelVendor::Google => count_google_tokens(request, cx), + ModelVendor::XAI => { + let model = x_ai::Model::from_id(self.model.id()).unwrap_or_default(); + count_xai_tokens(request, model, cx) + } ModelVendor::OpenAI => { let model = open_ai::Model::from_id(self.model.id()).unwrap_or_default(); count_open_ai_tokens(request, model, cx) From 129bff83585f79e77a72e787728bcd000eeca679 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Sun, 31 Aug 2025 19:52:43 -0400 Subject: [PATCH 474/744] agent: Make it so delete_path tool needs user confirmation (#37191) Closes https://github.com/zed-industries/zed/issues/37048 Release Notes: - agent: Make delete_path tool require user confirmation by default --- crates/assistant_tools/src/delete_path_tool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/assistant_tools/src/delete_path_tool.rs b/crates/assistant_tools/src/delete_path_tool.rs index b181eeff5ca0f1a45176921ed9e24973aae3839f..7c85f1ed7552931822500f76bb9f3b1b1f47fd0c 100644 --- a/crates/assistant_tools/src/delete_path_tool.rs +++ b/crates/assistant_tools/src/delete_path_tool.rs @@ -35,7 +35,7 @@ impl Tool for DeletePathTool { } fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false + true } fn may_perform_edits(&self) -> bool { From f290daf7eac29a1aafa89be1074b76feb78acebb Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Sun, 31 Aug 2025 20:08:17 -0400 Subject: [PATCH 475/744] docs: Improve Bedrock suggested IAM policy (#37278) Closes https://github.com/zed-industries/zed/issues/37251 H/T: @brandon-fryslie Release Notes: - N/A --- docs/src/ai/llm-providers.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index 5ef6081421240ae13ab53b27fd966aec64ca3b82..ecc4cb004befc199cf77708367e639a6dd6b029d 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -40,7 +40,6 @@ Ensure your credentials have the following permissions set up: - `bedrock:InvokeModelWithResponseStream` - `bedrock:InvokeModel` -- `bedrock:ConverseStream` Your IAM policy should look similar to: @@ -52,8 +51,7 @@ Your IAM policy should look similar to: "Effect": "Allow", "Action": [ "bedrock:InvokeModel", - "bedrock:InvokeModelWithResponseStream", - "bedrock:ConverseStream" + "bedrock:InvokeModelWithResponseStream" ], "Resource": "*" } From a852bcc09410b47dcabbe9b089725777024d125e Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Mon, 1 Sep 2025 02:24:00 +0200 Subject: [PATCH 476/744] Improve system window tabs visibility (#37244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow up of https://github.com/zed-industries/zed/pull/33334 After chatting with @MrSubidubi we found out that he had an old defaults setting (most likely from when he encountered a previous window tabbing bug): ``` ❯ defaults read dev.zed.Zed-Nightly { NSNavPanelExpandedSizeForOpenMode = "{800, 448}"; NSNavPanelExpandedSizeForSaveMode = "{800, 448}"; NSNavPanelExpandedStateForSaveMode = 1; NSOSPLastRootDirectory = {length = 828, bytes = 0x626f6f6b 3c030000 00000410 30000000 ... dc010000 00000000 }; "NSWindow Frame NSNavPanelAutosaveName" = "557 1726 800 448 -323 982 2560 1440 "; "NSWindowTabbingShoudShowTabBarKey-GPUIWindow-GPUIWindow-(null)-HT-FS" = 1; } ``` > That suffix is AppKit’s fallback autosave name when no tabbing identifier is set. It encodes the NSWindow subclass (GPUIWindow), plus traits like HT (hidden titlebar) and FS (fullscreen). Which explains why it only happened on the Nightly build, since each bundle has it's own defaults. It also explains why the tabbar would disappear when he activated the `use_system_window_tabs` setting, because with that setting activated, the tabbing identifier becomes "zed" (instead of the default one when omitted) for which he didn't have the `NSWindowTabbingShoudShowTabBarKey` default. The original implementation was perhaps a bit naive and relied fully on macOS to determine if the tabbar should be shown. I've updated the code to always hide the tabbar, if the setting is turned off and there is only 1 tab entry. While testing, I also noticed that the menu's like 'merge all windows' wouldn't become active when the setting was turned on, only after a full workspace reload. So I added a setting observer as well, to immediately set the correct window properties to enable all the features without a reload. Release Notes: - N/A --- crates/gpui/src/platform.rs | 1 + crates/gpui/src/platform/mac/window.rs | 21 ++++++++++ crates/gpui/src/window.rs | 7 ++++ crates/title_bar/src/system_window_tabs.rs | 49 ++++++++++++++++++++-- crates/zed/src/main.rs | 2 +- 5 files changed, 76 insertions(+), 4 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index eb1d73814388a26503e9ada782bc358dc712b53c..d3425c8835bb474ffbed6bc79371340d569d1bfb 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -522,6 +522,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn merge_all_windows(&self) {} fn move_tab_to_new_window(&self) {} fn toggle_window_tab_overview(&self) {} + fn set_tabbing_identifier(&self, _identifier: Option) {} #[cfg(target_os = "windows")] fn get_raw_handle(&self) -> windows::HWND; diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 0262cbb1213ca670cece780959c740f292764630..686cfb314e58c4e10e916a07931fb5f4248ea54e 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -781,6 +781,8 @@ impl MacWindow { if let Some(tabbing_identifier) = tabbing_identifier { let tabbing_id = NSString::alloc(nil).init_str(tabbing_identifier.as_str()); let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; + } else { + let _: () = msg_send![native_window, setTabbingIdentifier:nil]; } } WindowKind::PopUp => { @@ -1018,6 +1020,25 @@ impl PlatformWindow for MacWindow { } } + fn set_tabbing_identifier(&self, tabbing_identifier: Option) { + let native_window = self.0.lock().native_window; + unsafe { + let allows_automatic_window_tabbing = tabbing_identifier.is_some(); + if allows_automatic_window_tabbing { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES]; + } else { + let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO]; + } + + if let Some(tabbing_identifier) = tabbing_identifier { + let tabbing_id = NSString::alloc(nil).init_str(tabbing_identifier.as_str()); + let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id]; + } else { + let _: () = msg_send![native_window, setTabbingIdentifier:nil]; + } + } + } + fn scale_factor(&self) -> f32 { self.0.as_ref().lock().scale_factor() } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 4504f512551b678b9304a4c180f54b15c34af956..c2719665d423a4431184d56a9b6bff16f8ad443b 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4390,6 +4390,13 @@ impl Window { self.platform_window.toggle_window_tab_overview() } + /// Sets the tabbing identifier for the window. + /// This is macOS specific. + pub fn set_tabbing_identifier(&self, tabbing_identifier: Option) { + self.platform_window + .set_tabbing_identifier(tabbing_identifier) + } + /// Toggles the inspector mode on this window. #[cfg(any(feature = "inspector", debug_assertions))] pub fn toggle_inspector(&mut self, cx: &mut App) { diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs index cc50fbc2b99b56c2d8dab95e0c56deb33da2bb4b..ba898da716f042573840f8f9c9f375747ac5cc04 100644 --- a/crates/title_bar/src/system_window_tabs.rs +++ b/crates/title_bar/src/system_window_tabs.rs @@ -1,4 +1,4 @@ -use settings::Settings; +use settings::{Settings, SettingsStore}; use gpui::{ AnyWindowHandle, Context, Hsla, InteractiveElement, MouseButton, ParentElement, ScrollHandle, @@ -11,7 +11,7 @@ use ui::{ LabelSize, Tab, h_flex, prelude::*, right_click_menu, }; use workspace::{ - CloseWindow, ItemSettings, Workspace, + CloseWindow, ItemSettings, Workspace, WorkspaceSettings, item::{ClosePosition, ShowCloseButton}, }; @@ -53,6 +53,46 @@ impl SystemWindowTabs { } pub fn init(cx: &mut App) { + let mut was_use_system_window_tabs = + WorkspaceSettings::get_global(cx).use_system_window_tabs; + + cx.observe_global::(move |cx| { + let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs; + if use_system_window_tabs == was_use_system_window_tabs { + return; + } + was_use_system_window_tabs = use_system_window_tabs; + + let tabbing_identifier = if use_system_window_tabs { + Some(String::from("zed")) + } else { + None + }; + + if use_system_window_tabs { + SystemWindowTabController::init(cx); + } + + cx.windows().iter().for_each(|handle| { + let _ = handle.update(cx, |_, window, cx| { + window.set_tabbing_identifier(tabbing_identifier.clone()); + if use_system_window_tabs { + let tabs = if let Some(tabs) = window.tabbed_windows() { + tabs + } else { + vec![SystemWindowTab::new( + SharedString::from(window.window_title()), + window.window_handle(), + )] + }; + + SystemWindowTabController::add_tab(cx, handle.window_id(), tabs); + } + }); + }); + }) + .detach(); + cx.observe_new(|workspace: &mut Workspace, _, _| { workspace.register_action_renderer(|div, _, window, cx| { let window_id = window.window_handle().window_id(); @@ -336,6 +376,7 @@ impl SystemWindowTabs { impl Render for SystemWindowTabs { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs; let active_background_color = cx.theme().colors().title_bar_background; let inactive_background_color = cx.theme().colors().tab_bar_background; let entity = cx.entity(); @@ -368,7 +409,9 @@ impl Render for SystemWindowTabs { .collect::>(); let number_of_tabs = tab_items.len().max(1); - if !window.tab_bar_visible() && !visible { + if (!window.tab_bar_visible() && !visible) + || (!use_system_window_tabs && number_of_tabs == 1) + { return h_flex().into_any_element(); } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 79cf2bfa66fb217680dea86720eb46402f116958..d1d221fb37ddf4d76804f326f3d60ae7a09cdcbc 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -955,7 +955,7 @@ async fn installation_id() -> Result { async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp) -> Result<()> { if let Some(locations) = restorable_workspace_locations(cx, &app_state).await { let use_system_window_tabs = cx - .update(|cx| WorkspaceSettings::get(None, cx).use_system_window_tabs) + .update(|cx| WorkspaceSettings::get_global(cx).use_system_window_tabs) .unwrap_or(false); let mut results: Vec> = Vec::new(); let mut tasks = Vec::new(); From 62083fe7963dd5bed4579bb12abac1b7800cdbaa Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 1 Sep 2025 09:49:52 +0200 Subject: [PATCH 477/744] gpui: Do not render ligatures between different styled text runs (#37175) Currently when we render text with differing styles adjacently we might form a ligature between the text, causing the ligature forming characters to take on one of the two styles. This can especially become confusing when a ligature is formed between actual text and inlay hints. Annoyingly, the only ways to prevent this with core text is to either render each run separately, or to insert a zero-width non-joiner to force core text to break the ligatures apart, as it otherwise will merge subsequent font runs of the same fonts. We currently do layouting on a per line basis and it is unlikely we want to change that as it would incur a lot of complexity and annoyances to merge things back into a line, so this goes with the other approach of inserting ZWNJ characters instead. Note that neither linux nor windows seem to currently render ligatures, so this only concerns macOS rendering at the moment. Release Notes: - Fixed ligatures forming between real text and inlay hints on macOS --- crates/gpui/src/platform/mac/text_system.rs | 170 ++++++++++++++++---- crates/gpui/src/text_system.rs | 102 +++++++----- crates/gpui/src/text_system/line_layout.rs | 15 +- 3 files changed, 205 insertions(+), 82 deletions(-) diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index 72a0f2e565d9937e3aaf4082b663c3e2ae6ac91d..ba7017b58f76f028a1c5c80959e9359bc379c0cb 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -43,7 +43,7 @@ use pathfinder_geometry::{ vector::{Vector2F, Vector2I}, }; use smallvec::SmallVec; -use std::{borrow::Cow, char, cmp, convert::TryFrom, sync::Arc}; +use std::{borrow::Cow, char, convert::TryFrom, sync::Arc}; use super::open_type::apply_features_and_fallbacks; @@ -67,6 +67,7 @@ struct MacTextSystemState { font_ids_by_postscript_name: HashMap, font_ids_by_font_key: HashMap>, postscript_names_by_font_id: HashMap, + zwnjs_scratch_space: Vec<(usize, usize)>, } impl MacTextSystem { @@ -79,6 +80,7 @@ impl MacTextSystem { font_ids_by_postscript_name: HashMap::default(), font_ids_by_font_key: HashMap::default(), postscript_names_by_font_id: HashMap::default(), + zwnjs_scratch_space: Vec::new(), })) } } @@ -424,29 +426,41 @@ impl MacTextSystemState { } fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { + const ZWNJ: char = '\u{200C}'; + const ZWNJ_STR: &str = "\u{200C}"; + const ZWNJ_SIZE_16: usize = ZWNJ.len_utf16(); + + self.zwnjs_scratch_space.clear(); // Construct the attributed string, converting UTF8 ranges to UTF16 ranges. let mut string = CFMutableAttributedString::new(); - { - string.replace_str(&CFString::new(text), CFRange::init(0, 0)); - let utf16_line_len = string.char_len() as usize; - let mut ix_converter = StringIndexConverter::new(text); + { + let mut ix_converter = StringIndexConverter::new(&text); + let mut last_font_run = None; for run in font_runs { - let utf8_end = ix_converter.utf8_ix + run.len; - let utf16_start = ix_converter.utf16_ix; - - if utf16_start >= utf16_line_len { - break; + let text = &text[ix_converter.utf8_ix..][..run.len]; + // if the fonts are the same, we need to disconnect the text with a ZWNJ + // to prevent core text from forming ligatures between them + let needs_zwnj = last_font_run.replace(run.font_id) == Some(run.font_id); + + let n_zwnjs = self.zwnjs_scratch_space.len(); + let utf16_start = ix_converter.utf16_ix + n_zwnjs * ZWNJ_SIZE_16; + ix_converter.advance_to_utf8_ix(ix_converter.utf8_ix + run.len); + + string.replace_str(&CFString::new(text), CFRange::init(utf16_start as isize, 0)); + if needs_zwnj { + let zwnjs_pos = string.char_len(); + self.zwnjs_scratch_space.push((n_zwnjs, zwnjs_pos as usize)); + string.replace_str( + &CFString::from_static_string(ZWNJ_STR), + CFRange::init(zwnjs_pos, 0), + ); } - - ix_converter.advance_to_utf8_ix(utf8_end); - let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len); + let utf16_end = string.char_len() as usize; let cf_range = CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize); - - let font: &FontKitFont = &self.fonts[run.font_id.0]; - + let font = &self.fonts[run.font_id.0]; unsafe { string.set_attribute( cf_range, @@ -454,17 +468,12 @@ impl MacTextSystemState { &font.native_font().clone_with_font_size(font_size.into()), ); } - - if utf16_end == utf16_line_len { - break; - } } } - // Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets. let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef()); let glyph_runs = line.glyph_runs(); - let mut runs = Vec::with_capacity(glyph_runs.len() as usize); + let mut runs = >::with_capacity(glyph_runs.len() as usize); let mut ix_converter = StringIndexConverter::new(text); for run in glyph_runs.into_iter() { let attributes = run.attributes().unwrap(); @@ -476,28 +485,44 @@ impl MacTextSystemState { }; let font_id = self.id_for_native_font(font); - let mut glyphs = Vec::with_capacity(run.glyph_count().try_into().unwrap_or(0)); - for ((glyph_id, position), glyph_utf16_ix) in run + let mut glyphs = match runs.last_mut() { + Some(run) if run.font_id == font_id => &mut run.glyphs, + _ => { + runs.push(ShapedRun { + font_id, + glyphs: Vec::with_capacity(run.glyph_count().try_into().unwrap_or(0)), + }); + &mut runs.last_mut().unwrap().glyphs + } + }; + for ((&glyph_id, position), &glyph_utf16_ix) in run .glyphs() .iter() .zip(run.positions().iter()) .zip(run.string_indices().iter()) { - let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap(); + let mut glyph_utf16_ix = usize::try_from(glyph_utf16_ix).unwrap(); + let r = self + .zwnjs_scratch_space + .binary_search_by(|&(_, it)| it.cmp(&glyph_utf16_ix)); + match r { + // this glyph is a ZWNJ, skip it + Ok(_) => continue, + // adjust the index to account for the ZWNJs we've inserted + Err(idx) => glyph_utf16_ix -= idx * ZWNJ_SIZE_16, + } if ix_converter.utf16_ix > glyph_utf16_ix { // We cannot reuse current index converter, as it can only seek forward. Restart the search. ix_converter = StringIndexConverter::new(text); } ix_converter.advance_to_utf16_ix(glyph_utf16_ix); glyphs.push(ShapedGlyph { - id: GlyphId(*glyph_id as u32), + id: GlyphId(glyph_id as u32), position: point(position.x as f32, position.y as f32).map(px), index: ix_converter.utf8_ix, is_emoji: self.is_emoji(font_id), }); } - - runs.push(ShapedRun { font_id, glyphs }); } let typographic_bounds = line.get_typographic_bounds(); LineLayout { @@ -696,4 +721,93 @@ mod tests { // There's no glyph for \u{feff} assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b } + + #[test] + fn test_layout_line_zwnj_insertion() { + let fonts = MacTextSystem::new(); + let font_id = fonts.font_id(&font("Helvetica")).unwrap(); + + let text = "hello world"; + let font_runs = &[ + FontRun { font_id, len: 5 }, // "hello" + FontRun { font_id, len: 6 }, // " world" + ]; + + let layout = fonts.layout_line(text, px(16.), font_runs); + assert_eq!(layout.len, text.len()); + + for run in &layout.runs { + for glyph in &run.glyphs { + assert!( + glyph.index < text.len(), + "Glyph index {} is out of bounds for text length {}", + glyph.index, + text.len() + ); + } + } + + // Test with different font runs - should not insert ZWNJ + let font_id2 = fonts.font_id(&font("Times")).unwrap_or(font_id); + let font_runs_different = &[ + FontRun { font_id, len: 5 }, // "hello" + // " world" + FontRun { + font_id: font_id2, + len: 6, + }, + ]; + + let layout2 = fonts.layout_line(text, px(16.), font_runs_different); + assert_eq!(layout2.len, text.len()); + + for run in &layout2.runs { + for glyph in &run.glyphs { + assert!( + glyph.index < text.len(), + "Glyph index {} is out of bounds for text length {}", + glyph.index, + text.len() + ); + } + } + } + + #[test] + fn test_layout_line_zwnj_edge_cases() { + let fonts = MacTextSystem::new(); + let font_id = fonts.font_id(&font("Helvetica")).unwrap(); + + let text = "hello"; + let font_runs = &[FontRun { font_id, len: 5 }]; + let layout = fonts.layout_line(text, px(16.), font_runs); + assert_eq!(layout.len, text.len()); + + let text = "abc"; + let font_runs = &[ + FontRun { font_id, len: 1 }, // "a" + FontRun { font_id, len: 1 }, // "b" + FontRun { font_id, len: 1 }, // "c" + ]; + let layout = fonts.layout_line(text, px(16.), font_runs); + assert_eq!(layout.len, text.len()); + + for run in &layout.runs { + for glyph in &run.glyphs { + assert!( + glyph.index < text.len(), + "Glyph index {} is out of bounds for text length {}", + glyph.index, + text.len() + ); + } + } + + // Test with empty text + let text = ""; + let font_runs = &[]; + let layout = fonts.layout_line(text, px(16.), font_runs); + assert_eq!(layout.len, 0); + assert!(layout.runs.is_empty()); + } } diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 53991089da94c58d0035bff0d607ad3ab57a69bd..be34b9e2aac055bd9f17c2f69b3c72d24e392593 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -413,9 +413,10 @@ impl WindowTextSystem { let mut wrapped_lines = 0; let mut process_line = |line_text: SharedString| { + font_runs.clear(); let line_end = line_start + line_text.len(); - let mut last_font: Option = None; + let mut last_font: Option = None; let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); let mut run_start = line_start; while run_start < line_end { @@ -425,23 +426,14 @@ impl WindowTextSystem { let run_len_within_line = cmp::min(line_end, run_start + run.len) - run_start; - if last_font == Some(run.font.clone()) { - font_runs.last_mut().unwrap().len += run_len_within_line; - } else { - last_font = Some(run.font.clone()); - font_runs.push(FontRun { - len: run_len_within_line, - font_id: self.resolve_font(&run.font), - }); - } - - if decoration_runs.last().is_some_and(|last_run| { - last_run.color == run.color - && last_run.underline == run.underline - && last_run.strikethrough == run.strikethrough - && last_run.background_color == run.background_color - }) { - decoration_runs.last_mut().unwrap().len += run_len_within_line as u32; + let decoration_changed = if let Some(last_run) = decoration_runs.last_mut() + && last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + && last_run.background_color == run.background_color + { + last_run.len += run_len_within_line as u32; + false } else { decoration_runs.push(DecorationRun { len: run_len_within_line as u32, @@ -450,6 +442,21 @@ impl WindowTextSystem { underline: run.underline, strikethrough: run.strikethrough, }); + true + }; + + if let Some(font_run) = font_runs.last_mut() + && Some(font_run.font_id) == last_font + && !decoration_changed + { + font_run.len += run_len_within_line; + } else { + let font_id = self.resolve_font(&run.font); + last_font = Some(font_id); + font_runs.push(FontRun { + len: run_len_within_line, + font_id, + }); } if run_len_within_line == run.len { @@ -484,8 +491,6 @@ impl WindowTextSystem { runs.next(); } } - - font_runs.clear(); }; let mut split_lines = text.split('\n'); @@ -519,37 +524,54 @@ impl WindowTextSystem { /// Subsets of the line can be styled independently with the `runs` parameter. /// Generally, you should prefer to use `TextLayout::shape_line` instead, which /// can be painted directly. - pub fn layout_line( + pub fn layout_line( &self, - text: Text, + text: &str, font_size: Pixels, runs: &[TextRun], force_width: Option, - ) -> Arc - where - Text: AsRef, - SharedString: From, - { + ) -> Arc { + let mut last_run = None::<&TextRun>; + let mut last_font: Option = None; let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); + font_runs.clear(); + for run in runs.iter() { - let font_id = self.resolve_font(&run.font); - if let Some(last_run) = font_runs.last_mut() - && last_run.font_id == font_id + let decoration_changed = if let Some(last_run) = last_run + && last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + // we do not consider differing background color relevant, as it does not affect glyphs + // && last_run.background_color == run.background_color { - last_run.len += run.len; - continue; + false + } else { + last_run = Some(run); + true + }; + + if let Some(font_run) = font_runs.last_mut() + && Some(font_run.font_id) == last_font + && !decoration_changed + { + font_run.len += run.len; + } else { + let font_id = self.resolve_font(&run.font); + last_font = Some(font_id); + font_runs.push(FontRun { + len: run.len, + font_id, + }); } - font_runs.push(FontRun { - len: run.len, - font_id, - }); } - let layout = - self.line_layout_cache - .layout_line_internal(text, font_size, &font_runs, force_width); + let layout = self.line_layout_cache.layout_line( + &SharedString::new(text), + font_size, + &font_runs, + force_width, + ); - font_runs.clear(); self.font_runs_pool.lock().push(font_runs); layout diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index 43694702a82566b8f84199dcfc4ff996da93588e..4ac1d258970802ed1c4fe86bd98f2971b78fbc04 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -501,7 +501,7 @@ impl LineLayoutCache { } else { drop(current_frame); let text = SharedString::from(text); - let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs); + let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs, None); let wrap_boundaries = if let Some(wrap_width) = wrap_width { unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width, max_lines) } else { @@ -535,19 +535,6 @@ impl LineLayoutCache { text: Text, font_size: Pixels, runs: &[FontRun], - ) -> Arc - where - Text: AsRef, - SharedString: From, - { - self.layout_line_internal(text, font_size, runs, None) - } - - pub fn layout_line_internal( - &self, - text: Text, - font_size: Pixels, - runs: &[FontRun], force_width: Option, ) -> Arc where From 3315fd94d27fde9f4d327ce6a8c4a009fd605505 Mon Sep 17 00:00:00 2001 From: Ivan Trubach Date: Mon, 1 Sep 2025 11:21:55 +0300 Subject: [PATCH 478/744] editor: Add an option to disable rounded corners for text selection (#36987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #19891 Similar to VSCode’s `editor.roundedSelection` option. #### Before/after
Enabled (default)Disabled
Editor-based UIsimage imageimage image
Terminalimageimage
Release Notes: - Added setting `rounded_selection` to disable rounded corners for text selection. --- assets/settings/default.json | 2 ++ crates/editor/src/editor_settings.rs | 6 ++++++ crates/editor/src/element.rs | 9 +++++++-- crates/terminal_view/src/terminal_element.rs | 9 +++++++-- docs/src/configuring-zed.md | 6 ++++++ 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index b15eb6e5ce8de85bb088108f065a31494b9087a1..2aec3aa7b9d56b3a04d2c8d1f80bb0d37c91b8cc 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -223,6 +223,8 @@ "current_line_highlight": "all", // Whether to highlight all occurrences of the selected text in an editor. "selection_highlight": true, + // Whether the text selection should have rounded corners. + "rounded_selection": true, // The debounce delay before querying highlights from the language // server based on the current cursor location. "lsp_highlight_debounce": 75, diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index c2baa9de024b1988f9acb77a529936f947103f56..084c4eb5c618cbf3d290b317b0035f1b8f307b3f 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -17,6 +17,7 @@ pub struct EditorSettings { pub cursor_shape: Option, pub current_line_highlight: CurrentLineHighlight, pub selection_highlight: bool, + pub rounded_selection: bool, pub lsp_highlight_debounce: u64, pub hover_popover_enabled: bool, pub hover_popover_delay: u64, @@ -441,6 +442,10 @@ pub struct EditorSettingsContent { /// /// Default: true pub selection_highlight: Option, + /// Whether the text selection should have rounded corners. + /// + /// Default: true + pub rounded_selection: Option, /// The debounce delay before querying highlights from the language /// server based on the current cursor location. /// @@ -794,6 +799,7 @@ impl Settings for EditorSettings { "editor.selectionHighlight", &mut current.selection_highlight, ); + vscode.bool_setting("editor.roundedSelection", &mut current.rounded_selection); vscode.bool_setting("editor.hover.enabled", &mut current.hover_popover_enabled); vscode.u64_setting("editor.hover.delay", &mut current.hover_popover_delay); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ca6eac080e6121880eae63b4dc60ca6d32c6da5d..f384afa1ae988d8d224f9ec3de70932543519571 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -6063,7 +6063,7 @@ impl EditorElement { }; self.paint_lines_background(layout, window, cx); - let invisible_display_ranges = self.paint_highlights(layout, window); + let invisible_display_ranges = self.paint_highlights(layout, window, cx); self.paint_document_colors(layout, window); self.paint_lines(&invisible_display_ranges, layout, window, cx); self.paint_redactions(layout, window); @@ -6085,6 +6085,7 @@ impl EditorElement { &mut self, layout: &mut EditorLayout, window: &mut Window, + cx: &mut App, ) -> SmallVec<[Range; 32]> { window.paint_layer(layout.position_map.text_hitbox.bounds, |window| { let mut invisible_display_ranges = SmallVec::<[Range; 32]>::new(); @@ -6101,7 +6102,11 @@ impl EditorElement { ); } - let corner_radius = 0.15 * layout.position_map.line_height; + let corner_radius = if EditorSettings::get_global(cx).rounded_selection { + 0.15 * layout.position_map.line_height + } else { + Pixels::ZERO + }; for (player_color, selections) in &layout.selections { for selection in selections.iter() { diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 56715b604eeffe0b42302adcdf0d6fdd93919879..5bbf5ad36b3de89514d92ce9e305988817cec32f 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1,4 +1,4 @@ -use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine}; +use editor::{CursorLayout, EditorSettings, HighlightedRange, HighlightedRangeLine}; use gpui::{ AbsoluteLength, AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, Element, ElementId, Entity, FocusHandle, Font, FontFeatures, FontStyle, FontWeight, @@ -1257,12 +1257,17 @@ impl Element for TerminalElement { if let Some((start_y, highlighted_range_lines)) = to_highlighted_range_lines(relative_highlighted_range, layout, origin) { + let corner_radius = if EditorSettings::get_global(cx).rounded_selection { + 0.15 * layout.dimensions.line_height + } else { + Pixels::ZERO + }; let hr = HighlightedRange { start_y, line_height: layout.dimensions.line_height, lines: highlighted_range_lines, color: *color, - corner_radius: 0.15 * layout.dimensions.line_height, + corner_radius: corner_radius, }; hr.paint(true, bounds, window); } diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 2b1d801f8010c8ad00f1295c38803bd80df1c282..e245b3ca2facecb097b315f28d98ef2ea5a20048 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -685,6 +685,12 @@ List of `string` values - Setting: `selection_highlight` - Default: `true` +## Rounded Selection + +- Description: Whether the text selection should have rounded corners. +- Setting: `rounded_selection` +- Default: `true` + ## Cursor Blink - Description: Whether or not the cursor blinks. From acff65ed3f70d9d48b1ad189e68b5ce136b0967b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Mon, 1 Sep 2025 16:33:59 +0800 Subject: [PATCH 479/744] windows: Update documents about WSL (#37292) Release Notes: - N/A --- crates/cli/src/main.rs | 6 ++++-- crates/zed/src/main.rs | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 151e96e3cf68ab94295a8386d2842539e6a986a2..d67843b4c93eb64b01fbdd6e26955d96a0c50e70 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -84,13 +84,15 @@ struct Args { /// Run zed in dev-server mode #[arg(long)] dev_server_token: Option, - /// The username and WSL distribution to use when opening paths. ,If not specified, + /// The username and WSL distribution to use when opening paths. If not specified, /// Zed will attempt to open the paths directly. /// /// The username is optional, and if not specified, the default user for the distribution /// will be used. /// - /// Example: `me@Ubuntu` or `Ubuntu` for default distribution. + /// Example: `me@Ubuntu` or `Ubuntu`. + /// + /// WARN: You should not fill in this field by hand. #[arg(long, value_name = "USER@DISTRO")] wsl: Option, /// Not supported in Zed CLI, only supported on Zed binary diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index d1d221fb37ddf4d76804f326f3d60ae7a09cdcbc..3a7baa1559d68cbce8cfbf96b0bf4384aa1f7e0b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1183,13 +1183,15 @@ struct Args { #[arg(long, value_name = "DIR")] user_data_dir: Option, - /// The username and WSL distribution to use when opening paths. ,If not specified, + /// The username and WSL distribution to use when opening paths. If not specified, /// Zed will attempt to open the paths directly. /// /// The username is optional, and if not specified, the default user for the distribution /// will be used. /// - /// Example: `me@Ubuntu` or `Ubuntu` for default distribution. + /// Example: `me@Ubuntu` or `Ubuntu`. + /// + /// WARN: You should not fill in this field by hand. #[arg(long, value_name = "USER@DISTRO")] wsl: Option, From 2790eb604a1de04107f2412dadc06d4f75415380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E6=A2=85?= <1554694323@qq.com> Date: Mon, 1 Sep 2025 16:49:09 +0800 Subject: [PATCH 480/744] deepseek: Fix API URL (#33905) Closes #33904 Release Notes: - Add support for custom API Urls for DeepSeek Provider --------- Co-authored-by: Peter Tripp --- assets/settings/default.json | 2 +- crates/deepseek/src/deepseek.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 2aec3aa7b9d56b3a04d2c8d1f80bb0d37c91b8cc..623a4612d06975ca4681d75a775d061e594608b2 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1776,7 +1776,7 @@ "api_url": "http://localhost:1234/api/v0" }, "deepseek": { - "api_url": "https://api.deepseek.com" + "api_url": "https://api.deepseek.com/v1" }, "mistral": { "api_url": "https://api.mistral.ai/v1" diff --git a/crates/deepseek/src/deepseek.rs b/crates/deepseek/src/deepseek.rs index c2554c67e93b4c1d3772e60a62063fdae0511f05..e09a9e0f7a19642253245b381abdc9fa05d0af00 100644 --- a/crates/deepseek/src/deepseek.rs +++ b/crates/deepseek/src/deepseek.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::convert::TryFrom; -pub const DEEPSEEK_API_URL: &str = "https://api.deepseek.com"; +pub const DEEPSEEK_API_URL: &str = "https://api.deepseek.com/v1"; #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(rename_all = "lowercase")] @@ -263,7 +263,7 @@ pub async fn stream_completion( api_key: &str, request: Request, ) -> Result>> { - let uri = format!("{api_url}/v1/chat/completions"); + let uri = format!("{api_url}/chat/completions"); let request_builder = HttpRequest::builder() .method(Method::POST) .uri(uri) From 61175ab9cdbe84feb647bddde84ee4766d627d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Mon, 1 Sep 2025 23:26:25 +0800 Subject: [PATCH 481/744] =?UTF-8?q?windows:=20Don=E2=80=99t=20skip=20the?= =?UTF-8?q?=20typo=20check=20for=20the=20windows=20folder=20(#37314)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Try to narrow down the scope of typo checking Release Notes: - N/A --- crates/gpui/src/platform/windows/directx_renderer.rs | 2 +- crates/gpui/src/platform/windows/events.rs | 2 +- crates/gpui/src/platform/windows/vsync.rs | 2 +- typos.toml | 5 ++++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/gpui/src/platform/windows/directx_renderer.rs b/crates/gpui/src/platform/windows/directx_renderer.rs index f84a1c1b6d0d158684e4c6cad6edbf72105425e0..c496d29a0338ec4d758e436e85a3066163705db6 100644 --- a/crates/gpui/src/platform/windows/directx_renderer.rs +++ b/crates/gpui/src/platform/windows/directx_renderer.rs @@ -1760,7 +1760,7 @@ mod amd { anyhow::bail!("Failed to initialize AMD AGS, error code: {}", result); } - // Vulkan acctually returns this as the driver version + // Vulkan actually returns this as the driver version let software_version = if !gpu_info.radeon_software_version.is_null() { std::ffi::CStr::from_ptr(gpu_info.radeon_software_version) .to_string_lossy() diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 4def6a11a5f16f235b1d7018ecbbdec5565ab951..f4e3e5c3029936ce6bf9c10096fe0546376ff43c 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -708,7 +708,7 @@ impl WindowsWindowInner { .system_settings .auto_hide_taskbar_position { - // Fot the auto-hide taskbar, adjust in by 1 pixel on taskbar edge, + // For 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 { diff --git a/crates/gpui/src/platform/windows/vsync.rs b/crates/gpui/src/platform/windows/vsync.rs index 6d09b0960f11cefce007413066620b3b332e1ae9..5cbcb8e99e2741c4b37cad4d550e290c4cab869f 100644 --- a/crates/gpui/src/platform/windows/vsync.rs +++ b/crates/gpui/src/platform/windows/vsync.rs @@ -94,7 +94,7 @@ impl VSyncProvider { // DwmFlush and DCompositionWaitForCompositorClock returns very early // instead of waiting until vblank when the monitor goes to sleep or is // unplugged (nothing to present due to desktop occlusion). We use 1ms as - // a threshhold for the duration of the wait functions and fallback to + // a threshold for the duration of the wait functions and fallback to // Sleep() if it returns before that. This could happen during normal // operation for the first call after the vsync thread becomes non-idle, // but it shouldn't happen often. diff --git a/typos.toml b/typos.toml index e5f02b64159faddd165d6d4571b929c82ad5bed0..ab33d9ccb44701d6652a7916b01c51d59e82a23b 100644 --- a/typos.toml +++ b/typos.toml @@ -36,7 +36,10 @@ extend-exclude = [ # glsl isn't recognized by this tool. "extensions/glsl/languages/glsl/", # Windows likes its abbreviations. - "crates/gpui/src/platform/windows/", + "crates/gpui/src/platform/windows/directx_renderer.rs", + "crates/gpui/src/platform/windows/events.rs", + "crates/gpui/src/platform/windows/direct_write.rs", + "crates/gpui/src/platform/windows/window.rs", # Some typos in the base mdBook CSS. "docs/theme/css/", # Spellcheck triggers on `|Fixe[sd]|` regex part. From d910feac1dfef3b9b3228f25acc7bc7e0193d9ad Mon Sep 17 00:00:00 2001 From: localcc Date: Mon, 1 Sep 2025 20:07:45 +0200 Subject: [PATCH 482/744] Implement perceptual gamma / contrast correction (#37167) Closes #36023 This improves font rendering quality by doing perceptual gamma+contrast correction which makes font edges look nicer and more legible. A comparison image: (left is old, right is new) Screenshot 2025-08-29 140015 This is most noticeable on smaller fonts / low-dpi displays Release Notes: - Improved font rendering quality --- Cargo.toml | 1 + .../platform/windows/alpha_correction.hlsl | 28 ++++ .../platform/windows/color_text_raster.hlsl | 14 +- .../gpui/src/platform/windows/direct_write.rs | 141 +++++------------- .../src/platform/windows/directx_renderer.rs | 76 +++++++++- crates/gpui/src/platform/windows/platform.rs | 15 +- crates/gpui/src/platform/windows/shaders.hlsl | 9 +- 7 files changed, 157 insertions(+), 127 deletions(-) create mode 100644 crates/gpui/src/platform/windows/alpha_correction.hlsl diff --git a/Cargo.toml b/Cargo.toml index 48017d9c6b4858fb7e5415b92bd993e534d1fabb..b20b37edb9ea08f49c757cc2d8764ce62494d688 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -696,6 +696,7 @@ features = [ "Win32_Graphics_Dxgi_Common", "Win32_Graphics_Gdi", "Win32_Graphics_Imaging", + "Win32_Graphics_Hlsl", "Win32_Networking_WinSock", "Win32_Security", "Win32_Security_Credentials", diff --git a/crates/gpui/src/platform/windows/alpha_correction.hlsl b/crates/gpui/src/platform/windows/alpha_correction.hlsl new file mode 100644 index 0000000000000000000000000000000000000000..7844a15f48bb27a137b913c94cba3fdc6d1fada9 --- /dev/null +++ b/crates/gpui/src/platform/windows/alpha_correction.hlsl @@ -0,0 +1,28 @@ +float color_brightness(float3 color) { + // REC. 601 luminance coefficients for percieved brightness + return dot(color, float3(0.30f, 0.59f, 0.11f)); +} + +float light_on_dark_contrast(float enhancedContrast, float3 color) { + float brightness = color_brightness(color); + float multiplier = saturate(4.0f * (0.75f - brightness)); + return enhancedContrast * multiplier; +} + +float enhance_contrast(float alpha, float k) { + return alpha * (k + 1.0f) / (alpha * k + 1.0f); +} + +float apply_alpha_correction(float a, float b, float4 g) { + float brightness_adjustment = g.x * b + g.y; + float correction = brightness_adjustment * a + (g.z * b + g.w); + return a + a * (1.0f - a) * correction; +} + +float apply_contrast_and_gamma_correction(float sample, float3 color, float enhanced_contrast_factor, float4 gamma_ratios) { + float enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color); + float brightness = color_brightness(color); + + float contrasted = enhance_contrast(sample, enhanced_contrast); + return apply_alpha_correction(contrasted, brightness, gamma_ratios); +} diff --git a/crates/gpui/src/platform/windows/color_text_raster.hlsl b/crates/gpui/src/platform/windows/color_text_raster.hlsl index ccc5fa26f00d57f2b69e85965a66b6ecea98a833..322c743a993f11e2324b6fdb45c019919329f612 100644 --- a/crates/gpui/src/platform/windows/color_text_raster.hlsl +++ b/crates/gpui/src/platform/windows/color_text_raster.hlsl @@ -1,3 +1,5 @@ +#include "alpha_correction.hlsl" + struct RasterVertexOutput { float4 position : SV_Position; float2 texcoord : TEXCOORD0; @@ -23,17 +25,19 @@ struct Bounds { int2 size; }; -Texture2D t_layer : register(t0); +Texture2D t_layer : register(t0); SamplerState s_layer : register(s0); cbuffer GlyphLayerTextureParams : register(b0) { Bounds bounds; float4 run_color; + float4 gamma_ratios; + float grayscale_enhanced_contrast; + float3 _pad; }; float4 emoji_rasterization_fragment(PixelInput input): SV_Target { - float3 sampled = t_layer.Sample(s_layer, input.texcoord.xy).rgb; - float alpha = (sampled.r + sampled.g + sampled.b) / 3; - - return float4(run_color.rgb, alpha); + float sample = t_layer.Sample(s_layer, input.texcoord.xy).r; + float alpha_corrected = apply_contrast_and_gamma_correction(sample, run_color.rgb, grayscale_enhanced_contrast, gamma_ratios); + return float4(run_color.rgb, alpha_corrected * run_color.a); } diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index a86a1fab62a404c4f49e785491bb2925a6f3cf61..e81b87c733bf277b8f534a3fda8d6db55ce34e36 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -10,12 +10,8 @@ use windows::{ Foundation::*, Globalization::GetUserDefaultLocaleName, Graphics::{ - Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, - Direct3D11::*, - DirectWrite::*, - Dxgi::Common::*, - Gdi::{IsRectEmpty, LOGFONTW}, - Imaging::*, + Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, Direct3D11::*, DirectWrite::*, + Dxgi::Common::*, Gdi::LOGFONTW, }, System::SystemServices::LOCALE_NAME_MAX_LENGTH, UI::WindowsAndMessaging::*, @@ -40,12 +36,10 @@ pub(crate) struct DirectWriteTextSystem(RwLock); struct DirectWriteComponent { locale: String, factory: IDWriteFactory5, - bitmap_factory: AgileReference, in_memory_loader: IDWriteInMemoryFontFileLoader, builder: IDWriteFontSetBuilder1, text_renderer: Arc, - render_params: IDWriteRenderingParams3, gpu_state: GPUState, } @@ -76,11 +70,10 @@ struct FontIdentifier { } impl DirectWriteComponent { - pub fn new(bitmap_factory: &IWICImagingFactory, gpu_context: &DirectXDevices) -> Result { + pub fn new(gpu_context: &DirectXDevices) -> Result { // todo: ideally this would not be a large unsafe block but smaller isolated ones for easier auditing unsafe { let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)?; - let bitmap_factory = AgileReference::new(bitmap_factory)?; // The `IDWriteInMemoryFontFileLoader` here is supported starting from // Windows 10 Creators Update, which consequently requires the entire // `DirectWriteTextSystem` to run on `win10 1703`+. @@ -92,36 +85,14 @@ impl DirectWriteComponent { let locale = String::from_utf16_lossy(&locale_vec); let text_renderer = Arc::new(TextRendererWrapper::new(&locale)); - let render_params = { - let default_params: IDWriteRenderingParams3 = - factory.CreateRenderingParams()?.cast()?; - let gamma = default_params.GetGamma(); - let enhanced_contrast = default_params.GetEnhancedContrast(); - let gray_contrast = default_params.GetGrayscaleEnhancedContrast(); - let cleartype_level = default_params.GetClearTypeLevel(); - let grid_fit_mode = default_params.GetGridFitMode(); - - factory.CreateCustomRenderingParams( - gamma, - enhanced_contrast, - gray_contrast, - cleartype_level, - DWRITE_PIXEL_GEOMETRY_RGB, - DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC, - grid_fit_mode, - )? - }; - let gpu_state = GPUState::new(gpu_context)?; Ok(DirectWriteComponent { locale, factory, - bitmap_factory, in_memory_loader, builder, text_renderer, - render_params, gpu_state, }) } @@ -212,11 +183,8 @@ impl GPUState { } impl DirectWriteTextSystem { - pub(crate) fn new( - gpu_context: &DirectXDevices, - bitmap_factory: &IWICImagingFactory, - ) -> Result { - let components = DirectWriteComponent::new(bitmap_factory, gpu_context)?; + pub(crate) fn new(gpu_context: &DirectXDevices) -> Result { + let components = DirectWriteComponent::new(gpu_context)?; let system_font_collection = unsafe { let mut result = std::mem::zeroed(); components @@ -762,14 +730,14 @@ impl DirectWriteState { unsafe { font.font_face.GetRecommendedRenderingMode( params.font_size.0, - // The dpi here seems that it has the same effect with `Some(&transform)` - 1.0, - 1.0, + // Using 96 as scale is applied by the transform + 96.0, + 96.0, Some(&transform), false, DWRITE_OUTLINE_THRESHOLD_ANTIALIASED, DWRITE_MEASURING_MODE_NATURAL, - &self.components.render_params, + None, &mut rendering_mode, &mut grid_fit_mode, )?; @@ -782,8 +750,7 @@ impl DirectWriteState { rendering_mode, DWRITE_MEASURING_MODE_NATURAL, grid_fit_mode, - // We're using cleartype not grayscale for monochrome is because it provides better quality - DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE, + DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE, baseline_origin_x, baseline_origin_y, ) @@ -794,10 +761,14 @@ impl DirectWriteState { fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { let glyph_analysis = self.create_glyph_run_analysis(params)?; - let bounds = unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1)? }; - // Some glyphs cannot be drawn with ClearType, such as bitmap fonts. In that case - // GetAlphaTextureBounds() supposedly returns an empty RECT, but I haven't tested that yet. - if !unsafe { IsRectEmpty(&bounds) }.as_bool() { + let bounds = unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1)? }; + + if bounds.right < bounds.left { + Ok(Bounds { + origin: point(0.into(), 0.into()), + size: size(0.into(), 0.into()), + }) + } else { Ok(Bounds { origin: point(bounds.left.into(), bounds.top.into()), size: size( @@ -805,25 +776,6 @@ impl DirectWriteState { (bounds.bottom - bounds.top).into(), ), }) - } else { - // If it's empty, retry with grayscale AA. - let bounds = - unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1)? }; - - if bounds.right < bounds.left { - Ok(Bounds { - origin: point(0.into(), 0.into()), - size: size(0.into(), 0.into()), - }) - } else { - Ok(Bounds { - origin: point(bounds.left.into(), bounds.top.into()), - size: size( - (bounds.right - bounds.left).into(), - (bounds.bottom - bounds.top).into(), - ), - }) - } } } @@ -872,13 +824,12 @@ impl DirectWriteState { glyph_bounds: Bounds, ) -> Result> { let mut bitmap_data = - vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize * 3]; + vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize]; let glyph_analysis = self.create_glyph_run_analysis(params)?; unsafe { glyph_analysis.CreateAlphaTexture( - // We're using cleartype not grayscale for monochrome is because it provides better quality - DWRITE_TEXTURE_CLEARTYPE_3x1, + DWRITE_TEXTURE_ALIASED_1x1, &RECT { left: glyph_bounds.origin.x.0, top: glyph_bounds.origin.y.0, @@ -889,30 +840,6 @@ impl DirectWriteState { )?; } - let bitmap_factory = self.components.bitmap_factory.resolve()?; - let bitmap = unsafe { - bitmap_factory.CreateBitmapFromMemory( - glyph_bounds.size.width.0 as u32, - glyph_bounds.size.height.0 as u32, - &GUID_WICPixelFormat24bppRGB, - glyph_bounds.size.width.0 as u32 * 3, - &bitmap_data, - ) - }?; - - let grayscale_bitmap = - unsafe { WICConvertBitmapSource(&GUID_WICPixelFormat8bppGray, &bitmap) }?; - - let mut bitmap_data = - vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize]; - unsafe { - grayscale_bitmap.CopyPixels( - std::ptr::null() as _, - glyph_bounds.size.width.0 as u32, - &mut bitmap_data, - ) - }?; - Ok(bitmap_data) } @@ -981,25 +908,24 @@ impl DirectWriteState { DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC, DWRITE_MEASURING_MODE_NATURAL, DWRITE_GRID_FIT_MODE_DEFAULT, - DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE, + DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE, baseline_origin_x, baseline_origin_y, ) }?; let color_bounds = - unsafe { color_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1) }?; + unsafe { color_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1) }?; let color_size = size( color_bounds.right - color_bounds.left, color_bounds.bottom - color_bounds.top, ); if color_size.width > 0 && color_size.height > 0 { - let mut alpha_data = - vec![0u8; (color_size.width * color_size.height * 3) as usize]; + let mut alpha_data = vec![0u8; (color_size.width * color_size.height) as usize]; unsafe { color_analysis.CreateAlphaTexture( - DWRITE_TEXTURE_CLEARTYPE_3x1, + DWRITE_TEXTURE_ALIASED_1x1, &color_bounds, &mut alpha_data, ) @@ -1015,10 +941,6 @@ impl DirectWriteState { } }; let bounds = bounds(point(color_bounds.left, color_bounds.top), color_size); - let alpha_data = alpha_data - .chunks_exact(3) - .flat_map(|chunk| [chunk[0], chunk[1], chunk[2], 255]) - .collect::>(); glyph_layers.push(GlyphLayerTexture::new( &self.components.gpu_state, run_color, @@ -1135,10 +1057,18 @@ impl DirectWriteState { unsafe { device_context.PSSetSamplers(0, Some(&gpu_state.sampler)) }; unsafe { device_context.OMSetBlendState(&gpu_state.blend_state, None, 0xffffffff) }; + let crate::FontInfo { + gamma_ratios, + grayscale_enhanced_contrast, + } = DirectXRenderer::get_font_info(); + for layer in glyph_layers { let params = GlyphLayerTextureParams { run_color: layer.run_color, bounds: layer.bounds, + gamma_ratios: *gamma_ratios, + grayscale_enhanced_contrast: *grayscale_enhanced_contrast, + _pad: [0f32; 3], }; unsafe { let mut dest = std::mem::zeroed(); @@ -1298,7 +1228,7 @@ impl GlyphLayerTexture { Height: texture_size.height as u32, MipLevels: 1, ArraySize: 1, - Format: DXGI_FORMAT_R8G8B8A8_UNORM, + Format: DXGI_FORMAT_R8_UNORM, SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0, @@ -1334,7 +1264,7 @@ impl GlyphLayerTexture { 0, None, alpha_data.as_ptr() as _, - (texture_size.width * 4) as u32, + texture_size.width as u32, 0, ) }; @@ -1352,6 +1282,9 @@ impl GlyphLayerTexture { struct GlyphLayerTextureParams { bounds: Bounds, run_color: Rgba, + gamma_ratios: [f32; 4], + grayscale_enhanced_contrast: f32, + _pad: [f32; 3], } struct TextRendererWrapper(pub IDWriteTextRenderer); diff --git a/crates/gpui/src/platform/windows/directx_renderer.rs b/crates/gpui/src/platform/windows/directx_renderer.rs index c496d29a0338ec4d758e436e85a3066163705db6..0c092e22283d29ba1b522012a51f6cab77f51865 100644 --- a/crates/gpui/src/platform/windows/directx_renderer.rs +++ b/crates/gpui/src/platform/windows/directx_renderer.rs @@ -1,4 +1,7 @@ -use std::{mem::ManuallyDrop, sync::Arc}; +use std::{ + mem::ManuallyDrop, + sync::{Arc, OnceLock}, +}; use ::util::ResultExt; use anyhow::{Context, Result}; @@ -9,6 +12,7 @@ use windows::{ Direct3D::*, Direct3D11::*, DirectComposition::*, + DirectWrite::*, Dxgi::{Common::*, *}, }, }, @@ -27,6 +31,11 @@ const RENDER_TARGET_FORMAT: DXGI_FORMAT = DXGI_FORMAT_B8G8R8A8_UNORM; // This configuration is used for MSAA rendering on paths only, and it's guaranteed to be supported by DirectX 11. const PATH_MULTISAMPLE_COUNT: u32 = 4; +pub(crate) struct FontInfo { + pub gamma_ratios: [f32; 4], + pub grayscale_enhanced_contrast: f32, +} + pub(crate) struct DirectXRenderer { hwnd: HWND, atlas: Arc, @@ -35,6 +44,7 @@ pub(crate) struct DirectXRenderer { globals: DirectXGlobalElements, pipelines: DirectXRenderPipelines, direct_composition: Option, + font_info: &'static FontInfo, } /// Direct3D objects @@ -171,6 +181,7 @@ impl DirectXRenderer { globals, pipelines, direct_composition, + font_info: Self::get_font_info(), }) } @@ -183,10 +194,12 @@ impl DirectXRenderer { &self.devices.device_context, self.globals.global_params_buffer[0].as_ref().unwrap(), &[GlobalParams { + gamma_ratios: self.font_info.gamma_ratios, viewport_size: [ self.resources.viewport[0].Width, self.resources.viewport[0].Height, ], + grayscale_enhanced_contrast: self.font_info.grayscale_enhanced_contrast, _pad: 0, }], )?; @@ -617,6 +630,52 @@ impl DirectXRenderer { driver_info: driver_version, }) } + + pub(crate) fn get_font_info() -> &'static FontInfo { + static CACHED_FONT_INFO: OnceLock = OnceLock::new(); + CACHED_FONT_INFO.get_or_init(|| unsafe { + let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED).unwrap(); + let render_params: IDWriteRenderingParams1 = + factory.CreateRenderingParams().unwrap().cast().unwrap(); + FontInfo { + gamma_ratios: Self::get_gamma_ratios(render_params.GetGamma()), + grayscale_enhanced_contrast: render_params.GetGrayscaleEnhancedContrast(), + } + }) + } + + // Gamma ratios for brightening/darkening edges for better contrast + // https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.cpp#L50 + fn get_gamma_ratios(gamma: f32) -> [f32; 4] { + const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [ + [0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0], // gamma = 1.0 + [0.0166 / 4.0, -0.0807 / 4.0, 0.2227 / 4.0, -0.0751 / 4.0], // gamma = 1.1 + [0.0350 / 4.0, -0.1760 / 4.0, 0.4325 / 4.0, -0.1370 / 4.0], // gamma = 1.2 + [0.0543 / 4.0, -0.2821 / 4.0, 0.6302 / 4.0, -0.1876 / 4.0], // gamma = 1.3 + [0.0739 / 4.0, -0.3963 / 4.0, 0.8167 / 4.0, -0.2287 / 4.0], // gamma = 1.4 + [0.0933 / 4.0, -0.5161 / 4.0, 0.9926 / 4.0, -0.2616 / 4.0], // gamma = 1.5 + [0.1121 / 4.0, -0.6395 / 4.0, 1.1588 / 4.0, -0.2877 / 4.0], // gamma = 1.6 + [0.1300 / 4.0, -0.7649 / 4.0, 1.3159 / 4.0, -0.3080 / 4.0], // gamma = 1.7 + [0.1469 / 4.0, -0.8911 / 4.0, 1.4644 / 4.0, -0.3234 / 4.0], // gamma = 1.8 + [0.1627 / 4.0, -1.0170 / 4.0, 1.6051 / 4.0, -0.3347 / 4.0], // gamma = 1.9 + [0.1773 / 4.0, -1.1420 / 4.0, 1.7385 / 4.0, -0.3426 / 4.0], // gamma = 2.0 + [0.1908 / 4.0, -1.2652 / 4.0, 1.8650 / 4.0, -0.3476 / 4.0], // gamma = 2.1 + [0.2031 / 4.0, -1.3864 / 4.0, 1.9851 / 4.0, -0.3501 / 4.0], // gamma = 2.2 + ]; + + const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32; + const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32; + + let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10; + let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index]; + + [ + ratios[0] * NORM13, + ratios[1] * NORM24, + ratios[2] * NORM13, + ratios[3] * NORM24, + ] + } } impl DirectXResources { @@ -822,8 +881,10 @@ impl DirectXGlobalElements { #[derive(Debug, Default)] #[repr(C)] struct GlobalParams { + gamma_ratios: [f32; 4], viewport_size: [f32; 2], - _pad: u64, + grayscale_enhanced_contrast: f32, + _pad: u32, } struct PipelineState { @@ -1544,6 +1605,10 @@ pub(crate) mod shader_resources { #[cfg(debug_assertions)] pub(super) fn build_shader_blob(entry: ShaderModule, target: ShaderTarget) -> Result { unsafe { + use windows::Win32::Graphics::{ + Direct3D::ID3DInclude, Hlsl::D3D_COMPILE_STANDARD_FILE_INCLUDE, + }; + let shader_name = if matches!(entry, ShaderModule::EmojiRasterization) { "color_text_raster.hlsl" } else { @@ -1572,10 +1637,15 @@ pub(crate) mod shader_resources { let entry_point = PCSTR::from_raw(entry.as_ptr()); let target_cstr = PCSTR::from_raw(target.as_ptr()); + // really dirty trick because winapi bindings are unhappy otherwise + let include_handler = &std::mem::transmute::( + D3D_COMPILE_STANDARD_FILE_INCLUDE as usize, + ); + let ret = D3DCompileFromFile( &HSTRING::from(shader_path.to_str().unwrap()), None, - None, + include_handler, entry_point, target_cstr, D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION, diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 3a6ccff90f06156345a71482fe723c76d4c2ca39..b06f369aabb860d7b0de3603ecc7e8357571fd2c 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -1,7 +1,6 @@ use std::{ cell::RefCell, ffi::OsStr, - mem::ManuallyDrop, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -18,10 +17,7 @@ use windows::{ UI::ViewManagement::UISettings, Win32::{ Foundation::*, - Graphics::{ - Gdi::*, - Imaging::{CLSID_WICImagingFactory, IWICImagingFactory}, - }, + Graphics::Gdi::*, Security::Credentials::*, System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*}, UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*}, @@ -41,7 +37,6 @@ pub(crate) struct WindowsPlatform { foreground_executor: ForegroundExecutor, text_system: Arc, windows_version: WindowsVersion, - bitmap_factory: ManuallyDrop, drop_target_helper: IDropTargetHelper, validation_number: usize, main_thread_id_win32: u32, @@ -101,12 +96,8 @@ impl WindowsPlatform { let foreground_executor = ForegroundExecutor::new(dispatcher); let directx_devices = DirectXDevices::new(disable_direct_composition) .context("Unable to init directx devices.")?; - let bitmap_factory = ManuallyDrop::new(unsafe { - CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_INPROC_SERVER) - .context("Error creating bitmap factory.")? - }); let text_system = Arc::new( - DirectWriteTextSystem::new(&directx_devices, &bitmap_factory) + DirectWriteTextSystem::new(&directx_devices) .context("Error creating DirectWriteTextSystem")?, ); let drop_target_helper: IDropTargetHelper = unsafe { @@ -128,7 +119,6 @@ impl WindowsPlatform { text_system, disable_direct_composition, windows_version, - bitmap_factory, drop_target_helper, validation_number, main_thread_id_win32, @@ -716,7 +706,6 @@ impl Platform for WindowsPlatform { impl Drop for WindowsPlatform { fn drop(&mut self) { unsafe { - ManuallyDrop::drop(&mut self.bitmap_factory); OleUninitialize(); } } diff --git a/crates/gpui/src/platform/windows/shaders.hlsl b/crates/gpui/src/platform/windows/shaders.hlsl index 6fabe859e3fe6de58c438642455964e135258860..2cef54ae6166e313795eb42210b5f07c1bc378fc 100644 --- a/crates/gpui/src/platform/windows/shaders.hlsl +++ b/crates/gpui/src/platform/windows/shaders.hlsl @@ -1,6 +1,10 @@ +#include "alpha_correction.hlsl" + cbuffer GlobalParams: register(b0) { + float4 gamma_ratios; float2 global_viewport_size; - uint2 _pad; + float grayscale_enhanced_contrast; + uint _pad; }; Texture2D t_sprite: register(t0); @@ -1098,7 +1102,8 @@ MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexI float4 monochrome_sprite_fragment(MonochromeSpriteFragmentInput input): SV_Target { float sample = t_sprite.Sample(s_sprite, input.tile_position).r; - return float4(input.color.rgb, input.color.a * sample); + float alpha_corrected = apply_contrast_and_gamma_correction(sample, input.color.rgb, grayscale_enhanced_contrast, gamma_ratios); + return float4(input.color.rgb, input.color.a * alpha_corrected); } /* From 5b73b40df89fd4caf7596fb15b5b1ee54bd210df Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 1 Sep 2025 15:57:15 -0300 Subject: [PATCH 483/744] ACP Terminal support (#37129) Exposes terminal support via ACP and migrates our agent to use it. - N/A --------- Co-authored-by: Bennet Bo Fenner --- Cargo.lock | 9 +- Cargo.toml | 2 +- crates/acp_thread/Cargo.toml | 3 + crates/acp_thread/src/acp_thread.rs | 270 +++++++++++++--- crates/acp_thread/src/terminal.rs | 115 +++++-- crates/agent2/Cargo.toml | 2 - crates/agent2/src/agent.rs | 94 +++++- crates/agent2/src/thread.rs | 48 ++- crates/agent2/src/tools/terminal_tool.rs | 339 +++----------------- crates/agent_servers/src/acp.rs | 119 +++++-- crates/agent_ui/src/acp/entry_view_state.rs | 44 ++- crates/agent_ui/src/acp/thread_view.rs | 14 +- 12 files changed, 619 insertions(+), 440 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fed7077281333f53f4a9ce7b746227e3369d663b..a2ba36a91c445514ac9c8e1932b9b0135ca36b0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,7 @@ dependencies = [ "language_model", "markdown", "parking_lot", + "portable-pty", "project", "prompt_store", "rand 0.8.5", @@ -30,6 +31,7 @@ dependencies = [ "serde_json", "settings", "smol", + "task", "tempfile", "terminal", "ui", @@ -37,6 +39,7 @@ dependencies = [ "util", "uuid", "watch", + "which 6.0.3", "workspace-hack", ] @@ -192,9 +195,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.1.1" +version = "0.2.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b91e5ec3ce05e8effb2a7a3b7b1a587daa6699b9f98bbde6a35e44b8c6c773a" +checksum = "4ec42b8b612665799c7667890df4b5f5cb441b18a68619fd770f1e054480ee3f" dependencies = [ "anyhow", "async-broadcast", @@ -248,7 +251,6 @@ dependencies = [ "open", "parking_lot", "paths", - "portable-pty", "pretty_assertions", "project", "prompt_store", @@ -274,7 +276,6 @@ dependencies = [ "uuid", "watch", "web_search", - "which 6.0.3", "workspace-hack", "worktree", "zlog", diff --git a/Cargo.toml b/Cargo.toml index b20b37edb9ea08f49c757cc2d8764ce62494d688..6cf3d8858b01eb41afdeb860ca4284be0b9280a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -430,7 +430,7 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agent-client-protocol = "0.1" +agent-client-protocol = { version = "0.2.0-alpha.3", features = ["unstable"] } 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 196614f731c6e330328e46eb75ba58cf928cf6cc..8d7bea8659c3f22d053e47d4b050bc4072e521ba 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -31,18 +31,21 @@ language.workspace = true language_model.workspace = true markdown.workspace = true parking_lot = { workspace = true, optional = true } +portable-pty.workspace = true project.workspace = true prompt_store.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true +task.workspace = true terminal.workspace = true ui.workspace = true url.workspace = true util.workspace = true uuid.workspace = true watch.workspace = true +which.workspace = true workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 394619732a72c205b6c5c940cc8b2b7d3a6d3d38..ab6aa98e99d4977256512b7c178dff26b71fc7e7 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -7,6 +7,7 @@ use agent_settings::AgentSettings; use collections::HashSet; pub use connection::*; pub use diff::*; +use futures::future::Shared; use language::language_settings::FormatOnSave; pub use mention::*; use project::lsp_store::{FormatTrigger, LspFormatTarget}; @@ -15,7 +16,7 @@ use settings::Settings as _; pub use terminal::*; use action_log::ActionLog; -use agent_client_protocol as acp; +use agent_client_protocol::{self as acp}; use anyhow::{Context as _, Result, anyhow}; use editor::Bias; use futures::{FutureExt, channel::oneshot, future::BoxFuture}; @@ -33,7 +34,8 @@ use std::rc::Rc; use std::time::{Duration, Instant}; use std::{fmt::Display, mem, path::PathBuf, sync::Arc}; use ui::App; -use util::ResultExt; +use util::{ResultExt, get_system_shell}; +use uuid::Uuid; #[derive(Debug)] pub struct UserMessage { @@ -183,37 +185,46 @@ impl ToolCall { tool_call: acp::ToolCall, status: ToolCallStatus, language_registry: Arc, + terminals: &HashMap>, cx: &mut App, - ) -> Self { + ) -> Result { let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") { first_line.to_owned() + "…" } else { tool_call.title }; - Self { + let mut content = Vec::with_capacity(tool_call.content.len()); + for item in tool_call.content { + content.push(ToolCallContent::from_acp( + item, + language_registry.clone(), + terminals, + cx, + )?); + } + + let result = Self { id: tool_call.id, label: cx .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)), kind: tool_call.kind, - content: tool_call - .content - .into_iter() - .map(|content| ToolCallContent::from_acp(content, language_registry.clone(), cx)) - .collect(), + content, locations: tool_call.locations, resolved_locations: Vec::default(), status, raw_input: tool_call.raw_input, raw_output: tool_call.raw_output, - } + }; + Ok(result) } fn update_fields( &mut self, fields: acp::ToolCallUpdateFields, language_registry: Arc, + terminals: &HashMap>, cx: &mut App, - ) { + ) -> Result<()> { let acp::ToolCallUpdateFields { kind, status, @@ -248,14 +259,15 @@ impl ToolCall { // Reuse existing content if we can for (old, new) in self.content.iter_mut().zip(content.by_ref()) { - old.update_from_acp(new, language_registry.clone(), cx); + old.update_from_acp(new, language_registry.clone(), terminals, cx)?; } for new in content { self.content.push(ToolCallContent::from_acp( new, language_registry.clone(), + terminals, cx, - )) + )?) } self.content.truncate(new_content_len); } @@ -279,6 +291,7 @@ impl ToolCall { } self.raw_output = Some(raw_output); } + Ok(()) } pub fn diffs(&self) -> impl Iterator> { @@ -549,13 +562,16 @@ impl ToolCallContent { pub fn from_acp( content: acp::ToolCallContent, language_registry: Arc, + terminals: &HashMap>, cx: &mut App, - ) -> Self { + ) -> Result { match content { - acp::ToolCallContent::Content { content } => { - Self::ContentBlock(ContentBlock::new(content, &language_registry, cx)) - } - acp::ToolCallContent::Diff { diff } => Self::Diff(cx.new(|cx| { + acp::ToolCallContent::Content { content } => Ok(Self::ContentBlock(ContentBlock::new( + content, + &language_registry, + cx, + ))), + acp::ToolCallContent::Diff { diff } => Ok(Self::Diff(cx.new(|cx| { Diff::finalized( diff.path, diff.old_text, @@ -563,7 +579,12 @@ impl ToolCallContent { language_registry, cx, ) - })), + }))), + acp::ToolCallContent::Terminal { terminal_id } => terminals + .get(&terminal_id) + .cloned() + .map(Self::Terminal) + .ok_or_else(|| anyhow::anyhow!("Terminal with id `{}` not found", terminal_id)), } } @@ -571,8 +592,9 @@ impl ToolCallContent { &mut self, new: acp::ToolCallContent, language_registry: Arc, + terminals: &HashMap>, cx: &mut App, - ) { + ) -> Result<()> { let needs_update = match (&self, &new) { (Self::Diff(old_diff), acp::ToolCallContent::Diff { diff: new_diff }) => { old_diff.read(cx).needs_update( @@ -585,8 +607,9 @@ impl ToolCallContent { }; if needs_update { - *self = Self::from_acp(new, language_registry, cx); + *self = Self::from_acp(new, language_registry, terminals, cx)?; } + Ok(()) } pub fn to_markdown(&self, cx: &App) -> String { @@ -763,6 +786,8 @@ pub struct AcpThread { token_usage: Option, prompt_capabilities: acp::PromptCapabilities, _observe_prompt_capabilities: Task>, + determine_shell: Shared>, + terminals: HashMap>, } #[derive(Debug)] @@ -846,6 +871,20 @@ impl AcpThread { } }); + let determine_shell = cx + .background_spawn(async move { + if cfg!(windows) { + return get_system_shell(); + } + + if which::which("bash").is_ok() { + "bash".into() + } else { + get_system_shell() + } + }) + .shared(); + Self { action_log, shared_buffers: Default::default(), @@ -859,6 +898,8 @@ impl AcpThread { token_usage: None, prompt_capabilities, _observe_prompt_capabilities: task, + terminals: HashMap::default(), + determine_shell, } } @@ -1082,27 +1123,28 @@ impl AcpThread { let update = update.into(); let languages = self.project.read(cx).languages().clone(); - let (ix, current_call) = self - .tool_call_mut(update.id()) + let ix = self + .index_for_tool_call(update.id()) .context("Tool call not found")?; + let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else { + unreachable!() + }; + match update { ToolCallUpdate::UpdateFields(update) => { let location_updated = update.fields.locations.is_some(); - current_call.update_fields(update.fields, languages, cx); + call.update_fields(update.fields, languages, &self.terminals, cx)?; if location_updated { self.resolve_locations(update.id, cx); } } ToolCallUpdate::UpdateDiff(update) => { - current_call.content.clear(); - current_call - .content - .push(ToolCallContent::Diff(update.diff)); + call.content.clear(); + call.content.push(ToolCallContent::Diff(update.diff)); } ToolCallUpdate::UpdateTerminal(update) => { - current_call.content.clear(); - current_call - .content + call.content.clear(); + call.content .push(ToolCallContent::Terminal(update.terminal)); } } @@ -1125,21 +1167,30 @@ impl AcpThread { /// Fails if id does not match an existing entry. pub fn upsert_tool_call_inner( &mut self, - tool_call_update: acp::ToolCallUpdate, + update: acp::ToolCallUpdate, status: ToolCallStatus, cx: &mut Context, ) -> Result<(), acp::Error> { let language_registry = self.project.read(cx).languages().clone(); - let id = tool_call_update.id.clone(); + let id = update.id.clone(); - if let Some((ix, current_call)) = self.tool_call_mut(&id) { - current_call.update_fields(tool_call_update.fields, language_registry, cx); - current_call.status = status; + if let Some(ix) = self.index_for_tool_call(&id) { + let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else { + unreachable!() + }; + + call.update_fields(update.fields, language_registry, &self.terminals, cx)?; + call.status = status; cx.emit(AcpThreadEvent::EntryUpdated(ix)); } else { - let call = - ToolCall::from_acp(tool_call_update.try_into()?, status, language_registry, cx); + let call = ToolCall::from_acp( + update.try_into()?, + status, + language_registry, + &self.terminals, + cx, + )?; self.push_entry(AgentThreadEntry::ToolCall(call), cx); }; @@ -1147,6 +1198,22 @@ impl AcpThread { Ok(()) } + fn index_for_tool_call(&self, id: &acp::ToolCallId) -> Option { + self.entries + .iter() + .enumerate() + .rev() + .find_map(|(index, entry)| { + if let AgentThreadEntry::ToolCall(tool_call) = entry + && &tool_call.id == id + { + Some(index) + } else { + None + } + }) + } + fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> { // The tool call we are looking for is typically the last one, or very close to the end. // At the moment, it doesn't seem like a hashmap would be a good fit for this use case. @@ -1829,6 +1896,133 @@ impl AcpThread { }) } + pub fn create_terminal( + &self, + mut command: String, + args: Vec, + extra_env: Vec, + cwd: Option, + output_byte_limit: Option, + cx: &mut Context, + ) -> Task>> { + for arg in args { + command.push(' '); + command.push_str(&arg); + } + + let shell_command = if cfg!(windows) { + format!("$null | & {{{}}}", command.replace("\"", "'")) + } else if let Some(cwd) = cwd.as_ref().and_then(|cwd| cwd.as_os_str().to_str()) { + // Make sure once we're *inside* the shell, we cd into `cwd` + format!("(cd {cwd}; {}) self.project.update(cx, |project, cx| { + project.directory_environment(dir.as_path().into(), cx) + }), + None => Task::ready(None).shared(), + }; + + let env = cx.spawn(async move |_, _| { + let mut env = env.await.unwrap_or_default(); + if cfg!(unix) { + env.insert("PAGER".into(), "cat".into()); + } + for var in extra_env { + env.insert(var.name, var.value); + } + env + }); + + let project = self.project.clone(); + let language_registry = project.read(cx).languages().clone(); + let determine_shell = self.determine_shell.clone(); + + let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into()); + let terminal_task = cx.spawn({ + let terminal_id = terminal_id.clone(); + async move |_this, cx| { + let program = determine_shell.await; + let env = env.await; + let terminal = project + .update(cx, |project, cx| { + project.create_terminal_task( + task::SpawnInTerminal { + command: Some(program), + args, + cwd: cwd.clone(), + env, + ..Default::default() + }, + cx, + ) + })? + .await?; + + cx.new(|cx| { + Terminal::new( + terminal_id, + command, + cwd, + output_byte_limit.map(|l| l as usize), + terminal, + language_registry, + cx, + ) + }) + } + }); + + cx.spawn(async move |this, cx| { + let terminal = terminal_task.await?; + this.update(cx, |this, _cx| { + this.terminals.insert(terminal_id, terminal.clone()); + terminal + }) + }) + } + + pub fn kill_terminal( + &mut self, + terminal_id: acp::TerminalId, + cx: &mut Context, + ) -> Result<()> { + self.terminals + .get(&terminal_id) + .context("Terminal not found")? + .update(cx, |terminal, cx| { + terminal.kill(cx); + }); + + Ok(()) + } + + pub fn release_terminal( + &mut self, + terminal_id: acp::TerminalId, + cx: &mut Context, + ) -> Result<()> { + self.terminals + .remove(&terminal_id) + .context("Terminal not found")? + .update(cx, |terminal, cx| { + terminal.kill(cx); + }); + + Ok(()) + } + + pub fn terminal(&self, terminal_id: acp::TerminalId) -> Result> { + self.terminals + .get(&terminal_id) + .context("Terminal not found") + .cloned() + } + pub fn to_markdown(&self, cx: &App) -> String { self.entries.iter().map(|e| e.to_markdown(cx)).collect() } diff --git a/crates/acp_thread/src/terminal.rs b/crates/acp_thread/src/terminal.rs index 41d7fb89bb2eb59207bf0a6557129a088b435f3a..6b4cdb73469d9dd7d1a1759bf3aa28d005d1f13e 100644 --- a/crates/acp_thread/src/terminal.rs +++ b/crates/acp_thread/src/terminal.rs @@ -1,34 +1,43 @@ -use gpui::{App, AppContext, Context, Entity}; +use agent_client_protocol as acp; + +use futures::{FutureExt as _, future::Shared}; +use gpui::{App, AppContext, Context, Entity, Task}; use language::LanguageRegistry; use markdown::Markdown; use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant}; pub struct Terminal { + id: acp::TerminalId, command: Entity, working_dir: Option, terminal: Entity, started_at: Instant, output: Option, + output_byte_limit: Option, + _output_task: Shared>, } pub struct TerminalOutput { pub ended_at: Instant, pub exit_status: Option, - pub was_content_truncated: bool, + pub content: String, pub original_content_len: usize, pub content_line_count: usize, - pub finished_with_empty_output: bool, } impl Terminal { pub fn new( + id: acp::TerminalId, command: String, working_dir: Option, + output_byte_limit: Option, terminal: Entity, language_registry: Arc, cx: &mut Context, ) -> Self { + let command_task = terminal.read(cx).wait_for_completed_task(cx); Self { + id, command: cx.new(|cx| { Markdown::new( format!("```\n{}\n```", command).into(), @@ -41,27 +50,93 @@ impl Terminal { terminal, started_at: Instant::now(), output: None, + output_byte_limit, + _output_task: cx + .spawn(async move |this, cx| { + let exit_status = command_task.await; + + this.update(cx, |this, cx| { + let (content, original_content_len) = this.truncated_output(cx); + let content_line_count = this.terminal.read(cx).total_lines(); + + this.output = Some(TerminalOutput { + ended_at: Instant::now(), + exit_status, + content, + original_content_len, + content_line_count, + }); + cx.notify(); + }) + .ok(); + + let exit_status = exit_status.map(portable_pty::ExitStatus::from); + + acp::TerminalExitStatus { + exit_code: exit_status.as_ref().map(|e| e.exit_code()), + signal: exit_status.and_then(|e| e.signal().map(Into::into)), + } + }) + .shared(), } } - pub fn finish( - &mut self, - exit_status: Option, - original_content_len: usize, - truncated_content_len: usize, - content_line_count: usize, - finished_with_empty_output: bool, - cx: &mut Context, - ) { - self.output = Some(TerminalOutput { - ended_at: Instant::now(), - exit_status, - was_content_truncated: truncated_content_len < original_content_len, - original_content_len, - content_line_count, - finished_with_empty_output, + pub fn id(&self) -> &acp::TerminalId { + &self.id + } + + pub fn wait_for_exit(&self) -> Shared> { + self._output_task.clone() + } + + pub fn kill(&mut self, cx: &mut App) { + self.terminal.update(cx, |terminal, _cx| { + terminal.kill_active_task(); }); - cx.notify(); + } + + pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse { + if let Some(output) = self.output.as_ref() { + let exit_status = output.exit_status.map(portable_pty::ExitStatus::from); + + acp::TerminalOutputResponse { + output: output.content.clone(), + truncated: output.original_content_len > output.content.len(), + exit_status: Some(acp::TerminalExitStatus { + exit_code: exit_status.as_ref().map(|e| e.exit_code()), + signal: exit_status.and_then(|e| e.signal().map(Into::into)), + }), + } + } else { + let (current_content, original_len) = self.truncated_output(cx); + + acp::TerminalOutputResponse { + truncated: current_content.len() < original_len, + output: current_content, + exit_status: None, + } + } + } + + fn truncated_output(&self, cx: &App) -> (String, usize) { + let terminal = self.terminal.read(cx); + let mut content = terminal.get_content(); + + let original_content_len = content.len(); + + if let Some(limit) = self.output_byte_limit + && content.len() > limit + { + let mut end_ix = limit.min(content.len()); + while !content.is_char_boundary(end_ix) { + end_ix -= 1; + } + // Don't truncate mid-line, clear the remainder of the last line + end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix); + content.truncate(end_ix); + } + + (content, original_content_len) } pub fn command(&self) -> &Entity { diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 68246a96b0288cb9091a4073a33712c0b69df67d..0e9c8fcf7237627d2cb7b17b977b68322160e6d5 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -48,7 +48,6 @@ log.workspace = true open.workspace = true parking_lot.workspace = true paths.workspace = true -portable-pty.workspace = true project.workspace = true prompt_store.workspace = true rust-embed.workspace = true @@ -68,7 +67,6 @@ util.workspace = true uuid.workspace = true watch.workspace = true web_search.workspace = true -which.workspace = true workspace-hack.workspace = true zstd.workspace = true diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index bb6a3c097ca27d6103c1072986f6d3255bc6c69f..e96b4c0cfa32be910a7a77e58a1911deb7e5357a 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -2,7 +2,7 @@ use crate::{ ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization, UserMessageContent, templates::Templates, }; -use crate::{HistoryStore, TitleUpdated, TokenUsageUpdated}; +use crate::{HistoryStore, TerminalHandle, ThreadEnvironment, TitleUpdated, TokenUsageUpdated}; use acp_thread::{AcpThread, AgentModelSelector}; use action_log::ActionLog; use agent_client_protocol as acp; @@ -10,7 +10,8 @@ use agent_settings::AgentSettings; use anyhow::{Context as _, Result, anyhow}; use collections::{HashSet, IndexMap}; use fs::Fs; -use futures::channel::mpsc; +use futures::channel::{mpsc, oneshot}; +use futures::future::Shared; use futures::{StreamExt, future}; use gpui::{ App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, @@ -23,7 +24,7 @@ use prompt_store::{ use settings::update_settings_file; use std::any::Any; use std::collections::HashMap; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::Arc; use util::ResultExt; @@ -276,13 +277,6 @@ impl NativeAgent { cx: &mut Context, ) -> Entity { let connection = Rc::new(NativeAgentConnection(cx.entity())); - let registry = LanguageModelRegistry::read_global(cx); - let summarization_model = registry.thread_summary_model().map(|c| c.model); - - thread_handle.update(cx, |thread, cx| { - thread.set_summarization_model(summarization_model, cx); - thread.add_default_tools(cx) - }); let thread = thread_handle.read(cx); let session_id = thread.id().clone(); @@ -301,6 +295,20 @@ impl NativeAgent { cx, ) }); + + let registry = LanguageModelRegistry::read_global(cx); + let summarization_model = registry.thread_summary_model().map(|c| c.model); + + thread_handle.update(cx, |thread, cx| { + thread.set_summarization_model(summarization_model, cx); + thread.add_default_tools( + Rc::new(AcpThreadEnvironment { + acp_thread: acp_thread.downgrade(), + }) as _, + cx, + ) + }); + let subscriptions = vec![ cx.observe_release(&acp_thread, |this, acp_thread, _cx| { this.sessions.remove(acp_thread.session_id()); @@ -1001,7 +1009,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { ) -> Option> { self.0.read_with(cx, |agent, _cx| { agent.sessions.get(session_id).map(|session| { - Rc::new(NativeAgentSessionEditor { + Rc::new(NativeAgentSessionTruncate { thread: session.thread.clone(), acp_thread: session.acp_thread.clone(), }) as _ @@ -1050,12 +1058,12 @@ impl acp_thread::AgentTelemetry for NativeAgentConnection { } } -struct NativeAgentSessionEditor { +struct NativeAgentSessionTruncate { thread: Entity, acp_thread: WeakEntity, } -impl acp_thread::AgentSessionTruncate for NativeAgentSessionEditor { +impl acp_thread::AgentSessionTruncate for NativeAgentSessionTruncate { fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task> { match self.thread.update(cx, |thread, cx| { thread.truncate(message_id.clone(), cx)?; @@ -1104,6 +1112,66 @@ impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle { } } +pub struct AcpThreadEnvironment { + acp_thread: WeakEntity, +} + +impl ThreadEnvironment for AcpThreadEnvironment { + fn create_terminal( + &self, + command: String, + cwd: Option, + output_byte_limit: Option, + cx: &mut AsyncApp, + ) -> Task>> { + let task = self.acp_thread.update(cx, |thread, cx| { + thread.create_terminal(command, vec![], vec![], cwd, output_byte_limit, cx) + }); + + let acp_thread = self.acp_thread.clone(); + cx.spawn(async move |cx| { + let terminal = task?.await?; + + let (drop_tx, drop_rx) = oneshot::channel(); + let terminal_id = terminal.read_with(cx, |terminal, _cx| terminal.id().clone())?; + + cx.spawn(async move |cx| { + drop_rx.await.ok(); + acp_thread.update(cx, |thread, cx| thread.release_terminal(terminal_id, cx)) + }) + .detach(); + + let handle = AcpTerminalHandle { + terminal, + _drop_tx: Some(drop_tx), + }; + + Ok(Rc::new(handle) as _) + }) + } +} + +pub struct AcpTerminalHandle { + terminal: Entity, + _drop_tx: Option>, +} + +impl TerminalHandle for AcpTerminalHandle { + fn id(&self, cx: &AsyncApp) -> Result { + self.terminal.read_with(cx, |term, _cx| term.id().clone()) + } + + fn wait_for_exit(&self, cx: &AsyncApp) -> Result>> { + self.terminal + .read_with(cx, |term, _cx| term.wait_for_exit()) + } + + fn current_output(&self, cx: &AsyncApp) -> Result { + self.terminal + .read_with(cx, |term, cx| term.current_output(cx)) + } +} + #[cfg(test)] mod tests { use crate::HistoryEntryId; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 8ff5b845066c8af90eb713aef2a0c87e6d114a85..6421e4982e9fef67af3c61f54f3374d59172f807 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -45,14 +45,15 @@ use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; use settings::{Settings, update_settings_file}; use smol::stream::StreamExt; -use std::fmt::Write; use std::{ collections::BTreeMap, ops::RangeInclusive, path::Path, + rc::Rc, sync::Arc, time::{Duration, Instant}, }; +use std::{fmt::Write, path::PathBuf}; use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock}; use uuid::Uuid; @@ -523,6 +524,22 @@ pub enum AgentMessageContent { ToolUse(LanguageModelToolUse), } +pub trait TerminalHandle { + fn id(&self, cx: &AsyncApp) -> Result; + fn current_output(&self, cx: &AsyncApp) -> Result; + fn wait_for_exit(&self, cx: &AsyncApp) -> Result>>; +} + +pub trait ThreadEnvironment { + fn create_terminal( + &self, + command: String, + cwd: Option, + output_byte_limit: Option, + cx: &mut AsyncApp, + ) -> Task>>; +} + #[derive(Debug)] pub enum ThreadEvent { UserMessage(UserMessage), @@ -535,6 +552,14 @@ pub enum ThreadEvent { Stop(acp::StopReason), } +#[derive(Debug)] +pub struct NewTerminal { + pub command: String, + pub output_byte_limit: Option, + pub cwd: Option, + pub response: oneshot::Sender>>, +} + #[derive(Debug)] pub struct ToolCallAuthorization { pub tool_call: acp::ToolCallUpdate, @@ -1024,7 +1049,11 @@ impl Thread { } } - pub fn add_default_tools(&mut self, cx: &mut Context) { + pub fn add_default_tools( + &mut self, + environment: Rc, + cx: &mut Context, + ) { let language_registry = self.project.read(cx).languages().clone(); self.add_tool(CopyPathTool::new(self.project.clone())); self.add_tool(CreateDirectoryTool::new(self.project.clone())); @@ -1045,7 +1074,7 @@ impl Thread { self.project.clone(), self.action_log.clone(), )); - self.add_tool(TerminalTool::new(self.project.clone(), cx)); + self.add_tool(TerminalTool::new(self.project.clone(), environment)); self.add_tool(ThinkingTool); self.add_tool(WebSearchTool); } @@ -2389,19 +2418,6 @@ impl ToolCallEventStream { .ok(); } - pub fn update_terminal(&self, terminal: Entity) { - self.stream - .0 - .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( - acp_thread::ToolCallUpdateTerminal { - id: acp::ToolCallId(self.tool_use_id.to_string().into()), - terminal, - } - .into(), - ))) - .ok(); - } - pub fn authorize(&self, title: impl Into, cx: &mut App) -> Task> { if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { return Task::ready(Ok(())); diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index 2270a7c32f076bee774c7c8177c4276985adc0b6..9ed585b1386e4958fe8d458a0376a70e0ef70862 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -1,19 +1,19 @@ use agent_client_protocol as acp; use anyhow::Result; -use futures::{FutureExt as _, future::Shared}; -use gpui::{App, AppContext, Entity, SharedString, Task}; +use gpui::{App, Entity, SharedString, Task}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{ path::{Path, PathBuf}, + rc::Rc, sync::Arc, }; -use util::{ResultExt, get_system_shell, markdown::MarkdownInlineCode}; +use util::markdown::MarkdownInlineCode; -use crate::{AgentTool, ToolCallEventStream}; +use crate::{AgentTool, ThreadEnvironment, ToolCallEventStream}; -const COMMAND_OUTPUT_LIMIT: usize = 16 * 1024; +const COMMAND_OUTPUT_LIMIT: u64 = 16 * 1024; /// Executes a shell one-liner and returns the combined output. /// @@ -36,25 +36,14 @@ pub struct TerminalToolInput { pub struct TerminalTool { project: Entity, - determine_shell: Shared>, + environment: Rc, } impl TerminalTool { - pub fn new(project: Entity, cx: &mut App) -> Self { - let determine_shell = cx.background_spawn(async move { - if cfg!(windows) { - return get_system_shell(); - } - - if which::which("bash").is_ok() { - "bash".into() - } else { - get_system_shell() - } - }); + pub fn new(project: Entity, environment: Rc) -> Self { Self { project, - determine_shell: determine_shell.shared(), + environment, } } } @@ -99,128 +88,49 @@ impl AgentTool for TerminalTool { event_stream: ToolCallEventStream, cx: &mut App, ) -> Task> { - let language_registry = self.project.read(cx).languages().clone(); let working_dir = match working_dir(&input, &self.project, cx) { Ok(dir) => dir, Err(err) => return Task::ready(Err(err)), }; - let program = self.determine_shell.clone(); - let command = if cfg!(windows) { - format!("$null | & {{{}}}", input.command.replace("\"", "'")) - } else if let Some(cwd) = working_dir - .as_ref() - .and_then(|cwd| cwd.as_os_str().to_str()) - { - // Make sure once we're *inside* the shell, we cd into `cwd` - format!("(cd {cwd}; {}) self.project.update(cx, |project, cx| { - project.directory_environment(dir.as_path().into(), cx) - }), - None => Task::ready(None).shared(), - }; - - let env = cx.spawn(async move |_| { - let mut env = env.await.unwrap_or_default(); - if cfg!(unix) { - env.insert("PAGER".into(), "cat".into()); - } - env - }); let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx); + cx.spawn(async move |cx| { + authorize.await?; + + let terminal = self + .environment + .create_terminal( + input.command.clone(), + working_dir, + Some(COMMAND_OUTPUT_LIMIT), + cx, + ) + .await?; - cx.spawn({ - async move |cx| { - authorize.await?; - - let program = program.await; - let env = env.await; - let terminal = self - .project - .update(cx, |project, cx| { - project.create_terminal_task( - task::SpawnInTerminal { - command: Some(program), - args, - cwd: working_dir.clone(), - env, - ..Default::default() - }, - cx, - ) - })? - .await?; - let acp_terminal = cx.new(|cx| { - acp_thread::Terminal::new( - input.command.clone(), - working_dir.clone(), - terminal.clone(), - language_registry, - cx, - ) - })?; - event_stream.update_terminal(acp_terminal.clone()); - - let exit_status = terminal - .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))? - .await; - let (content, content_line_count) = terminal.read_with(cx, |terminal, _| { - (terminal.get_content(), terminal.total_lines()) - })?; - - let (processed_content, finished_with_empty_output) = process_content( - &content, - &input.command, - exit_status.map(portable_pty::ExitStatus::from), - ); + let terminal_id = terminal.id(cx)?; + event_stream.update_fields(acp::ToolCallUpdateFields { + content: Some(vec![acp::ToolCallContent::Terminal { terminal_id }]), + ..Default::default() + }); - acp_terminal - .update(cx, |terminal, cx| { - terminal.finish( - exit_status, - content.len(), - processed_content.len(), - content_line_count, - finished_with_empty_output, - cx, - ); - }) - .log_err(); + let exit_status = terminal.wait_for_exit(cx)?.await; + let output = terminal.current_output(cx)?; - Ok(processed_content) - } + Ok(process_content(output, &input.command, exit_status)) }) } } fn process_content( - content: &str, + output: acp::TerminalOutputResponse, command: &str, - exit_status: Option, -) -> (String, bool) { - let should_truncate = content.len() > COMMAND_OUTPUT_LIMIT; - - let content = if should_truncate { - let mut end_ix = COMMAND_OUTPUT_LIMIT.min(content.len()); - while !content.is_char_boundary(end_ix) { - end_ix -= 1; - } - // Don't truncate mid-line, clear the remainder of the last line - end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix); - &content[..end_ix] - } else { - content - }; - let content = content.trim(); + exit_status: acp::TerminalExitStatus, +) -> String { + let content = output.output.trim(); let is_empty = content.is_empty(); + let content = format!("```\n{content}\n```"); - let content = if should_truncate { + let content = if output.truncated { format!( "Command output too long. The first {} bytes:\n\n{content}", content.len(), @@ -229,24 +139,21 @@ fn process_content( content }; - let content = match exit_status { - Some(exit_status) if exit_status.success() => { + let content = match exit_status.exit_code { + Some(0) => { if is_empty { "Command executed successfully.".to_string() } else { content } } - Some(exit_status) => { + Some(exit_code) => { if is_empty { - format!( - "Command \"{command}\" failed with exit code {}.", - exit_status.exit_code() - ) + format!("Command \"{command}\" failed with exit code {}.", exit_code) } else { format!( "Command \"{command}\" failed with exit code {}.\n\n{content}", - exit_status.exit_code() + exit_code ) } } @@ -257,7 +164,7 @@ fn process_content( ) } }; - (content, is_empty) + content } fn working_dir( @@ -300,169 +207,3 @@ fn working_dir( anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees."); } } - -#[cfg(test)] -mod tests { - use agent_settings::AgentSettings; - use editor::EditorSettings; - use fs::RealFs; - use gpui::{BackgroundExecutor, TestAppContext}; - use pretty_assertions::assert_eq; - use serde_json::json; - use settings::{Settings, SettingsStore}; - use terminal::terminal_settings::TerminalSettings; - use theme::ThemeSettings; - use util::test::TempTree; - - use crate::ThreadEvent; - - use super::*; - - fn init_test(executor: &BackgroundExecutor, cx: &mut TestAppContext) { - zlog::init_test(); - - executor.allow_parking(); - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - Project::init_settings(cx); - ThemeSettings::register(cx); - TerminalSettings::register(cx); - EditorSettings::register(cx); - AgentSettings::register(cx); - }); - } - - #[gpui::test] - async fn test_interactive_command(executor: BackgroundExecutor, cx: &mut TestAppContext) { - if cfg!(windows) { - return; - } - - init_test(&executor, cx); - - let fs = Arc::new(RealFs::new(None, executor)); - let tree = TempTree::new(json!({ - "project": {}, - })); - let project: Entity = - Project::test(fs, [tree.path().join("project").as_path()], cx).await; - - let input = TerminalToolInput { - command: "cat".to_owned(), - cd: tree - .path() - .join("project") - .as_path() - .to_string_lossy() - .to_string(), - }; - let (event_stream_tx, mut event_stream_rx) = ToolCallEventStream::test(); - let result = cx - .update(|cx| Arc::new(TerminalTool::new(project, cx)).run(input, event_stream_tx, cx)); - - let auth = event_stream_rx.expect_authorization().await; - auth.response.send(auth.options[0].id.clone()).unwrap(); - event_stream_rx.expect_terminal().await; - assert_eq!(result.await.unwrap(), "Command executed successfully."); - } - - #[gpui::test] - async fn test_working_directory(executor: BackgroundExecutor, cx: &mut TestAppContext) { - if cfg!(windows) { - return; - } - - init_test(&executor, cx); - - let fs = Arc::new(RealFs::new(None, executor)); - let tree = TempTree::new(json!({ - "project": {}, - "other-project": {}, - })); - let project: Entity = - Project::test(fs, [tree.path().join("project").as_path()], cx).await; - - let check = |input, expected, cx: &mut TestAppContext| { - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let result = cx.update(|cx| { - Arc::new(TerminalTool::new(project.clone(), cx)).run(input, stream_tx, cx) - }); - cx.run_until_parked(); - let event = stream_rx.try_next(); - if let Ok(Some(Ok(ThreadEvent::ToolCallAuthorization(auth)))) = event { - auth.response.send(auth.options[0].id.clone()).unwrap(); - } - - cx.spawn(async move |_| { - let output = result.await; - assert_eq!(output.ok(), expected); - }) - }; - - check( - TerminalToolInput { - command: "pwd".into(), - cd: ".".into(), - }, - Some(format!( - "```\n{}\n```", - tree.path().join("project").display() - )), - cx, - ) - .await; - - check( - TerminalToolInput { - command: "pwd".into(), - cd: "other-project".into(), - }, - None, // other-project is a dir, but *not* a worktree (yet) - cx, - ) - .await; - - // Absolute path above the worktree root - check( - TerminalToolInput { - command: "pwd".into(), - cd: tree.path().to_string_lossy().into(), - }, - None, - cx, - ) - .await; - - project - .update(cx, |project, cx| { - project.create_worktree(tree.path().join("other-project"), true, cx) - }) - .await - .unwrap(); - - check( - TerminalToolInput { - command: "pwd".into(), - cd: "other-project".into(), - }, - Some(format!( - "```\n{}\n```", - tree.path().join("other-project").display() - )), - cx, - ) - .await; - - check( - TerminalToolInput { - command: "pwd".into(), - cd: ".".into(), - }, - None, - cx, - ) - .await; - } -} diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index b1d4bea5c35c113277847690906dd2f21e12050c..b29bfd5d8919f87594ccd26cbf9d7fdc3520ad30 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -134,6 +134,7 @@ impl AcpConnection { read_text_file: true, write_text_file: true, }, + terminal: true, }, }) .await?; @@ -344,11 +345,7 @@ impl acp::Client for ClientDelegate { let cx = &mut self.cx.clone(); let task = self - .sessions - .borrow() - .get(&arguments.session_id) - .context("Failed to get session")? - .thread + .session_thread(&arguments.session_id)? .update(cx, |thread, cx| { thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx) })??; @@ -364,11 +361,7 @@ impl acp::Client for ClientDelegate { ) -> Result<(), acp::Error> { let cx = &mut self.cx.clone(); let task = self - .sessions - .borrow() - .get(&arguments.session_id) - .context("Failed to get session")? - .thread + .session_thread(&arguments.session_id)? .update(cx, |thread, cx| { thread.write_text_file(arguments.path, arguments.content, cx) })?; @@ -382,16 +375,12 @@ impl acp::Client for ClientDelegate { &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| { + let task = self.session_thread(&arguments.session_id)?.update( + &mut self.cx.clone(), + |thread, cx| { thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx) - })?; + }, + )?; let content = task.await?; @@ -402,16 +391,92 @@ impl acp::Client for ClientDelegate { &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")?; + self.session_thread(¬ification.session_id)? + .update(&mut self.cx.clone(), |thread, cx| { + thread.handle_session_update(notification.update, cx) + })??; + + Ok(()) + } + + async fn create_terminal( + &self, + args: acp::CreateTerminalRequest, + ) -> Result { + let terminal = self + .session_thread(&args.session_id)? + .update(&mut self.cx.clone(), |thread, cx| { + thread.create_terminal( + args.command, + args.args, + args.env, + args.cwd, + args.output_byte_limit, + cx, + ) + })? + .await?; + Ok( + terminal.read_with(&self.cx, |terminal, _| acp::CreateTerminalResponse { + terminal_id: terminal.id().clone(), + })?, + ) + } - session.thread.update(cx, |thread, cx| { - thread.handle_session_update(notification.update, cx) - })??; + async fn kill_terminal(&self, args: acp::KillTerminalRequest) -> Result<(), acp::Error> { + self.session_thread(&args.session_id)? + .update(&mut self.cx.clone(), |thread, cx| { + thread.kill_terminal(args.terminal_id, cx) + })??; + + Ok(()) + } + + async fn release_terminal(&self, args: acp::ReleaseTerminalRequest) -> Result<(), acp::Error> { + self.session_thread(&args.session_id)? + .update(&mut self.cx.clone(), |thread, cx| { + thread.release_terminal(args.terminal_id, cx) + })??; Ok(()) } + + async fn terminal_output( + &self, + args: acp::TerminalOutputRequest, + ) -> Result { + self.session_thread(&args.session_id)? + .read_with(&mut self.cx.clone(), |thread, cx| { + let out = thread + .terminal(args.terminal_id)? + .read(cx) + .current_output(cx); + + Ok(out) + })? + } + + async fn wait_for_terminal_exit( + &self, + args: acp::WaitForTerminalExitRequest, + ) -> Result { + let exit_status = self + .session_thread(&args.session_id)? + .update(&mut self.cx.clone(), |thread, cx| { + anyhow::Ok(thread.terminal(args.terminal_id)?.read(cx).wait_for_exit()) + })?? + .await; + + Ok(acp::WaitForTerminalExitResponse { exit_status }) + } +} + +impl ClientDelegate { + fn session_thread(&self, session_id: &acp::SessionId) -> Result> { + let sessions = self.sessions.borrow(); + sessions + .get(session_id) + .context("Failed to get session") + .map(|session| session.thread.clone()) + } } diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 76b3709325a0c84a72bc71db8a67a3d4bd72dd06..0103219e31e8210440e66637cce8101d283210ea 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -125,22 +125,35 @@ impl EntryViewState { views }; + let is_tool_call_completed = + matches!(tool_call.status, acp_thread::ToolCallStatus::Completed); + for terminal in terminals { - views.entry(terminal.entity_id()).or_insert_with(|| { - let element = create_terminal( - self.workspace.clone(), - self.project.clone(), - terminal.clone(), - window, - cx, - ) - .into_any(); - cx.emit(EntryViewEvent { - entry_index: index, - view_event: ViewEvent::NewTerminal(id.clone()), - }); - element - }); + match views.entry(terminal.entity_id()) { + collections::hash_map::Entry::Vacant(entry) => { + let element = create_terminal( + self.workspace.clone(), + self.project.clone(), + terminal.clone(), + window, + cx, + ) + .into_any(); + cx.emit(EntryViewEvent { + entry_index: index, + view_event: ViewEvent::NewTerminal(id.clone()), + }); + entry.insert(element); + } + collections::hash_map::Entry::Occupied(_entry) => { + if is_tool_call_completed && terminal.read(cx).output().is_none() { + cx.emit(EntryViewEvent { + entry_index: index, + view_event: ViewEvent::TerminalMovedToBackground(id.clone()), + }); + } + } + } } for diff in diffs { @@ -217,6 +230,7 @@ pub struct EntryViewEvent { pub enum ViewEvent { NewDiff(ToolCallId), NewTerminal(ToolCallId), + TerminalMovedToBackground(ToolCallId), MessageEditorEvent(Entity, MessageEditorEvent), } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index eff9ceedd433ea8beb833108fb9fea1eb3f706da..5e842d713d3d2e20f71e5a0b5e5c1fce773bed8d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -827,6 +827,9 @@ impl AcpThreadView { self.expanded_tool_calls.insert(tool_call_id.clone()); } } + ViewEvent::TerminalMovedToBackground(tool_call_id) => { + self.expanded_tool_calls.remove(tool_call_id); + } ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => { if let Some(thread) = self.thread() && let Some(AgentThreadEntry::UserMessage(user_message)) = @@ -2418,7 +2421,8 @@ impl AcpThreadView { let output = terminal_data.output(); let command_finished = output.is_some(); - let truncated_output = output.is_some_and(|output| output.was_content_truncated); + let truncated_output = + output.is_some_and(|output| output.original_content_len > output.content.len()); let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0); let command_failed = command_finished @@ -2540,14 +2544,14 @@ impl AcpThreadView { .when(truncated_output, |header| { let tooltip = if let Some(output) = output { if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { - "Output exceeded terminal max lines and was \ - truncated, the model received the first 16 KB." - .to_string() + format!("Output exceeded terminal max lines and was \ + truncated, the model received the first {}.", format_file_size(output.content.len() as u64, true)) } else { format!( "Output is {} long, and to avoid unexpected token usage, \ - only 16 KB was sent back to the model.", + only {} was sent back to the agent.", format_file_size(output.original_content_len as u64, true), + format_file_size(output.content.len() as u64, true) ) } } else { From 965dbc988fae39cc9dfdd7cdac6f38e8234b3913 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 1 Sep 2025 15:33:11 -0400 Subject: [PATCH 484/744] gpui: Fix typo in Windows alpha correction shader (#37328) This PR fixes a typo in the Windows alpha correction shader that is now caught by https://github.com/zed-industries/zed/pull/37314. Another case that could be addressed by Bors. Release Notes: - N/A --- crates/gpui/src/platform/windows/alpha_correction.hlsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/windows/alpha_correction.hlsl b/crates/gpui/src/platform/windows/alpha_correction.hlsl index 7844a15f48bb27a137b913c94cba3fdc6d1fada9..dc8d0b5dc52e9ef1484bfdf776161b5d5d8ce1b9 100644 --- a/crates/gpui/src/platform/windows/alpha_correction.hlsl +++ b/crates/gpui/src/platform/windows/alpha_correction.hlsl @@ -1,5 +1,5 @@ float color_brightness(float3 color) { - // REC. 601 luminance coefficients for percieved brightness + // REC. 601 luminance coefficients for perceived brightness return dot(color, float3(0.30f, 0.59f, 0.11f)); } From 2ba25b5c940e9e2a9584001b7deb3ea783f8a259 Mon Sep 17 00:00:00 2001 From: claytonrcarter Date: Mon, 1 Sep 2025 16:00:01 -0400 Subject: [PATCH 485/744] editor: Support rewrap in block comments (#34418) This updates `editor: rewrap` to work within doc comments, based on the code that extends such comments on newline. I added some tests, and I've tested it out in JS, C and PHP. (Though PHP depends on https://github.com/zed-extensions/php/pull/40) Closes #19794 Closes #18221 **Caveat:** ~~This will not rewrap an existing single-line block comment, such as the one provided in #18221:~~ this will now rewrap as expected ```c /* we can triangulate any convex polygon by picking a vertex and connecting it to the next two vertices; we first read two vertices, and then, for every subsequent vertex, we can form a triangle by connecting it to the first and previous vertex */ ``` However, it will rewrap a similar comment if it is shaped like a doc comment. In other words, this will rewrap as expected: ```c /* * we can triangulate any convex polygon by picking a vertex and connecting it to the next two vertices; we first read two vertices, and then, for every subsequent vertex, we can form a triangle by connecting it to the first and previous vertex */ ``` This seems like a reasonable improvement and limitation to me, especially as a first step. cc @smitbarmase because I think that you've been making a lot of the `newline` and `rewrap` changes recently. (Thank you for those, by the way!) Release Notes: - Added support for rewrap in block comments. --------- Co-authored-by: Smit Barmase --- crates/editor/src/editor.rs | 215 +++++++++++---- crates/editor/src/editor_tests.rs | 425 +++++++++++++++++++++++++++++- 2 files changed, 581 insertions(+), 59 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 04780e79f84c6f762b246bfb662eb693675e5d38..d5621e8165cb7afe1acb869479e780f960ccb269 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11816,6 +11816,18 @@ impl Editor { let buffer = self.buffer.read(cx).snapshot(cx); let selections = self.selections.all::(cx); + #[derive(Clone, Debug, PartialEq)] + enum CommentFormat { + /// single line comment, with prefix for line + Line(String), + /// single line within a block comment, with prefix for line + BlockLine(String), + /// a single line of a block comment that includes the initial delimiter + BlockCommentWithStart(BlockCommentConfig), + /// a single line of a block comment that includes the ending delimiter + BlockCommentWithEnd(BlockCommentConfig), + } + // Split selections to respect paragraph, indent, and comment prefix boundaries. let wrap_ranges = selections.into_iter().flat_map(|selection| { let mut non_blank_rows_iter = (selection.start.row..=selection.end.row) @@ -11832,37 +11844,75 @@ impl Editor { let language_scope = buffer.language_scope_at(selection.head()); let indent_and_prefix_for_row = - |row: u32| -> (IndentSize, Option, Option) { + |row: u32| -> (IndentSize, Option, Option) { let indent = buffer.indent_size_for_line(MultiBufferRow(row)); - let (comment_prefix, rewrap_prefix) = - if let Some(language_scope) = &language_scope { - let indent_end = Point::new(row, indent.len); - let comment_prefix = language_scope + let (comment_prefix, rewrap_prefix) = if let Some(language_scope) = + &language_scope + { + let indent_end = Point::new(row, indent.len); + let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row))); + let line_text_after_indent = buffer + .text_for_range(indent_end..line_end) + .collect::(); + + let is_within_comment_override = buffer + .language_scope_at(indent_end) + .is_some_and(|scope| scope.override_name() == Some("comment")); + let comment_delimiters = if is_within_comment_override { + // we are within a comment syntax node, but we don't + // yet know what kind of comment: block, doc or line + match ( + language_scope.documentation_comment(), + language_scope.block_comment(), + ) { + (Some(config), _) | (_, Some(config)) + if buffer.contains_str_at(indent_end, &config.start) => + { + Some(CommentFormat::BlockCommentWithStart(config.clone())) + } + (Some(config), _) | (_, Some(config)) + if line_text_after_indent.ends_with(config.end.as_ref()) => + { + Some(CommentFormat::BlockCommentWithEnd(config.clone())) + } + (Some(config), _) | (_, Some(config)) + if buffer.contains_str_at(indent_end, &config.prefix) => + { + Some(CommentFormat::BlockLine(config.prefix.to_string())) + } + (_, _) => language_scope + .line_comment_prefixes() + .iter() + .find(|prefix| buffer.contains_str_at(indent_end, prefix)) + .map(|prefix| CommentFormat::Line(prefix.to_string())), + } + } else { + // we not in an overridden comment node, but we may + // be within a non-overridden line comment node + language_scope .line_comment_prefixes() .iter() .find(|prefix| buffer.contains_str_at(indent_end, prefix)) - .map(|prefix| prefix.to_string()); - let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row))); - let line_text_after_indent = buffer - .text_for_range(indent_end..line_end) - .collect::(); - let rewrap_prefix = language_scope - .rewrap_prefixes() - .iter() - .find_map(|prefix_regex| { - prefix_regex.find(&line_text_after_indent).map(|mat| { - if mat.start() == 0 { - Some(mat.as_str().to_string()) - } else { - None - } - }) - }) - .flatten(); - (comment_prefix, rewrap_prefix) - } else { - (None, None) + .map(|prefix| CommentFormat::Line(prefix.to_string())) }; + + let rewrap_prefix = language_scope + .rewrap_prefixes() + .iter() + .find_map(|prefix_regex| { + prefix_regex.find(&line_text_after_indent).map(|mat| { + if mat.start() == 0 { + Some(mat.as_str().to_string()) + } else { + None + } + }) + }) + .flatten(); + (comment_delimiters, rewrap_prefix) + } else { + (None, None) + }; (indent, comment_prefix, rewrap_prefix) }; @@ -11873,22 +11923,22 @@ impl Editor { let mut prev_row = first_row; let ( mut current_range_indent, - mut current_range_comment_prefix, + mut current_range_comment_delimiters, mut current_range_rewrap_prefix, ) = indent_and_prefix_for_row(first_row); for row in non_blank_rows_iter.skip(1) { let has_paragraph_break = row > prev_row + 1; - let (row_indent, row_comment_prefix, row_rewrap_prefix) = + let (row_indent, row_comment_delimiters, row_rewrap_prefix) = indent_and_prefix_for_row(row); let has_indent_change = row_indent != current_range_indent; - let has_comment_change = row_comment_prefix != current_range_comment_prefix; + let has_comment_change = row_comment_delimiters != current_range_comment_delimiters; let has_boundary_change = has_comment_change || row_rewrap_prefix.is_some() - || (has_indent_change && current_range_comment_prefix.is_some()); + || (has_indent_change && current_range_comment_delimiters.is_some()); if has_paragraph_break || has_boundary_change { ranges.push(( @@ -11896,13 +11946,13 @@ impl Editor { Point::new(current_range_start, 0) ..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))), current_range_indent, - current_range_comment_prefix.clone(), + current_range_comment_delimiters.clone(), current_range_rewrap_prefix.clone(), from_empty_selection, )); current_range_start = row; current_range_indent = row_indent; - current_range_comment_prefix = row_comment_prefix; + current_range_comment_delimiters = row_comment_delimiters; current_range_rewrap_prefix = row_rewrap_prefix; } prev_row = row; @@ -11913,7 +11963,7 @@ impl Editor { Point::new(current_range_start, 0) ..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))), current_range_indent, - current_range_comment_prefix, + current_range_comment_delimiters, current_range_rewrap_prefix, from_empty_selection, )); @@ -11927,7 +11977,7 @@ impl Editor { for ( language_settings, wrap_range, - indent_size, + mut indent_size, comment_prefix, rewrap_prefix, from_empty_selection, @@ -11947,16 +11997,26 @@ impl Editor { let tab_size = language_settings.tab_size; + let (line_prefix, inside_comment) = match &comment_prefix { + Some(CommentFormat::Line(prefix) | CommentFormat::BlockLine(prefix)) => { + (Some(prefix.as_str()), true) + } + Some(CommentFormat::BlockCommentWithEnd(BlockCommentConfig { prefix, .. })) => { + (Some(prefix.as_ref()), true) + } + Some(CommentFormat::BlockCommentWithStart(BlockCommentConfig { + start: _, + end: _, + prefix, + tab_size, + })) => { + indent_size.len += tab_size; + (Some(prefix.as_ref()), true) + } + None => (None, false), + }; let indent_prefix = indent_size.chars().collect::(); - let mut line_prefix = indent_prefix.clone(); - let mut inside_comment = false; - if let Some(prefix) = &comment_prefix { - line_prefix.push_str(prefix); - inside_comment = true; - } - if let Some(prefix) = &rewrap_prefix { - line_prefix.push_str(prefix); - } + let line_prefix = format!("{indent_prefix}{}", line_prefix.unwrap_or("")); let allow_rewrap_based_on_language = match language_settings.allow_rewrap { RewrapBehavior::InComments => inside_comment, @@ -12001,6 +12061,8 @@ impl Editor { let start_offset = start.to_offset(&buffer); let end = Point::new(end_row, buffer.line_len(MultiBufferRow(end_row))); let selection_text = buffer.text_for_range(start..end).collect::(); + let mut first_line_delimiter = None; + let mut last_line_delimiter = None; let Some(lines_without_prefixes) = selection_text .lines() .enumerate() @@ -12008,6 +12070,46 @@ impl Editor { let line_trimmed = line.trim_start(); if rewrap_prefix.is_some() && ix > 0 { Ok(line_trimmed) + } else if let Some( + CommentFormat::BlockCommentWithStart(BlockCommentConfig { + start, + prefix, + end, + tab_size, + }) + | CommentFormat::BlockCommentWithEnd(BlockCommentConfig { + start, + prefix, + end, + tab_size, + }), + ) = &comment_prefix + { + let line_trimmed = line_trimmed + .strip_prefix(start.as_ref()) + .map(|s| { + let mut indent_size = indent_size; + indent_size.len -= tab_size; + let indent_prefix: String = indent_size.chars().collect(); + first_line_delimiter = Some((indent_prefix, start)); + s.trim_start() + }) + .unwrap_or(line_trimmed); + let line_trimmed = line_trimmed + .strip_suffix(end.as_ref()) + .map(|s| { + last_line_delimiter = Some(end); + s.trim_end() + }) + .unwrap_or(line_trimmed); + let line_trimmed = line_trimmed + .strip_prefix(prefix.as_ref()) + .unwrap_or(line_trimmed); + Ok(line_trimmed) + } else if let Some(CommentFormat::BlockLine(prefix)) = &comment_prefix { + line_trimmed.strip_prefix(prefix).with_context(|| { + format!("line did not start with prefix {prefix:?}: {line:?}") + }) } else { line_trimmed .strip_prefix(&line_prefix.trim_start()) @@ -12034,14 +12136,25 @@ impl Editor { line_prefix.clone() }; - let wrapped_text = wrap_with_prefix( - line_prefix, - subsequent_lines_prefix, - lines_without_prefixes.join("\n"), - wrap_column, - tab_size, - options.preserve_existing_whitespace, - ); + let wrapped_text = { + let mut wrapped_text = wrap_with_prefix( + line_prefix, + subsequent_lines_prefix, + lines_without_prefixes.join("\n"), + wrap_column, + tab_size, + options.preserve_existing_whitespace, + ); + + if let Some((indent, delimiter)) = first_line_delimiter { + wrapped_text = format!("{indent}{delimiter}\n{wrapped_text}"); + } + if let Some(last_line) = last_line_delimiter { + wrapped_text = format!("{wrapped_text}\n{indent_prefix}{last_line}"); + } + + wrapped_text + }; // TODO: should always use char-based diff while still supporting cursor behavior that // matches vim. diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 10ebae8e27a07115de1e202187f491026bd7f503..ddfd32be8d11f421ff9cd41aa49996bd27dd06fb 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5561,14 +5561,18 @@ async fn test_rewrap(cx: &mut TestAppContext) { }, None, )); - let rust_language = Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - line_comments: vec!["// ".into(), "/// ".into()], - ..LanguageConfig::default() - }, - Some(tree_sitter_rust::LANGUAGE.into()), - )); + let rust_language = Arc::new( + Language::new( + LanguageConfig { + name: "Rust".into(), + line_comments: vec!["// ".into(), "/// ".into()], + ..LanguageConfig::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_override_query("[(line_comment)(block_comment)] @comment.inclusive") + .unwrap(), + ); let plaintext_language = Arc::new(Language::new( LanguageConfig { @@ -5884,6 +5888,411 @@ async fn test_rewrap(cx: &mut TestAppContext) { } } +#[gpui::test] +async fn test_rewrap_block_comments(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.languages.0.extend([( + "Rust".into(), + LanguageSettingsContent { + allow_rewrap: Some(language_settings::RewrapBehavior::InComments), + preferred_line_length: Some(40), + ..Default::default() + }, + )]) + }); + + let mut cx = EditorTestContext::new(cx).await; + + let rust_lang = Arc::new( + Language::new( + LanguageConfig { + name: "Rust".into(), + line_comments: vec!["// ".into()], + block_comment: Some(BlockCommentConfig { + start: "/*".into(), + end: "*/".into(), + prefix: "* ".into(), + tab_size: 1, + }), + documentation_comment: Some(BlockCommentConfig { + start: "/**".into(), + end: "*/".into(), + prefix: "* ".into(), + tab_size: 1, + }), + + ..LanguageConfig::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_override_query("[(line_comment) (block_comment)] @comment.inclusive") + .unwrap(), + ); + + // regular block comment + assert_rewrap( + indoc! {" + /* + *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. + */ + /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ + "}, + indoc! {" + /* + *ˇ Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ + /* + *ˇ Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ + "}, + rust_lang.clone(), + &mut cx, + ); + + // indent is respected + assert_rewrap( + indoc! {" + {} + /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ + "}, + indoc! {" + {} + /* + *ˇ Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ + "}, + rust_lang.clone(), + &mut cx, + ); + + // short block comments with inline delimiters + assert_rewrap( + indoc! {" + /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ + /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. + */ + /* + *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ + "}, + indoc! {" + /* + *ˇ Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ + /* + *ˇ Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ + /* + *ˇ Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ + "}, + rust_lang.clone(), + &mut cx, + ); + + // multiline block comment with inline start/end delimiters + assert_rewrap( + indoc! {" + /*ˇ Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. */ + "}, + indoc! {" + /* + *ˇ Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ + "}, + rust_lang.clone(), + &mut cx, + ); + + // block comment rewrap still respects paragraph bounds + assert_rewrap( + indoc! {" + /* + *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * + * Lorem ipsum dolor sit amet, consectetur adipiscing elit. + */ + "}, + indoc! {" + /* + *ˇ Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + * + * Lorem ipsum dolor sit amet, consectetur adipiscing elit. + */ + "}, + rust_lang.clone(), + &mut cx, + ); + + // documentation comments + assert_rewrap( + indoc! {" + /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ + /** + *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. + */ + "}, + indoc! {" + /** + *ˇ Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ + /** + *ˇ Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ + "}, + rust_lang.clone(), + &mut cx, + ); + + // different, adjacent comments + assert_rewrap( + indoc! {" + /** + *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. + */ + /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ + //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. + "}, + indoc! {" + /** + *ˇ Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ + /* + *ˇ Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ + //ˇ Lorem ipsum dolor sit amet, + // consectetur adipiscing elit. + "}, + rust_lang.clone(), + &mut cx, + ); + + // selection w/ single short block comment + assert_rewrap( + indoc! {" + «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ» + "}, + indoc! {" + «/* + * Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ˇ» + "}, + rust_lang.clone(), + &mut cx, + ); + + // rewrapping a single comment w/ abutting comments + assert_rewrap( + indoc! {" + /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */ + /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ + "}, + indoc! {" + /* + * ˇLorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ + /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ + "}, + rust_lang.clone(), + &mut cx, + ); + + // selection w/ non-abutting short block comments + assert_rewrap( + indoc! {" + «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ + + /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ» + "}, + indoc! {" + «/* + * Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ + + /* + * Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ˇ» + "}, + rust_lang.clone(), + &mut cx, + ); + + // selection of multiline block comments + assert_rewrap( + indoc! {" + «/* Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. */ˇ» + "}, + indoc! {" + «/* + * Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ˇ» + "}, + rust_lang.clone(), + &mut cx, + ); + + // partial selection of multiline block comments + assert_rewrap( + indoc! {" + «/* Lorem ipsum dolor sit amet,ˇ» + * consectetur adipiscing elit. */ + /* Lorem ipsum dolor sit amet, + «* consectetur adipiscing elit. */ˇ» + "}, + indoc! {" + «/* + * Lorem ipsum dolor sit amet,ˇ» + * consectetur adipiscing elit. */ + /* Lorem ipsum dolor sit amet, + «* consectetur adipiscing elit. + */ˇ» + "}, + rust_lang.clone(), + &mut cx, + ); + + // selection w/ abutting short block comments + // TODO: should not be combined; should rewrap as 2 comments + assert_rewrap( + indoc! {" + «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ + /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ» + "}, + // desired behavior: + // indoc! {" + // «/* + // * Lorem ipsum dolor sit amet, + // * consectetur adipiscing elit. + // */ + // /* + // * Lorem ipsum dolor sit amet, + // * consectetur adipiscing elit. + // */ˇ» + // "}, + // actual behaviour: + indoc! {" + «/* + * Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. Lorem + * ipsum dolor sit amet, consectetur + * adipiscing elit. + */ˇ» + "}, + rust_lang.clone(), + &mut cx, + ); + + // TODO: same as above, but with delimiters on separate line + // assert_rewrap( + // indoc! {" + // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. + // */ + // /* + // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ» + // "}, + // // desired: + // // indoc! {" + // // «/* + // // * Lorem ipsum dolor sit amet, + // // * consectetur adipiscing elit. + // // */ + // // /* + // // * Lorem ipsum dolor sit amet, + // // * consectetur adipiscing elit. + // // */ˇ» + // // "}, + // // actual: (but with trailing w/s on the empty lines) + // indoc! {" + // «/* + // * Lorem ipsum dolor sit amet, + // * consectetur adipiscing elit. + // * + // */ + // /* + // * + // * Lorem ipsum dolor sit amet, + // * consectetur adipiscing elit. + // */ˇ» + // "}, + // rust_lang.clone(), + // &mut cx, + // ); + + // TODO these are unhandled edge cases; not correct, just documenting known issues + assert_rewrap( + indoc! {" + /* + //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. + */ + /* + //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ + /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */ + "}, + // desired: + // indoc! {" + // /* + // *ˇ Lorem ipsum dolor sit amet, + // * consectetur adipiscing elit. + // */ + // /* + // *ˇ Lorem ipsum dolor sit amet, + // * consectetur adipiscing elit. + // */ + // /* + // *ˇ Lorem ipsum dolor sit amet + // */ /* consectetur adipiscing elit. */ + // "}, + // actual: + indoc! {" + /* + //ˇ Lorem ipsum dolor sit amet, + // consectetur adipiscing elit. + */ + /* + * //ˇ Lorem ipsum dolor sit amet, + * consectetur adipiscing elit. + */ + /* + *ˇ Lorem ipsum dolor sit amet */ /* + * consectetur adipiscing elit. + */ + "}, + rust_lang, + &mut cx, + ); + + #[track_caller] + fn assert_rewrap( + unwrapped_text: &str, + wrapped_text: &str, + language: Arc, + cx: &mut EditorTestContext, + ) { + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(unwrapped_text); + cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx)); + cx.assert_editor_state(wrapped_text); + } +} + #[gpui::test] async fn test_hard_wrap(cx: &mut TestAppContext) { init_test(cx, |_| {}); From 634a1343ddf402bf72b5e153984542aee0dc2315 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 2 Sep 2025 00:51:15 +0300 Subject: [PATCH 486/744] Bump xcb dependency (#37335) Deals with https://github.com/zed-industries/zed/security/dependabot/65 Release Notes: - N/A --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2ba36a91c445514ac9c8e1932b9b0135ca36b0b..abbe5e0297b9db27c30be0085282d6c7463e7c90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20136,9 +20136,9 @@ dependencies = [ [[package]] name = "xcb" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1e2f212bb1a92cd8caac8051b829a6582ede155ccb60b5d5908b81b100952be" +checksum = "f07c123b796139bfe0603e654eaf08e132e52387ba95b252c78bad3640ba37ea" dependencies = [ "bitflags 1.3.2", "libc", From 8a8a9a4f079ef5a66252ad73d01476d45891e647 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 1 Sep 2025 16:53:43 -0500 Subject: [PATCH 487/744] settings_ui: Add dynamic settings UI item (#37331) Closes #ISSUE Adds a first draft of a way for "Dynamic" settings items to be added, where Dynamic means settings where multiple sets of options are possible (i.e. discriminated union, rust enum, etc). The implementation is very similar to that of `Group`, except that instead of rendering all of it's descendants, it contains a function to determine _which_ descendant to render, whether that be a single item or a nested group of items. Currently this is done in a type-unsafe way with indices, a future improvement could be to make the API more type safe, and easier to manually implement correctly. An example of a "Dynamic" setting is `theme`, where it can either be a string of the desired theme name, or an object with `mode: "light" | "dark" | "system"` as well as theme names for `light` and `dark`. In the system implemented by this PR, this would become a dynamic settings UI item, where option `0` is a single item, the theme name selector, and option `1` is a group, containing items for the `mode`, and `light`/`dark` options. Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings/src/settings_ui.rs | 9 ++ crates/settings_ui/src/settings_ui.rs | 113 ++++++++++++------ .../src/settings_ui_macros.rs | 6 +- 3 files changed, 93 insertions(+), 35 deletions(-) diff --git a/crates/settings/src/settings_ui.rs b/crates/settings/src/settings_ui.rs index 8b30ebc9d5968943d3814f7569d1367d389e386a..40ac3d9db9f82625f58007b182d6fb2ffb43a648 100644 --- a/crates/settings/src/settings_ui.rs +++ b/crates/settings/src/settings_ui.rs @@ -29,6 +29,11 @@ pub enum SettingsUiEntryVariant { path: &'static str, item: SettingsUiItemSingle, }, + Dynamic { + path: &'static str, + options: Vec, + determine_option: fn(&serde_json::Value, &mut App) -> usize, + }, // todo(settings_ui): remove None, } @@ -90,6 +95,10 @@ pub enum SettingsUiItem { items: Vec, }, Single(SettingsUiItemSingle), + Dynamic { + options: Vec, + determine_option: fn(&serde_json::Value, &mut App) -> usize, + }, None, } diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index ae03170a1a9a2cb3e53c67402c95c8e79e739ab9..37edfd5679d259b1d81159f02472fc682bf17243 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -1,6 +1,7 @@ mod appearance_settings_controls; use std::any::TypeId; +use std::collections::VecDeque; use std::ops::{Not, Range}; use anyhow::Context as _; @@ -131,7 +132,7 @@ impl Item for SettingsPage { // - there should be an index of text -> item mappings, for using fuzzy::match // - Do we want to show the parent groups when a item is matched? -struct UIEntry { +struct UiEntry { title: &'static str, path: &'static str, _depth: usize, @@ -147,22 +148,46 @@ struct UIEntry { next_sibling: Option, // expanded: bool, render: Option, + select_descendant: Option usize>, +} + +impl UiEntry { + fn first_descendant_index(&self) -> Option { + return self + .descendant_range + .is_empty() + .not() + .then_some(self.descendant_range.start); + } + + fn nth_descendant_index(&self, tree: &[UiEntry], n: usize) -> Option { + let first_descendant_index = self.first_descendant_index()?; + let mut current_index = 0; + let mut current_descendant_index = Some(first_descendant_index); + while let Some(descendant_index) = current_descendant_index + && current_index < n + { + current_index += 1; + current_descendant_index = tree[descendant_index].next_sibling; + } + current_descendant_index + } } struct SettingsUiTree { root_entry_indices: Vec, - entries: Vec, + entries: Vec, active_entry_index: usize, } fn build_tree_item( - tree: &mut Vec, - group: SettingsUiEntryVariant, + tree: &mut Vec, + entry: SettingsUiEntryVariant, depth: usize, prev_index: Option, ) { let index = tree.len(); - tree.push(UIEntry { + tree.push(UiEntry { title: "", path: "", _depth: depth, @@ -170,11 +195,12 @@ fn build_tree_item( total_descendant_range: index + 1..index + 1, render: None, next_sibling: None, + select_descendant: None, }); if let Some(prev_index) = prev_index { tree[prev_index].next_sibling = Some(index); } - match group { + match entry { SettingsUiEntryVariant::Group { path, title, @@ -199,6 +225,24 @@ fn build_tree_item( tree[index].title = path; tree[index].render = Some(item); } + SettingsUiEntryVariant::Dynamic { + path, + options, + determine_option, + } => { + tree[index].path = path; + tree[index].select_descendant = Some(determine_option); + for option in options { + let prev_index = tree[index] + .descendant_range + .is_empty() + .not() + .then_some(tree[index].descendant_range.end - 1); + tree[index].descendant_range.end = tree.len() + 1; + build_tree_item(tree, option.item, depth + 1, prev_index); + tree[index].total_descendant_range.end = tree.len(); + } + } SettingsUiEntryVariant::None => { return; } @@ -265,27 +309,28 @@ fn render_content( window: &mut Window, cx: &mut Context, ) -> impl IntoElement { - let Some(entry) = tree.entries.get(tree.active_entry_index) else { + let Some(active_entry) = tree.entries.get(tree.active_entry_index) else { return div() .size_full() .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error)); }; let mut content = v_flex().size_full().gap_4(); - let mut child_index = entry - .descendant_range - .is_empty() - .not() - .then_some(entry.descendant_range.start); - let mut path = smallvec::smallvec![entry.path]; + let mut path = smallvec::smallvec![active_entry.path]; + let mut entry_index_queue = VecDeque::new(); - while let Some(index) = child_index { - let child = &tree.entries[index]; - child_index = child.next_sibling; - if child.render.is_none() { - // todo(settings_ui): subgroups? - continue; + if let Some(child_index) = active_entry.first_descendant_index() { + entry_index_queue.push_back(child_index); + let mut index = child_index; + while let Some(next_sibling_index) = tree.entries[index].next_sibling { + entry_index_queue.push_back(next_sibling_index); + index = next_sibling_index; } + }; + + while let Some(index) = entry_index_queue.pop_front() { + // todo(settings_ui): subgroups? + let child = &tree.entries[index]; path.push(child.path); let settings_value = settings_value_from_settings_and_path( path.clone(), @@ -294,24 +339,23 @@ fn render_content( SettingsStore::global(cx).raw_user_settings(), SettingsStore::global(cx).raw_default_settings(), ); + if let Some(select_descendant) = child.select_descendant { + let selected_descendant = select_descendant(settings_value.read(), cx); + if let Some(descendant_index) = + child.nth_descendant_index(&tree.entries, selected_descendant) + { + entry_index_queue.push_front(descendant_index); + } + } + path.pop(); + let Some(child_render) = child.render.as_ref() else { + continue; + }; content = content.child( div() - .child( - Label::new(SharedString::new_static(tree.entries[index].title)) - .size(LabelSize::Large) - .when(tree.active_entry_index == index, |this| { - this.color(Color::Selected) - }), - ) - .child(render_item_single( - settings_value, - child.render.as_ref().unwrap(), - window, - cx, - )), + .child(Label::new(SharedString::new_static(child.title)).size(LabelSize::Large)) + .child(render_item_single(settings_value, child_render, window, cx)), ); - - path.pop(); } return content; @@ -405,6 +449,7 @@ fn read_settings_value_from_path<'a>( settings_contents: &'a serde_json::Value, path: &[&'static str], ) -> Option<&'a serde_json::Value> { + // todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested let Some((key, remaining)) = path.split_first() else { return Some(settings_contents); }; diff --git a/crates/settings_ui_macros/src/settings_ui_macros.rs b/crates/settings_ui_macros/src/settings_ui_macros.rs index 6e37745a7c24155de631e47ffc8c265209ee24e8..5250febe98cb17c74cf03d909e430b1415e29569 100644 --- a/crates/settings_ui_macros/src/settings_ui_macros.rs +++ b/crates/settings_ui_macros/src/settings_ui_macros.rs @@ -12,7 +12,6 @@ use syn::{Data, DeriveInput, LitStr, Token, parse_macro_input}; /// /// ``` /// use settings::SettingsUi; -/// use settings_ui_macros::SettingsUi; /// /// #[derive(SettingsUi)] /// #[settings_ui(group = "Standard")] @@ -102,6 +101,11 @@ fn map_ui_item_to_render(path: &str, ty: TokenStream) -> TokenStream { path: #path, item, }, + settings::SettingsUiItem::Dynamic{ options, determine_option } => settings::SettingsUiEntryVariant::Dynamic { + path: #path, + options, + determine_option, + }, settings::SettingsUiItem::None => settings::SettingsUiEntryVariant::None, } } From 60d17cccd35b919d6226ae86559c4505d7c1e833 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 1 Sep 2025 17:42:33 -0500 Subject: [PATCH 488/744] settings_ui: Move settings UI trait to file content (#37337) Closes #ISSUE Initially, the `SettingsUi` trait was tied to `Settings`, however, given that the `Settings::FileContent` type (which may be the same as the type that implements `Settings`) will be the type that more directly maps to the JSON structure (and therefore have the documentation, correct field names (or `serde` rename attributes), etc) it makes more sense to have the deriving of `SettingsUi` occur on the `FileContent` type rather than the `Settings` type. In order for this to work a relatively important change had to be made to the derive macro, that being that it now "unwraps" options into their inner type, so a field with type `Option` where `Foo: SettingsUi` will treat the field as if it were just `Foo`, expecting there to be a default set in `default.json`. This imposes some restrictions on what `Settings::FileContent` can be as seen in 1e19398 where `FileContent` itself can't be optional without manually implementing `SettingsUi`, as well as introducing some risk that if the `FileContent` type has `serde(default)`, the default value will override the default value from `default.json` in the UI even though it may differ (but it should!). A future PR should probably replace the other settings with `FileContent = Option` (all of which currently have `T == bool`) with wrapper structs and have `KEY = None` so the further niceties `derive(SettingsUi)` will provide such as path renaming, custom UI, auto naming and doc comment extraction can be used. Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/agent_settings/src/agent_settings.rs | 4 +-- crates/audio/src/audio_settings.rs | 4 +-- crates/auto_update/src/auto_update.rs | 13 ++++----- crates/call/src/call_settings.rs | 4 +-- crates/client/src/client.rs | 12 ++++----- crates/collab_ui/src/panel_settings.rs | 10 +++---- crates/dap/src/debugger_settings.rs | 2 ++ crates/editor/src/editor_settings.rs | 4 +-- .../file_finder/src/file_finder_settings.rs | 4 +-- crates/git_ui/src/git_panel_settings.rs | 4 +-- crates/go_to_line/src/cursor_position.rs | 10 +++---- crates/language/src/language_settings.rs | 4 +-- crates/language_models/src/settings.rs | 4 +-- crates/onboarding/src/base_keymap_picker.rs | 2 +- crates/onboarding/src/basics_page.rs | 2 +- .../src/outline_panel_settings.rs | 4 +-- .../src/project_panel_settings.rs | 4 +-- .../recent_projects/src/remote_connections.rs | 4 +-- crates/repl/src/jupyter_settings.rs | 4 +-- crates/settings/src/base_keymap_setting.rs | 23 +++++++++++----- crates/settings/src/settings_store.rs | 18 ++++++------- crates/settings/src/settings_ui.rs | 23 ++++++++-------- .../src/settings_ui_macros.rs | 27 +++++++++++++++++++ crates/terminal/src/terminal_settings.rs | 4 +-- crates/theme/src/settings.rs | 4 +-- crates/title_bar/src/title_bar_settings.rs | 6 ++--- crates/vim/src/vim.rs | 4 +-- crates/workspace/src/item.rs | 8 +++--- crates/workspace/src/workspace_settings.rs | 10 +++---- crates/worktree/src/worktree_settings.rs | 4 +-- 30 files changed, 136 insertions(+), 94 deletions(-) diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 3e21e18a11ba68726f15d88bec93b95f01f89500..8aebdcd288c8451d9bc391f1fc1598d6098d55af 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -48,7 +48,7 @@ pub enum NotifyWhenAgentWaiting { Never, } -#[derive(Default, Clone, Debug, SettingsUi)] +#[derive(Default, Clone, Debug)] pub struct AgentSettings { pub enabled: bool, pub button: bool, @@ -223,7 +223,7 @@ impl AgentSettingsContent { } } -#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)] +#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi)] pub struct AgentSettingsContent { /// Whether the Agent is enabled. /// diff --git a/crates/audio/src/audio_settings.rs b/crates/audio/src/audio_settings.rs index e42918825cd3a25bb18d6f0b357801949520833f..d30d950273f2138f3bd54c573513373574f1ce43 100644 --- a/crates/audio/src/audio_settings.rs +++ b/crates/audio/src/audio_settings.rs @@ -4,7 +4,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct AudioSettings { /// Opt into the new audio system. #[serde(rename = "experimental.rodio_audio", default)] @@ -12,7 +12,7 @@ pub struct AudioSettings { } /// Configuration of audio in Zed. -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] #[serde(default)] pub struct AudioSettingsContent { /// Whether to use the experimental audio system diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 71dcf25aeea9d8ebd4feb01db9161dc177fcdd26..f0ae3fdb1cfef667a9f737aa6545a42046a9d322 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -113,20 +113,19 @@ impl Drop for MacOsUnmounter { } } -#[derive(SettingsUi)] struct AutoUpdateSetting(bool); /// Whether or not to automatically check for updates. /// /// Default: true -#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)] +#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi)] #[serde(transparent)] struct AutoUpdateSettingContent(bool); impl Settings for AutoUpdateSetting { const KEY: Option<&'static str> = Some("auto_update"); - type FileContent = Option; + type FileContent = AutoUpdateSettingContent; fn load(sources: SettingsSources, _: &mut App) -> Result { let auto_update = [ @@ -136,17 +135,19 @@ impl Settings for AutoUpdateSetting { sources.user, ] .into_iter() - .find_map(|value| value.copied().flatten()) - .unwrap_or(sources.default.ok_or_else(Self::missing_default)?); + .find_map(|value| value.copied()) + .unwrap_or(*sources.default); Ok(Self(auto_update.0)) } fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { - vscode.enum_setting("update.mode", current, |s| match s { + let mut cur = &mut Some(*current); + vscode.enum_setting("update.mode", &mut cur, |s| match s { "none" | "manual" => Some(AutoUpdateSettingContent(false)), _ => Some(AutoUpdateSettingContent(true)), }); + *current = cur.unwrap(); } } diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs index 64d11d0df64eedbbc29f06b8205f0318d999ea30..7b0838e3a96185c1e4c33b8116fbd6a41b35f3dc 100644 --- a/crates/call/src/call_settings.rs +++ b/crates/call/src/call_settings.rs @@ -4,14 +4,14 @@ use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Deserialize, Debug, SettingsUi)] +#[derive(Deserialize, Debug)] pub struct CallSettings { pub mute_on_join: bool, pub share_on_join: bool, } /// Configuration of voice calls in Zed. -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] pub struct CallSettingsContent { /// Whether the microphone should be muted when joining a channel or a call. /// diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index c5bb1af0d7605cfcfc28d86bc389189d653e28ae..1287b4563c99cbd387b3a18d98fbbc734e55e4db 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -96,12 +96,12 @@ actions!( ] ); -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct ClientSettingsContent { server_url: Option, } -#[derive(Deserialize, SettingsUi)] +#[derive(Deserialize)] pub struct ClientSettings { pub server_url: String, } @@ -122,12 +122,12 @@ impl Settings for ClientSettings { fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } -#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct ProxySettingsContent { proxy: Option, } -#[derive(Deserialize, Default, SettingsUi)] +#[derive(Deserialize, Default)] pub struct ProxySettings { pub proxy: Option, } @@ -520,14 +520,14 @@ impl Drop for PendingEntitySubscription { } } -#[derive(Copy, Clone, Deserialize, Debug, SettingsUi)] +#[derive(Copy, Clone, Deserialize, Debug)] pub struct TelemetrySettings { pub diagnostics: bool, pub metrics: bool, } /// Control what info is collected by Zed. -#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] pub struct TelemetrySettingsContent { /// Send debug info like crash reports. /// diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index 4e5c8ad8f005d00a8802ab0a1f79ff7fbb3d0861..64f0a9366df7cdef1f2c05809752fb1cf912111b 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/crates/collab_ui/src/panel_settings.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources, SettingsUi}; use workspace::dock::DockPosition; -#[derive(Deserialize, Debug, SettingsUi)] +#[derive(Deserialize, Debug)] pub struct CollaborationPanelSettings { pub button: bool, pub dock: DockPosition, @@ -20,14 +20,14 @@ pub enum ChatPanelButton { WhenInCall, } -#[derive(Deserialize, Debug, SettingsUi)] +#[derive(Deserialize, Debug)] pub struct ChatPanelSettings { pub button: ChatPanelButton, pub dock: DockPosition, pub default_width: Pixels, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] pub struct ChatPanelSettingsContent { /// When to show the panel button in the status bar. /// @@ -43,14 +43,14 @@ pub struct ChatPanelSettingsContent { pub default_width: Option, } -#[derive(Deserialize, Debug, SettingsUi)] +#[derive(Deserialize, Debug)] pub struct NotificationPanelSettings { pub button: bool, pub dock: DockPosition, pub default_width: Pixels, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] pub struct PanelSettingsContent { /// Whether to show the panel button in the status bar. /// diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs index 6843f19e3811967084cc61a3874ec86451ab6faf..929bff747e8685ec9a4b36fa9db63d12a769faa2 100644 --- a/crates/dap/src/debugger_settings.rs +++ b/crates/dap/src/debugger_settings.rs @@ -14,6 +14,8 @@ pub enum DebugPanelDockPosition { #[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi)] #[serde(default)] +// todo(settings_ui) @ben: I'm pretty sure not having the fields be optional here is a bug, +// it means the defaults will override previously set values if a single key is missing #[settings_ui(group = "Debugger", path = "debugger")] pub struct DebuggerSettings { /// Determines the stepping granularity. diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 084c4eb5c618cbf3d290b317b0035f1b8f307b3f..3e4e86f023530b89fa3e325474ecea801ff06ecb 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -11,7 +11,7 @@ use util::serde::default_true; /// Imports from the VSCode settings at /// https://code.visualstudio.com/docs/reference/default-settings -#[derive(Deserialize, Clone, SettingsUi)] +#[derive(Deserialize, Clone)] pub struct EditorSettings { pub cursor_blink: bool, pub cursor_shape: Option, @@ -415,7 +415,7 @@ pub enum SnippetSortOrder { None, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct EditorSettingsContent { /// Whether the cursor blinks in the editor. /// diff --git a/crates/file_finder/src/file_finder_settings.rs b/crates/file_finder/src/file_finder_settings.rs index 20057417a2ddbce7acd7fd5a8e09e54aab779638..007af53b1144ed4caa7985d75cdf4707f13ed13e 100644 --- a/crates/file_finder/src/file_finder_settings.rs +++ b/crates/file_finder/src/file_finder_settings.rs @@ -3,7 +3,7 @@ use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Deserialize, Debug, Clone, Copy, PartialEq, SettingsUi)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] pub struct FileFinderSettings { pub file_icons: bool, pub modal_max_width: Option, @@ -11,7 +11,7 @@ pub struct FileFinderSettings { pub include_ignored: Option, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] pub struct FileFinderSettingsContent { /// Whether to show file icons in the file finder. /// diff --git a/crates/git_ui/src/git_panel_settings.rs b/crates/git_ui/src/git_panel_settings.rs index 576949220405e408df1b23d189e661405c4c39e4..39d6540db52046845521a23c0290be4e6e595492 100644 --- a/crates/git_ui/src/git_panel_settings.rs +++ b/crates/git_ui/src/git_panel_settings.rs @@ -36,7 +36,7 @@ pub enum StatusStyle { LabelColor, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] pub struct GitPanelSettingsContent { /// Whether to show the panel button in the status bar. /// @@ -77,7 +77,7 @@ pub struct GitPanelSettingsContent { pub collapse_untracked_diff: Option, } -#[derive(Deserialize, Debug, Clone, PartialEq, SettingsUi)] +#[derive(Deserialize, Debug, Clone, PartialEq)] pub struct GitPanelSettings { pub button: bool, pub dock: DockPosition, diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 345af8a867c6ff6c1790450d2b28cd275c04ebbb..5840993ece84b1e8098ee341395e7f77fb791ace 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -293,7 +293,7 @@ impl StatusItemView for CursorPosition { } } -#[derive(Clone, Copy, Default, PartialEq, JsonSchema, Deserialize, Serialize, SettingsUi)] +#[derive(Clone, Copy, Default, PartialEq, JsonSchema, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub(crate) enum LineIndicatorFormat { Short, @@ -301,14 +301,14 @@ pub(crate) enum LineIndicatorFormat { Long, } -#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)] +#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi)] #[serde(transparent)] pub(crate) struct LineIndicatorFormatContent(LineIndicatorFormat); impl Settings for LineIndicatorFormat { const KEY: Option<&'static str> = Some("line_indicator_format"); - type FileContent = Option; + type FileContent = LineIndicatorFormatContent; fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { let format = [ @@ -317,8 +317,8 @@ impl Settings for LineIndicatorFormat { sources.user, ] .into_iter() - .find_map(|value| value.copied().flatten()) - .unwrap_or(sources.default.ok_or_else(Self::missing_default)?); + .find_map(|value| value.copied()) + .unwrap_or(*sources.default); Ok(format.0) } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index a44df4993af5f29cbfce337d2c90dd8f840d97a6..f04b83bc7336143672647a07107fa27bc55f5823 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -55,7 +55,7 @@ pub fn all_language_settings<'a>( } /// The settings for all languages. -#[derive(Debug, Clone, SettingsUi)] +#[derive(Debug, Clone)] pub struct AllLanguageSettings { /// The edit prediction settings. pub edit_predictions: EditPredictionSettings, @@ -292,7 +292,7 @@ pub struct CopilotSettings { } /// The settings for all languages. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct AllLanguageSettingsContent { /// The settings for enabling/disabling features. #[serde(default)] diff --git a/crates/language_models/src/settings.rs b/crates/language_models/src/settings.rs index 1d03ab48f7de3ab9a20c1a099803e6b759b8ea81..8b7ab5fc2547bd0b014238739f1b940dad831f66 100644 --- a/crates/language_models/src/settings.rs +++ b/crates/language_models/src/settings.rs @@ -29,7 +29,7 @@ pub fn init_settings(cx: &mut App) { AllLanguageModelSettings::register(cx); } -#[derive(Default, SettingsUi)] +#[derive(Default)] pub struct AllLanguageModelSettings { pub anthropic: AnthropicSettings, pub bedrock: AmazonBedrockSettings, @@ -46,7 +46,7 @@ pub struct AllLanguageModelSettings { pub zed_dot_dev: ZedDotDevSettings, } -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, SettingsUi)] pub struct AllLanguageModelSettingsContent { pub anthropic: Option, pub bedrock: Option, diff --git a/crates/onboarding/src/base_keymap_picker.rs b/crates/onboarding/src/base_keymap_picker.rs index 0ac07d9a9d3b17921112d6accf6f4c9c9dd65ef6..79a716cc9eaf36a77668900f63ba42a896a334eb 100644 --- a/crates/onboarding/src/base_keymap_picker.rs +++ b/crates/onboarding/src/base_keymap_picker.rs @@ -187,7 +187,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate { ); update_settings_file::(self.fs.clone(), cx, move |setting, _| { - *setting = Some(base_keymap) + setting.base_keymap = Some(base_keymap) }); } diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 441d2ca4b748e43c8c5db41a113c54e185b2f1de..991386cb389b1ac5bdeb2e76bae4a210fe3b2cce 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -325,7 +325,7 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE let fs = ::global(cx); update_settings_file::(fs, cx, move |setting, _| { - *setting = Some(keymap_base); + setting.base_keymap = Some(keymap_base); }); } } diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index c33125654f043022bfaa7a31200d43d1d6326607..48c6621e3509c1eda69a6a5e92602ba2ab12a484 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -18,7 +18,7 @@ pub enum ShowIndentGuides { Never, } -#[derive(Deserialize, Debug, Clone, Copy, PartialEq, SettingsUi)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] pub struct OutlinePanelSettings { pub button: bool, pub default_width: Pixels, @@ -61,7 +61,7 @@ pub struct IndentGuidesSettingsContent { pub show: Option, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] pub struct OutlinePanelSettingsContent { /// Whether to show the outline panel button in the status bar. /// diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index 9c7bd4fd66e9e5b884867bf13f88856c126974b6..db9b2b85d545e85a0cff3ec13a8f75e28dac88fa 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -28,7 +28,7 @@ pub enum EntrySpacing { Standard, } -#[derive(Deserialize, Debug, Clone, Copy, PartialEq, SettingsUi)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] pub struct ProjectPanelSettings { pub button: bool, pub hide_gitignore: bool, @@ -92,7 +92,7 @@ pub enum ShowDiagnostics { All, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] pub struct ProjectPanelSettingsContent { /// Whether to show the project panel button in the status bar. /// diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs index 47607813b547e28b9b4a37449f8daaa6ec022764..e543bf219ff0bc8226e819798a9ea74a098d0f98 100644 --- a/crates/recent_projects/src/remote_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -30,7 +30,7 @@ use ui::{ use util::serde::default_true; use workspace::{AppState, ModalView, Workspace}; -#[derive(Deserialize, SettingsUi)] +#[derive(Deserialize)] pub struct SshSettings { pub ssh_connections: Option>, /// Whether to read ~/.ssh/config for ssh connection sources. @@ -121,7 +121,7 @@ pub struct SshProject { pub paths: Vec, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct RemoteSettingsContent { pub ssh_connections: Option>, pub read_ssh_config: Option, diff --git a/crates/repl/src/jupyter_settings.rs b/crates/repl/src/jupyter_settings.rs index c3bfd2079dfae21c9b990b15faec4cf7d4ffaa68..6f3d6b1db631267e9b41ae7598d6e573387f2ac6 100644 --- a/crates/repl/src/jupyter_settings.rs +++ b/crates/repl/src/jupyter_settings.rs @@ -6,7 +6,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Debug, Default, SettingsUi)] +#[derive(Debug, Default)] pub struct JupyterSettings { pub kernel_selections: HashMap, } @@ -20,7 +20,7 @@ impl JupyterSettings { } } -#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] pub struct JupyterSettingsContent { /// Default kernels to select for each language. /// diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs index 087f25185a99cb927892e3ada22d92c1c319a390..fb5b445b49a1fdbfac34ce8bc1a3d17d8241e009 100644 --- a/crates/settings/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -100,25 +100,36 @@ impl BaseKeymap { } } +#[derive( + Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default, SettingsUi, +)] +// extracted so that it can be an option, and still work with derive(SettingsUi) +pub struct BaseKeymapSetting { + pub base_keymap: Option, +} + impl Settings for BaseKeymap { - const KEY: Option<&'static str> = Some("base_keymap"); + const KEY: Option<&'static str> = None; - type FileContent = Option; + type FileContent = BaseKeymapSetting; fn load( sources: SettingsSources, _: &mut gpui::App, ) -> anyhow::Result { - if let Some(Some(user_value)) = sources.user.copied() { + if let Some(Some(user_value)) = sources.user.map(|setting| setting.base_keymap) { return Ok(user_value); } - if let Some(Some(server_value)) = sources.server.copied() { + if let Some(Some(server_value)) = sources.server.map(|setting| setting.base_keymap) { return Ok(server_value); } - sources.default.ok_or_else(Self::missing_default) + sources + .default + .base_keymap + .ok_or_else(Self::missing_default) } fn import_from_vscode(_vscode: &VsCodeSettings, current: &mut Self::FileContent) { - *current = Some(BaseKeymap::VSCode); + current.base_keymap = Some(BaseKeymap::VSCode); } } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 023f8cbfba3d96b0a6cad2e1c6ebb930f0bcdf9e..c1a7fd9e3c4812b78627ae6899ce068d499cbdf7 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -39,7 +39,7 @@ use crate::{ /// A value that can be defined as a user setting. /// /// Settings can be loaded from a combination of multiple JSON files. -pub trait Settings: SettingsUi + 'static + Send + Sync { +pub trait Settings: 'static + Send + Sync { /// The name of a key within the JSON file from which this setting should /// be deserialized. If this is `None`, then the setting will be deserialized /// from the root object. @@ -57,7 +57,7 @@ pub trait Settings: SettingsUi + 'static + Send + Sync { const PRESERVED_KEYS: Option<&'static [&'static str]> = None; /// The type that is stored in an individual JSON file. - type FileContent: Clone + Default + Serialize + DeserializeOwned + JsonSchema; + type FileContent: Clone + Default + Serialize + DeserializeOwned + JsonSchema + SettingsUi; /// The logic for combining together values from one or more JSON files into the /// final value for this setting. @@ -1565,7 +1565,7 @@ impl AnySettingValue for SettingValue { } fn settings_ui_item(&self) -> SettingsUiEntry { - ::settings_ui_entry() + <::FileContent as SettingsUi>::settings_ui_entry() } } @@ -2147,12 +2147,12 @@ mod tests { } } - #[derive(Debug, Deserialize, PartialEq, SettingsUi)] + #[derive(Debug, Deserialize, PartialEq)] struct TurboSetting(bool); impl Settings for TurboSetting { const KEY: Option<&'static str> = Some("turbo"); - type FileContent = Option; + type FileContent = bool; fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() @@ -2161,7 +2161,7 @@ mod tests { fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut Self::FileContent) {} } - #[derive(Clone, Debug, PartialEq, Deserialize, SettingsUi)] + #[derive(Clone, Debug, PartialEq, Deserialize)] struct MultiKeySettings { #[serde(default)] key1: String, @@ -2169,7 +2169,7 @@ mod tests { key2: String, } - #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] + #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] struct MultiKeySettingsJson { key1: Option, key2: Option, @@ -2194,7 +2194,7 @@ mod tests { } } - #[derive(Debug, Deserialize, SettingsUi)] + #[derive(Debug, Deserialize)] struct JournalSettings { pub path: String, pub hour_format: HourFormat, @@ -2207,7 +2207,7 @@ mod tests { Hour24, } - #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] + #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)] struct JournalSettingsJson { pub path: Option, pub hour_format: Option, diff --git a/crates/settings/src/settings_ui.rs b/crates/settings/src/settings_ui.rs index 40ac3d9db9f82625f58007b182d6fb2ffb43a648..3a77627d5976f20c5732b47a1dfc164bc0a74b58 100644 --- a/crates/settings/src/settings_ui.rs +++ b/crates/settings/src/settings_ui.rs @@ -7,9 +7,16 @@ use crate::SettingsStore; pub trait SettingsUi { fn settings_ui_item() -> SettingsUiItem { + // todo(settings_ui): remove this default impl, only entry should have a default impl + // because it's expected that the macro or custom impl use the item and the known paths to create the entry SettingsUiItem::None } - fn settings_ui_entry() -> SettingsUiEntry; + + fn settings_ui_entry() -> SettingsUiEntry { + SettingsUiEntry { + item: SettingsUiEntryVariant::None, + } + } } pub struct SettingsUiEntry { @@ -106,11 +113,11 @@ impl SettingsUi for bool { fn settings_ui_item() -> SettingsUiItem { SettingsUiItem::Single(SettingsUiItemSingle::SwitchField) } +} - fn settings_ui_entry() -> SettingsUiEntry { - SettingsUiEntry { - item: SettingsUiEntryVariant::None, - } +impl SettingsUi for Option { + fn settings_ui_item() -> SettingsUiItem { + SettingsUiItem::Single(SettingsUiItemSingle::SwitchField) } } @@ -118,10 +125,4 @@ impl SettingsUi for u64 { fn settings_ui_item() -> SettingsUiItem { SettingsUiItem::Single(SettingsUiItemSingle::NumericStepper) } - - fn settings_ui_entry() -> SettingsUiEntry { - SettingsUiEntry { - item: SettingsUiEntryVariant::None, - } - } } diff --git a/crates/settings_ui_macros/src/settings_ui_macros.rs b/crates/settings_ui_macros/src/settings_ui_macros.rs index 5250febe98cb17c74cf03d909e430b1415e29569..3840bc38da88a37fdbf17b8b06795f81c58ad132 100644 --- a/crates/settings_ui_macros/src/settings_ui_macros.rs +++ b/crates/settings_ui_macros/src/settings_ui_macros.rs @@ -88,7 +88,34 @@ pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenSt proc_macro::TokenStream::from(expanded) } +fn extract_type_from_option(ty: TokenStream) -> TokenStream { + match option_inner_type(ty.clone()) { + Some(inner_type) => inner_type, + None => ty, + } +} + +fn option_inner_type(ty: TokenStream) -> Option { + let ty = syn::parse2::(ty).ok()?; + let syn::Type::Path(path) = ty else { + return None; + }; + let segment = path.path.segments.last()?; + if segment.ident != "Option" { + return None; + } + let syn::PathArguments::AngleBracketed(args) = &segment.arguments else { + return None; + }; + let arg = args.args.first()?; + let syn::GenericArgument::Type(ty) = arg else { + return None; + }; + return Some(ty.to_token_stream()); +} + fn map_ui_item_to_render(path: &str, ty: TokenStream) -> TokenStream { + let ty = extract_type_from_option(ty); quote! { settings::SettingsUiEntry { item: match #ty::settings_ui_item() { diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 01f2d85f09e416b6c8ac40d7fa283d1f1e296cd5..c3051e089c68e3df0733c9e6cf7c8a42f56e742d 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -24,7 +24,7 @@ pub struct Toolbar { pub breadcrumbs: bool, } -#[derive(Clone, Debug, Deserialize, SettingsUi)] +#[derive(Clone, Debug, Deserialize)] pub struct TerminalSettings { pub shell: Shell, pub working_directory: WorkingDirectory, @@ -135,7 +135,7 @@ pub enum ActivateScript { Pyenv, } -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct TerminalSettingsContent { /// What shell to use when opening a terminal. /// diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 61b41eba0642f10312a4c78df447ac7344f7e2dc..11db22d97485f5d400abdd8638da501abd55a192 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -87,7 +87,7 @@ impl From for String { } /// Customizable settings for the UI and theme system. -#[derive(Clone, PartialEq, SettingsUi)] +#[derive(Clone, PartialEq)] pub struct ThemeSettings { /// The UI font size. Determines the size of text in the UI, /// as well as the size of a [gpui::Rems] unit. @@ -365,7 +365,7 @@ impl IconThemeSelection { } /// Settings for rendering text in UI and text buffers. -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct ThemeSettingsContent { /// The default font size for text in the UI. #[serde(default)] diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs index 29d74c8590a63cd8aa75bdaa3655111d76fcf757..0dc301f7eef6789bf1c0a2ad51cb63dff77d0337 100644 --- a/crates/title_bar/src/title_bar_settings.rs +++ b/crates/title_bar/src/title_bar_settings.rs @@ -3,8 +3,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Copy, Clone, Deserialize, Debug, SettingsUi)] -#[settings_ui(group = "Title Bar", path = "title_bar")] +#[derive(Copy, Clone, Deserialize, Debug)] pub struct TitleBarSettings { pub show_branch_icon: bool, pub show_onboarding_banner: bool, @@ -15,7 +14,8 @@ pub struct TitleBarSettings { pub show_menus: bool, } -#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] +#[settings_ui(group = "Title Bar", path = "title_bar")] pub struct TitleBarSettingsContent { /// Whether to show the branch icon beside branch switcher in the title bar. /// diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index a5cd909d5b53079d1da49591a5eca21416ba415a..5a4ac425183e1843db7075c0f5054a16f82948f9 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1774,7 +1774,7 @@ struct CursorShapeSettings { pub insert: Option, } -#[derive(Deserialize, SettingsUi)] +#[derive(Deserialize)] struct VimSettings { pub default_mode: Mode, pub toggle_relative_line_numbers: bool, @@ -1785,7 +1785,7 @@ struct VimSettings { pub cursor_shape: CursorShapeSettings, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] struct VimSettingsContent { pub default_mode: Option, pub toggle_relative_line_numbers: Option, diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index a513f8c9317645469e5d5ca54c3b5351383c1ca3..731e1691479ad7eec1388c174d85081a177127cc 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -49,7 +49,7 @@ impl Default for SaveOptions { } } -#[derive(Deserialize, SettingsUi)] +#[derive(Deserialize)] pub struct ItemSettings { pub git_status: bool, pub close_position: ClosePosition, @@ -59,7 +59,7 @@ pub struct ItemSettings { pub show_close_button: ShowCloseButton, } -#[derive(Deserialize, SettingsUi)] +#[derive(Deserialize)] pub struct PreviewTabsSettings { pub enabled: bool, pub enable_preview_from_file_finder: bool, @@ -101,7 +101,7 @@ pub enum ActivateOnClose { LeftNeighbour, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct ItemSettingsContent { /// Whether to show the Git file status on a tab item. /// @@ -130,7 +130,7 @@ pub struct ItemSettingsContent { show_close_button: Option, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct PreviewTabsSettingsContent { /// Whether to show opened editors as preview tabs. /// Preview tabs do not stay open, are reused until explicitly set to be kept open opened (via double-click or editing) and show file names in italic. diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 419e33e54435779012207a024ea49e44a8acb1c2..1a7e548e4eda1f41e36c6ad0883cdd57be8828d7 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -8,7 +8,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Deserialize, SettingsUi)] +#[derive(Deserialize)] pub struct WorkspaceSettings { pub active_pane_modifiers: ActivePanelModifiers, pub bottom_dock_layout: BottomDockLayout, @@ -118,7 +118,7 @@ pub enum RestoreOnStartupBehavior { LastSession, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct WorkspaceSettingsContent { /// Active pane styling settings. pub active_pane_modifiers: Option, @@ -216,14 +216,14 @@ pub struct WorkspaceSettingsContent { pub zoomed_padding: Option, } -#[derive(Deserialize, SettingsUi)] +#[derive(Deserialize)] pub struct TabBarSettings { pub show: bool, pub show_nav_history_buttons: bool, pub show_tab_bar_buttons: bool, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct TabBarSettingsContent { /// Whether or not to show the tab bar in the editor. /// @@ -266,7 +266,7 @@ pub enum PaneSplitDirectionVertical { Right, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)] #[serde(rename_all = "snake_case")] pub struct CenteredLayoutSettings { /// The relative width of the left padding of the central pane from the diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index df3a4d35570ad21b80f968539afbe681c58e2a06..6a8e2b5d89b0201b81f45817adb439fe85e24d91 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources, SettingsUi}; use util::paths::PathMatcher; -#[derive(Clone, PartialEq, Eq, SettingsUi)] +#[derive(Clone, PartialEq, Eq)] pub struct WorktreeSettings { pub file_scan_inclusions: PathMatcher, pub file_scan_exclusions: PathMatcher, @@ -31,7 +31,7 @@ impl WorktreeSettings { } } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct WorktreeSettingsContent { /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides /// `file_scan_inclusions`. From 54cec5b484c3e71b5fe47331e1e21ad0cf801425 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 1 Sep 2025 19:26:42 -0500 Subject: [PATCH 489/744] settings_ui: Get editor settings working (#37330) Closes #ISSUE This PR includes the necessary work to get `EditorSettings` showing up in the settings UI. Including making the `path` field on `SettingsUiItem`'s optional so that top level items such as `EditorSettings` which have `Settings::KEY = None` (i.e. are treated like `serde(flatten)`) have their paths computed correctly for JSON reading/updating. It includes the first examples of a pattern I expect to continue with the `SettingsUi` work with respect to settings reorganization, that being adding missing defaults, and adding explicit values (or aliases) to settings which previously relied on `null` being a value for optional fields. Release Notes: - N/A *or* Added/Fixed/Improved ... --- assets/settings/default.json | 8 +- crates/editor/src/editor_settings.rs | 59 +++++--- crates/language/src/buffer.rs | 6 +- crates/project/src/project_settings.rs | 13 +- crates/settings/src/settings.rs | 4 +- crates/settings/src/settings_store.rs | 2 +- .../{settings_ui.rs => settings_ui_core.rs} | 109 +++++++++----- crates/settings_ui/src/settings_ui.rs | 137 ++++++++++++------ .../src/settings_ui_macros.rs | 60 +++----- 9 files changed, 246 insertions(+), 152 deletions(-) rename crates/settings/src/{settings_ui.rs => settings_ui_core.rs} (51%) diff --git a/assets/settings/default.json b/assets/settings/default.json index 623a4612d06975ca4681d75a775d061e594608b2..0b5481bd4e4e2177302e38199bb66e87471d2904 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -188,8 +188,8 @@ // 4. A box drawn around the following character // "hollow" // - // Default: not set, defaults to "bar" - "cursor_shape": null, + // Default: "bar" + "cursor_shape": "bar", // Determines when the mouse cursor should be hidden in an editor or input box. // // 1. Never hide the mouse cursor: @@ -282,8 +282,8 @@ // - "warning" // - "info" // - "hint" - // - null — allow all diagnostics (default) - "diagnostics_max_severity": null, + // - "all" — allow all diagnostics (default) + "diagnostics_max_severity": "all", // Whether to show wrap guides (vertical rulers) in the editor. // Setting this to true will show a guide at the 'preferred_line_length' value // if 'soft_wrap' is set to 'preferred_line_length', and will show any diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 3e4e86f023530b89fa3e325474ecea801ff06ecb..44cb0749760e9e3af91bc837df0ef0589e251703 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -61,7 +61,9 @@ pub struct EditorSettings { } /// How to render LSP `textDocument/documentColor` colors in the editor. -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive( + Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi, +)] #[serde(rename_all = "snake_case")] pub enum DocumentColorsRenderMode { /// Do not query and render document colors. @@ -75,7 +77,7 @@ pub enum DocumentColorsRenderMode { Background, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)] #[serde(rename_all = "snake_case")] pub enum CurrentLineHighlight { // Don't highlight the current line. @@ -89,7 +91,7 @@ pub enum CurrentLineHighlight { } /// When to populate a new search's query based on the text under the cursor. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)] #[serde(rename_all = "snake_case")] pub enum SeedQuerySetting { /// Always populate the search query with the word under the cursor. @@ -101,7 +103,9 @@ pub enum SeedQuerySetting { } /// What to do when multibuffer is double clicked in some of its excerpts (parts of singleton buffers). -#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive( + Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi, +)] #[serde(rename_all = "snake_case")] pub enum DoubleClickInMultibuffer { /// Behave as a regular buffer and select the whole word. @@ -120,7 +124,9 @@ pub struct Jupyter { pub enabled: bool, } -#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive( + Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi, +)] #[serde(rename_all = "snake_case")] pub struct JupyterContent { /// Whether the Jupyter feature is enabled. @@ -292,7 +298,9 @@ pub struct ScrollbarAxes { } /// Whether to allow drag and drop text selection in buffer. -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive( + Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi, +)] pub struct DragAndDropSelection { /// When true, enables drag and drop text selection in buffer. /// @@ -332,7 +340,7 @@ pub enum ScrollbarDiagnostics { /// The key to use for adding multiple cursors /// /// Default: alt -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)] #[serde(rename_all = "snake_case")] pub enum MultiCursorModifier { Alt, @@ -343,7 +351,7 @@ pub enum MultiCursorModifier { /// Whether the editor will scroll beyond the last line. /// /// Default: one_page -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)] #[serde(rename_all = "snake_case")] pub enum ScrollBeyondLastLine { /// The editor will not scroll beyond the last line. @@ -357,7 +365,9 @@ pub enum ScrollBeyondLastLine { } /// Default options for buffer and project search items. -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive( + Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi, +)] pub struct SearchSettings { /// Whether to show the project search button in the status bar. #[serde(default = "default_true")] @@ -373,7 +383,9 @@ pub struct SearchSettings { } /// What to do when go to definition yields no results. -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive( + Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi, +)] #[serde(rename_all = "snake_case")] pub enum GoToDefinitionFallback { /// Disables the fallback. @@ -386,7 +398,9 @@ pub enum GoToDefinitionFallback { /// Determines when the mouse cursor should be hidden in an editor or input box. /// /// Default: on_typing_and_movement -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive( + Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi, +)] #[serde(rename_all = "snake_case")] pub enum HideMouseMode { /// Never hide the mouse cursor @@ -401,7 +415,9 @@ pub enum HideMouseMode { /// Determines how snippets are sorted relative to other completion items. /// /// Default: inline -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive( + Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi, +)] #[serde(rename_all = "snake_case")] pub enum SnippetSortOrder { /// Place snippets at the top of the completion list @@ -416,6 +432,7 @@ pub enum SnippetSortOrder { } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[settings_ui(group = "Editor")] pub struct EditorSettingsContent { /// Whether the cursor blinks in the editor. /// @@ -424,7 +441,7 @@ pub struct EditorSettingsContent { /// Cursor shape for the default editor. /// Can be "bar", "block", "underline", or "hollow". /// - /// Default: None + /// Default: bar pub cursor_shape: Option, /// Determines when the mouse cursor should be hidden in an editor or input box. /// @@ -601,7 +618,7 @@ pub struct EditorSettingsContent { } // Status bar related settings -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)] pub struct StatusBarContent { /// Whether to display the active language button in the status bar. /// @@ -614,7 +631,7 @@ pub struct StatusBarContent { } // Toolbar related settings -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)] pub struct ToolbarContent { /// Whether to display breadcrumbs in the editor toolbar. /// @@ -640,7 +657,9 @@ pub struct ToolbarContent { } /// Scrollbar related settings -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] +#[derive( + Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default, SettingsUi, +)] pub struct ScrollbarContent { /// When to show the scrollbar in the editor. /// @@ -675,7 +694,9 @@ pub struct ScrollbarContent { } /// Minimap related settings -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[derive( + Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi, +)] pub struct MinimapContent { /// When to show the minimap in the editor. /// @@ -723,7 +744,9 @@ pub struct ScrollbarAxesContent { } /// Gutter related settings -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive( + Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi, +)] pub struct GutterContent { /// Whether to show line numbers in the gutter. /// diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1a1d9fb4a7dc3a3d2a847cee3661361343a6871e..c978f6c4ef9f60e092d67a655adc6d95693788a8 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -32,7 +32,7 @@ use parking_lot::Mutex; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; -use settings::WorktreeId; +use settings::{SettingsUi, WorktreeId}; use smallvec::SmallVec; use smol::future::yield_now; use std::{ @@ -173,7 +173,9 @@ pub enum IndentKind { } /// The shape of a selection cursor. -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive( + Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi, +)] #[serde(rename_all = "snake_case")] pub enum CursorShape { /// A vertical bar diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 30a71c4caeb676509239151a4766beb590fdb47e..4a97130f15d582df392c25d6d64482bc4ca17834 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -281,7 +281,17 @@ impl Default for GlobalLspSettings { } #[derive( - Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema, + Clone, + Copy, + Debug, + Eq, + PartialEq, + Ord, + PartialOrd, + Serialize, + Deserialize, + JsonSchema, + SettingsUi, )] #[serde(rename_all = "snake_case")] pub enum DiagnosticSeverity { @@ -290,6 +300,7 @@ pub enum DiagnosticSeverity { Error, Warning, Info, + #[serde(alias = "all")] Hint, } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 983cd31dd31d6b9c2cd017568fffe0812f9ae4e5..7e567cc085101713b0f6b100d0b47f6bf4c3531f 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -4,7 +4,7 @@ mod keymap_file; mod settings_file; mod settings_json; mod settings_store; -mod settings_ui; +mod settings_ui_core; mod vscode_import; use gpui::{App, Global}; @@ -24,7 +24,7 @@ pub use settings_store::{ InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, SettingsStore, }; -pub use settings_ui::*; +pub use settings_ui_core::*; // Re-export the derive macro pub use settings_ui_macros::SettingsUi; pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource}; diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index c1a7fd9e3c4812b78627ae6899ce068d499cbdf7..60eb132ad8b4f6419f463f32b1874ea97be07ec1 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -33,7 +33,7 @@ pub type EditorconfigProperties = ec4rs::Properties; use crate::{ ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, SettingsUiEntry, VsCodeSettings, WorktreeId, parse_json_with_comments, replace_value_in_json_text, - settings_ui::SettingsUi, update_value_in_json_text, + settings_ui_core::SettingsUi, update_value_in_json_text, }; /// A value that can be defined as a user setting. diff --git a/crates/settings/src/settings_ui.rs b/crates/settings/src/settings_ui_core.rs similarity index 51% rename from crates/settings/src/settings_ui.rs rename to crates/settings/src/settings_ui_core.rs index 3a77627d5976f20c5732b47a1dfc164bc0a74b58..3a5fa3016b78ff90bdf9c6166255809f4f89b1d2 100644 --- a/crates/settings/src/settings_ui.rs +++ b/crates/settings/src/settings_ui_core.rs @@ -1,3 +1,5 @@ +use std::any::TypeId; + use anyhow::Context as _; use fs::Fs; use gpui::{AnyElement, App, AppContext as _, ReadGlobal as _, Window}; @@ -14,40 +16,26 @@ pub trait SettingsUi { fn settings_ui_entry() -> SettingsUiEntry { SettingsUiEntry { - item: SettingsUiEntryVariant::None, + path: None, + title: "None entry", + item: SettingsUiItem::None, } } } pub struct SettingsUiEntry { // todo(settings_ui): move this back here once there isn't a None variant - // pub path: &'static str, - // pub title: &'static str, - pub item: SettingsUiEntryVariant, -} - -pub enum SettingsUiEntryVariant { - Group { - path: &'static str, - title: &'static str, - items: Vec, - }, - Item { - path: &'static str, - item: SettingsUiItemSingle, - }, - Dynamic { - path: &'static str, - options: Vec, - determine_option: fn(&serde_json::Value, &mut App) -> usize, - }, - // todo(settings_ui): remove - None, + /// The path in the settings JSON file for this setting. Relative to parent + /// None implies `#[serde(flatten)]` or `Settings::KEY.is_none()` for top level settings + pub path: Option<&'static str>, + pub title: &'static str, + pub item: SettingsUiItem, } pub enum SettingsUiItemSingle { SwitchField, - NumericStepper, + /// A numeric stepper for a specific type of number + NumericStepper(NumType), ToggleGroup(&'static [&'static str]), /// This should be used when toggle group size > 6 DropDown(&'static [&'static str]), @@ -96,16 +84,19 @@ impl SettingsValue { } } +pub struct SettingsUiItemDynamic { + pub options: Vec, + pub determine_option: fn(&serde_json::Value, &mut App) -> usize, +} + +pub struct SettingsUiItemGroup { + pub items: Vec, +} + pub enum SettingsUiItem { - Group { - title: &'static str, - items: Vec, - }, + Group(SettingsUiItemGroup), Single(SettingsUiItemSingle), - Dynamic { - options: Vec, - determine_option: fn(&serde_json::Value, &mut App) -> usize, - }, + Dynamic(SettingsUiItemDynamic), None, } @@ -121,8 +112,56 @@ impl SettingsUi for Option { } } -impl SettingsUi for u64 { - fn settings_ui_item() -> SettingsUiItem { - SettingsUiItem::Single(SettingsUiItemSingle::NumericStepper) +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NumType { + U64 = 0, + U32 = 1, + F32 = 2, +} +pub static NUM_TYPE_NAMES: std::sync::LazyLock<[&'static str; NumType::COUNT]> = + std::sync::LazyLock::new(|| NumType::ALL.map(NumType::type_name)); +pub static NUM_TYPE_IDS: std::sync::LazyLock<[TypeId; NumType::COUNT]> = + std::sync::LazyLock::new(|| NumType::ALL.map(NumType::type_id)); + +impl NumType { + const COUNT: usize = 3; + const ALL: [NumType; Self::COUNT] = [NumType::U64, NumType::U32, NumType::F32]; + + pub fn type_id(self) -> TypeId { + match self { + NumType::U64 => TypeId::of::(), + NumType::U32 => TypeId::of::(), + NumType::F32 => TypeId::of::(), + } + } + + pub fn type_name(self) -> &'static str { + match self { + NumType::U64 => std::any::type_name::(), + NumType::U32 => std::any::type_name::(), + NumType::F32 => std::any::type_name::(), + } } } + +macro_rules! numeric_stepper_for_num_type { + ($type:ty, $num_type:ident) => { + impl SettingsUi for $type { + fn settings_ui_item() -> SettingsUiItem { + SettingsUiItem::Single(SettingsUiItemSingle::NumericStepper(NumType::$num_type)) + } + } + + impl SettingsUi for Option<$type> { + fn settings_ui_item() -> SettingsUiItem { + SettingsUiItem::Single(SettingsUiItemSingle::NumericStepper(NumType::$num_type)) + } + } + }; +} + +numeric_stepper_for_num_type!(u64, U64); +numeric_stepper_for_num_type!(u32, U32); +// todo(settings_ui) is there a better ui for f32? +numeric_stepper_for_num_type!(f32, F32); diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 37edfd5679d259b1d81159f02472fc682bf17243..01f539d2422b01d4eeae7b855e1bfebf2296d383 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -9,7 +9,10 @@ use command_palette_hooks::CommandPaletteFilter; use editor::EditorSettingsControls; use feature_flags::{FeatureFlag, FeatureFlagViewExt}; use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, actions}; -use settings::{SettingsStore, SettingsUiEntryVariant, SettingsUiItemSingle, SettingsValue}; +use settings::{ + NumType, SettingsStore, SettingsUiEntry, SettingsUiItem, SettingsUiItemDynamic, + SettingsUiItemGroup, SettingsUiItemSingle, SettingsValue, +}; use smallvec::SmallVec; use ui::{NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple, prelude::*}; use workspace::{ @@ -134,7 +137,7 @@ impl Item for SettingsPage { struct UiEntry { title: &'static str, - path: &'static str, + path: Option<&'static str>, _depth: usize, // a // b < a descendant range < a total descendant range @@ -182,14 +185,14 @@ struct SettingsUiTree { fn build_tree_item( tree: &mut Vec, - entry: SettingsUiEntryVariant, + entry: SettingsUiEntry, depth: usize, prev_index: Option, ) { let index = tree.len(); tree.push(UiEntry { - title: "", - path: "", + title: entry.title, + path: entry.path, _depth: depth, descendant_range: index + 1..index + 1, total_descendant_range: index + 1..index + 1, @@ -200,14 +203,8 @@ fn build_tree_item( if let Some(prev_index) = prev_index { tree[prev_index].next_sibling = Some(index); } - match entry { - SettingsUiEntryVariant::Group { - path, - title, - items: group_items, - } => { - tree[index].path = path; - tree[index].title = title; + match entry.item { + SettingsUiItem::Group(SettingsUiItemGroup { items: group_items }) => { for group_item in group_items { let prev_index = tree[index] .descendant_range @@ -215,22 +212,17 @@ fn build_tree_item( .not() .then_some(tree[index].descendant_range.end - 1); tree[index].descendant_range.end = tree.len() + 1; - build_tree_item(tree, group_item.item, depth + 1, prev_index); + build_tree_item(tree, group_item, depth + 1, prev_index); tree[index].total_descendant_range.end = tree.len(); } } - SettingsUiEntryVariant::Item { path, item } => { - tree[index].path = path; - // todo(settings_ui) create title from path in macro, and use here - tree[index].title = path; + SettingsUiItem::Single(item) => { tree[index].render = Some(item); } - SettingsUiEntryVariant::Dynamic { - path, + SettingsUiItem::Dynamic(SettingsUiItemDynamic { options, determine_option, - } => { - tree[index].path = path; + }) => { tree[index].select_descendant = Some(determine_option); for option in options { let prev_index = tree[index] @@ -239,11 +231,11 @@ fn build_tree_item( .not() .then_some(tree[index].descendant_range.end - 1); tree[index].descendant_range.end = tree.len() + 1; - build_tree_item(tree, option.item, depth + 1, prev_index); + build_tree_item(tree, option, depth + 1, prev_index); tree[index].total_descendant_range.end = tree.len(); } } - SettingsUiEntryVariant::None => { + SettingsUiItem::None => { return; } } @@ -255,21 +247,17 @@ impl SettingsUiTree { let mut tree = vec![]; let mut root_entry_indices = vec![]; for item in settings_store.settings_ui_items() { - if matches!(item.item, SettingsUiEntryVariant::None) { + if matches!(item.item, SettingsUiItem::None) + // todo(settings_ui): How to handle top level single items? BaseKeymap is in this category. Probably need a way to + // link them to other groups + || matches!(item.item, SettingsUiItem::Single(_)) + { continue; } - assert!( - matches!(item.item, SettingsUiEntryVariant::Group { .. }), - "top level items must be groups: {:?}", - match item.item { - SettingsUiEntryVariant::Item { path, .. } => path, - _ => unreachable!(), - } - ); let prev_root_entry_index = root_entry_indices.last().copied(); root_entry_indices.push(tree.len()); - build_tree_item(&mut tree, item.item, 0, prev_root_entry_index); + build_tree_item(&mut tree, item, 0, prev_root_entry_index); } root_entry_indices.sort_by_key(|i| tree[*i].title); @@ -314,9 +302,12 @@ fn render_content( .size_full() .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error)); }; - let mut content = v_flex().size_full().gap_4(); + let mut content = v_flex().size_full().gap_4().overflow_hidden(); - let mut path = smallvec::smallvec![active_entry.path]; + let mut path = smallvec::smallvec![]; + if let Some(active_entry_path) = active_entry.path { + path.push(active_entry_path); + } let mut entry_index_queue = VecDeque::new(); if let Some(child_index) = active_entry.first_descendant_index() { @@ -331,7 +322,11 @@ fn render_content( while let Some(index) = entry_index_queue.pop_front() { // todo(settings_ui): subgroups? let child = &tree.entries[index]; - path.push(child.path); + let mut pushed_path = false; + if let Some(child_path) = child.path { + path.push(child_path); + pushed_path = true; + } let settings_value = settings_value_from_settings_and_path( path.clone(), // PERF: how to structure this better? There feels like there's a way to avoid the clone @@ -347,7 +342,9 @@ fn render_content( entry_index_queue.push_front(descendant_index); } } - path.pop(); + if pushed_path { + path.pop(); + } let Some(child_render) = child.render.as_ref() else { continue; }; @@ -433,8 +430,8 @@ fn render_item_single( SettingsUiItemSingle::SwitchField => { render_any_item(settings_value, render_switch_field, window, cx) } - SettingsUiItemSingle::NumericStepper => { - render_any_item(settings_value, render_numeric_stepper, window, cx) + SettingsUiItemSingle::NumericStepper(num_type) => { + render_any_numeric_stepper(settings_value, *num_type, window, cx) } SettingsUiItemSingle::ToggleGroup(variants) => { render_toggle_button_group(settings_value, variants, window, cx) @@ -468,6 +465,7 @@ fn downcast_any_item( .map(|value| serde_json::from_value::(value).expect("value is not a T")); // todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values let default_value = serde_json::from_value::(settings_value.default_value) + .with_context(|| format!("path: {:?}", settings_value.path.join("."))) .expect("default value is not an Option"); let deserialized_setting_value = SettingsValue { title: settings_value.title, @@ -488,14 +486,62 @@ fn render_any_item( render_fn(deserialized_setting_value, window, cx) } -fn render_numeric_stepper( - value: SettingsValue, +fn render_any_numeric_stepper( + settings_value: SettingsValue, + num_type: NumType, + window: &mut Window, + cx: &mut App, +) -> AnyElement { + match num_type { + NumType::U64 => render_numeric_stepper::( + downcast_any_item(settings_value), + u64::saturating_sub, + u64::saturating_add, + |n| { + serde_json::Number::try_from(n) + .context("Failed to convert u64 to serde_json::Number") + }, + window, + cx, + ), + NumType::U32 => render_numeric_stepper::( + downcast_any_item(settings_value), + u32::saturating_sub, + u32::saturating_add, + |n| { + serde_json::Number::try_from(n) + .context("Failed to convert u32 to serde_json::Number") + }, + window, + cx, + ), + NumType::F32 => render_numeric_stepper::( + downcast_any_item(settings_value), + |a, b| a - b, + |a, b| a + b, + |n| { + serde_json::Number::from_f64(n as f64) + .context("Failed to convert f32 to serde_json::Number") + }, + window, + cx, + ), + } +} + +fn render_numeric_stepper< + T: serde::de::DeserializeOwned + std::fmt::Display + Copy + From + 'static, +>( + value: SettingsValue, + saturating_sub: fn(T, T) -> T, + saturating_add: fn(T, T) -> T, + to_serde_number: fn(T) -> anyhow::Result, _window: &mut Window, _cx: &mut App, ) -> AnyElement { let id = element_id_from_path(&value.path); let path = value.path.clone(); - let num = value.value.unwrap_or_else(|| value.default_value); + let num = *value.read(); NumericStepper::new( id, @@ -503,8 +549,7 @@ fn render_numeric_stepper( { let path = value.path.clone(); move |_, _, cx| { - let Some(number) = serde_json::Number::from_u128(num.saturating_sub(1) as u128) - else { + let Some(number) = to_serde_number(saturating_sub(num, 1.into())).ok() else { return; }; let new_value = serde_json::Value::Number(number); @@ -512,7 +557,7 @@ fn render_numeric_stepper( } }, move |_, _, cx| { - let Some(number) = serde_json::Number::from_u128(num.saturating_add(1) as u128) else { + let Some(number) = to_serde_number(saturating_add(num, 1.into())).ok() else { return; }; diff --git a/crates/settings_ui_macros/src/settings_ui_macros.rs b/crates/settings_ui_macros/src/settings_ui_macros.rs index 3840bc38da88a37fdbf17b8b06795f81c58ad132..947840a5d990ef32a277380f83840b10c811ff65 100644 --- a/crates/settings_ui_macros/src/settings_ui_macros.rs +++ b/crates/settings_ui_macros/src/settings_ui_macros.rs @@ -57,30 +57,22 @@ pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenSt } } - if path_name.is_none() && group_name.is_some() { - // todo(settings_ui) derive path from settings - panic!("path is required when group is specified"); - } + let ui_item_fn_body = generate_ui_item_body(group_name.as_ref(), path_name.as_ref(), &input); - let ui_render_fn_body = generate_ui_item_body(group_name.as_ref(), path_name.as_ref(), &input); + // todo(settings_ui): Reformat title to be title case with spaces if group name not present, + // and make group name optional, repurpose group as tag indicating item is group + let title = group_name.unwrap_or(input.ident.to_string()); - let settings_ui_item_fn_body = path_name - .as_ref() - .map(|path_name| map_ui_item_to_render(path_name, quote! { Self })) - .unwrap_or(quote! { - settings::SettingsUiEntry { - item: settings::SettingsUiEntryVariant::None - } - }); + let ui_entry_fn_body = map_ui_item_to_entry(path_name.as_deref(), &title, quote! { Self }); let expanded = quote! { impl #impl_generics settings::SettingsUi for #name #ty_generics #where_clause { fn settings_ui_item() -> settings::SettingsUiItem { - #ui_render_fn_body + #ui_item_fn_body } fn settings_ui_entry() -> settings::SettingsUiEntry { - #settings_ui_item_fn_body + #ui_entry_fn_body } } }; @@ -114,27 +106,14 @@ fn option_inner_type(ty: TokenStream) -> Option { return Some(ty.to_token_stream()); } -fn map_ui_item_to_render(path: &str, ty: TokenStream) -> TokenStream { +fn map_ui_item_to_entry(path: Option<&str>, title: &str, ty: TokenStream) -> TokenStream { let ty = extract_type_from_option(ty); + let path = path.map_or_else(|| quote! {None}, |path| quote! {Some(#path)}); quote! { settings::SettingsUiEntry { - item: match #ty::settings_ui_item() { - settings::SettingsUiItem::Group{title, items} => settings::SettingsUiEntryVariant::Group { - title, - path: #path, - items, - }, - settings::SettingsUiItem::Single(item) => settings::SettingsUiEntryVariant::Item { - path: #path, - item, - }, - settings::SettingsUiItem::Dynamic{ options, determine_option } => settings::SettingsUiEntryVariant::Dynamic { - path: #path, - options, - determine_option, - }, - settings::SettingsUiItem::None => settings::SettingsUiEntryVariant::None, - } + title: #title, + path: #path, + item: #ty::settings_ui_item(), } } } @@ -146,16 +125,10 @@ fn generate_ui_item_body( ) -> TokenStream { match (group_name, path_name, &input.data) { (_, _, Data::Union(_)) => unimplemented!("Derive SettingsUi for Unions"), - (None, None, Data::Struct(_)) => quote! { - settings::SettingsUiItem::None - }, - (Some(_), None, Data::Struct(_)) => quote! { - settings::SettingsUiItem::None - }, - (None, Some(_), Data::Struct(_)) => quote! { + (None, _, Data::Struct(_)) => quote! { settings::SettingsUiItem::None }, - (Some(group_name), _, Data::Struct(data_struct)) => { + (Some(_), _, Data::Struct(data_struct)) => { let fields = data_struct .fields .iter() @@ -180,10 +153,11 @@ fn generate_ui_item_body( field.ty.to_token_stream(), ) }) - .map(|(name, ty)| map_ui_item_to_render(&name, ty)); + // todo(settings_ui): Re-format field name as nice title, and support setting different title with attr + .map(|(name, ty)| map_ui_item_to_entry(Some(&name), &name, ty)); quote! { - settings::SettingsUiItem::Group{ title: #group_name, items: vec![#(#fields),*] } + settings::SettingsUiItem::Group(settings::SettingsUiItemGroup{ items: vec![#(#fields),*] }) } } (None, _, Data::Enum(data_enum)) => { From 970242480a156f89843454cc6bc69b2921a13794 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 1 Sep 2025 20:17:27 -0500 Subject: [PATCH 490/744] settings_ui: Improve case handling (#37342) Closes #ISSUE Improves the derive macro for `SettingsUi` so that titles generated from struct and field names are shown in title case, and toggle button groups use title case for rendering, while using lower case/snake case in JSON Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 1 + crates/settings/src/settings_ui_core.rs | 15 +++++++-- crates/settings_ui/src/settings_ui.rs | 33 ++++++++++++------- crates/settings_ui_macros/Cargo.toml | 1 + .../src/settings_ui_macros.rs | 28 ++++++++++------ 5 files changed, 53 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abbe5e0297b9db27c30be0085282d6c7463e7c90..4ed798b39082206d99f0d2c0f0ae9e7c0ffdb40d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14921,6 +14921,7 @@ dependencies = [ name = "settings_ui_macros" version = "0.1.0" dependencies = [ + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.101", diff --git a/crates/settings/src/settings_ui_core.rs b/crates/settings/src/settings_ui_core.rs index 3a5fa3016b78ff90bdf9c6166255809f4f89b1d2..8ab744f5a8244057e0b87a66cc3e4c7dcf02527f 100644 --- a/crates/settings/src/settings_ui_core.rs +++ b/crates/settings/src/settings_ui_core.rs @@ -24,7 +24,6 @@ pub trait SettingsUi { } pub struct SettingsUiEntry { - // todo(settings_ui): move this back here once there isn't a None variant /// The path in the settings JSON file for this setting. Relative to parent /// None implies `#[serde(flatten)]` or `Settings::KEY.is_none()` for top level settings pub path: Option<&'static str>, @@ -36,9 +35,19 @@ pub enum SettingsUiItemSingle { SwitchField, /// A numeric stepper for a specific type of number NumericStepper(NumType), - ToggleGroup(&'static [&'static str]), + ToggleGroup { + /// Must be the same length as `labels` + variants: &'static [&'static str], + /// Must be the same length as `variants` + labels: &'static [&'static str], + }, /// This should be used when toggle group size > 6 - DropDown(&'static [&'static str]), + DropDown { + /// Must be the same length as `labels` + variants: &'static [&'static str], + /// Must be the same length as `variants` + labels: &'static [&'static str], + }, Custom(Box, &mut Window, &mut App) -> AnyElement>), } diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 01f539d2422b01d4eeae7b855e1bfebf2296d383..80e82a304965e4ddb7eb37306fb5345f5552e6c7 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -329,6 +329,7 @@ fn render_content( } let settings_value = settings_value_from_settings_and_path( path.clone(), + child.title, // PERF: how to structure this better? There feels like there's a way to avoid the clone // and every value lookup SettingsStore::global(cx).raw_user_settings(), @@ -433,10 +434,11 @@ fn render_item_single( SettingsUiItemSingle::NumericStepper(num_type) => { render_any_numeric_stepper(settings_value, *num_type, window, cx) } - SettingsUiItemSingle::ToggleGroup(variants) => { - render_toggle_button_group(settings_value, variants, window, cx) - } - SettingsUiItemSingle::DropDown(_) => { + SettingsUiItemSingle::ToggleGroup { + variants: values, + labels: titles, + } => render_toggle_button_group(settings_value, values, titles, window, cx), + SettingsUiItemSingle::DropDown { .. } => { unimplemented!("This") } } @@ -603,6 +605,7 @@ fn render_switch_field( fn render_toggle_button_group( value: SettingsValue, variants: &'static [&'static str], + labels: &'static [&'static str], _: &mut Window, _: &mut App, ) -> AnyElement { @@ -612,15 +615,18 @@ fn render_toggle_button_group( group_name: &'static str, value: SettingsValue, variants: &'static [&'static str], + labels: &'static [&'static str], ) -> AnyElement { - let mut variants_array: [&'static str; LEN] = ["default"; LEN]; - variants_array.copy_from_slice(variants); + let mut variants_array: [(&'static str, &'static str); LEN] = [("unused", "unused"); LEN]; + for i in 0..LEN { + variants_array[i] = (variants[i], labels[i]); + } let active_value = value.read(); let selected_idx = variants_array .iter() .enumerate() - .find_map(|(idx, variant)| { + .find_map(|(idx, (variant, _))| { if variant == &active_value { Some(idx) } else { @@ -628,11 +634,13 @@ fn render_toggle_button_group( } }); + let mut idx = 0; ToggleButtonGroup::single_row( group_name, - variants_array.map(|variant| { + variants_array.map(|(variant, label)| { let path = value.path.clone(); - ToggleButtonSimple::new(variant, move |_, _, cx| { + idx += 1; + ToggleButtonSimple::new(label, move |_, _, cx| { SettingsValue::write_value( &path, serde_json::Value::String(variant.to_string()), @@ -649,7 +657,7 @@ fn render_toggle_button_group( macro_rules! templ_toggl_with_const_param { ($len:expr) => { if variants.len() == $len { - return make_toggle_group::<$len>(value.title, value, variants); + return make_toggle_group::<$len>(value.title, value, variants, labels); } }; } @@ -664,6 +672,7 @@ fn render_toggle_button_group( fn settings_value_from_settings_and_path( path: SmallVec<[&'static str; 1]>, + title: &'static str, user_settings: &serde_json::Value, default_settings: &serde_json::Value, ) -> SettingsValue { @@ -677,8 +686,8 @@ fn settings_value_from_settings_and_path( default_value, value, path: path.clone(), - // todo(settings_ui) title for items - title: path.last().expect("path non empty"), + // todo(settings_ui) is title required inside SettingsValue? + title, }; return settings_value; } diff --git a/crates/settings_ui_macros/Cargo.toml b/crates/settings_ui_macros/Cargo.toml index e242e7546d1527632dba6eece9b17ccea27295f4..1561e874f4de0248db9509b899b798578a179f3c 100644 --- a/crates/settings_ui_macros/Cargo.toml +++ b/crates/settings_ui_macros/Cargo.toml @@ -16,6 +16,7 @@ workspace = true default = [] [dependencies] +heck.workspace = true proc-macro2.workspace = true quote.workspace = true syn.workspace = true diff --git a/crates/settings_ui_macros/src/settings_ui_macros.rs b/crates/settings_ui_macros/src/settings_ui_macros.rs index 947840a5d990ef32a277380f83840b10c811ff65..c98705d5f8d4de3f42b4756a32353123f5779fbc 100644 --- a/crates/settings_ui_macros/src/settings_ui_macros.rs +++ b/crates/settings_ui_macros/src/settings_ui_macros.rs @@ -1,3 +1,4 @@ +use heck::{ToSnakeCase as _, ToTitleCase as _}; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; use syn::{Data, DeriveInput, LitStr, Token, parse_macro_input}; @@ -59,9 +60,8 @@ pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenSt let ui_item_fn_body = generate_ui_item_body(group_name.as_ref(), path_name.as_ref(), &input); - // todo(settings_ui): Reformat title to be title case with spaces if group name not present, - // and make group name optional, repurpose group as tag indicating item is group - let title = group_name.unwrap_or(input.ident.to_string()); + // todo(settings_ui): make group name optional, repurpose group as tag indicating item is group, and have "title" tag for custom title + let title = group_name.unwrap_or(input.ident.to_string().to_title_case()); let ui_entry_fn_body = map_ui_item_to_entry(path_name.as_deref(), &title, quote! { Self }); @@ -154,7 +154,7 @@ fn generate_ui_item_body( ) }) // todo(settings_ui): Re-format field name as nice title, and support setting different title with attr - .map(|(name, ty)| map_ui_item_to_entry(Some(&name), &name, ty)); + .map(|(name, ty)| map_ui_item_to_entry(Some(&name), &name.to_title_case(), ty)); quote! { settings::SettingsUiItem::Group(settings::SettingsUiItemGroup{ items: vec![#(#fields),*] }) @@ -162,14 +162,15 @@ fn generate_ui_item_body( } (None, _, Data::Enum(data_enum)) => { let mut lowercase = false; + let mut snake_case = false; for attr in &input.attrs { if attr.path().is_ident("serde") { attr.parse_nested_meta(|meta| { if meta.path.is_ident("rename_all") { meta.input.parse::()?; let lit = meta.input.parse::()?.value(); - // todo(settings_ui) snake case - lowercase = lit == "lowercase" || lit == "snake_case"; + lowercase = lit == "lowercase"; + snake_case = lit == "snake_case"; } Ok(()) }) @@ -181,20 +182,27 @@ fn generate_ui_item_body( let variants = data_enum.variants.iter().map(|variant| { let string = variant.ident.clone().to_string(); - if lowercase { + let title = string.to_title_case(); + let string = if lowercase { string.to_lowercase() + } else if snake_case { + string.to_snake_case() } else { string - } + }; + + (string, title) }); + let (variants, labels): (Vec<_>, Vec<_>) = variants.unzip(); + if length > 6 { quote! { - settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::DropDown(&[#(#variants),*])) + settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::DropDown{ variants: &[#(#variants),*], labels: &[#(#labels),*] }) } } else { quote! { - settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::ToggleGroup(&[#(#variants),*])) + settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::ToggleGroup{ variants: &[#(#variants),*], labels: &[#(#labels),*] }) } } } From f06be6f3ec798782d59bddc0a2b1e6b32325800a Mon Sep 17 00:00:00 2001 From: Maksim Bondarenkov <119937608+ognevny@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:02:41 +0300 Subject: [PATCH 491/744] docs: Add link to msys2 docs page (#37327) it was removed earlier. better to keep this link because the page contains some useful information Release Notes: - N/A --- docs/src/development/windows.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/development/windows.md b/docs/src/development/windows.md index a4ad220bcc859d7d49edec7f967537ee4de2418a..65e65f4cc1a5df4eeebcec83fd985468d9866d6b 100644 --- a/docs/src/development/windows.md +++ b/docs/src/development/windows.md @@ -116,6 +116,8 @@ cargo test --workspace Zed does not support unofficial MSYS2 Zed packages built for Mingw-w64. Please report any issues you may have with [mingw-w64-zed](https://packages.msys2.org/base/mingw-w64-zed) to [msys2/MINGW-packages/issues](https://github.com/msys2/MINGW-packages/issues?q=is%3Aissue+is%3Aopen+zed). +Please refer to [MSYS2 documentation](https://www.msys2.org/docs/ides-editors/#zed) first. + ## Troubleshooting ### Setting `RUSTFLAGS` env var breaks builds From 374a8bc4cb6fd1eb8d8a35b0654f280feba47997 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 2 Sep 2025 10:48:33 +0200 Subject: [PATCH 492/744] acp: Add support for slash commands (#37304) Depends on https://github.com/zed-industries/agent-client-protocol/pull/45 Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra Co-authored-by: Agus Zubiaga --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 8 + crates/acp_thread/src/connection.rs | 2 +- crates/agent2/src/agent.rs | 1 + crates/agent_servers/src/acp.rs | 9 +- .../agent_ui/src/acp/completion_provider.rs | 438 ++++++++++++++---- crates/agent_ui/src/acp/entry_view_state.rs | 22 +- crates/agent_ui/src/acp/message_editor.rs | 435 ++++++++--------- crates/agent_ui/src/acp/thread_view.rs | 16 +- crates/project/src/lsp_store.rs | 15 + 11 files changed, 626 insertions(+), 326 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ed798b39082206d99f0d2c0f0ae9e7c0ffdb40d..da9eeabee4a37fcdcc10a2448c6a4c434ab2ee7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,9 +195,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.2.0-alpha.3" +version = "0.2.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec42b8b612665799c7667890df4b5f5cb441b18a68619fd770f1e054480ee3f" +checksum = "603941db1d130ee275840c465b73a2312727d4acef97449550ccf033de71301f" dependencies = [ "anyhow", "async-broadcast", diff --git a/Cargo.toml b/Cargo.toml index 6cf3d8858b01eb41afdeb860ca4284be0b9280a2..a96dc5d4d57ac5e5be60ace099e42b6a8123c42e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -430,7 +430,7 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agent-client-protocol = { version = "0.2.0-alpha.3", features = ["unstable"] } +agent-client-protocol = { version = "0.2.0-alpha.4", features = ["unstable"]} 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 ab6aa98e99d4977256512b7c178dff26b71fc7e7..f9a955eb9f5c7f5cd5ab077ed3c3afd9dfcd4b8b 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -785,6 +785,7 @@ pub struct AcpThread { session_id: acp::SessionId, token_usage: Option, prompt_capabilities: acp::PromptCapabilities, + available_commands: Vec, _observe_prompt_capabilities: Task>, determine_shell: Shared>, terminals: HashMap>, @@ -858,6 +859,7 @@ impl AcpThread { action_log: Entity, session_id: acp::SessionId, mut prompt_capabilities_rx: watch::Receiver, + available_commands: Vec, cx: &mut Context, ) -> Self { let prompt_capabilities = *prompt_capabilities_rx.borrow(); @@ -897,6 +899,7 @@ impl AcpThread { session_id, token_usage: None, prompt_capabilities, + available_commands, _observe_prompt_capabilities: task, terminals: HashMap::default(), determine_shell, @@ -907,6 +910,10 @@ impl AcpThread { self.prompt_capabilities } + pub fn available_commands(&self) -> Vec { + self.available_commands.clone() + } + pub fn connection(&self) -> &Rc { &self.connection } @@ -2864,6 +2871,7 @@ mod tests { audio: true, embedded_context: true, }), + vec![], cx, ) }); diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 96abd1d2b4cf92698e7046cd4b7e24e6043280ff..7901b08c907811ac1b6b74b975ca66b6b901868f 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -75,7 +75,6 @@ pub trait AgentConnection { fn telemetry(&self) -> Option> { None } - fn into_any(self: Rc) -> Rc; } @@ -339,6 +338,7 @@ mod test_support { audio: true, embedded_context: true, }), + vec![], cx, ) }); diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index e96b4c0cfa32be910a7a77e58a1911deb7e5357a..241e3d389f96a320d8a43e23493c4738a76802d6 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -292,6 +292,7 @@ impl NativeAgent { action_log.clone(), session_id.clone(), prompt_capabilities_rx, + vec![], cx, ) }); diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index b29bfd5d8919f87594ccd26cbf9d7fdc3520ad30..7907083e144c3d6188120a8ae8e24aa0ddbd765b 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -28,7 +28,7 @@ pub struct AcpConnection { connection: Rc, sessions: Rc>>, auth_methods: Vec, - prompt_capabilities: acp::PromptCapabilities, + agent_capabilities: acp::AgentCapabilities, _io_task: Task>, _wait_task: Task>, _stderr_task: Task>, @@ -148,7 +148,7 @@ impl AcpConnection { connection, server_name, sessions, - prompt_capabilities: response.agent_capabilities.prompt_capabilities, + agent_capabilities: response.agent_capabilities, _io_task: io_task, _wait_task: wait_task, _stderr_task: stderr_task, @@ -156,7 +156,7 @@ impl AcpConnection { } pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities { - &self.prompt_capabilities + &self.agent_capabilities.prompt_capabilities } } @@ -223,7 +223,8 @@ impl AgentConnection for AcpConnection { action_log, session_id.clone(), // ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically. - watch::Receiver::constant(self.prompt_capabilities), + watch::Receiver::constant(self.agent_capabilities.prompt_capabilities), + response.available_commands, cx, ) })?; diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 5b4096706981f11e8340e1017a7955676865eb90..59106c3795aa14794e1fca9ee32049e0cff1314f 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,4 +1,4 @@ -use std::cell::Cell; +use std::cell::{Cell, RefCell}; use std::ops::Range; use std::rc::Rc; use std::sync::Arc; @@ -13,6 +13,7 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{App, Entity, Task, WeakEntity}; use language::{Buffer, CodeLabel, HighlightId}; use lsp::CompletionContext; +use project::lsp_store::CompletionDocumentation; use project::{ Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId, }; @@ -23,7 +24,7 @@ use ui::prelude::*; use workspace::Workspace; use crate::AgentPanel; -use crate::acp::message_editor::MessageEditor; +use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::context_picker::file_context_picker::{FileMatch, search_files}; use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules}; use crate::context_picker::symbol_context_picker::SymbolMatch; @@ -67,6 +68,7 @@ pub struct ContextPickerCompletionProvider { history_store: Entity, prompt_store: Option>, prompt_capabilities: Rc>, + available_commands: Rc>>, } impl ContextPickerCompletionProvider { @@ -76,6 +78,7 @@ impl ContextPickerCompletionProvider { history_store: Entity, prompt_store: Option>, prompt_capabilities: Rc>, + available_commands: Rc>>, ) -> Self { Self { message_editor, @@ -83,6 +86,7 @@ impl ContextPickerCompletionProvider { history_store, prompt_store, prompt_capabilities, + available_commands, } } @@ -369,7 +373,42 @@ impl ContextPickerCompletionProvider { }) } - fn search( + fn search_slash_commands( + &self, + query: String, + cx: &mut App, + ) -> Task> { + let commands = self.available_commands.borrow().clone(); + if commands.is_empty() { + return Task::ready(Vec::new()); + } + + cx.spawn(async move |cx| { + let candidates = commands + .iter() + .enumerate() + .map(|(id, command)| StringMatchCandidate::new(id, &command.name)) + .collect::>(); + + let matches = fuzzy::match_strings( + &candidates, + &query, + false, + true, + 100, + &Arc::new(AtomicBool::default()), + cx.background_executor().clone(), + ) + .await; + + matches + .into_iter() + .map(|mat| commands[mat.candidate_id].clone()) + .collect() + }) + } + + fn search_mentions( &self, mode: Option, query: String, @@ -651,10 +690,10 @@ impl CompletionProvider for ContextPickerCompletionProvider { let offset_to_line = buffer.point_to_offset(line_start); let mut lines = buffer.text_for_range(line_start..position).lines(); let line = lines.next()?; - MentionCompletion::try_parse( - self.prompt_capabilities.get().embedded_context, + ContextCompletion::try_parse( line, offset_to_line, + self.prompt_capabilities.get().embedded_context, ) }); let Some(state) = state else { @@ -667,97 +706,169 @@ impl CompletionProvider for ContextPickerCompletionProvider { let project = workspace.read(cx).project().clone(); let snapshot = buffer.read(cx).snapshot(); - let source_range = snapshot.anchor_before(state.source_range.start) - ..snapshot.anchor_after(state.source_range.end); + let source_range = snapshot.anchor_before(state.source_range().start) + ..snapshot.anchor_after(state.source_range().end); let editor = self.message_editor.clone(); - let MentionCompletion { mode, argument, .. } = state; - let query = argument.unwrap_or_else(|| "".to_string()); - - let search_task = self.search(mode, query, Arc::::default(), cx); - - cx.spawn(async move |_, cx| { - let matches = search_task.await; - - let completions = cx.update(|cx| { - matches - .into_iter() - .filter_map(|mat| match mat { - Match::File(FileMatch { mat, is_recent }) => { - let project_path = ProjectPath { - worktree_id: WorktreeId::from_usize(mat.worktree_id), - path: mat.path.clone(), + match state { + ContextCompletion::SlashCommand(SlashCommandCompletion { + command, argument, .. + }) => { + let search_task = self.search_slash_commands(command.unwrap_or_default(), cx); + cx.background_spawn(async move { + let completions = search_task + .await + .into_iter() + .map(|command| { + let new_text = if let Some(argument) = argument.as_ref() { + format!("/{} {}", command.name, argument) + } else { + format!("/{} ", command.name) }; - Self::completion_for_path( - project_path, - &mat.path_prefix, - is_recent, - mat.is_dir, - source_range.clone(), - editor.clone(), - project.clone(), - cx, - ) - } - - Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol( - symbol, - source_range.clone(), - editor.clone(), - workspace.clone(), - cx, - ), - - Match::Thread(thread) => Some(Self::completion_for_thread( - thread, - source_range.clone(), - false, - editor.clone(), - cx, - )), - - Match::RecentThread(thread) => Some(Self::completion_for_thread( - thread, - source_range.clone(), - true, - editor.clone(), - cx, - )), - - Match::Rules(user_rules) => Some(Self::completion_for_rules( - user_rules, - source_range.clone(), - editor.clone(), - cx, - )), + let is_missing_argument = argument.is_none() && command.input.is_some(); + Completion { + replace_range: source_range.clone(), + new_text, + label: CodeLabel::plain(command.name.to_string(), None), + documentation: Some(CompletionDocumentation::SingleLine( + command.description.into(), + )), + source: project::CompletionSource::Custom, + icon_path: None, + insert_text_mode: None, + confirm: Some(Arc::new({ + let editor = editor.clone(); + move |intent, _window, cx| { + if !is_missing_argument { + cx.defer({ + let editor = editor.clone(); + move |cx| { + editor + .update(cx, |_editor, cx| { + match intent { + CompletionIntent::Complete + | CompletionIntent::CompleteWithInsert + | CompletionIntent::CompleteWithReplace => { + if !is_missing_argument { + cx.emit(MessageEditorEvent::Send); + } + } + CompletionIntent::Compose => {} + } + }) + .ok(); + } + }); + } + is_missing_argument + } + })), + } + }) + .collect(); + + Ok(vec![CompletionResponse { + completions, + // Since this does its own filtering (see `filter_completions()` returns false), + // there is no benefit to computing whether this set of completions is incomplete. + is_incomplete: true, + }]) + }) + } + ContextCompletion::Mention(MentionCompletion { mode, argument, .. }) => { + let query = argument.unwrap_or_default(); + let search_task = + self.search_mentions(mode, query, Arc::::default(), cx); - Match::Fetch(url) => Self::completion_for_fetch( - source_range.clone(), - url, - editor.clone(), - cx, - ), + cx.spawn(async move |_, cx| { + let matches = search_task.await; - Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry( - entry, - source_range.clone(), - editor.clone(), - &workspace, - cx, - ), - }) - .collect() - })?; - - Ok(vec![CompletionResponse { - completions, - // Since this does its own filtering (see `filter_completions()` returns false), - // there is no benefit to computing whether this set of completions is incomplete. - is_incomplete: true, - }]) - }) + let completions = cx.update(|cx| { + matches + .into_iter() + .filter_map(|mat| match mat { + Match::File(FileMatch { mat, is_recent }) => { + let project_path = ProjectPath { + worktree_id: WorktreeId::from_usize(mat.worktree_id), + path: mat.path.clone(), + }; + + Self::completion_for_path( + project_path, + &mat.path_prefix, + is_recent, + mat.is_dir, + source_range.clone(), + editor.clone(), + project.clone(), + cx, + ) + } + + Match::Symbol(SymbolMatch { symbol, .. }) => { + Self::completion_for_symbol( + symbol, + source_range.clone(), + editor.clone(), + workspace.clone(), + cx, + ) + } + + Match::Thread(thread) => Some(Self::completion_for_thread( + thread, + source_range.clone(), + false, + editor.clone(), + cx, + )), + + Match::RecentThread(thread) => Some(Self::completion_for_thread( + thread, + source_range.clone(), + true, + editor.clone(), + cx, + )), + + Match::Rules(user_rules) => Some(Self::completion_for_rules( + user_rules, + source_range.clone(), + editor.clone(), + cx, + )), + + Match::Fetch(url) => Self::completion_for_fetch( + source_range.clone(), + url, + editor.clone(), + cx, + ), + + Match::Entry(EntryMatch { entry, .. }) => { + Self::completion_for_entry( + entry, + source_range.clone(), + editor.clone(), + &workspace, + cx, + ) + } + }) + .collect() + })?; + + Ok(vec![CompletionResponse { + completions, + // Since this does its own filtering (see `filter_completions()` returns false), + // there is no benefit to computing whether this set of completions is incomplete. + is_incomplete: true, + }]) + }) + } + } } fn is_completion_trigger( @@ -775,14 +886,14 @@ impl CompletionProvider for ContextPickerCompletionProvider { let offset_to_line = buffer.point_to_offset(line_start); let mut lines = buffer.text_for_range(line_start..position).lines(); if let Some(line) = lines.next() { - MentionCompletion::try_parse( - self.prompt_capabilities.get().embedded_context, + ContextCompletion::try_parse( line, offset_to_line, + self.prompt_capabilities.get().embedded_context, ) .map(|completion| { - completion.source_range.start <= offset_to_line + position.column as usize - && completion.source_range.end >= offset_to_line + position.column as usize + completion.source_range().start <= offset_to_line + position.column as usize + && completion.source_range().end >= offset_to_line + position.column as usize }) .unwrap_or(false) } else { @@ -851,7 +962,7 @@ fn confirm_completion_callback( .clone() .update(cx, |message_editor, cx| { message_editor - .confirm_completion( + .confirm_mention_completion( crease_text, start, content_len, @@ -867,6 +978,89 @@ fn confirm_completion_callback( }) } +enum ContextCompletion { + SlashCommand(SlashCommandCompletion), + Mention(MentionCompletion), +} + +impl ContextCompletion { + fn source_range(&self) -> Range { + match self { + Self::SlashCommand(completion) => completion.source_range.clone(), + Self::Mention(completion) => completion.source_range.clone(), + } + } + + fn try_parse(line: &str, offset_to_line: usize, allow_non_file_mentions: bool) -> Option { + if let Some(command) = SlashCommandCompletion::try_parse(line, offset_to_line) { + Some(Self::SlashCommand(command)) + } else if let Some(mention) = + MentionCompletion::try_parse(allow_non_file_mentions, line, offset_to_line) + { + Some(Self::Mention(mention)) + } else { + None + } + } +} + +#[derive(Debug, Default, PartialEq)] +struct SlashCommandCompletion { + source_range: Range, + command: Option, + argument: Option, +} + +impl SlashCommandCompletion { + fn try_parse(line: &str, offset_to_line: usize) -> Option { + // If we decide to support commands that are not at the beginning of the prompt, we can remove this check + if !line.starts_with('/') || offset_to_line != 0 { + return None; + } + + let last_command_start = line.rfind('/')?; + if last_command_start >= line.len() { + return Some(Self::default()); + } + if last_command_start > 0 + && line + .chars() + .nth(last_command_start - 1) + .is_some_and(|c| !c.is_whitespace()) + { + return None; + } + + let rest_of_line = &line[last_command_start + 1..]; + + let mut command = None; + let mut argument = None; + let mut end = last_command_start + 1; + + if let Some(command_text) = rest_of_line.split_whitespace().next() { + command = Some(command_text.to_string()); + end += command_text.len(); + + // Find the start of arguments after the command + if let Some(args_start) = + rest_of_line[command_text.len()..].find(|c: char| !c.is_whitespace()) + { + let args = &rest_of_line[command_text.len() + args_start..].trim_end(); + if !args.is_empty() { + argument = Some(args.to_string()); + end += args.len() + 1; + } + } + } + + Some(Self { + source_range: last_command_start + offset_to_line..end + offset_to_line, + command, + argument, + }) + } +} + #[derive(Debug, Default, PartialEq)] struct MentionCompletion { source_range: Range, @@ -932,6 +1126,62 @@ impl MentionCompletion { mod tests { use super::*; + #[test] + fn test_slash_command_completion_parse() { + assert_eq!( + SlashCommandCompletion::try_parse("/", 0), + Some(SlashCommandCompletion { + source_range: 0..1, + command: None, + argument: None, + }) + ); + + assert_eq!( + SlashCommandCompletion::try_parse("/help", 0), + Some(SlashCommandCompletion { + source_range: 0..5, + command: Some("help".to_string()), + argument: None, + }) + ); + + assert_eq!( + SlashCommandCompletion::try_parse("/help ", 0), + Some(SlashCommandCompletion { + source_range: 0..5, + command: Some("help".to_string()), + argument: None, + }) + ); + + assert_eq!( + SlashCommandCompletion::try_parse("/help arg1", 0), + Some(SlashCommandCompletion { + source_range: 0..10, + command: Some("help".to_string()), + argument: Some("arg1".to_string()), + }) + ); + + assert_eq!( + SlashCommandCompletion::try_parse("/help arg1 arg2", 0), + Some(SlashCommandCompletion { + source_range: 0..15, + command: Some("help".to_string()), + argument: Some("arg1 arg2".to_string()), + }) + ); + + assert_eq!(SlashCommandCompletion::try_parse("Lorem Ipsum", 0), None); + + assert_eq!(SlashCommandCompletion::try_parse("Lorem /", 0), None); + + assert_eq!(SlashCommandCompletion::try_parse("Lorem /help", 0), None); + + assert_eq!(SlashCommandCompletion::try_parse("Lorem/", 0), None); + } + #[test] fn test_mention_completion_parse() { assert_eq!(MentionCompletion::try_parse(true, "Lorem Ipsum", 0), None); diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 0103219e31e8210440e66637cce8101d283210ea..4a91e93fa894953ef2f1f730ffa0d3896213e625 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -1,7 +1,11 @@ -use std::{cell::Cell, ops::Range, rc::Rc}; +use std::{ + cell::{Cell, RefCell}, + ops::Range, + rc::Rc, +}; use acp_thread::{AcpThread, AgentThreadEntry}; -use agent_client_protocol::{PromptCapabilities, ToolCallId}; +use agent_client_protocol::{self as acp, ToolCallId}; use agent2::HistoryStore; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; @@ -26,8 +30,8 @@ pub struct EntryViewState { history_store: Entity, prompt_store: Option>, entries: Vec, - prevent_slash_commands: bool, - prompt_capabilities: Rc>, + prompt_capabilities: Rc>, + available_commands: Rc>>, } impl EntryViewState { @@ -36,8 +40,8 @@ impl EntryViewState { project: Entity, history_store: Entity, prompt_store: Option>, - prompt_capabilities: Rc>, - prevent_slash_commands: bool, + prompt_capabilities: Rc>, + available_commands: Rc>>, ) -> Self { Self { workspace, @@ -45,8 +49,8 @@ impl EntryViewState { history_store, prompt_store, entries: Vec::new(), - prevent_slash_commands, prompt_capabilities, + available_commands, } } @@ -85,8 +89,8 @@ impl EntryViewState { self.history_store.clone(), self.prompt_store.clone(), self.prompt_capabilities.clone(), + self.available_commands.clone(), "Edit message - @ to include context", - self.prevent_slash_commands, editor::EditorMode::AutoHeight { min_lines: 1, max_lines: None, @@ -471,7 +475,7 @@ mod tests { history_store, None, Default::default(), - false, + Default::default(), ) }); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index b9e85e0ee34b3dccd0dcd4a22c1fbaa05031e2d9..b51bc2e0a3d10a3647fafd816684c7382c5dde36 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -12,7 +12,7 @@ use collections::{HashMap, HashSet}; use editor::{ Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, - SemanticsProvider, ToOffset, + ToOffset, actions::Paste, display_map::{Crease, CreaseId, FoldId}, }; @@ -22,8 +22,8 @@ use futures::{ }; use gpui::{ Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId, - EventEmitter, FocusHandle, Focusable, HighlightStyle, Image, ImageFormat, Img, KeyContext, - Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, pulsating_between, + EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, Subscription, Task, + TextStyle, WeakEntity, pulsating_between, }; use language::{Buffer, Language}; use language_model::LanguageModelImage; @@ -33,7 +33,7 @@ use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::Settings; use std::{ - cell::Cell, + cell::{Cell, RefCell}, ffi::OsStr, fmt::Write, ops::{Range, RangeInclusive}, @@ -42,20 +42,18 @@ use std::{ sync::Arc, time::Duration, }; -use text::{OffsetRangeExt, ToOffset as _}; +use text::OffsetRangeExt; use theme::ThemeSettings; use ui::{ ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Element as _, FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement, Render, SelectableButton, SharedString, Styled, - TextSize, TintColor, Toggleable, Window, div, h_flex, px, + TextSize, TintColor, Toggleable, Window, div, h_flex, }; use util::{ResultExt, debug_panic}; use workspace::{Workspace, notifications::NotifyResultExt as _}; use zed_actions::agent::Chat; -const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50); - pub struct MessageEditor { mention_set: MentionSet, editor: Entity, @@ -63,7 +61,6 @@ pub struct MessageEditor { workspace: WeakEntity, history_store: Entity, prompt_store: Option>, - prevent_slash_commands: bool, prompt_capabilities: Rc>, _subscriptions: Vec, _parse_slash_command_task: Task<()>, @@ -86,8 +83,8 @@ impl MessageEditor { history_store: Entity, prompt_store: Option>, prompt_capabilities: Rc>, + available_commands: Rc>>, placeholder: impl Into>, - prevent_slash_commands: bool, mode: EditorMode, window: &mut Window, cx: &mut Context, @@ -99,16 +96,14 @@ impl MessageEditor { }, None, ); - let completion_provider = ContextPickerCompletionProvider::new( + let completion_provider = Rc::new(ContextPickerCompletionProvider::new( cx.weak_entity(), workspace.clone(), history_store.clone(), prompt_store.clone(), prompt_capabilities.clone(), - ); - let semantics_provider = Rc::new(SlashCommandSemanticsProvider { - range: Cell::new(None), - }); + available_commands, + )); let mention_set = MentionSet::default(); let editor = cx.new(|cx| { let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); @@ -119,15 +114,12 @@ impl MessageEditor { editor.set_show_indent_guides(false, cx); editor.set_soft_wrap(); editor.set_use_modal_editing(true); - editor.set_completion_provider(Some(Rc::new(completion_provider))); + editor.set_completion_provider(Some(completion_provider.clone())); editor.set_context_menu_options(ContextMenuOptions { min_entries_visible: 12, max_entries_visible: 12, placement: Some(ContextMenuPlacement::Above), }); - if prevent_slash_commands { - editor.set_semantics_provider(Some(semantics_provider.clone())); - } editor.register_addon(MessageEditorAddon::new()); editor }); @@ -143,17 +135,8 @@ impl MessageEditor { let mut subscriptions = Vec::new(); subscriptions.push(cx.subscribe_in(&editor, window, { - let semantics_provider = semantics_provider.clone(); move |this, editor, event, window, cx| { if let EditorEvent::Edited { .. } = event { - if prevent_slash_commands { - this.highlight_slash_command( - semantics_provider.clone(), - editor.clone(), - window, - cx, - ); - } let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx)); this.mention_set.remove_invalid(snapshot); cx.notify(); @@ -168,7 +151,6 @@ impl MessageEditor { workspace, history_store, prompt_store, - prevent_slash_commands, prompt_capabilities, _subscriptions: subscriptions, _parse_slash_command_task: Task::ready(()), @@ -191,7 +173,7 @@ impl MessageEditor { .text_anchor }); - self.confirm_completion( + self.confirm_mention_completion( thread.title.clone(), start, thread.title.len(), @@ -227,7 +209,7 @@ impl MessageEditor { .collect() } - pub fn confirm_completion( + pub fn confirm_mention_completion( &mut self, crease_text: SharedString, start: text::Anchor, @@ -687,7 +669,6 @@ impl MessageEditor { .mention_set .contents(&self.prompt_capabilities.get(), cx); let editor = self.editor.clone(); - let prevent_slash_commands = self.prevent_slash_commands; cx.spawn(async move |_, cx| { let contents = contents.await?; @@ -706,14 +687,16 @@ impl MessageEditor { let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot); if crease_range.start > ix { - let chunk = if prevent_slash_commands - && ix == 0 - && parse_slash_command(&text[ix..]).is_some() - { - format!(" {}", &text[ix..crease_range.start]).into() - } else { - text[ix..crease_range.start].into() - }; + //todo(): Custom slash command ContentBlock? + // let chunk = if prevent_slash_commands + // && ix == 0 + // && parse_slash_command(&text[ix..]).is_some() + // { + // format!(" {}", &text[ix..crease_range.start]).into() + // } else { + // text[ix..crease_range.start].into() + // }; + let chunk = text[ix..crease_range.start].into(); chunks.push(chunk); } let chunk = match mention { @@ -769,14 +752,16 @@ impl MessageEditor { } if ix < text.len() { - let last_chunk = if prevent_slash_commands - && ix == 0 - && parse_slash_command(&text[ix..]).is_some() - { - format!(" {}", text[ix..].trim_end()) - } else { - text[ix..].trim_end().to_owned() - }; + //todo(): Custom slash command ContentBlock? + // let last_chunk = if prevent_slash_commands + // && ix == 0 + // && parse_slash_command(&text[ix..]).is_some() + // { + // format!(" {}", text[ix..].trim_end()) + // } else { + // text[ix..].trim_end().to_owned() + // }; + let last_chunk = text[ix..].trim_end().to_owned(); if !last_chunk.is_empty() { chunks.push(last_chunk.into()); } @@ -971,7 +956,14 @@ impl MessageEditor { cx, ); }); - tasks.push(self.confirm_completion(file_name, anchor, content_len, uri, window, cx)); + tasks.push(self.confirm_mention_completion( + file_name, + anchor, + content_len, + uri, + window, + cx, + )); } cx.spawn(async move |_, _| { join_all(tasks).await; @@ -1133,48 +1125,6 @@ impl MessageEditor { cx.notify(); } - fn highlight_slash_command( - &mut self, - semantics_provider: Rc, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) { - struct InvalidSlashCommand; - - self._parse_slash_command_task = cx.spawn_in(window, async move |_, cx| { - cx.background_executor() - .timer(PARSE_SLASH_COMMAND_DEBOUNCE) - .await; - editor - .update_in(cx, |editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - let range = parse_slash_command(&editor.text(cx)); - semantics_provider.range.set(range); - if let Some((start, end)) = range { - editor.highlight_text::( - vec![ - snapshot.buffer_snapshot.anchor_after(start) - ..snapshot.buffer_snapshot.anchor_before(end), - ], - HighlightStyle { - underline: Some(UnderlineStyle { - thickness: px(1.), - color: Some(gpui::red()), - wavy: true, - }), - ..Default::default() - }, - cx, - ); - } else { - editor.clear_highlights::(cx); - } - }) - .ok(); - }) - } - pub fn text(&self, cx: &App) -> String { self.editor.read(cx).text(cx) } @@ -1264,7 +1214,7 @@ pub(crate) fn insert_crease_for_mention( let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len); let placeholder = FoldPlaceholder { - render: render_fold_icon_button( + render: render_mention_fold_button( crease_label, crease_icon, start..end, @@ -1294,7 +1244,7 @@ pub(crate) fn insert_crease_for_mention( Some((crease_id, tx)) } -fn render_fold_icon_button( +fn render_mention_fold_button( label: SharedString, icon: SharedString, range: Range, @@ -1471,118 +1421,6 @@ impl MentionSet { } } -struct SlashCommandSemanticsProvider { - range: Cell>, -} - -impl SemanticsProvider for SlashCommandSemanticsProvider { - fn hover( - &self, - buffer: &Entity, - position: text::Anchor, - cx: &mut App, - ) -> Option>>> { - let snapshot = buffer.read(cx).snapshot(); - let offset = position.to_offset(&snapshot); - let (start, end) = self.range.get()?; - if !(start..end).contains(&offset) { - return None; - } - let range = snapshot.anchor_after(start)..snapshot.anchor_after(end); - Some(Task::ready(Some(vec![project::Hover { - contents: vec![project::HoverBlock { - text: "Slash commands are not supported".into(), - kind: project::HoverBlockKind::PlainText, - }], - range: Some(range), - language: None, - }]))) - } - - fn inline_values( - &self, - _buffer_handle: Entity, - _range: Range, - _cx: &mut App, - ) -> Option>>> { - None - } - - fn inlay_hints( - &self, - _buffer_handle: Entity, - _range: Range, - _cx: &mut App, - ) -> Option>>> { - None - } - - fn resolve_inlay_hint( - &self, - _hint: project::InlayHint, - _buffer_handle: Entity, - _server_id: lsp::LanguageServerId, - _cx: &mut App, - ) -> Option>> { - None - } - - fn supports_inlay_hints(&self, _buffer: &Entity, _cx: &mut App) -> bool { - false - } - - fn document_highlights( - &self, - _buffer: &Entity, - _position: text::Anchor, - _cx: &mut App, - ) -> Option>>> { - None - } - - fn definitions( - &self, - _buffer: &Entity, - _position: text::Anchor, - _kind: editor::GotoDefinitionKind, - _cx: &mut App, - ) -> Option>>>> { - None - } - - fn range_for_rename( - &self, - _buffer: &Entity, - _position: text::Anchor, - _cx: &mut App, - ) -> Option>>>> { - None - } - - fn perform_rename( - &self, - _buffer: &Entity, - _position: text::Anchor, - _new_name: String, - _cx: &mut App, - ) -> Option>> { - None - } -} - -fn parse_slash_command(text: &str) -> Option<(usize, usize)> { - if let Some(remainder) = text.strip_prefix('/') { - let pos = remainder - .find(char::is_whitespace) - .unwrap_or(remainder.len()); - let command = &remainder[..pos]; - if !command.is_empty() && command.chars().all(char::is_alphanumeric) { - return Some((0, 1 + command.len())); - } - } - None -} - pub struct MessageEditorAddon {} impl MessageEditorAddon { @@ -1610,7 +1448,13 @@ impl Addon for MessageEditorAddon { #[cfg(test)] mod tests { - use std::{cell::Cell, ops::Range, path::Path, rc::Rc, sync::Arc}; + use std::{ + cell::{Cell, RefCell}, + ops::Range, + path::Path, + rc::Rc, + sync::Arc, + }; use acp_thread::MentionUri; use agent_client_protocol as acp; @@ -1657,8 +1501,8 @@ mod tests { history_store.clone(), None, Default::default(), + Default::default(), "Test", - false, EditorMode::AutoHeight { min_lines: 1, max_lines: None, @@ -1764,7 +1608,163 @@ mod tests { } #[gpui::test] - async fn test_context_completion_provider(cx: &mut TestAppContext) { + async fn test_completion_provider_commands(cx: &mut TestAppContext) { + init_test(cx); + + let app_state = cx.update(AppState::test); + + cx.update(|cx| { + language::init(cx); + editor::init(cx); + workspace::init(app_state.clone(), cx); + Project::init_settings(cx); + }); + + let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; + let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let workspace = window.root(cx).unwrap(); + + let mut cx = VisualTestContext::from_window(*window, cx); + + let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); + let available_commands = Rc::new(RefCell::new(vec![ + acp::AvailableCommand { + name: "quick-math".to_string(), + description: "2 + 2 = 4 - 1 = 3".to_string(), + input: None, + }, + acp::AvailableCommand { + name: "say-hello".to_string(), + description: "Say hello to whoever you want".to_string(), + input: Some(acp::AvailableCommandInput::Unstructured { + hint: "Who do you want to say hello to?".to_string(), + }), + }, + ])); + + let editor = workspace.update_in(&mut cx, |workspace, window, cx| { + let workspace_handle = cx.weak_entity(); + let message_editor = cx.new(|cx| { + MessageEditor::new( + workspace_handle, + project.clone(), + history_store.clone(), + None, + prompt_capabilities.clone(), + available_commands.clone(), + "Test", + EditorMode::AutoHeight { + max_lines: None, + min_lines: 1, + }, + window, + cx, + ) + }); + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item( + Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), + true, + true, + None, + window, + cx, + ); + }); + message_editor.read(cx).focus_handle(cx).focus(window); + message_editor.read(cx).editor().clone() + }); + + cx.simulate_input("/"); + + editor.update_in(&mut cx, |editor, window, cx| { + assert_eq!(editor.text(cx), "/"); + assert!(editor.has_visible_completions_menu()); + + assert_eq!( + current_completion_labels_with_documentation(editor), + &[ + ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()), + ("say-hello".into(), "Say hello to whoever you want".into()) + ] + ); + editor.set_text("", window, cx); + }); + + cx.simulate_input("/qui"); + + editor.update_in(&mut cx, |editor, window, cx| { + assert_eq!(editor.text(cx), "/qui"); + assert!(editor.has_visible_completions_menu()); + + assert_eq!( + current_completion_labels_with_documentation(editor), + &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())] + ); + editor.set_text("", window, cx); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + assert!(editor.has_visible_completions_menu()); + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + cx.run_until_parked(); + + editor.update_in(&mut cx, |editor, window, cx| { + assert_eq!(editor.text(cx), "/quick-math "); + assert!(!editor.has_visible_completions_menu()); + editor.set_text("", window, cx); + }); + + cx.simulate_input("/say"); + + editor.update_in(&mut cx, |editor, _window, cx| { + assert_eq!(editor.text(cx), "/say"); + assert!(editor.has_visible_completions_menu()); + + assert_eq!( + current_completion_labels_with_documentation(editor), + &[("say-hello".into(), "Say hello to whoever you want".into())] + ); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + assert!(editor.has_visible_completions_menu()); + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + cx.run_until_parked(); + + editor.update_in(&mut cx, |editor, _window, cx| { + assert_eq!(editor.text(cx), "/say-hello "); + assert!(editor.has_visible_completions_menu()); + + assert_eq!( + current_completion_labels_with_documentation(editor), + &[("say-hello".into(), "Say hello to whoever you want".into())] + ); + }); + + cx.simulate_input("GPT5"); + + editor.update_in(&mut cx, |editor, window, cx| { + assert!(editor.has_visible_completions_menu()); + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + cx.run_until_parked(); + + editor.update_in(&mut cx, |editor, _window, cx| { + assert_eq!(editor.text(cx), "/say-hello GPT5"); + assert!(!editor.has_visible_completions_menu()); + }); + } + + #[gpui::test] + async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) { init_test(cx); let app_state = cx.update(AppState::test); @@ -1857,8 +1857,8 @@ mod tests { history_store.clone(), None, prompt_capabilities.clone(), + Default::default(), "Test", - false, EditorMode::AutoHeight { max_lines: None, min_lines: 1, @@ -1888,7 +1888,6 @@ mod tests { assert_eq!(editor.text(cx), "Lorem @"); assert!(editor.has_visible_completions_menu()); - // Only files since we have default capabilities assert_eq!( current_completion_labels(editor), &[ @@ -2284,4 +2283,20 @@ mod tests { .map(|completion| completion.label.text) .collect::>() } + + fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> { + let completions = editor.current_completions().expect("Missing completions"); + completions + .into_iter() + .map(|completion| { + ( + completion.label.text, + completion + .documentation + .map(|d| d.text().to_string()) + .unwrap_or_default(), + ) + }) + .collect::>() + } } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 5e842d713d3d2e20f71e5a0b5e5c1fce773bed8d..c039826c83affe53d8354bf2c744bbe40b1015b8 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -35,7 +35,7 @@ use project::{Project, ProjectEntryId}; use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::{Settings as _, SettingsStore}; -use std::cell::Cell; +use std::cell::{Cell, RefCell}; use std::path::Path; use std::sync::Arc; use std::time::Instant; @@ -284,6 +284,7 @@ pub struct AcpThreadView { should_be_following: bool, editing_message: Option, prompt_capabilities: Rc>, + available_commands: Rc>>, is_loading_contents: bool, _cancel_task: Option>, _subscriptions: [Subscription; 3], @@ -325,7 +326,7 @@ impl AcpThreadView { cx: &mut Context, ) -> Self { let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); - let prevent_slash_commands = agent.clone().downcast::().is_some(); + let available_commands = Rc::new(RefCell::new(vec![])); let placeholder = if agent.name() == "Zed Agent" { format!("Message the {} — @ to include context", agent.name()) @@ -340,8 +341,8 @@ impl AcpThreadView { history_store.clone(), prompt_store.clone(), prompt_capabilities.clone(), + available_commands.clone(), placeholder, - prevent_slash_commands, editor::EditorMode::AutoHeight { min_lines: MIN_EDITOR_LINES, max_lines: Some(MAX_EDITOR_LINES), @@ -364,7 +365,7 @@ impl AcpThreadView { history_store.clone(), prompt_store.clone(), prompt_capabilities.clone(), - prevent_slash_commands, + available_commands.clone(), ) }); @@ -396,11 +397,12 @@ impl AcpThreadView { editing_message: None, edits_expanded: false, plan_expanded: false, + prompt_capabilities, + available_commands, editor_expanded: false, should_be_following: false, history_store, hovered_recent_history_item: None, - prompt_capabilities, is_loading_contents: false, _subscriptions: subscriptions, _cancel_task: None, @@ -486,6 +488,9 @@ impl AcpThreadView { Ok(thread) => { let action_log = thread.read(cx).action_log().clone(); + this.available_commands + .replace(thread.read(cx).available_commands()); + this.prompt_capabilities .set(thread.read(cx).prompt_capabilities()); @@ -5532,6 +5537,7 @@ pub(crate) mod tests { audio: true, embedded_context: true, }), + vec![], cx, ) }))) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 3f04f38607415e9678944c5546aa84abf4446597..1315915203e917a7be8dd6e41031495971ddec24 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -12952,6 +12952,21 @@ pub enum CompletionDocumentation { }, } +impl CompletionDocumentation { + #[cfg(any(test, feature = "test-support"))] + pub fn text(&self) -> SharedString { + match self { + CompletionDocumentation::Undocumented => "".into(), + CompletionDocumentation::SingleLine(s) => s.clone(), + CompletionDocumentation::MultiLinePlainText(s) => s.clone(), + CompletionDocumentation::MultiLineMarkdown(s) => s.clone(), + CompletionDocumentation::SingleLineAndMultiLinePlainText { single_line, .. } => { + single_line.clone() + } + } + } +} + impl From for CompletionDocumentation { fn from(docs: lsp::Documentation) -> Self { match docs { From 35c0d02c7cf0afb79b2c641a14d42ba3c7921a7c Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Tue, 2 Sep 2025 12:42:29 +0200 Subject: [PATCH 493/744] project: Temporarily disable terminal activation scripts on windows (#37361) They seem to break things on window right now Release Notes: - N/A --- crates/project/src/terminals.rs | 1 + crates/terminal/src/terminal.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 597da04617e9670e623196ef21f02c366e49d392..8789366d1d40111b679dc83d34b57e62e360ab51 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -186,6 +186,7 @@ impl Project { )?, }, None => match activation_script.clone() { + #[cfg(not(target_os = "windows"))] activation_script if !activation_script.is_empty() => { let activation_script = activation_script.join("; "); let to_run = if let Some(command) = spawn_task.command { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 0f4f2ae97b67b9fd43a63b54088f66c74ca1c855..c0c663f4987fd08aecbcc58b234333fef20a981c 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -531,7 +531,7 @@ impl TerminalBuilder { }, }; - if !activation_script.is_empty() && no_task { + if cfg!(not(target_os = "windows")) && !activation_script.is_empty() && no_task { for activation_script in activation_script { terminal.input(activation_script.into_bytes()); terminal.write_to_pty(b"\n"); From 47ad1b2143414482eeff20bab8b3b70d3c103875 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 2 Sep 2025 09:03:11 -0400 Subject: [PATCH 494/744] agent2: Fix terminal tool call content not being shown once truncated (#37318) We render terminals as inline if their content is below a certain line count, and scrollable past that point. In the scrollable case we weren't setting a height for the terminal's container, causing it to be rendered at height 0, which means no lines would be displayed. This PR fixes that by setting an explicit height for the scrollable case, like we do in the agent1 UI code. Release Notes: - agent: Fixed a bug that caused terminals in the panel to be empty after their content reached a certain size. --- crates/agent_ui/src/acp/thread_view.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c039826c83affe53d8354bf2c744bbe40b1015b8..c3bf7219b4919c7e479f22f327b73d90b0b1dad7 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2655,7 +2655,18 @@ impl AcpThreadView { .bg(cx.theme().colors().editor_background) .rounded_b_md() .text_ui_sm(cx) - .children(terminal_view.clone()), + .h_full() + .children(terminal_view.map(|terminal_view| { + if terminal_view + .read(cx) + .content_mode(window, cx) + .is_scrollable() + { + div().h_72().child(terminal_view).into_any_element() + } else { + terminal_view.into_any_element() + } + })), ) }) .into_any() From 60b95d925360e7e1c8e05faca00987982c5bd9db Mon Sep 17 00:00:00 2001 From: localcc Date: Tue, 2 Sep 2025 15:59:27 +0200 Subject: [PATCH 495/744] Use premultiplied alpha for emoji rendering (#37370) This improves emoji rendering on windows removing artifacts at the edges by using premultiplied alpha. A bit more context can be found in #37167 Release Notes: - N/A --- .../platform/windows/color_text_raster.hlsl | 3 ++- .../gpui/src/platform/windows/direct_write.rs | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/platform/windows/color_text_raster.hlsl b/crates/gpui/src/platform/windows/color_text_raster.hlsl index 322c743a993f11e2324b6fdb45c019919329f612..2fbc156ba5ea9e443366558d10d0b8791c2eb488 100644 --- a/crates/gpui/src/platform/windows/color_text_raster.hlsl +++ b/crates/gpui/src/platform/windows/color_text_raster.hlsl @@ -39,5 +39,6 @@ cbuffer GlyphLayerTextureParams : register(b0) { float4 emoji_rasterization_fragment(PixelInput input): SV_Target { float sample = t_layer.Sample(s_layer, input.texcoord.xy).r; float alpha_corrected = apply_contrast_and_gamma_correction(sample, run_color.rgb, grayscale_enhanced_contrast, gamma_ratios); - return float4(run_color.rgb, alpha_corrected * run_color.a); + float alpha = alpha_corrected * run_color.a; + return float4(run_color.rgb * alpha, alpha); } diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index e81b87c733bf277b8f534a3fda8d6db55ce34e36..5e44a609db6b5276ef8da040fd821786db67f6af 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -112,10 +112,10 @@ impl GPUState { RenderTarget: [ D3D11_RENDER_TARGET_BLEND_DESC { BlendEnable: true.into(), - SrcBlend: D3D11_BLEND_SRC_ALPHA, + SrcBlend: D3D11_BLEND_ONE, DestBlend: D3D11_BLEND_INV_SRC_ALPHA, BlendOp: D3D11_BLEND_OP_ADD, - SrcBlendAlpha: D3D11_BLEND_SRC_ALPHA, + SrcBlendAlpha: D3D11_BLEND_ONE, DestBlendAlpha: D3D11_BLEND_INV_SRC_ALPHA, BlendOpAlpha: D3D11_BLEND_OP_ADD, RenderTargetWriteMask: D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8, @@ -1132,6 +1132,20 @@ impl DirectWriteState { }; } + // Convert from premultiplied to straight alpha + for chunk in rasterized.chunks_exact_mut(4) { + let b = chunk[0] as f32; + let g = chunk[1] as f32; + let r = chunk[2] as f32; + let a = chunk[3] as f32; + if a > 0.0 { + let inv_a = 255.0 / a; + chunk[0] = (b * inv_a).clamp(0.0, 255.0) as u8; + chunk[1] = (g * inv_a).clamp(0.0, 255.0) as u8; + chunk[2] = (r * inv_a).clamp(0.0, 255.0) as u8; + } + } + Ok(rasterized) } From 2f279c5de437f9119e692fdce5132f601fc604f2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 2 Sep 2025 07:07:23 -0700 Subject: [PATCH 496/744] Fix small errors preventing WSL support from working (#37350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On nightly, when I run `zed` under WSL, I get an error parsing the shebang line ``` /usr/bin/env: ‘sh\r’: No such file or directory ``` I believe that this is because in CI, Git checks out the file with CRLF line endings, and that is how it is copied into the installer. Also, the file extension was incorrect when downloading the production remote server (a gzipped binary), preventing extraction from working properly. Release Notes: - N/A --- .gitattributes | 3 +++ crates/remote/src/transport/wsl.rs | 7 ++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitattributes b/.gitattributes index 9973cfb4db9ce8e9c79e84b9861a946f2f1c2f15..0dedc2d567dac982b217453c266a046b09ea4830 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ # Prevent GitHub from displaying comments within JSON files as errors. *.json linguist-language=JSON-with-Comments + +# Ensure the WSL script always has LF line endings, even on Windows +crates/zed/resources/windows/zed-wsl text eol=lf diff --git a/crates/remote/src/transport/wsl.rs b/crates/remote/src/transport/wsl.rs index ea8f2443d9a674492674bdc2fb19f2a021b03dcc..2b4d29eafeede14f305c4d21f61188b858253285 100644 --- a/crates/remote/src/transport/wsl.rs +++ b/crates/remote/src/transport/wsl.rs @@ -164,10 +164,7 @@ impl WslRemoteConnection { delegate.set_status(Some("Installing remote server"), cx); let wanted_version = match release_channel { - ReleaseChannel::Nightly => None, - ReleaseChannel::Dev => { - return Err(anyhow!("Dev builds require manual installation")); - } + ReleaseChannel::Nightly | ReleaseChannel::Dev => None, _ => Some(cx.update(|cx| AppVersion::global(cx))?), }; @@ -176,7 +173,7 @@ impl WslRemoteConnection { .await?; let tmp_path = RemotePathBuf::new( - PathBuf::from(format!("{}.{}.tmp", dst_path, std::process::id())), + PathBuf::from(format!("{}.{}.gz", dst_path, std::process::id())), PathStyle::Posix, ); From f06c18765f2029c7824c99e3628c13b9bafb3979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Tue, 2 Sep 2025 22:12:24 +0800 Subject: [PATCH 497/744] Rename from `create_ssh_worktree` to `create_remote_worktree` (#37358) This is a left-over issue of #37035 Release Notes: - N/A --- crates/project/src/worktree_store.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index b814e46bd1584cb076a057623b8b66800365876d..1eeeefc40ad09012e5d280c0821052cd6f8db098 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -228,7 +228,7 @@ impl WorktreeStore { Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab")))) } else { let abs_path = RemotePathBuf::new(abs_path.to_path_buf(), *path_style); - self.create_ssh_worktree(upstream_client.clone(), abs_path, visible, cx) + self.create_remote_worktree(upstream_client.clone(), abs_path, visible, cx) } } WorktreeStoreState::Local { fs } => { @@ -251,7 +251,7 @@ impl WorktreeStore { }) } - fn create_ssh_worktree( + fn create_remote_worktree( &mut self, client: AnyProtoClient, abs_path: RemotePathBuf, From 2eb7ac97e031441330a3b50d6dc02c58431599ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Tue, 2 Sep 2025 22:32:24 +0800 Subject: [PATCH 498/744] windows: Use a message-only window for `WindowsPlatform` (#37313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, we were using `PostThreadMessage` to pass messages to `WindowsPlatform`. This PR switches to an approach similar to `winit` which using a hidden window as the message window (I guess that’s why winit uses a hidden window?). The difference is that this PR creates it as a message-only window. Thanks to @reflectronic for the original PR #37255, this implementation just fits better with the current code style. Release Notes: - N/A --------- Co-authored-by: reflectronic --- .../gpui/src/platform/windows/dispatcher.rs | 17 +- crates/gpui/src/platform/windows/events.rs | 19 +- crates/gpui/src/platform/windows/platform.rs | 383 ++++++++++++------ crates/gpui/src/platform/windows/window.rs | 10 +- 4 files changed, 275 insertions(+), 154 deletions(-) diff --git a/crates/gpui/src/platform/windows/dispatcher.rs b/crates/gpui/src/platform/windows/dispatcher.rs index f554dea1284c5e3293cd0705d1754d07e4b6395a..3707a69047cf53cf68a40b3711e135f77dff8be3 100644 --- a/crates/gpui/src/platform/windows/dispatcher.rs +++ b/crates/gpui/src/platform/windows/dispatcher.rs @@ -14,34 +14,37 @@ use windows::{ }, Win32::{ Foundation::{LPARAM, WPARAM}, - UI::WindowsAndMessaging::PostThreadMessageW, + UI::WindowsAndMessaging::PostMessageW, }, }; -use crate::{PlatformDispatcher, TaskLabel, WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD}; +use crate::{ + HWND, PlatformDispatcher, SafeHwnd, TaskLabel, WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD, +}; pub(crate) struct WindowsDispatcher { main_sender: Sender, parker: Mutex, main_thread_id: ThreadId, - main_thread_id_win32: u32, + platform_window_handle: SafeHwnd, validation_number: usize, } impl WindowsDispatcher { pub(crate) fn new( main_sender: Sender, - main_thread_id_win32: u32, + platform_window_handle: HWND, validation_number: usize, ) -> Self { let parker = Mutex::new(Parker::new()); let main_thread_id = current().id(); + let platform_window_handle = platform_window_handle.into(); WindowsDispatcher { main_sender, parker, main_thread_id, - main_thread_id_win32, + platform_window_handle, validation_number, } } @@ -84,8 +87,8 @@ impl PlatformDispatcher for WindowsDispatcher { fn dispatch_on_main_thread(&self, runnable: Runnable) { match self.main_sender.send(runnable) { Ok(_) => unsafe { - PostThreadMessageW( - self.main_thread_id_win32, + PostMessageW( + Some(self.platform_window_handle.as_raw()), WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD, WPARAM(self.validation_number), LPARAM(0), diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index f4e3e5c3029936ce6bf9c10096fe0546376ff43c..06b242465f71ffe6e5a0c6c90132d548f0e3d8ff 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -24,6 +24,7 @@ pub(crate) const WM_GPUI_CLOSE_ONE_WINDOW: u32 = WM_USER + 2; pub(crate) const WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD: u32 = WM_USER + 3; pub(crate) const WM_GPUI_DOCK_MENU_ACTION: u32 = WM_USER + 4; pub(crate) const WM_GPUI_FORCE_UPDATE_WINDOW: u32 = WM_USER + 5; +pub(crate) const WM_GPUI_KEYBOARD_LAYOUT_CHANGED: u32 = WM_USER + 6; const SIZE_MOVE_LOOP_TIMER_ID: usize = 1; const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1; @@ -99,7 +100,7 @@ impl WindowsWindowInner { 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_INPUTLANGCHANGE => self.handle_input_language_changed(), WM_SHOWWINDOW => self.handle_window_visibility_changed(handle, wparam), WM_GPUI_CURSOR_STYLE_CHANGED => self.handle_cursor_changed(lparam), WM_GPUI_FORCE_UPDATE_WINDOW => self.draw_window(handle, true), @@ -264,8 +265,8 @@ impl WindowsWindowInner { callback(); } unsafe { - PostThreadMessageW( - self.main_thread_id_win32, + PostMessageW( + Some(self.platform_window_handle), WM_GPUI_CLOSE_ONE_WINDOW, WPARAM(self.validation_number), LPARAM(handle.0 as isize), @@ -1146,11 +1147,15 @@ impl WindowsWindowInner { Some(0) } - fn handle_input_language_changed(&self, lparam: LPARAM) -> Option { - let thread = self.main_thread_id_win32; - let validation = self.validation_number; + fn handle_input_language_changed(&self) -> Option { unsafe { - PostThreadMessageW(thread, WM_INPUTLANGCHANGE, WPARAM(validation), lparam).log_err(); + PostMessageW( + Some(self.platform_window_handle), + WM_GPUI_KEYBOARD_LAYOUT_CHANGED, + WPARAM(self.validation_number), + LPARAM(0), + ) + .log_err(); } Some(0) } diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index b06f369aabb860d7b0de3603ecc7e8357571fd2c..e12a74b966e11a15fe1fe1c8479dfa42d97ee446 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -2,7 +2,7 @@ use std::{ cell::RefCell, ffi::OsStr, path::{Path, PathBuf}, - rc::Rc, + rc::{Rc, Weak}, sync::Arc, }; @@ -19,7 +19,7 @@ use windows::{ Foundation::*, Graphics::Gdi::*, Security::Credentials::*, - System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*}, + System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*}, UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*}, }, core::*, @@ -28,21 +28,27 @@ use windows::{ use crate::*; pub(crate) struct WindowsPlatform { - state: RefCell, + inner: Rc, raw_window_handles: Arc>>, // The below members will never change throughout the entire lifecycle of the app. icon: HICON, - main_receiver: flume::Receiver, background_executor: BackgroundExecutor, foreground_executor: ForegroundExecutor, text_system: Arc, windows_version: WindowsVersion, drop_target_helper: IDropTargetHelper, - validation_number: usize, - main_thread_id_win32: u32, + handle: HWND, disable_direct_composition: bool, } +struct WindowsPlatformInner { + state: RefCell, + raw_window_handles: std::sync::Weak>>, + // The below members will never change throughout the entire lifecycle of the app. + validation_number: usize, + main_receiver: flume::Receiver, +} + pub(crate) struct WindowsPlatformState { callbacks: PlatformCallbacks, menus: Vec, @@ -83,11 +89,36 @@ impl WindowsPlatform { OleInitialize(None).context("unable to initialize Windows OLE")?; } let (main_sender, main_receiver) = flume::unbounded::(); - let main_thread_id_win32 = unsafe { GetCurrentThreadId() }; let validation_number = rand::random::(); + let raw_window_handles = Arc::new(RwLock::new(SmallVec::new())); + register_platform_window_class(); + let mut context = PlatformWindowCreateContext { + inner: None, + raw_window_handles: Arc::downgrade(&raw_window_handles), + validation_number, + main_receiver: Some(main_receiver), + }; + let result = unsafe { + CreateWindowExW( + WINDOW_EX_STYLE(0), + PLATFORM_WINDOW_CLASS_NAME, + None, + WINDOW_STYLE(0), + 0, + 0, + 0, + 0, + Some(HWND_MESSAGE), + None, + None, + Some(&context as *const _ as *const _), + ) + }; + let inner = context.inner.take().unwrap()?; + let handle = result?; let dispatcher = Arc::new(WindowsDispatcher::new( main_sender, - main_thread_id_win32, + handle, validation_number, )); let disable_direct_composition = std::env::var(DISABLE_DIRECT_COMPOSITION) @@ -105,23 +136,19 @@ impl WindowsPlatform { .context("Error creating drop target helper.")? }; let icon = load_icon().unwrap_or_default(); - let state = RefCell::new(WindowsPlatformState::new()); - let raw_window_handles = Arc::new(RwLock::new(SmallVec::new())); let windows_version = WindowsVersion::new().context("Error retrieve windows version")?; Ok(Self { - state, + inner, + handle, raw_window_handles, icon, - main_receiver, background_executor, foreground_executor, text_system, disable_direct_composition, windows_version, drop_target_helper, - validation_number, - main_thread_id_win32, }) } @@ -143,119 +170,20 @@ impl WindowsPlatform { }); } - fn close_one_window(&self, target_window: HWND) -> bool { - let mut lock = self.raw_window_handles.write(); - let index = lock - .iter() - .position(|handle| handle.as_raw() == target_window) - .unwrap(); - lock.remove(index); - - lock.is_empty() - } - - #[inline] - fn run_foreground_task(&self) { - for runnable in self.main_receiver.drain() { - runnable.run(); - } - } - fn generate_creation_info(&self) -> WindowCreationInfo { WindowCreationInfo { icon: self.icon, executor: self.foreground_executor.clone(), - current_cursor: self.state.borrow().current_cursor, + current_cursor: self.inner.state.borrow().current_cursor, windows_version: self.windows_version, drop_target_helper: self.drop_target_helper.clone(), - validation_number: self.validation_number, - main_receiver: self.main_receiver.clone(), - main_thread_id_win32: self.main_thread_id_win32, + validation_number: self.inner.validation_number, + main_receiver: self.inner.main_receiver.clone(), + platform_window_handle: self.handle, disable_direct_composition: self.disable_direct_composition, } } - fn handle_dock_action_event(&self, action_idx: usize) { - let mut lock = self.state.borrow_mut(); - if let Some(mut callback) = lock.callbacks.app_menu_action.take() { - let Some(action) = lock - .jump_list - .dock_menus - .get(action_idx) - .map(|dock_menu| dock_menu.action.boxed_clone()) - else { - lock.callbacks.app_menu_action = Some(callback); - log::error!("Dock menu for index {action_idx} not found"); - return; - }; - drop(lock); - callback(&*action); - self.state.borrow_mut().callbacks.app_menu_action = Some(callback); - } - } - - fn handle_input_lang_change(&self) { - let mut lock = self.state.borrow_mut(); - if let Some(mut callback) = lock.callbacks.keyboard_layout_change.take() { - drop(lock); - callback(); - self.state - .borrow_mut() - .callbacks - .keyboard_layout_change - .get_or_insert(callback); - } - } - - // Returns if the app should quit. - fn handle_events(&self) { - let mut msg = MSG::default(); - unsafe { - while GetMessageW(&mut msg, None, 0, 0).as_bool() { - match msg.message { - WM_QUIT => return, - WM_INPUTLANGCHANGE - | WM_GPUI_CLOSE_ONE_WINDOW - | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD - | WM_GPUI_DOCK_MENU_ACTION => { - if self.handle_gpui_events(msg.message, msg.wParam, msg.lParam, &msg) { - return; - } - } - _ => { - DispatchMessageW(&msg); - } - } - } - } - } - - // Returns true if the app should quit. - fn handle_gpui_events( - &self, - message: u32, - wparam: WPARAM, - lparam: LPARAM, - msg: *const MSG, - ) -> bool { - if wparam.0 != self.validation_number { - unsafe { DispatchMessageW(msg) }; - return false; - } - match message { - WM_GPUI_CLOSE_ONE_WINDOW => { - if self.close_one_window(HWND(lparam.0 as _)) { - return true; - } - } - WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD => self.run_foreground_task(), - WM_GPUI_DOCK_MENU_ACTION => self.handle_dock_action_event(lparam.0 as _), - WM_INPUTLANGCHANGE => self.handle_input_lang_change(), - _ => unreachable!(), - } - false - } - fn set_dock_menus(&self, menus: Vec) { let mut actions = Vec::new(); menus.into_iter().for_each(|menu| { @@ -263,7 +191,7 @@ impl WindowsPlatform { actions.push(dock_menu); } }); - let mut lock = self.state.borrow_mut(); + let mut lock = self.inner.state.borrow_mut(); lock.jump_list.dock_menus = actions; update_jump_list(&lock.jump_list).log_err(); } @@ -279,7 +207,7 @@ impl WindowsPlatform { actions.push(dock_menu); } }); - let mut lock = self.state.borrow_mut(); + let mut lock = self.inner.state.borrow_mut(); lock.jump_list.dock_menus = actions; lock.jump_list.recent_workspaces = entries; update_jump_list(&lock.jump_list) @@ -346,15 +274,25 @@ impl Platform for WindowsPlatform { } fn on_keyboard_layout_change(&self, callback: Box) { - self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback); + self.inner + .state + .borrow_mut() + .callbacks + .keyboard_layout_change = Some(callback); } fn run(&self, on_finish_launching: Box) { on_finish_launching(); self.begin_vsync_thread(); - self.handle_events(); - if let Some(ref mut callback) = self.state.borrow_mut().callbacks.quit { + let mut msg = MSG::default(); + unsafe { + while GetMessageW(&mut msg, None, 0, 0).as_bool() { + DispatchMessageW(&msg); + } + } + + if let Some(ref mut callback) = self.inner.state.borrow_mut().callbacks.quit { callback(); } } @@ -469,7 +407,7 @@ impl Platform for WindowsPlatform { } fn on_open_urls(&self, callback: Box)>) { - self.state.borrow_mut().callbacks.open_urls = Some(callback); + self.inner.state.borrow_mut().callbacks.open_urls = Some(callback); } fn prompt_for_paths( @@ -539,19 +477,19 @@ impl Platform for WindowsPlatform { } fn on_quit(&self, callback: Box) { - self.state.borrow_mut().callbacks.quit = Some(callback); + self.inner.state.borrow_mut().callbacks.quit = Some(callback); } fn on_reopen(&self, callback: Box) { - self.state.borrow_mut().callbacks.reopen = Some(callback); + self.inner.state.borrow_mut().callbacks.reopen = Some(callback); } fn set_menus(&self, menus: Vec, _keymap: &Keymap) { - self.state.borrow_mut().menus = menus.into_iter().map(|menu| menu.owned()).collect(); + self.inner.state.borrow_mut().menus = menus.into_iter().map(|menu| menu.owned()).collect(); } fn get_menus(&self) -> Option> { - Some(self.state.borrow().menus.clone()) + Some(self.inner.state.borrow().menus.clone()) } fn set_dock_menu(&self, menus: Vec, _keymap: &Keymap) { @@ -559,15 +497,19 @@ impl Platform for WindowsPlatform { } fn on_app_menu_action(&self, callback: Box) { - self.state.borrow_mut().callbacks.app_menu_action = Some(callback); + self.inner.state.borrow_mut().callbacks.app_menu_action = Some(callback); } fn on_will_open_app_menu(&self, callback: Box) { - self.state.borrow_mut().callbacks.will_open_app_menu = Some(callback); + self.inner.state.borrow_mut().callbacks.will_open_app_menu = Some(callback); } fn on_validate_app_menu_command(&self, callback: Box bool>) { - self.state.borrow_mut().callbacks.validate_app_menu_command = Some(callback); + self.inner + .state + .borrow_mut() + .callbacks + .validate_app_menu_command = Some(callback); } fn app_path(&self) -> Result { @@ -581,7 +523,7 @@ impl Platform for WindowsPlatform { fn set_cursor_style(&self, style: CursorStyle) { let hcursor = load_cursor(style); - let mut lock = self.state.borrow_mut(); + let mut lock = self.inner.state.borrow_mut(); if lock.current_cursor.map(|c| c.0) != hcursor.map(|c| c.0) { self.post_message( WM_GPUI_CURSOR_STYLE_CHANGED, @@ -684,10 +626,10 @@ impl Platform for WindowsPlatform { fn perform_dock_menu_action(&self, action: usize) { unsafe { - PostThreadMessageW( - self.main_thread_id_win32, + PostMessageW( + Some(self.handle), WM_GPUI_DOCK_MENU_ACTION, - WPARAM(self.validation_number), + WPARAM(self.inner.validation_number), LPARAM(action as isize), ) .log_err(); @@ -703,9 +645,118 @@ impl Platform for WindowsPlatform { } } +impl WindowsPlatformInner { + fn new(context: &mut PlatformWindowCreateContext) -> Result> { + let state = RefCell::new(WindowsPlatformState::new()); + Ok(Rc::new(Self { + state, + raw_window_handles: context.raw_window_handles.clone(), + validation_number: context.validation_number, + main_receiver: context.main_receiver.take().unwrap(), + })) + } + + fn handle_msg( + self: &Rc, + handle: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + let handled = match msg { + WM_GPUI_CLOSE_ONE_WINDOW + | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD + | WM_GPUI_DOCK_MENU_ACTION + | WM_GPUI_KEYBOARD_LAYOUT_CHANGED => self.handle_gpui_events(msg, wparam, lparam), + _ => None, + }; + if let Some(result) = handled { + LRESULT(result) + } else { + unsafe { DefWindowProcW(handle, msg, wparam, lparam) } + } + } + + fn handle_gpui_events(&self, message: u32, wparam: WPARAM, lparam: LPARAM) -> Option { + if wparam.0 != self.validation_number { + log::error!("Wrong validation number while processing message: {message}"); + return None; + } + match message { + WM_GPUI_CLOSE_ONE_WINDOW => { + if self.close_one_window(HWND(lparam.0 as _)) { + unsafe { PostQuitMessage(0) }; + } + Some(0) + } + WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD => self.run_foreground_task(), + WM_GPUI_DOCK_MENU_ACTION => self.handle_dock_action_event(lparam.0 as _), + WM_GPUI_KEYBOARD_LAYOUT_CHANGED => self.handle_keyboard_layout_change(), + _ => unreachable!(), + } + } + + fn close_one_window(&self, target_window: HWND) -> bool { + let Some(all_windows) = self.raw_window_handles.upgrade() else { + log::error!("Failed to upgrade raw window handles"); + return false; + }; + let mut lock = all_windows.write(); + let index = lock + .iter() + .position(|handle| handle.as_raw() == target_window) + .unwrap(); + lock.remove(index); + + lock.is_empty() + } + + #[inline] + fn run_foreground_task(&self) -> Option { + for runnable in self.main_receiver.drain() { + runnable.run(); + } + Some(0) + } + + fn handle_dock_action_event(&self, action_idx: usize) -> Option { + let mut lock = self.state.borrow_mut(); + let mut callback = lock.callbacks.app_menu_action.take()?; + let Some(action) = lock + .jump_list + .dock_menus + .get(action_idx) + .map(|dock_menu| dock_menu.action.boxed_clone()) + else { + lock.callbacks.app_menu_action = Some(callback); + log::error!("Dock menu for index {action_idx} not found"); + return Some(1); + }; + drop(lock); + callback(&*action); + self.state.borrow_mut().callbacks.app_menu_action = Some(callback); + Some(0) + } + + fn handle_keyboard_layout_change(&self) -> Option { + let mut callback = self + .state + .borrow_mut() + .callbacks + .keyboard_layout_change + .take()?; + callback(); + self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback); + Some(0) + } +} + impl Drop for WindowsPlatform { fn drop(&mut self) { unsafe { + DestroyWindow(self.handle) + .context("Destroying platform window") + .log_err(); OleUninitialize(); } } @@ -719,10 +770,17 @@ pub(crate) struct WindowCreationInfo { pub(crate) drop_target_helper: IDropTargetHelper, pub(crate) validation_number: usize, pub(crate) main_receiver: flume::Receiver, - pub(crate) main_thread_id_win32: u32, + pub(crate) platform_window_handle: HWND, pub(crate) disable_direct_composition: bool, } +struct PlatformWindowCreateContext { + inner: Option>>, + raw_window_handles: std::sync::Weak>>, + validation_number: usize, + main_receiver: Option>, +} + fn open_target(target: impl AsRef) -> Result<()> { let target = target.as_ref(); let ret = unsafe { @@ -893,6 +951,61 @@ fn should_auto_hide_scrollbars() -> Result { Ok(ui_settings.AutoHideScrollBars()?) } +const PLATFORM_WINDOW_CLASS_NAME: PCWSTR = w!("Zed::PlatformWindow"); + +fn register_platform_window_class() { + let wc = WNDCLASSW { + lpfnWndProc: Some(window_procedure), + lpszClassName: PCWSTR(PLATFORM_WINDOW_CLASS_NAME.as_ptr()), + ..Default::default() + }; + unsafe { RegisterClassW(&wc) }; +} + +unsafe extern "system" fn window_procedure( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + if msg == WM_NCCREATE { + let params = lparam.0 as *const CREATESTRUCTW; + let params = unsafe { &*params }; + let creation_context = params.lpCreateParams as *mut PlatformWindowCreateContext; + let creation_context = unsafe { &mut *creation_context }; + return match WindowsPlatformInner::new(creation_context) { + Ok(inner) => { + let weak = Box::new(Rc::downgrade(&inner)); + unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) }; + creation_context.inner = Some(Ok(inner)); + unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } + } + Err(error) => { + creation_context.inner = Some(Err(error)); + LRESULT(0) + } + }; + } + + 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 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)) }; + } + + result +} + #[cfg(test)] mod tests { use crate::{ClipboardItem, read_from_clipboard, write_to_clipboard}; diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index e3711d1a26b04a5fccbff3530c240f7a5fadd7ad..7fd4aff3c69ea5ba1c99dc018dc21e35748beeb3 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -73,7 +73,7 @@ pub(crate) struct WindowsWindowInner { pub(crate) windows_version: WindowsVersion, pub(crate) validation_number: usize, pub(crate) main_receiver: flume::Receiver, - pub(crate) main_thread_id_win32: u32, + pub(crate) platform_window_handle: HWND, } impl WindowsWindowState { @@ -228,7 +228,7 @@ impl WindowsWindowInner { windows_version: context.windows_version, validation_number: context.validation_number, main_receiver: context.main_receiver.clone(), - main_thread_id_win32: context.main_thread_id_win32, + platform_window_handle: context.platform_window_handle, })) } @@ -342,7 +342,7 @@ struct WindowCreateContext { drop_target_helper: IDropTargetHelper, validation_number: usize, main_receiver: flume::Receiver, - main_thread_id_win32: u32, + platform_window_handle: HWND, appearance: WindowAppearance, disable_direct_composition: bool, } @@ -361,7 +361,7 @@ impl WindowsWindow { drop_target_helper, validation_number, main_receiver, - main_thread_id_win32, + platform_window_handle, disable_direct_composition, } = creation_info; register_window_class(icon); @@ -419,7 +419,7 @@ impl WindowsWindow { drop_target_helper, validation_number, main_receiver, - main_thread_id_win32, + platform_window_handle, appearance, disable_direct_composition, }; From a96015b3c5593368730cb5a97956d1e8960b4578 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Tue, 2 Sep 2025 16:51:13 +0200 Subject: [PATCH 499/744] activity_indicator: Show extension installation and updates (#37374) This PR fixes an issue where extension operations would never show in the activity indicator despite this being implemented for ages. This happened because we were always returning `None` whenever the app has a global auto updater, which is always the case, so the code path for showing extension updates in the indicator could never be hit despite existing prior. Also slightly improves the messages shown for ongoing extension operations, as these were previously context unaware. While I was at this, I also quickly took a stab at cleaning up some remotely related stuff, namely: - The `AnimationExt` trait is now by default only implemented for anything that also implements `IntoElement`. This prevents `with_animation` from showing up for e.g. `u32` within the suggestions (finally). - Commonly used animations are now implemented in the `CommonAnimationExt` trait within the `ui` crate so the needed code does not always need to be copied and element IDs for the animations are truly unique. Relevant change here regarding the original issue is the change from the `return match` to just a `match` within the activitiy indicator, which solved the issue at hand. If we find this to be too noisy at some point, we can easily revisit, but I think this holds important enough information to be shown in the activity indicator, especially whilst developing extensions. Release Notes: - Extension installation and updates will now be shown in the activity indicator. --- .../src/activity_indicator.rs | 100 ++++++++++-------- crates/agent_ui/src/acp/thread_view.rs | 43 ++------ crates/agent_ui/src/active_thread.rs | 45 ++------ crates/agent_ui/src/agent_configuration.rs | 19 ++-- .../configure_context_server_modal.rs | 16 ++- crates/agent_ui/src/agent_diff.rs | 14 +-- crates/agent_ui/src/text_thread_editor.rs | 24 ++--- crates/ai_onboarding/src/ai_upsell_card.rs | 15 +-- crates/assistant_tools/src/edit_file_tool.rs | 10 +- crates/assistant_tools/src/terminal_tool.rs | 12 +-- crates/debugger_ui/src/dropdown_menus.rs | 12 +-- crates/git_ui/src/git_panel.rs | 22 ++-- crates/gpui/src/elements/animation.rs | 2 +- .../src/provider/copilot_chat.rs | 14 +-- .../recent_projects/src/remote_connections.rs | 20 ++-- crates/repl/src/outputs.rs | 17 +-- crates/ui/src/components/icon.rs | 5 +- crates/ui/src/components/image.rs | 5 +- crates/ui/src/traits.rs | 2 + crates/ui/src/traits/animation_ext.rs | 42 ++++++++ crates/ui/src/traits/transformable.rs | 7 ++ crates/ui/src/ui.rs | 1 + .../zed/src/zed/quick_action_bar/repl_menu.rs | 14 +-- 23 files changed, 203 insertions(+), 258 deletions(-) create mode 100644 crates/ui/src/traits/animation_ext.rs create mode 100644 crates/ui/src/traits/transformable.rs diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 6641db0805fed2fbade1e66cde143f58123dd3d4..b65d1472a7552d56ec319e12295088a2973796d5 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -1,11 +1,10 @@ use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType}; use editor::Editor; -use extension_host::ExtensionStore; +use extension_host::{ExtensionOperation, ExtensionStore}; use futures::StreamExt; use gpui::{ - Animation, AnimationExt as _, App, Context, CursorStyle, Entity, EventEmitter, - InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement, - Styled, Transformation, Window, actions, percentage, + App, Context, CursorStyle, Entity, EventEmitter, InteractiveElement as _, ParentElement as _, + Render, SharedString, StatefulInteractiveElement, Styled, Window, actions, }; use language::{ BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName, @@ -25,7 +24,10 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; +use ui::{ + ButtonLike, CommonAnimationExt, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, + prelude::*, +}; use util::truncate_and_trailoff; use workspace::{StatusItemView, Workspace, item::ItemHandle}; @@ -405,13 +407,7 @@ impl ActivityIndicator { icon: Some( Icon::new(IconName::ArrowCircle) .size(IconSize::Small) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ) + .with_rotate_animation(2) .into_any_element(), ), message, @@ -433,11 +429,7 @@ impl ActivityIndicator { icon: Some( Icon::new(IconName::ArrowCircle) .size(IconSize::Small) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) + .with_rotate_animation(2) .into_any_element(), ), message: format!("Debug: {}", session.read(cx).adapter()), @@ -460,11 +452,7 @@ impl ActivityIndicator { icon: Some( Icon::new(IconName::ArrowCircle) .size(IconSize::Small) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) + .with_rotate_animation(2) .into_any_element(), ), message: job_info.message.into(), @@ -671,8 +659,9 @@ impl ActivityIndicator { } // Show any application auto-update info. - if let Some(updater) = &self.auto_updater { - return match &updater.read(cx).status() { + self.auto_updater + .as_ref() + .and_then(|updater| match &updater.read(cx).status() { AutoUpdateStatus::Checking => Some(Content { icon: Some( Icon::new(IconName::Download) @@ -728,28 +717,49 @@ impl ActivityIndicator { tooltip_message: None, }), AutoUpdateStatus::Idle => None, - }; - } - - if let Some(extension_store) = - ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx)) - && let Some(extension_id) = extension_store.outstanding_operations().keys().next() - { - return Some(Content { - icon: Some( - Icon::new(IconName::Download) - .size(IconSize::Small) - .into_any_element(), - ), - message: format!("Updating {extension_id} extension…"), - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_error_message(&DismissErrorMessage, window, cx) - })), - tooltip_message: None, - }); - } + }) + .or_else(|| { + if let Some(extension_store) = + ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx)) + && let Some((extension_id, operation)) = + extension_store.outstanding_operations().iter().next() + { + let (message, icon, rotate) = match operation { + ExtensionOperation::Install => ( + format!("Installing {extension_id} extension…"), + IconName::LoadCircle, + true, + ), + ExtensionOperation::Upgrade => ( + format!("Updating {extension_id} extension…"), + IconName::Download, + false, + ), + ExtensionOperation::Remove => ( + format!("Removing {extension_id} extension…"), + IconName::LoadCircle, + true, + ), + }; - None + Some(Content { + icon: Some(Icon::new(icon).size(IconSize::Small).map(|this| { + if rotate { + this.with_rotate_animation(3).into_any_element() + } else { + this.into_any_element() + } + })), + message, + on_click: Some(Arc::new(|this, window, cx| { + this.dismiss_error_message(&Default::default(), window, cx) + })), + tooltip_message: None, + }) + } else { + None + } + }) } fn version_tooltip_message(version: &VersionCheckType) -> String { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c3bf7219b4919c7e479f22f327b73d90b0b1dad7..a9421723d125d27f3eb13c43fb17936f9078dae8 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -23,9 +23,9 @@ use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, - Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, - Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, - point, prelude::*, pulsating_between, + Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window, + WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*, + pulsating_between, }; use language::Buffer; @@ -45,8 +45,8 @@ use terminal_view::terminal_panel::TerminalPanel; use text::Anchor; use theme::ThemeSettings; use ui::{ - Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, - Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*, + Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, + PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -2515,13 +2515,7 @@ impl AcpThreadView { Icon::new(IconName::ArrowCircle) .size(IconSize::XSmall) .color(Color::Info) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ), + .with_rotate_animation(2) ) }) .child( @@ -2948,16 +2942,7 @@ impl AcpThreadView { Icon::new(IconName::ArrowCircle) .size(IconSize::Small) .color(Color::Muted) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage( - delta, - ))) - }, - ) - .into_any_element(), + .with_rotate_animation(2) ) .child(Label::new("Authenticating…").size(LabelSize::Small)), ) @@ -3270,13 +3255,7 @@ impl AcpThreadView { acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress) .size(IconSize::Small) .color(Color::Accent) - .with_animation( - "running", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ) + .with_rotate_animation(2) .into_any_element(), acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete) .size(IconSize::Small) @@ -5000,11 +4979,7 @@ fn loading_contents_spinner(size: IconSize) -> AnyElement { Icon::new(IconName::LoadCircle) .size(size) .color(Color::Accent) - .with_animation( - "load_context_circle", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) + .with_rotate_animation(3) .into_any_element() } diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index e0cecad6e2e8b37d649a9dbc0d91268096670365..371a59e7eb9eb88dc5200251f971ef851162b630 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -23,9 +23,8 @@ use gpui::{ AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry, ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla, ListAlignment, ListOffset, ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful, - StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation, - UnderlineStyle, WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, percentage, - pulsating_between, + StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, + WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, pulsating_between, }; use language::{Buffer, Language, LanguageRegistry}; use language_model::{ @@ -46,8 +45,8 @@ use std::time::Duration; use text::ToPoint; use theme::ThemeSettings; use ui::{ - Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize, - Tooltip, prelude::*, + Banner, CommonAnimationExt, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, + ScrollbarState, TextSize, Tooltip, prelude::*, }; use util::ResultExt as _; use util::markdown::MarkdownCodeBlock; @@ -2647,15 +2646,7 @@ impl ActiveThread { Icon::new(IconName::ArrowCircle) .color(Color::Accent) .size(IconSize::Small) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate( - percentage(delta), - )) - }, - ) + .with_rotate_animation(2) }), ), ) @@ -2831,17 +2822,11 @@ impl ActiveThread { } ToolUseStatus::Pending | ToolUseStatus::InputStillStreaming - | ToolUseStatus::Running => { - let icon = Icon::new(IconName::ArrowCircle) - .color(Color::Accent) - .size(IconSize::Small); - icon.with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) - .into_any_element() - } + | ToolUseStatus::Running => Icon::new(IconName::ArrowCircle) + .color(Color::Accent) + .size(IconSize::Small) + .with_rotate_animation(2) + .into_any_element(), ToolUseStatus::Finished(_) => div().w_0().into_any_element(), ToolUseStatus::Error(_) => { let icon = Icon::new(IconName::Close) @@ -2930,15 +2915,7 @@ impl ActiveThread { Icon::new(IconName::ArrowCircle) .size(IconSize::Small) .color(Color::Accent) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage( - delta, - ))) - }, - ), + .with_rotate_animation(2), ) .child( Label::new("Running…") diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 5f0b6f33c38b0b064fcb8b287a901a33e9e7186b..5981a3c52bf52ff4549b2f73a6322e308725750d 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -3,7 +3,7 @@ mod configure_context_server_modal; mod manage_profiles_modal; mod tool_picker; -use std::{ops::Range, sync::Arc, time::Duration}; +use std::{ops::Range, sync::Arc}; use agent_servers::{AgentServerCommand, AllAgentServersSettings, CustomAgentServerSettings}; use agent_settings::AgentSettings; @@ -17,9 +17,8 @@ use extension::ExtensionManifest; use extension_host::ExtensionStore; use fs::Fs; use gpui::{ - Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity, - EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, - WeakEntity, percentage, + Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable, + Hsla, ScrollHandle, Subscription, Task, WeakEntity, }; use language::LanguageRegistry; use language_model::{ @@ -32,8 +31,9 @@ use project::{ }; use settings::{Settings, SettingsStore, update_settings_file}; use ui::{ - Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, - Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, + Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, + Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, + prelude::*, }; use util::ResultExt as _; use workspace::{Workspace, create_and_open_local_file}; @@ -670,10 +670,9 @@ impl AgentConfiguration { Icon::new(IconName::LoadCircle) .size(IconSize::XSmall) .color(Color::Accent) - .with_animation( - SharedString::from(format!("{}-starting", context_server_id.0,)), - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + .with_keyed_rotate_animation( + SharedString::from(format!("{}-starting", context_server_id.0)), + 3, ) .into_any_element(), "Server is starting.", diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index c898a5acb5b8d0a45780efb383ece19b4cfe289d..e5027b876ac0f996e1f4df2a61af1477c6490c10 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -1,16 +1,14 @@ use std::{ path::PathBuf, sync::{Arc, Mutex}, - time::Duration, }; use anyhow::{Context as _, Result}; use context_server::{ContextServerCommand, ContextServerId}; use editor::{Editor, EditorElement, EditorStyle}; use gpui::{ - Animation, AnimationExt as _, AsyncWindowContext, DismissEvent, Entity, EventEmitter, - FocusHandle, Focusable, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, - WeakEntity, percentage, prelude::*, + AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, + TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*, }; use language::{Language, LanguageRegistry}; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; @@ -24,7 +22,9 @@ use project::{ }; use settings::{Settings as _, update_settings_file}; use theme::ThemeSettings; -use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*}; +use ui::{ + CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*, +}; use util::ResultExt as _; use workspace::{ModalView, Workspace}; @@ -638,11 +638,7 @@ impl ConfigureContextServerModal { Icon::new(IconName::ArrowCircle) .size(IconSize::XSmall) .color(Color::Info) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) + .with_rotate_animation(2) .into_any_element(), ) .child( diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 4bd525e9d0461a7a180cccc1748e7f8983c0b665..74bcb266d52ac25c91f3243c3e76f1e1f25d770e 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -14,9 +14,8 @@ use editor::{ scroll::Autoscroll, }; use gpui::{ - Action, Animation, AnimationExt, AnyElement, AnyView, App, AppContext, Empty, Entity, - EventEmitter, FocusHandle, Focusable, Global, SharedString, Subscription, Task, Transformation, - WeakEntity, Window, percentage, prelude::*, + Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle, + Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*, }; use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point}; @@ -29,9 +28,8 @@ use std::{ collections::hash_map::Entry, ops::Range, sync::Arc, - time::Duration, }; -use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider}; +use ui::{CommonAnimationExt, IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider}; use util::ResultExt; use workspace::{ Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, @@ -1084,11 +1082,7 @@ impl Render for AgentDiffToolbar { Icon::new(IconName::LoadCircle) .size(IconSize::Small) .color(Color::Accent) - .with_animation( - "load_circle", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ), + .with_rotate_animation(3), ) .into_any(); diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 70ec94beeadb1ae84839bab6747715223f2540c9..d979db5e0468b696d32ed755aec1ef47e2fd3df3 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -25,8 +25,8 @@ use gpui::{ Action, Animation, AnimationExt, AnyElement, AnyView, App, ClipboardEntry, ClipboardItem, Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement, IntoElement, ParentElement, Pixels, Render, RenderImage, SharedString, Size, - StatefulInteractiveElement, Styled, Subscription, Task, Transformation, WeakEntity, actions, - div, img, percentage, point, prelude::*, pulsating_between, size, + StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, actions, div, img, point, + prelude::*, pulsating_between, size, }; use language::{ BufferSnapshot, LspAdapterDelegate, ToOffset, @@ -53,8 +53,8 @@ use std::{ }; use text::SelectionGoal; use ui::{ - ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, - prelude::*, + ButtonLike, CommonAnimationExt, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, + TintColor, Tooltip, prelude::*, }; use util::{ResultExt, maybe}; use workspace::{ @@ -1061,15 +1061,7 @@ impl TextThreadEditor { Icon::new(IconName::ArrowCircle) .size(IconSize::XSmall) .color(Color::Info) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate( - percentage(delta), - )) - }, - ) + .with_rotate_animation(2) .into_any_element(), ); note = Some(Self::esc_kbd(cx).into_any_element()); @@ -2790,11 +2782,7 @@ fn invoked_slash_command_fold_placeholder( .child(Label::new(format!("/{}", command.name))) .map(|parent| match &command.status { InvokedSlashCommandStatus::Running(_) => { - parent.child(Icon::new(IconName::ArrowCircle).with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(4)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - )) + parent.child(Icon::new(IconName::ArrowCircle).with_rotate_animation(4)) } InvokedSlashCommandStatus::Error(message) => parent.child( Label::new(format!("error: {message}")) diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 106dcb0aef0ee35836b2c7c576d7c68799ea988a..efe6e4165e445c4cd92f4d08dfc0c1e1947acd55 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -1,12 +1,9 @@ -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; use client::{Client, UserStore, zed_urls}; use cloud_llm_client::Plan; -use gpui::{ - Animation, AnimationExt, AnyElement, App, Entity, IntoElement, RenderOnce, Transformation, - Window, percentage, -}; -use ui::{Divider, Vector, VectorName, prelude::*}; +use gpui::{AnyElement, App, Entity, IntoElement, RenderOnce, Window}; +use ui::{CommonAnimationExt, Divider, Vector, VectorName, prelude::*}; use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions}; @@ -147,11 +144,7 @@ impl RenderOnce for AiUpsellCard { rems_from_px(72.), ) .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3))) - .with_animation( - "loading_stamp", - Animation::new(Duration::from_secs(10)).repeat(), - |this, delta| this.transform(Transformation::rotate(percentage(delta))), - ), + .with_rotate_animation(10), ); let pro_trial_stamp = div() diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 7b208ccc7768c9c0df2904573e2d47504a8eb61f..d13f9891c3af1933ee49428c223d3e6737871047 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -17,7 +17,7 @@ use editor::{ use futures::StreamExt; use gpui::{ Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task, - TextStyleRefinement, Transformation, WeakEntity, percentage, pulsating_between, px, + TextStyleRefinement, WeakEntity, pulsating_between, px, }; use indoc::formatdoc; use language::{ @@ -44,7 +44,7 @@ use std::{ time::Duration, }; use theme::ThemeSettings; -use ui::{Disclosure, Tooltip, prelude::*}; +use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*}; use util::ResultExt; use workspace::Workspace; @@ -939,11 +939,7 @@ impl ToolCard for EditFileToolCard { Icon::new(IconName::ArrowCircle) .size(IconSize::XSmall) .color(Color::Info) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ), + .with_rotate_animation(2), ) }) .when_some(error_message, |header, error_message| { diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 774f32426540e077e5bde72081db789329f86262..1605003671621b90e58a5f62e521c0aba2c990c6 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -8,8 +8,8 @@ use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{Tool, ToolCard, ToolResult, ToolUseStatus}; use futures::{FutureExt as _, future::Shared}; use gpui::{ - Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, - TextStyleRefinement, Transformation, WeakEntity, Window, percentage, + AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement, + WeakEntity, Window, }; use language::LineEnding; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; @@ -28,7 +28,7 @@ use std::{ }; use terminal_view::TerminalView; use theme::ThemeSettings; -use ui::{Disclosure, Tooltip, prelude::*}; +use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*}; use util::{ ResultExt, get_system_shell, markdown::MarkdownInlineCode, size::format_file_size, time::duration_alt_display, @@ -522,11 +522,7 @@ impl ToolCard for TerminalToolCard { Icon::new(IconName::ArrowCircle) .size(IconSize::XSmall) .color(Color::Info) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ), + .with_rotate_animation(2), ) }) .when(tool_failed || command_failed, |header| { diff --git a/crates/debugger_ui/src/dropdown_menus.rs b/crates/debugger_ui/src/dropdown_menus.rs index c5399f6f69648dcfa775a6dd6da62bd637124f2c..c611d5d44f36b4eafb578a400da615bbd96b4cd2 100644 --- a/crates/debugger_ui/src/dropdown_menus.rs +++ b/crates/debugger_ui/src/dropdown_menus.rs @@ -1,9 +1,9 @@ -use std::{rc::Rc, time::Duration}; +use std::rc::Rc; use collections::HashMap; -use gpui::{Animation, AnimationExt as _, Entity, Transformation, WeakEntity, percentage}; +use gpui::{Entity, WeakEntity}; use project::debugger::session::{ThreadId, ThreadStatus}; -use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*}; +use ui::{CommonAnimationExt, ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*}; use util::{maybe, truncate_and_trailoff}; use crate::{ @@ -152,11 +152,7 @@ impl DebugPanel { Icon::new(IconName::ArrowCircle) .size(IconSize::Small) .color(Color::Muted) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) + .with_rotate_animation(2) .into_any_element() } else { match running_state.thread_status(cx).unwrap_or_default() { diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 4ecb4a8829659ca9a25152db8d1eff529cfff2b1..64163b0ebc33f908de5c5cd8c97a24418bf4ba43 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -31,11 +31,11 @@ use git::{ UnstageAll, }; use gpui::{ - Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, - DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, - ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, Point, - PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle, - WeakEntity, actions, anchored, deferred, percentage, uniform_list, + Action, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, DismissEvent, Entity, + EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, + ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, + Subscription, Task, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, + uniform_list, }; use itertools::Itertools; use language::{Buffer, File}; @@ -63,8 +63,8 @@ use std::{collections::HashSet, sync::Arc, time::Duration, usize}; use strum::{IntoEnumIterator, VariantNames}; use time::OffsetDateTime; use ui::{ - Checkbox, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, PopoverMenu, Scrollbar, - ScrollbarState, SplitButton, Tooltip, prelude::*, + Checkbox, CommonAnimationExt, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, + PopoverMenu, Scrollbar, ScrollbarState, SplitButton, Tooltip, prelude::*, }; use util::{ResultExt, TryFutureExt, maybe}; use workspace::SERIALIZATION_THROTTLE_TIME; @@ -3088,13 +3088,7 @@ impl GitPanel { Icon::new(IconName::ArrowCircle) .size(IconSize::XSmall) .color(Color::Info) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ), + .with_rotate_animation(2), ) .child( Label::new("Generating Commit...") diff --git a/crates/gpui/src/elements/animation.rs b/crates/gpui/src/elements/animation.rs index 11dd19e260c20e49b87e05137771be73a3f816ea..e72fb00456d14dec74ffc56e040511c189af1d18 100644 --- a/crates/gpui/src/elements/animation.rs +++ b/crates/gpui/src/elements/animation.rs @@ -87,7 +87,7 @@ pub trait AnimationExt { } } -impl AnimationExt for E {} +impl AnimationExt for E {} /// A GPUI element that applies an animation to another element pub struct AnimationElement { diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index bd284eb72b207dee90048f06dc44a8e21ae8d34f..071424eabe3c1ad3436de201860d6220ab664a06 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -14,10 +14,7 @@ use copilot::{Copilot, Status}; use futures::future::BoxFuture; use futures::stream::BoxStream; use futures::{FutureExt, Stream, StreamExt}; -use gpui::{ - Action, Animation, AnimationExt, AnyView, App, AsyncApp, Entity, Render, Subscription, Task, - Transformation, percentage, svg, -}; +use gpui::{Action, AnyView, App, AsyncApp, Entity, Render, Subscription, Task, svg}; use language::language_settings::all_language_settings; use language_model::{ AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, @@ -28,8 +25,7 @@ use language_model::{ StopReason, TokenUsage, }; use settings::SettingsStore; -use std::time::Duration; -use ui::prelude::*; +use ui::{CommonAnimationExt, prelude::*}; use util::debug_panic; use crate::provider::x_ai::count_xai_tokens; @@ -672,11 +668,7 @@ impl Render for ConfigurationView { }), ) } else { - let loading_icon = Icon::new(IconName::ArrowCircle).with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(4)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ); + let loading_icon = Icon::new(IconName::ArrowCircle).with_rotate_animation(4); const ERROR_LABEL: &str = "Copilot Chat requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different Assistant provider."; diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs index e543bf219ff0bc8226e819798a9ea74a098d0f98..a7f915301f42850b03be951f596a8542842a6877 100644 --- a/crates/recent_projects/src/remote_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -1,5 +1,5 @@ use std::collections::BTreeSet; -use std::{path::PathBuf, sync::Arc, time::Duration}; +use std::{path::PathBuf, sync::Arc}; use anyhow::{Context as _, Result}; use auto_update::AutoUpdater; @@ -7,9 +7,9 @@ use editor::Editor; use extension_host::ExtensionStore; use futures::channel::oneshot; use gpui::{ - Animation, AnimationExt, AnyWindowHandle, App, AsyncApp, DismissEvent, Entity, EventEmitter, - Focusable, FontFeatures, ParentElement as _, PromptLevel, Render, SemanticVersion, - SharedString, Task, TextStyleRefinement, Transformation, WeakEntity, percentage, + AnyWindowHandle, App, AsyncApp, DismissEvent, Entity, EventEmitter, Focusable, FontFeatures, + ParentElement as _, PromptLevel, Render, SemanticVersion, SharedString, Task, + TextStyleRefinement, WeakEntity, }; use language::CursorShape; @@ -24,8 +24,8 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources, SettingsUi}; use theme::ThemeSettings; use ui::{ - ActiveTheme, Color, Context, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, - LabelCommon, Styled, Window, prelude::*, + ActiveTheme, Color, CommonAnimationExt, Context, Icon, IconName, IconSize, InteractiveElement, + IntoElement, Label, LabelCommon, Styled, Window, prelude::*, }; use util::serde::default_true; use workspace::{AppState, ModalView, Workspace}; @@ -268,13 +268,7 @@ impl Render for RemoteConnectionPrompt { .child( Icon::new(IconName::ArrowCircle) .size(IconSize::Medium) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ), + .with_rotate_animation(2), ) .child( div() diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index 767b103435e1f80b2b6802bdc2525fcd992931bc..2cd6494d66be1b615e10e537c139e4b2e22af863 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -33,16 +33,13 @@ //! This module is designed to work with Jupyter message protocols, //! interpreting and displaying various types of Jupyter output. -use std::time::Duration; - use editor::{Editor, MultiBuffer}; -use gpui::{ - Animation, AnimationExt, AnyElement, ClipboardItem, Entity, Render, Transformation, WeakEntity, - percentage, -}; +use gpui::{AnyElement, ClipboardItem, Entity, Render, WeakEntity}; use language::Buffer; use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType}; -use ui::{Context, IntoElement, Styled, Tooltip, Window, div, prelude::*, v_flex}; +use ui::{ + CommonAnimationExt, Context, IntoElement, Styled, Tooltip, Window, div, prelude::*, v_flex, +}; mod image; use image::ImageView; @@ -481,11 +478,7 @@ impl Render for ExecutionView { Icon::new(IconName::ArrowCircle) .size(IconSize::Small) .color(Color::Muted) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ), + .with_rotate_animation(3), ) .child(Label::new("Executing...").color(Color::Muted)) .into_any_element(), diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index bc0ec462e9fbb964abc1e305933ce759ddde0ebc..8f7ef41108afd22a7f932e8ab6ed1b74078244ec 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -9,6 +9,7 @@ use gpui::{AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation pub use icon_decoration::*; pub use icons::*; +use crate::traits::transformable::Transformable; use crate::{Indicator, prelude::*}; #[derive(IntoElement)] @@ -180,8 +181,10 @@ impl Icon { self.size = size; self } +} - pub fn transform(mut self, transformation: Transformation) -> Self { +impl Transformable for Icon { + fn transform(mut self, transformation: Transformation) -> Self { self.transformation = transformation; self } diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs index 6e552ddcee83e20d3812f78c67270c0291c2c0e7..8a14cffd3b2de2e184fd87a9212775c470e3118d 100644 --- a/crates/ui/src/components/image.rs +++ b/crates/ui/src/components/image.rs @@ -7,6 +7,7 @@ use strum::{EnumIter, EnumString, IntoStaticStr}; use crate::Color; use crate::prelude::*; +use crate::traits::transformable::Transformable; #[derive( Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString, IntoStaticStr, Serialize, Deserialize, @@ -74,8 +75,10 @@ impl Vector { self.size = size; self } +} - pub fn transform(mut self, transformation: Transformation) -> Self { +impl Transformable for Vector { + fn transform(mut self, transformation: Transformation) -> Self { self.transformation = transformation; self } diff --git a/crates/ui/src/traits.rs b/crates/ui/src/traits.rs index 628c76aaddecaa291b3cfad2e6d16ccd6478c767..9627f6d6ad275dbcd4281cd6a85741d932b688fe 100644 --- a/crates/ui/src/traits.rs +++ b/crates/ui/src/traits.rs @@ -1,6 +1,8 @@ +pub mod animation_ext; pub mod clickable; pub mod disableable; pub mod fixed; pub mod styled_ext; pub mod toggleable; +pub mod transformable; pub mod visible_on_hover; diff --git a/crates/ui/src/traits/animation_ext.rs b/crates/ui/src/traits/animation_ext.rs new file mode 100644 index 0000000000000000000000000000000000000000..4907c71ff2ad29104e3e9a3f408c1c9b69af8d44 --- /dev/null +++ b/crates/ui/src/traits/animation_ext.rs @@ -0,0 +1,42 @@ +use std::time::Duration; + +use gpui::{Animation, AnimationElement, AnimationExt, Transformation, percentage}; + +use crate::{prelude::*, traits::transformable::Transformable}; + +/// An extension trait for adding common animations to animatable components. +pub trait CommonAnimationExt: AnimationExt { + /// Render this component as rotating over the given duration. + /// + /// NOTE: This method uses the location of the caller to generate an ID for this state. + /// If this is not sufficient to identify your state (e.g. you're rendering a list item), + /// you can provide a custom ElementID using the `use_keyed_rotate_animation` method. + #[track_caller] + fn with_rotate_animation(self, duration: u64) -> AnimationElement + where + Self: Transformable + Sized, + { + self.with_keyed_rotate_animation( + ElementId::CodeLocation(*std::panic::Location::caller()), + duration, + ) + } + + /// Render this component as rotating with the given element ID over the given duration. + fn with_keyed_rotate_animation( + self, + id: impl Into, + duration: u64, + ) -> AnimationElement + where + Self: Transformable + Sized, + { + self.with_animation( + id, + Animation::new(Duration::from_secs(duration)).repeat(), + |component, delta| component.transform(Transformation::rotate(percentage(delta))), + ) + } +} + +impl CommonAnimationExt for T {} diff --git a/crates/ui/src/traits/transformable.rs b/crates/ui/src/traits/transformable.rs new file mode 100644 index 0000000000000000000000000000000000000000..f52141f304d51807e45f97d5091f5a5087467679 --- /dev/null +++ b/crates/ui/src/traits/transformable.rs @@ -0,0 +1,7 @@ +use gpui::Transformation; + +/// A trait for components that can be transformed. +pub trait Transformable { + /// Sets the transformation for the element. + fn transform(self, transformation: Transformation) -> Self; +} diff --git a/crates/ui/src/ui.rs b/crates/ui/src/ui.rs index dadc5ecdd12d6c3b7e3431977f1606d56c456cfa..17e707f11b3a43392bff755f1ba7904b61b02f92 100644 --- a/crates/ui/src/ui.rs +++ b/crates/ui/src/ui.rs @@ -17,3 +17,4 @@ pub mod utils; pub use components::*; pub use prelude::*; pub use styles::*; +pub use traits::animation_ext::*; diff --git a/crates/zed/src/zed/quick_action_bar/repl_menu.rs b/crates/zed/src/zed/quick_action_bar/repl_menu.rs index eaa989f88dc9e3e3e969841f02fa334a8f6f594e..82eb82de1e2807346eb3ade2ced8a7946413f0a4 100644 --- a/crates/zed/src/zed/quick_action_bar/repl_menu.rs +++ b/crates/zed/src/zed/quick_action_bar/repl_menu.rs @@ -1,7 +1,5 @@ -use std::time::Duration; - use gpui::ElementId; -use gpui::{Animation, AnimationExt, AnyElement, Entity, Transformation, percentage}; +use gpui::{AnyElement, Entity}; use picker::Picker; use repl::{ ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, Session, @@ -10,8 +8,8 @@ use repl::{ worktree_id_for_editor, }; use ui::{ - ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu, - PopoverMenuHandle, Tooltip, prelude::*, + ButtonLike, CommonAnimationExt, ContextMenu, IconWithIndicator, Indicator, IntoElement, + PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*, }; use util::ResultExt; @@ -224,11 +222,7 @@ impl QuickActionBar { .child(if menu_state.icon_is_animating { Icon::new(menu_state.icon) .color(menu_state.icon_color) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(5)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) + .with_rotate_animation(5) .into_any_element() } else { IconWithIndicator::new( From c89435154450e8d8878f6f03ff39c772b1cfc503 Mon Sep 17 00:00:00 2001 From: Dino Date: Tue, 2 Sep 2025 16:11:35 +0100 Subject: [PATCH 500/744] vim: Fix change surrounding quotes with whitespace within (#37321) This commit fixes a bug with Zed's vim mode surrounds plugin when dealing with replacing pairs with quote and the contents between the pairs had some whitespace within them. For example, with the following string: ``` ' str ' ``` If one was to use the `cs'"` command, to replace single quotes with double quotes, the result would actually be: ``` "str" ``` As the whitespace before and after the closing character was removed. This happens because of the way the plugin decides whether to add or remove whitespace after and before the opening and closing characters, repsectively. For example, using `cs{[` yields a different result from using `cs{]`, the former adds a space while the latter does not. However, since for quotes the opening and closing character is exactly the same, this behavior is not possible, so this commit updates the code in `vim::surrounds::Vim.change_surrounds` so that it never adds or removes whitespace when dealing with any type of quotes. Closes #12247 Release Notes: - Fixed whitespace handling when changing surrounding pairs to quotes in vim mode --- crates/vim/src/surrounds.rs | 43 ++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index ca65204fab2a8ddd21e4788ca5c1e5cbe325447c..83500cf88b56c8f556887eb874901f50b6178018 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -240,7 +240,24 @@ impl Vim { newline: false, }, }; - let surround = pair.end != surround_alias((*text).as_ref()); + + // Determines whether space should be added/removed after + // and before the surround pairs. + // For example, using `cs{[` will add a space before and + // after the pair, while using `cs{]` will not, notice the + // use of the closing bracket instead of the opening bracket + // on the target object. + // In the case of quotes, the opening and closing is the + // same, so no space will ever be added or removed. + let surround = match target { + Object::Quotes + | Object::BackQuotes + | Object::AnyQuotes + | Object::MiniQuotes + | Object::DoubleQuotes => true, + _ => pair.end != surround_alias((*text).as_ref()), + }; + let (display_map, selections) = editor.selections.all_adjusted_display(cx); let mut edits = Vec::new(); let mut anchors = Vec::new(); @@ -1128,6 +1145,30 @@ mod test { ];"}, Mode::Normal, ); + + // test change quotes. + cx.set_state(indoc! {"' ˇstr '"}, Mode::Normal); + cx.simulate_keystrokes("c s ' \""); + cx.assert_state(indoc! {"ˇ\" str \""}, Mode::Normal); + + // test multi cursor change quotes + cx.set_state( + indoc! {" + ' ˇstr ' + some example text here + ˇ' str ' + "}, + Mode::Normal, + ); + cx.simulate_keystrokes("c s ' \""); + cx.assert_state( + indoc! {" + ˇ\" str \" + some example text here + ˇ\" str \" + "}, + Mode::Normal, + ); } #[gpui::test] From 8e7caa429dcfbf022393ba6241b63193ffb89e95 Mon Sep 17 00:00:00 2001 From: Jonathan Camp Date: Tue, 2 Sep 2025 17:26:12 +0200 Subject: [PATCH 501/744] remove extra brace in rules template (#37356) Release Notes: - Fixed: remove extra brace in rules template --- assets/prompts/assistant_system_prompt.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/prompts/assistant_system_prompt.hbs b/assets/prompts/assistant_system_prompt.hbs index b4545f5a7449bf8c562ea15d722ae8199c42e97a..f47c1ffa908b861eb81d37642a7634616c92a0d9 100644 --- a/assets/prompts/assistant_system_prompt.hbs +++ b/assets/prompts/assistant_system_prompt.hbs @@ -172,7 +172,7 @@ The user has specified the following rules that should be applied: Rules title: {{title}} {{/if}} `````` -{{contents}}} +{{contents}} `````` {{/each}} {{/if}} From 7e3fbeb59d4c51241f9eabb154bb46f36f5767b2 Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Tue, 2 Sep 2025 17:59:58 +0200 Subject: [PATCH 502/744] Add the Glossary from the channel into Zed (#37360) This should make it easier for contributors to learn all the terms used in the Zed code base. Release Notes: - N/A --- CONTRIBUTING.md | 2 + docs/src/development/glossary.md | 108 +++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 docs/src/development/glossary.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd5bbdc2e1d7f7a98e42fdaba21a6189eb92c638..407ba002c7bc5a75c922faa72f1f270c62e82410 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,6 +65,8 @@ If you would like to add a new icon to the Zed icon theme, [open a Discussion](h ## Bird's-eye view of Zed +We suggest you keep the [zed glossary](docs/src/development/GLOSSARY.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase. + Zed is made up of several smaller crates - let's go over those you're most likely to interact with: - [`gpui`](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. **We recommend familiarizing yourself with the root level GPUI documentation.** diff --git a/docs/src/development/glossary.md b/docs/src/development/glossary.md new file mode 100644 index 0000000000000000000000000000000000000000..08bc9f0e91b7e4f054ef9f10892b9be9feffcfce --- /dev/null +++ b/docs/src/development/glossary.md @@ -0,0 +1,108 @@ +These are some terms and structures frequently used throughout the zed codebase. This is a best effort list. + +Questions: + +- Can we generate this list from doc comments throughout zed? +- We should have a section that shows the various UI parts and their names. (Can't do that in the channel.) + +## Naming conventions + +These are generally true for the whole codebase. Note that Name can be anything +here. An example would be `AnyElement` and `LspStore`. + +- `AnyName`: A type erased version of _name_. Think `Box`. +- `NameStore`: A wrapper type which abstracts over whether operations are running locally or on a remote. + +## GPUI + +### State menagement + +- `App`: A singleton which holds the full application state including all the entities. Crucially: `App` is not `Send`, which means that `App` only exists on the thread that created it (which is the main/UI thread, usually). Thus, if you see a `&mut App`, know that you're on UI thread. +- `Context`: A wrapper around the `App` struct with specialized behavior for a specific `Entity`. Think of it as `(&mut App, Entity)`. The specialized behavior is surfaced in the API surface of `Context`. E.g., `App::spawn` takes an `AsyncFnOnce(AsyncApp) -> Ret`, whereas `Context::spawn` takes an `AsyncFnOnce(WeakEntity, AsyncApp) -> Ret`. +- `AsyncApp`: An owned version of `App` for use in async contexts. This type is _still_ not `Send` (so `AsyncApp` = you're on the main thread) and any use of it may be fallible (to account for the fact that the `App` might've been terminated by the time this closure runs). + The convenience of `AsyncApp` lies in the fact that you usually interface with `App` via `&mut App`, which would be inconvenient to use with async closures; `AsyncApp` is owned, so you can use it in async closures with no sweat. +- `AppContext` A trait which abstracts over `App`, `AsyncApp` & `Context` and their Test versions. +- `Task`: A future running or scheduled to run on the background or foreground + executor. In contradiction to regular Futures Tasks do not need `.await` to start running. You do need to await them to get the result of the task. +- `Executor`: Used to spawn tasks that run either on the foreground or background thread. Try to run the tasks on the background thread. + - `BackgroundExecutor`: A threadpool running `Task`s. + - `ForegroundExecutor`: The main thread running `Task`s. +- `Entity`: A strong, well-typed reference to a struct which is managed by gpui. Effectively a pointer/map key into the `App::EntityMap`. +- `WeakEntity`: A runtime checked reference to an `Entity` which may no longer exist. Similar to [`std::rc::Weak`](https://doc.rust-lang.org/std/rc/struct.Weak.html). +- `Global`: A singleton type which has only one value, that is stored in the `App`. +- `Event`: A datatype which can be send by an `Entity` to subscribers +- `Action`: An event that represents a user's keyboard input that can be handled by listeners + Example: `file finder: toggle` +- `Observing`: reacting entities notifying they've changed +- `Subscription`: An event handler that is used to react to the changes of state in the application. + 1. Emitted event handling + 2. Observing `{new,release,on notify}` of an entity + +### UI + +- `View`: An `Entity` which can produce an `Element` through its implementation of `Render`. +- `Element`: A type that can be laid out and painted to the screen. +- `element expression`: An expression that builds an element tree, example: + +```rust +h_flex() + .id(text[i]) + .relative() + .when(selected, |this| { + this.child( + div() + .h_4() + .absolute() + etc etc +``` + +- `Component`: A builder which can be rendered turning it into an `Element`. +- `Dispatch tree`: TODO +- `Focus`: The place where keystrokes are handled first +- `Focus tree`: Path from the place thats the current focus to the UI Root. Example TODO + +## Zed UI + +- `Window`: A struct in zed representing a zed window in your desktop environment (see image below). There can be multiple if you have multiple zed instances open. Mostly passed around for rendering. +- `Modal`: A UI element that floats on top of the rest of the UI +- `Picker`: A struct representing a list of items in floating on top of the UI (Modal). You can select an item and confirm. What happens on select or confirm is determined by the picker's delegate. (The 'Model' in the image below is a picker.) +- `PickerDelegate`: A trait used to specialize behavior for a `Picker`. The `Picker` stores the `PickerDelegate` in the field delegate. +- `Center`: The middle of the zed window, the center is split into multiple `Pane`s. In the codebase this is a field on the `Workspace` struct. (see image below). +- `Pane`: An area in the `Center` where we can place items, such as an editor, multi-buffer or terminal (see image below). +- `Panel`: An `Entity` implementing the `Panel` trait. These can be placed in a `Dock`. In the image below we see the: `ProjectPanel` in the left dock, the `DebugPanel` in the bottom dock, and `AgentPanel` in the right dock. Note `Editor` does not implement `Panel` and hence is not a `Panel`. +- `Dock`: A UI element similar to a `Pane` which can be opened and hidden. There can be up to 3 docks open at a time, left right and below the center. A dock contains one or more `Panel`s not `Pane`s. (see image). + image + +- `Project`: One or more `Worktree`s +- `Worktree`: Represents either local or remote files. + image + +- [Multibuffer](https://zed.dev/docs/multibuffers): A list of Editors, a multi-buffer allows editing multiple files simultaneously. A multi-buffer opens when an operation in Zed returns multiple locations, examples: _search_ or _go to definition_. See project search in the image below. + +image + +## Editor + +- `Editor`: _The_ text editor, nearly everything in zed is an `Editor`, even single line inputs. Each pane in the image above contains one or more `Editor` instances. +- `Workspace`: The root of the window +- `Entry`: A file, dir, pending dir or unloaded dir. +- `Buffer`: The in-memory representation of a 'file' together with relevant data such as syntax trees, git status and diagnostics. +- `pending selection`: You have mouse down and you're dragging but you have not yet released. + +## Collab + +- `Collab session`: Multiple users working in a shared `Project` +- `Upstream client`: The zed client which has shared their workspace +- `Downstream client`: The zed client joining a shared workspace + +## Debugger + +- `DapStore`: Is an entity that manages debugger sessions +- `debugger::Session`: Is an entity that manages the lifecycle of a debug session and communication with DAPS +- `BreakpointStore`: Is an entity that manages breakpoints states in local and remote instances of Zed +- `DebugSession`: Manages a debug session's UI and running state +- `RunningState`: Directily manages all the views of a debug session +- `VariableList`: The variable and watch list view of a debug session +- `Console`: TODO +- `Terminal`: TODO +- `BreakpointList`: TODO From ad3ddd381da7899174fe5bad064ef2c3b70f9c4f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 2 Sep 2025 12:18:49 -0400 Subject: [PATCH 503/744] Revert "gpui: Do not render ligatures between different styled text runs (#37175) (#37382) This reverts commit 62083fe7963dd5bed4579bb12abac1b7800cdbaa. We're reverting this as it causes layout shift when typing/selecting with ligatures: https://github.com/user-attachments/assets/80b78909-62f5-404f-8cca-3535c5594ceb Release Notes: - Reverted #37175 --- crates/gpui/src/platform/mac/text_system.rs | 170 ++++---------------- crates/gpui/src/text_system.rs | 102 +++++------- crates/gpui/src/text_system/line_layout.rs | 15 +- 3 files changed, 82 insertions(+), 205 deletions(-) diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index ba7017b58f76f028a1c5c80959e9359bc379c0cb..72a0f2e565d9937e3aaf4082b663c3e2ae6ac91d 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -43,7 +43,7 @@ use pathfinder_geometry::{ vector::{Vector2F, Vector2I}, }; use smallvec::SmallVec; -use std::{borrow::Cow, char, convert::TryFrom, sync::Arc}; +use std::{borrow::Cow, char, cmp, convert::TryFrom, sync::Arc}; use super::open_type::apply_features_and_fallbacks; @@ -67,7 +67,6 @@ struct MacTextSystemState { font_ids_by_postscript_name: HashMap, font_ids_by_font_key: HashMap>, postscript_names_by_font_id: HashMap, - zwnjs_scratch_space: Vec<(usize, usize)>, } impl MacTextSystem { @@ -80,7 +79,6 @@ impl MacTextSystem { font_ids_by_postscript_name: HashMap::default(), font_ids_by_font_key: HashMap::default(), postscript_names_by_font_id: HashMap::default(), - zwnjs_scratch_space: Vec::new(), })) } } @@ -426,41 +424,29 @@ impl MacTextSystemState { } fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { - const ZWNJ: char = '\u{200C}'; - const ZWNJ_STR: &str = "\u{200C}"; - const ZWNJ_SIZE_16: usize = ZWNJ.len_utf16(); - - self.zwnjs_scratch_space.clear(); // Construct the attributed string, converting UTF8 ranges to UTF16 ranges. let mut string = CFMutableAttributedString::new(); - { - let mut ix_converter = StringIndexConverter::new(&text); - let mut last_font_run = None; + string.replace_str(&CFString::new(text), CFRange::init(0, 0)); + let utf16_line_len = string.char_len() as usize; + + let mut ix_converter = StringIndexConverter::new(text); for run in font_runs { - let text = &text[ix_converter.utf8_ix..][..run.len]; - // if the fonts are the same, we need to disconnect the text with a ZWNJ - // to prevent core text from forming ligatures between them - let needs_zwnj = last_font_run.replace(run.font_id) == Some(run.font_id); - - let n_zwnjs = self.zwnjs_scratch_space.len(); - let utf16_start = ix_converter.utf16_ix + n_zwnjs * ZWNJ_SIZE_16; - ix_converter.advance_to_utf8_ix(ix_converter.utf8_ix + run.len); - - string.replace_str(&CFString::new(text), CFRange::init(utf16_start as isize, 0)); - if needs_zwnj { - let zwnjs_pos = string.char_len(); - self.zwnjs_scratch_space.push((n_zwnjs, zwnjs_pos as usize)); - string.replace_str( - &CFString::from_static_string(ZWNJ_STR), - CFRange::init(zwnjs_pos, 0), - ); + let utf8_end = ix_converter.utf8_ix + run.len; + let utf16_start = ix_converter.utf16_ix; + + if utf16_start >= utf16_line_len { + break; } - let utf16_end = string.char_len() as usize; + + ix_converter.advance_to_utf8_ix(utf8_end); + let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len); let cf_range = CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize); - let font = &self.fonts[run.font_id.0]; + + let font: &FontKitFont = &self.fonts[run.font_id.0]; + unsafe { string.set_attribute( cf_range, @@ -468,12 +454,17 @@ impl MacTextSystemState { &font.native_font().clone_with_font_size(font_size.into()), ); } + + if utf16_end == utf16_line_len { + break; + } } } + // Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets. let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef()); let glyph_runs = line.glyph_runs(); - let mut runs = >::with_capacity(glyph_runs.len() as usize); + let mut runs = Vec::with_capacity(glyph_runs.len() as usize); let mut ix_converter = StringIndexConverter::new(text); for run in glyph_runs.into_iter() { let attributes = run.attributes().unwrap(); @@ -485,44 +476,28 @@ impl MacTextSystemState { }; let font_id = self.id_for_native_font(font); - let mut glyphs = match runs.last_mut() { - Some(run) if run.font_id == font_id => &mut run.glyphs, - _ => { - runs.push(ShapedRun { - font_id, - glyphs: Vec::with_capacity(run.glyph_count().try_into().unwrap_or(0)), - }); - &mut runs.last_mut().unwrap().glyphs - } - }; - for ((&glyph_id, position), &glyph_utf16_ix) in run + let mut glyphs = Vec::with_capacity(run.glyph_count().try_into().unwrap_or(0)); + for ((glyph_id, position), glyph_utf16_ix) in run .glyphs() .iter() .zip(run.positions().iter()) .zip(run.string_indices().iter()) { - let mut glyph_utf16_ix = usize::try_from(glyph_utf16_ix).unwrap(); - let r = self - .zwnjs_scratch_space - .binary_search_by(|&(_, it)| it.cmp(&glyph_utf16_ix)); - match r { - // this glyph is a ZWNJ, skip it - Ok(_) => continue, - // adjust the index to account for the ZWNJs we've inserted - Err(idx) => glyph_utf16_ix -= idx * ZWNJ_SIZE_16, - } + let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap(); if ix_converter.utf16_ix > glyph_utf16_ix { // We cannot reuse current index converter, as it can only seek forward. Restart the search. ix_converter = StringIndexConverter::new(text); } ix_converter.advance_to_utf16_ix(glyph_utf16_ix); glyphs.push(ShapedGlyph { - id: GlyphId(glyph_id as u32), + id: GlyphId(*glyph_id as u32), position: point(position.x as f32, position.y as f32).map(px), index: ix_converter.utf8_ix, is_emoji: self.is_emoji(font_id), }); } + + runs.push(ShapedRun { font_id, glyphs }); } let typographic_bounds = line.get_typographic_bounds(); LineLayout { @@ -721,93 +696,4 @@ mod tests { // There's no glyph for \u{feff} assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b } - - #[test] - fn test_layout_line_zwnj_insertion() { - let fonts = MacTextSystem::new(); - let font_id = fonts.font_id(&font("Helvetica")).unwrap(); - - let text = "hello world"; - let font_runs = &[ - FontRun { font_id, len: 5 }, // "hello" - FontRun { font_id, len: 6 }, // " world" - ]; - - let layout = fonts.layout_line(text, px(16.), font_runs); - assert_eq!(layout.len, text.len()); - - for run in &layout.runs { - for glyph in &run.glyphs { - assert!( - glyph.index < text.len(), - "Glyph index {} is out of bounds for text length {}", - glyph.index, - text.len() - ); - } - } - - // Test with different font runs - should not insert ZWNJ - let font_id2 = fonts.font_id(&font("Times")).unwrap_or(font_id); - let font_runs_different = &[ - FontRun { font_id, len: 5 }, // "hello" - // " world" - FontRun { - font_id: font_id2, - len: 6, - }, - ]; - - let layout2 = fonts.layout_line(text, px(16.), font_runs_different); - assert_eq!(layout2.len, text.len()); - - for run in &layout2.runs { - for glyph in &run.glyphs { - assert!( - glyph.index < text.len(), - "Glyph index {} is out of bounds for text length {}", - glyph.index, - text.len() - ); - } - } - } - - #[test] - fn test_layout_line_zwnj_edge_cases() { - let fonts = MacTextSystem::new(); - let font_id = fonts.font_id(&font("Helvetica")).unwrap(); - - let text = "hello"; - let font_runs = &[FontRun { font_id, len: 5 }]; - let layout = fonts.layout_line(text, px(16.), font_runs); - assert_eq!(layout.len, text.len()); - - let text = "abc"; - let font_runs = &[ - FontRun { font_id, len: 1 }, // "a" - FontRun { font_id, len: 1 }, // "b" - FontRun { font_id, len: 1 }, // "c" - ]; - let layout = fonts.layout_line(text, px(16.), font_runs); - assert_eq!(layout.len, text.len()); - - for run in &layout.runs { - for glyph in &run.glyphs { - assert!( - glyph.index < text.len(), - "Glyph index {} is out of bounds for text length {}", - glyph.index, - text.len() - ); - } - } - - // Test with empty text - let text = ""; - let font_runs = &[]; - let layout = fonts.layout_line(text, px(16.), font_runs); - assert_eq!(layout.len, 0); - assert!(layout.runs.is_empty()); - } } diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index be34b9e2aac055bd9f17c2f69b3c72d24e392593..53991089da94c58d0035bff0d607ad3ab57a69bd 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -413,10 +413,9 @@ impl WindowTextSystem { let mut wrapped_lines = 0; let mut process_line = |line_text: SharedString| { - font_runs.clear(); let line_end = line_start + line_text.len(); - let mut last_font: Option = None; + let mut last_font: Option = None; let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); let mut run_start = line_start; while run_start < line_end { @@ -426,14 +425,23 @@ impl WindowTextSystem { let run_len_within_line = cmp::min(line_end, run_start + run.len) - run_start; - let decoration_changed = if let Some(last_run) = decoration_runs.last_mut() - && last_run.color == run.color - && last_run.underline == run.underline - && last_run.strikethrough == run.strikethrough - && last_run.background_color == run.background_color - { - last_run.len += run_len_within_line as u32; - false + if last_font == Some(run.font.clone()) { + font_runs.last_mut().unwrap().len += run_len_within_line; + } else { + last_font = Some(run.font.clone()); + font_runs.push(FontRun { + len: run_len_within_line, + font_id: self.resolve_font(&run.font), + }); + } + + if decoration_runs.last().is_some_and(|last_run| { + last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + && last_run.background_color == run.background_color + }) { + decoration_runs.last_mut().unwrap().len += run_len_within_line as u32; } else { decoration_runs.push(DecorationRun { len: run_len_within_line as u32, @@ -442,21 +450,6 @@ impl WindowTextSystem { underline: run.underline, strikethrough: run.strikethrough, }); - true - }; - - if let Some(font_run) = font_runs.last_mut() - && Some(font_run.font_id) == last_font - && !decoration_changed - { - font_run.len += run_len_within_line; - } else { - let font_id = self.resolve_font(&run.font); - last_font = Some(font_id); - font_runs.push(FontRun { - len: run_len_within_line, - font_id, - }); } if run_len_within_line == run.len { @@ -491,6 +484,8 @@ impl WindowTextSystem { runs.next(); } } + + font_runs.clear(); }; let mut split_lines = text.split('\n'); @@ -524,54 +519,37 @@ impl WindowTextSystem { /// Subsets of the line can be styled independently with the `runs` parameter. /// Generally, you should prefer to use `TextLayout::shape_line` instead, which /// can be painted directly. - pub fn layout_line( + pub fn layout_line( &self, - text: &str, + text: Text, font_size: Pixels, runs: &[TextRun], force_width: Option, - ) -> Arc { - let mut last_run = None::<&TextRun>; - let mut last_font: Option = None; + ) -> Arc + where + Text: AsRef, + SharedString: From, + { let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); - font_runs.clear(); - for run in runs.iter() { - let decoration_changed = if let Some(last_run) = last_run - && last_run.color == run.color - && last_run.underline == run.underline - && last_run.strikethrough == run.strikethrough - // we do not consider differing background color relevant, as it does not affect glyphs - // && last_run.background_color == run.background_color - { - false - } else { - last_run = Some(run); - true - }; - - if let Some(font_run) = font_runs.last_mut() - && Some(font_run.font_id) == last_font - && !decoration_changed + let font_id = self.resolve_font(&run.font); + if let Some(last_run) = font_runs.last_mut() + && last_run.font_id == font_id { - font_run.len += run.len; - } else { - let font_id = self.resolve_font(&run.font); - last_font = Some(font_id); - font_runs.push(FontRun { - len: run.len, - font_id, - }); + last_run.len += run.len; + continue; } + font_runs.push(FontRun { + len: run.len, + font_id, + }); } - let layout = self.line_layout_cache.layout_line( - &SharedString::new(text), - font_size, - &font_runs, - force_width, - ); + let layout = + self.line_layout_cache + .layout_line_internal(text, font_size, &font_runs, force_width); + font_runs.clear(); self.font_runs_pool.lock().push(font_runs); layout diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index 4ac1d258970802ed1c4fe86bd98f2971b78fbc04..43694702a82566b8f84199dcfc4ff996da93588e 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -501,7 +501,7 @@ impl LineLayoutCache { } else { drop(current_frame); let text = SharedString::from(text); - let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs, None); + let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs); let wrap_boundaries = if let Some(wrap_width) = wrap_width { unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width, max_lines) } else { @@ -535,6 +535,19 @@ impl LineLayoutCache { text: Text, font_size: Pixels, runs: &[FontRun], + ) -> Arc + where + Text: AsRef, + SharedString: From, + { + self.layout_line_internal(text, font_size, runs, None) + } + + pub fn layout_line_internal( + &self, + text: Text, + font_size: Pixels, + runs: &[FontRun], force_width: Option, ) -> Arc where From a02616374604492fe884ae456139adfab7a44865 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:26:56 -0300 Subject: [PATCH 504/744] inline assistant: Adjust completion menu item font size (#37375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now the @ completion menu items font size respect/match the buffer's font size, as opposed to being rendered a bit bigger. | Before | After | |--------|--------| | Screenshot 2025-09-02 at 11 
09@2x | Screenshot 2025-09-02 at 11  09
2@2x | Release Notes: - inline assistant: Improved @-mention menu item font size, better matching the buffer's font size. --- crates/agent_ui/src/inline_prompt_editor.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 3abefac8e8964ffdddc1397132541d0056f33ea8..d268c2f21154ee0dae5eeab1e35a900f6b773d69 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -93,8 +93,8 @@ impl Render for PromptEditor { }; let bottom_padding = match &self.mode { - PromptEditorMode::Buffer { .. } => Pixels::from(0.), - PromptEditorMode::Terminal { .. } => Pixels::from(8.0), + PromptEditorMode::Buffer { .. } => rems_from_px(2.0), + PromptEditorMode::Terminal { .. } => rems_from_px(8.0), }; buttons.extend(self.render_buttons(window, cx)); @@ -762,20 +762,22 @@ impl PromptEditor { ) } - fn render_editor(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { - let font_size = TextSize::Default.rems(cx); - let line_height = font_size.to_pixels(window.rem_size()) * 1.3; + fn render_editor(&mut self, _window: &mut Window, cx: &mut Context) -> AnyElement { + let colors = cx.theme().colors(); div() .key_context("InlineAssistEditor") .size_full() .p_2() .pl_1() - .bg(cx.theme().colors().editor_background) + .bg(colors.editor_background) .child({ let settings = ThemeSettings::get_global(cx); + let font_size = settings.buffer_font_size(cx); + let line_height = font_size * 1.2; + let text_style = TextStyle { - color: cx.theme().colors().editor_foreground, + color: colors.editor_foreground, font_family: settings.buffer_font.family.clone(), font_features: settings.buffer_font.features.clone(), font_size: font_size.into(), @@ -786,7 +788,7 @@ impl PromptEditor { EditorElement::new( &self.editor, EditorStyle { - background: cx.theme().colors().editor_background, + background: colors.editor_background, local_player: cx.theme().players().local(), text: text_style, ..Default::default() From d2318be8d9600ab3f232f3dc582516bf35cfc747 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:27:06 -0300 Subject: [PATCH 505/744] terminal view: Hide inline assist button if AI is disabled (#37378) Closes https://github.com/zed-industries/zed/issues/37372 Release Notes: - Fix the terminal inline assistant button showing despite `disable_ai` being turned on. --------- Co-authored-by: MrSubidubi --- crates/agent_ui/src/inline_assistant.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 13f1234b4de0aec6f7540c0f34950b8b5c3ef585..7beb2f8ff80627399e7ebe774d8849d038621aef 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -144,7 +144,8 @@ impl InlineAssistant { let Some(terminal_panel) = workspace.read(cx).panel::(cx) else { return; }; - let enabled = AgentSettings::get_global(cx).enabled; + let enabled = !DisableAiSettings::get_global(cx).disable_ai + && AgentSettings::get_global(cx).enabled; terminal_panel.update(cx, |terminal_panel, cx| { terminal_panel.set_assistant_enabled(enabled, cx) }); From ac8c653ae6d784bc2395f8ec3a0674fba5d770a8 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Tue, 2 Sep 2025 12:28:07 -0400 Subject: [PATCH 506/744] Fix race condition between feature flag and deserialization (#37381) Right now if you open Zed, and we deserialize an agent that's behind a feature flag (e.g. CC), we don't restore it because the feature flag check hasn't happened yet at the time we're deserializing (due to auth not having finished yet). This is a simple fix: assume that if you had serialized it in the first place, you must have had the feature flag enabled, so go ahead and reopen it for you. Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index fac880b783271ffd8c9524464a8f0a178f276895..c80ddb5c92ef4b670123433effe297da4cbbda23 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1911,13 +1911,17 @@ impl AgentPanel { AgentType::Gemini => { self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx) } - AgentType::ClaudeCode => self.external_thread( - Some(crate::ExternalAgent::ClaudeCode), - None, - None, - window, - cx, - ), + AgentType::ClaudeCode => { + self.selected_agent = AgentType::ClaudeCode; + self.serialize(cx); + self.external_thread( + Some(crate::ExternalAgent::ClaudeCode), + None, + None, + window, + cx, + ) + } AgentType::Custom { name, command } => self.external_thread( Some(crate::ExternalAgent::Custom { name, command }), None, From dfa066dfe8fc63fe9070756a1b3c02442835fe0c Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 2 Sep 2025 13:39:55 -0300 Subject: [PATCH 507/744] acp: Display slash command hints (#37376) Displays the slash command's argument hint while it hasn't been provided: https://github.com/user-attachments/assets/f3bb148c-247d-43bc-810d-92055a313514 Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner --- .../agent_ui/src/acp/completion_provider.rs | 10 +- crates/agent_ui/src/acp/message_editor.rs | 123 ++++++++++++++++-- crates/project/src/project.rs | 1 - 3 files changed, 116 insertions(+), 18 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 59106c3795aa14794e1fca9ee32049e0cff1314f..dc38c65868385e1e5ee913dd76160c7bdebbd0ad 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1005,14 +1005,14 @@ impl ContextCompletion { } #[derive(Debug, Default, PartialEq)] -struct SlashCommandCompletion { - source_range: Range, - command: Option, - argument: Option, +pub struct SlashCommandCompletion { + pub source_range: Range, + pub command: Option, + pub argument: Option, } impl SlashCommandCompletion { - fn try_parse(line: &str, offset_to_line: usize) -> Option { + pub fn try_parse(line: &str, offset_to_line: usize) -> Option { // If we decide to support commands that are not at the beginning of the prompt, we can remove this check if !line.starts_with('/') || offset_to_line != 0 { return None; diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index b51bc2e0a3d10a3647fafd816684c7382c5dde36..3350374aa529a3feb2473679860a9614bb413854 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1,5 +1,5 @@ use crate::{ - acp::completion_provider::ContextPickerCompletionProvider, + acp::completion_provider::{ContextPickerCompletionProvider, SlashCommandCompletion}, context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content}, }; use acp_thread::{MentionUri, selection_name}; @@ -11,10 +11,10 @@ use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, - EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, - ToOffset, + EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId, + MultiBuffer, ToOffset, actions::Paste, - display_map::{Crease, CreaseId, FoldId}, + display_map::{Crease, CreaseId, FoldId, Inlay}, }; use futures::{ FutureExt as _, @@ -25,10 +25,12 @@ use gpui::{ EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, Subscription, Task, TextStyle, WeakEntity, pulsating_between, }; -use language::{Buffer, Language}; +use language::{Buffer, Language, language_settings::InlayHintKind}; use language_model::LanguageModelImage; use postage::stream::Stream as _; -use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree}; +use project::{ + CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree, +}; use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::Settings; @@ -62,6 +64,7 @@ pub struct MessageEditor { history_store: Entity, prompt_store: Option>, prompt_capabilities: Rc>, + available_commands: Rc>>, _subscriptions: Vec, _parse_slash_command_task: Task<()>, } @@ -76,6 +79,8 @@ pub enum MessageEditorEvent { impl EventEmitter for MessageEditor {} +const COMMAND_HINT_INLAY_ID: usize = 0; + impl MessageEditor { pub fn new( workspace: WeakEntity, @@ -102,7 +107,7 @@ impl MessageEditor { history_store.clone(), prompt_store.clone(), prompt_capabilities.clone(), - available_commands, + available_commands.clone(), )); let mention_set = MentionSet::default(); let editor = cx.new(|cx| { @@ -133,12 +138,33 @@ impl MessageEditor { }) .detach(); + let mut has_hint = false; let mut subscriptions = Vec::new(); + subscriptions.push(cx.subscribe_in(&editor, window, { move |this, editor, event, window, cx| { if let EditorEvent::Edited { .. } = event { - let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx)); + let snapshot = editor.update(cx, |editor, cx| { + let new_hints = this + .command_hint(editor.buffer(), cx) + .into_iter() + .collect::>(); + let has_new_hint = !new_hints.is_empty(); + editor.splice_inlays( + if has_hint { + &[InlayId::Hint(COMMAND_HINT_INLAY_ID)] + } else { + &[] + }, + new_hints, + cx, + ); + has_hint = has_new_hint; + + editor.snapshot(window, cx) + }); this.mention_set.remove_invalid(snapshot); + cx.notify(); } } @@ -152,11 +178,55 @@ impl MessageEditor { history_store, prompt_store, prompt_capabilities, + available_commands, _subscriptions: subscriptions, _parse_slash_command_task: Task::ready(()), } } + fn command_hint(&self, buffer: &Entity, cx: &App) -> Option { + let available_commands = self.available_commands.borrow(); + if available_commands.is_empty() { + return None; + } + + let snapshot = buffer.read(cx).snapshot(cx); + let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?; + if parsed_command.argument.is_some() { + return None; + } + + let command_name = parsed_command.command?; + let available_command = available_commands + .iter() + .find(|command| command.name == command_name)?; + + let acp::AvailableCommandInput::Unstructured { mut hint } = + available_command.input.clone()?; + + let mut hint_pos = parsed_command.source_range.end + 1; + if hint_pos > snapshot.len() { + hint_pos = snapshot.len(); + hint.insert(0, ' '); + } + + let hint_pos = snapshot.anchor_after(hint_pos); + + Some(Inlay::hint( + COMMAND_HINT_INLAY_ID, + hint_pos, + &InlayHint { + position: hint_pos.text_anchor, + label: InlayHintLabel::String(hint), + kind: Some(InlayHintKind::Parameter), + padding_left: false, + padding_right: false, + tooltip: None, + resolve_state: project::ResolveState::Resolved, + }, + )) + } + pub fn insert_thread_summary( &mut self, thread: agent2::DbThreadMetadata, @@ -1184,6 +1254,7 @@ impl Render for MessageEditor { local_player: cx.theme().players().local(), text: text_style, syntax: cx.theme().syntax().clone(), + inlay_hints_style: editor::make_inlay_hints_style(cx), ..Default::default() }, ) @@ -1639,7 +1710,7 @@ mod tests { name: "say-hello".to_string(), description: "Say hello to whoever you want".to_string(), input: Some(acp::AvailableCommandInput::Unstructured { - hint: "Who do you want to say hello to?".to_string(), + hint: "".to_string(), }), }, ])); @@ -1714,7 +1785,7 @@ mod tests { cx.run_until_parked(); editor.update_in(&mut cx, |editor, window, cx| { - assert_eq!(editor.text(cx), "/quick-math "); + assert_eq!(editor.display_text(cx), "/quick-math "); assert!(!editor.has_visible_completions_menu()); editor.set_text("", window, cx); }); @@ -1722,7 +1793,7 @@ mod tests { cx.simulate_input("/say"); editor.update_in(&mut cx, |editor, _window, cx| { - assert_eq!(editor.text(cx), "/say"); + assert_eq!(editor.display_text(cx), "/say"); assert!(editor.has_visible_completions_menu()); assert_eq!( @@ -1740,6 +1811,7 @@ mod tests { editor.update_in(&mut cx, |editor, _window, cx| { assert_eq!(editor.text(cx), "/say-hello "); + assert_eq!(editor.display_text(cx), "/say-hello "); assert!(editor.has_visible_completions_menu()); assert_eq!( @@ -1757,8 +1829,35 @@ mod tests { cx.run_until_parked(); - editor.update_in(&mut cx, |editor, _window, cx| { + editor.update_in(&mut cx, |editor, window, cx| { assert_eq!(editor.text(cx), "/say-hello GPT5"); + assert_eq!(editor.display_text(cx), "/say-hello GPT5"); + assert!(!editor.has_visible_completions_menu()); + + // Delete argument + for _ in 0..4 { + editor.backspace(&editor::actions::Backspace, window, cx); + } + }); + + cx.run_until_parked(); + + editor.update_in(&mut cx, |editor, window, cx| { + assert_eq!(editor.text(cx), "/say-hello "); + // Hint is visible because argument was deleted + assert_eq!(editor.display_text(cx), "/say-hello "); + + // Delete last command letter + editor.backspace(&editor::actions::Backspace, window, cx); + editor.backspace(&editor::actions::Backspace, window, cx); + }); + + cx.run_until_parked(); + + editor.update_in(&mut cx, |editor, _window, cx| { + // Hint goes away once command no longer matches an available one + assert_eq!(editor.text(cx), "/say-hell"); + assert_eq!(editor.display_text(cx), "/say-hell"); assert!(!editor.has_visible_completions_menu()); }); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 557367edf522a103ee1a8b55f5264be561d1698e..229249d48c5ab370c4b354cda7cbf9312790759d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -666,7 +666,6 @@ pub enum ResolveState { CanResolve(LanguageServerId, Option), Resolving, } - impl InlayHint { pub fn text(&self) -> Rope { match &self.label { From c01f12b15d264d1836db276a6ab7c15b75c921de Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Tue, 2 Sep 2025 11:23:35 -0600 Subject: [PATCH 508/744] zeta: Small refactoring in license detection check - rfind instead of iterated ends_with (#37329) Release Notes: - N/A --- crates/zeta/src/license_detection.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/crates/zeta/src/license_detection.rs b/crates/zeta/src/license_detection.rs index 2939f8a0c491422099e14ae7cc76997a9031e7a0..5f207a44e8bd2028e6a2b416e978f101cfe5bd57 100644 --- a/crates/zeta/src/license_detection.rs +++ b/crates/zeta/src/license_detection.rs @@ -202,20 +202,13 @@ fn check_pattern(pattern: &[PatternPart], input: &str) -> bool { match_any_chars.end += part.match_any_chars.end; continue; } - let mut matched = false; - for skip_count in match_any_chars.start..=match_any_chars.end { - let end_ix = input_ix.saturating_sub(skip_count); - if end_ix < part.text.len() { - break; - } - if input[..end_ix].ends_with(&part.text) { - matched = true; - input_ix = end_ix - part.text.len(); - match_any_chars = part.match_any_chars.clone(); - break; - } - } - if !matched && !part.optional { + let search_range_start = input_ix.saturating_sub(match_any_chars.end + part.text.len()); + let search_range_end = input_ix.saturating_sub(match_any_chars.start); + let found_ix = &input[search_range_start..search_range_end].rfind(&part.text); + if let Some(found_ix) = found_ix { + input_ix = search_range_start + found_ix; + match_any_chars = part.match_any_chars.clone(); + } else if !part.optional { log::trace!( "Failed to match pattern `...{}` against input `...{}`", &part.text[part.text.len().saturating_sub(128)..], From 5ac6ae501f21e6700b062d1513d0e59e1a29079e Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 2 Sep 2025 13:57:48 -0400 Subject: [PATCH 509/744] docs: Link glossary (#37387) Follow-up to: https://github.com/zed-industries/zed/pull/37360 Add glossary.md to SUMMARY.md so it's linked to the public documentation. Release Notes: - N/A --- docs/src/SUMMARY.md | 1 + docs/src/development/glossary.md | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 9d07881914d1f73a7333d3dc67ad1d3ca6731bc5..a470018b2c9e111f11144149855b833876c511d1 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -157,5 +157,6 @@ - [FreeBSD](./development/freebsd.md) - [Local Collaboration](./development/local-collaboration.md) - [Using Debuggers](./development/debuggers.md) + - [Glossary](./development/glossary.md) - [Release Process](./development/releases.md) - [Debugging Crashes](./development/debugging-crashes.md) diff --git a/docs/src/development/glossary.md b/docs/src/development/glossary.md index 08bc9f0e91b7e4f054ef9f10892b9be9feffcfce..d0ae12fe03a9955667a69eeb6e270981421b6c02 100644 --- a/docs/src/development/glossary.md +++ b/docs/src/development/glossary.md @@ -1,9 +1,17 @@ -These are some terms and structures frequently used throughout the zed codebase. This is a best effort list. +# Zed Development: Glossary + +These are some terms and structures frequently used throughout the zed codebase. + +This is a best effort list and a work in progress. + + ## Naming conventions From 4c411b9fc825a6a6d5eb6267b6379d6d613a48c9 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:59:07 +0530 Subject: [PATCH 510/744] language_models: Make `JsonSchemaSubset` the default `tool_input_format` for the OpenAI-compatible provider (#34921) Closes #30188 Closes #34911 Closes #34906 Many OpenAI-compatible providers do not automatically filter the tool schema to comply with the underlying model's requirements; they simply proxy the request. This creates issues, as models like **Gemini**, **Grok**, and **Claude** (when accessed via LiteLLM on Bedrock) are incompatible with Zed's default tool schema. This PR addresses this by defaulting to a more compatible schema subset instead of the full schema. ### Why this approach? * **Avoids Poor User Experience:** One alternative was to add an option for users to manually set the JSON schema for models that return a `400 Bad Request` due to an invalid tool schema. This was discarded as it provides a poor user experience. * **Simplifies Complex Logic:** Another option was to filter the schema based on the model ID. However, as demonstrated in the attached issues, this is unreliable. For instance, `claude-4-sonnet` fails when proxied through LiteLLM on Bedrock. Reliably determining behavior would require a non-trivial implementation to manage provider-and-model combinations. * **Better Default Behavior:** The current approach ensures that tool usage works out-of-the-box for the majority of cases by default, providing the most robust and user-friendly solution. Release Notes: - Improved tool compatibility with OpenAI API-compatible providers Signed-off-by: Umesh Yadav Co-authored-by: Peter Tripp --- crates/language_models/src/provider/open_ai_compatible.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index 55df534cc9416149ca5574e16f0230f1c8160220..789eb00a5746c729103f77a1e92d0e58fc4c1ab0 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -9,7 +9,7 @@ use language_model::{ AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, - LanguageModelToolChoice, RateLimiter, + LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter, }; use menu; use open_ai::{ResponseStreamEvent, stream_completion}; @@ -322,6 +322,10 @@ impl LanguageModel for OpenAiCompatibleLanguageModel { self.model.capabilities.tools } + fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { + LanguageModelToolSchemaFormat::JsonSchemaSubset + } + fn supports_images(&self) -> bool { self.model.capabilities.images } From 88a79750ccf26691aa666b2369679ed9ce198d56 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 2 Sep 2025 13:53:53 -0600 Subject: [PATCH 511/744] Disable external agents over collab (#37377) Release Notes: - Disable UI to boot external agents in collab projects (as they don't work) --- Cargo.toml | 3 +++ crates/agent_ui/src/acp/thread_view.rs | 9 ++++++- crates/agent_ui/src/agent_panel.rs | 35 ++++++++++++++++++-------- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a96dc5d4d57ac5e5be60ace099e42b6a8123c42e..2c8143756b33b35f892076c3e71d6bf458a47ea0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -848,6 +848,9 @@ too_many_arguments = "allow" # We often have large enum variants yet we rarely actually bother with splitting them up. large_enum_variant = "allow" +# Boolean expressions can be hard to read, requiring only the minimal form gets in the way +nonminimal_bol = "allow" + [workspace.metadata.cargo-machete] ignored = [ "bindgen", diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index a9421723d125d27f3eb13c43fb17936f9078dae8..c60710eaeeff29040ea9e3d98b002b865c799857 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -8,7 +8,7 @@ use action_log::ActionLog; use agent_client_protocol::{self as acp, PromptCapabilities}; use agent_servers::{AgentServer, AgentServerDelegate, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; -use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore}; +use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer}; use anyhow::{Context as _, Result, anyhow, bail}; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; @@ -418,6 +418,13 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) -> ThreadState { + if project.read(cx).is_via_collab() + && agent.clone().downcast::().is_none() + { + return ThreadState::LoadError(LoadError::Other( + "External agents are not yet supported for remote projects.".into(), + )); + } let root_dir = project .read(cx) .visible_worktrees(cx) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index c80ddb5c92ef4b670123433effe297da4cbbda23..3da63c281ef72765479d23cc508ef4ca50ff927c 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1091,6 +1091,7 @@ impl AgentPanel { let workspace = self.workspace.clone(); let project = self.project.clone(); let fs = self.fs.clone(); + let is_via_collab = self.project.read(cx).is_via_collab(); const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent"; @@ -1122,17 +1123,21 @@ impl AgentPanel { agent } None => { - cx.background_spawn(async move { - KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY) - }) - .await - .log_err() - .flatten() - .and_then(|value| { - serde_json::from_str::(&value).log_err() - }) - .unwrap_or_default() - .agent + if is_via_collab { + ExternalAgent::NativeAgent + } else { + cx.background_spawn(async move { + KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY) + }) + .await + .log_err() + .flatten() + .and_then(|value| { + serde_json::from_str::(&value).log_err() + }) + .unwrap_or_default() + .agent + } } }; @@ -2527,6 +2532,11 @@ impl AgentPanel { .with_handle(self.new_thread_menu_handle.clone()) .menu({ let workspace = self.workspace.clone(); + let is_via_collab = workspace + .update(cx, |workspace, cx| { + workspace.project().read(cx).is_via_collab() + }) + .unwrap_or_default(); move |window, cx| { telemetry::event!("New Thread Clicked"); @@ -2617,6 +2627,7 @@ impl AgentPanel { ContextMenuEntry::new("New Gemini CLI Thread") .icon(IconName::AiGemini) .icon_color(Color::Muted) + .disabled(is_via_collab) .handler({ let workspace = workspace.clone(); move |window, cx| { @@ -2643,6 +2654,7 @@ impl AgentPanel { menu.item( ContextMenuEntry::new("New Claude Code Thread") .icon(IconName::AiClaude) + .disabled(is_via_collab) .icon_color(Color::Muted) .handler({ let workspace = workspace.clone(); @@ -2675,6 +2687,7 @@ impl AgentPanel { ContextMenuEntry::new(format!("New {} Thread", agent_name)) .icon(IconName::Terminal) .icon_color(Color::Muted) + .disabled(is_via_collab) .handler({ let workspace = workspace.clone(); let agent_name = agent_name.clone(); From 5e01fb8f1c47099149d0009997ad8026affba8d8 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Tue, 2 Sep 2025 16:39:24 -0400 Subject: [PATCH 512/744] Nice errors for unsupported ACP slash commands (#37393) If we get back slash-commands that aren't supported, tell the user that this is the problem. Release Notes: - Improve error messages for unsupported ACP slash-commands --------- Co-authored-by: Conrad Irwin --- crates/agent_ui/src/acp/entry_view_state.rs | 7 +- crates/agent_ui/src/acp/message_editor.rs | 198 +++++++++++++++++++- crates/agent_ui/src/acp/thread_view.rs | 57 +++++- 3 files changed, 251 insertions(+), 11 deletions(-) diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 4a91e93fa894953ef2f1f730ffa0d3896213e625..e60b923ca78c4613e9b8d8063a280f560d788d44 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -11,7 +11,7 @@ use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; use gpui::{ AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable, - ScrollHandle, TextStyleRefinement, WeakEntity, Window, + ScrollHandle, SharedString, TextStyleRefinement, WeakEntity, Window, }; use language::language_settings::SoftWrap; use project::Project; @@ -32,6 +32,7 @@ pub struct EntryViewState { entries: Vec, prompt_capabilities: Rc>, available_commands: Rc>>, + agent_name: SharedString, } impl EntryViewState { @@ -42,6 +43,7 @@ impl EntryViewState { prompt_store: Option>, prompt_capabilities: Rc>, available_commands: Rc>>, + agent_name: SharedString, ) -> Self { Self { workspace, @@ -51,6 +53,7 @@ impl EntryViewState { entries: Vec::new(), prompt_capabilities, available_commands, + agent_name, } } @@ -90,6 +93,7 @@ impl EntryViewState { self.prompt_store.clone(), self.prompt_capabilities.clone(), self.available_commands.clone(), + self.agent_name.clone(), "Edit message - @ to include context", editor::EditorMode::AutoHeight { min_lines: 1, @@ -476,6 +480,7 @@ mod tests { None, Default::default(), Default::default(), + "Test Agent".into(), ) }); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 3350374aa529a3feb2473679860a9614bb413854..ebe0e5c1c6dbcee71df010f4702e7567a8c26b2f 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -22,8 +22,8 @@ use futures::{ }; use gpui::{ Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId, - EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, Subscription, Task, - TextStyle, WeakEntity, pulsating_between, + EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString, + Subscription, Task, TextStyle, WeakEntity, pulsating_between, }; use language::{Buffer, Language, language_settings::InlayHintKind}; use language_model::LanguageModelImage; @@ -49,8 +49,8 @@ use theme::ThemeSettings; use ui::{ ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Element as _, FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, - LabelCommon, LabelSize, ParentElement, Render, SelectableButton, SharedString, Styled, - TextSize, TintColor, Toggleable, Window, div, h_flex, + LabelCommon, LabelSize, ParentElement, Render, SelectableButton, Styled, TextSize, TintColor, + Toggleable, Window, div, h_flex, }; use util::{ResultExt, debug_panic}; use workspace::{Workspace, notifications::NotifyResultExt as _}; @@ -65,6 +65,7 @@ pub struct MessageEditor { prompt_store: Option>, prompt_capabilities: Rc>, available_commands: Rc>>, + agent_name: SharedString, _subscriptions: Vec, _parse_slash_command_task: Task<()>, } @@ -89,6 +90,7 @@ impl MessageEditor { prompt_store: Option>, prompt_capabilities: Rc>, available_commands: Rc>>, + agent_name: SharedString, placeholder: impl Into>, mode: EditorMode, window: &mut Window, @@ -179,6 +181,7 @@ impl MessageEditor { prompt_store, prompt_capabilities, available_commands, + agent_name, _subscriptions: subscriptions, _parse_slash_command_task: Task::ready(()), } @@ -731,10 +734,52 @@ impl MessageEditor { }) } + fn validate_slash_commands( + text: &str, + available_commands: &[acp::AvailableCommand], + agent_name: &str, + ) -> Result<()> { + if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) { + if let Some(command_name) = parsed_command.command { + // Check if this command is in the list of available commands from the server + let is_supported = available_commands + .iter() + .any(|cmd| cmd.name == command_name); + + if !is_supported { + return Err(anyhow!( + "The /{} command is not supported by {}.\n\nAvailable commands: {}", + command_name, + agent_name, + if available_commands.is_empty() { + "none".to_string() + } else { + available_commands + .iter() + .map(|cmd| format!("/{}", cmd.name)) + .collect::>() + .join(", ") + } + )); + } + } + } + Ok(()) + } + pub fn contents( &self, cx: &mut Context, ) -> Task, Vec>)>> { + // Check for unsupported slash commands before spawning async task + let text = self.editor.read(cx).text(cx); + let available_commands = self.available_commands.borrow().clone(); + if let Err(err) = + Self::validate_slash_commands(&text, &available_commands, &self.agent_name) + { + return Task::ready(Err(err)); + } + let contents = self .mention_set .contents(&self.prompt_capabilities.get(), cx); @@ -744,7 +789,7 @@ impl MessageEditor { let contents = contents.await?; let mut all_tracked_buffers = Vec::new(); - editor.update(cx, |editor, cx| { + let result = editor.update(cx, |editor, cx| { let mut ix = 0; let mut chunks: Vec = Vec::new(); let text = editor.text(cx); @@ -837,9 +882,9 @@ impl MessageEditor { } } }); - - (chunks, all_tracked_buffers) - }) + Ok((chunks, all_tracked_buffers)) + })?; + result }) } @@ -1573,6 +1618,7 @@ mod tests { None, Default::default(), Default::default(), + "Test Agent".into(), "Test", EditorMode::AutoHeight { min_lines: 1, @@ -1650,6 +1696,140 @@ mod tests { pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]); } + #[gpui::test] + async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/test", + json!({ + ".zed": { + "tasks.json": r#"[{"label": "test", "command": "echo"}]"# + }, + "src": { + "main.rs": "fn main() {}", + }, + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; + let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); + // Start with no available commands - simulating Claude which doesn't support slash commands + let available_commands = Rc::new(RefCell::new(vec![])); + + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let workspace_handle = workspace.downgrade(); + let message_editor = workspace.update_in(cx, |_, window, cx| { + cx.new(|cx| { + MessageEditor::new( + workspace_handle.clone(), + project.clone(), + history_store.clone(), + None, + prompt_capabilities.clone(), + available_commands.clone(), + "Claude Code".into(), + "Test", + EditorMode::AutoHeight { + min_lines: 1, + max_lines: None, + }, + window, + cx, + ) + }) + }); + let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone()); + + // Test that slash commands fail when no available_commands are set (empty list means no commands supported) + editor.update_in(cx, |editor, window, cx| { + editor.set_text("/file test.txt", window, cx); + }); + + let contents_result = message_editor + .update(cx, |message_editor, cx| message_editor.contents(cx)) + .await; + + // Should fail because available_commands is empty (no commands supported) + assert!(contents_result.is_err()); + let error_message = contents_result.unwrap_err().to_string(); + assert!(error_message.contains("not supported by Claude Code")); + assert!(error_message.contains("Available commands: none")); + + // Now simulate Claude providing its list of available commands (which doesn't include file) + available_commands.replace(vec![acp::AvailableCommand { + name: "help".to_string(), + description: "Get help".to_string(), + input: None, + }]); + + // Test that unsupported slash commands trigger an error when we have a list of available commands + editor.update_in(cx, |editor, window, cx| { + editor.set_text("/file test.txt", window, cx); + }); + + let contents_result = message_editor + .update(cx, |message_editor, cx| message_editor.contents(cx)) + .await; + + assert!(contents_result.is_err()); + let error_message = contents_result.unwrap_err().to_string(); + assert!(error_message.contains("not supported by Claude Code")); + assert!(error_message.contains("/file")); + assert!(error_message.contains("Available commands: /help")); + + // Test that supported commands work fine + editor.update_in(cx, |editor, window, cx| { + editor.set_text("/help", window, cx); + }); + + let contents_result = message_editor + .update(cx, |message_editor, cx| message_editor.contents(cx)) + .await; + + // Should succeed because /help is in available_commands + assert!(contents_result.is_ok()); + + // Test that regular text works fine + editor.update_in(cx, |editor, window, cx| { + editor.set_text("Hello Claude!", window, cx); + }); + + let (content, _) = message_editor + .update(cx, |message_editor, cx| message_editor.contents(cx)) + .await + .unwrap(); + + assert_eq!(content.len(), 1); + if let acp::ContentBlock::Text(text) = &content[0] { + assert_eq!(text.text, "Hello Claude!"); + } else { + panic!("Expected ContentBlock::Text"); + } + + // Test that @ mentions still work + editor.update_in(cx, |editor, window, cx| { + editor.set_text("Check this @", window, cx); + }); + + // The @ mention functionality should not be affected + let (content, _) = message_editor + .update(cx, |message_editor, cx| message_editor.contents(cx)) + .await + .unwrap(); + + assert_eq!(content.len(), 1); + if let acp::ContentBlock::Text(text) = &content[0] { + assert_eq!(text.text, "Check this @"); + } else { + panic!("Expected ContentBlock::Text"); + } + } + struct MessageEditorItem(Entity); impl Item for MessageEditorItem { @@ -1725,6 +1905,7 @@ mod tests { None, prompt_capabilities.clone(), available_commands.clone(), + "Test Agent".into(), "Test", EditorMode::AutoHeight { max_lines: None, @@ -1957,6 +2138,7 @@ mod tests { None, prompt_capabilities.clone(), Default::default(), + "Test Agent".into(), "Test", EditorMode::AutoHeight { max_lines: None, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c60710eaeeff29040ea9e3d98b002b865c799857..357a8543712eec5ea0723c4f36f05e4d6d5c0b9d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -342,6 +342,7 @@ impl AcpThreadView { prompt_store.clone(), prompt_capabilities.clone(), available_commands.clone(), + agent.name(), placeholder, editor::EditorMode::AutoHeight { min_lines: MIN_EDITOR_LINES, @@ -366,6 +367,7 @@ impl AcpThreadView { prompt_store.clone(), prompt_capabilities.clone(), available_commands.clone(), + agent.name(), ) }); @@ -495,8 +497,25 @@ impl AcpThreadView { Ok(thread) => { let action_log = thread.read(cx).action_log().clone(); - this.available_commands - .replace(thread.read(cx).available_commands()); + let mut available_commands = thread.read(cx).available_commands(); + + if connection + .auth_methods() + .iter() + .any(|method| method.id.0.as_ref() == "claude-login") + { + available_commands.push(acp::AvailableCommand { + name: "login".to_owned(), + description: "Authenticate".to_owned(), + input: None, + }); + available_commands.push(acp::AvailableCommand { + name: "logout".to_owned(), + description: "Authenticate".to_owned(), + input: None, + }); + } + this.available_commands.replace(available_commands); this.prompt_capabilities .set(thread.read(cx).prompt_capabilities()); @@ -914,6 +933,40 @@ impl AcpThreadView { return; } + let text = self.message_editor.read(cx).text(cx); + let text = text.trim(); + if text == "/login" || text == "/logout" { + let ThreadState::Ready { thread, .. } = &self.thread_state else { + return; + }; + + let connection = thread.read(cx).connection().clone(); + if !connection + .auth_methods() + .iter() + .any(|method| method.id.0.as_ref() == "claude-login") + { + return; + }; + let this = cx.weak_entity(); + let agent = self.agent.clone(); + window.defer(cx, |window, cx| { + Self::handle_auth_required( + this, + AuthRequired { + description: None, + provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID), + }, + agent, + connection, + window, + cx, + ); + }); + cx.notify(); + return; + } + let contents = self .message_editor .update(cx, |message_editor, cx| message_editor.contents(cx)); From 6dcae2711dd7f7aa5813826dfc4aa416fee2abc9 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 3 Sep 2025 03:00:09 +0530 Subject: [PATCH 513/744] terminal: Fix not able to select text during continuous output (#37395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #37211 Regressed in https://github.com/zed-industries/zed/pull/33305 Every time the terminal updates, we emit `SearchEvent::MatchesInvalidated` to trigger a re-run of the buffer search, which calls `clear_matches` to drop stale results. https://github.com/zed-industries/zed/pull/33305 PR also cleared the selection when clearing matches, which caused this issue. We could fix it by only clearing matches and selection when they’re non-empty, but it’s better to not clear the selection at all. This matches how the editor behaves and keeps it consistent. This PR reverts that part of code. Release Notes: - Fixed an issue where text selection was lost during continuous terminal output. --- crates/terminal/src/terminal.rs | 5 ----- crates/terminal_view/src/terminal_view.rs | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index c0c663f4987fd08aecbcc58b234333fef20a981c..a8b1fcf0f2a31cbd80612d2e19506d38d52fe0af 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1141,11 +1141,6 @@ impl Terminal { } } - pub fn clear_matches(&mut self) { - self.matches.clear(); - self.set_selection(None); - } - pub fn select_matches(&mut self, matches: &[RangeInclusive]) { let matches_to_select = self .matches diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 9e479464af224c4d85119d6ca2e0b25c360f9c3d..2548a7c24460be3161147b69e30c6191ba5dd2e6 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1523,7 +1523,7 @@ impl SearchableItem for TerminalView { /// Clear stored matches fn clear_matches(&mut self, _window: &mut Window, cx: &mut Context) { - self.terminal().update(cx, |term, _| term.clear_matches()) + self.terminal().update(cx, |term, _| term.matches.clear()) } /// Store matches returned from find_matches somewhere for rendering From 8770fcc841f7e8ffbd8ca126a75f92c86135aa67 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 2 Sep 2025 23:57:29 +0200 Subject: [PATCH 514/744] acp: Enable claude code feature flag for everyone (#37390) Release Notes: - N/A --- crates/feature_flags/src/feature_flags.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index f5f7fc42b35eba2ccd437c1e4cc4add0b4091773..b9e9f3ae9f36ecaa7cf4f4c7a41dc8ccab973730 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -108,6 +108,10 @@ pub struct ClaudeCodeFeatureFlag; impl FeatureFlag for ClaudeCodeFeatureFlag { const NAME: &'static str = "claude-code"; + + fn enabled_for_all() -> bool { + true + } } pub trait FeatureFlagViewExt { From e4df866664b4efa2adc78eb1a0fe3b6a89ece8d6 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 3 Sep 2025 03:41:10 +0530 Subject: [PATCH 515/744] editor: Do not show edit prediction during in-progress IME composition (#37400) Closes #37249 We no longer show edit prediction when composing IME since it isn't useful for unfinished alphabet. Release Notes: - Fixed edit predictions showing up during partial IME composition. --- crates/editor/src/editor.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d5621e8165cb7afe1acb869479e780f960ccb269..0494eb6c1f2e446202727ad987304250a3fd0291 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7744,6 +7744,11 @@ impl Editor { return None; } + if self.ime_transaction.is_some() { + self.discard_edit_prediction(false, cx); + return None; + } + let selection = self.selections.newest_anchor(); let cursor = selection.head(); let multibuffer = self.buffer.read(cx).snapshot(cx); From 7aecab8e14f1abaf6ebb3b4cb3857a53b3fd29e5 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Tue, 2 Sep 2025 16:02:36 -0700 Subject: [PATCH 516/744] agent2: Only setup real client for real models (#37403) Before we were setting up lots of test setup regardless of if we were actually going to be making real requests or not. This will hopefully help with intermittent test errors we're seeing on Windows in CI. Release Notes: - N/A --- crates/agent2/src/tests/mod.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 4527cdb056164efa8e3bc81c19969a3fa02d7036..9132c9a316e7e5122d24a0b413ae700a7adbaba9 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -72,7 +72,6 @@ async fn test_echo(cx: &mut TestAppContext) { } #[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_thinking(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); @@ -1349,7 +1348,6 @@ async fn test_cancellation(cx: &mut TestAppContext) { } #[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); @@ -1688,7 +1686,6 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) { } #[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_title_generation(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); @@ -2353,15 +2350,20 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { settings::init(cx); Project::init_settings(cx); agent_settings::init(cx); - gpui_tokio::init(cx); - let http_client = ReqwestClient::user_agent("agent tests").unwrap(); - cx.set_http_client(Arc::new(http_client)); - client::init_settings(cx); - let client = Client::production(cx); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - language_model::init(client.clone(), cx); - language_models::init(user_store, client.clone(), cx); + match model { + TestModel::Fake => {} + TestModel::Sonnet4 => { + gpui_tokio::init(cx); + let http_client = ReqwestClient::user_agent("agent tests").unwrap(); + cx.set_http_client(Arc::new(http_client)); + client::init_settings(cx); + let client = Client::production(cx); + let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + language_model::init(client.clone(), cx); + language_models::init(user_store, client.clone(), cx); + } + }; watch_settings(fs.clone(), cx); }); From e5a968b709feeeac1e12de054645671d64071676 Mon Sep 17 00:00:00 2001 From: Dino Date: Wed, 3 Sep 2025 00:03:14 +0100 Subject: [PATCH 517/744] vim: Fix change surround with any brackets text object (#37386) This commit fixes an issue with how the `AnyBrackets` object was handled with change surrounds (`cs`). With the keymap below, if one was to use `csb{` with the text `(bracketed)` and the cursor inside the parentheses, the text would not change. ```json { "context": "vim_operator == a || vim_operator == i || vim_operator == cs", "bindings": { "b": "vim::AnyBrackets" } } ``` Unfortunately there was no implementation for finding a corresponding `BracketPair` for the `AnyBrackets` object, meaning that, when using `cs` (change surrounds) the code would simply do nothing. This commit updates this logic so as to try and find the nearest surrounding bracket (parentheses, curly brackets, square brackets or angle brackets), ensuring that `cs` also works with `AnyBrackets`. Closes #24439 Release Notes: - Fixed handling of `AnyBrackets` in vim's change surrounds (`cs`) --- crates/vim/src/object.rs | 2 +- crates/vim/src/surrounds.rs | 255 ++++++++++++++++++++++++++---------- 2 files changed, 190 insertions(+), 67 deletions(-) diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 693de9f6971ff02639eee33e29e22e14902a7d37..366acb740bca32f5e191dd22309dd026c0d7ddd3 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -1510,7 +1510,7 @@ pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> D map.max_point() } -fn surrounding_markers( +pub fn surrounding_markers( map: &DisplaySnapshot, relative_to: DisplayPoint, around: bool, diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index 83500cf88b56c8f556887eb874901f50b6178018..7c36ebe6747488376d2264e4984175fb536fed4f 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -1,7 +1,7 @@ use crate::{ Vim, motion::{self, Motion}, - object::Object, + object::{Object, surrounding_markers}, state::Mode, }; use editor::{Bias, movement}; @@ -224,7 +224,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - if let Some(will_replace_pair) = object_to_bracket_pair(target) { + if let Some(will_replace_pair) = self.object_to_bracket_pair(target, cx) { self.stop_recording(cx); self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { @@ -358,7 +358,7 @@ impl Vim { cx: &mut Context, ) -> bool { let mut valid = false; - if let Some(pair) = object_to_bracket_pair(object) { + if let Some(pair) = self.object_to_bracket_pair(object, cx) { self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); @@ -405,6 +405,140 @@ impl Vim { } valid } + + fn object_to_bracket_pair( + &self, + object: Object, + cx: &mut Context, + ) -> Option { + match object { + Object::Quotes => Some(BracketPair { + start: "'".to_string(), + end: "'".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::BackQuotes => Some(BracketPair { + start: "`".to_string(), + end: "`".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::DoubleQuotes => Some(BracketPair { + start: "\"".to_string(), + end: "\"".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::VerticalBars => Some(BracketPair { + start: "|".to_string(), + end: "|".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::Parentheses => Some(BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::SquareBrackets => Some(BracketPair { + start: "[".to_string(), + end: "]".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::CurlyBrackets => Some(BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::AngleBrackets => Some(BracketPair { + start: "<".to_string(), + end: ">".to_string(), + close: true, + surround: true, + newline: false, + }), + Object::AnyBrackets => { + // If we're dealing with `AnyBrackets`, which can map to multiple + // bracket pairs, we'll need to first determine which `BracketPair` to + // target. + // As such, we keep track of the smallest range size, so + // that in cases like `({ name: "John" })` if the cursor is + // inside the curly brackets, we target the curly brackets + // instead of the parentheses. + let mut bracket_pair = None; + let mut min_range_size = usize::MAX; + + let _ = self.editor.update(cx, |editor, cx| { + let (display_map, selections) = editor.selections.all_adjusted_display(cx); + // Even if there's multiple cursors, we'll simply rely on + // the first one to understand what bracket pair to map to. + // I believe we could, if worth it, go one step above and + // have a `BracketPair` per selection, so that `AnyBracket` + // could work in situations where the transformation below + // could be done. + // + // ``` + // (< name:ˇ'Zed' >) + // <[ name:ˇ'DeltaDB' ]> + // ``` + // + // After using `csb{`: + // + // ``` + // (ˇ{ name:'Zed' }) + // <ˇ{ name:'DeltaDB' }> + // ``` + if let Some(selection) = selections.first() { + let relative_to = selection.head(); + let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; + let cursor_offset = relative_to.to_offset(&display_map, Bias::Left); + + for &(open, close) in bracket_pairs.iter() { + if let Some(range) = surrounding_markers( + &display_map, + relative_to, + true, + false, + open, + close, + ) { + let start_offset = range.start.to_offset(&display_map, Bias::Left); + let end_offset = range.end.to_offset(&display_map, Bias::Right); + + if cursor_offset >= start_offset && cursor_offset <= end_offset { + let size = end_offset - start_offset; + if size < min_range_size { + min_range_size = size; + bracket_pair = Some(BracketPair { + start: open.to_string(), + end: close.to_string(), + close: true, + surround: true, + newline: false, + }) + } + } + } + } + } + }); + + bracket_pair + } + _ => None, + } + } } fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> { @@ -505,74 +639,12 @@ fn pair_to_object(pair: &BracketPair) -> Option { } } -fn object_to_bracket_pair(object: Object) -> Option { - match object { - Object::Quotes => Some(BracketPair { - start: "'".to_string(), - end: "'".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::BackQuotes => Some(BracketPair { - start: "`".to_string(), - end: "`".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::DoubleQuotes => Some(BracketPair { - start: "\"".to_string(), - end: "\"".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::VerticalBars => Some(BracketPair { - start: "|".to_string(), - end: "|".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::Parentheses => Some(BracketPair { - start: "(".to_string(), - end: ")".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::SquareBrackets => Some(BracketPair { - start: "[".to_string(), - end: "]".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::CurlyBrackets => Some(BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::AngleBrackets => Some(BracketPair { - start: "<".to_string(), - end: ">".to_string(), - close: true, - surround: true, - newline: false, - }), - _ => None, - } -} - #[cfg(test)] mod test { use gpui::KeyBinding; use indoc::indoc; - use crate::{PushAddSurrounds, state::Mode, test::VimTestContext}; + use crate::{PushAddSurrounds, object::AnyBrackets, state::Mode, test::VimTestContext}; #[gpui::test] async fn test_add_surrounds(cx: &mut gpui::TestAppContext) { @@ -1171,6 +1243,57 @@ mod test { ); } + #[gpui::test] + async fn test_change_surrounds_any_brackets(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // Update keybindings so that using `csb` triggers Vim's `AnyBrackets` + // action. + cx.update(|_, cx| { + cx.bind_keys([KeyBinding::new( + "b", + AnyBrackets, + Some("vim_operator == a || vim_operator == i || vim_operator == cs"), + )]); + }); + + cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal); + cx.simulate_keystrokes("c s b ["); + cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); + + cx.set_state(indoc! {"[braˇcketed]"}, Mode::Normal); + cx.simulate_keystrokes("c s b {"); + cx.assert_state(indoc! {"ˇ{ bracketed }"}, Mode::Normal); + + cx.set_state(indoc! {""}, Mode::Normal); + cx.simulate_keystrokes("c s b ["); + cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); + + cx.set_state(indoc! {"(braˇcketed)"}, Mode::Normal); + cx.simulate_keystrokes("c s b ["); + cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); + + cx.set_state(indoc! {"(< name: ˇ'Zed' >)"}, Mode::Normal); + cx.simulate_keystrokes("c s b {"); + cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal); + + cx.set_state( + indoc! {" + (< name: ˇ'Zed' >) + (< nˇame: 'DeltaDB' >) + "}, + Mode::Normal, + ); + cx.simulate_keystrokes("c s b {"); + cx.set_state( + indoc! {" + (ˇ{ name: 'Zed' }) + (ˇ{ name: 'DeltaDB' }) + "}, + Mode::Normal, + ); + } + #[gpui::test] async fn test_surrounds(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; From 4368c1b56ba2c6f13957ca884459f00770a6fd01 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 3 Sep 2025 04:43:46 +0530 Subject: [PATCH 518/744] language_models: Add OpenRouterError and map OpenRouter errors to LanguageModelCompletionError (#34227) Improves the error handling for openrouter and adds automatic retry like anthropic for few of the status codes. Release Notes: - Improves error messages for Openrouter provider - Automatic retry when rate limited or Server error from Openrouter --- Cargo.lock | 3 + crates/language_model/Cargo.toml | 1 + crates/language_model/src/language_model.rs | 67 ++++ .../src/provider/open_router.rs | 63 ++-- crates/open_router/Cargo.toml | 2 + crates/open_router/src/open_router.rs | 350 +++++++++++------- 6 files changed, 334 insertions(+), 152 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da9eeabee4a37fcdcc10a2448c6a4c434ab2ee7c..42e343e062958a99c5e242ebfb0c5fc1516d694d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9147,6 +9147,7 @@ dependencies = [ "icons", "image", "log", + "open_router", "parking_lot", "proto", "schemars", @@ -11222,6 +11223,8 @@ dependencies = [ "schemars", "serde", "serde_json", + "strum 0.27.1", + "thiserror 2.0.12", "workspace-hack", ] diff --git a/crates/language_model/Cargo.toml b/crates/language_model/Cargo.toml index f9920623b5ea3bff79535f92753fae0b723f850f..d4513f617b0d9f79e960c6cec6ca1a5dd806cea6 100644 --- a/crates/language_model/Cargo.toml +++ b/crates/language_model/Cargo.toml @@ -17,6 +17,7 @@ test-support = [] [dependencies] anthropic = { workspace = true, features = ["schemars"] } +open_router.workspace = true anyhow.workspace = true base64.workspace = true client.workspace = true diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index d5313b6a3aa5b38e6adb40b26edb827e66fb7dae..fac302104fd9a4da82f5a383d5cd86b64fde4731 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -17,6 +17,7 @@ use futures::{StreamExt, future::BoxFuture, stream::BoxStream}; use gpui::{AnyView, App, AsyncApp, SharedString, Task, Window}; use http_client::{StatusCode, http}; use icons::IconName; +use open_router::OpenRouterError; use parking_lot::Mutex; use schemars::JsonSchema; use serde::{Deserialize, Serialize, de::DeserializeOwned}; @@ -347,6 +348,72 @@ impl From for LanguageModelCompletionError { } } +impl From for LanguageModelCompletionError { + fn from(error: OpenRouterError) -> Self { + let provider = LanguageModelProviderName::new("OpenRouter"); + match error { + OpenRouterError::SerializeRequest(error) => Self::SerializeRequest { provider, error }, + OpenRouterError::BuildRequestBody(error) => Self::BuildRequestBody { provider, error }, + OpenRouterError::HttpSend(error) => Self::HttpSend { provider, error }, + OpenRouterError::DeserializeResponse(error) => { + Self::DeserializeResponse { provider, error } + } + OpenRouterError::ReadResponse(error) => Self::ApiReadResponseError { provider, error }, + OpenRouterError::RateLimit { retry_after } => Self::RateLimitExceeded { + provider, + retry_after: Some(retry_after), + }, + OpenRouterError::ServerOverloaded { retry_after } => Self::ServerOverloaded { + provider, + retry_after, + }, + OpenRouterError::ApiError(api_error) => api_error.into(), + } + } +} + +impl From for LanguageModelCompletionError { + fn from(error: open_router::ApiError) -> Self { + use open_router::ApiErrorCode::*; + let provider = LanguageModelProviderName::new("OpenRouter"); + match error.code { + InvalidRequestError => Self::BadRequestFormat { + provider, + message: error.message, + }, + AuthenticationError => Self::AuthenticationError { + provider, + message: error.message, + }, + PaymentRequiredError => Self::AuthenticationError { + provider, + message: format!("Payment required: {}", error.message), + }, + PermissionError => Self::PermissionError { + provider, + message: error.message, + }, + RequestTimedOut => Self::HttpResponseError { + provider, + status_code: StatusCode::REQUEST_TIMEOUT, + message: error.message, + }, + RateLimitError => Self::RateLimitExceeded { + provider, + retry_after: None, + }, + ApiError => Self::ApiInternalServerError { + provider, + message: error.message, + }, + OverloadedError => Self::ServerOverloaded { + provider, + retry_after: None, + }, + } + } +} + /// Indicates the format used to define the input schema for a language model tool. #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum LanguageModelToolSchemaFormat { diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index aaa0bd620ccf1a961b8c97c0c9fe3ba348b51cca..9138f6b82e7e74e9e6a7468306b2f5cf6768987e 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -152,6 +152,7 @@ impl State { .open_router .api_url .clone(); + cx.spawn(async move |this, cx| { let (api_key, from_env) = if let Ok(api_key) = std::env::var(OPENROUTER_API_KEY_VAR) { (api_key, true) @@ -161,11 +162,11 @@ impl State { .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( - String::from_utf8(api_key) - .context(format!("invalid {} API key", PROVIDER_NAME))?, + String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?, false, ) }; + this.update(cx, |this, cx| { this.api_key = Some(api_key); this.api_key_from_env = from_env; @@ -183,7 +184,9 @@ impl State { let api_url = settings.api_url.clone(); cx.spawn(async move |this, cx| { - let models = list_models(http_client.as_ref(), &api_url).await?; + let models = list_models(http_client.as_ref(), &api_url) + .await + .map_err(|e| anyhow::anyhow!("OpenRouter error: {:?}", e))?; this.update(cx, |this, cx| { this.available_models = models; @@ -334,27 +337,37 @@ impl OpenRouterLanguageModel { &self, request: open_router::Request, cx: &AsyncApp, - ) -> BoxFuture<'static, Result>>> - { + ) -> BoxFuture< + 'static, + Result< + futures::stream::BoxStream< + 'static, + Result, + >, + LanguageModelCompletionError, + >, + > { let http_client = self.http_client.clone(); let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, cx| { let settings = &AllLanguageModelSettings::get_global(cx).open_router; (state.api_key.clone(), settings.api_url.clone()) }) else { - return futures::future::ready(Err(anyhow!( - "App state dropped: Unable to read API key or API URL from the application state" - ))) + return futures::future::ready(Err(LanguageModelCompletionError::Other(anyhow!( + "App state dropped" + )))) .boxed(); }; - let future = self.request_limiter.stream(async move { - let api_key = api_key.ok_or_else(|| anyhow!("Missing OpenRouter API Key"))?; + async move { + let Some(api_key) = api_key else { + return Err(LanguageModelCompletionError::NoApiKey { + provider: PROVIDER_NAME, + }); + }; let request = stream_completion(http_client.as_ref(), &api_url, &api_key, request); - let response = request.await?; - Ok(response) - }); - - async move { Ok(future.await?.boxed()) }.boxed() + request.await.map_err(Into::into) + } + .boxed() } } @@ -435,12 +448,12 @@ impl LanguageModel for OpenRouterLanguageModel { >, > { let request = into_open_router(request, &self.model, self.max_output_tokens()); - let completions = self.stream_completion(request, cx); - async move { - let mapper = OpenRouterEventMapper::new(); - Ok(mapper.map_stream(completions.await?).boxed()) - } - .boxed() + let request = self.stream_completion(request, cx); + let future = self.request_limiter.stream(async move { + let response = request.await?; + Ok(OpenRouterEventMapper::new().map_stream(response)) + }); + async move { Ok(future.await?.boxed()) }.boxed() } } @@ -608,13 +621,17 @@ impl OpenRouterEventMapper { pub fn map_stream( mut self, - events: Pin>>>, + events: Pin< + Box< + dyn Send + Stream>, + >, + >, ) -> impl Stream> { events.flat_map(move |event| { futures::stream::iter(match event { Ok(event) => self.map_event(event), - Err(error) => vec![Err(LanguageModelCompletionError::from(anyhow!(error)))], + Err(error) => vec![Err(error.into())], }) }) } diff --git a/crates/open_router/Cargo.toml b/crates/open_router/Cargo.toml index bbc4fe190fa3985ef82505078d76dd06adf2abd9..8920c157dc3d6ea0974bd978816eb58cde19919d 100644 --- a/crates/open_router/Cargo.toml +++ b/crates/open_router/Cargo.toml @@ -22,4 +22,6 @@ http_client.workspace = true schemars = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true +thiserror.workspace = true +strum.workspace = true workspace-hack.workspace = true diff --git a/crates/open_router/src/open_router.rs b/crates/open_router/src/open_router.rs index 65ef519d2c887e57e67d68ca6fcaea64ad67ee3e..dfaa49746d093810924f744cd1aeb3e8747ddb00 100644 --- a/crates/open_router/src/open_router.rs +++ b/crates/open_router/src/open_router.rs @@ -1,12 +1,31 @@ -use anyhow::{Context, Result, anyhow}; +use anyhow::{Result, anyhow}; use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; -use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest}; +use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, http}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::convert::TryFrom; +use std::{convert::TryFrom, io, time::Duration}; +use strum::EnumString; +use thiserror::Error; pub const OPEN_ROUTER_API_URL: &str = "https://openrouter.ai/api/v1"; +fn extract_retry_after(headers: &http::HeaderMap) -> Option { + if let Some(reset) = headers.get("X-RateLimit-Reset") { + if let Ok(s) = reset.to_str() { + if let Ok(epoch_ms) = s.parse::() { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64; + if epoch_ms > now { + return Some(std::time::Duration::from_millis(epoch_ms - now)); + } + } + } + } + None +} + fn is_none_or_empty, U>(opt: &Option) -> bool { opt.as_ref().is_none_or(|v| v.as_ref().is_empty()) } @@ -413,76 +432,12 @@ pub struct ModelArchitecture { pub input_modalities: Vec, } -pub async fn complete( - client: &dyn HttpClient, - api_url: &str, - api_key: &str, - request: Request, -) -> Result { - let uri = format!("{api_url}/chat/completions"); - let request_builder = HttpRequest::builder() - .method(Method::POST) - .uri(uri) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key.trim())) - .header("HTTP-Referer", "https://zed.dev") - .header("X-Title", "Zed Editor"); - - let mut request_body = request; - request_body.stream = false; - - let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request_body)?))?; - let mut response = client.send(request).await?; - - if response.status().is_success() { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - let response: Response = serde_json::from_str(&body)?; - Ok(response) - } else { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - #[derive(Deserialize)] - struct OpenRouterResponse { - error: OpenRouterError, - } - - #[derive(Deserialize)] - struct OpenRouterError { - message: String, - #[serde(default)] - code: String, - } - - match serde_json::from_str::(&body) { - Ok(response) if !response.error.message.is_empty() => { - let error_message = if !response.error.code.is_empty() { - format!("{}: {}", response.error.code, response.error.message) - } else { - response.error.message - }; - - Err(anyhow!( - "Failed to connect to OpenRouter API: {}", - error_message - )) - } - _ => Err(anyhow!( - "Failed to connect to OpenRouter API: {} {}", - response.status(), - body, - )), - } - } -} - pub async fn stream_completion( client: &dyn HttpClient, api_url: &str, api_key: &str, request: Request, -) -> Result>> { +) -> Result>, OpenRouterError> { let uri = format!("{api_url}/chat/completions"); let request_builder = HttpRequest::builder() .method(Method::POST) @@ -492,8 +447,15 @@ pub async fn stream_completion( .header("HTTP-Referer", "https://zed.dev") .header("X-Title", "Zed Editor"); - let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?; - let mut response = client.send(request).await?; + let request = request_builder + .body(AsyncBody::from( + serde_json::to_string(&request).map_err(OpenRouterError::SerializeRequest)?, + )) + .map_err(OpenRouterError::BuildRequestBody)?; + let mut response = client + .send(request) + .await + .map_err(OpenRouterError::HttpSend)?; if response.status().is_success() { let reader = BufReader::new(response.into_body()); @@ -513,86 +475,85 @@ pub async fn stream_completion( match serde_json::from_str::(line) { Ok(response) => Some(Ok(response)), Err(error) => { - #[derive(Deserialize)] - struct ErrorResponse { - error: String, - } - - match serde_json::from_str::(line) { - Ok(err_response) => Some(Err(anyhow!(err_response.error))), - Err(_) => { - if line.trim().is_empty() { - None - } else { - Some(Err(anyhow!( - "Failed to parse response: {}. Original content: '{}'", - error, line - ))) - } - } + if line.trim().is_empty() { + None + } else { + Some(Err(OpenRouterError::DeserializeResponse(error))) } } } } } - Err(error) => Some(Err(anyhow!(error))), + Err(error) => Some(Err(OpenRouterError::ReadResponse(error))), } }) .boxed()) } else { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; + let code = ApiErrorCode::from_status(response.status().as_u16()); - #[derive(Deserialize)] - struct OpenRouterResponse { - error: OpenRouterError, - } - - #[derive(Deserialize)] - struct OpenRouterError { - message: String, - #[serde(default)] - code: String, - } - - match serde_json::from_str::(&body) { - Ok(response) if !response.error.message.is_empty() => { - let error_message = if !response.error.code.is_empty() { - format!("{}: {}", response.error.code, response.error.message) - } else { - response.error.message - }; - - Err(anyhow!( - "Failed to connect to OpenRouter API: {}", - error_message - )) + let mut body = String::new(); + response + .body_mut() + .read_to_string(&mut body) + .await + .map_err(OpenRouterError::ReadResponse)?; + + let error_response = match serde_json::from_str::(&body) { + Ok(OpenRouterErrorResponse { error }) => error, + Err(_) => OpenRouterErrorBody { + code: response.status().as_u16(), + message: body, + metadata: None, + }, + }; + + match code { + ApiErrorCode::RateLimitError => { + let retry_after = extract_retry_after(response.headers()); + Err(OpenRouterError::RateLimit { + retry_after: retry_after.unwrap_or_else(|| std::time::Duration::from_secs(60)), + }) + } + ApiErrorCode::OverloadedError => { + let retry_after = extract_retry_after(response.headers()); + Err(OpenRouterError::ServerOverloaded { retry_after }) } - _ => Err(anyhow!( - "Failed to connect to OpenRouter API: {} {}", - response.status(), - body, - )), + _ => Err(OpenRouterError::ApiError(ApiError { + code: code, + message: error_response.message, + })), } } } -pub async fn list_models(client: &dyn HttpClient, api_url: &str) -> Result> { +pub async fn list_models( + client: &dyn HttpClient, + api_url: &str, +) -> Result, OpenRouterError> { let uri = format!("{api_url}/models"); let request_builder = HttpRequest::builder() .method(Method::GET) .uri(uri) .header("Accept", "application/json"); - let request = request_builder.body(AsyncBody::default())?; - let mut response = client.send(request).await?; + let request = request_builder + .body(AsyncBody::default()) + .map_err(OpenRouterError::BuildRequestBody)?; + let mut response = client + .send(request) + .await + .map_err(OpenRouterError::HttpSend)?; let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; + response + .body_mut() + .read_to_string(&mut body) + .await + .map_err(OpenRouterError::ReadResponse)?; if response.status().is_success() { let response: ListModelsResponse = - serde_json::from_str(&body).context("Unable to parse OpenRouter models response")?; + serde_json::from_str(&body).map_err(OpenRouterError::DeserializeResponse)?; let models = response .data @@ -637,10 +598,141 @@ pub async fn list_models(client: &dyn HttpClient, api_url: &str) -> Result(&body) { + Ok(OpenRouterErrorResponse { error }) => error, + Err(_) => OpenRouterErrorBody { + code: response.status().as_u16(), + message: body, + metadata: None, + }, + }; + + match code { + ApiErrorCode::RateLimitError => { + let retry_after = extract_retry_after(response.headers()); + Err(OpenRouterError::RateLimit { + retry_after: retry_after.unwrap_or_else(|| std::time::Duration::from_secs(60)), + }) + } + ApiErrorCode::OverloadedError => { + let retry_after = extract_retry_after(response.headers()); + Err(OpenRouterError::ServerOverloaded { retry_after }) + } + _ => Err(OpenRouterError::ApiError(ApiError { + code: code, + message: error_response.message, + })), + } + } +} + +#[derive(Debug)] +pub enum OpenRouterError { + /// Failed to serialize the HTTP request body to JSON + SerializeRequest(serde_json::Error), + + /// Failed to construct the HTTP request body + BuildRequestBody(http::Error), + + /// Failed to send the HTTP request + HttpSend(anyhow::Error), + + /// Failed to deserialize the response from JSON + DeserializeResponse(serde_json::Error), + + /// Failed to read from response stream + ReadResponse(io::Error), + + /// Rate limit exceeded + RateLimit { retry_after: Duration }, + + /// Server overloaded + ServerOverloaded { retry_after: Option }, + + /// API returned an error response + ApiError(ApiError), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OpenRouterErrorBody { + pub code: u16, + pub message: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub metadata: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OpenRouterErrorResponse { + pub error: OpenRouterErrorBody, +} + +#[derive(Debug, Serialize, Deserialize, Error)] +#[error("OpenRouter API Error: {code}: {message}")] +pub struct ApiError { + pub code: ApiErrorCode, + pub message: String, +} + +/// An OpenROuter API error code. +/// +#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString, Serialize, Deserialize)] +#[strum(serialize_all = "snake_case")] +pub enum ApiErrorCode { + /// 400: Bad Request (invalid or missing params, CORS) + InvalidRequestError, + /// 401: Invalid credentials (OAuth session expired, disabled/invalid API key) + AuthenticationError, + /// 402: Your account or API key has insufficient credits. Add more credits and retry the request. + PaymentRequiredError, + /// 403: Your chosen model requires moderation and your input was flagged + PermissionError, + /// 408: Your request timed out + RequestTimedOut, + /// 429: You are being rate limited + RateLimitError, + /// 502: Your chosen model is down or we received an invalid response from it + ApiError, + /// 503: There is no available model provider that meets your routing requirements + OverloadedError, +} + +impl std::fmt::Display for ApiErrorCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + ApiErrorCode::InvalidRequestError => "invalid_request_error", + ApiErrorCode::AuthenticationError => "authentication_error", + ApiErrorCode::PaymentRequiredError => "payment_required_error", + ApiErrorCode::PermissionError => "permission_error", + ApiErrorCode::RequestTimedOut => "request_timed_out", + ApiErrorCode::RateLimitError => "rate_limit_error", + ApiErrorCode::ApiError => "api_error", + ApiErrorCode::OverloadedError => "overloaded_error", + }; + write!(f, "{s}") + } +} + +impl ApiErrorCode { + pub fn from_status(status: u16) -> Self { + match status { + 400 => ApiErrorCode::InvalidRequestError, + 401 => ApiErrorCode::AuthenticationError, + 402 => ApiErrorCode::PaymentRequiredError, + 403 => ApiErrorCode::PermissionError, + 408 => ApiErrorCode::RequestTimedOut, + 429 => ApiErrorCode::RateLimitError, + 502 => ApiErrorCode::ApiError, + 503 => ApiErrorCode::OverloadedError, + _ => ApiErrorCode::ApiError, + } } } From 4b96ad3fbaf131fe11f32b18f5f93514027f88b7 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 2 Sep 2025 19:14:47 -0400 Subject: [PATCH 519/744] gpui: Remove `http_client` feature (#37401) This PR removes the `http_client` feature from the `gpui` crate, as it wasn't really doing anything. It only controlled whether we depend on the `http_client` crate, but from what I can tell we always depended on it anyways. Obviates https://github.com/zed-industries/zed/pull/36615. Release Notes: - N/A --- Cargo.toml | 4 +--- crates/gpui/Cargo.toml | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2c8143756b33b35f892076c3e71d6bf458a47ea0..90b01945f71264eb367239809c9811034a71f2d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -299,9 +299,7 @@ git_hosting_providers = { path = "crates/git_hosting_providers" } git_ui = { path = "crates/git_ui" } go_to_line = { path = "crates/go_to_line" } google_ai = { path = "crates/google_ai" } -gpui = { path = "crates/gpui", default-features = false, features = [ - "http_client", -] } +gpui = { path = "crates/gpui", default-features = false } gpui_macros = { path = "crates/gpui_macros" } gpui_tokio = { path = "crates/gpui_tokio" } html_to_markdown = { path = "crates/html_to_markdown" } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 9f5b66087da1110f50ac08d9106ec960e2f965aa..dd91eb4d4ee408b5381701f8ef5f4dae13344994 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -12,13 +12,13 @@ license = "Apache-2.0" workspace = true [features] -default = ["http_client", "font-kit", "wayland", "x11", "windows-manifest"] +default = ["font-kit", "wayland", "x11", "windows-manifest"] test-support = [ "leak-detection", "collections/test-support", "rand", "util/test-support", - "http_client?/test-support", + "http_client/test-support", "wayland", "x11", ] @@ -91,7 +91,7 @@ derive_more.workspace = true etagere = "0.2" futures.workspace = true gpui_macros.workspace = true -http_client = { optional = true, workspace = true } +http_client.workspace = true image.workspace = true inventory.workspace = true itertools.workspace = true From 946efb03df74a90dcaa15b2838820fda9d7e1daf Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 2 Sep 2025 20:18:15 -0300 Subject: [PATCH 520/744] Add option for code context menu items to have dynamic width (#37404) Follow up to https://github.com/zed-industries/zed/pull/30598 This PR introduces the `display_options` field in the `CompletionResponse`, allowing a code context menu width to be dynamically dictated based on its larger item. This will allow us to have the @-mentions and slash commands completion menus in the agent panel not be bigger than it needs to be. It may also be relevant/useful in the future for other use cases. For now, we set all instances of code context menus to use a fixed width, as defined in the PR linked above, which means this PR shouldn't cause any visual change. Release Notes: - N/A Co-authored-by: Michael Sloan --- .../agent_ui/src/acp/completion_provider.rs | 5 ++- .../src/context_picker/completion_provider.rs | 6 ++- crates/agent_ui/src/slash_command.rs | 9 +++- .../src/chat_panel/message_editor.rs | 6 ++- .../src/session/running/console.rs | 4 +- crates/editor/src/code_context_menus.rs | 41 ++++++++++++++++++- crates/editor/src/editor.rs | 29 +++++++++---- crates/inspector_ui/src/div_inspector.rs | 6 ++- crates/keymap_editor/src/keymap_editor.rs | 3 +- crates/project/src/lsp_store.rs | 10 +++-- crates/project/src/project.rs | 12 ++++++ 11 files changed, 110 insertions(+), 21 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index dc38c65868385e1e5ee913dd76160c7bdebbd0ad..6d2253b40686baf6719dfc00df72a505842c4bc9 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -15,7 +15,8 @@ use language::{Buffer, CodeLabel, HighlightId}; use lsp::CompletionContext; use project::lsp_store::CompletionDocumentation; use project::{ - Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId, + Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project, + ProjectPath, Symbol, WorktreeId, }; use prompt_store::PromptStore; use rope::Point; @@ -771,6 +772,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { Ok(vec![CompletionResponse { completions, + display_options: CompletionDisplayOptions::default(), // Since this does its own filtering (see `filter_completions()` returns false), // there is no benefit to computing whether this set of completions is incomplete. is_incomplete: true, @@ -862,6 +864,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { Ok(vec![CompletionResponse { completions, + display_options: CompletionDisplayOptions::default(), // Since this does its own filtering (see `filter_completions()` returns false), // there is no benefit to computing whether this set of completions is incomplete. is_incomplete: true, diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index 020d799c799f4184494fc3d9ec6a0ef8119a9897..b67b463e3bfa654baefece2c97fc505460830f2d 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -13,7 +13,10 @@ use http_client::HttpClientWithUrl; use itertools::Itertools; use language::{Buffer, CodeLabel, HighlightId}; use lsp::CompletionContext; -use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, Symbol, WorktreeId}; +use project::{ + Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, ProjectPath, + Symbol, WorktreeId, +}; use prompt_store::PromptStore; use rope::Point; use text::{Anchor, OffsetRangeExt, ToPoint}; @@ -897,6 +900,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { Ok(vec![CompletionResponse { completions, + display_options: CompletionDisplayOptions::default(), // Since this does its own filtering (see `filter_completions()` returns false), // there is no benefit to computing whether this set of completions is incomplete. is_incomplete: true, diff --git a/crates/agent_ui/src/slash_command.rs b/crates/agent_ui/src/slash_command.rs index 87e5d45fe85218c35316cb0a043caadf8837a2ea..c2f26c4f2ed33860196790746dd296e8c617b810 100644 --- a/crates/agent_ui/src/slash_command.rs +++ b/crates/agent_ui/src/slash_command.rs @@ -7,7 +7,10 @@ use fuzzy::{StringMatchCandidate, match_strings}; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window}; use language::{Anchor, Buffer, ToPoint}; use parking_lot::Mutex; -use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation}; +use project::{ + CompletionDisplayOptions, CompletionIntent, CompletionSource, + lsp_store::CompletionDocumentation, +}; use rope::Point; use std::{ ops::Range, @@ -133,6 +136,7 @@ impl SlashCommandCompletionProvider { vec![project::CompletionResponse { completions, + display_options: CompletionDisplayOptions::default(), is_incomplete: false, }] }) @@ -237,6 +241,7 @@ impl SlashCommandCompletionProvider { Ok(vec![project::CompletionResponse { completions, + display_options: CompletionDisplayOptions::default(), // TODO: Could have slash commands indicate whether their completions are incomplete. is_incomplete: true, }]) @@ -244,6 +249,7 @@ impl SlashCommandCompletionProvider { } else { Task::ready(Ok(vec![project::CompletionResponse { completions: Vec::new(), + display_options: CompletionDisplayOptions::default(), is_incomplete: true, }])) } @@ -305,6 +311,7 @@ impl CompletionProvider for SlashCommandCompletionProvider { else { return Task::ready(Ok(vec![project::CompletionResponse { completions: Vec::new(), + display_options: CompletionDisplayOptions::default(), is_incomplete: false, }])); }; diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 5fead5bcf10cc62dc6f60414978366cb2eac313b..3864ca69d88dd8231aa4b2f5b656c11f41b07282 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -12,7 +12,9 @@ use language::{ Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, ToOffset, language_settings::SoftWrap, }; -use project::{Completion, CompletionResponse, CompletionSource, search::SearchQuery}; +use project::{ + Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource, search::SearchQuery, +}; use settings::Settings; use std::{ ops::Range, @@ -275,6 +277,7 @@ impl MessageEditor { Task::ready(Ok(vec![CompletionResponse { completions: Vec::new(), + display_options: CompletionDisplayOptions::default(), is_incomplete: false, }])) } @@ -317,6 +320,7 @@ impl MessageEditor { CompletionResponse { is_incomplete: completions.len() >= LIMIT, + display_options: CompletionDisplayOptions::default(), completions, } } diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index a801cedd26924198d6a0639a3832ab0253b53adb..43d86d95c4a5ad6cc0b7729a3f3579b27e1dfee7 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -15,7 +15,7 @@ use gpui::{ use language::{Anchor, Buffer, CodeLabel, TextBufferSnapshot, ToOffset}; use menu::{Confirm, SelectNext, SelectPrevious}; use project::{ - Completion, CompletionResponse, + Completion, CompletionDisplayOptions, CompletionResponse, debugger::session::{CompletionsQuery, OutputToken, Session}, lsp_store::CompletionDocumentation, search_history::{SearchHistory, SearchHistoryCursor}, @@ -685,6 +685,7 @@ impl ConsoleQueryBarCompletionProvider { Ok(vec![project::CompletionResponse { is_incomplete: completions.len() >= LIMIT, + display_options: CompletionDisplayOptions::default(), completions, }]) }) @@ -797,6 +798,7 @@ impl ConsoleQueryBarCompletionProvider { Ok(vec![project::CompletionResponse { completions, + display_options: CompletionDisplayOptions::default(), is_incomplete: false, }]) }) diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 01e74284eff4cb140efe43202ef5dda9a002f94d..6d57048985955730bef2c7840d645c87b56915fc 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -11,9 +11,9 @@ use language::{Buffer, LanguageName, LanguageRegistry}; use markdown::{Markdown, MarkdownElement}; use multi_buffer::{Anchor, ExcerptId}; use ordered_float::OrderedFloat; -use project::CompletionSource; use project::lsp_store::CompletionDocumentation; use project::{CodeAction, Completion, TaskSourceKind}; +use project::{CompletionDisplayOptions, CompletionSource}; use task::DebugScenario; use task::TaskContext; @@ -232,6 +232,7 @@ pub struct CompletionsMenu { markdown_cache: Rc)>>>, language_registry: Option>, language: Option, + display_options: CompletionDisplayOptions, snippet_sort_order: SnippetSortOrder, } @@ -271,6 +272,7 @@ impl CompletionsMenu { is_incomplete: bool, buffer: Entity, completions: Box<[Completion]>, + display_options: CompletionDisplayOptions, snippet_sort_order: SnippetSortOrder, language_registry: Option>, language: Option, @@ -304,6 +306,7 @@ impl CompletionsMenu { markdown_cache: RefCell::new(VecDeque::new()).into(), language_registry, language, + display_options, snippet_sort_order, }; @@ -375,6 +378,7 @@ impl CompletionsMenu { markdown_cache: RefCell::new(VecDeque::new()).into(), language_registry: None, language: None, + display_options: CompletionDisplayOptions::default(), snippet_sort_order, } } @@ -737,6 +741,33 @@ impl CompletionsMenu { cx: &mut Context, ) -> AnyElement { let show_completion_documentation = self.show_completion_documentation; + let widest_completion_ix = if self.display_options.dynamic_width { + let completions = self.completions.borrow(); + let widest_completion_ix = self + .entries + .borrow() + .iter() + .enumerate() + .max_by_key(|(_, mat)| { + let completion = &completions[mat.candidate_id]; + let documentation = &completion.documentation; + + let mut len = completion.label.text.chars().count(); + if let Some(CompletionDocumentation::SingleLine(text)) = documentation { + if show_completion_documentation { + len += text.chars().count(); + } + } + + len + }) + .map(|(ix, _)| ix); + drop(completions); + widest_completion_ix + } else { + None + }; + let selected_item = self.selected_item; let completions = self.completions.clone(); let entries = self.entries.clone(); @@ -863,7 +894,13 @@ impl CompletionsMenu { .max_h(max_height_in_lines as f32 * window.line_height()) .track_scroll(self.scroll_handle.clone()) .with_sizing_behavior(ListSizingBehavior::Infer) - .w(rems(34.)); + .map(|this| { + if self.display_options.dynamic_width { + this.with_width_from_item(widest_completion_ix) + } else { + this.w(rems(34.)) + } + }); Popover::new().child(list).into_any_element() } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0494eb6c1f2e446202727ad987304250a3fd0291..00fe6637bf6c5ff8d8caa3c9c223fc55ffaee3cc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -147,21 +147,22 @@ use multi_buffer::{ use parking_lot::Mutex; use persistence::DB; use project::{ - BreakpointWithPosition, CodeAction, Completion, CompletionIntent, CompletionResponse, - CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, Location, LocationLink, - PrepareRenameResponse, Project, ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind, - debugger::breakpoint_store::Breakpoint, + BreakpointWithPosition, CodeAction, Completion, CompletionDisplayOptions, CompletionIntent, + CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, + Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectPath, + ProjectTransaction, TaskSourceKind, debugger::{ breakpoint_store::{ - BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore, - BreakpointStoreEvent, + Breakpoint, BreakpointEditAction, BreakpointSessionState, BreakpointState, + BreakpointStore, BreakpointStoreEvent, }, session::{Session, SessionEvent}, }, git_store::{GitStoreEvent, RepositoryEvent}, lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, - project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter}, - project_settings::{GitGutterSetting, ProjectSettings}, + project_settings::{ + DiagnosticSeverity, GitGutterSetting, GoToDiagnosticSeverityFilter, ProjectSettings, + }, }; use rand::{seq::SliceRandom, thread_rng}; use rpc::{ErrorCode, ErrorExt, proto::PeerId}; @@ -5635,17 +5636,25 @@ impl Editor { // that having one source with `is_incomplete: true` doesn't cause all to be re-queried. let mut completions = Vec::new(); let mut is_incomplete = false; + let mut display_options: Option = None; if let Some(provider_responses) = provider_responses.await.log_err() && !provider_responses.is_empty() { for response in provider_responses { completions.extend(response.completions); is_incomplete = is_incomplete || response.is_incomplete; + match display_options.as_mut() { + None => { + display_options = Some(response.display_options); + } + Some(options) => options.merge(&response.display_options), + } } if completion_settings.words == WordsCompletionMode::Fallback { words = Task::ready(BTreeMap::default()); } } + let display_options = display_options.unwrap_or_default(); let mut words = words.await; if let Some(word_to_exclude) = &word_to_exclude { @@ -5687,6 +5696,7 @@ impl Editor { is_incomplete, buffer.clone(), completions.into(), + display_options, snippet_sort_order, languages, language, @@ -22260,6 +22270,7 @@ fn snippet_completions( if scopes.is_empty() { return Task::ready(Ok(CompletionResponse { completions: vec![], + display_options: CompletionDisplayOptions::default(), is_incomplete: false, })); } @@ -22284,6 +22295,7 @@ fn snippet_completions( if last_word.is_empty() { return Ok(CompletionResponse { completions: vec![], + display_options: CompletionDisplayOptions::default(), is_incomplete: true, }); } @@ -22405,6 +22417,7 @@ fn snippet_completions( Ok(CompletionResponse { completions, + display_options: CompletionDisplayOptions::default(), is_incomplete, }) }) diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs index c3d687e57adbb0b1883d81f6e7ba726d0d60974d..fa8b76517f0125e7319f035b41996e445451510a 100644 --- a/crates/inspector_ui/src/div_inspector.rs +++ b/crates/inspector_ui/src/div_inspector.rs @@ -14,7 +14,10 @@ use language::{ DiagnosticSeverity, LanguageServerId, Point, ToOffset as _, ToPoint as _, }; use project::lsp_store::CompletionDocumentation; -use project::{Completion, CompletionResponse, CompletionSource, Project, ProjectPath}; +use project::{ + Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource, Project, + ProjectPath, +}; use std::fmt::Write as _; use std::ops::Range; use std::path::Path; @@ -664,6 +667,7 @@ impl CompletionProvider for RustStyleCompletionProvider { confirm: None, }) .collect(), + display_options: CompletionDisplayOptions::default(), is_incomplete: false, }])) } diff --git a/crates/keymap_editor/src/keymap_editor.rs b/crates/keymap_editor/src/keymap_editor.rs index 12149061124d2b3144a32b7f54a65ce5af70d492..a8e356276b31e1d6daa79fdf85f6ff3566f9749d 100644 --- a/crates/keymap_editor/src/keymap_editor.rs +++ b/crates/keymap_editor/src/keymap_editor.rs @@ -22,7 +22,7 @@ use gpui::{ }; use language::{Language, LanguageConfig, ToOffset as _}; use notifications::status_toast::{StatusToast, ToastIcon}; -use project::Project; +use project::{CompletionDisplayOptions, Project}; use settings::{BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets}; use ui::{ ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator, @@ -2911,6 +2911,7 @@ impl CompletionProvider for KeyContextCompletionProvider { confirm: None, }) .collect(), + display_options: CompletionDisplayOptions::default(), is_incomplete: false, }])) } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 1315915203e917a7be8dd6e41031495971ddec24..73f5da086c78b3a3e597be3d32bdcb7a15649117 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -16,10 +16,10 @@ pub mod lsp_ext_command; pub mod rust_analyzer_ext; use crate::{ - CodeAction, ColorPresentation, Completion, CompletionResponse, CompletionSource, - CoreCompletion, DocumentColor, Hover, InlayHint, LocationLink, LspAction, LspPullDiagnostics, - ManifestProvidersStore, Project, ProjectItem, ProjectPath, ProjectTransaction, - PulledDiagnostics, ResolveState, Symbol, + CodeAction, ColorPresentation, Completion, CompletionDisplayOptions, CompletionResponse, + CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, LocationLink, LspAction, + LspPullDiagnostics, ManifestProvidersStore, Project, ProjectItem, ProjectPath, + ProjectTransaction, PulledDiagnostics, ResolveState, Symbol, buffer_store::{BufferStore, BufferStoreEvent}, environment::ProjectEnvironment, lsp_command::{self, *}, @@ -5828,6 +5828,7 @@ impl LspStore { .await; Ok(vec![CompletionResponse { completions, + display_options: CompletionDisplayOptions::default(), is_incomplete: completion_response.is_incomplete, }]) }) @@ -5920,6 +5921,7 @@ impl LspStore { .await; Some(CompletionResponse { completions, + display_options: CompletionDisplayOptions::default(), is_incomplete: completion_response.is_incomplete, }) }); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 229249d48c5ab370c4b354cda7cbf9312790759d..46dd3b7d9e51aa06aa45b9cccb87533f2b90f58c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -574,11 +574,23 @@ impl std::fmt::Debug for Completion { /// Response from a source of completions. pub struct CompletionResponse { pub completions: Vec, + pub display_options: CompletionDisplayOptions, /// When false, indicates that the list is complete and so does not need to be re-queried if it /// can be filtered instead. pub is_incomplete: bool, } +#[derive(Default)] +pub struct CompletionDisplayOptions { + pub dynamic_width: bool, +} + +impl CompletionDisplayOptions { + pub fn merge(&mut self, other: &CompletionDisplayOptions) { + self.dynamic_width = self.dynamic_width && other.dynamic_width; + } +} + /// Response from language server completion request. #[derive(Clone, Debug, Default)] pub(crate) struct CoreCompletionResponse { From 9f749881b31a2cd10fefb240068e6bb1bd46c709 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 3 Sep 2025 04:52:57 +0530 Subject: [PATCH 521/744] language_models: Fix tool_choice null issue for other providers (#34554) Follow up: #34532 Closes #35434 Mostly fixes a issue were when the tool_choice is none it was getting serialised as null. This was fixed for openrouter just wanted to follow up and cleanup for other providers which might have this issue as this is against the spec. Release Notes: - N/A --- crates/lmstudio/src/lmstudio.rs | 3 ++- crates/mistral/src/mistral.rs | 3 ++- crates/open_ai/src/open_ai.rs | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/lmstudio/src/lmstudio.rs b/crates/lmstudio/src/lmstudio.rs index 43c78115cdd4f517a51052991121620a0a93c363..ef2f7b6208f62e079609049b8eff83a80034741e 100644 --- a/crates/lmstudio/src/lmstudio.rs +++ b/crates/lmstudio/src/lmstudio.rs @@ -86,11 +86,12 @@ impl Model { } #[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] +#[serde(rename_all = "lowercase")] pub enum ToolChoice { Auto, Required, None, + #[serde(untagged)] Other(ToolDefinition), } diff --git a/crates/mistral/src/mistral.rs b/crates/mistral/src/mistral.rs index 55986e7e5bfd69ec91f11089753562e9e1984fcc..d6f62cfaa07bc211881817e6178a8673a9a670a6 100644 --- a/crates/mistral/src/mistral.rs +++ b/crates/mistral/src/mistral.rs @@ -286,12 +286,13 @@ pub enum Prediction { } #[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "lowercase")] pub enum ToolChoice { Auto, Required, None, Any, + #[serde(untagged)] Function(ToolDefinition), } diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index f9a983b433b9d918424f9696269dd0bbd72adefd..279245c0b7d5a545a5d5c7725347f0b5153a4deb 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -269,11 +269,12 @@ pub struct Request { } #[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] +#[serde(rename_all = "lowercase")] pub enum ToolChoice { Auto, Required, None, + #[serde(untagged)] Other(ToolDefinition), } From 63b3839a83984a48ef57e96253f3602c16cf22b3 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 3 Sep 2025 04:58:36 +0530 Subject: [PATCH 522/744] language_models: Prevent sending the tools object to unsupported models for Ollama (#37221) Closes #32758 Release Notes: - Resolved an issue with the Ollama provider that caused requests to fail with a 400 error for models that don't support tools. The tools object is now only sent to compatible models to ensure successful requests. --- crates/language_models/src/provider/ollama.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index 3f2d47fba35959e6c26205193dddba45c2df25cc..8975115d907875569f63e4247cf7edcdbcb91f8a 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -347,7 +347,11 @@ impl OllamaLanguageModel { .model .supports_thinking .map(|supports_thinking| supports_thinking && request.thinking_allowed), - tools: request.tools.into_iter().map(tool_into_ollama).collect(), + tools: if self.model.supports_tools.unwrap_or(false) { + request.tools.into_iter().map(tool_into_ollama).collect() + } else { + vec![] + }, } } } From 564ded71c144f441700323bfa3043390206421fe Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 2 Sep 2025 19:29:21 -0400 Subject: [PATCH 523/744] acp: Disable external agents over SSH (#37402) Follow-up to #37377 Show a clearer error here until SSH support is implemented. Release Notes: - N/A --- Cargo.toml | 2 +- crates/agent_ui/src/acp/thread_view.rs | 4 +--- crates/agent_ui/src/agent_panel.rs | 16 +++++++--------- crates/git_ui/src/branch_picker.rs | 1 - crates/vim/src/visual.rs | 1 - 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 90b01945f71264eb367239809c9811034a71f2d9..08551ef75a315563f9cdb22c0ed92742d550df61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -847,7 +847,7 @@ too_many_arguments = "allow" large_enum_variant = "allow" # Boolean expressions can be hard to read, requiring only the minimal form gets in the way -nonminimal_bol = "allow" +nonminimal_bool = "allow" [workspace.metadata.cargo-machete] ignored = [ diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 357a8543712eec5ea0723c4f36f05e4d6d5c0b9d..dc8abd99ae9e9afb07a5e3360e1216a07a528d01 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -420,9 +420,7 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) -> ThreadState { - if project.read(cx).is_via_collab() - && agent.clone().downcast::().is_none() - { + if !project.read(cx).is_local() && agent.clone().downcast::().is_none() { return ThreadState::LoadError(LoadError::Other( "External agents are not yet supported for remote projects.".into(), )); diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 3da63c281ef72765479d23cc508ef4ca50ff927c..2383963d6c26c6afea1b03d7f28b29bf3a9b4223 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1091,7 +1091,7 @@ impl AgentPanel { let workspace = self.workspace.clone(); let project = self.project.clone(); let fs = self.fs.clone(); - let is_via_collab = self.project.read(cx).is_via_collab(); + let is_not_local = !self.project.read(cx).is_local(); const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent"; @@ -1123,7 +1123,7 @@ impl AgentPanel { agent } None => { - if is_via_collab { + if is_not_local { ExternalAgent::NativeAgent } else { cx.background_spawn(async move { @@ -2532,10 +2532,8 @@ impl AgentPanel { .with_handle(self.new_thread_menu_handle.clone()) .menu({ let workspace = self.workspace.clone(); - let is_via_collab = workspace - .update(cx, |workspace, cx| { - workspace.project().read(cx).is_via_collab() - }) + let is_not_local = workspace + .update(cx, |workspace, cx| !workspace.project().read(cx).is_local()) .unwrap_or_default(); move |window, cx| { @@ -2627,7 +2625,7 @@ impl AgentPanel { ContextMenuEntry::new("New Gemini CLI Thread") .icon(IconName::AiGemini) .icon_color(Color::Muted) - .disabled(is_via_collab) + .disabled(is_not_local) .handler({ let workspace = workspace.clone(); move |window, cx| { @@ -2654,7 +2652,7 @@ impl AgentPanel { menu.item( ContextMenuEntry::new("New Claude Code Thread") .icon(IconName::AiClaude) - .disabled(is_via_collab) + .disabled(is_not_local) .icon_color(Color::Muted) .handler({ let workspace = workspace.clone(); @@ -2687,7 +2685,7 @@ impl AgentPanel { ContextMenuEntry::new(format!("New {} Thread", agent_name)) .icon(IconName::Terminal) .icon_color(Color::Muted) - .disabled(is_via_collab) + .disabled(is_not_local) .handler({ let workspace = workspace.clone(); let agent_name = agent_name.clone(); diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index fb56cdcc5def31765d26545fcfa79b5f8c44e884..2b5a543e932aeed0ca0f00e2596e7fa79a35fb9a 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -341,7 +341,6 @@ impl PickerDelegate for BranchListDelegate { }; picker .update(cx, |picker, _| { - #[allow(clippy::nonminimal_bool)] if !query.is_empty() && !matches .first() diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index fcce00f0c0ffee43f1b7980fcf9fe3a70f6e7794..c62712af311eb09f1203ecb54d939402f936b21c 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -216,7 +216,6 @@ impl Vim { // If the file ends with a newline (which is common) we don't do this. // so that if you go to the end of such a file you can use "up" to go // to the previous line and have it work somewhat as expected. - #[allow(clippy::nonminimal_bool)] if !selection.reversed && !selection.is_empty() && !(selection.end.column() == 0 && selection.end == map.max_point()) From 45fa6d81acaa2882eebdcbffbf5e586fe7e8dee0 Mon Sep 17 00:00:00 2001 From: Vitaly Slobodin Date: Wed, 3 Sep 2025 01:32:43 +0200 Subject: [PATCH 525/744] tailwind: Add `HTML+ERB` to the list of supported languages (#36797) Hi! As part of https://github.com/zed-extensions/ruby/issues/162 we would like to rename HTML/ERB to HTML+ERB since it is more syntactically correct to treat such language as ERB on top of HTML rather than HTML or ERB. To keep the user experience intact, we outlined the prerequisites in the linked issue. This is the first PR that adds the HTML+ERB language name to the list of enabled languages for the Emmet extension. We will do the same for the Tailwind configuration in the Zed codebase. Once the new versions of Emmet and Zed are released, we will merge the pull request in the Ruby extension repository and release the updated version. After that, we will remove the old HTML/ERB and YAML/ERB languages. Let me know if that sounds good. Thanks! Release Notes: - N/A Co-authored-by: Marshall Bowers --- crates/languages/src/lib.rs | 1 + crates/languages/src/tailwind.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index d391e67d33fe90ac7e678c1458b8985e221fafc3..168cf8f57ca25444e54c11bb8e594faa94726b5d 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -296,6 +296,7 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { "Astro", "CSS", "ERB", + "HTML+ERB", "HTML/ERB", "HEEX", "HTML", diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 47eb25405339b81b3c458b2443d4af77da883cf0..7215dc0d591daae93ca0ee37043bc1372fa32cd2 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -184,6 +184,7 @@ impl LspAdapter for TailwindLspAdapter { (LanguageName::new("Elixir"), "phoenix-heex".to_string()), (LanguageName::new("HEEX"), "phoenix-heex".to_string()), (LanguageName::new("ERB"), "erb".to_string()), + (LanguageName::new("HTML+ERB"), "erb".to_string()), (LanguageName::new("HTML/ERB"), "erb".to_string()), (LanguageName::new("PHP"), "php".to_string()), (LanguageName::new("Vue.js"), "vue".to_string()), From 893eb92f9185d864af0503936b8d2ac448d98cce Mon Sep 17 00:00:00 2001 From: versecafe <147033096+versecafe@users.noreply.github.com> Date: Tue, 2 Sep 2025 16:40:07 -0700 Subject: [PATCH 526/744] docs: Note edge case for macOS 26 (#37392) - I believe this is caused by metal not being found due to it being on the XcodeBeta path, not sure if there's a better fix for this but it'll work until 26 is the latest release Release Notes: - N/A --- crates/gpui/README.md | 2 +- docs/src/development/macos.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/gpui/README.md b/crates/gpui/README.md index 9faab7b6801873f531f8138e375cdad73fc23dc4..672d83e8ff0d72f641598d1a0b2c69a98650d45c 100644 --- a/crates/gpui/README.md +++ b/crates/gpui/README.md @@ -23,7 +23,7 @@ On macOS, GPUI uses Metal for rendering. In order to use Metal, you need to do t - Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account. -> Ensure you launch XCode after installing, and install the macOS components, which is the default option. +> Ensure you launch Xcode after installing, and install the macOS components, which is the default option. If you are on macOS 26 (Tahoe) you will need to use `--features gpui/runtime_shaders` or add the feature in the root `Cargo.toml` - Install [Xcode command line tools](https://developer.apple.com/xcode/resources/) diff --git a/docs/src/development/macos.md b/docs/src/development/macos.md index f081f0b5f12b11e57ee9c82e38be03c16292311e..d90cc89abc3e5dc70b2184c85690ea472ec647dc 100644 --- a/docs/src/development/macos.md +++ b/docs/src/development/macos.md @@ -10,7 +10,7 @@ Clone down the [Zed repository](https://github.com/zed-industries/zed). - Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account. -> Ensure you launch Xcode after installing, and install the macOS components, which is the default option. +> Ensure you launch Xcode after installing, and install the macOS components, which is the default option. If you are on macOS 26 (Tahoe) you will need to use `--features gpui/runtime_shaders` or add the feature in the root `Cargo.toml` - Install [Xcode command line tools](https://developer.apple.com/xcode/resources/) From ae0ee70abddb7883bd5e9610d063508ce5ad532d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Krzywa=C5=BCnia?= <1815898+ribelo@users.noreply.github.com> Date: Wed, 3 Sep 2025 02:03:56 +0200 Subject: [PATCH 527/744] Add configurable timeout for context server tool calls (#33348) Closes: #32668 - Add [tool_call_timeout_millis](https://github.com/cline/cline/pull/1904) field to ContextServerCommand, like in Cline - Update ModelContextServerBinary to include timeout configuration - Modify Client to store and use configurable request timeout - Replace hardcoded REQUEST_TIMEOUT with self.request_timeout - Rename REQUEST_TIMEOUT to DEFAULT_REQUEST_TIMEOUT for clarity - Maintain backward compatibility with 60-second default Release Notes: - context_server: Add support for configurable timeout for MCP tool calls --------- Co-authored-by: Ben Brandt --- crates/agent2/src/tests/mod.rs | 1 + crates/context_server/src/client.rs | 18 ++++++++++++++---- crates/context_server/src/context_server.rs | 4 ++++ crates/project/src/context_server_store.rs | 7 +++++++ .../src/context_server_store/extension.rs | 1 + crates/project/src/project_settings.rs | 1 + 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 9132c9a316e7e5122d24a0b413ae700a7adbaba9..884580ed69009d168b3266870acf4f698a2f5450 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -2477,6 +2477,7 @@ fn setup_context_server( path: "somebinary".into(), args: Vec::new(), env: None, + timeout: None, }, }, ); diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index 03cf047ac5273071354756119864ab6914e524c6..b3b44dbde67d92ce620d85a39a0925f27a4e2086 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -25,7 +25,7 @@ use crate::{ }; const JSON_RPC_VERSION: &str = "2.0"; -const REQUEST_TIMEOUT: Duration = Duration::from_secs(60); +const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(60); // Standard JSON-RPC error codes pub const PARSE_ERROR: i32 = -32700; @@ -60,6 +60,7 @@ pub(crate) struct Client { executor: BackgroundExecutor, #[allow(dead_code)] transport: Arc, + request_timeout: Option, } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -143,6 +144,7 @@ pub struct ModelContextServerBinary { pub executable: PathBuf, pub args: Vec, pub env: Option>, + pub timeout: Option, } impl Client { @@ -169,8 +171,9 @@ impl Client { .map(|name| name.to_string_lossy().to_string()) .unwrap_or_else(String::new); + let timeout = binary.timeout.map(Duration::from_millis); let transport = Arc::new(StdioTransport::new(binary, working_directory, &cx)?); - Self::new(server_id, server_name.into(), transport, cx) + Self::new(server_id, server_name.into(), transport, timeout, cx) } /// Creates a new Client instance for a context server. @@ -178,6 +181,7 @@ impl Client { server_id: ContextServerId, server_name: Arc, transport: Arc, + request_timeout: Option, cx: AsyncApp, ) -> Result { let (outbound_tx, outbound_rx) = channel::unbounded::(); @@ -237,6 +241,7 @@ impl Client { io_tasks: Mutex::new(Some((input_task, output_task))), output_done_rx: Mutex::new(Some(output_done_rx)), transport, + request_timeout, }) } @@ -327,8 +332,13 @@ impl Client { method: &str, params: impl Serialize, ) -> Result { - self.request_with(method, params, None, Some(REQUEST_TIMEOUT)) - .await + self.request_with( + method, + params, + None, + self.request_timeout.or(Some(DEFAULT_REQUEST_TIMEOUT)), + ) + .await } pub async fn request_with( diff --git a/crates/context_server/src/context_server.rs b/crates/context_server/src/context_server.rs index 9ca78138dbc229a6aedd5c53c460d9205502df94..b126bb393784664692b5de39fee5ed7f66e9948a 100644 --- a/crates/context_server/src/context_server.rs +++ b/crates/context_server/src/context_server.rs @@ -34,6 +34,8 @@ pub struct ContextServerCommand { pub path: PathBuf, pub args: Vec, pub env: Option>, + /// Timeout for tool calls in milliseconds. Defaults to 60000 (60 seconds) if not specified. + pub timeout: Option, } impl std::fmt::Debug for ContextServerCommand { @@ -123,6 +125,7 @@ impl ContextServer { executable: Path::new(&command.path).to_path_buf(), args: command.args.clone(), env: command.env.clone(), + timeout: command.timeout, }, working_directory, cx.clone(), @@ -131,6 +134,7 @@ impl ContextServer { client::ContextServerId(self.id.0.clone()), self.id().0, transport.clone(), + None, cx.clone(), )?, }) diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index 49a430c26110fc58a9494d414ecbcf45a6c76c49..20188df5c4ae38b2ae305daee5b3eecc25319951 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -976,6 +976,7 @@ mod tests { path: "somebinary".into(), args: vec!["arg".to_string()], env: None, + timeout: None, }, }, ), @@ -1016,6 +1017,7 @@ mod tests { path: "somebinary".into(), args: vec!["anotherArg".to_string()], env: None, + timeout: None, }, }, ), @@ -1098,6 +1100,7 @@ mod tests { path: "somebinary".into(), args: vec!["arg".to_string()], env: None, + timeout: None, }, }, )], @@ -1150,6 +1153,7 @@ mod tests { path: "somebinary".into(), args: vec!["arg".to_string()], env: None, + timeout: None, }, }, )], @@ -1177,6 +1181,7 @@ mod tests { command: ContextServerCommand { path: "somebinary".into(), args: vec!["arg".to_string()], + timeout: None, env: None, }, }, @@ -1230,6 +1235,7 @@ mod tests { path: "somebinary".into(), args: vec!["arg".to_string()], env: None, + timeout: None, }, } } @@ -1318,6 +1324,7 @@ mod tests { path: self.path.clone(), args: vec!["arg1".to_string(), "arg2".to_string()], env: None, + timeout: None, })) } diff --git a/crates/project/src/context_server_store/extension.rs b/crates/project/src/context_server_store/extension.rs index 2a3a0c2e4b99e56c66993d0db1fbec5b3fb9ef29..ca5cacf3b549523dee8b85242bea86653eecbf7a 100644 --- a/crates/project/src/context_server_store/extension.rs +++ b/crates/project/src/context_server_store/extension.rs @@ -69,6 +69,7 @@ impl registry::ContextServerDescriptor for ContextServerDescriptor { path: command.command, args: command.args, env: Some(command.env.into_iter().collect()), + timeout: None, }) }) } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 4a97130f15d582df392c25d6d64482bc4ca17834..40874638111eb3b85d4f65ad0b531072ab082624 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -604,6 +604,7 @@ impl Settings for ProjectSettings { path: cmd.command, args: cmd.args.unwrap_or_default(), env: cmd.env, + timeout: None, } } } From e1b0a98c348870fa4ccbda82b10acf4d4b8035d9 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 2 Sep 2025 20:24:00 -0400 Subject: [PATCH 528/744] ci: Remove Windows crash analysis CI scripts (#36694) We'll just SSH into the Windows runners and look for crashes there. Reverts #35926 Release Notes: - N/A --------- Co-authored-by: Peter Tripp --- .github/actions/run_tests_windows/action.yml | 159 ------------------- 1 file changed, 159 deletions(-) diff --git a/.github/actions/run_tests_windows/action.yml b/.github/actions/run_tests_windows/action.yml index 0a550c7d32b823db22cf205cf991020182a7d3b5..8392ca1d375856c7f649e73d2445ce4f873924b1 100644 --- a/.github/actions/run_tests_windows/action.yml +++ b/.github/actions/run_tests_windows/action.yml @@ -20,167 +20,8 @@ runs: with: node-version: "18" - - name: Configure crash dumps - shell: powershell - run: | - # Record the start time for this CI run - $runStartTime = Get-Date - $runStartTimeStr = $runStartTime.ToString("yyyy-MM-dd HH:mm:ss") - Write-Host "CI run started at: $runStartTimeStr" - - # Save the timestamp for later use - echo "CI_RUN_START_TIME=$($runStartTime.Ticks)" >> $env:GITHUB_ENV - - # Create crash dump directory in workspace (non-persistent) - $dumpPath = "$env:GITHUB_WORKSPACE\crash_dumps" - New-Item -ItemType Directory -Force -Path $dumpPath | Out-Null - - Write-Host "Setting up crash dump detection..." - Write-Host "Workspace dump path: $dumpPath" - - # Note: We're NOT modifying registry on stateful runners - # Instead, we'll check default Windows crash locations after tests - - name: Run tests shell: powershell working-directory: ${{ inputs.working-directory }} run: | - $env:RUST_BACKTRACE = "full" - - # Enable Windows debugging features - $env:_NT_SYMBOL_PATH = "srv*https://msdl.microsoft.com/download/symbols" - - # .NET crash dump environment variables (ephemeral) - $env:COMPlus_DbgEnableMiniDump = "1" - $env:COMPlus_DbgMiniDumpType = "4" - $env:COMPlus_CreateDumpDiagnostics = "1" - cargo nextest run --workspace --no-fail-fast - - - name: Analyze crash dumps - if: always() - shell: powershell - run: | - Write-Host "Checking for crash dumps..." - - # Get the CI run start time from the environment - $runStartTime = [DateTime]::new([long]$env:CI_RUN_START_TIME) - Write-Host "Only analyzing dumps created after: $($runStartTime.ToString('yyyy-MM-dd HH:mm:ss'))" - - # Check all possible crash dump locations - $searchPaths = @( - "$env:GITHUB_WORKSPACE\crash_dumps", - "$env:LOCALAPPDATA\CrashDumps", - "$env:TEMP", - "$env:GITHUB_WORKSPACE", - "$env:USERPROFILE\AppData\Local\CrashDumps", - "C:\Windows\System32\config\systemprofile\AppData\Local\CrashDumps" - ) - - $dumps = @() - foreach ($path in $searchPaths) { - if (Test-Path $path) { - Write-Host "Searching in: $path" - $found = Get-ChildItem "$path\*.dmp" -ErrorAction SilentlyContinue | Where-Object { - $_.CreationTime -gt $runStartTime - } - if ($found) { - $dumps += $found - Write-Host " Found $($found.Count) dump(s) from this CI run" - } - } - } - - if ($dumps) { - Write-Host "Found $($dumps.Count) crash dump(s)" - - # Install debugging tools if not present - $cdbPath = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe" - if (-not (Test-Path $cdbPath)) { - Write-Host "Installing Windows Debugging Tools..." - $url = "https://go.microsoft.com/fwlink/?linkid=2237387" - Invoke-WebRequest -Uri $url -OutFile winsdksetup.exe - Start-Process -Wait winsdksetup.exe -ArgumentList "/features OptionId.WindowsDesktopDebuggers /quiet" - } - - foreach ($dump in $dumps) { - Write-Host "`n==================================" - Write-Host "Analyzing crash dump: $($dump.Name)" - Write-Host "Size: $([math]::Round($dump.Length / 1MB, 2)) MB" - Write-Host "Time: $($dump.CreationTime)" - Write-Host "==================================" - - # Set symbol path - $env:_NT_SYMBOL_PATH = "srv*C:\symbols*https://msdl.microsoft.com/download/symbols" - - # Run analysis - $analysisOutput = & $cdbPath -z $dump.FullName -c "!analyze -v; ~*k; lm; q" 2>&1 | Out-String - - # Extract key information - if ($analysisOutput -match "ExceptionCode:\s*([\w]+)") { - Write-Host "Exception Code: $($Matches[1])" - if ($Matches[1] -eq "c0000005") { - Write-Host "Exception Type: ACCESS VIOLATION" - } - } - - if ($analysisOutput -match "EXCEPTION_RECORD:\s*(.+)") { - Write-Host "Exception Record: $($Matches[1])" - } - - if ($analysisOutput -match "FAULTING_IP:\s*\n(.+)") { - Write-Host "Faulting Instruction: $($Matches[1])" - } - - # Save full analysis - $analysisFile = "$($dump.FullName).analysis.txt" - $analysisOutput | Out-File -FilePath $analysisFile - Write-Host "`nFull analysis saved to: $analysisFile" - - # Print stack trace section - Write-Host "`n--- Stack Trace Preview ---" - $stackSection = $analysisOutput -split "STACK_TEXT:" | Select-Object -Last 1 - $stackLines = $stackSection -split "`n" | Select-Object -First 20 - $stackLines | ForEach-Object { Write-Host $_ } - Write-Host "--- End Stack Trace Preview ---" - } - - Write-Host "`n⚠️ Crash dumps detected! Download the 'crash-dumps' artifact for detailed analysis." - - # Copy dumps to workspace for artifact upload - $artifactPath = "$env:GITHUB_WORKSPACE\crash_dumps_collected" - New-Item -ItemType Directory -Force -Path $artifactPath | Out-Null - - foreach ($dump in $dumps) { - $destName = "$($dump.Directory.Name)_$($dump.Name)" - Copy-Item $dump.FullName -Destination "$artifactPath\$destName" - if (Test-Path "$($dump.FullName).analysis.txt") { - Copy-Item "$($dump.FullName).analysis.txt" -Destination "$artifactPath\$destName.analysis.txt" - } - } - - Write-Host "Copied $($dumps.Count) dump(s) to artifact directory" - } else { - Write-Host "No crash dumps from this CI run found" - } - - - name: Upload crash dumps - if: always() - uses: actions/upload-artifact@v4 - with: - name: crash-dumps-${{ github.run_id }}-${{ github.run_attempt }} - path: | - crash_dumps_collected/*.dmp - crash_dumps_collected/*.txt - if-no-files-found: ignore - retention-days: 7 - - - name: Check test results - shell: powershell - working-directory: ${{ inputs.working-directory }} - run: | - # Re-check test results to fail the job if tests failed - if ($LASTEXITCODE -ne 0) { - Write-Host "Tests failed with exit code: $LASTEXITCODE" - exit $LASTEXITCODE - } From 161d128d45e9d99c0629917f0589ce6ac1e4d000 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Tue, 2 Sep 2025 20:25:10 -0400 Subject: [PATCH 529/744] Handle model refusal in ACP threads (#37383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the model refuses a prompt, we now: * Show an error if it was a user prompt (and truncate it out of the history) * Respond with a failed tool call if the refusal was for a tool call Screenshot 2025-09-02 at 5 11 45 PM Screenshot 2025-09-02 at 5 11 38 PM Release Notes: - Improve handling of model refusals in ACP threads --- crates/acp_thread/src/acp_thread.rs | 223 +++++++++++++++++- .../src/acp/model_selector_popover.rs | 8 + crates/agent_ui/src/acp/thread_view.rs | 137 +++++++++++ crates/agent_ui/src/active_thread.rs | 16 +- crates/agent_ui/src/agent_diff.rs | 5 +- crates/agent_ui/src/agent_panel.rs | 9 +- 6 files changed, 387 insertions(+), 11 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index f9a955eb9f5c7f5cd5ab077ed3c3afd9dfcd4b8b..804e4683a7cc20ac2bcd80f10139d641aa864b98 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -804,6 +804,7 @@ pub enum AcpThreadEvent { Error, LoadError(LoadError), PromptCapabilitiesUpdated, + Refusal, } impl EventEmitter for AcpThread {} @@ -1569,15 +1570,42 @@ impl AcpThread { this.send_task.take(); } - // Truncate entries if the last prompt was refused. + // Handle refusal - distinguish between user prompt and tool call refusals if let Ok(Ok(acp::PromptResponse { stop_reason: acp::StopReason::Refusal, })) = result - && let Some((ix, _)) = this.last_user_message() { - let range = ix..this.entries.len(); - this.entries.truncate(ix); - cx.emit(AcpThreadEvent::EntriesRemoved(range)); + if let Some((user_msg_ix, _)) = this.last_user_message() { + // Check if there's a completed tool call with results after the last user message + // This indicates the refusal is in response to tool output, not the user's prompt + let has_completed_tool_call_after_user_msg = + this.entries.iter().skip(user_msg_ix + 1).any(|entry| { + if let AgentThreadEntry::ToolCall(tool_call) = entry { + // Check if the tool call has completed and has output + matches!(tool_call.status, ToolCallStatus::Completed) + && tool_call.raw_output.is_some() + } else { + false + } + }); + + if has_completed_tool_call_after_user_msg { + // Refusal is due to tool output - don't truncate, just notify + // The model refused based on what the tool returned + cx.emit(AcpThreadEvent::Refusal); + } else { + // User prompt was refused - truncate back to before the user message + let range = user_msg_ix..this.entries.len(); + if range.start < range.end { + this.entries.truncate(user_msg_ix); + cx.emit(AcpThreadEvent::EntriesRemoved(range)); + } + cx.emit(AcpThreadEvent::Refusal); + } + } else { + // No user message found, treat as general refusal + cx.emit(AcpThreadEvent::Refusal); + } } cx.emit(AcpThreadEvent::Stopped); @@ -2681,6 +2709,187 @@ mod tests { assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]); } + #[gpui::test] + async fn test_tool_result_refusal(cx: &mut TestAppContext) { + use std::sync::atomic::AtomicUsize; + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, None, cx).await; + + // Create a connection that simulates refusal after tool result + let prompt_count = Arc::new(AtomicUsize::new(0)); + let connection = Rc::new(FakeAgentConnection::new().on_user_message({ + let prompt_count = prompt_count.clone(); + move |_request, thread, mut cx| { + let count = prompt_count.fetch_add(1, SeqCst); + async move { + if count == 0 { + // First prompt: Generate a tool call with result + thread.update(&mut cx, |thread, cx| { + thread + .handle_session_update( + acp::SessionUpdate::ToolCall(acp::ToolCall { + id: acp::ToolCallId("tool1".into()), + title: "Test Tool".into(), + kind: acp::ToolKind::Fetch, + status: acp::ToolCallStatus::Completed, + content: vec![], + locations: vec![], + raw_input: Some(serde_json::json!({"query": "test"})), + raw_output: Some( + serde_json::json!({"result": "inappropriate content"}), + ), + }), + cx, + ) + .unwrap(); + })?; + + // Now return refusal because of the tool result + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Refusal, + }) + } else { + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + } + } + .boxed_local() + } + })); + + let thread = cx + .update(|cx| connection.new_thread(project, Path::new("/test"), cx)) + .await + .unwrap(); + + // Track if we see a Refusal event + let saw_refusal_event = Arc::new(std::sync::Mutex::new(false)); + let saw_refusal_event_captured = saw_refusal_event.clone(); + thread.update(cx, |_thread, cx| { + cx.subscribe( + &thread, + move |_thread, _event_thread, event: &AcpThreadEvent, _cx| { + if matches!(event, AcpThreadEvent::Refusal) { + *saw_refusal_event_captured.lock().unwrap() = true; + } + }, + ) + .detach(); + }); + + // Send a user message - this will trigger tool call and then refusal + let send_task = thread.update(cx, |thread, cx| { + thread.send( + vec![acp::ContentBlock::Text(acp::TextContent { + text: "Hello".into(), + annotations: None, + })], + cx, + ) + }); + cx.background_executor.spawn(send_task).detach(); + cx.run_until_parked(); + + // Verify that: + // 1. A Refusal event WAS emitted (because it's a tool result refusal, not user prompt) + // 2. The user message was NOT truncated + assert!( + *saw_refusal_event.lock().unwrap(), + "Refusal event should be emitted for tool result refusals" + ); + + thread.read_with(cx, |thread, _| { + let entries = thread.entries(); + assert!(entries.len() >= 2, "Should have user message and tool call"); + + // Verify user message is still there + assert!( + matches!(entries[0], AgentThreadEntry::UserMessage(_)), + "User message should not be truncated" + ); + + // Verify tool call is there with result + if let AgentThreadEntry::ToolCall(tool_call) = &entries[1] { + assert!( + tool_call.raw_output.is_some(), + "Tool call should have output" + ); + } else { + panic!("Expected tool call at index 1"); + } + }); + } + + #[gpui::test] + async fn test_user_prompt_refusal_emits_event(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, None, cx).await; + + let refuse_next = Arc::new(AtomicBool::new(false)); + let connection = Rc::new(FakeAgentConnection::new().on_user_message({ + let refuse_next = refuse_next.clone(); + move |_request, _thread, _cx| { + if refuse_next.load(SeqCst) { + async move { + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Refusal, + }) + } + .boxed_local() + } else { + async move { + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + } + .boxed_local() + } + } + })); + + let thread = cx + .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) + .await + .unwrap(); + + // Track if we see a Refusal event + let saw_refusal_event = Arc::new(std::sync::Mutex::new(false)); + let saw_refusal_event_captured = saw_refusal_event.clone(); + thread.update(cx, |_thread, cx| { + cx.subscribe( + &thread, + move |_thread, _event_thread, event: &AcpThreadEvent, _cx| { + if matches!(event, AcpThreadEvent::Refusal) { + *saw_refusal_event_captured.lock().unwrap() = true; + } + }, + ) + .detach(); + }); + + // Send a message that will be refused + refuse_next.store(true, SeqCst); + cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx))) + .await + .unwrap(); + + // Verify that a Refusal event WAS emitted for user prompt refusal + assert!( + *saw_refusal_event.lock().unwrap(), + "Refusal event should be emitted for user prompt refusals" + ); + + // Verify the message was truncated (user prompt refusal) + thread.read_with(cx, |thread, cx| { + assert_eq!(thread.to_markdown(cx), ""); + }); + } + #[gpui::test] async fn test_refusal(cx: &mut TestAppContext) { init_test(cx); @@ -2744,8 +2953,8 @@ mod tests { ); }); - // Simulate refusing the second message, ensuring the conversation gets - // truncated to before sending it. + // Simulate refusing the second message. The message should be truncated + // when a user prompt is refused. refuse_next.store(true, SeqCst); cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["world".into()], cx))) .await diff --git a/crates/agent_ui/src/acp/model_selector_popover.rs b/crates/agent_ui/src/acp/model_selector_popover.rs index e52101113a61c7379be54e25f1784ac16b660200..e8e0d4be7f9dd06f2a7b98761dc2b6287f968ba4 100644 --- a/crates/agent_ui/src/acp/model_selector_popover.rs +++ b/crates/agent_ui/src/acp/model_selector_popover.rs @@ -36,6 +36,14 @@ impl AcpModelSelectorPopover { pub fn toggle(&self, window: &mut Window, cx: &mut Context) { self.menu_handle.toggle(window, cx); } + + pub fn active_model_name(&self, cx: &App) -> Option { + self.selector + .read(cx) + .delegate + .active_model() + .map(|model| model.name.clone()) + } } impl Render for AcpModelSelectorPopover { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index dc8abd99ae9e9afb07a5e3360e1216a07a528d01..60b3166a57aebc02fba82d4b350de0e48b84ef94 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -78,10 +78,12 @@ enum ThreadFeedback { Negative, } +#[derive(Debug)] enum ThreadError { PaymentRequired, ModelRequestLimitReached(cloud_llm_client::Plan), ToolUseLimitReached, + Refusal, AuthenticationRequired(SharedString), Other(SharedString), } @@ -1255,6 +1257,14 @@ impl AcpThreadView { cx, ); } + AcpThreadEvent::Refusal => { + self.thread_retry_status.take(); + self.thread_error = Some(ThreadError::Refusal); + let model_or_agent_name = self.get_current_model_name(cx); + let notification_message = + format!("{} refused to respond to this request", model_or_agent_name); + self.notify_with_sound(¬ification_message, IconName::Warning, window, cx); + } AcpThreadEvent::Error => { self.thread_retry_status.take(); self.notify_with_sound( @@ -4740,6 +4750,7 @@ impl AcpThreadView { fn render_thread_error(&self, window: &mut Window, cx: &mut Context) -> Option
{ let content = match self.thread_error.as_ref()? { ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx), + ThreadError::Refusal => self.render_refusal_error(cx), ThreadError::AuthenticationRequired(error) => { self.render_authentication_required_error(error.clone(), cx) } @@ -4755,6 +4766,43 @@ impl AcpThreadView { Some(div().child(content)) } + fn get_current_model_name(&self, cx: &App) -> SharedString { + // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet") + // For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI") + // This provides better clarity about what refused the request + if self + .agent + .clone() + .downcast::() + .is_some() + { + // Native agent - use the model name + self.model_selector + .as_ref() + .and_then(|selector| selector.read(cx).active_model_name(cx)) + .unwrap_or_else(|| SharedString::from("The model")) + } else { + // ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI") + self.agent.name() + } + } + + fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout { + let model_or_agent_name = self.get_current_model_name(cx); + let refusal_message = format!( + "{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.", + model_or_agent_name + ); + + Callout::new() + .severity(Severity::Error) + .title("Request Refused") + .icon(IconName::XCircle) + .description(refusal_message.clone()) + .actions_slot(self.create_copy_button(&refusal_message)) + .dismiss_action(self.dismiss_error_button(cx)) + } + fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout { let can_resume = self .thread() @@ -5382,6 +5430,33 @@ pub(crate) mod tests { ); } + #[gpui::test] + async fn test_refusal_handling(cx: &mut TestAppContext) { + init_test(cx); + + let (thread_view, cx) = + setup_thread_view(StubAgentServer::new(RefusalAgentConnection), cx).await; + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Do something harmful", window, cx); + }); + + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.send(window, cx); + }); + + cx.run_until_parked(); + + // Check that the refusal error is set + thread_view.read_with(cx, |thread_view, _cx| { + assert!( + matches!(thread_view.thread_error, Some(ThreadError::Refusal)), + "Expected refusal error to be set" + ); + }); + } + #[gpui::test] async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) { init_test(cx); @@ -5617,6 +5692,68 @@ pub(crate) mod tests { } } + /// Simulates a model which always returns a refusal response + #[derive(Clone)] + struct RefusalAgentConnection; + + impl AgentConnection for RefusalAgentConnection { + fn new_thread( + self: Rc, + project: Entity, + _cwd: &Path, + cx: &mut gpui::App, + ) -> Task>> { + Task::ready(Ok(cx.new(|cx| { + let action_log = cx.new(|_| ActionLog::new(project.clone())); + AcpThread::new( + "RefusalAgentConnection", + self, + project, + action_log, + SessionId("test".into()), + watch::Receiver::constant(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }), + Vec::new(), + cx, + ) + }))) + } + + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] + } + + fn authenticate( + &self, + _method_id: acp::AuthMethodId, + _cx: &mut App, + ) -> Task> { + unimplemented!() + } + + fn prompt( + &self, + _id: Option, + _params: acp::PromptRequest, + _cx: &mut App, + ) -> Task> { + Task::ready(Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Refusal, + })) + } + + fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { + unimplemented!() + } + + fn into_any(self: Rc) -> Rc { + self + } + } + pub(crate) fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 371a59e7eb9eb88dc5200251f971ef851162b630..fbba3eaffdd818bd1496b83f9f3081cbf52735ed 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1001,8 +1001,22 @@ impl ActiveThread { // Don't notify for intermediate tool use } Ok(StopReason::Refusal) => { + let model_name = self + .thread + .read(cx) + .configured_model() + .map(|configured| configured.model.name().0.to_string()) + .unwrap_or_else(|| "The model".to_string()); + let refusal_message = format!( + "{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.", + model_name + ); + self.last_error = Some(ThreadError::Message { + header: SharedString::from("Request Refused"), + message: SharedString::from(refusal_message), + }); self.notify_with_sound( - "Language model refused to respond", + format!("{} refused to respond", model_name), IconName::Warning, window, cx, diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 74bcb266d52ac25c91f3243c3e76f1e1f25d770e..f9d7321ca8dd72b791a462d50f262ce0f5531fd5 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1517,7 +1517,10 @@ impl AgentDiff { self.update_reviewing_editors(workspace, window, cx); } } - AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) => { + AcpThreadEvent::Stopped + | AcpThreadEvent::Error + | AcpThreadEvent::LoadError(_) + | AcpThreadEvent::Refusal => { self.update_reviewing_editors(workspace, window, cx); } AcpThreadEvent::TitleUpdated diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 2383963d6c26c6afea1b03d7f28b29bf3a9b4223..cfa5b56358863ece6ab1f6dd024e7be365766853 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -3532,6 +3532,11 @@ impl AgentPanel { ) -> AnyElement { let message_with_header = format!("{}\n{}", header, message); + // Don't show Retry button for refusals + let is_refusal = header == "Request Refused"; + let retry_button = self.render_retry_button(thread); + let copy_button = self.create_copy_button(message_with_header); + Callout::new() .severity(Severity::Error) .icon(IconName::XCircle) @@ -3540,8 +3545,8 @@ impl AgentPanel { .actions_slot( h_flex() .gap_0p5() - .child(self.render_retry_button(thread)) - .child(self.create_copy_button(message_with_header)), + .when(!is_refusal, |this| this.child(retry_button)) + .child(copy_button), ) .dismiss_action(self.dismiss_error_button(thread, cx)) .into_any_element() From 9d6727609070196b81cb675fc98a5edd248a531a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 2 Sep 2025 21:28:35 -0300 Subject: [PATCH 530/744] agent: Fix cut off slash command descriptions (#37408) Release Notes: - N/A --- crates/agent_ui/src/acp/completion_provider.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 6d2253b40686baf6719dfc00df72a505842c4bc9..44e81433ab5a9d904f329e238b24960e2d568750 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -733,7 +733,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { replace_range: source_range.clone(), new_text, label: CodeLabel::plain(command.name.to_string(), None), - documentation: Some(CompletionDocumentation::SingleLine( + documentation: Some(CompletionDocumentation::MultiLinePlainText( command.description.into(), )), source: project::CompletionSource::Custom, @@ -772,7 +772,9 @@ impl CompletionProvider for ContextPickerCompletionProvider { Ok(vec![CompletionResponse { completions, - display_options: CompletionDisplayOptions::default(), + display_options: CompletionDisplayOptions { + dynamic_width: true, + }, // Since this does its own filtering (see `filter_completions()` returns false), // there is no benefit to computing whether this set of completions is incomplete. is_incomplete: true, @@ -864,7 +866,9 @@ impl CompletionProvider for ContextPickerCompletionProvider { Ok(vec![CompletionResponse { completions, - display_options: CompletionDisplayOptions::default(), + display_options: CompletionDisplayOptions { + dynamic_width: true, + }, // Since this does its own filtering (see `filter_completions()` returns false), // there is no benefit to computing whether this set of completions is incomplete. is_incomplete: true, From 035d7ddcf89e80d3445b61444f14fbd2dc6d75b0 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 2 Sep 2025 20:37:40 -0400 Subject: [PATCH 531/744] ci: Skip Nix for commits on release branches and tags (#37407) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When doing stable/preview releases simultaneously there are two tags and two branches pushed. Previously nix was attempting 1 job for each. Our current mac parallelism is 4. Can't easily test this. 🤷 Release Notes: - N/A --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a34833d0fddd8ce53e1b06d839d97987b688edfb..d416b4af0eedf38da249e39181bd8017b57f752c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,7 @@ jobs: echo "run_license=false" >> "$GITHUB_OUTPUT" echo "$CHANGED_FILES" | grep -qP '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' && \ + echo "$GITHUB_REF_NAME" | grep -qvP '^v[0-9]+\.[0-9]+\.[0-9x](-pre)?$' && \ echo "run_nix=true" >> "$GITHUB_OUTPUT" || \ echo "run_nix=false" >> "$GITHUB_OUTPUT" From 7ea7f4e76765cb3337b1efe4656fbe1c26a9791a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 2 Sep 2025 20:52:04 -0400 Subject: [PATCH 532/744] reqwest_client: Remove example (#37410) This PR removes the example from the `reqwest_client` crate, as it doesn't seem worth maintaining. Release Notes: - N/A --- Cargo.lock | 1 - crates/reqwest_client/Cargo.toml | 5 --- crates/reqwest_client/examples/client.rs | 41 ------------------------ 3 files changed, 47 deletions(-) delete mode 100644 crates/reqwest_client/examples/client.rs diff --git a/Cargo.lock b/Cargo.lock index 42e343e062958a99c5e242ebfb0c5fc1516d694d..50e57005fd1796af9a53edc9a2fc62539c61a948 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13748,7 +13748,6 @@ dependencies = [ "regex", "reqwest 0.12.15 (git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415)", "serde", - "smol", "tokio", "workspace-hack", ] diff --git a/crates/reqwest_client/Cargo.toml b/crates/reqwest_client/Cargo.toml index 20b5177a733f13efb0c1f0292a65ca36934a078b..68a354c13b94c01336791d021a926cacc6da4d62 100644 --- a/crates/reqwest_client/Cargo.toml +++ b/crates/reqwest_client/Cargo.toml @@ -15,10 +15,6 @@ test-support = [] path = "src/reqwest_client.rs" doctest = true -[[example]] -name = "client" -path = "examples/client.rs" - [dependencies] anyhow.workspace = true bytes.workspace = true @@ -26,7 +22,6 @@ futures.workspace = true http_client.workspace = true http_client_tls.workspace = true serde.workspace = true -smol.workspace = true log.workspace = true tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } regex.workspace = true diff --git a/crates/reqwest_client/examples/client.rs b/crates/reqwest_client/examples/client.rs deleted file mode 100644 index 71af8d72cb34103102a8762723509937dc0b5dcd..0000000000000000000000000000000000000000 --- a/crates/reqwest_client/examples/client.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::time::Instant; - -use futures::AsyncReadExt as _; -use futures::stream::FuturesUnordered; -use http_client::AsyncBody; -use http_client::HttpClient; -use reqwest_client::ReqwestClient; -use smol::stream::StreamExt; - -fn main() { - let app = gpui::Application::new(); - app.run(|cx| { - cx.spawn(async move |cx| { - let client = ReqwestClient::new(); - let start = Instant::now(); - let requests = [ - client.get("https://www.google.com/", AsyncBody::empty(), true), - client.get("https://zed.dev/", AsyncBody::empty(), true), - client.get("https://docs.rs/", AsyncBody::empty(), true), - ]; - let mut requests = requests.into_iter().collect::>(); - while let Some(response) = requests.next().await { - let mut body = String::new(); - response - .unwrap() - .into_body() - .read_to_string(&mut body) - .await - .unwrap(); - println!("{}", &body.len()); - } - println!("{:?}", start.elapsed()); - - cx.update(|cx| { - cx.quit(); - }) - .ok(); - }) - .detach(); - }) -} From 1ed17fdd941f43891f4358980528c1d5231c731e Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 2 Sep 2025 21:00:19 -0400 Subject: [PATCH 533/744] Bump Zed to v0.204 (#37415) Release Notes: -N/A --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 50e57005fd1796af9a53edc9a2fc62539c61a948..88150a29310eccb928e85949530d0adc2cae97c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20398,7 +20398,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.203.0" +version = "0.204.0" dependencies = [ "acp_tools", "activity_indicator", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index bb46a5a4f65ac76a7cff2a5bc43525db30ed0930..f82f544acc9bbd6f42b0017d84f27cf793b64799 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.203.0" +version = "0.204.0" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] From f23096034b35e9520e2799bc274d9f08d1426bcc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 2 Sep 2025 20:49:04 -0700 Subject: [PATCH 534/744] Remove wsl command line args on non-windows platforms (#37422) Release Notes: - N/A --- crates/cli/src/main.rs | 15 +++++++++++++-- crates/zed/src/main.rs | 8 +++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index d67843b4c93eb64b01fbdd6e26955d96a0c50e70..d4b4a350f61b5bd1249b33ff3925dd281e9d529c 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -93,6 +93,7 @@ struct Args { /// Example: `me@Ubuntu` or `Ubuntu`. /// /// WARN: You should not fill in this field by hand. + #[cfg(target_os = "windows")] #[arg(long, value_name = "USER@DISTRO")] wsl: Option, /// Not supported in Zed CLI, only supported on Zed binary @@ -303,6 +304,11 @@ fn main() -> Result<()> { ]); } + #[cfg(target_os = "windows")] + let wsl = args.wsl.as_ref(); + #[cfg(not(target_os = "windows"))] + let wsl = None; + for path in args.paths_with_position.iter() { if path.starts_with("zed://") || path.starts_with("http://") @@ -321,7 +327,7 @@ fn main() -> Result<()> { paths.push(tmp_file.path().to_string_lossy().to_string()); let (tmp_file, _) = tmp_file.keep()?; anonymous_fd_tmp_files.push((file, tmp_file)); - } else if let Some(wsl) = &args.wsl { + } else if let Some(wsl) = wsl { urls.push(format!("file://{}", parse_path_in_wsl(path, wsl)?)); } else { paths.push(parse_path_with_position(path)?); @@ -340,11 +346,16 @@ fn main() -> Result<()> { let (_, handshake) = server.accept().context("Handshake after Zed spawn")?; let (tx, rx) = (handshake.requests, handshake.responses); + #[cfg(target_os = "windows")] + let wsl = args.wsl; + #[cfg(not(target_os = "windows"))] + let wsl = None; + tx.send(CliRequest::Open { paths, urls, diff_paths, - wsl: args.wsl, + wsl, wait: args.wait, open_new_workspace, env, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3a7baa1559d68cbce8cfbf96b0bf4384aa1f7e0b..52e475edf8620a1ce97ce732f12777d9bb0cad1a 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -707,11 +707,16 @@ pub fn main() { .map(|chunk| [chunk[0].clone(), chunk[1].clone()]) .collect(); + #[cfg(target_os = "windows")] + let wsl = args.wsl; + #[cfg(not(target_os = "windows"))] + let wsl = None; + if !urls.is_empty() || !diff_paths.is_empty() { open_listener.open(RawOpenRequest { urls, diff_paths, - wsl: args.wsl, + wsl, }) } @@ -1192,6 +1197,7 @@ struct Args { /// Example: `me@Ubuntu` or `Ubuntu`. /// /// WARN: You should not fill in this field by hand. + #[cfg(target_os = "windows")] #[arg(long, value_name = "USER@DISTRO")] wsl: Option, From 2a7761fe172217213c8092c52a42bfa0898b2688 Mon Sep 17 00:00:00 2001 From: chris <7566903+cwwhitman@users.noreply.github.com> Date: Tue, 2 Sep 2025 21:36:36 -0700 Subject: [PATCH 535/744] Instruct macOS users to run `xcodebuild -downloadComponent MetalToolchain` (#37411) Co-authored-by: Conrad Irwin Closes #ISSUE Release Notes: - N/A Co-authored-by: Conrad Irwin --- docs/src/development/macos.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/development/macos.md b/docs/src/development/macos.md index d90cc89abc3e5dc70b2184c85690ea472ec647dc..c7e92623d4e226cb575da524fd8241fba3730fd6 100644 --- a/docs/src/development/macos.md +++ b/docs/src/development/macos.md @@ -69,6 +69,8 @@ xcrun: error: unable to find utility "metal", not a developer tool or in PATH Try `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer` +If you're on macOS 26, try `xcodebuild -downloadComponent MetalToolchain` + ### Cargo errors claiming that a dependency is using unstable features Try `cargo clean` and `cargo build`. From 5a9e18603dfd41a1d8ee81aa0844d4a33bd179e0 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Wed, 3 Sep 2025 07:31:48 +0200 Subject: [PATCH 536/744] gpui: Fix intra rustdoc links (#37320) The only warnings remaining are links to private modules/items, but I lack knowledge to work out if the referenced modules/items should be made public, or if the links should be rewritten into exposed traits/items. Links to associated items such as trait implementations have to be written using full markdown format such as: ... [[ `App::update_global` ]](( BorrowAppContext::update_global )) This is due to https://github.com/rust-lang/rust/issues/74563 which sadly prohibits fully-qualified syntax: ... [[ `::update_global` ]] Release Notes: - N/A Probably related to https://github.com/zed-industries/zed/pull/37072 --- crates/gpui/src/app.rs | 2 +- crates/gpui/src/app/async_context.rs | 4 ++-- crates/gpui/src/colors.rs | 4 ++-- crates/gpui/src/element.rs | 4 ++-- crates/gpui/src/elements/div.rs | 10 +++++----- crates/gpui/src/elements/list.rs | 4 ++-- crates/gpui/src/executor.rs | 6 +++--- crates/gpui/src/gpui.rs | 12 ++++++------ crates/gpui/src/input.rs | 2 +- crates/gpui/src/path_builder.rs | 2 +- crates/gpui/src/text_system.rs | 8 ++++---- crates/gpui/src/window.rs | 22 +++++++++++----------- crates/util/src/schemars.rs | 2 +- crates/util/src/util.rs | 2 +- 14 files changed, 42 insertions(+), 42 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 669a95bd91577577fc460ba30bdacc867e3f3e60..69d5c0ee4375443ad42a7b25a64a138406ac95a2 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2313,7 +2313,7 @@ pub struct AnyDrag { } /// Contains state associated with a tooltip. You'll only need this struct if you're implementing -/// tooltip behavior on a custom element. Otherwise, use [Div::tooltip]. +/// tooltip behavior on a custom element. Otherwise, use [Div::tooltip](crate::Interactivity::tooltip). #[derive(Clone)] pub struct AnyTooltip { /// The view used to display the tooltip diff --git a/crates/gpui/src/app/async_context.rs b/crates/gpui/src/app/async_context.rs index 5eb436290415ed91c89ae85964bbb5093faa38f3..f3b8c0ce77d98a1083d13983c4a2f06f1f543c16 100644 --- a/crates/gpui/src/app/async_context.rs +++ b/crates/gpui/src/app/async_context.rs @@ -218,7 +218,7 @@ impl AsyncApp { Some(read(app.try_global()?, &app)) } - /// A convenience method for [App::update_global] + /// A convenience method for [`App::update_global`](BorrowAppContext::update_global) /// for updating the global state of the specified type. pub fn update_global( &self, @@ -293,7 +293,7 @@ impl AsyncWindowContext { .update(self, |_, window, cx| read(cx.global(), window, cx)) } - /// A convenience method for [`App::update_global`]. + /// A convenience method for [`App::update_global`](BorrowAppContext::update_global). /// for updating the global state of the specified type. pub fn update_global( &mut self, diff --git a/crates/gpui/src/colors.rs b/crates/gpui/src/colors.rs index 5e14c1238addbb02b0c6a02942aae05b703583ea..ef11ef57fdb363dae3f910db2e540e3de02fe453 100644 --- a/crates/gpui/src/colors.rs +++ b/crates/gpui/src/colors.rs @@ -88,9 +88,9 @@ impl Deref for GlobalColors { impl Global for GlobalColors {} -/// Implement this trait to allow global [Color] access via `cx.default_colors()`. +/// Implement this trait to allow global [Colors] access via `cx.default_colors()`. pub trait DefaultColors { - /// Returns the default [`gpui::Colors`] + /// Returns the default [`Colors`] fn default_colors(&self) -> &Arc; } diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index f537bc5ac840432732ae8c9fb608ba74ffefa168..a3fc6269f33d8726b55f8e8be4aadb52109a7606 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -14,13 +14,13 @@ //! tree and any callbacks they have registered with GPUI are dropped and the process repeats. //! //! But some state is too simple and voluminous to store in every view that needs it, e.g. -//! whether a hover has been started or not. For this, GPUI provides the [`Element::State`], associated type. +//! whether a hover has been started or not. For this, GPUI provides the [`Element::PrepaintState`], associated type. //! //! # Implementing your own elements //! //! Elements are intended to be the low level, imperative API to GPUI. They are responsible for upholding, //! or breaking, GPUI's features as they deem necessary. As an example, most GPUI elements are expected -//! to stay in the bounds that their parent element gives them. But with [`WindowContext::break_content_mask`], +//! to stay in the bounds that their parent element gives them. But with [`Window::with_content_mask`], //! you can ignore this restriction and paint anywhere inside of the window's bounds. This is useful for overlays //! and popups and anything else that shows up 'on top' of other elements. //! With great power, comes great responsibility. diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index c9826b704e5732424f06e951c452331cb199a0fa..443bcb14bbec7c5fac39fdd0f5e5d621d84df610 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -533,7 +533,7 @@ impl Interactivity { } /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. - /// The imperative API equivalent to [`InteractiveElement::tooltip`] + /// The imperative API equivalent to [`StatefulInteractiveElement::tooltip`] pub fn tooltip(&mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) where Self: Sized, @@ -550,7 +550,7 @@ impl Interactivity { /// Use the given callback to construct a new tooltip view when the mouse hovers over this element. /// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into - /// the tooltip. The imperative API equivalent to [`InteractiveElement::hoverable_tooltip`] + /// the tooltip. The imperative API equivalent to [`StatefulInteractiveElement::hoverable_tooltip`] pub fn hoverable_tooltip( &mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, @@ -676,7 +676,7 @@ pub trait InteractiveElement: Sized { #[cfg(any(test, feature = "test-support"))] /// Set a key that can be used to look up this element's bounds - /// in the [`VisualTestContext::debug_bounds`] map + /// in the [`crate::VisualTestContext::debug_bounds`] map /// This is a noop in release builds fn debug_selector(mut self, f: impl FnOnce() -> String) -> Self { self.interactivity().debug_selector = Some(f()); @@ -685,7 +685,7 @@ pub trait InteractiveElement: Sized { #[cfg(not(any(test, feature = "test-support")))] /// Set a key that can be used to look up this element's bounds - /// in the [`VisualTestContext::debug_bounds`] map + /// in the [`crate::VisualTestContext::debug_bounds`] map /// This is a noop in release builds #[inline] fn debug_selector(self, _: impl FnOnce() -> String) -> Self { @@ -1087,7 +1087,7 @@ pub trait StatefulInteractiveElement: InteractiveElement { /// On drag initiation, this callback will be used to create a new view to render the dragged value for a /// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with - /// the [`Self::on_drag_move`] API. + /// the [`InteractiveElement::on_drag_move`] API. /// The callback also has access to the offset of triggering click from the origin of parent element. /// The fluent API equivalent to [`Interactivity::on_drag`] /// diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 6758f4eee1e03c5b32f1dd924ed6d37fa31cf767..ed4ca64e83513531b9176f05c4c00b0af71aea74 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -5,7 +5,7 @@ //! In order to minimize re-renders, this element's state is stored intrusively //! on your own views, so that your code can coordinate directly with the list element's cached state. //! -//! If all of your elements are the same height, see [`UniformList`] for a simpler API +//! If all of your elements are the same height, see [`crate::UniformList`] for a simpler API use crate::{ AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId, @@ -235,7 +235,7 @@ impl ListState { } /// Register with the list state that the items in `old_range` have been replaced - /// by new items. As opposed to [`splice`], this method allows an iterator of optional focus handles + /// by new items. As opposed to [`Self::splice`], this method allows an iterator of optional focus handles /// to be supplied to properly integrate with items in the list that can be focused. If a focused item /// is scrolled out of view, the list will continue to render it to allow keyboard interaction. pub fn splice_focusable( diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 273a3ea503bad26de075a8eb1c6cec01d23f453b..0b28dd030baff6bc95ede07e50e358660a9c1353 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -391,7 +391,7 @@ impl BackgroundExecutor { } /// in tests, run all tasks that are ready to run. If after doing so - /// the test still has outstanding tasks, this will panic. (See also `allow_parking`) + /// the test still has outstanding tasks, this will panic. (See also [`Self::allow_parking`]) #[cfg(any(test, feature = "test-support"))] pub fn run_until_parked(&self) { self.dispatcher.as_test().unwrap().run_until_parked() @@ -405,7 +405,7 @@ impl BackgroundExecutor { self.dispatcher.as_test().unwrap().allow_parking(); } - /// undoes the effect of [`allow_parking`]. + /// undoes the effect of [`Self::allow_parking`]. #[cfg(any(test, feature = "test-support"))] pub fn forbid_parking(&self) { self.dispatcher.as_test().unwrap().forbid_parking(); @@ -480,7 +480,7 @@ impl ForegroundExecutor { /// Variant of `async_task::spawn_local` that includes the source location of the spawn in panics. /// /// Copy-modified from: -/// https://github.com/smol-rs/async-task/blob/ca9dbe1db9c422fd765847fa91306e30a6bb58a9/src/runnable.rs#L405 +/// #[track_caller] fn spawn_local_with_source_location( future: Fut, diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 0f5b98df39712157845b56d9c167f59d3f8831ab..3c4ee41c16ab7cfc5e42007291e330282b330ecb 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -24,7 +24,7 @@ //! - State management and communication with [`Entity`]'s. Whenever you need to store application state //! that communicates between different parts of your application, you'll want to use GPUI's //! entities. Entities are owned by GPUI and are only accessible through an owned smart pointer -//! similar to an [`std::rc::Rc`]. See the [`app::context`] module for more information. +//! similar to an [`std::rc::Rc`]. See [`app::Context`] for more information. //! //! - High level, declarative UI with views. All UI in GPUI starts with a view. A view is simply //! a [`Entity`] that can be rendered, by implementing the [`Render`] trait. At the start of each frame, GPUI @@ -37,7 +37,7 @@ //! provide a nice wrapper around an imperative API that provides as much flexibility and control as //! you need. Elements have total control over how they and their child elements are rendered and //! can be used for making efficient views into large lists, implement custom layouting for a code editor, -//! and anything else you can think of. See the [`element`] module for more information. +//! and anything else you can think of. See the [`elements`] module for more information. //! //! Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services. //! This context is your main interface to GPUI, and is used extensively throughout the framework. @@ -51,9 +51,9 @@ //! Use this for implementing keyboard shortcuts, such as cmd-q (See `action` module for more information). //! - Platform services, such as `quit the app` or `open a URL` are available as methods on the [`app::App`]. //! - An async executor that is integrated with the platform's event loop. See the [`executor`] module for more information., -//! - The [`gpui::test`](test) macro provides a convenient way to write tests for your GPUI applications. Tests also have their -//! own kind of context, a [`TestAppContext`] which provides ways of simulating common platform input. See [`app::test_context`] -//! and [`test`] modules for more details. +//! - The [`gpui::test`](macro@test) macro provides a convenient way to write tests for your GPUI applications. Tests also have their +//! own kind of context, a [`TestAppContext`] which provides ways of simulating common platform input. See [`TestAppContext`] +//! and [`mod@test`] modules for more details. //! //! Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop //! a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, @@ -117,7 +117,7 @@ pub mod private { mod seal { /// A mechanism for restricting implementations of a trait to only those in GPUI. - /// See: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/ + /// See: pub trait Sealed {} } diff --git a/crates/gpui/src/input.rs b/crates/gpui/src/input.rs index 4acd7f90c1273a1eb51b1be2ccc672a79e6f7710..dc36ef9e16feedf31c01cd38327fd12729f894b3 100644 --- a/crates/gpui/src/input.rs +++ b/crates/gpui/src/input.rs @@ -72,7 +72,7 @@ pub trait EntityInputHandler: 'static + Sized { ) -> Option; } -/// The canonical implementation of [`PlatformInputHandler`]. Call [`Window::handle_input`] +/// The canonical implementation of [`crate::PlatformInputHandler`]. Call [`Window::handle_input`] /// with an instance during your element's paint. pub struct ElementInputHandler { view: Entity, diff --git a/crates/gpui/src/path_builder.rs b/crates/gpui/src/path_builder.rs index 38903ea5885a4fbd0ed1454046a9021aa572d6e3..40a6e71e0a1738adf1ed261183d2340682826992 100644 --- a/crates/gpui/src/path_builder.rs +++ b/crates/gpui/src/path_builder.rs @@ -318,7 +318,7 @@ impl PathBuilder { Ok(Self::build_path(buf)) } - /// Builds a [`Path`] from a [`lyon::VertexBuffers`]. + /// Builds a [`Path`] from a [`lyon::tessellation::VertexBuffers`]. pub fn build_path(buf: VertexBuffers) -> Path { if buf.vertices.is_empty() { return Path::new(Point::default()); diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 53991089da94c58d0035bff0d607ad3ab57a69bd..4d4087f45d4093c239218f96f015d153fa77dc10 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -351,7 +351,7 @@ impl WindowTextSystem { /// /// Note that this method can only shape a single line of text. It will panic /// if the text contains newlines. If you need to shape multiple lines of text, - /// use `TextLayout::shape_text` instead. + /// use [`Self::shape_text`] instead. pub fn shape_line( &self, text: SharedString, @@ -517,7 +517,7 @@ impl WindowTextSystem { /// Layout the given line of text, at the given font_size. /// Subsets of the line can be styled independently with the `runs` parameter. - /// Generally, you should prefer to use `TextLayout::shape_line` instead, which + /// Generally, you should prefer to use [`Self::shape_line`] instead, which /// can be painted directly. pub fn layout_line( &self, @@ -668,7 +668,7 @@ impl Display for FontStyle { } } -/// A styled run of text, for use in [`TextLayout`]. +/// A styled run of text, for use in [`crate::TextLayout`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TextRun { /// A number of utf8 bytes @@ -694,7 +694,7 @@ impl TextRun { } } -/// An identifier for a specific glyph, as returned by [`TextSystem::layout_line`]. +/// An identifier for a specific glyph, as returned by [`WindowTextSystem::layout_line`]. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] #[repr(C)] pub struct GlyphId(pub(crate) u32); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index c2719665d423a4431184d56a9b6bff16f8ad443b..0ec73c4b0040e6c65cd8819ecf5d20a9ec1900d0 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -585,7 +585,7 @@ pub enum HitboxBehavior { /// if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { /// cx.stop_propagation(); /// } - /// } + /// }) /// ``` /// /// This has effects beyond event handling - any use of hitbox checking, such as hover @@ -605,11 +605,11 @@ pub enum HitboxBehavior { /// bubble-phase handler for every mouse event type **except** `ScrollWheelEvent`: /// /// ``` - /// window.on_mouse_event(move |_: &EveryMouseEventTypeExceptScroll, phase, window, _cx| { + /// window.on_mouse_event(move |_: &EveryMouseEventTypeExceptScroll, phase, window, cx| { /// if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { /// cx.stop_propagation(); /// } - /// } + /// }) /// ``` /// /// See the documentation of [`Hitbox::is_hovered`] for details of why `ScrollWheelEvent` is @@ -1909,7 +1909,7 @@ impl Window { } /// Produces a new frame and assigns it to `rendered_frame`. To actually show - /// the contents of the new [Scene], use [present]. + /// the contents of the new [`Scene`], use [`Self::present`]. #[profiling::function] pub fn draw(&mut self, cx: &mut App) -> ArenaClearNeeded { self.invalidate_entities(); @@ -2451,7 +2451,7 @@ impl Window { /// Perform prepaint on child elements in a "retryable" manner, so that any side effects /// of prepaints can be discarded before prepainting again. This is used to support autoscroll /// where we need to prepaint children to detect the autoscroll bounds, then adjust the - /// element offset and prepaint again. See [`List`] for an example. This method should only be + /// element offset and prepaint again. See [`crate::List`] for an example. This method should only be /// called during the prepaint phase of element drawing. pub fn transact(&mut self, f: impl FnOnce(&mut Self) -> Result) -> Result { self.invalidator.debug_assert_prepaint(); @@ -2476,9 +2476,9 @@ impl Window { result } - /// When you call this method during [`prepaint`], containing elements will attempt to + /// When you call this method during [`Element::prepaint`], containing elements will attempt to /// scroll to cause the specified bounds to become visible. When they decide to autoscroll, they will call - /// [`prepaint`] again with a new set of bounds. See [`List`] for an example of an element + /// [`Element::prepaint`] again with a new set of bounds. See [`crate::List`] for an example of an element /// that supports this method being called on the elements it contains. This method should only be /// called during the prepaint phase of element drawing. pub fn request_autoscroll(&mut self, bounds: Bounds) { @@ -2486,8 +2486,8 @@ impl Window { self.requested_autoscroll = Some(bounds); } - /// This method can be called from a containing element such as [`List`] to support the autoscroll behavior - /// described in [`request_autoscroll`]. + /// This method can be called from a containing element such as [`crate::List`] to support the autoscroll behavior + /// described in [`Self::request_autoscroll`]. pub fn take_autoscroll(&mut self) -> Option> { self.invalidator.debug_assert_prepaint(); self.requested_autoscroll.take() @@ -2815,7 +2815,7 @@ impl Window { /// Paint one or more quads into the scene for the next frame at the current stacking context. /// Quads are colored rectangular regions with an optional background, border, and corner radius. - /// see [`fill`](crate::fill), [`outline`](crate::outline), and [`quad`](crate::quad) to construct this type. + /// see [`fill`], [`outline`], and [`quad`] to construct this type. /// /// This method should only be called as part of the paint phase of element drawing. /// @@ -4821,7 +4821,7 @@ impl HasDisplayHandle for Window { } } -/// An identifier for an [`Element`](crate::Element). +/// An identifier for an [`Element`]. /// /// Can be constructed with a string, a number, or both, as well /// as other internal representations. diff --git a/crates/util/src/schemars.rs b/crates/util/src/schemars.rs index a59d24c3251b6aebfa8adb9a3dfa809c34627c73..22e0570cdb85efa82904153eda619b84b430eb61 100644 --- a/crates/util/src/schemars.rs +++ b/crates/util/src/schemars.rs @@ -7,7 +7,7 @@ const DEFS_PATH: &str = "#/$defs/"; /// /// This asserts that JsonSchema::schema_name() + "2" does not exist because this indicates that /// there are multiple types that use this name, and unfortunately schemars APIs do not support -/// resolving this ambiguity - see https://github.com/GREsau/schemars/issues/449 +/// resolving this ambiguity - see /// /// This takes a closure for `schema` because some settings types are not available on the remote /// server, and so will crash when attempting to access e.g. GlobalThemeRegistry. diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 0aceec5d7ae4b672afc6111bd4f2389d7b1b6af7..c66adb8b3a7ef93828e95683596f43b91f96f994 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -329,7 +329,7 @@ pub fn load_login_shell_environment() -> Result<()> { /// Configures the process to start a new session, to prevent interactive shells from taking control /// of the terminal. /// -/// For more details: https://registerspill.thorstenball.com/p/how-to-lose-control-of-your-shell +/// For more details: pub fn set_pre_exec_to_start_new_session( command: &mut std::process::Command, ) -> &mut std::process::Command { From 8d5861322bba51c6cf4e718496c4cb21161182cc Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 3 Sep 2025 08:50:53 +0300 Subject: [PATCH 537/744] Allow wrapping markdown text into `*` by selecting text and writing the `*` (#37426) Release Notes: - Allowed wrapping markdown text into `*` by selecting text and writing the `*` --- crates/languages/src/markdown/config.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/languages/src/markdown/config.toml b/crates/languages/src/markdown/config.toml index 926dcd70d9f9207c03154690e7d4e9866f9aacea..36071cb5392462a51c10e0513b39979580ec67f5 100644 --- a/crates/languages/src/markdown/config.toml +++ b/crates/languages/src/markdown/config.toml @@ -12,6 +12,7 @@ brackets = [ { start = "\"", end = "\"", close = false, newline = false }, { start = "'", end = "'", close = false, newline = false }, { start = "`", end = "`", close = false, newline = false }, + { start = "*", end = "*", close = false, newline = false, surround = true }, ] rewrap_prefixes = [ "[-*+]\\s+", From d7fd5910d7d0ae4404be21ada5be1f39d83d3aea Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Wed, 3 Sep 2025 00:35:31 -0600 Subject: [PATCH 538/744] Use slice from Rope chunk when possible while iterating lines (#37430) Release Notes: - N/A --- crates/rope/src/rope.rs | 48 +++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 0d3f5abbdefd3850d02e66a1efbef5b58a5f2835..41b2a2d033eb49a1851c02e7066be22d807bca4b 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -936,24 +936,36 @@ impl Lines<'_> { self.current_line.clear(); while let Some(chunk) = self.chunks.peek() { - let lines = chunk.split('\n'); + let chunk_lines = chunk.split('\n'); if self.reversed { - let mut lines = lines.rev().peekable(); - while let Some(line) = lines.next() { - self.current_line.insert_str(0, line); - if lines.peek().is_some() { + let mut chunk_lines = chunk_lines.rev().peekable(); + if let Some(chunk_line) = chunk_lines.next() { + let done = chunk_lines.peek().is_some(); + if done { self.chunks - .seek(self.chunks.offset() - line.len() - "\n".len()); + .seek(self.chunks.offset() - chunk_line.len() - "\n".len()); + if self.current_line.is_empty() { + return Some(chunk_line); + } + } + self.current_line.insert_str(0, chunk_line); + if done { return Some(&self.current_line); } } } else { - let mut lines = lines.peekable(); - while let Some(line) = lines.next() { - self.current_line.push_str(line); - if lines.peek().is_some() { + let mut chunk_lines = chunk_lines.peekable(); + if let Some(chunk_line) = chunk_lines.next() { + let done = chunk_lines.peek().is_some(); + if done { self.chunks - .seek(self.chunks.offset() + line.len() + "\n".len()); + .seek(self.chunks.offset() + chunk_line.len() + "\n".len()); + if self.current_line.is_empty() { + return Some(chunk_line); + } + } + self.current_line.push_str(chunk_line); + if done { return Some(&self.current_line); } } @@ -1573,6 +1585,20 @@ mod tests { assert_eq!(lines.next(), Some("defg")); assert_eq!(lines.next(), Some("abc")); assert_eq!(lines.next(), None); + + let rope = Rope::from("abc\nlonger line test\nhi"); + let mut lines = rope.chunks().lines(); + assert_eq!(lines.next(), Some("abc")); + assert_eq!(lines.next(), Some("longer line test")); + assert_eq!(lines.next(), Some("hi")); + assert_eq!(lines.next(), None); + + let rope = Rope::from("abc\nlonger line test\nhi"); + let mut lines = rope.reversed_chunks_in_range(0..rope.len()).lines(); + assert_eq!(lines.next(), Some("hi")); + assert_eq!(lines.next(), Some("longer line test")); + assert_eq!(lines.next(), Some("abc")); + assert_eq!(lines.next(), None); } #[gpui::test(iterations = 100)] From ae840c6ef39007bd00ef8f63161825bd18cdcc50 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 3 Sep 2025 03:40:14 -0400 Subject: [PATCH 539/744] acp: Fix handling of single-file worktrees (#37412) When the first visible worktree is a single-file worktree, we would previously try to use the absolute path of that file as the root directory for external agents, causing an error. This PR changes how we handle this situation: we'll use the root of the first non-single-file visible worktree if there are any, and if there are none, the parent directory of the first single-file visible worktree. Related to #37213 Release Notes: - acp: Fixed being unable to run external agents when a single file (not part of a project) was opened in Zed. --- crates/agent_ui/src/acp/thread_view.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 60b3166a57aebc02fba82d4b350de0e48b84ef94..bf981d10f7685c64f8c3e8c03895dda8c5d839eb 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -427,11 +427,24 @@ impl AcpThreadView { "External agents are not yet supported for remote projects.".into(), )); } - let root_dir = project - .read(cx) - .visible_worktrees(cx) + let mut worktrees = project.read(cx).visible_worktrees(cx).collect::>(); + // Pick the first non-single-file worktree for the root directory if there are any, + // and otherwise the parent of a single-file worktree, falling back to $HOME if there are no visible worktrees. + worktrees.sort_by(|l, r| { + l.read(cx) + .is_single_file() + .cmp(&r.read(cx).is_single_file()) + }); + let root_dir = worktrees + .into_iter() + .filter_map(|worktree| { + if worktree.read(cx).is_single_file() { + Some(worktree.read(cx).abs_path().parent()?.into()) + } else { + Some(worktree.read(cx).abs_path()) + } + }) .next() - .map(|worktree| worktree.read(cx).abs_path()) .unwrap_or_else(|| paths::home_dir().as_path().into()); let (tx, mut rx) = watch::channel("Loading…".into()); let delegate = AgentServerDelegate::new(project.clone(), Some(tx)); From 6feae92616d473b0df61b6eb84cca3e8ea8115b6 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 3 Sep 2025 11:02:21 +0200 Subject: [PATCH 540/744] rust: Improve highlighting in derive macros (#37439) Follow-up to https://github.com/zed-industries/zed/pull/37049 This fixes an issue where we would lose highlighting in derive macros if one of the names was qualified. | Before | After | | --- | --- | | Bildschirmfoto 2025-09-03 um 10 39
25 | Bildschirmfoto 2025-09-03 um 10
38 14 | Release Notes: - N/A --- crates/languages/src/rust/highlights.scm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/languages/src/rust/highlights.scm b/crates/languages/src/rust/highlights.scm index 3f44c5fc0e46d280f63d0b212cc237ba4cbb0e8b..ec7e2d42510c58d25f09c13e78d2f75bf7d20b5c 100644 --- a/crates/languages/src/rust/highlights.scm +++ b/crates/languages/src/rust/highlights.scm @@ -196,12 +196,12 @@ operator: "/" @operator (identifier) @attribute (scoped_identifier name: (identifier) @attribute) (token_tree (identifier) @attribute (#match? @attribute "^[a-z\\d_]*$")) - (token_tree (identifier) @variable "::" (identifier) @type (#match? @type "^[A-Z]")) + (token_tree (identifier) @none "::" (#match? @none "^[a-z\\d_]*$")) ])) (inner_attribute_item (attribute [ (identifier) @attribute (scoped_identifier name: (identifier) @attribute) (token_tree (identifier) @attribute (#match? @attribute "^[a-z\\d_]*$")) - (token_tree (identifier) @variable "::" (identifier) @type (#match? @type "^[A-Z]")) + (token_tree (identifier) @none "::" (#match? @none "^[a-z\\d_]*$")) ])) From c4466628624f96378d6c81853add98eab7aabdaf Mon Sep 17 00:00:00 2001 From: localcc Date: Wed, 3 Sep 2025 11:21:45 +0200 Subject: [PATCH 541/744] Fix font rendering at very large scales (#37440) Release Notes: - Fixed fonts disappearing at very large scales on windows --- crates/gpui/src/platform/windows/direct_write.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index 5e44a609db6b5276ef8da040fd821786db67f6af..285f6a1143d7edadd1530abf07051fae254595da 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -742,6 +742,10 @@ impl DirectWriteState { &mut grid_fit_mode, )?; } + let rendering_mode = match rendering_mode { + DWRITE_RENDERING_MODE1_OUTLINE => DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC, + m => m, + }; let glyph_analysis = unsafe { self.components.factory.CreateGlyphRunAnalysis( From 9a8c5053c22ee386f0b3f50f16cbfc6c8fe4b4e4 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 3 Sep 2025 06:54:31 -0300 Subject: [PATCH 542/744] agent: Update message editor placeholder (#37441) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index bf981d10f7685c64f8c3e8c03895dda8c5d839eb..589633ae250580f6eb66a513534c79a898fdc0d6 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -332,6 +332,11 @@ impl AcpThreadView { let placeholder = if agent.name() == "Zed Agent" { format!("Message the {} — @ to include context", agent.name()) + } else if agent.name() == "Claude Code" || !available_commands.borrow().is_empty() { + format!( + "Message {} — @ to include context, / for commands", + agent.name() + ) } else { format!("Message {} — @ to include context", agent.name()) }; From 40199266b6634cc3165f3842abae1d562ef4dcca Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 3 Sep 2025 18:44:33 +0800 Subject: [PATCH 543/744] gpui: Fix overflow_hidden to support clip with border radius (#35083) Release Notes: - N/A --- Same case in HTML example: https://developer.mozilla.org/en-US/play?id=p7FhB3JAhiVfLHAXnsbrn7JYYX%2Byq1gje%2B%2BTZarnXvvjmaAx3NlrXqMAoI35s4zeakShKee6lydHYeHr ```html
Let build applications with GPUI.
Let build applications with GPUI.
This is not overflow: hidden.
``` image ### Before image ### After ```bash cargo run -p gpui --example content_mask ``` image | - [x] Metal - [x] Blade - [x] DirectX - [x] ContentMask radius must reduce the container border widths. - [x] The dash border render not correct, when not all side have borders. --- crates/editor/src/element.rs | 25 +- crates/gpui/examples/content_mask.rs | 228 ++++++++++++++++++ crates/gpui/src/elements/list.rs | 78 ++++-- crates/gpui/src/elements/uniform_list.rs | 5 +- crates/gpui/src/platform/blade/shaders.wgsl | 60 +++-- crates/gpui/src/platform/mac/shaders.metal | 24 +- crates/gpui/src/platform/windows/shaders.hlsl | 49 ++-- crates/gpui/src/style.rs | 66 ++--- crates/gpui/src/window.rs | 23 +- crates/terminal_view/src/terminal_element.rs | 2 +- crates/ui/src/components/scrollbar.rs | 16 +- 11 files changed, 446 insertions(+), 130 deletions(-) create mode 100644 crates/gpui/examples/content_mask.rs diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f384afa1ae988d8d224f9ec3de70932543519571..500cce7e0a63bf0a3c985fdd6b507af389775792 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -6025,6 +6025,7 @@ impl EditorElement { window.with_content_mask( Some(ContentMask { bounds: layout.position_map.text_hitbox.bounds, + ..Default::default() }), |window| { let editor = self.editor.read(cx); @@ -6967,9 +6968,15 @@ impl EditorElement { } else { let mut bounds = layout.hitbox.bounds; bounds.origin.x += layout.gutter_hitbox.bounds.size.width; - window.with_content_mask(Some(ContentMask { bounds }), |window| { - block.element.paint(window, cx); - }) + window.with_content_mask( + Some(ContentMask { + bounds, + ..Default::default() + }), + |window| { + block.element.paint(window, cx); + }, + ) } } } @@ -8270,9 +8277,13 @@ impl Element for EditorElement { } let rem_size = self.rem_size(cx); + let content_mask = ContentMask { + bounds, + ..Default::default() + }; window.with_rem_size(rem_size, |window| { window.with_text_style(Some(text_style), |window| { - window.with_content_mask(Some(ContentMask { bounds }), |window| { + window.with_content_mask(Some(content_mask), |window| { let (mut snapshot, is_read_only) = self.editor.update(cx, |editor, cx| { (editor.snapshot(window, cx), editor.read_only(cx)) }); @@ -9380,9 +9391,13 @@ impl Element for EditorElement { ..Default::default() }; let rem_size = self.rem_size(cx); + let content_mask = ContentMask { + bounds, + ..Default::default() + }; window.with_rem_size(rem_size, |window| { window.with_text_style(Some(text_style), |window| { - window.with_content_mask(Some(ContentMask { bounds }), |window| { + window.with_content_mask(Some(content_mask), |window| { self.paint_mouse_listeners(layout, window, cx); self.paint_background(layout, window, cx); self.paint_indent_guides(layout, window, cx); diff --git a/crates/gpui/examples/content_mask.rs b/crates/gpui/examples/content_mask.rs new file mode 100644 index 0000000000000000000000000000000000000000..8d40cc5bba7c35395cbeef009f6b648ed68826ed --- /dev/null +++ b/crates/gpui/examples/content_mask.rs @@ -0,0 +1,228 @@ +use gpui::{ + App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, + rgb, size, +}; + +struct Example {} + +impl Render for Example { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .font_family(".SystemUIFont") + .flex() + .flex_col() + .size_full() + .p_4() + .gap_4() + .bg(rgb(0x505050)) + .justify_center() + .items_center() + .text_center() + .shadow_lg() + .text_sm() + .text_color(rgb(0xffffff)) + .child( + div() + .overflow_hidden() + .rounded(px(32.)) + .border(px(8.)) + .border_color(gpui::white()) + .text_color(gpui::white()) + .child( + div() + .bg(gpui::black()) + .py_2() + .px_7() + .border_l_2() + .border_r_2() + .border_b_3() + .border_color(gpui::red()) + .child("Let build applications with GPUI"), + ) + .child( + div() + .bg(rgb(0x222222)) + .text_sm() + .py_1() + .px_7() + .border_l_3() + .border_r_3() + .border_color(gpui::green()) + .child("The fast, productive UI framework for Rust"), + ) + .child( + div() + .bg(rgb(0x222222)) + .w_full() + .flex() + .flex_row() + .text_sm() + .text_color(rgb(0xc0c0c0)) + .child( + div() + .flex_1() + .p_2() + .border_3() + .border_dashed() + .border_color(gpui::blue()) + .child("Rust"), + ) + .child( + div() + .flex_1() + .p_2() + .border_t_3() + .border_r_3() + .border_b_3() + .border_dashed() + .border_color(gpui::blue()) + .child("GPU Rendering"), + ), + ), + ) + .child( + div() + .flex() + .flex_col() + .w(px(320.)) + .gap_1() + .overflow_hidden() + .rounded(px(16.)) + .child( + div() + .w_full() + .p_2() + .bg(gpui::red()) + .child("Clip background"), + ), + ) + .child( + div() + .flex() + .flex_col() + .w(px(320.)) + .gap_1() + .rounded(px(16.)) + .child( + div() + .w_full() + .p_2() + .bg(gpui::yellow()) + .text_color(gpui::black()) + .child("No content mask"), + ), + ) + .child( + div() + .flex() + .flex_col() + .w(px(320.)) + .gap_1() + .overflow_hidden() + .rounded(px(16.)) + .child( + div() + .w_full() + .p_2() + .border_4() + .border_color(gpui::blue()) + .bg(gpui::blue().alpha(0.4)) + .child("Clip borders"), + ), + ) + .child( + div() + .flex() + .flex_col() + .w(px(320.)) + .gap_1() + .overflow_hidden() + .rounded(px(20.)) + .child( + div().w_full().border_2().border_color(gpui::black()).child( + div() + .size_full() + .bg(gpui::green().alpha(0.4)) + .p_2() + .border_8() + .border_color(gpui::green()) + .child("Clip nested elements"), + ), + ), + ) + .child( + div() + .flex() + .flex_col() + .w(px(320.)) + .gap_1() + .overflow_hidden() + .rounded(px(32.)) + .child( + div() + .w_full() + .p_2() + .bg(gpui::black()) + .border_2() + .border_dashed() + .rounded_lg() + .border_color(gpui::white()) + .child("dash border full and rounded"), + ) + .child( + div() + .w_full() + .flex() + .flex_row() + .gap_2() + .child( + div() + .w_full() + .p_2() + .bg(gpui::black()) + .border_x_2() + .border_dashed() + .rounded_lg() + .border_color(gpui::white()) + .child("border x"), + ) + .child( + div() + .w_full() + .p_2() + .bg(gpui::black()) + .border_y_2() + .border_dashed() + .rounded_lg() + .border_color(gpui::white()) + .child("border y"), + ), + ) + .child( + div() + .w_full() + .p_2() + .bg(gpui::black()) + .border_2() + .border_dashed() + .border_color(gpui::white()) + .child("border full and no rounded"), + ), + ) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(800.), px(600.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| Example {}), + ) + .unwrap(); + cx.activate(true); + }); +} diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index ed4ca64e83513531b9176f05c4c00b0af71aea74..9ae497cef92c9f1aa68eea4af6aea8bf410a817f 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -8,10 +8,10 @@ //! If all of your elements are the same height, see [`crate::UniformList`] for a simpler API use crate::{ - AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId, - FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, - Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, - Window, point, px, size, + AnyElement, App, AvailableSpace, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, + EntityId, FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, + IntoElement, Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, + StyleRefinement, Styled, Window, point, px, size, }; use collections::VecDeque; use refineable::Refineable as _; @@ -705,6 +705,7 @@ impl StateInner { &mut self, bounds: Bounds, padding: Edges, + corner_radii: Corners, autoscroll: bool, render_item: &mut RenderItemFn, window: &mut Window, @@ -728,9 +729,15 @@ impl StateInner { let mut item_origin = bounds.origin + Point::new(px(0.), padding.top); item_origin.y -= layout_response.scroll_top.offset_in_item; for item in &mut layout_response.item_layouts { - window.with_content_mask(Some(ContentMask { bounds }), |window| { - item.element.prepaint_at(item_origin, window, cx); - }); + window.with_content_mask( + Some(ContentMask { + bounds, + corner_radii, + }), + |window| { + item.element.prepaint_at(item_origin, window, cx); + }, + ); if let Some(autoscroll_bounds) = window.take_autoscroll() && autoscroll @@ -952,19 +959,34 @@ impl Element for List { state.items = new_items; } - let padding = style - .padding - .to_pixels(bounds.size.into(), window.rem_size()); - let layout = - match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) { - Ok(layout) => layout, - Err(autoscroll_request) => { - state.logical_scroll_top = Some(autoscroll_request); - state - .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx) - .unwrap() - } - }; + let rem_size = window.rem_size(); + let padding = style.padding.to_pixels(bounds.size.into(), rem_size); + let corner_radii = style.corner_radii.to_pixels(rem_size); + let layout = match state.prepaint_items( + bounds, + padding, + corner_radii, + true, + &mut self.render_item, + window, + cx, + ) { + Ok(layout) => layout, + Err(autoscroll_request) => { + state.logical_scroll_top = Some(autoscroll_request); + state + .prepaint_items( + bounds, + padding, + corner_radii, + false, + &mut self.render_item, + window, + cx, + ) + .unwrap() + } + }; state.last_layout_bounds = Some(bounds); state.last_padding = Some(padding); @@ -982,11 +1004,17 @@ impl Element for List { cx: &mut App, ) { let current_view = window.current_view(); - window.with_content_mask(Some(ContentMask { bounds }), |window| { - for item in &mut prepaint.layout.item_layouts { - item.element.paint(window, cx); - } - }); + window.with_content_mask( + Some(ContentMask { + bounds, + ..Default::default() + }), + |window| { + for item in &mut prepaint.layout.item_layouts { + item.element.paint(window, cx); + } + }, + ); let list_state = self.state.clone(); let height = bounds.size.height; diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index cdf90d4eb8934de99a21c65b6c9efa2a2fdde258..db3b8c88395388bf57eb16a0bb73ce2d6f005779 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -411,7 +411,10 @@ impl Element for UniformList { (self.render_items)(visible_range.clone(), window, cx) }; - let content_mask = ContentMask { bounds }; + let content_mask = ContentMask { + bounds, + ..Default::default() + }; window.with_content_mask(Some(content_mask), |window| { for (mut item, ix) in items.into_iter().zip(visible_range.clone()) { let item_origin = padded_bounds.origin diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index 95980b54fe4f25b3936d6b095219c5674211dd0a..dbab4237e3eb87c570096ae7b6d7328fa1da6ff2 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -53,6 +53,11 @@ struct Corners { bottom_left: f32, } +struct ContentMask { + bounds: Bounds, + corner_radii: Corners, +} + struct Edges { top: f32, right: f32, @@ -440,7 +445,7 @@ struct Quad { order: u32, border_style: u32, bounds: Bounds, - content_mask: Bounds, + content_mask: ContentMask, background: Background, border_color: Hsla, corner_radii: Corners, @@ -478,7 +483,7 @@ fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta out.background_color1 = gradient.color1; out.border_color = hsla_to_rgba(quad.border_color); out.quad_id = instance_id; - out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask); + out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask.bounds); return out; } @@ -491,8 +496,19 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { let quad = b_quads[input.quad_id]; - let background_color = gradient_color(quad.background, input.position.xy, quad.bounds, + // Signed distance field threshold for inclusion of pixels. 0.5 is the + // minimum distance between the center of the pixel and the edge. + let antialias_threshold = 0.5; + + var background_color = gradient_color(quad.background, input.position.xy, quad.bounds, input.background_solid, input.background_color0, input.background_color1); + var border_color = input.border_color; + + // Apply content_mask corner radii clipping + let clip_sdf = quad_sdf(input.position.xy, quad.content_mask.bounds, quad.content_mask.corner_radii); + let clip_alpha = saturate(antialias_threshold - clip_sdf); + background_color.a *= clip_alpha; + border_color.a *= clip_alpha; let unrounded = quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 && @@ -513,10 +529,6 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { let point = input.position.xy - quad.bounds.origin; let center_to_point = point - half_size; - // Signed distance field threshold for inclusion of pixels. 0.5 is the - // minimum distance between the center of the pixel and the edge. - let antialias_threshold = 0.5; - // Radius of the nearest corner let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); @@ -607,8 +619,6 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { var color = background_color; if (border_sdf < antialias_threshold) { - var border_color = input.border_color; - // Dashed border logic when border_style == 1 if (quad.border_style == 1) { // Position along the perimeter in "dash space", where each dash @@ -644,7 +654,11 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { let is_horizontal = corner_center_to_point.x < corner_center_to_point.y; - let border_width = select(border.y, border.x, is_horizontal); + var border_width = select(border.y, border.x, is_horizontal); + // When border width of some side is 0, we need to use the other side width for dash velocity. + if (border_width == 0.0) { + border_width = select(border.x, border.y, is_horizontal); + } dash_velocity = dv_numerator / border_width; t = select(point.y, point.x, is_horizontal) * dash_velocity; max_t = select(size.y, size.x, is_horizontal) * dash_velocity; @@ -856,7 +870,7 @@ struct Shadow { blur_radius: f32, bounds: Bounds, corner_radii: Corners, - content_mask: Bounds, + content_mask: ContentMask, color: Hsla, } var b_shadows: array; @@ -884,7 +898,7 @@ fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) ins out.position = to_device_position(unit_vertex, shadow.bounds); out.color = hsla_to_rgba(shadow.color); out.shadow_id = instance_id; - out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask); + out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask.bounds); return out; } @@ -899,7 +913,6 @@ fn fs_shadow(input: ShadowVarying) -> @location(0) vec4 { let half_size = shadow.bounds.size / 2.0; let center = shadow.bounds.origin + half_size; let center_to_point = input.position.xy - center; - let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii); // The signal is only non-zero in a limited range, so don't waste samples @@ -1027,7 +1040,7 @@ struct Underline { order: u32, pad: u32, bounds: Bounds, - content_mask: Bounds, + content_mask: ContentMask, color: Hsla, thickness: f32, wavy: u32, @@ -1051,7 +1064,7 @@ fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) out.position = to_device_position(unit_vertex, underline.bounds); out.color = hsla_to_rgba(underline.color); out.underline_id = instance_id; - out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask); + out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask.bounds); return out; } @@ -1093,7 +1106,7 @@ struct MonochromeSprite { order: u32, pad: u32, bounds: Bounds, - content_mask: Bounds, + content_mask: ContentMask, color: Hsla, tile: AtlasTile, transformation: TransformationMatrix, @@ -1117,7 +1130,7 @@ fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index out.tile_position = to_tile_position(unit_vertex, sprite.tile); out.color = hsla_to_rgba(sprite.color); - out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); + out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask.bounds); return out; } @@ -1139,7 +1152,7 @@ struct PolychromeSprite { grayscale: u32, opacity: f32, bounds: Bounds, - content_mask: Bounds, + content_mask: ContentMask, corner_radii: Corners, tile: AtlasTile, } @@ -1161,7 +1174,7 @@ fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index out.position = to_device_position(unit_vertex, sprite.bounds); out.tile_position = to_tile_position(unit_vertex, sprite.tile); out.sprite_id = instance_id; - out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); + out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask.bounds); return out; } @@ -1234,3 +1247,12 @@ fn fs_surface(input: SurfaceVarying) -> @location(0) vec4 { return ycbcr_to_RGB * y_cb_cr; } + +fn max_corner_radii(a: Corners, b: Corners) -> Corners { + return Corners( + max(a.top_left, b.top_left), + max(a.top_right, b.top_right), + max(a.bottom_right, b.bottom_right), + max(a.bottom_left, b.bottom_left) + ); +} diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index 83c978b853443d5c612f514625f94b6d6725be8a..6aa1d18ee895ec84a6f19edbcc6618cf713aa5a5 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders.metal @@ -99,8 +99,21 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], constant Quad *quads [[buffer(QuadInputIndex_Quads)]]) { Quad quad = quads[input.quad_id]; + + // Signed distance field threshold for inclusion of pixels. 0.5 is the + // minimum distance between the center of the pixel and the edge. + const float antialias_threshold = 0.5; + float4 background_color = fill_color(quad.background, input.position.xy, quad.bounds, input.background_solid, input.background_color0, input.background_color1); + float4 border_color = input.border_color; + + // Apply content_mask corner radii clipping + float clip_sdf = quad_sdf(input.position.xy, quad.content_mask.bounds, + quad.content_mask.corner_radii); + float clip_alpha = saturate(antialias_threshold - clip_sdf); + background_color.a *= clip_alpha; + border_color *= clip_alpha; bool unrounded = quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 && @@ -121,10 +134,6 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], float2 point = input.position.xy - float2(quad.bounds.origin.x, quad.bounds.origin.y); float2 center_to_point = point - half_size; - // Signed distance field threshold for inclusion of pixels. 0.5 is the - // minimum distance between the center of the pixel and the edge. - const float antialias_threshold = 0.5; - // Radius of the nearest corner float corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); @@ -164,7 +173,6 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], straight_border_inner_corner_to_point.x > 0.0 || straight_border_inner_corner_to_point.y > 0.0; - // Whether the point is far enough inside the quad, such that the pixels are // not affected by the straight border. bool is_within_inner_straight_border = @@ -208,8 +216,6 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], float4 color = background_color; if (border_sdf < antialias_threshold) { - float4 border_color = input.border_color; - // Dashed border logic when border_style == 1 if (quad.border_style == 1) { // Position along the perimeter in "dash space", where each dash @@ -244,6 +250,10 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], // perimeter. This way each line starts and ends with a dash. bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y; float border_width = is_horizontal ? border.x : border.y; + // When border width of some side is 0, we need to use the other side width for dash velocity. + if (border_width == 0.0) { + border_width = is_horizontal ? border.y : border.x; + } dash_velocity = dv_numerator / border_width; t = is_horizontal ? point.x : point.y; t *= dash_velocity; diff --git a/crates/gpui/src/platform/windows/shaders.hlsl b/crates/gpui/src/platform/windows/shaders.hlsl index 2cef54ae6166e313795eb42210b5f07c1bc378fc..296a6c825f8b057b5330d108c21c27ffb4fa1b75 100644 --- a/crates/gpui/src/platform/windows/shaders.hlsl +++ b/crates/gpui/src/platform/windows/shaders.hlsl @@ -453,11 +453,16 @@ float quarter_ellipse_sdf(float2 pt, float2 radii) { ** */ +struct ContentMask { + Bounds bounds; + Corners corner_radii; +}; + struct Quad { uint order; uint border_style; Bounds bounds; - Bounds content_mask; + ContentMask content_mask; Background background; Hsla border_color; Corners corner_radii; @@ -496,7 +501,7 @@ QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint quad_id: SV_Insta quad.background.solid, quad.background.colors ); - float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask); + float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask.bounds); float4 border_color = hsla_to_rgba(quad.border_color); QuadVertexOutput output; @@ -512,8 +517,21 @@ QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint quad_id: SV_Insta float4 quad_fragment(QuadFragmentInput input): SV_Target { Quad quad = quads[input.quad_id]; + + // Signed distance field threshold for inclusion of pixels. 0.5 is the + // minimum distance between the center of the pixel and the edge. + const float antialias_threshold = 0.5; + float4 background_color = gradient_color(quad.background, input.position.xy, quad.bounds, - input.background_solid, input.background_color0, input.background_color1); + input.background_solid, input.background_color0, input.background_color1); + float4 border_color = input.border_color; + + // Apply content_mask corner radii clipping + float clip_sdf = quad_sdf(input.position.xy, quad.content_mask.bounds, + quad.content_mask.corner_radii); + float clip_alpha = saturate(antialias_threshold - clip_sdf); + background_color.a *= clip_alpha; + border_color *= clip_alpha; bool unrounded = quad.corner_radii.top_left == 0.0 && quad.corner_radii.top_right == 0.0 && @@ -534,10 +552,6 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target { float2 the_point = input.position.xy - quad.bounds.origin; float2 center_to_point = the_point - half_size; - // Signed distance field threshold for inclusion of pixels. 0.5 is the - // minimum distance between the center of the pixel and the edge. - const float antialias_threshold = 0.5; - // Radius of the nearest corner float corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); @@ -620,7 +634,6 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target { float4 color = background_color; if (border_sdf < antialias_threshold) { - float4 border_color = input.border_color; // Dashed border logic when border_style == 1 if (quad.border_style == 1) { // Position along the perimeter in "dash space", where each dash @@ -655,6 +668,10 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target { // perimeter. This way each line starts and ends with a dash. bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y; float border_width = is_horizontal ? border.x : border.y; + // When border width of some side is 0, we need to use the other side width for dash velocity. + if (border_width == 0.0) { + border_width = is_horizontal ? border.y : border.x; + } dash_velocity = dv_numerator / border_width; t = is_horizontal ? the_point.x : the_point.y; t *= dash_velocity; @@ -805,7 +822,7 @@ struct Shadow { float blur_radius; Bounds bounds; Corners corner_radii; - Bounds content_mask; + ContentMask content_mask; Hsla color; }; @@ -834,7 +851,7 @@ ShadowVertexOutput shadow_vertex(uint vertex_id: SV_VertexID, uint shadow_id: SV bounds.size += 2.0 * margin; float4 device_position = to_device_position(unit_vertex, bounds); - float4 clip_distance = distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask); + float4 clip_distance = distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask.bounds); float4 color = hsla_to_rgba(shadow.color); ShadowVertexOutput output; @@ -987,7 +1004,7 @@ struct Underline { uint order; uint pad; Bounds bounds; - Bounds content_mask; + ContentMask content_mask; Hsla color; float thickness; uint wavy; @@ -1013,7 +1030,7 @@ UnderlineVertexOutput underline_vertex(uint vertex_id: SV_VertexID, uint underli Underline underline = underlines[underline_id]; float4 device_position = to_device_position(unit_vertex, underline.bounds); float4 clip_distance = distance_from_clip_rect(unit_vertex, underline.bounds, - underline.content_mask); + underline.content_mask.bounds); float4 color = hsla_to_rgba(underline.color); UnderlineVertexOutput output; @@ -1061,7 +1078,7 @@ struct MonochromeSprite { uint order; uint pad; Bounds bounds; - Bounds content_mask; + ContentMask content_mask; Hsla color; AtlasTile tile; TransformationMatrix transformation; @@ -1088,7 +1105,7 @@ MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexI MonochromeSprite sprite = mono_sprites[sprite_id]; float4 device_position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation); - float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); + float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask.bounds); float2 tile_position = to_tile_position(unit_vertex, sprite.tile); float4 color = hsla_to_rgba(sprite.color); @@ -1118,7 +1135,7 @@ struct PolychromeSprite { uint grayscale; float opacity; Bounds bounds; - Bounds content_mask; + ContentMask content_mask; Corners corner_radii; AtlasTile tile; }; @@ -1143,7 +1160,7 @@ PolychromeSpriteVertexOutput polychrome_sprite_vertex(uint vertex_id: SV_VertexI PolychromeSprite sprite = poly_sprites[sprite_id]; float4 device_position = to_device_position(unit_vertex, sprite.bounds); float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, - sprite.content_mask); + sprite.content_mask.bounds); float2 tile_position = to_tile_position(unit_vertex, sprite.tile); PolychromeSpriteVertexOutput output; diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 5b69ce7fa6eb06affc2f77c0d1bdfbe4165c206a..09f598f9b0deb4ddd9910e32e4dbe020b54c849a 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -601,7 +601,19 @@ impl Style { (false, false) => Bounds::from_corners(min, max), }; - Some(ContentMask { bounds }) + let corner_radii = self.corner_radii.to_pixels(rem_size); + let border_widths = self.border_widths.to_pixels(rem_size); + Some(ContentMask { + bounds: Bounds { + origin: bounds.origin - point(border_widths.left, border_widths.top), + size: bounds.size + + size( + border_widths.left + border_widths.right, + border_widths.top + border_widths.bottom, + ), + }, + corner_radii, + }) } } } @@ -661,64 +673,16 @@ impl Style { if self.is_border_visible() { let border_widths = self.border_widths.to_pixels(rem_size); - let max_border_width = border_widths.max(); - let max_corner_radius = corner_radii.max(); - - let top_bounds = Bounds::from_corners( - bounds.origin, - bounds.top_right() + point(Pixels::ZERO, max_border_width.max(max_corner_radius)), - ); - let bottom_bounds = Bounds::from_corners( - bounds.bottom_left() - point(Pixels::ZERO, max_border_width.max(max_corner_radius)), - bounds.bottom_right(), - ); - let left_bounds = Bounds::from_corners( - top_bounds.bottom_left(), - bottom_bounds.origin + point(max_border_width, Pixels::ZERO), - ); - let right_bounds = Bounds::from_corners( - top_bounds.bottom_right() - point(max_border_width, Pixels::ZERO), - bottom_bounds.top_right(), - ); - let mut background = self.border_color.unwrap_or_default(); background.a = 0.; - let quad = quad( + window.paint_quad(quad( bounds, corner_radii, background, border_widths, self.border_color.unwrap_or_default(), self.border_style, - ); - - window.with_content_mask(Some(ContentMask { bounds: top_bounds }), |window| { - window.paint_quad(quad.clone()); - }); - window.with_content_mask( - Some(ContentMask { - bounds: right_bounds, - }), - |window| { - window.paint_quad(quad.clone()); - }, - ); - window.with_content_mask( - Some(ContentMask { - bounds: bottom_bounds, - }), - |window| { - window.paint_quad(quad.clone()); - }, - ); - window.with_content_mask( - Some(ContentMask { - bounds: left_bounds, - }), - |window| { - window.paint_quad(quad); - }, - ); + )); } #[cfg(debug_assertions)] diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 0ec73c4b0040e6c65cd8819ecf5d20a9ec1900d0..cebf911b734798a3965857db0f3fd6fe2be2b011 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1283,6 +1283,8 @@ pub(crate) struct DispatchEventResult { pub struct ContentMask { /// The bounds pub bounds: Bounds

, + /// The corner radii of the content mask. + pub corner_radii: Corners

, } impl ContentMask { @@ -1290,13 +1292,31 @@ impl ContentMask { pub fn scale(&self, factor: f32) -> ContentMask { ContentMask { bounds: self.bounds.scale(factor), + corner_radii: self.corner_radii.scale(factor), } } /// Intersect the content mask with the given content mask. pub fn intersect(&self, other: &Self) -> Self { let bounds = self.bounds.intersect(&other.bounds); - ContentMask { bounds } + ContentMask { + bounds, + corner_radii: Corners { + top_left: self.corner_radii.top_left.max(other.corner_radii.top_left), + top_right: self + .corner_radii + .top_right + .max(other.corner_radii.top_right), + bottom_right: self + .corner_radii + .bottom_right + .max(other.corner_radii.bottom_right), + bottom_left: self + .corner_radii + .bottom_left + .max(other.corner_radii.bottom_left), + }, + } } } @@ -2557,6 +2577,7 @@ impl Window { origin: Point::default(), size: self.viewport_size, }, + ..Default::default() }) } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 5bbf5ad36b3de89514d92ce9e305988817cec32f..e4d7f226912cdb6123081dd5cfb38f205d9a9333 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1184,7 +1184,7 @@ impl Element for TerminalElement { cx: &mut App, ) { let paint_start = Instant::now(); - window.with_content_mask(Some(ContentMask { bounds }), |window| { + window.with_content_mask(Some(ContentMask { bounds, ..Default::default() }), |window| { let scroll_top = self.terminal_view.read(cx).scroll_top; window.paint_quad(fill(bounds, layout.background_color)); diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index 605028202fffa37d67bbdb4a9f33a97459390dfa..475575a483ea4659b572f8f82e13faf8b539fb37 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -303,9 +303,13 @@ impl Element for Scrollbar { window: &mut Window, _: &mut App, ) -> Self::PrepaintState { - window.with_content_mask(Some(ContentMask { bounds }), |window| { - window.insert_hitbox(bounds, HitboxBehavior::Normal) - }) + window.with_content_mask( + Some(ContentMask { + bounds, + ..Default::default() + }), + |window| window.insert_hitbox(bounds, HitboxBehavior::Normal), + ) } fn paint( @@ -319,7 +323,11 @@ impl Element for Scrollbar { cx: &mut App, ) { const EXTRA_PADDING: Pixels = px(5.0); - window.with_content_mask(Some(ContentMask { bounds }), |window| { + let content_mask = ContentMask { + bounds, + ..Default::default() + }; + window.with_content_mask(Some(content_mask), |window| { let axis = self.kind; let colors = cx.theme().colors(); let thumb_state = self.state.thumb_state.get(); From 91cbb2ec25693c901f847a9bcbbebb97146c74bc Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Wed, 3 Sep 2025 12:59:14 +0200 Subject: [PATCH 544/744] Add onboarding banner for claude code support (#37443) Release Notes: - N/A --------- Co-authored-by: Danilo Leal --- assets/images/acp_logo_serif.svg | 48 +++- crates/agent_ui/src/agent_panel.rs | 7 +- crates/agent_ui/src/ui.rs | 2 + .../agent_ui/src/ui/acp_onboarding_modal.rs | 20 +- .../src/ui/claude_code_onboarding_modal.rs | 254 ++++++++++++++++++ crates/title_bar/src/onboarding_banner.rs | 11 +- crates/title_bar/src/title_bar.rs | 10 +- crates/zed_actions/src/lib.rs | 2 + 8 files changed, 330 insertions(+), 24 deletions(-) create mode 100644 crates/agent_ui/src/ui/claude_code_onboarding_modal.rs diff --git a/assets/images/acp_logo_serif.svg b/assets/images/acp_logo_serif.svg index 6bc359cf82dde8060a66c051c8727f0e0624b938..a04d32e51c43acf358baa733f03284dbb6de1369 100644 --- a/assets/images/acp_logo_serif.svg +++ b/assets/images/acp_logo_serif.svg @@ -1,2 +1,46 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index cfa5b56358863ece6ab1f6dd024e7be365766853..305261183e92de6dbe2ad5756293e8b0bbf77849 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -10,11 +10,11 @@ use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; use zed_actions::OpenBrowser; -use zed_actions::agent::ReauthenticateAgent; +use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent}; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::agent_diff::AgentDiffThread; -use crate::ui::AcpOnboardingModal; +use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal}; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, @@ -207,6 +207,9 @@ pub fn init(cx: &mut App) { .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| { AcpOnboardingModal::toggle(workspace, window, cx) }) + .register_action(|workspace, _: &OpenClaudeCodeOnboardingModal, window, cx| { + ClaudeCodeOnboardingModal::toggle(workspace, window, cx) + }) .register_action(|_workspace, _: &ResetOnboarding, window, cx| { window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx); window.refresh(); diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 600698b07e1e2bf43d78c5c225838476f04a5c76..1a3264bd77ccda1a27ffd19f3c61c3635fe78dc9 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -1,6 +1,7 @@ mod acp_onboarding_modal; mod agent_notification; mod burn_mode_tooltip; +mod claude_code_onboarding_modal; mod context_pill; mod end_trial_upsell; mod onboarding_modal; @@ -10,6 +11,7 @@ mod unavailable_editing_tooltip; pub use acp_onboarding_modal::*; pub use agent_notification::*; pub use burn_mode_tooltip::*; +pub use claude_code_onboarding_modal::*; pub use context_pill::*; pub use end_trial_upsell::*; pub use onboarding_modal::*; diff --git a/crates/agent_ui/src/ui/acp_onboarding_modal.rs b/crates/agent_ui/src/ui/acp_onboarding_modal.rs index 0ed9de7221014476f21c0406e6be8ac3592fca7c..8433904fb3b540c2d78c8634b7a6755303d6e15c 100644 --- a/crates/agent_ui/src/ui/acp_onboarding_modal.rs +++ b/crates/agent_ui/src/ui/acp_onboarding_modal.rs @@ -141,20 +141,12 @@ impl Render for AcpOnboardingModal { .bg(gpui::black().opacity(0.15)), ) .child( - h_flex() - .gap_4() - .child( - Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.)) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), - ) - .child( - Vector::new( - VectorName::AcpLogoSerif, - rems_from_px(111.), - rems_from_px(41.), - ) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), - ), + Vector::new( + VectorName::AcpLogoSerif, + rems_from_px(257.), + rems_from_px(47.), + ) + .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), ) .child( v_flex() diff --git a/crates/agent_ui/src/ui/claude_code_onboarding_modal.rs b/crates/agent_ui/src/ui/claude_code_onboarding_modal.rs new file mode 100644 index 0000000000000000000000000000000000000000..06980f18977aefe228bb7f09962e69fe2b3a5068 --- /dev/null +++ b/crates/agent_ui/src/ui/claude_code_onboarding_modal.rs @@ -0,0 +1,254 @@ +use client::zed_urls; +use gpui::{ + ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render, + linear_color_stop, linear_gradient, +}; +use ui::{TintColor, Vector, VectorName, prelude::*}; +use workspace::{ModalView, Workspace}; + +use crate::agent_panel::{AgentPanel, AgentType}; + +macro_rules! claude_code_onboarding_event { + ($name:expr) => { + telemetry::event!($name, source = "ACP Claude Code Onboarding"); + }; + ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => { + telemetry::event!($name, source = "ACP Claude Code Onboarding", $($key $(= $value)?),+); + }; +} + +pub struct ClaudeCodeOnboardingModal { + focus_handle: FocusHandle, + workspace: Entity, +} + +impl ClaudeCodeOnboardingModal { + pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { + let workspace_entity = cx.entity(); + workspace.toggle_modal(window, cx, |_window, cx| Self { + workspace: workspace_entity, + focus_handle: cx.focus_handle(), + }); + } + + fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { + self.workspace.update(cx, |workspace, cx| { + workspace.focus_panel::(window, cx); + + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel.new_agent_thread(AgentType::ClaudeCode, window, cx); + }); + } + }); + + cx.emit(DismissEvent); + + claude_code_onboarding_event!("Open Panel Clicked"); + } + + fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { + cx.open_url(&zed_urls::external_agents_docs(cx)); + cx.notify(); + + claude_code_onboarding_event!("Documentation Link Clicked"); + } + + fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { + cx.emit(DismissEvent); + } +} + +impl EventEmitter for ClaudeCodeOnboardingModal {} + +impl Focusable for ClaudeCodeOnboardingModal { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl ModalView for ClaudeCodeOnboardingModal {} + +impl Render for ClaudeCodeOnboardingModal { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let illustration_element = |icon: IconName, label: Option, opacity: f32| { + h_flex() + .px_1() + .py_0p5() + .gap_1() + .rounded_sm() + .bg(cx.theme().colors().element_active.opacity(0.05)) + .border_1() + .border_color(cx.theme().colors().border) + .border_dashed() + .child( + Icon::new(icon) + .size(IconSize::Small) + .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))), + ) + .map(|this| { + if let Some(label_text) = label { + this.child( + Label::new(label_text) + .size(LabelSize::Small) + .color(Color::Muted), + ) + } else { + this.child( + div().w_16().h_1().rounded_full().bg(cx + .theme() + .colors() + .element_active + .opacity(0.6)), + ) + } + }) + .opacity(opacity) + }; + + let illustration = h_flex() + .relative() + .h(rems_from_px(126.)) + .bg(cx.theme().colors().editor_background) + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .justify_center() + .gap_8() + .rounded_t_md() + .overflow_hidden() + .child( + div().absolute().inset_0().w(px(515.)).h(px(126.)).child( + Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.)) + .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))), + ), + ) + .child(div().absolute().inset_0().size_full().bg(linear_gradient( + 0., + linear_color_stop( + cx.theme().colors().elevated_surface_background.opacity(0.1), + 0.9, + ), + linear_color_stop( + cx.theme().colors().elevated_surface_background.opacity(0.), + 0., + ), + ))) + .child( + div() + .absolute() + .inset_0() + .size_full() + .bg(gpui::black().opacity(0.15)), + ) + .child( + Vector::new( + VectorName::AcpLogoSerif, + rems_from_px(257.), + rems_from_px(47.), + ) + .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), + ) + .child( + v_flex() + .gap_1p5() + .child(illustration_element(IconName::Stop, None, 0.15)) + .child(illustration_element( + IconName::AiGemini, + Some("New Gemini CLI Thread".into()), + 0.3, + )) + .child( + h_flex() + .pl_1() + .pr_2() + .py_0p5() + .gap_1() + .rounded_sm() + .bg(cx.theme().colors().element_active.opacity(0.2)) + .border_1() + .border_color(cx.theme().colors().border) + .child( + Icon::new(IconName::AiClaude) + .size(IconSize::Small) + .color(Color::Muted), + ) + .child(Label::new("New Claude Code Thread").size(LabelSize::Small)), + ) + .child(illustration_element( + IconName::Stop, + Some("Your Agent Here".into()), + 0.3, + )) + .child(illustration_element(IconName::Stop, None, 0.15)), + ); + + let heading = v_flex() + .w_full() + .gap_1() + .child( + Label::new("Beta Release") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child(Headline::new("Claude Code: Natively in Zed").size(HeadlineSize::Large)); + + let copy = "Powered by the Agent Client Protocol, you can now run Claude Code as\na first-class citizen in Zed's agent panel."; + + let open_panel_button = Button::new("open-panel", "Start with Claude Code") + .icon_size(IconSize::Indicator) + .style(ButtonStyle::Tinted(TintColor::Accent)) + .full_width() + .on_click(cx.listener(Self::open_panel)); + + let docs_button = Button::new("add-other-agents", "Add Other Agents") + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::Indicator) + .icon_color(Color::Muted) + .full_width() + .on_click(cx.listener(Self::view_docs)); + + let close_button = h_flex().absolute().top_2().right_2().child( + IconButton::new("cancel", IconName::Close).on_click(cx.listener( + |_, _: &ClickEvent, _window, cx| { + claude_code_onboarding_event!("Canceled", trigger = "X click"); + cx.emit(DismissEvent); + }, + )), + ); + + v_flex() + .id("acp-onboarding") + .key_context("AcpOnboardingModal") + .relative() + .w(rems(34.)) + .h_full() + .elevation_3(cx) + .track_focus(&self.focus_handle(cx)) + .overflow_hidden() + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| { + claude_code_onboarding_event!("Canceled", trigger = "Action"); + cx.emit(DismissEvent); + })) + .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| { + this.focus_handle.focus(window); + })) + .child(illustration) + .child( + v_flex() + .p_4() + .gap_2() + .child(heading) + .child(Label::new(copy).color(Color::Muted)) + .child( + v_flex() + .w_full() + .mt_2() + .gap_1() + .child(open_panel_button) + .child(docs_button), + ), + ) + .child(close_button) + } +} diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs index 1c2894249000861f6de14f4960205e5deffab47b..6adc5769498ee19a7139c3fd02bd586e32185778 100644 --- a/crates/title_bar/src/onboarding_banner.rs +++ b/crates/title_bar/src/onboarding_banner.rs @@ -7,6 +7,7 @@ pub struct OnboardingBanner { dismissed: bool, source: String, details: BannerDetails, + visible_when: Option bool>>, } #[derive(Clone)] @@ -42,12 +43,18 @@ impl OnboardingBanner { label: label.into(), subtitle: subtitle.or(Some(SharedString::from("Introducing:"))), }, + visible_when: None, dismissed: get_dismissed(source), } } - fn should_show(&self, _cx: &mut App) -> bool { - !self.dismissed + pub fn visible_when(mut self, predicate: impl Fn(&mut App) -> bool + 'static) -> Self { + self.visible_when = Some(Box::new(predicate)); + self + } + + fn should_show(&self, cx: &mut App) -> bool { + !self.dismissed && self.visible_when.as_ref().map_or(true, |f| f(cx)) } fn dismiss(&mut self, cx: &mut Context) { diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 2b13ef58c3a8707b81d6870590efe5337ffef048..f031b8394afc551c8077419f504104936095a0c3 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -279,13 +279,15 @@ impl TitleBar { let banner = cx.new(|cx| { OnboardingBanner::new( - "ACP Onboarding", - IconName::Sparkle, - "Bring Your Own Agent", + "ACP Claude Code Onboarding", + IconName::AiClaude, + "Claude Code", Some("Introducing:".into()), - zed_actions::agent::OpenAcpOnboardingModal.boxed_clone(), + zed_actions::agent::OpenClaudeCodeOnboardingModal.boxed_clone(), cx, ) + // When updating this to a non-AI feature release, remove this line. + .visible_when(|cx| !project::DisableAiSettings::get_global(cx).disable_ai) }); let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx)); diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 8f4c42ca496e26d23765eb006d7eb0fe9db197ee..bc47b8f1e47d3a550d44af3bc852b136fa8b8bfc 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -286,6 +286,8 @@ pub mod agent { OpenOnboardingModal, /// Opens the ACP onboarding modal. OpenAcpOnboardingModal, + /// Opens the Claude Code onboarding modal. + OpenClaudeCodeOnboardingModal, /// Resets the agent onboarding state. ResetOnboarding, /// Starts a chat conversation with the agent. From 7633bbf55a79d29acd9feb330f17f323a9de800f Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Wed, 3 Sep 2025 14:08:48 +0200 Subject: [PATCH 545/744] acp: Fix issue with claude code /logout command (#37452) ### First issue In the scenario where you have an API key configured in Zed and you run `/logout`, clicking on `Use Anthropic API Key` would show `Method not implemented`. This happened because we were only intercepting the `Use Anthropic API Key` click if the provider was NOT authenticated, which would not be the case when the user has an API key set. ### Second issue When clicking on `Reset API Key` the modal would be dismissed even though you picked no Authentication Method (which means you still would be unauthenticated) --- This PR fixes both of these issues Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 32 +++++++++++++++++++------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 589633ae250580f6eb66a513534c79a898fdc0d6..992e12177abe144b1ba00b7a5e2a9c8806866593 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -666,6 +666,10 @@ impl AcpThreadView { move |_, ev, window, cx| { if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev && &provider_id == updated_provider_id + && LanguageModelRegistry::global(cx) + .read(cx) + .provider(&provider_id) + .map_or(false, |provider| provider.is_authenticated(cx)) { this.update(cx, |this, cx| { this.thread_state = Self::initial_state( @@ -1365,11 +1369,11 @@ impl AcpThreadView { .read(cx) .provider(&language_model::ANTHROPIC_PROVIDER_ID) .unwrap(); - if !provider.is_authenticated(cx) { - let this = cx.weak_entity(); - let agent = self.agent.clone(); - let connection = connection.clone(); - window.defer(cx, |window, cx| { + let this = cx.weak_entity(); + let agent = self.agent.clone(); + let connection = connection.clone(); + window.defer(cx, move |window, cx| { + if !provider.is_authenticated(cx) { Self::handle_auth_required( this, AuthRequired { @@ -1381,9 +1385,21 @@ impl AcpThreadView { window, cx, ); - }); - return; - } + } else { + this.update(cx, |this, cx| { + this.thread_state = Self::initial_state( + agent, + None, + this.workspace.clone(), + this.project.clone(), + window, + cx, + ) + }) + .ok(); + } + }); + return; } else if method.0.as_ref() == "vertex-ai" && std::env::var("GOOGLE_API_KEY").is_err() && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err() From ebc22c290bd0222261c7719dfc781ce5692060ca Mon Sep 17 00:00:00 2001 From: Nia Date: Wed, 3 Sep 2025 15:44:07 +0200 Subject: [PATCH 546/744] gpui: Don't risk accidentally panicking during tests (#37457) See the failure in https://github.com/zed-industries/zed/actions/runs/17413839503/job/49437345296 Release Notes: - N/A --- crates/gpui/src/test.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/gpui/src/test.rs b/crates/gpui/src/test.rs index 4794fd002e28595a5d165ff3ac5876ea31c8ce20..5ae72d2be1688893374e16a55445558b5bc33040 100644 --- a/crates/gpui/src/test.rs +++ b/crates/gpui/src/test.rs @@ -64,6 +64,9 @@ pub fn run_test( if attempt < max_retries { println!("attempt {} failed, retrying", attempt); attempt += 1; + // The panic payload might itself trigger an unwind on drop: + // https://doc.rust-lang.org/std/panic/fn.catch_unwind.html#notes + std::mem::forget(error); } else { if is_multiple_runs { eprintln!("failing seed: {}", seed); From d80f9dda75528449d27ea7eff5ea170e617689d6 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 3 Sep 2025 16:11:36 +0200 Subject: [PATCH 547/744] languages: Fix python tasks failing when binary contains whitespaces (#37454) Fixes https://github.com/zed-industries/zed/issues/33459 Release Notes: - Fixed python tasks failing when the python binary path contains whitespaces --- crates/languages/src/python.rs | 39 ++++++++----------- .../project/src/debugger/locators/python.rs | 16 ++------ 2 files changed, 21 insertions(+), 34 deletions(-) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 5bdc4aa0d94a7355c60ab8912d9a328a657ad77f..bd3a7b34cf873328e480012ca96fcbe9fc3eff95 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -409,9 +409,6 @@ const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName = const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName = VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN")); -const PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW: VariableName = - VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW")); - const PYTHON_MODULE_NAME_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("PYTHON_MODULE_NAME")); @@ -435,7 +432,7 @@ impl ContextProvider for PythonContextProvider { let worktree_id = location_file.as_ref().map(|f| f.worktree_id(cx)); cx.spawn(async move |cx| { - let raw_toolchain = if let Some(worktree_id) = worktree_id { + let active_toolchain = if let Some(worktree_id) = worktree_id { let file_path = location_file .as_ref() .and_then(|f| f.path().parent()) @@ -453,15 +450,13 @@ impl ContextProvider for PythonContextProvider { String::from("python3") }; - let active_toolchain = format!("\"{raw_toolchain}\""); let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain); - let raw_toolchain_var = (PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW, raw_toolchain); Ok(task::TaskVariables::from_iter( test_target .into_iter() .chain(module_target.into_iter()) - .chain([toolchain, raw_toolchain_var]), + .chain([toolchain]), )) }) } @@ -478,31 +473,31 @@ impl ContextProvider for PythonContextProvider { // Execute a selection TaskTemplate { label: "execute selection".to_owned(), - command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(), args: vec![ "-c".to_owned(), VariableName::SelectedText.template_value_with_whitespace(), ], - cwd: Some("$ZED_WORKTREE_ROOT".into()), + cwd: Some(VariableName::WorktreeRoot.template_value()), ..TaskTemplate::default() }, // Execute an entire file TaskTemplate { label: format!("run '{}'", VariableName::File.template_value()), - command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(), args: vec![VariableName::File.template_value_with_whitespace()], - cwd: Some("$ZED_WORKTREE_ROOT".into()), + cwd: Some(VariableName::WorktreeRoot.template_value()), ..TaskTemplate::default() }, // Execute a file as module TaskTemplate { label: format!("run module '{}'", VariableName::File.template_value()), - command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(), args: vec![ "-m".to_owned(), - PYTHON_MODULE_NAME_TASK_VARIABLE.template_value(), + PYTHON_MODULE_NAME_TASK_VARIABLE.template_value_with_whitespace(), ], - cwd: Some("$ZED_WORKTREE_ROOT".into()), + cwd: Some(VariableName::WorktreeRoot.template_value()), tags: vec!["python-module-main-method".to_owned()], ..TaskTemplate::default() }, @@ -514,19 +509,19 @@ impl ContextProvider for PythonContextProvider { // Run tests for an entire file TaskTemplate { label: format!("unittest '{}'", VariableName::File.template_value()), - command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(), args: vec![ "-m".to_owned(), "unittest".to_owned(), VariableName::File.template_value_with_whitespace(), ], - cwd: Some("$ZED_WORKTREE_ROOT".into()), + cwd: Some(VariableName::WorktreeRoot.template_value()), ..TaskTemplate::default() }, // Run test(s) for a specific target within a file TaskTemplate { label: "unittest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(), - command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(), args: vec![ "-m".to_owned(), "unittest".to_owned(), @@ -536,7 +531,7 @@ impl ContextProvider for PythonContextProvider { "python-unittest-class".to_owned(), "python-unittest-method".to_owned(), ], - cwd: Some("$ZED_WORKTREE_ROOT".into()), + cwd: Some(VariableName::WorktreeRoot.template_value()), ..TaskTemplate::default() }, ] @@ -546,25 +541,25 @@ impl ContextProvider for PythonContextProvider { // Run tests for an entire file TaskTemplate { label: format!("pytest '{}'", VariableName::File.template_value()), - command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(), args: vec![ "-m".to_owned(), "pytest".to_owned(), VariableName::File.template_value_with_whitespace(), ], - cwd: Some("$ZED_WORKTREE_ROOT".into()), + cwd: Some(VariableName::WorktreeRoot.template_value()), ..TaskTemplate::default() }, // Run test(s) for a specific target within a file TaskTemplate { label: "pytest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(), - command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(), args: vec![ "-m".to_owned(), "pytest".to_owned(), PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(), ], - cwd: Some("$ZED_WORKTREE_ROOT".into()), + cwd: Some(VariableName::WorktreeRoot.template_value()), tags: vec![ "python-pytest-class".to_owned(), "python-pytest-method".to_owned(), diff --git a/crates/project/src/debugger/locators/python.rs b/crates/project/src/debugger/locators/python.rs index 71efbb75b91c819fcfdb857769877452f9e5a730..06f7ab2e796c8139f2f8723b95f7f4503250a0c3 100644 --- a/crates/project/src/debugger/locators/python.rs +++ b/crates/project/src/debugger/locators/python.rs @@ -25,7 +25,7 @@ impl DapLocator for PythonLocator { if adapter.0.as_ref() != "Debugpy" { return None; } - let valid_program = build_config.command.starts_with("$ZED_") + let valid_program = build_config.command.starts_with("\"$ZED_") || Path::new(&build_config.command) .file_name() .is_some_and(|name| name.to_str().is_some_and(|path| path.starts_with("python"))); @@ -33,13 +33,7 @@ impl DapLocator for PythonLocator { // We cannot debug selections. return None; } - let command = if build_config.command - == VariableName::Custom("PYTHON_ACTIVE_ZED_TOOLCHAIN".into()).template_value() - { - VariableName::Custom("PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW".into()).template_value() - } else { - build_config.command.clone() - }; + let command = build_config.command.clone(); let module_specifier_position = build_config .args .iter() @@ -57,10 +51,8 @@ impl DapLocator for PythonLocator { let program_position = mod_name .is_none() .then(|| { - build_config - .args - .iter() - .position(|arg| *arg == "\"$ZED_FILE\"") + let zed_file = VariableName::File.template_value_with_whitespace(); + build_config.args.iter().position(|arg| *arg == zed_file) }) .flatten(); let args = if let Some(position) = program_position { From 92283285aef346cd69e6c748d3fde635474803b2 Mon Sep 17 00:00:00 2001 From: localcc Date: Wed, 3 Sep 2025 16:14:56 +0200 Subject: [PATCH 548/744] Fix rendering on devices that don't support MapOnDefaultTextures (#37456) Closes #37231 Release Notes: - N/A --- crates/gpui/src/platform/windows/direct_write.rs | 2 +- crates/gpui/src/platform/windows/directx_atlas.rs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index 285f6a1143d7edadd1530abf07051fae254595da..ec6d008e7b9614c146056da7adba2607ea9be909 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -1253,7 +1253,7 @@ impl GlyphLayerTexture { }, Usage: D3D11_USAGE_DEFAULT, BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, + CPUAccessFlags: 0, MiscFlags: 0, }; diff --git a/crates/gpui/src/platform/windows/directx_atlas.rs b/crates/gpui/src/platform/windows/directx_atlas.rs index 6bced4c11d922ed2c514b9a70fe7e582d7b15a6b..38c22a41bf9d32cf43f585050390b75602a6bf42 100644 --- a/crates/gpui/src/platform/windows/directx_atlas.rs +++ b/crates/gpui/src/platform/windows/directx_atlas.rs @@ -3,9 +3,8 @@ use etagere::BucketedAtlasAllocator; use parking_lot::Mutex; use windows::Win32::Graphics::{ Direct3D11::{ - D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_CPU_ACCESS_WRITE, D3D11_TEXTURE2D_DESC, - D3D11_USAGE_DEFAULT, ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView, - ID3D11Texture2D, + D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, + ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView, ID3D11Texture2D, }, Dxgi::Common::*, }; @@ -189,7 +188,7 @@ impl DirectXAtlasState { }, Usage: D3D11_USAGE_DEFAULT, BindFlags: bind_flag.0 as u32, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, + CPUAccessFlags: 0, MiscFlags: 0, }; let mut texture: Option = None; From c1ca7303a86362e3b985c37e296bb28c31a351a6 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 3 Sep 2025 16:22:35 +0200 Subject: [PATCH 549/744] editor: Make blame and inline blame work for multibuffers (#37366) Release Notes: - Added blame view and inline blame support for multi buffer editors --------- Co-authored-by: Kirill Bulatov --- crates/collab/src/tests/editor_tests.rs | 22 +- crates/editor/src/editor.rs | 139 +++----- crates/editor/src/editor_tests.rs | 19 +- crates/editor/src/element.rs | 38 ++- crates/editor/src/git/blame.rs | 370 ++++++++++++++-------- crates/multi_buffer/src/multi_buffer.rs | 8 +- crates/outline_panel/src/outline_panel.rs | 131 +++++--- crates/project/src/lsp_store.rs | 5 +- crates/search/src/project_search.rs | 21 +- crates/sum_tree/src/tree_map.rs | 1 + 10 files changed, 429 insertions(+), 325 deletions(-) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index bfea497e9b57d806af1f13bb3af7e88521d03816..a3f63c527693a19bb7ac1cd87c104cee3d5cfa6e 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -3425,16 +3425,16 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA assert_eq!( entries, vec![ - Some(blame_entry("1b1b1b", 0..1)), - Some(blame_entry("0d0d0d", 1..2)), - Some(blame_entry("3a3a3a", 2..3)), - Some(blame_entry("4c4c4c", 3..4)), + Some((buffer_id_b, blame_entry("1b1b1b", 0..1))), + Some((buffer_id_b, blame_entry("0d0d0d", 1..2))), + Some((buffer_id_b, blame_entry("3a3a3a", 2..3))), + Some((buffer_id_b, blame_entry("4c4c4c", 3..4))), ] ); blame.update(cx, |blame, _| { - for (idx, entry) in entries.iter().flatten().enumerate() { - let details = blame.details_for_entry(entry).unwrap(); + for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() { + let details = blame.details_for_entry(*buffer, entry).unwrap(); assert_eq!(details.message, format!("message for idx-{}", idx)); assert_eq!( details.permalink.unwrap().to_string(), @@ -3474,9 +3474,9 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA entries, vec![ None, - Some(blame_entry("0d0d0d", 1..2)), - Some(blame_entry("3a3a3a", 2..3)), - Some(blame_entry("4c4c4c", 3..4)), + Some((buffer_id_b, blame_entry("0d0d0d", 1..2))), + Some((buffer_id_b, blame_entry("3a3a3a", 2..3))), + Some((buffer_id_b, blame_entry("4c4c4c", 3..4))), ] ); }); @@ -3511,8 +3511,8 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA vec![ None, None, - Some(blame_entry("3a3a3a", 2..3)), - Some(blame_entry("4c4c4c", 3..4)), + Some((buffer_id_b, blame_entry("3a3a3a", 2..3))), + Some((buffer_id_b, blame_entry("4c4c4c", 3..4))), ] ); }); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 00fe6637bf6c5ff8d8caa3c9c223fc55ffaee3cc..a8e2b001f330423e2108a22eb6f5113c3aa3e78b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -190,7 +190,6 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use sum_tree::TreeMap; use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables}; use text::{BufferId, FromAnchor, OffsetUtf16, Rope}; use theme::{ @@ -227,7 +226,7 @@ const MAX_SELECTION_HISTORY_LEN: usize = 1024; pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000); #[doc(hidden)] pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); -const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100); +pub const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100); pub(crate) const CODE_ACTION_TIMEOUT: Duration = Duration::from_secs(5); pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(5); @@ -1060,8 +1059,8 @@ pub struct Editor { placeholder_text: Option>, highlight_order: usize, highlighted_rows: HashMap>, - background_highlights: TreeMap, - gutter_highlights: TreeMap, + background_highlights: HashMap, + gutter_highlights: HashMap, scrollbar_marker_state: ScrollbarMarkerState, active_indent_guides_state: ActiveIndentGuidesState, nav_history: Option, @@ -2112,8 +2111,8 @@ impl Editor { placeholder_text: None, highlight_order: 0, highlighted_rows: HashMap::default(), - background_highlights: TreeMap::default(), - gutter_highlights: TreeMap::default(), + background_highlights: HashMap::default(), + gutter_highlights: HashMap::default(), scrollbar_marker_state: ScrollbarMarkerState::default(), active_indent_guides_state: ActiveIndentGuidesState::default(), nav_history: None, @@ -6630,7 +6629,7 @@ impl Editor { buffer_row: Some(point.row), ..Default::default() }; - let Some(blame_entry) = blame + let Some((buffer, blame_entry)) = blame .update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next()) .flatten() else { @@ -6640,12 +6639,19 @@ impl Editor { let anchor = self.selections.newest_anchor().head(); let position = self.to_pixel_point(anchor, &snapshot, window); if let (Some(position), Some(last_bounds)) = (position, self.last_bounds) { - self.show_blame_popover(&blame_entry, position + last_bounds.origin, true, cx); + self.show_blame_popover( + buffer, + &blame_entry, + position + last_bounds.origin, + true, + cx, + ); }; } fn show_blame_popover( &mut self, + buffer: BufferId, blame_entry: &BlameEntry, position: gpui::Point, ignore_timeout: bool, @@ -6669,7 +6675,7 @@ impl Editor { return; }; let blame = blame.read(cx); - let details = blame.details_for_entry(&blame_entry); + let details = blame.details_for_entry(buffer, &blame_entry); let markdown = cx.new(|cx| { Markdown::new( details @@ -19071,7 +19077,7 @@ impl Editor { let snapshot = self.snapshot(window, cx); let cursor = self.selections.newest::(cx).head(); let (buffer, point, _) = snapshot.buffer_snapshot.point_to_buffer_point(cursor)?; - let blame_entry = blame + let (_, blame_entry) = blame .update(cx, |blame, cx| { blame .blame_for_rows( @@ -19086,7 +19092,7 @@ impl Editor { }) .flatten()?; let renderer = cx.global::().0.clone(); - let repo = blame.read(cx).repository(cx)?; + let repo = blame.read(cx).repository(cx, buffer.remote_id())?; let workspace = self.workspace()?.downgrade(); renderer.open_blame_commit(blame_entry, repo, workspace, window, cx); None @@ -19122,18 +19128,17 @@ impl Editor { cx: &mut Context, ) { if let Some(project) = self.project() { - let Some(buffer) = self.buffer().read(cx).as_singleton() else { - return; - }; - - if buffer.read(cx).file().is_none() { + if let Some(buffer) = self.buffer().read(cx).as_singleton() + && buffer.read(cx).file().is_none() + { return; } let focused = self.focus_handle(cx).contains_focused(window, cx); let project = project.clone(); - let blame = cx.new(|cx| GitBlame::new(buffer, project, user_triggered, focused, cx)); + let blame = cx + .new(|cx| GitBlame::new(self.buffer.clone(), project, user_triggered, focused, cx)); self.blame_subscription = Some(cx.observe_in(&blame, window, |_, _, _, cx| cx.notify())); self.blame = Some(blame); @@ -19783,7 +19788,24 @@ impl Editor { let buffer = &snapshot.buffer_snapshot; let start = buffer.anchor_before(0); let end = buffer.anchor_after(buffer.len()); - self.background_highlights_in_range(start..end, &snapshot, cx.theme()) + self.sorted_background_highlights_in_range(start..end, &snapshot, cx.theme()) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn sorted_background_highlights_in_range( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + theme: &Theme, + ) -> Vec<(Range, Hsla)> { + let mut res = self.background_highlights_in_range(search_range, display_snapshot, theme); + res.sort_by(|a, b| { + a.0.start + .cmp(&b.0.start) + .then_with(|| a.0.end.cmp(&b.0.end)) + .then_with(|| a.1.cmp(&b.1)) + }); + res } #[cfg(feature = "test-support")] @@ -19848,6 +19870,9 @@ impl Editor { .is_some_and(|(_, highlights)| !highlights.is_empty()) } + /// Returns all background highlights for a given range. + /// + /// The order of highlights is not deterministic, do sort the ranges if needed for the logic. pub fn background_highlights_in_range( &self, search_range: Range, @@ -19886,84 +19911,6 @@ impl Editor { results } - pub fn background_highlight_row_ranges( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - count: usize, - ) -> Vec> { - let mut results = Vec::new(); - let Some((_, ranges)) = self - .background_highlights - .get(&HighlightKey::Type(TypeId::of::())) - else { - return vec![]; - }; - - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe - .end - .cmp(&search_range.start, &display_snapshot.buffer_snapshot); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - let mut push_region = |start: Option, end: Option| { - if let (Some(start_display), Some(end_display)) = (start, end) { - results.push( - start_display.to_display_point(display_snapshot) - ..=end_display.to_display_point(display_snapshot), - ); - } - }; - let mut start_row: Option = None; - let mut end_row: Option = None; - if ranges.len() > count { - return Vec::new(); - } - for range in &ranges[start_ix..] { - if range - .start - .cmp(&search_range.end, &display_snapshot.buffer_snapshot) - .is_ge() - { - break; - } - let end = range.end.to_point(&display_snapshot.buffer_snapshot); - if let Some(current_row) = &end_row - && end.row == current_row.row - { - continue; - } - let start = range.start.to_point(&display_snapshot.buffer_snapshot); - if start_row.is_none() { - assert_eq!(end_row, None); - start_row = Some(start); - end_row = Some(end); - continue; - } - if let Some(current_end) = end_row.as_mut() { - if start.row > current_end.row + 1 { - push_region(start_row, end_row); - start_row = Some(start); - end_row = Some(end); - } else { - // Merge two hunks. - *current_end = end; - } - } else { - unreachable!(); - } - } - // We might still have a hunk that was not rendered (if there was a search hit on the last line) - push_region(start_row, end_row); - results - } - pub fn gutter_highlights_in_range( &self, search_range: Range, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ddfd32be8d11f421ff9cd41aa49996bd27dd06fb..1893839ea67995d316dca64418cc05b13586ac4b 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -15453,37 +15453,34 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { ); let snapshot = editor.snapshot(window, cx); - let mut highlighted_ranges = editor.background_highlights_in_range( + let highlighted_ranges = editor.sorted_background_highlights_in_range( anchor_range(Point::new(3, 4)..Point::new(7, 4)), &snapshot, cx.theme(), ); - // Enforce a consistent ordering based on color without relying on the ordering of the - // highlight's `TypeId` which is non-executor. - highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); assert_eq!( highlighted_ranges, &[ ( - DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4), - Hsla::red(), + DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5), + Hsla::green(), ), ( - DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5), + DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4), Hsla::red(), ), ( - DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5), + DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6), Hsla::green(), ), ( - DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6), - Hsla::green(), + DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5), + Hsla::red(), ), ] ); assert_eq!( - editor.background_highlights_in_range( + editor.sorted_background_highlights_in_range( anchor_range(Point::new(5, 6)..Point::new(6, 4)), &snapshot, cx.theme(), diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 500cce7e0a63bf0a3c985fdd6b507af389775792..fd5e544725696c63af2458447c66813f12f4cba3 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -117,6 +117,7 @@ struct SelectionLayout { struct InlineBlameLayout { element: AnyElement, bounds: Bounds, + buffer_id: BufferId, entry: BlameEntry, } @@ -1157,7 +1158,7 @@ impl EditorElement { cx.notify(); } - if let Some((bounds, blame_entry)) = &position_map.inline_blame_bounds { + if let Some((bounds, buffer_id, blame_entry)) = &position_map.inline_blame_bounds { let mouse_over_inline_blame = bounds.contains(&event.position); let mouse_over_popover = editor .inline_blame_popover @@ -1170,7 +1171,7 @@ impl EditorElement { .is_some_and(|state| state.keyboard_grace); if mouse_over_inline_blame || mouse_over_popover { - editor.show_blame_popover(blame_entry, event.position, false, cx); + editor.show_blame_popover(*buffer_id, blame_entry, event.position, false, cx); } else if !keyboard_grace { editor.hide_blame_popover(cx); } @@ -2454,7 +2455,7 @@ impl EditorElement { padding * em_width }; - let entry = blame + let (buffer_id, entry) = blame .update(cx, |blame, cx| { blame.blame_for_rows(&[*row_info], cx).next() }) @@ -2489,13 +2490,22 @@ impl EditorElement { let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); let bounds = Bounds::new(absolute_offset, size); - self.layout_blame_entry_popover(entry.clone(), blame, line_height, text_hitbox, window, cx); + self.layout_blame_entry_popover( + entry.clone(), + blame, + line_height, + text_hitbox, + row_info.buffer_id?, + window, + cx, + ); element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx); Some(InlineBlameLayout { element, bounds, + buffer_id, entry, }) } @@ -2506,6 +2516,7 @@ impl EditorElement { blame: Entity, line_height: Pixels, text_hitbox: &Hitbox, + buffer: BufferId, window: &mut Window, cx: &mut App, ) { @@ -2530,6 +2541,7 @@ impl EditorElement { popover_state.markdown, workspace, &blame, + buffer, window, cx, ) @@ -2604,14 +2616,16 @@ impl EditorElement { .into_iter() .enumerate() .flat_map(|(ix, blame_entry)| { + let (buffer_id, blame_entry) = blame_entry?; let mut element = render_blame_entry( ix, &blame, - blame_entry?, + blame_entry, &self.style, &mut last_used_color, self.editor.clone(), workspace.clone(), + buffer_id, blame_renderer.clone(), cx, )?; @@ -7401,12 +7415,13 @@ fn render_blame_entry_popover( markdown: Entity, workspace: WeakEntity, blame: &Entity, + buffer: BufferId, window: &mut Window, cx: &mut App, ) -> Option { let renderer = cx.global::().0.clone(); let blame = blame.read(cx); - let repository = blame.repository(cx)?; + let repository = blame.repository(cx, buffer)?; renderer.render_blame_entry_popover( blame_entry, scroll_handle, @@ -7427,6 +7442,7 @@ fn render_blame_entry( last_used_color: &mut Option<(PlayerColor, Oid)>, editor: Entity, workspace: Entity, + buffer: BufferId, renderer: Arc, cx: &mut App, ) -> Option { @@ -7447,8 +7463,8 @@ fn render_blame_entry( last_used_color.replace((sha_color, blame_entry.sha)); let blame = blame.read(cx); - let details = blame.details_for_entry(&blame_entry); - let repository = blame.repository(cx)?; + let details = blame.details_for_entry(buffer, &blame_entry); + let repository = blame.repository(cx, buffer)?; renderer.render_blame_entry( &style.text, blame_entry, @@ -8755,7 +8771,7 @@ impl Element for EditorElement { return None; } let blame = editor.blame.as_ref()?; - let blame_entry = blame + let (_, blame_entry) = blame .update(cx, |blame, cx| { let row_infos = snapshot.row_infos(snapshot.longest_row()).next()?; @@ -9305,7 +9321,7 @@ impl Element for EditorElement { text_hitbox: text_hitbox.clone(), inline_blame_bounds: inline_blame_layout .as_ref() - .map(|layout| (layout.bounds, layout.entry.clone())), + .map(|layout| (layout.bounds, layout.buffer_id, layout.entry.clone())), display_hunks: display_hunks.clone(), diff_hunk_control_bounds, }); @@ -9969,7 +9985,7 @@ pub(crate) struct PositionMap { pub snapshot: EditorSnapshot, pub text_hitbox: Hitbox, pub gutter_hitbox: Hitbox, - pub inline_blame_bounds: Option<(Bounds, BlameEntry)>, + pub inline_blame_bounds: Option<(Bounds, BufferId, BlameEntry)>, pub display_hunks: Vec<(DisplayDiffHunk, Option)>, pub diff_hunk_control_bounds: Vec<(DisplayRow, Bounds)>, } diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index b11617ccec22f08737ba40ab257e19a81eddfd89..27a9b8870383b7f1136e31028bacedc8744e0650 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -10,16 +10,18 @@ use gpui::{ AnyElement, App, AppContext as _, Context, Entity, Hsla, ScrollHandle, Subscription, Task, TextStyle, WeakEntity, Window, }; -use language::{Bias, Buffer, BufferSnapshot, Edit}; +use itertools::Itertools; +use language::{Bias, BufferSnapshot, Edit}; use markdown::Markdown; -use multi_buffer::RowInfo; +use multi_buffer::{MultiBuffer, RowInfo}; use project::{ - Project, ProjectItem, + Project, ProjectItem as _, git_store::{GitStoreEvent, Repository, RepositoryEvent}, }; use smallvec::SmallVec; use std::{sync::Arc, time::Duration}; use sum_tree::SumTree; +use text::BufferId; use workspace::Workspace; #[derive(Clone, Debug, Default)] @@ -63,16 +65,19 @@ impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 { } } -pub struct GitBlame { - project: Entity, - buffer: Entity, +struct GitBlameBuffer { entries: SumTree, - commit_details: HashMap, buffer_snapshot: BufferSnapshot, buffer_edits: text::Subscription, + commit_details: HashMap, +} + +pub struct GitBlame { + project: Entity, + multi_buffer: WeakEntity, + buffers: HashMap, task: Task>, focused: bool, - generated: bool, changed_while_blurred: bool, user_triggered: bool, regenerate_on_edit_task: Task>, @@ -184,44 +189,44 @@ impl gpui::Global for GlobalBlameRenderer {} impl GitBlame { pub fn new( - buffer: Entity, + multi_buffer: Entity, project: Entity, user_triggered: bool, focused: bool, cx: &mut Context, ) -> Self { - let entries = SumTree::from_item( - GitBlameEntry { - rows: buffer.read(cx).max_point().row + 1, - blame: None, + let multi_buffer_subscription = cx.subscribe( + &multi_buffer, + |git_blame, multi_buffer, event, cx| match event { + multi_buffer::Event::DirtyChanged => { + if !multi_buffer.read(cx).is_dirty(cx) { + git_blame.generate(cx); + } + } + multi_buffer::Event::ExcerptsAdded { .. } + | multi_buffer::Event::ExcerptsEdited { .. } => git_blame.regenerate_on_edit(cx), + _ => {} }, - &(), ); - let buffer_subscriptions = cx.subscribe(&buffer, |this, buffer, event, cx| match event { - language::BufferEvent::DirtyChanged => { - if !buffer.read(cx).is_dirty() { - this.generate(cx); - } - } - language::BufferEvent::Edited => { - this.regenerate_on_edit(cx); - } - _ => {} - }); - let project_subscription = cx.subscribe(&project, { - let buffer = buffer.clone(); + let multi_buffer = multi_buffer.downgrade(); - move |this, _, event, cx| { + move |git_blame, _, event, cx| { if let project::Event::WorktreeUpdatedEntries(_, updated) = event { - let project_entry_id = buffer.read(cx).entry_id(cx); + let Some(multi_buffer) = multi_buffer.upgrade() else { + return; + }; + let project_entry_id = multi_buffer + .read(cx) + .as_singleton() + .and_then(|it| it.read(cx).entry_id(cx)); if updated .iter() .any(|(_, entry_id, _)| project_entry_id == Some(*entry_id)) { log::debug!("Updated buffers. Regenerating blame data...",); - this.generate(cx); + git_blame.generate(cx); } } } @@ -239,24 +244,17 @@ impl GitBlame { _ => {} }); - let buffer_snapshot = buffer.read(cx).snapshot(); - let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe()); - let mut this = Self { project, - buffer, - buffer_snapshot, - entries, - buffer_edits, + multi_buffer: multi_buffer.downgrade(), + buffers: HashMap::default(), user_triggered, focused, changed_while_blurred: false, - commit_details: HashMap::default(), task: Task::ready(Ok(())), - generated: false, regenerate_on_edit_task: Task::ready(Ok(())), _regenerate_subscriptions: vec![ - buffer_subscriptions, + multi_buffer_subscription, project_subscription, git_store_subscription, ], @@ -265,56 +263,63 @@ impl GitBlame { this } - pub fn repository(&self, cx: &App) -> Option> { + pub fn repository(&self, cx: &App, id: BufferId) -> Option> { self.project .read(cx) .git_store() .read(cx) - .repository_and_path_for_buffer_id(self.buffer.read(cx).remote_id(), cx) + .repository_and_path_for_buffer_id(id, cx) .map(|(repo, _)| repo) } pub fn has_generated_entries(&self) -> bool { - self.generated + !self.buffers.is_empty() } - pub fn details_for_entry(&self, entry: &BlameEntry) -> Option { - self.commit_details.get(&entry.sha).cloned() + pub fn details_for_entry( + &self, + buffer: BufferId, + entry: &BlameEntry, + ) -> Option { + self.buffers + .get(&buffer)? + .commit_details + .get(&entry.sha) + .cloned() } pub fn blame_for_rows<'a>( &'a mut self, rows: &'a [RowInfo], - cx: &App, - ) -> impl 'a + Iterator> { - self.sync(cx); - - let buffer_id = self.buffer_snapshot.remote_id(); - let mut cursor = self.entries.cursor::(&()); + cx: &'a mut App, + ) -> impl Iterator> + use<'a> { rows.iter().map(move |info| { - let row = info - .buffer_row - .filter(|_| info.buffer_id == Some(buffer_id))?; - cursor.seek_forward(&row, Bias::Right); - cursor.item()?.blame.clone() + let buffer_id = info.buffer_id?; + self.sync(cx, buffer_id); + + let buffer_row = info.buffer_row?; + let mut cursor = self.buffers.get(&buffer_id)?.entries.cursor::(&()); + cursor.seek_forward(&buffer_row, Bias::Right); + Some((buffer_id, cursor.item()?.blame.clone()?)) }) } - pub fn max_author_length(&mut self, cx: &App) -> usize { - self.sync(cx); - + pub fn max_author_length(&mut self, cx: &mut App) -> usize { let mut max_author_length = 0; - - for entry in self.entries.iter() { - let author_len = entry - .blame - .as_ref() - .and_then(|entry| entry.author.as_ref()) - .map(|author| author.len()); - if let Some(author_len) = author_len - && author_len > max_author_length - { - max_author_length = author_len; + self.sync_all(cx); + + for buffer in self.buffers.values() { + for entry in buffer.entries.iter() { + let author_len = entry + .blame + .as_ref() + .and_then(|entry| entry.author.as_ref()) + .map(|author| author.len()); + if let Some(author_len) = author_len + && author_len > max_author_length + { + max_author_length = author_len; + } } } @@ -336,22 +341,48 @@ impl GitBlame { } } - fn sync(&mut self, cx: &App) { - let edits = self.buffer_edits.consume(); - let new_snapshot = self.buffer.read(cx).snapshot(); + fn sync_all(&mut self, cx: &mut App) { + let Some(multi_buffer) = self.multi_buffer.upgrade() else { + return; + }; + multi_buffer + .read(cx) + .excerpt_buffer_ids() + .into_iter() + .for_each(|id| self.sync(cx, id)); + } + + fn sync(&mut self, cx: &mut App, buffer_id: BufferId) { + let Some(blame_buffer) = self.buffers.get_mut(&buffer_id) else { + return; + }; + let Some(buffer) = self + .multi_buffer + .upgrade() + .and_then(|multi_buffer| multi_buffer.read(cx).buffer(buffer_id)) + else { + return; + }; + let edits = blame_buffer.buffer_edits.consume(); + let new_snapshot = buffer.read(cx).snapshot(); let mut row_edits = edits .into_iter() .map(|edit| { - let old_point_range = self.buffer_snapshot.offset_to_point(edit.old.start) - ..self.buffer_snapshot.offset_to_point(edit.old.end); + let old_point_range = blame_buffer.buffer_snapshot.offset_to_point(edit.old.start) + ..blame_buffer.buffer_snapshot.offset_to_point(edit.old.end); let new_point_range = new_snapshot.offset_to_point(edit.new.start) ..new_snapshot.offset_to_point(edit.new.end); if old_point_range.start.column - == self.buffer_snapshot.line_len(old_point_range.start.row) + == blame_buffer + .buffer_snapshot + .line_len(old_point_range.start.row) && (new_snapshot.chars_at(edit.new.start).next() == Some('\n') - || self.buffer_snapshot.line_len(old_point_range.end.row) == 0) + || blame_buffer + .buffer_snapshot + .line_len(old_point_range.end.row) + == 0) { Edit { old: old_point_range.start.row + 1..old_point_range.end.row + 1, @@ -375,7 +406,7 @@ impl GitBlame { .peekable(); let mut new_entries = SumTree::default(); - let mut cursor = self.entries.cursor::(&()); + let mut cursor = blame_buffer.entries.cursor::(&()); while let Some(mut edit) = row_edits.next() { while let Some(next_edit) = row_edits.peek() { @@ -433,17 +464,28 @@ impl GitBlame { new_entries.append(cursor.suffix(), &()); drop(cursor); - self.buffer_snapshot = new_snapshot; - self.entries = new_entries; + blame_buffer.buffer_snapshot = new_snapshot; + blame_buffer.entries = new_entries; } #[cfg(test)] fn check_invariants(&mut self, cx: &mut Context) { - self.sync(cx); - assert_eq!( - self.entries.summary().rows, - self.buffer.read(cx).max_point().row + 1 - ); + self.sync_all(cx); + for (&id, buffer) in &self.buffers { + assert_eq!( + buffer.entries.summary().rows, + self.multi_buffer + .upgrade() + .unwrap() + .read(cx) + .buffer(id) + .unwrap() + .read(cx) + .max_point() + .row + + 1 + ); + } } fn generate(&mut self, cx: &mut Context) { @@ -451,62 +493,105 @@ impl GitBlame { self.changed_while_blurred = true; return; } - let buffer_edits = self.buffer.update(cx, |buffer, _| buffer.subscribe()); - let snapshot = self.buffer.read(cx).snapshot(); let blame = self.project.update(cx, |project, cx| { - project.blame_buffer(&self.buffer, None, cx) + let Some(multi_buffer) = self.multi_buffer.upgrade() else { + return Vec::new(); + }; + multi_buffer + .read(cx) + .all_buffer_ids() + .into_iter() + .filter_map(|id| { + let buffer = multi_buffer.read(cx).buffer(id)?; + let snapshot = buffer.read(cx).snapshot(); + let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe()); + + let blame_buffer = project.blame_buffer(&buffer, None, cx); + Some((id, snapshot, buffer_edits, blame_buffer)) + }) + .collect::>() }); let provider_registry = GitHostingProviderRegistry::default_global(cx); self.task = cx.spawn(async move |this, cx| { - let result = cx + let (result, errors) = cx .background_spawn({ - let snapshot = snapshot.clone(); async move { - let Some(Blame { - entries, - messages, - remote_url, - }) = blame.await? - else { - return Ok(None); - }; - - let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row); - let commit_details = - parse_commit_messages(messages, remote_url, provider_registry).await; - - anyhow::Ok(Some((entries, commit_details))) + let mut res = vec![]; + let mut errors = vec![]; + for (id, snapshot, buffer_edits, blame) in blame { + match blame.await { + Ok(Some(Blame { + entries, + messages, + remote_url, + })) => { + let entries = build_blame_entry_sum_tree( + entries, + snapshot.max_point().row, + ); + let commit_details = parse_commit_messages( + messages, + remote_url, + provider_registry.clone(), + ) + .await; + + res.push(( + id, + snapshot, + buffer_edits, + Some(entries), + commit_details, + )); + } + Ok(None) => { + res.push((id, snapshot, buffer_edits, None, Default::default())) + } + Err(e) => errors.push(e), + } + } + (res, errors) } }) .await; - this.update(cx, |this, cx| match result { - Ok(None) => { - // Nothing to do, e.g. no repository found + this.update(cx, |this, cx| { + this.buffers.clear(); + for (id, snapshot, buffer_edits, entries, commit_details) in result { + let Some(entries) = entries else { + continue; + }; + this.buffers.insert( + id, + GitBlameBuffer { + buffer_edits, + buffer_snapshot: snapshot, + entries, + commit_details, + }, + ); } - Ok(Some((entries, commit_details))) => { - this.buffer_edits = buffer_edits; - this.buffer_snapshot = snapshot; - this.entries = entries; - this.commit_details = commit_details; - this.generated = true; - cx.notify(); + cx.notify(); + if !errors.is_empty() { + this.project.update(cx, |_, cx| { + if this.user_triggered { + log::error!("failed to get git blame data: {errors:?}"); + let notification = errors + .into_iter() + .format_with(",", |e, f| f(&format_args!("{:#}", e))) + .to_string(); + cx.emit(project::Event::Toast { + notification_id: "git-blame".into(), + message: notification, + }); + } else { + // If we weren't triggered by a user, we just log errors in the background, instead of sending + // notifications. + log::debug!("failed to get git blame data: {errors:?}"); + } + }) } - Err(error) => this.project.update(cx, |_, cx| { - if this.user_triggered { - log::error!("failed to get git blame data: {error:?}"); - let notification = format!("{:#}", error).trim().to_string(); - cx.emit(project::Event::Toast { - notification_id: "git-blame".into(), - message: notification, - }); - } else { - // If we weren't triggered by a user, we just log errors in the background, instead of sending - // notifications. - log::debug!("failed to get git blame data: {error:?}"); - } - }), }) }); } @@ -520,7 +605,7 @@ impl GitBlame { this.update(cx, |this, cx| { this.generate(cx); }) - }) + }); } } @@ -659,6 +744,9 @@ mod tests { ) .collect::>(), expected + .into_iter() + .map(|it| Some((buffer_id, it?))) + .collect::>() ); } @@ -705,6 +793,7 @@ mod tests { }) .await .unwrap(); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let blame = cx.new(|cx| GitBlame::new(buffer.clone(), project.clone(), true, true, cx)); @@ -785,6 +874,7 @@ mod tests { .await .unwrap(); let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id()); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx)); @@ -806,14 +896,14 @@ mod tests { ) .collect::>(), vec![ - Some(blame_entry("1b1b1b", 0..1)), - Some(blame_entry("0d0d0d", 1..2)), - Some(blame_entry("3a3a3a", 2..3)), + Some((buffer_id, blame_entry("1b1b1b", 0..1))), + Some((buffer_id, blame_entry("0d0d0d", 1..2))), + Some((buffer_id, blame_entry("3a3a3a", 2..3))), None, None, - Some(blame_entry("3a3a3a", 5..6)), - Some(blame_entry("0d0d0d", 6..7)), - Some(blame_entry("3a3a3a", 7..8)), + Some((buffer_id, blame_entry("3a3a3a", 5..6))), + Some((buffer_id, blame_entry("0d0d0d", 6..7))), + Some((buffer_id, blame_entry("3a3a3a", 7..8))), ] ); // Subset of lines @@ -831,8 +921,8 @@ mod tests { ) .collect::>(), vec![ - Some(blame_entry("0d0d0d", 1..2)), - Some(blame_entry("3a3a3a", 2..3)), + Some((buffer_id, blame_entry("0d0d0d", 1..2))), + Some((buffer_id, blame_entry("3a3a3a", 2..3))), None ] ); @@ -852,7 +942,7 @@ mod tests { cx ) .collect::>(), - vec![Some(blame_entry("0d0d0d", 1..2)), None, None] + vec![Some((buffer_id, blame_entry("0d0d0d", 1..2))), None, None] ); }); } @@ -895,6 +985,7 @@ mod tests { .await .unwrap(); let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id()); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx)); @@ -1061,8 +1152,9 @@ mod tests { }) .await .unwrap(); + let mbuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx)); + let git_blame = cx.new(|cx| GitBlame::new(mbuffer.clone(), project, false, true, cx)); cx.executor().run_until_parked(); git_blame.update(cx, |blame, cx| blame.check_invariants(cx)); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index e27cbf868a67eec98c0538a4410f4b7ff11b7d0c..874e58d2a354628958ef160fdae2836b563d3c7b 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -735,7 +735,7 @@ impl MultiBuffer { pub fn as_singleton(&self) -> Option> { if self.singleton { - return Some( + Some( self.buffers .borrow() .values() @@ -743,7 +743,7 @@ impl MultiBuffer { .unwrap() .buffer .clone(), - ); + ) } else { None } @@ -2552,6 +2552,10 @@ impl MultiBuffer { .collect() } + pub fn all_buffer_ids(&self) -> Vec { + self.buffers.borrow().keys().copied().collect() + } + pub fn buffer(&self, buffer_id: BufferId) -> Option> { self.buffers .borrow() diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 1521d012955050c932d2d2f797a25ab8f3c99d48..a8d5046f90f3136ef4c913ea0e87fd55f77dcd6a 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -5402,8 +5402,9 @@ mod tests { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); - populate_with_test_ra_project(&fs, "/rust-analyzer").await; - let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await; + let root = path!("/rust-analyzer"); + populate_with_test_ra_project(&fs, root).await; + let project = Project::test(fs.clone(), [Path::new(root)], cx).await; project.read_with(cx, |project, _| { project.languages().add(Arc::new(rust_lang())) }); @@ -5448,15 +5449,16 @@ mod tests { }); }); - let all_matches = r#"/rust-analyzer/ + let all_matches = format!( + r#"{root}/ crates/ ide/src/ inlay_hints/ fn_lifetime_fn.rs - search: match config.param_names_for_lifetime_elision_hints { - search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints { - search: Some(it) if config.param_names_for_lifetime_elision_hints => { - search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }, + search: match config.param_names_for_lifetime_elision_hints {{ + search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{ + search: Some(it) if config.param_names_for_lifetime_elision_hints => {{ + search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }}, inlay_hints.rs search: pub param_names_for_lifetime_elision_hints: bool, search: param_names_for_lifetime_elision_hints: self @@ -5467,7 +5469,9 @@ mod tests { analysis_stats.rs search: param_names_for_lifetime_elision_hints: true, config.rs - search: param_names_for_lifetime_elision_hints: self"#; + search: param_names_for_lifetime_elision_hints: self"# + ); + let select_first_in_all_matches = |line_to_select: &str| { assert!(all_matches.contains(line_to_select)); all_matches.replacen( @@ -5524,7 +5528,7 @@ mod tests { cx, ), format!( - r#"/rust-analyzer/ + r#"{root}/ crates/ ide/src/ inlay_hints/ @@ -5594,7 +5598,7 @@ mod tests { cx, ), format!( - r#"/rust-analyzer/ + r#"{root}/ crates/ ide/src/{SELECTED_MARKER} rust-analyzer/src/ @@ -5631,8 +5635,9 @@ mod tests { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); - populate_with_test_ra_project(&fs, "/rust-analyzer").await; - let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await; + let root = path!("/rust-analyzer"); + populate_with_test_ra_project(&fs, root).await; + let project = Project::test(fs.clone(), [Path::new(root)], cx).await; project.read_with(cx, |project, _| { project.languages().add(Arc::new(rust_lang())) }); @@ -5676,15 +5681,16 @@ mod tests { ); }); }); - let all_matches = r#"/rust-analyzer/ + let all_matches = format!( + r#"{root}/ crates/ ide/src/ inlay_hints/ fn_lifetime_fn.rs - search: match config.param_names_for_lifetime_elision_hints { - search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints { - search: Some(it) if config.param_names_for_lifetime_elision_hints => { - search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }, + search: match config.param_names_for_lifetime_elision_hints {{ + search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{ + search: Some(it) if config.param_names_for_lifetime_elision_hints => {{ + search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }}, inlay_hints.rs search: pub param_names_for_lifetime_elision_hints: bool, search: param_names_for_lifetime_elision_hints: self @@ -5695,7 +5701,8 @@ mod tests { analysis_stats.rs search: param_names_for_lifetime_elision_hints: true, config.rs - search: param_names_for_lifetime_elision_hints: self"#; + search: param_names_for_lifetime_elision_hints: self"# + ); cx.executor() .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); @@ -5768,8 +5775,9 @@ mod tests { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); - populate_with_test_ra_project(&fs, path!("/rust-analyzer")).await; - let project = Project::test(fs.clone(), [path!("/rust-analyzer").as_ref()], cx).await; + let root = path!("/rust-analyzer"); + populate_with_test_ra_project(&fs, root).await; + let project = Project::test(fs.clone(), [Path::new(root)], cx).await; project.read_with(cx, |project, _| { project.languages().add(Arc::new(rust_lang())) }); @@ -5813,9 +5821,8 @@ mod tests { ); }); }); - let root_path = format!("{}/", path!("/rust-analyzer")); let all_matches = format!( - r#"{root_path} + r#"{root}/ crates/ ide/src/ inlay_hints/ @@ -5977,7 +5984,7 @@ mod tests { let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/root", + path!("/root"), json!({ "one": { "a.txt": "aaa aaa" @@ -5989,7 +5996,7 @@ mod tests { }), ) .await; - let project = Project::test(fs.clone(), [Path::new("/root/one")], cx).await; + let project = Project::test(fs.clone(), [Path::new(path!("/root/one"))], cx).await; let workspace = add_outline_panel(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let outline_panel = outline_panel(&workspace, cx); @@ -6000,7 +6007,7 @@ mod tests { let items = workspace .update(cx, |workspace, window, cx| { workspace.open_paths( - vec![PathBuf::from("/root/two")], + vec![PathBuf::from(path!("/root/two"))], OpenOptions { visible: Some(OpenVisible::OnlyDirectories), ..Default::default() @@ -6064,13 +6071,17 @@ mod tests { outline_panel.selected_entry(), cx, ), - r#"/root/one/ + format!( + r#"{}/ a.txt search: aaa aaa <==== selected search: aaa aaa -/root/two/ +{}/ b.txt - search: a aaa"# + search: a aaa"#, + path!("/root/one"), + path!("/root/two"), + ), ); }); @@ -6090,11 +6101,15 @@ mod tests { outline_panel.selected_entry(), cx, ), - r#"/root/one/ + format!( + r#"{}/ a.txt <==== selected -/root/two/ +{}/ b.txt - search: a aaa"# + search: a aaa"#, + path!("/root/one"), + path!("/root/two"), + ), ); }); @@ -6114,9 +6129,13 @@ mod tests { outline_panel.selected_entry(), cx, ), - r#"/root/one/ + format!( + r#"{}/ a.txt -/root/two/ <==== selected"# +{}/ <==== selected"#, + path!("/root/one"), + path!("/root/two"), + ), ); }); @@ -6135,11 +6154,15 @@ mod tests { outline_panel.selected_entry(), cx, ), - r#"/root/one/ + format!( + r#"{}/ a.txt -/root/two/ <==== selected +{}/ <==== selected b.txt - search: a aaa"# + search: a aaa"#, + path!("/root/one"), + path!("/root/two"), + ) ); }); } @@ -6165,7 +6188,7 @@ struct OutlineEntryExcerpt { }), ) .await; - let project = Project::test(fs.clone(), [root.as_ref()], cx).await; + let project = Project::test(fs.clone(), [Path::new(root)], cx).await; project.read_with(cx, |project, _| { project.languages().add(Arc::new( rust_lang() @@ -6508,7 +6531,7 @@ outline: struct OutlineEntryExcerpt async fn test_frontend_repo_structure(cx: &mut TestAppContext) { init_test(cx); - let root = "/frontend-project"; + let root = path!("/frontend-project"); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( root, @@ -6545,7 +6568,7 @@ outline: struct OutlineEntryExcerpt }), ) .await; - let project = Project::test(fs.clone(), [root.as_ref()], cx).await; + let project = Project::test(fs.clone(), [Path::new(root)], cx).await; let workspace = add_outline_panel(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let outline_panel = outline_panel(&workspace, cx); @@ -6599,10 +6622,11 @@ outline: struct OutlineEntryExcerpt outline_panel.selected_entry(), cx, ), - r#"/frontend-project/ + format!( + r#"{root}/ public/lottie/ syntax-tree.json - search: { "something": "static" } <==== selected + search: {{ "something": "static" }} <==== selected src/ app/(site)/ (about)/jobs/[slug]/ @@ -6614,6 +6638,7 @@ outline: struct OutlineEntryExcerpt components/ ErrorBoundary.tsx search: static"# + ) ); }); @@ -6636,15 +6661,17 @@ outline: struct OutlineEntryExcerpt outline_panel.selected_entry(), cx, ), - r#"/frontend-project/ + format!( + r#"{root}/ public/lottie/ syntax-tree.json - search: { "something": "static" } + search: {{ "something": "static" }} src/ app/(site)/ <==== selected components/ ErrorBoundary.tsx search: static"# + ) ); }); @@ -6664,15 +6691,17 @@ outline: struct OutlineEntryExcerpt outline_panel.selected_entry(), cx, ), - r#"/frontend-project/ + format!( + r#"{root}/ public/lottie/ syntax-tree.json - search: { "something": "static" } + search: {{ "something": "static" }} src/ app/(site)/ components/ ErrorBoundary.tsx search: static <==== selected"# + ) ); }); @@ -6696,14 +6725,16 @@ outline: struct OutlineEntryExcerpt outline_panel.selected_entry(), cx, ), - r#"/frontend-project/ + format!( + r#"{root}/ public/lottie/ syntax-tree.json - search: { "something": "static" } + search: {{ "something": "static" }} src/ app/(site)/ components/ ErrorBoundary.tsx <==== selected"# + ) ); }); @@ -6727,15 +6758,17 @@ outline: struct OutlineEntryExcerpt outline_panel.selected_entry(), cx, ), - r#"/frontend-project/ + format!( + r#"{root}/ public/lottie/ syntax-tree.json - search: { "something": "static" } + search: {{ "something": "static" }} src/ app/(site)/ components/ ErrorBoundary.tsx <==== selected search: static"# + ) ); }); } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 73f5da086c78b3a3e597be3d32bdcb7a15649117..6e06c2dd95fa9bf8d3b1a0670fb119a0ef552de1 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -2566,10 +2566,7 @@ impl LocalLspStore { }; let Ok(file_url) = lsp::Uri::from_file_path(old_path.as_path()) else { - debug_panic!( - "`{}` is not parseable as an URI", - old_path.to_string_lossy() - ); + debug_panic!("{old_path:?} is not parseable as an URI"); return; }; self.unregister_buffer_from_language_servers(buffer, &file_url, cx); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 2668d270d7f008d49d6d067ba01d951d44a43a00..4a2dbf31fc96b43db34bd9977fafb09cc5ad60d1 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1384,6 +1384,9 @@ impl ProjectSearchView { let match_ranges = self.entity.read(cx).match_ranges.clone(); if match_ranges.is_empty() { self.active_match_index = None; + self.results_editor.update(cx, |editor, cx| { + editor.clear_background_highlights::(cx); + }); } else { self.active_match_index = Some(0); self.update_match_index(cx); @@ -2338,7 +2341,7 @@ pub fn perform_project_search( #[cfg(test)] pub mod tests { - use std::{ops::Deref as _, sync::Arc}; + use std::{ops::Deref as _, sync::Arc, time::Duration}; use super::*; use editor::{DisplayPoint, display_map::DisplayRow}; @@ -2381,6 +2384,7 @@ pub mod tests { "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;" ); let match_background_color = cx.theme().colors().search_match_background; + let selection_background_color = cx.theme().colors().editor_document_highlight_bracket_background; assert_eq!( search_view .results_editor @@ -2390,14 +2394,23 @@ pub mod tests { DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35), match_background_color ), + ( + DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40), + selection_background_color + ), ( DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40), match_background_color ), + ( + DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9), + selection_background_color + ), ( DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9), match_background_color - ) + ), + ] ); assert_eq!(search_view.active_match_index, Some(0)); @@ -4156,6 +4169,10 @@ pub mod tests { search_view.search(cx); }) .unwrap(); + // Ensure editor highlights appear after the search is done + cx.executor().advance_clock( + editor::SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(100), + ); cx.background_executor.run_until_parked(); } } diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 54e8ae8343f4778e04a37a7ebd3dbe2b6da587cd..fc93d40ae5021165cefdf9130a65af221806ee6d 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -2,6 +2,7 @@ use std::{cmp::Ordering, fmt::Debug}; use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary}; +/// A cheaply-clonable ordered map based on a [SumTree](crate::SumTree). #[derive(Clone, PartialEq, Eq)] pub struct TreeMap(SumTree>) where From 7327ef662b0012c29f11db283ca422f474752a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20von=20G=C3=B6wels?= Date: Wed, 3 Sep 2025 16:23:46 +0200 Subject: [PATCH 550/744] terminal_view: Fix focusing of center-pane terminals (#37359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With `reveal_stragegy=always` + `reveal_target=center`, `TerminalPanel::spawn_task` activates & focuses the pane of the task. This works fine in the terminal pane but doesn't for `reveal_target=center`. Please note: I'm not verified familiar with the architecture and internal APIs of zed. If there's a better way or if this fix is a bad idea, I'm fine with adapting this 😃 Closes #35908 Release Notes: - Fixed task focus when re-spawning a task with `reveal_target=center` --------- Co-authored-by: Marshall Bowers --- crates/terminal_view/src/terminal_panel.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 848737aeb24ef52a6819e57882ab022edef94e25..2ba7f617bf407299b2b0e670f66432ce053718be 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -41,7 +41,7 @@ use workspace::{ ui::IconName, }; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Result, anyhow}; use zed_actions::assistant::InlineAssist; const TERMINAL_PANEL_KEY: &str = "TerminalPanel"; @@ -905,11 +905,16 @@ impl TerminalPanel { RevealStrategy::Always => match reveal_target { RevealTarget::Center => { task_workspace.update_in(cx, |workspace, window, cx| { - workspace - .active_item(cx) - .context("retrieving active terminal item in the workspace")? - .item_focus_handle(cx) - .focus(window); + let did_activate = workspace.activate_item( + &terminal_to_replace, + true, + true, + window, + cx, + ); + + anyhow::ensure!(did_activate, "Failed to retrieve terminal pane"); + anyhow::Ok(()) })??; } From 0cbacb850082e92aa1503efac06b0f595444a8d3 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 3 Sep 2025 17:48:17 +0300 Subject: [PATCH 551/744] Make word deletions less greedy (#37352) Closes https://github.com/zed-industries/zed/issues/37144 Adjusts `editor::DeleteToPreviousWordStart`, `editor::DeleteToNextWordEnd`, `editor::DeleteToNextSubwordEnd` and `editor::DeleteToPreviousSubwordStart` actions to * take whitespace sequences with length >= 2 into account and stop after removing them (whilst movement would also include the word after such sequences) * take current language's brackets into account and stop after removing the text before them The latter is configurable and can be disabled with `"ignore_brackets": true` parameter in the action. Release Notes: - Improved word deletions to consider whitespace sequences and brackets by default --- assets/keymaps/default-linux.json | 4 +- assets/keymaps/default-macos.json | 6 +- assets/keymaps/default-windows.json | 4 +- assets/keymaps/linux/emacs.json | 2 +- assets/keymaps/linux/sublime_text.json | 4 +- assets/keymaps/macos/emacs.json | 2 +- assets/keymaps/macos/sublime_text.json | 4 +- assets/keymaps/macos/textmate.json | 8 +- assets/keymaps/vim.json | 2 +- crates/editor/src/actions.rs | 8 + crates/editor/src/editor.rs | 24 +- crates/editor/src/editor_tests.rs | 382 +++++++++++++++++++++++-- crates/editor/src/movement.rs | 104 ++++++- crates/language/src/language.rs | 1 + 14 files changed, 508 insertions(+), 47 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index a60dc92844b337409e717b56975789073eb964fb..28518490ccbe9d3a4e8161ffbc32ed5c27ae0d84 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -63,8 +63,8 @@ "ctrl-k": "editor::CutToEndOfLine", "ctrl-k ctrl-q": "editor::Rewrap", "ctrl-k q": "editor::Rewrap", - "ctrl-backspace": "editor::DeleteToPreviousWordStart", - "ctrl-delete": "editor::DeleteToNextWordEnd", + "ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], + "ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], "cut": "editor::Cut", "shift-delete": "editor::Cut", "ctrl-x": "editor::Cut", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index e72f4174ffe2afbd2605ed2bd859842f2c586107..954684c826b18828857c6411e2413aa514aeec45 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -70,9 +70,9 @@ "cmd-k q": "editor::Rewrap", "cmd-backspace": "editor::DeleteToBeginningOfLine", "cmd-delete": "editor::DeleteToEndOfLine", - "alt-backspace": "editor::DeleteToPreviousWordStart", - "ctrl-w": "editor::DeleteToPreviousWordStart", - "alt-delete": "editor::DeleteToNextWordEnd", + "alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], + "ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], + "alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], "cmd-x": "editor::Cut", "cmd-c": "editor::Copy", "cmd-v": "editor::Paste", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index dbd377409f4423dd12bac06b651efd079772dbb5..728907e60ca3361270f15b20f66aaf7571be6ac2 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -66,8 +66,8 @@ "ctrl-k": "editor::CutToEndOfLine", "ctrl-k ctrl-q": "editor::Rewrap", "ctrl-k q": "editor::Rewrap", - "ctrl-backspace": "editor::DeleteToPreviousWordStart", - "ctrl-delete": "editor::DeleteToNextWordEnd", + "ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], + "ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], "cut": "editor::Cut", "shift-delete": "editor::Cut", "ctrl-x": "editor::Cut", diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index 62910e297bb18f52917477806ceea1b79dcb5d86..0f936ba2f968abe0759e4bb294271a5e5f501848 100755 --- a/assets/keymaps/linux/emacs.json +++ b/assets/keymaps/linux/emacs.json @@ -42,7 +42,7 @@ "alt-,": "pane::GoBack", // xref-pop-marker-stack "ctrl-x h": "editor::SelectAll", // mark-whole-buffer "ctrl-d": "editor::Delete", // delete-char - "alt-d": "editor::DeleteToNextWordEnd", // kill-word + "alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word "ctrl-k": "editor::KillRingCut", // kill-line "ctrl-w": "editor::Cut", // kill-region "alt-w": "editor::Copy", // kill-ring-save diff --git a/assets/keymaps/linux/sublime_text.json b/assets/keymaps/linux/sublime_text.json index ece9d69dd102c019072678373e9328f302d4cb07..f526db45ff29e0828ce58df6ca9816bd71a4cbe5 100644 --- a/assets/keymaps/linux/sublime_text.json +++ b/assets/keymaps/linux/sublime_text.json @@ -50,8 +50,8 @@ "ctrl-k ctrl-u": "editor::ConvertToUpperCase", "ctrl-k ctrl-l": "editor::ConvertToLowerCase", "shift-alt-m": "markdown::OpenPreviewToTheSide", - "ctrl-backspace": "editor::DeleteToPreviousWordStart", - "ctrl-delete": "editor::DeleteToNextWordEnd", + "ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], + "ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], "alt-right": "editor::MoveToNextSubwordEnd", "alt-left": "editor::MoveToPreviousSubwordStart", "alt-shift-right": "editor::SelectToNextSubwordEnd", diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index 62910e297bb18f52917477806ceea1b79dcb5d86..0f936ba2f968abe0759e4bb294271a5e5f501848 100755 --- a/assets/keymaps/macos/emacs.json +++ b/assets/keymaps/macos/emacs.json @@ -42,7 +42,7 @@ "alt-,": "pane::GoBack", // xref-pop-marker-stack "ctrl-x h": "editor::SelectAll", // mark-whole-buffer "ctrl-d": "editor::Delete", // delete-char - "alt-d": "editor::DeleteToNextWordEnd", // kill-word + "alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word "ctrl-k": "editor::KillRingCut", // kill-line "ctrl-w": "editor::Cut", // kill-region "alt-w": "editor::Copy", // kill-ring-save diff --git a/assets/keymaps/macos/sublime_text.json b/assets/keymaps/macos/sublime_text.json index 9fa528c75fa75061c34d767c3e9f9082c9eb2a81..a1e61bf8859e2e4ea227ed3dbe22ec29eb35a149 100644 --- a/assets/keymaps/macos/sublime_text.json +++ b/assets/keymaps/macos/sublime_text.json @@ -52,8 +52,8 @@ "cmd-k cmd-l": "editor::ConvertToLowerCase", "cmd-shift-j": "editor::JoinLines", "shift-alt-m": "markdown::OpenPreviewToTheSide", - "ctrl-backspace": "editor::DeleteToPreviousWordStart", - "ctrl-delete": "editor::DeleteToNextWordEnd", + "ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], + "ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], "ctrl-right": "editor::MoveToNextSubwordEnd", "ctrl-left": "editor::MoveToPreviousSubwordStart", "ctrl-shift-right": "editor::SelectToNextSubwordEnd", diff --git a/assets/keymaps/macos/textmate.json b/assets/keymaps/macos/textmate.json index 0bd8873b1749d2423d97df480b1aadeb28fe9bab..f91f39b7f5c079f81b5fcf8e28e2092a33ff1aa4 100644 --- a/assets/keymaps/macos/textmate.json +++ b/assets/keymaps/macos/textmate.json @@ -21,10 +21,10 @@ { "context": "Editor", "bindings": { - "alt-backspace": "editor::DeleteToPreviousWordStart", - "alt-shift-backspace": "editor::DeleteToNextWordEnd", - "alt-delete": "editor::DeleteToNextWordEnd", - "alt-shift-delete": "editor::DeleteToNextWordEnd", + "alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], + "alt-shift-backspace": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], + "alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], + "alt-shift-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], "ctrl-backspace": "editor::DeleteToPreviousSubwordStart", "ctrl-delete": "editor::DeleteToNextSubwordEnd", "alt-left": ["editor::MoveToPreviousWordStart", { "stop_at_soft_wraps": true }], diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index fd33b888b742bff8ba6a3c1b1ff15b8dbe0c11f8..fa7f82e1032ead9cb1f1ce12f3484602954123ca 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -337,7 +337,7 @@ "ctrl-x ctrl-z": "editor::Cancel", "ctrl-x ctrl-e": "vim::LineDown", "ctrl-x ctrl-y": "vim::LineUp", - "ctrl-w": "editor::DeleteToPreviousWordStart", + "ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], "ctrl-u": "editor::DeleteToBeginningOfLine", "ctrl-t": "vim::Indent", "ctrl-d": "vim::Outdent", diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 3cc6c28464449907abbd19235f9123e44cca78ba..5b92f138c7bd494dd3d0fd30f3b8b3479995e53f 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -228,21 +228,29 @@ pub struct ShowCompletions { pub struct HandleInput(pub String); /// Deletes from the cursor to the end of the next word. +/// Stops before the end of the next word, if whitespace sequences of length >= 2 are encountered. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct DeleteToNextWordEnd { #[serde(default)] pub ignore_newlines: bool, + // Whether to stop before the end of the next word, if language-defined bracket is encountered. + #[serde(default)] + pub ignore_brackets: bool, } /// Deletes from the cursor to the start of the previous word. +/// Stops before the start of the previous word, if whitespace sequences of length >= 2 are encountered. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct DeleteToPreviousWordStart { #[serde(default)] pub ignore_newlines: bool, + // Whether to stop before the start of the previous word, if language-defined bracket is encountered. + #[serde(default)] + pub ignore_brackets: bool, } /// Folds all code blocks at the specified indentation level. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a8e2b001f330423e2108a22eb6f5113c3aa3e78b..5edc7f3c061efe05bd4112bcbf152695ab56c50d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -13153,11 +13153,17 @@ impl Editor { this.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { if selection.is_empty() { - let cursor = if action.ignore_newlines { + let mut cursor = if action.ignore_newlines { movement::previous_word_start(map, selection.head()) } else { movement::previous_word_start_or_newline(map, selection.head()) }; + cursor = movement::adjust_greedy_deletion( + map, + selection.head(), + cursor, + action.ignore_brackets, + ); selection.set_head(cursor, SelectionGoal::None); } }); @@ -13178,7 +13184,9 @@ impl Editor { this.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { if selection.is_empty() { - let cursor = movement::previous_subword_start(map, selection.head()); + let mut cursor = movement::previous_subword_start(map, selection.head()); + cursor = + movement::adjust_greedy_deletion(map, selection.head(), cursor, false); selection.set_head(cursor, SelectionGoal::None); } }); @@ -13254,11 +13262,17 @@ impl Editor { this.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { if selection.is_empty() { - let cursor = if action.ignore_newlines { + let mut cursor = if action.ignore_newlines { movement::next_word_end(map, selection.head()) } else { movement::next_word_end_or_newline(map, selection.head()) }; + cursor = movement::adjust_greedy_deletion( + map, + selection.head(), + cursor, + action.ignore_brackets, + ); selection.set_head(cursor, SelectionGoal::None); } }); @@ -13278,7 +13292,9 @@ impl Editor { this.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { if selection.is_empty() { - let cursor = movement::next_subword_end(map, selection.head()); + let mut cursor = movement::next_subword_end(map, selection.head()); + cursor = + movement::adjust_greedy_deletion(map, selection.head(), cursor, false); selection.set_head(cursor, SelectionGoal::None); } }); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 1893839ea67995d316dca64418cc05b13586ac4b..5efa3908256531e40845096187d44353ae140bbc 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2476,51 +2476,379 @@ async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) { } #[gpui::test] -fn test_delete_to_word_boundary(cx: &mut TestAppContext) { +async fn test_delete_to_word_boundary(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let editor = cx.add_window(|window, cx| { - let buffer = MultiBuffer::build_simple("one two three four", cx); - build_editor(buffer, window, cx) + let mut cx = EditorTestContext::new(cx).await; + + // For an empty selection, the preceding word fragment is deleted. + // For non-empty selections, only selected characters are deleted. + cx.set_state("onˇe two t«hreˇ»e four"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_previous_word_start( + &DeleteToPreviousWordStart { + ignore_newlines: false, + ignore_brackets: false, + }, + window, + cx, + ); }); + cx.assert_editor_state("ˇe two tˇe four"); - _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - // an empty selection - the preceding word fragment is deleted - DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), - // characters selected - they are deleted - DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12), - ]) - }); + cx.set_state("e tˇwo te «fˇ»our"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_next_word_end( + &DeleteToNextWordEnd { + ignore_newlines: false, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state("e tˇ te ˇour"); +} + +#[gpui::test] +async fn test_delete_whitespaces(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state("here is some text ˇwith a space"); + cx.update_editor(|editor, window, cx| { editor.delete_to_previous_word_start( &DeleteToPreviousWordStart { ignore_newlines: false, + ignore_brackets: true, }, window, cx, ); - assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four"); }); + // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action. + cx.assert_editor_state("here is some textˇwith a space"); - _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - // an empty selection - the following word fragment is deleted - DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3), - // characters selected - they are deleted - DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10), - ]) - }); + cx.set_state("here is some text ˇwith a space"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_previous_word_start( + &DeleteToPreviousWordStart { + ignore_newlines: false, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state("here is some textˇwith a space"); + + cx.set_state("here is some textˇ with a space"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_next_word_end( + &DeleteToNextWordEnd { + ignore_newlines: false, + ignore_brackets: true, + }, + window, + cx, + ); + }); + // Same happens in the other direction. + cx.assert_editor_state("here is some textˇwith a space"); + + cx.set_state("here is some textˇ with a space"); + cx.update_editor(|editor, window, cx| { editor.delete_to_next_word_end( &DeleteToNextWordEnd { ignore_newlines: false, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state("here is some textˇwith a space"); + + cx.set_state("here is some textˇ with a space"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_next_word_end( + &DeleteToNextWordEnd { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state("here is some textˇwith a space"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_previous_word_start( + &DeleteToPreviousWordStart { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state("here is some ˇwith a space"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_previous_word_start( + &DeleteToPreviousWordStart { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + // Single whitespaces are removed with the word behind them. + cx.assert_editor_state("here is ˇwith a space"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_previous_word_start( + &DeleteToPreviousWordStart { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state("here ˇwith a space"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_previous_word_start( + &DeleteToPreviousWordStart { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state("ˇwith a space"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_previous_word_start( + &DeleteToPreviousWordStart { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state("ˇwith a space"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_next_word_end( + &DeleteToNextWordEnd { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + // Same happens in the other direction. + cx.assert_editor_state("ˇ a space"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_next_word_end( + &DeleteToNextWordEnd { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state("ˇ space"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_next_word_end( + &DeleteToNextWordEnd { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state("ˇ"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_next_word_end( + &DeleteToNextWordEnd { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state("ˇ"); + cx.update_editor(|editor, window, cx| { + editor.delete_to_previous_word_start( + &DeleteToPreviousWordStart { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state("ˇ"); +} + +#[gpui::test] +async fn test_delete_to_bracket(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "\"".to_string(), + end: "\"".to_string(), + close: true, + surround: true, + newline: false, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + surround: true, + newline: true, + }, + ], + ..BracketPairConfig::default() + }, + ..LanguageConfig::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_brackets_query( + r#" + ("(" @open ")" @close) + ("\"" @open "\"" @close) + "#, + ) + .unwrap(), + ); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + cx.set_state(r#"macro!("// ˇCOMMENT");"#); + cx.update_editor(|editor, window, cx| { + editor.delete_to_previous_word_start( + &DeleteToPreviousWordStart { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + // Deletion stops before brackets if asked to not ignore them. + cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#); + cx.update_editor(|editor, window, cx| { + editor.delete_to_previous_word_start( + &DeleteToPreviousWordStart { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + // Deletion has to remove a single bracket and then stop again. + cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#); + + cx.update_editor(|editor, window, cx| { + editor.delete_to_previous_word_start( + &DeleteToPreviousWordStart { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state(r#"macro!ˇCOMMENT");"#); + + cx.update_editor(|editor, window, cx| { + editor.delete_to_previous_word_start( + &DeleteToPreviousWordStart { + ignore_newlines: true, + ignore_brackets: false, }, window, cx, ); - assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our"); }); + cx.assert_editor_state(r#"ˇCOMMENT");"#); + + cx.update_editor(|editor, window, cx| { + editor.delete_to_previous_word_start( + &DeleteToPreviousWordStart { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state(r#"ˇCOMMENT");"#); + + cx.update_editor(|editor, window, cx| { + editor.delete_to_next_word_end( + &DeleteToNextWordEnd { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + // Brackets on the right are not paired anymore, hence deletion does not stop at them + cx.assert_editor_state(r#"ˇ");"#); + + cx.update_editor(|editor, window, cx| { + editor.delete_to_next_word_end( + &DeleteToNextWordEnd { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state(r#"ˇ"#); + + cx.update_editor(|editor, window, cx| { + editor.delete_to_next_word_end( + &DeleteToNextWordEnd { + ignore_newlines: true, + ignore_brackets: false, + }, + window, + cx, + ); + }); + cx.assert_editor_state(r#"ˇ"#); + + cx.set_state(r#"macro!("// ˇCOMMENT");"#); + cx.update_editor(|editor, window, cx| { + editor.delete_to_previous_word_start( + &DeleteToPreviousWordStart { + ignore_newlines: true, + ignore_brackets: true, + }, + window, + cx, + ); + }); + cx.assert_editor_state(r#"macroˇCOMMENT");"#); } #[gpui::test] @@ -2533,9 +2861,11 @@ fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) { }); let del_to_prev_word_start = DeleteToPreviousWordStart { ignore_newlines: false, + ignore_brackets: false, }; let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart { ignore_newlines: true, + ignore_brackets: false, }; _ = editor.update(cx, |editor, window, cx| { @@ -2569,9 +2899,11 @@ fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) { }); let del_to_next_word_end = DeleteToNextWordEnd { ignore_newlines: false, + ignore_brackets: false, }; let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd { ignore_newlines: true, + ignore_brackets: false, }; _ = editor.update(cx, |editor, window, cx| { @@ -2600,6 +2932,8 @@ fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) { editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx); assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four"); editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx); + assert_eq!(editor.buffer.read(cx).read(cx).text(), "four"); + editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx); assert_eq!(editor.buffer.read(cx).read(cx).text(), ""); }); } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 7a008e3ba257173429448a02be7abe5ab00cedec..216bea169683409b219641cc3496de9280bb05f6 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -289,12 +289,114 @@ pub fn previous_word_start_or_newline(map: &DisplaySnapshot, point: DisplayPoint let classifier = map.buffer_snapshot.char_classifier_at(raw_point); find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| { - (classifier.kind(left) != classifier.kind(right) && !right.is_whitespace()) + (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right)) || left == '\n' || right == '\n' }) } +/// Text movements are too greedy, making deletions too greedy too. +/// Makes deletions more ergonomic by potentially reducing the deletion range based on its text contents: +/// * whitespace sequences with length >= 2 stop the deletion after removal (despite movement jumping over the word behind the whitespaces) +/// * brackets stop the deletion after removal (despite movement currently not accounting for these and jumping over) +pub fn adjust_greedy_deletion( + map: &DisplaySnapshot, + delete_from: DisplayPoint, + delete_until: DisplayPoint, + ignore_brackets: bool, +) -> DisplayPoint { + if delete_from == delete_until { + return delete_until; + } + let is_backward = delete_from > delete_until; + let delete_range = if is_backward { + map.display_point_to_point(delete_until, Bias::Left) + .to_offset(&map.buffer_snapshot) + ..map + .display_point_to_point(delete_from, Bias::Right) + .to_offset(&map.buffer_snapshot) + } else { + map.display_point_to_point(delete_from, Bias::Left) + .to_offset(&map.buffer_snapshot) + ..map + .display_point_to_point(delete_until, Bias::Right) + .to_offset(&map.buffer_snapshot) + }; + + let trimmed_delete_range = if ignore_brackets { + delete_range + } else { + let brackets_in_delete_range = map + .buffer_snapshot + .bracket_ranges(delete_range.clone()) + .into_iter() + .flatten() + .flat_map(|(left_bracket, right_bracket)| { + [ + left_bracket.start, + left_bracket.end, + right_bracket.start, + right_bracket.end, + ] + }) + .filter(|&bracket| delete_range.start < bracket && bracket < delete_range.end); + let closest_bracket = if is_backward { + brackets_in_delete_range.max() + } else { + brackets_in_delete_range.min() + }; + + if is_backward { + closest_bracket.unwrap_or(delete_range.start)..delete_range.end + } else { + delete_range.start..closest_bracket.unwrap_or(delete_range.end) + } + }; + + let mut whitespace_sequences = Vec::new(); + let mut current_offset = trimmed_delete_range.start; + let mut whitespace_sequence_length = 0; + let mut whitespace_sequence_start = 0; + for ch in map + .buffer_snapshot + .text_for_range(trimmed_delete_range.clone()) + .flat_map(str::chars) + { + if ch.is_whitespace() { + if whitespace_sequence_length == 0 { + whitespace_sequence_start = current_offset; + } + whitespace_sequence_length += 1; + } else { + if whitespace_sequence_length >= 2 { + whitespace_sequences.push((whitespace_sequence_start, current_offset)); + } + whitespace_sequence_start = 0; + whitespace_sequence_length = 0; + } + current_offset += ch.len_utf8(); + } + if whitespace_sequence_length >= 2 { + whitespace_sequences.push((whitespace_sequence_start, current_offset)); + } + + let closest_whitespace_end = if is_backward { + whitespace_sequences.last().map(|&(start, _)| start) + } else { + whitespace_sequences.first().map(|&(_, end)| end) + }; + + closest_whitespace_end + .unwrap_or_else(|| { + if is_backward { + trimmed_delete_range.start + } else { + trimmed_delete_range.end + } + }) + .to_display_point(map) +} + /// Returns a position of the previous subword boundary, where a subword is defined as a run of /// word characters of the same "subkind" - where subcharacter kinds are '_' character, /// lowerspace characters and uppercase characters. diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index b349122193f1f31b323e03ff0421dfc3705c92fa..0606ae3de9be5800401787a852bafd5cfd9051be 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1250,6 +1250,7 @@ struct InjectionPatternConfig { combined: bool, } +#[derive(Debug)] struct BracketsConfig { query: Query, open_capture_ix: u32, From c3480c3d6f0ff1f313c2a3678e29081c54c2ab96 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:59:49 -0300 Subject: [PATCH 552/744] docs: Update external agents content (#37413) Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner --- docs/src/ai/agent-panel.md | 13 +++++--- docs/src/ai/external-agents.md | 57 +++++++++++++++++++++++++++++----- docs/src/ai/overview.md | 2 +- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/docs/src/ai/agent-panel.md b/docs/src/ai/agent-panel.md index 002c7d64150d53f734b9f1bbce87567b7c05036a..ce91ca3401d07aba552b1ca007b3809e301071de 100644 --- a/docs/src/ai/agent-panel.md +++ b/docs/src/ai/agent-panel.md @@ -1,24 +1,27 @@ # Agent Panel -The Agent Panel allows you to interact with many LLMs and coding agents that can support you in various types of tasks, such as generating code, codebase understanding, and other general inquiries like writing emails, documentation, and more. +The Agent Panel allows you to interact with many LLMs and coding agents that can help with in various types of tasks, such as generating code, codebase understanding, and other general inquiries like writing emails, documentation, and more. To open it, use the `agent: new thread` action in [the Command Palette](../getting-started.md#command-palette) or click the ✨ (sparkles) icon in the status bar. -If you're using the Agent Panel for the first time, you need to have at least one LLM or agent provider configured. +## Getting Started + +If you're using the Agent Panel for the first time, you need to have at least one LLM provider or external agent configured. You can do that by: 1. [subscribing to our Pro plan](https://zed.dev/pricing), so you have access to our hosted models 2. [bringing your own API keys](./llm-providers.md#use-your-own-keys) for your desired provider -3. using an external agent like [Gemini CLI](./external-agents.md#gemini-cli) +3. using an external agent like [Gemini CLI](./external-agents.md#gemini-cli) or [Claude Code](./external-agents.md#claude-code) ## Overview {#overview} -After you've configured one or more LLM providers, type at the message editor and hit `enter` to submit your prompt. +With an LLM provider or an external agent configured, type at the message editor and hit `enter` to submit your prompt. If you need extra room to type, you can expand the message editor with {#kb agent::ExpandMessageEditor}. You should start to see the responses stream in with indications of [which tools](./tools.md) the model is using to fulfill your prompt. +From this point on, you can interact with the many supported features outlined below. -> Note that, at the moment, not all features outlined below work for external agents, like [Gemini CLI](./external-agents.md#gemini-cli)—features like _restoring threads from history_, _checkpoints_, _token usage display_, _model selection_, and others may be supported in the future. +> Note that for external agents, like [Gemini CLI](./external-agents.md#gemini-cli) or [Claude Code](./external-agents.md#claude-code), some of the features outlined below are _not_ currently supported—for example, _restoring threads from history_, _checkpoints_, _token usage display_, _model selection_, and others. All of them should hopefully be supported in the future. ### Creating New Threads diff --git a/docs/src/ai/external-agents.md b/docs/src/ai/external-agents.md index 3d263afdb060a5acc99e4315ac32c7063386317f..de374511d053d6226e22c3d55e1cf2a96b91ad85 100644 --- a/docs/src/ai/external-agents.md +++ b/docs/src/ai/external-agents.md @@ -1,8 +1,9 @@ # External Agents -Zed supports terminal-based agentic coding tools through the [Agent Client Protocol (ACP)](https://agentclientprotocol.com). +Zed supports terminal-based agents through the [Agent Client Protocol (ACP)](https://agentclientprotocol.com). -Currently, [Gemini CLI](https://github.com/google-gemini/gemini-cli) serves as the reference implementation, and you can [add custom ACP-compatible agents](#add-custom-agents) as well. +Currently, [Gemini CLI](https://github.com/google-gemini/gemini-cli) serves as the reference implementation. +[Claude Code](https://www.anthropic.com/claude-code) is also included by default, and you can [add custom ACP-compatible agents](#add-custom-agents) as well. ## Gemini CLI {#gemini-cli} @@ -13,9 +14,9 @@ This means that you're running the real Gemini CLI, with all of the advantages o ### Getting Started -As of Zed Stable v0.201.5 you should be able to use Gemini CLI directly from Zed. First open the agent panel with {#kb agent::ToggleFocus}, and then use the `+` button in the top right to start a New Gemini CLI thread. +As of [Zed Stable v0.201.5](https://zed.dev/releases/stable/0.201.5) you should be able to use Gemini CLI directly from Zed. First open the agent panel with {#kb agent::ToggleFocus}, and then use the `+` button in the top right to start a new Gemini CLI thread. -If you'd like to bind this to a keyboard shortcut, you can do so by editing your keybindings file to include: +If you'd like to bind this to a keyboard shortcut, you can do so by editing your `keymap.json` file via the `zed: open keymap` command to include: ```json [ @@ -40,23 +41,63 @@ The instructions to upgrade Gemini depend on how you originally installed it, bu After you have Gemini CLI running, you'll be prompted to choose your authentication method. -Most users should click the "Log in with Google". This will cause a browser window to pop-up and auth directly with Gemini CLI. Zed does not see your oauth or access tokens in this case. +Most users should click the "Log in with Google". This will cause a browser window to pop-up and auth directly with Gemini CLI. Zed does not see your OAuth or access tokens in this case. You can also use the "Gemini API Key". If you select this, and have the `GEMINI_API_KEY` set, then we will use that. Otherwise Zed will prompt you for an API key which will be stored securely in your keychain, and used to start Gemini CLI from within Zed. -The "Vertex AI" option is for those who are using Vertex AI, and have already configured their environment correctly. +The "Vertex AI" option is for those who are using [Vertex AI](https://cloud.google.com/vertex-ai), and have already configured their environment correctly. For more information, see the [Gemini CLI docs](https://github.com/google-gemini/gemini-cli/blob/main/docs/index.md). ### Usage Similar to Zed's first-party agent, you can use Gemini CLI to do anything that you need. - -You can @-mention files, recent threads, symbols, or fetch the web. +And to give it context, you can @-mention files, recent threads, symbols, or fetch the web. > Note that some first-party agent features don't yet work with Gemini CLI: editing past messages, resuming threads from history, checkpointing, and using the agent in SSH projects. > We hope to add these features in the near future. +## Claude Code + +Similar to Gemini CLI, you can also run [Claude Code](https://www.anthropic.com/claude-code) directly via Zed's [agent panel](./agent-panel.md). +Under the hood, Zed runs Claude Code and communicate to it over ACP, through [a dedicated adapter](https://github.com/zed-industries/claude-code-acp). + +### Getting Started + +Open the agent panel with {#kb agent::ToggleFocus}, and then use the `+` button in the top right to start a new Claude Code thread. + +If you'd like to bind this to a keyboard shortcut, you can do so by editing your `keymap.json` file via the `zed: open keymap` command to include: + +```json +[ + { + "bindings": { + "cmd-alt-c": ["agent::NewExternalAgentThread", { "agent": "claude_code" }] + } + } +] +``` + +#### Installation + +If you don't yet have Claude Code installed, then Zed will install a version for you. +If you do, then we will use the version of Claude Code on your path. + +### Usage + +Similar to Zed's first-party agent, you can use Claude Code to do anything that you need. +And to give it context, you can @-mention files, recent threads, symbols, or fetch the web. + +In complement to talking to it [over ACP](https://agentclientprotocol.com), Zed relies on the [Claude Code SDK](https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-overview) to support some of its specific features. +However, the SDK doesn't yet expose everything needed to fully support all of them: + +- Slash Commands: A subset of [built-in commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands#built-in-slash-commands) are supported, while [custom slash commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands#custom-slash-commands) are fully supported. +- [Subagents](https://docs.anthropic.com/en/docs/claude-code/sub-agents) are supported. +- [Hooks](https://docs.anthropic.com/en/docs/claude-code/hooks-guide) are currently _not_ supported. + +> Also note that some [first-party agent](./agent-panel.md) features don't yet work with Claude Code: editing past messages, resuming threads from history, checkpointing, and using the agent in SSH projects. +> We hope to add these features in the near future. + ## Add Custom Agents {#add-custom-agents} You can run any agent speaking ACP in Zed by changing your settings as follows: diff --git a/docs/src/ai/overview.md b/docs/src/ai/overview.md index 8bd45240fdad156e11f28e5ba92289c97de92218..55d37ea3526173b6bf88adc0f15754be51bf6866 100644 --- a/docs/src/ai/overview.md +++ b/docs/src/ai/overview.md @@ -14,7 +14,7 @@ Learn how to get started using AI with Zed and all its capabilities. ## Agentic Editing -- [Agent Panel](./agent-panel.md): Create and manage interactions with language models. +- [Agent Panel](./agent-panel.md): Create and manage interactions with LLM agents. - [Rules](./rules.md): How to define rules for AI interactions. From 13de400a2acc809bc4d69e38ee3a166d6d412614 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Thu, 4 Sep 2025 00:03:48 +0530 Subject: [PATCH 553/744] editor: Do not correct text contrast on non-opaque editor (#37471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don’t know the background color behind a non-opaque editor, so we should skip contrast correction in that case. This prevents single-editor mode (which is always transparent) from showing weird text colors when text is selected. We can’t account for the actual background during contrast correction because we compute contrast outside gpui, while the actual color blending happens inside gpui during drawing. image Release Notes: - Fixed an issue where Command Palette text looked faded when selected. --- crates/editor/src/element.rs | 8 ++++++-- crates/gpui/src/color.rs | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index fd5e544725696c63af2458447c66813f12f4cba3..c21d31aa5c14e3b596a6089630b29a3e55a3084d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3284,6 +3284,10 @@ impl EditorElement { if rows.start >= rows.end { return Vec::new(); } + if !base_background.is_opaque() { + // We don't actually know what color is behind this editor. + return Vec::new(); + } let highlight_iter = highlight_ranges.iter().cloned(); let selection_iter = selections.iter().flat_map(|(player_color, layouts)| { let color = player_color.selection; @@ -11005,7 +11009,7 @@ mod tests { #[gpui::test] fn test_merge_overlapping_ranges() { - let base_bg = Hsla::default(); + let base_bg = Hsla::white(); let color1 = Hsla { h: 0.0, s: 0.5, @@ -11075,7 +11079,7 @@ mod tests { #[gpui::test] fn test_bg_segments_per_row() { - let base_bg = Hsla::default(); + let base_bg = Hsla::white(); // Case A: selection spans three display rows: row 1 [5, end), full row 2, row 3 [0, 7) { diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index cb7329c03fbb2064da0ef5873eef92c2c33d4953..93c69744a6f9f3cc74e7696e9edf49001587376b 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -473,6 +473,11 @@ impl Hsla { self.a == 0.0 } + /// Returns true if the HSLA color is fully opaque, false otherwise. + pub fn is_opaque(&self) -> bool { + self.a == 1.0 + } + /// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors. /// /// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color. From 6bd52518821142b0d66a105220b0e1cc0c421930 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 3 Sep 2025 14:25:30 -0500 Subject: [PATCH 554/744] settings_ui: Add test for default values (#37466) Closes #ISSUE Adds a test that checks that all settings have default values in `default.json`. Currently only tests that settings supported by SettingsUi have defaults, as more settings are added to the settings editor they will be added to the test as well. Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 1 + crates/settings/src/settings_ui_core.rs | 3 +- crates/settings_ui/Cargo.toml | 5 ++ crates/settings_ui/src/settings_ui.rs | 68 +++++++++++++++++++++++-- crates/zed/Cargo.toml | 1 + crates/zed/src/zed.rs | 30 +++++++++++ 6 files changed, 102 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88150a29310eccb928e85949530d0adc2cae97c3..239323517c01045b3ddb0d23b1d99f0904d137a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14906,6 +14906,7 @@ version = "0.1.0" dependencies = [ "anyhow", "command_palette_hooks", + "debugger_ui", "editor", "feature_flags", "gpui", diff --git a/crates/settings/src/settings_ui_core.rs b/crates/settings/src/settings_ui_core.rs index 8ab744f5a8244057e0b87a66cc3e4c7dcf02527f..9086d3c7454465e8abcaf2d30d01a4f928e4ddef 100644 --- a/crates/settings/src/settings_ui_core.rs +++ b/crates/settings/src/settings_ui_core.rs @@ -27,6 +27,7 @@ pub struct SettingsUiEntry { /// The path in the settings JSON file for this setting. Relative to parent /// None implies `#[serde(flatten)]` or `Settings::KEY.is_none()` for top level settings pub path: Option<&'static str>, + /// What is displayed for the text for this entry pub title: &'static str, pub item: SettingsUiItem, } @@ -95,7 +96,7 @@ impl SettingsValue { pub struct SettingsUiItemDynamic { pub options: Vec, - pub determine_option: fn(&serde_json::Value, &mut App) -> usize, + pub determine_option: fn(&serde_json::Value, &App) -> usize, } pub struct SettingsUiItemGroup { diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 7c2b81aee0ecf48afb7131adf5ddb19a165ca351..3ecef880d2bb551c79df67aefa420796829c68ef 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -13,6 +13,7 @@ path = "src/settings_ui.rs" [features] default = [] +test-support = [] [dependencies] anyhow.workspace = true @@ -29,6 +30,10 @@ ui.workspace = true workspace.workspace = true workspace-hack.workspace = true + +[dev-dependencies] +debugger_ui.workspace = true + # Uncomment other workspace dependencies as needed # assistant.workspace = true # client.workspace = true diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 80e82a304965e4ddb7eb37306fb5345f5552e6c7..f316a318785c7f56d465c2d39e6b6ea9bbbd1bfa 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -151,7 +151,9 @@ struct UiEntry { next_sibling: Option, // expanded: bool, render: Option, - select_descendant: Option usize>, + /// For dynamic items this is a way to select a value from a list of values + /// this is always none for non-dynamic items + select_descendant: Option usize>, } impl UiEntry { @@ -177,7 +179,7 @@ impl UiEntry { } } -struct SettingsUiTree { +pub struct SettingsUiTree { root_entry_indices: Vec, entries: Vec, active_entry_index: usize, @@ -242,7 +244,7 @@ fn build_tree_item( } impl SettingsUiTree { - fn new(cx: &App) -> Self { + pub fn new(cx: &App) -> Self { let settings_store = SettingsStore::global(cx); let mut tree = vec![]; let mut root_entry_indices = vec![]; @@ -269,6 +271,62 @@ impl SettingsUiTree { active_entry_index, } } + + // todo(settings_ui): Make sure `Item::None` paths are added to the paths tree, + // so that we can keep none/skip and still test in CI that all settings have + #[cfg(feature = "test-support")] + pub fn all_paths(&self, cx: &App) -> Vec> { + fn all_paths_rec( + tree: &[UiEntry], + paths: &mut Vec>, + current_path: &mut Vec<&'static str>, + idx: usize, + cx: &App, + ) { + let child = &tree[idx]; + let mut pushed_path = false; + if let Some(path) = child.path.as_ref() { + current_path.push(path); + paths.push(current_path.clone()); + pushed_path = true; + } + // todo(settings_ui): handle dynamic nodes here + let selected_descendant_index = child + .select_descendant + .map(|select_descendant| { + read_settings_value_from_path( + SettingsStore::global(cx).raw_default_settings(), + ¤t_path, + ) + .map(|value| select_descendant(value, cx)) + }) + .and_then(|selected_descendant_index| { + selected_descendant_index.map(|index| child.nth_descendant_index(tree, index)) + }); + + if let Some(selected_descendant_index) = selected_descendant_index { + // just silently fail if we didn't find a setting value for the path + if let Some(descendant_index) = selected_descendant_index { + all_paths_rec(tree, paths, current_path, descendant_index, cx); + } + } else if let Some(desc_idx) = child.first_descendant_index() { + let mut desc_idx = Some(desc_idx); + while let Some(descendant_index) = desc_idx { + all_paths_rec(&tree, paths, current_path, descendant_index, cx); + desc_idx = tree[descendant_index].next_sibling; + } + } + if pushed_path { + current_path.pop(); + } + } + + let mut paths = Vec::new(); + for &index in &self.root_entry_indices { + all_paths_rec(&self.entries, &mut paths, &mut Vec::new(), index, cx); + } + paths + } } fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context) -> Div { @@ -444,9 +502,9 @@ fn render_item_single( } } -fn read_settings_value_from_path<'a>( +pub fn read_settings_value_from_path<'a>( settings_contents: &'a serde_json::Value, - path: &[&'static str], + path: &[&str], ) -> Option<&'a serde_json::Value> { // todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested let Some((key, remaining)) = path.split_first() else { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index f82f544acc9bbd6f42b0017d84f27cf793b64799..9aceec1fbebefd2cd87f83a8546064f376d198b1 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -188,6 +188,7 @@ itertools.workspace = true language = { workspace = true, features = ["test-support"] } pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } +settings_ui = { workspace = true, features = ["test-support"] } terminal_view = { workspace = true, features = ["test-support"] } tree-sitter-md.workspace = true tree-sitter-rust.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d0e4687a132a85645cdbfe52e67ebb6afd894c0e..96f0f261dcce9268976f92ec028f0581fb648913 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4855,4 +4855,34 @@ mod tests { "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost" ); } + + #[gpui::test] + fn test_settings_defaults(cx: &mut TestAppContext) { + cx.update(|cx| { + settings::init(cx); + workspace::init_settings(cx); + title_bar::init(cx); + editor::init_settings(cx); + debugger_ui::init(cx); + }); + let default_json = + cx.read(|cx| cx.global::().raw_default_settings().clone()); + + let all_paths = cx.read(|cx| settings_ui::SettingsUiTree::new(cx).all_paths(cx)); + let mut failures = Vec::new(); + for path in all_paths { + if settings_ui::read_settings_value_from_path(&default_json, &path).is_none() { + failures.push(path); + } + } + if !failures.is_empty() { + panic!( + "No default value found for paths: {:#?}", + failures + .into_iter() + .map(|path| path.join(".")) + .collect::>() + ); + } + } } From 0e76cc803634df61621b0b2a1382dc682196ca14 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 3 Sep 2025 16:39:04 -0300 Subject: [PATCH 555/744] acp: Display a new version call out when one is available (#37479) CleanShot 2025-09-03 at 16 13 59@2x Release Notes: - Agent Panel: Display a callout when a new version of an external agent is available --------- Co-authored-by: Cole Miller --- crates/agent_servers/src/agent_servers.rs | 17 +++- crates/agent_servers/src/e2e_tests.rs | 2 +- crates/agent_ui/src/acp/message_editor.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 101 +++++++++++++++++----- 4 files changed, 95 insertions(+), 27 deletions(-) diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index c610c53ea8d61d24ece6d3c80ec15505d259ea3b..6ac81639ca4e6e94cab22fc7cb7ee5344c92983e 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -45,11 +45,20 @@ pub fn init(cx: &mut App) { pub struct AgentServerDelegate { project: Entity, status_tx: Option>, + new_version_available: Option>>, } impl AgentServerDelegate { - pub fn new(project: Entity, status_tx: Option>) -> Self { - Self { project, status_tx } + pub fn new( + project: Entity, + status_tx: Option>, + new_version_tx: Option>>, + ) -> Self { + Self { + project, + status_tx, + new_version_available: new_version_tx, + } } pub fn project(&self) -> &Entity { @@ -73,6 +82,7 @@ impl AgentServerDelegate { ))); }; let status_tx = self.status_tx; + let new_version_available = self.new_version_available; cx.spawn(async move |cx| { if !ignore_system_version { @@ -160,6 +170,9 @@ impl AgentServerDelegate { ) .await .log_err(); + if let Some(mut new_version_available) = new_version_available { + new_version_available.send(Some(latest_version)).ok(); + } } } }) diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 7988b86081351b29c8a19b676498db26d0b83fc3..f801ef246807f93c4bbdc26a1ff3bd478cc476d0 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -498,7 +498,7 @@ pub async fn new_test_thread( current_dir: impl AsRef, cx: &mut TestAppContext, ) -> Entity { - let delegate = AgentServerDelegate::new(project.clone(), None); + let delegate = AgentServerDelegate::new(project.clone(), None, None); let connection = cx .update(|cx| server.connect(current_dir.as_ref(), delegate, cx)) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index ebe0e5c1c6dbcee71df010f4702e7567a8c26b2f..da121bb7a486d80f15125d2ecc526b3b01e059d3 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -700,7 +700,7 @@ impl MessageEditor { self.project.read(cx).fs().clone(), self.history_store.clone(), )); - let delegate = AgentServerDelegate::new(self.project.clone(), None); + let delegate = AgentServerDelegate::new(self.project.clone(), None, None); let connection = server.connect(Path::new(""), delegate, cx); cx.spawn(async move |_, cx| { let agent = connection.await?; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 992e12177abe144b1ba00b7a5e2a9c8806866593..5269b8b0f7a85b63e560b53115bcc8ebf74bb8ab 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -46,7 +46,7 @@ use text::Anchor; use theme::ThemeSettings; use ui::{ Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, - PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*, + PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -288,6 +288,7 @@ pub struct AcpThreadView { prompt_capabilities: Rc>, available_commands: Rc>>, is_loading_contents: bool, + new_server_version_available: Option, _cancel_task: Option>, _subscriptions: [Subscription; 3], } @@ -416,9 +417,23 @@ impl AcpThreadView { _subscriptions: subscriptions, _cancel_task: None, focus_handle: cx.focus_handle(), + new_server_version_available: None, } } + fn reset(&mut self, window: &mut Window, cx: &mut Context) { + self.thread_state = Self::initial_state( + self.agent.clone(), + None, + self.workspace.clone(), + self.project.clone(), + window, + cx, + ); + self.new_server_version_available.take(); + cx.notify(); + } + fn initial_state( agent: Rc, resume_thread: Option, @@ -451,8 +466,13 @@ impl AcpThreadView { }) .next() .unwrap_or_else(|| paths::home_dir().as_path().into()); - let (tx, mut rx) = watch::channel("Loading…".into()); - let delegate = AgentServerDelegate::new(project.clone(), Some(tx)); + let (status_tx, mut status_rx) = watch::channel("Loading…".into()); + let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None); + let delegate = AgentServerDelegate::new( + project.clone(), + Some(status_tx), + Some(new_version_available_tx), + ); let connect_task = agent.connect(&root_dir, delegate, cx); let load_task = cx.spawn_in(window, async move |this, cx| { @@ -627,10 +647,23 @@ impl AcpThreadView { .log_err(); }); + cx.spawn(async move |this, cx| { + while let Ok(new_version) = new_version_available_rx.recv().await { + if let Some(new_version) = new_version { + this.update(cx, |this, cx| { + this.new_server_version_available = Some(new_version.into()); + cx.notify(); + }) + .log_err(); + } + } + }) + .detach(); + let loading_view = cx.new(|cx| { let update_title_task = cx.spawn(async move |this, cx| { loop { - let status = rx.recv().await?; + let status = status_rx.recv().await?; this.update(cx, |this: &mut LoadingView, cx| { this.title = status; cx.notify(); @@ -672,15 +705,7 @@ impl AcpThreadView { .map_or(false, |provider| provider.is_authenticated(cx)) { this.update(cx, |this, cx| { - this.thread_state = Self::initial_state( - agent.clone(), - None, - this.workspace.clone(), - this.project.clone(), - window, - cx, - ); - cx.notify(); + this.reset(window, cx); }) .ok(); } @@ -1443,7 +1468,6 @@ impl AcpThreadView { cx.notify(); self.auth_task = Some(cx.spawn_in(window, { - let project = self.project.clone(); let agent = self.agent.clone(); async move |this, cx| { let result = authenticate.await; @@ -1472,14 +1496,7 @@ impl AcpThreadView { } this.handle_thread_error(err, cx); } else { - this.thread_state = Self::initial_state( - agent, - None, - this.workspace.clone(), - project.clone(), - window, - cx, - ) + this.reset(window, cx); } this.auth_task.take() }) @@ -1501,7 +1518,7 @@ impl AcpThreadView { let cwd = project.first_project_directory(cx); let shell = project.terminal_settings(&cwd, cx).shell.clone(); - let delegate = AgentServerDelegate::new(project_entity.clone(), None); + let delegate = AgentServerDelegate::new(project_entity.clone(), None, None); let command = ClaudeCode::login_command(delegate, cx); window.spawn(cx, async move |cx| { @@ -4800,6 +4817,38 @@ impl AcpThreadView { Some(div().child(content)) } + fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context) -> Div { + v_flex().w_full().justify_end().child( + h_flex() + .p_2() + .pr_3() + .w_full() + .gap_1p5() + .border_t_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().element_background) + .child( + h_flex() + .flex_1() + .gap_1p5() + .child( + Icon::new(IconName::Download) + .color(Color::Accent) + .size(IconSize::Small), + ) + .child(Label::new("New version available").size(LabelSize::Small)), + ) + .child( + Button::new("update-button", format!("Update to v{}", version)) + .label_size(LabelSize::Small) + .style(ButtonStyle::Tinted(TintColor::Accent)) + .on_click(cx.listener(|this, _, window, cx| { + this.reset(window, cx); + })), + ), + ) + } + fn get_current_model_name(&self, cx: &App) -> SharedString { // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet") // For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI") @@ -5210,6 +5259,12 @@ impl Render for AcpThreadView { }) .children(self.render_thread_retry_status_callout(window, cx)) .children(self.render_thread_error(window, cx)) + .when_some( + self.new_server_version_available.as_ref().filter(|_| { + !has_messages || !matches!(self.thread_state, ThreadState::Ready { .. }) + }), + |this, version| this.child(self.render_new_version_callout(&version, cx)), + ) .children( if let Some(usage_callout) = self.render_usage_callout(line_height, cx) { Some(usage_callout.into_any_element()) From eedfc5be5aad0a043aec03ebb922e7ee5e5fc9b3 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 3 Sep 2025 15:47:39 -0400 Subject: [PATCH 556/744] acp: Improve handling of invalid external agent server downloads (#37465) Related to #37213, #37150 When listing previously-downloaded versions of an external agent, don't try to use any downloads that are missing the agent entrypoint (indicating that they're corrupt/unusable), and delete those versions, so that we can attempt to download the latest version again. Also report clearer errors when failing to start a session due to an agent server entrypoint or root directory not existing. Release Notes: - N/A --- crates/agent_servers/src/agent_servers.rs | 27 ++++++++++++++--------- crates/agent_servers/src/claude.rs | 8 +++++++ crates/agent_servers/src/gemini.rs | 12 ++++++++-- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 6ac81639ca4e6e94cab22fc7cb7ee5344c92983e..e214dabfc763c2a46f1f4665c3d1f881d5ce406e 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -111,9 +111,11 @@ impl AgentServerDelegate { continue; }; - if let Some(version) = file_name - .to_str() - .and_then(|name| semver::Version::from_str(&name).ok()) + if let Some(name) = file_name.to_str() + && let Some(version) = semver::Version::from_str(name).ok() + && fs + .is_file(&dir.join(file_name).join(&entrypoint_path)) + .await { versions.push((version, file_name.to_owned())); } else { @@ -156,6 +158,7 @@ impl AgentServerDelegate { cx.background_spawn({ let file_name = file_name.clone(); let dir = dir.clone(); + let fs = fs.clone(); async move { let latest_version = node_runtime.npm_package_latest_version(&package_name).await; @@ -184,7 +187,7 @@ impl AgentServerDelegate { } let dir = dir.clone(); cx.background_spawn(Self::download_latest_version( - fs, + fs.clone(), dir.clone(), node_runtime, package_name, @@ -192,14 +195,18 @@ impl AgentServerDelegate { .await? .into() }; + + let agent_server_path = dir.join(version).join(entrypoint_path); + let agent_server_path_exists = fs.is_file(&agent_server_path).await; + anyhow::ensure!( + agent_server_path_exists, + "Missing entrypoint path {} after installation", + agent_server_path.to_string_lossy() + ); + anyhow::Ok(AgentServerCommand { path: node_path, - args: vec![ - dir.join(version) - .join(entrypoint_path) - .to_string_lossy() - .to_string(), - ], + args: vec![agent_server_path.to_string_lossy().to_string()], env: Default::default(), }) }) diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 0a4f152e8afd991fed90af12aa5bbff909c8aa2d..a02d8c37c1ebaa87acdbeee4ce384787758f12b7 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -76,6 +76,7 @@ impl AgentServer for ClaudeCode { cx: &mut App, ) -> Task>> { let root_dir = root_dir.to_path_buf(); + let fs = delegate.project().read(cx).fs().clone(); let server_name = self.name(); let settings = cx.read_global(|settings: &SettingsStore, _| { settings.get::(None).claude.clone() @@ -109,6 +110,13 @@ impl AgentServer for ClaudeCode { .insert("ANTHROPIC_API_KEY".to_owned(), api_key.key); } + let root_dir_exists = fs.is_dir(&root_dir).await; + anyhow::ensure!( + root_dir_exists, + "Session root {} does not exist or is not a directory", + root_dir.to_string_lossy() + ); + crate::acp::connect(server_name, command.clone(), &root_dir, cx).await }) } diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index a1553d288ab44d96bdfe08723a092ce231ba005b..b58ad703cda496c4413f30decbfa5e0b1d1b0735 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -36,6 +36,7 @@ impl AgentServer for Gemini { cx: &mut App, ) -> Task>> { let root_dir = root_dir.to_path_buf(); + let fs = delegate.project().read(cx).fs().clone(); let server_name = self.name(); let settings = cx.read_global(|settings: &SettingsStore, _| { settings.get::(None).gemini.clone() @@ -74,6 +75,13 @@ impl AgentServer for Gemini { .insert("GEMINI_API_KEY".to_owned(), api_key.key); } + let root_dir_exists = fs.is_dir(&root_dir).await; + anyhow::ensure!( + root_dir_exists, + "Session root {} does not exist or is not a directory", + root_dir.to_string_lossy() + ); + let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await; match &result { Ok(connection) => { @@ -92,7 +100,7 @@ impl AgentServer for Gemini { log::error!("connected to gemini, but missing prompt_capabilities.image (version is {current_version})"); return Err(LoadError::Unsupported { current_version: current_version.into(), - command: command.path.to_string_lossy().to_string().into(), + command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(), minimum_version: Self::MINIMUM_VERSION.into(), } .into()); @@ -129,7 +137,7 @@ impl AgentServer for Gemini { if !supported { return Err(LoadError::Unsupported { current_version: current_version.into(), - command: command.path.to_string_lossy().to_string().into(), + command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(), minimum_version: Self::MINIMUM_VERSION.into(), } .into()); From bb2d833373400d4debf416e90d5b2870f75dde96 Mon Sep 17 00:00:00 2001 From: localcc Date: Wed, 3 Sep 2025 21:52:47 +0200 Subject: [PATCH 557/744] Revert "gpui: Fix overflow_hidden to support clip with border radius" (#37480) This reverts commit 40199266b6634cc3165f3842abae1d562ef4dcca. The issue with the commit is: ContentMask::intersect is doing intersection of corner radii which makes inner containers use the max corner radius out of all the parents when it should be more complex to correctly clip children (clip sorting..?) Release Notes: - N/A --- crates/editor/src/element.rs | 25 +- crates/gpui/examples/content_mask.rs | 228 ------------------ crates/gpui/src/elements/list.rs | 78 ++---- crates/gpui/src/elements/uniform_list.rs | 5 +- crates/gpui/src/platform/blade/shaders.wgsl | 60 ++--- crates/gpui/src/platform/mac/shaders.metal | 24 +- crates/gpui/src/platform/windows/shaders.hlsl | 49 ++-- crates/gpui/src/style.rs | 66 +++-- crates/gpui/src/window.rs | 23 +- crates/terminal_view/src/terminal_element.rs | 2 +- crates/ui/src/components/scrollbar.rs | 16 +- 11 files changed, 130 insertions(+), 446 deletions(-) delete mode 100644 crates/gpui/examples/content_mask.rs diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c21d31aa5c14e3b596a6089630b29a3e55a3084d..9822ec23d5af41ee6fbfdd7c471f6fcc9437c78b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -6043,7 +6043,6 @@ impl EditorElement { window.with_content_mask( Some(ContentMask { bounds: layout.position_map.text_hitbox.bounds, - ..Default::default() }), |window| { let editor = self.editor.read(cx); @@ -6986,15 +6985,9 @@ impl EditorElement { } else { let mut bounds = layout.hitbox.bounds; bounds.origin.x += layout.gutter_hitbox.bounds.size.width; - window.with_content_mask( - Some(ContentMask { - bounds, - ..Default::default() - }), - |window| { - block.element.paint(window, cx); - }, - ) + window.with_content_mask(Some(ContentMask { bounds }), |window| { + block.element.paint(window, cx); + }) } } } @@ -8297,13 +8290,9 @@ impl Element for EditorElement { } let rem_size = self.rem_size(cx); - let content_mask = ContentMask { - bounds, - ..Default::default() - }; window.with_rem_size(rem_size, |window| { window.with_text_style(Some(text_style), |window| { - window.with_content_mask(Some(content_mask), |window| { + window.with_content_mask(Some(ContentMask { bounds }), |window| { let (mut snapshot, is_read_only) = self.editor.update(cx, |editor, cx| { (editor.snapshot(window, cx), editor.read_only(cx)) }); @@ -9411,13 +9400,9 @@ impl Element for EditorElement { ..Default::default() }; let rem_size = self.rem_size(cx); - let content_mask = ContentMask { - bounds, - ..Default::default() - }; window.with_rem_size(rem_size, |window| { window.with_text_style(Some(text_style), |window| { - window.with_content_mask(Some(content_mask), |window| { + window.with_content_mask(Some(ContentMask { bounds }), |window| { self.paint_mouse_listeners(layout, window, cx); self.paint_background(layout, window, cx); self.paint_indent_guides(layout, window, cx); diff --git a/crates/gpui/examples/content_mask.rs b/crates/gpui/examples/content_mask.rs deleted file mode 100644 index 8d40cc5bba7c35395cbeef009f6b648ed68826ed..0000000000000000000000000000000000000000 --- a/crates/gpui/examples/content_mask.rs +++ /dev/null @@ -1,228 +0,0 @@ -use gpui::{ - App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, - rgb, size, -}; - -struct Example {} - -impl Render for Example { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .font_family(".SystemUIFont") - .flex() - .flex_col() - .size_full() - .p_4() - .gap_4() - .bg(rgb(0x505050)) - .justify_center() - .items_center() - .text_center() - .shadow_lg() - .text_sm() - .text_color(rgb(0xffffff)) - .child( - div() - .overflow_hidden() - .rounded(px(32.)) - .border(px(8.)) - .border_color(gpui::white()) - .text_color(gpui::white()) - .child( - div() - .bg(gpui::black()) - .py_2() - .px_7() - .border_l_2() - .border_r_2() - .border_b_3() - .border_color(gpui::red()) - .child("Let build applications with GPUI"), - ) - .child( - div() - .bg(rgb(0x222222)) - .text_sm() - .py_1() - .px_7() - .border_l_3() - .border_r_3() - .border_color(gpui::green()) - .child("The fast, productive UI framework for Rust"), - ) - .child( - div() - .bg(rgb(0x222222)) - .w_full() - .flex() - .flex_row() - .text_sm() - .text_color(rgb(0xc0c0c0)) - .child( - div() - .flex_1() - .p_2() - .border_3() - .border_dashed() - .border_color(gpui::blue()) - .child("Rust"), - ) - .child( - div() - .flex_1() - .p_2() - .border_t_3() - .border_r_3() - .border_b_3() - .border_dashed() - .border_color(gpui::blue()) - .child("GPU Rendering"), - ), - ), - ) - .child( - div() - .flex() - .flex_col() - .w(px(320.)) - .gap_1() - .overflow_hidden() - .rounded(px(16.)) - .child( - div() - .w_full() - .p_2() - .bg(gpui::red()) - .child("Clip background"), - ), - ) - .child( - div() - .flex() - .flex_col() - .w(px(320.)) - .gap_1() - .rounded(px(16.)) - .child( - div() - .w_full() - .p_2() - .bg(gpui::yellow()) - .text_color(gpui::black()) - .child("No content mask"), - ), - ) - .child( - div() - .flex() - .flex_col() - .w(px(320.)) - .gap_1() - .overflow_hidden() - .rounded(px(16.)) - .child( - div() - .w_full() - .p_2() - .border_4() - .border_color(gpui::blue()) - .bg(gpui::blue().alpha(0.4)) - .child("Clip borders"), - ), - ) - .child( - div() - .flex() - .flex_col() - .w(px(320.)) - .gap_1() - .overflow_hidden() - .rounded(px(20.)) - .child( - div().w_full().border_2().border_color(gpui::black()).child( - div() - .size_full() - .bg(gpui::green().alpha(0.4)) - .p_2() - .border_8() - .border_color(gpui::green()) - .child("Clip nested elements"), - ), - ), - ) - .child( - div() - .flex() - .flex_col() - .w(px(320.)) - .gap_1() - .overflow_hidden() - .rounded(px(32.)) - .child( - div() - .w_full() - .p_2() - .bg(gpui::black()) - .border_2() - .border_dashed() - .rounded_lg() - .border_color(gpui::white()) - .child("dash border full and rounded"), - ) - .child( - div() - .w_full() - .flex() - .flex_row() - .gap_2() - .child( - div() - .w_full() - .p_2() - .bg(gpui::black()) - .border_x_2() - .border_dashed() - .rounded_lg() - .border_color(gpui::white()) - .child("border x"), - ) - .child( - div() - .w_full() - .p_2() - .bg(gpui::black()) - .border_y_2() - .border_dashed() - .rounded_lg() - .border_color(gpui::white()) - .child("border y"), - ), - ) - .child( - div() - .w_full() - .p_2() - .bg(gpui::black()) - .border_2() - .border_dashed() - .border_color(gpui::white()) - .child("border full and no rounded"), - ), - ) - } -} - -fn main() { - Application::new().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(800.), px(600.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| Example {}), - ) - .unwrap(); - cx.activate(true); - }); -} diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 9ae497cef92c9f1aa68eea4af6aea8bf410a817f..ed4ca64e83513531b9176f05c4c00b0af71aea74 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -8,10 +8,10 @@ //! If all of your elements are the same height, see [`crate::UniformList`] for a simpler API use crate::{ - AnyElement, App, AvailableSpace, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, - EntityId, FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, - IntoElement, Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, - StyleRefinement, Styled, Window, point, px, size, + AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId, + FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, + Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, + Window, point, px, size, }; use collections::VecDeque; use refineable::Refineable as _; @@ -705,7 +705,6 @@ impl StateInner { &mut self, bounds: Bounds, padding: Edges, - corner_radii: Corners, autoscroll: bool, render_item: &mut RenderItemFn, window: &mut Window, @@ -729,15 +728,9 @@ impl StateInner { let mut item_origin = bounds.origin + Point::new(px(0.), padding.top); item_origin.y -= layout_response.scroll_top.offset_in_item; for item in &mut layout_response.item_layouts { - window.with_content_mask( - Some(ContentMask { - bounds, - corner_radii, - }), - |window| { - item.element.prepaint_at(item_origin, window, cx); - }, - ); + window.with_content_mask(Some(ContentMask { bounds }), |window| { + item.element.prepaint_at(item_origin, window, cx); + }); if let Some(autoscroll_bounds) = window.take_autoscroll() && autoscroll @@ -959,34 +952,19 @@ impl Element for List { state.items = new_items; } - let rem_size = window.rem_size(); - let padding = style.padding.to_pixels(bounds.size.into(), rem_size); - let corner_radii = style.corner_radii.to_pixels(rem_size); - let layout = match state.prepaint_items( - bounds, - padding, - corner_radii, - true, - &mut self.render_item, - window, - cx, - ) { - Ok(layout) => layout, - Err(autoscroll_request) => { - state.logical_scroll_top = Some(autoscroll_request); - state - .prepaint_items( - bounds, - padding, - corner_radii, - false, - &mut self.render_item, - window, - cx, - ) - .unwrap() - } - }; + let padding = style + .padding + .to_pixels(bounds.size.into(), window.rem_size()); + let layout = + match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) { + Ok(layout) => layout, + Err(autoscroll_request) => { + state.logical_scroll_top = Some(autoscroll_request); + state + .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx) + .unwrap() + } + }; state.last_layout_bounds = Some(bounds); state.last_padding = Some(padding); @@ -1004,17 +982,11 @@ impl Element for List { cx: &mut App, ) { let current_view = window.current_view(); - window.with_content_mask( - Some(ContentMask { - bounds, - ..Default::default() - }), - |window| { - for item in &mut prepaint.layout.item_layouts { - item.element.paint(window, cx); - } - }, - ); + window.with_content_mask(Some(ContentMask { bounds }), |window| { + for item in &mut prepaint.layout.item_layouts { + item.element.paint(window, cx); + } + }); let list_state = self.state.clone(); let height = bounds.size.height; diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index db3b8c88395388bf57eb16a0bb73ce2d6f005779..cdf90d4eb8934de99a21c65b6c9efa2a2fdde258 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -411,10 +411,7 @@ impl Element for UniformList { (self.render_items)(visible_range.clone(), window, cx) }; - let content_mask = ContentMask { - bounds, - ..Default::default() - }; + let content_mask = ContentMask { bounds }; window.with_content_mask(Some(content_mask), |window| { for (mut item, ix) in items.into_iter().zip(visible_range.clone()) { let item_origin = padded_bounds.origin diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index dbab4237e3eb87c570096ae7b6d7328fa1da6ff2..95980b54fe4f25b3936d6b095219c5674211dd0a 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -53,11 +53,6 @@ struct Corners { bottom_left: f32, } -struct ContentMask { - bounds: Bounds, - corner_radii: Corners, -} - struct Edges { top: f32, right: f32, @@ -445,7 +440,7 @@ struct Quad { order: u32, border_style: u32, bounds: Bounds, - content_mask: ContentMask, + content_mask: Bounds, background: Background, border_color: Hsla, corner_radii: Corners, @@ -483,7 +478,7 @@ fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta out.background_color1 = gradient.color1; out.border_color = hsla_to_rgba(quad.border_color); out.quad_id = instance_id; - out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask.bounds); + out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask); return out; } @@ -496,19 +491,8 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { let quad = b_quads[input.quad_id]; - // Signed distance field threshold for inclusion of pixels. 0.5 is the - // minimum distance between the center of the pixel and the edge. - let antialias_threshold = 0.5; - - var background_color = gradient_color(quad.background, input.position.xy, quad.bounds, + let background_color = gradient_color(quad.background, input.position.xy, quad.bounds, input.background_solid, input.background_color0, input.background_color1); - var border_color = input.border_color; - - // Apply content_mask corner radii clipping - let clip_sdf = quad_sdf(input.position.xy, quad.content_mask.bounds, quad.content_mask.corner_radii); - let clip_alpha = saturate(antialias_threshold - clip_sdf); - background_color.a *= clip_alpha; - border_color.a *= clip_alpha; let unrounded = quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 && @@ -529,6 +513,10 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { let point = input.position.xy - quad.bounds.origin; let center_to_point = point - half_size; + // Signed distance field threshold for inclusion of pixels. 0.5 is the + // minimum distance between the center of the pixel and the edge. + let antialias_threshold = 0.5; + // Radius of the nearest corner let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); @@ -619,6 +607,8 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { var color = background_color; if (border_sdf < antialias_threshold) { + var border_color = input.border_color; + // Dashed border logic when border_style == 1 if (quad.border_style == 1) { // Position along the perimeter in "dash space", where each dash @@ -654,11 +644,7 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { let is_horizontal = corner_center_to_point.x < corner_center_to_point.y; - var border_width = select(border.y, border.x, is_horizontal); - // When border width of some side is 0, we need to use the other side width for dash velocity. - if (border_width == 0.0) { - border_width = select(border.x, border.y, is_horizontal); - } + let border_width = select(border.y, border.x, is_horizontal); dash_velocity = dv_numerator / border_width; t = select(point.y, point.x, is_horizontal) * dash_velocity; max_t = select(size.y, size.x, is_horizontal) * dash_velocity; @@ -870,7 +856,7 @@ struct Shadow { blur_radius: f32, bounds: Bounds, corner_radii: Corners, - content_mask: ContentMask, + content_mask: Bounds, color: Hsla, } var b_shadows: array; @@ -898,7 +884,7 @@ fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) ins out.position = to_device_position(unit_vertex, shadow.bounds); out.color = hsla_to_rgba(shadow.color); out.shadow_id = instance_id; - out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask.bounds); + out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask); return out; } @@ -913,6 +899,7 @@ fn fs_shadow(input: ShadowVarying) -> @location(0) vec4 { let half_size = shadow.bounds.size / 2.0; let center = shadow.bounds.origin + half_size; let center_to_point = input.position.xy - center; + let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii); // The signal is only non-zero in a limited range, so don't waste samples @@ -1040,7 +1027,7 @@ struct Underline { order: u32, pad: u32, bounds: Bounds, - content_mask: ContentMask, + content_mask: Bounds, color: Hsla, thickness: f32, wavy: u32, @@ -1064,7 +1051,7 @@ fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) out.position = to_device_position(unit_vertex, underline.bounds); out.color = hsla_to_rgba(underline.color); out.underline_id = instance_id; - out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask.bounds); + out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask); return out; } @@ -1106,7 +1093,7 @@ struct MonochromeSprite { order: u32, pad: u32, bounds: Bounds, - content_mask: ContentMask, + content_mask: Bounds, color: Hsla, tile: AtlasTile, transformation: TransformationMatrix, @@ -1130,7 +1117,7 @@ fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index out.tile_position = to_tile_position(unit_vertex, sprite.tile); out.color = hsla_to_rgba(sprite.color); - out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask.bounds); + out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); return out; } @@ -1152,7 +1139,7 @@ struct PolychromeSprite { grayscale: u32, opacity: f32, bounds: Bounds, - content_mask: ContentMask, + content_mask: Bounds, corner_radii: Corners, tile: AtlasTile, } @@ -1174,7 +1161,7 @@ fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index out.position = to_device_position(unit_vertex, sprite.bounds); out.tile_position = to_tile_position(unit_vertex, sprite.tile); out.sprite_id = instance_id; - out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask.bounds); + out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); return out; } @@ -1247,12 +1234,3 @@ fn fs_surface(input: SurfaceVarying) -> @location(0) vec4 { return ycbcr_to_RGB * y_cb_cr; } - -fn max_corner_radii(a: Corners, b: Corners) -> Corners { - return Corners( - max(a.top_left, b.top_left), - max(a.top_right, b.top_right), - max(a.bottom_right, b.bottom_right), - max(a.bottom_left, b.bottom_left) - ); -} diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index 6aa1d18ee895ec84a6f19edbcc6618cf713aa5a5..83c978b853443d5c612f514625f94b6d6725be8a 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders.metal @@ -99,21 +99,8 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], constant Quad *quads [[buffer(QuadInputIndex_Quads)]]) { Quad quad = quads[input.quad_id]; - - // Signed distance field threshold for inclusion of pixels. 0.5 is the - // minimum distance between the center of the pixel and the edge. - const float antialias_threshold = 0.5; - float4 background_color = fill_color(quad.background, input.position.xy, quad.bounds, input.background_solid, input.background_color0, input.background_color1); - float4 border_color = input.border_color; - - // Apply content_mask corner radii clipping - float clip_sdf = quad_sdf(input.position.xy, quad.content_mask.bounds, - quad.content_mask.corner_radii); - float clip_alpha = saturate(antialias_threshold - clip_sdf); - background_color.a *= clip_alpha; - border_color *= clip_alpha; bool unrounded = quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 && @@ -134,6 +121,10 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], float2 point = input.position.xy - float2(quad.bounds.origin.x, quad.bounds.origin.y); float2 center_to_point = point - half_size; + // Signed distance field threshold for inclusion of pixels. 0.5 is the + // minimum distance between the center of the pixel and the edge. + const float antialias_threshold = 0.5; + // Radius of the nearest corner float corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); @@ -173,6 +164,7 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], straight_border_inner_corner_to_point.x > 0.0 || straight_border_inner_corner_to_point.y > 0.0; + // Whether the point is far enough inside the quad, such that the pixels are // not affected by the straight border. bool is_within_inner_straight_border = @@ -216,6 +208,8 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], float4 color = background_color; if (border_sdf < antialias_threshold) { + float4 border_color = input.border_color; + // Dashed border logic when border_style == 1 if (quad.border_style == 1) { // Position along the perimeter in "dash space", where each dash @@ -250,10 +244,6 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]], // perimeter. This way each line starts and ends with a dash. bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y; float border_width = is_horizontal ? border.x : border.y; - // When border width of some side is 0, we need to use the other side width for dash velocity. - if (border_width == 0.0) { - border_width = is_horizontal ? border.y : border.x; - } dash_velocity = dv_numerator / border_width; t = is_horizontal ? point.x : point.y; t *= dash_velocity; diff --git a/crates/gpui/src/platform/windows/shaders.hlsl b/crates/gpui/src/platform/windows/shaders.hlsl index 296a6c825f8b057b5330d108c21c27ffb4fa1b75..2cef54ae6166e313795eb42210b5f07c1bc378fc 100644 --- a/crates/gpui/src/platform/windows/shaders.hlsl +++ b/crates/gpui/src/platform/windows/shaders.hlsl @@ -453,16 +453,11 @@ float quarter_ellipse_sdf(float2 pt, float2 radii) { ** */ -struct ContentMask { - Bounds bounds; - Corners corner_radii; -}; - struct Quad { uint order; uint border_style; Bounds bounds; - ContentMask content_mask; + Bounds content_mask; Background background; Hsla border_color; Corners corner_radii; @@ -501,7 +496,7 @@ QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint quad_id: SV_Insta quad.background.solid, quad.background.colors ); - float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask.bounds); + float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask); float4 border_color = hsla_to_rgba(quad.border_color); QuadVertexOutput output; @@ -517,21 +512,8 @@ QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint quad_id: SV_Insta float4 quad_fragment(QuadFragmentInput input): SV_Target { Quad quad = quads[input.quad_id]; - - // Signed distance field threshold for inclusion of pixels. 0.5 is the - // minimum distance between the center of the pixel and the edge. - const float antialias_threshold = 0.5; - float4 background_color = gradient_color(quad.background, input.position.xy, quad.bounds, - input.background_solid, input.background_color0, input.background_color1); - float4 border_color = input.border_color; - - // Apply content_mask corner radii clipping - float clip_sdf = quad_sdf(input.position.xy, quad.content_mask.bounds, - quad.content_mask.corner_radii); - float clip_alpha = saturate(antialias_threshold - clip_sdf); - background_color.a *= clip_alpha; - border_color *= clip_alpha; + input.background_solid, input.background_color0, input.background_color1); bool unrounded = quad.corner_radii.top_left == 0.0 && quad.corner_radii.top_right == 0.0 && @@ -552,6 +534,10 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target { float2 the_point = input.position.xy - quad.bounds.origin; float2 center_to_point = the_point - half_size; + // Signed distance field threshold for inclusion of pixels. 0.5 is the + // minimum distance between the center of the pixel and the edge. + const float antialias_threshold = 0.5; + // Radius of the nearest corner float corner_radius = pick_corner_radius(center_to_point, quad.corner_radii); @@ -634,6 +620,7 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target { float4 color = background_color; if (border_sdf < antialias_threshold) { + float4 border_color = input.border_color; // Dashed border logic when border_style == 1 if (quad.border_style == 1) { // Position along the perimeter in "dash space", where each dash @@ -668,10 +655,6 @@ float4 quad_fragment(QuadFragmentInput input): SV_Target { // perimeter. This way each line starts and ends with a dash. bool is_horizontal = corner_center_to_point.x < corner_center_to_point.y; float border_width = is_horizontal ? border.x : border.y; - // When border width of some side is 0, we need to use the other side width for dash velocity. - if (border_width == 0.0) { - border_width = is_horizontal ? border.y : border.x; - } dash_velocity = dv_numerator / border_width; t = is_horizontal ? the_point.x : the_point.y; t *= dash_velocity; @@ -822,7 +805,7 @@ struct Shadow { float blur_radius; Bounds bounds; Corners corner_radii; - ContentMask content_mask; + Bounds content_mask; Hsla color; }; @@ -851,7 +834,7 @@ ShadowVertexOutput shadow_vertex(uint vertex_id: SV_VertexID, uint shadow_id: SV bounds.size += 2.0 * margin; float4 device_position = to_device_position(unit_vertex, bounds); - float4 clip_distance = distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask.bounds); + float4 clip_distance = distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask); float4 color = hsla_to_rgba(shadow.color); ShadowVertexOutput output; @@ -1004,7 +987,7 @@ struct Underline { uint order; uint pad; Bounds bounds; - ContentMask content_mask; + Bounds content_mask; Hsla color; float thickness; uint wavy; @@ -1030,7 +1013,7 @@ UnderlineVertexOutput underline_vertex(uint vertex_id: SV_VertexID, uint underli Underline underline = underlines[underline_id]; float4 device_position = to_device_position(unit_vertex, underline.bounds); float4 clip_distance = distance_from_clip_rect(unit_vertex, underline.bounds, - underline.content_mask.bounds); + underline.content_mask); float4 color = hsla_to_rgba(underline.color); UnderlineVertexOutput output; @@ -1078,7 +1061,7 @@ struct MonochromeSprite { uint order; uint pad; Bounds bounds; - ContentMask content_mask; + Bounds content_mask; Hsla color; AtlasTile tile; TransformationMatrix transformation; @@ -1105,7 +1088,7 @@ MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexI MonochromeSprite sprite = mono_sprites[sprite_id]; float4 device_position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation); - float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask.bounds); + float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask); float2 tile_position = to_tile_position(unit_vertex, sprite.tile); float4 color = hsla_to_rgba(sprite.color); @@ -1135,7 +1118,7 @@ struct PolychromeSprite { uint grayscale; float opacity; Bounds bounds; - ContentMask content_mask; + Bounds content_mask; Corners corner_radii; AtlasTile tile; }; @@ -1160,7 +1143,7 @@ PolychromeSpriteVertexOutput polychrome_sprite_vertex(uint vertex_id: SV_VertexI PolychromeSprite sprite = poly_sprites[sprite_id]; float4 device_position = to_device_position(unit_vertex, sprite.bounds); float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, - sprite.content_mask.bounds); + sprite.content_mask); float2 tile_position = to_tile_position(unit_vertex, sprite.tile); PolychromeSpriteVertexOutput output; diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 09f598f9b0deb4ddd9910e32e4dbe020b54c849a..5b69ce7fa6eb06affc2f77c0d1bdfbe4165c206a 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -601,19 +601,7 @@ impl Style { (false, false) => Bounds::from_corners(min, max), }; - let corner_radii = self.corner_radii.to_pixels(rem_size); - let border_widths = self.border_widths.to_pixels(rem_size); - Some(ContentMask { - bounds: Bounds { - origin: bounds.origin - point(border_widths.left, border_widths.top), - size: bounds.size - + size( - border_widths.left + border_widths.right, - border_widths.top + border_widths.bottom, - ), - }, - corner_radii, - }) + Some(ContentMask { bounds }) } } } @@ -673,16 +661,64 @@ impl Style { if self.is_border_visible() { let border_widths = self.border_widths.to_pixels(rem_size); + let max_border_width = border_widths.max(); + let max_corner_radius = corner_radii.max(); + + let top_bounds = Bounds::from_corners( + bounds.origin, + bounds.top_right() + point(Pixels::ZERO, max_border_width.max(max_corner_radius)), + ); + let bottom_bounds = Bounds::from_corners( + bounds.bottom_left() - point(Pixels::ZERO, max_border_width.max(max_corner_radius)), + bounds.bottom_right(), + ); + let left_bounds = Bounds::from_corners( + top_bounds.bottom_left(), + bottom_bounds.origin + point(max_border_width, Pixels::ZERO), + ); + let right_bounds = Bounds::from_corners( + top_bounds.bottom_right() - point(max_border_width, Pixels::ZERO), + bottom_bounds.top_right(), + ); + let mut background = self.border_color.unwrap_or_default(); background.a = 0.; - window.paint_quad(quad( + let quad = quad( bounds, corner_radii, background, border_widths, self.border_color.unwrap_or_default(), self.border_style, - )); + ); + + window.with_content_mask(Some(ContentMask { bounds: top_bounds }), |window| { + window.paint_quad(quad.clone()); + }); + window.with_content_mask( + Some(ContentMask { + bounds: right_bounds, + }), + |window| { + window.paint_quad(quad.clone()); + }, + ); + window.with_content_mask( + Some(ContentMask { + bounds: bottom_bounds, + }), + |window| { + window.paint_quad(quad.clone()); + }, + ); + window.with_content_mask( + Some(ContentMask { + bounds: left_bounds, + }), + |window| { + window.paint_quad(quad); + }, + ); } #[cfg(debug_assertions)] diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index cebf911b734798a3965857db0f3fd6fe2be2b011..0ec73c4b0040e6c65cd8819ecf5d20a9ec1900d0 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1283,8 +1283,6 @@ pub(crate) struct DispatchEventResult { pub struct ContentMask { /// The bounds pub bounds: Bounds

, - /// The corner radii of the content mask. - pub corner_radii: Corners

, } impl ContentMask { @@ -1292,31 +1290,13 @@ impl ContentMask { pub fn scale(&self, factor: f32) -> ContentMask { ContentMask { bounds: self.bounds.scale(factor), - corner_radii: self.corner_radii.scale(factor), } } /// Intersect the content mask with the given content mask. pub fn intersect(&self, other: &Self) -> Self { let bounds = self.bounds.intersect(&other.bounds); - ContentMask { - bounds, - corner_radii: Corners { - top_left: self.corner_radii.top_left.max(other.corner_radii.top_left), - top_right: self - .corner_radii - .top_right - .max(other.corner_radii.top_right), - bottom_right: self - .corner_radii - .bottom_right - .max(other.corner_radii.bottom_right), - bottom_left: self - .corner_radii - .bottom_left - .max(other.corner_radii.bottom_left), - }, - } + ContentMask { bounds } } } @@ -2577,7 +2557,6 @@ impl Window { origin: Point::default(), size: self.viewport_size, }, - ..Default::default() }) } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index e4d7f226912cdb6123081dd5cfb38f205d9a9333..5bbf5ad36b3de89514d92ce9e305988817cec32f 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1184,7 +1184,7 @@ impl Element for TerminalElement { cx: &mut App, ) { let paint_start = Instant::now(); - window.with_content_mask(Some(ContentMask { bounds, ..Default::default() }), |window| { + window.with_content_mask(Some(ContentMask { bounds }), |window| { let scroll_top = self.terminal_view.read(cx).scroll_top; window.paint_quad(fill(bounds, layout.background_color)); diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index 475575a483ea4659b572f8f82e13faf8b539fb37..605028202fffa37d67bbdb4a9f33a97459390dfa 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -303,13 +303,9 @@ impl Element for Scrollbar { window: &mut Window, _: &mut App, ) -> Self::PrepaintState { - window.with_content_mask( - Some(ContentMask { - bounds, - ..Default::default() - }), - |window| window.insert_hitbox(bounds, HitboxBehavior::Normal), - ) + window.with_content_mask(Some(ContentMask { bounds }), |window| { + window.insert_hitbox(bounds, HitboxBehavior::Normal) + }) } fn paint( @@ -323,11 +319,7 @@ impl Element for Scrollbar { cx: &mut App, ) { const EXTRA_PADDING: Pixels = px(5.0); - let content_mask = ContentMask { - bounds, - ..Default::default() - }; - window.with_content_mask(Some(content_mask), |window| { + window.with_content_mask(Some(ContentMask { bounds }), |window| { let axis = self.kind; let colors = cx.theme().colors(); let thumb_state = self.state.thumb_state.get(); From 2aa0114b40e090cf54a76663761d2ae772da9b4d Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:59:12 -0300 Subject: [PATCH 558/744] ai onboarding: Add some fast-follow adjustments (#37486) Closes https://github.com/zed-industries/zed/issues/37305 Release Notes: - N/A --------- Co-authored-by: Ben Kunkle Co-authored-by: Anthony Eid --- crates/agent_ui/src/agent_panel.rs | 14 ++++++++++++++ .../src/edit_prediction_onboarding_content.rs | 8 ++++++-- crates/ai_onboarding/src/young_account_banner.rs | 2 +- crates/zeta/src/onboarding_modal.rs | 13 +++++++++++-- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 305261183e92de6dbe2ad5756293e8b0bbf77849..d021eaefb5ebff43fad1fe4822b3758550a0179f 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2967,6 +2967,20 @@ impl AgentPanel { return false; } + let user_store = self.user_store.read(cx); + + if user_store + .plan() + .is_some_and(|plan| matches!(plan, Plan::ZedPro)) + && user_store + .subscription_period() + .and_then(|period| period.0.checked_add_days(chrono::Days::new(1))) + .is_some_and(|date| date < chrono::Utc::now()) + { + OnboardingUpsell::set_dismissed(true, cx); + return false; + } + match &self.active_view { ActiveView::History | ActiveView::Configuration => false, ActiveView::ExternalAgentThread { thread_view, .. } diff --git a/crates/ai_onboarding/src/edit_prediction_onboarding_content.rs b/crates/ai_onboarding/src/edit_prediction_onboarding_content.rs index e883d8da8ce01bfea3f08676666c308a90f6d650..50b729c37ee8cf98e188d66e716bdafa4ad11ca1 100644 --- a/crates/ai_onboarding/src/edit_prediction_onboarding_content.rs +++ b/crates/ai_onboarding/src/edit_prediction_onboarding_content.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use client::{Client, UserStore}; +use cloud_llm_client::Plan; use gpui::{Entity, IntoElement, ParentElement}; use ui::prelude::*; @@ -35,6 +36,8 @@ impl EditPredictionOnboarding { impl Render for EditPredictionOnboarding { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_free_plan = self.user_store.read(cx).plan() == Some(Plan::ZedFree); + let github_copilot = v_flex() .gap_1() .child(Label::new(if self.copilot_is_configured { @@ -67,7 +70,8 @@ impl Render for EditPredictionOnboarding { self.continue_with_zed_ai.clone(), cx, )) - .child(ui::Divider::horizontal()) - .child(github_copilot) + .when(is_free_plan, |this| { + this.child(ui::Divider::horizontal()).child(github_copilot) + }) } } diff --git a/crates/ai_onboarding/src/young_account_banner.rs b/crates/ai_onboarding/src/young_account_banner.rs index ed9a6b3b35fb2e8e3afaa9d9b658539dd3fa6541..ae13b9556885c1552f7e90935f844347cd76a778 100644 --- a/crates/ai_onboarding/src/young_account_banner.rs +++ b/crates/ai_onboarding/src/young_account_banner.rs @@ -6,7 +6,7 @@ pub struct YoungAccountBanner; impl RenderOnce for YoungAccountBanner { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, we cannot offer plans to GitHub accounts created fewer than 30 days ago. To request an exception, reach out to billing-support@zed.dev."; + const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for free plan usage or Pro plan free trial. To request an exception, reach out to billing-support@zed.dev."; let label = div() .w_full() diff --git a/crates/zeta/src/onboarding_modal.rs b/crates/zeta/src/onboarding_modal.rs index 3a58c8c7b812b193724eaf911cb15db264204964..6b743d95f2d2765be0d332e1aa8a03a9647131aa 100644 --- a/crates/zeta/src/onboarding_modal.rs +++ b/crates/zeta/src/onboarding_modal.rs @@ -14,7 +14,7 @@ use settings::update_settings_file; use ui::{Vector, VectorName, prelude::*}; use workspace::{ModalView, Workspace}; -/// Introduces user to Zed's Edit Prediction feature and terms of service +/// Introduces user to Zed's Edit Prediction feature pub struct ZedPredictModal { onboarding: Entity, focus_handle: FocusHandle, @@ -86,7 +86,16 @@ impl Focusable for ZedPredictModal { } } -impl ModalView for ZedPredictModal {} +impl ModalView for ZedPredictModal { + fn on_before_dismiss( + &mut self, + _window: &mut Window, + cx: &mut Context, + ) -> workspace::DismissDecision { + ZedPredictUpsell::set_dismissed(true, cx); + workspace::DismissDecision::Dismiss(true) + } +} impl Render for ZedPredictModal { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { From ec1528b8905e6ecacaad07e4641cc0b6342aef7c Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:09:59 -0300 Subject: [PATCH 559/744] thread view: Refine the terminal tool card header UI (#37488) Rendering the disclosure button last (on the far right of the header container) to avoid awkward layouts when there's truncation and elapsed time information being displayed. Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 46 +++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 5269b8b0f7a85b63e560b53115bcc8ebf74bb8ab..e277caf58da8bfdf97c5991d69a67cbd8006fc5e 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2637,28 +2637,6 @@ impl AcpThreadView { .with_rotate_animation(2) ) }) - .child( - Disclosure::new( - SharedString::from(format!( - "terminal-tool-disclosure-{}", - terminal.entity_id() - )), - is_expanded, - ) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .visible_on_hover(&header_group) - .on_click(cx.listener({ - let id = tool_call.id.clone(); - move |this, _event, _window, _cx| { - if is_expanded { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - } - })), - ) .when(truncated_output, |header| { let tooltip = if let Some(output) = output { if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { @@ -2717,7 +2695,29 @@ impl AcpThreadView { ))) }), ) - }); + }) + .child( + Disclosure::new( + SharedString::from(format!( + "terminal-tool-disclosure-{}", + terminal.entity_id() + )), + is_expanded, + ) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronDown) + .visible_on_hover(&header_group) + .on_click(cx.listener({ + let id = tool_call.id.clone(); + move |this, _event, _window, _cx| { + if is_expanded { + this.expanded_tool_calls.remove(&id); + } else { + this.expanded_tool_calls.insert(id.clone()); + } + } + })), + ); let terminal_view = self .entry_view_state From bb13228ad599dbb46d61f30ce0ae808df9c77e4d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 3 Sep 2025 14:24:32 -0700 Subject: [PATCH 560/744] Revert "Remote: Change "sh -c" to "sh -lc" (#36760)" (#37417) This reverts commit bf5ed6d1c9795369310b5b9d6c752d9dc54991b5. We believe this may be breaking some users whose shell initialization scripts change the working directory. Release Notes: - N/A --- crates/remote/src/transport/ssh.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 0995e0dd611ae667cc2e68638773c8b80bf2f22b..42c6da04b5c39b0b1133b2d13549585fa9433ef7 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -254,7 +254,7 @@ impl RemoteConnection for SshRemoteConnection { let ssh_proxy_process = match self .socket - .ssh_command("sh", &["-lc", &start_proxy_command]) + .ssh_command("sh", &["-c", &start_proxy_command]) // IMPORTANT: we kill this process when we drop the task that uses it. .kill_on_drop(true) .spawn() @@ -529,7 +529,7 @@ impl SshRemoteConnection { .run_command( "sh", &[ - "-lc", + "-c", &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) @@ -607,7 +607,7 @@ impl SshRemoteConnection { .run_command( "sh", &[ - "-lc", + "-c", &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) @@ -655,7 +655,7 @@ impl SshRemoteConnection { dst_path = &dst_path.to_string() ) }; - self.socket.run_command("sh", &["-lc", &script]).await?; + self.socket.run_command("sh", &["-c", &script]).await?; Ok(()) } @@ -797,7 +797,7 @@ impl SshSocket { } async fn platform(&self) -> Result { - let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?; + let uname = self.run_command("sh", &["-c", "uname -sm"]).await?; let Some((os, arch)) = uname.split_once(" ") else { anyhow::bail!("unknown uname: {uname:?}") }; @@ -828,7 +828,7 @@ impl SshSocket { } async fn shell(&self) -> String { - match self.run_command("sh", &["-lc", "echo $SHELL"]).await { + match self.run_command("sh", &["-c", "echo $SHELL"]).await { Ok(shell) => shell.trim().to_owned(), Err(e) => { log::error!("Failed to get shell: {e}"); From 3b7dbb87b07e8b784ce52fbbf36e06ae9011e3b3 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:31:54 -0300 Subject: [PATCH 561/744] docs: Add note about `CLAUDE.md` usage (#37496) Some users asked whether Claude Code in Zed can also observe/consume `CLAUDE.md` guidelines, regardless of whether they're at the root `.claude` directory or within the project. Answer is yes and the documentation will mention it now! Release Notes: - N/A --- docs/src/ai/external-agents.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/ai/external-agents.md b/docs/src/ai/external-agents.md index de374511d053d6226e22c3d55e1cf2a96b91ad85..c0794a6a0181369e97d50cca64c9a711fbd8e5f4 100644 --- a/docs/src/ai/external-agents.md +++ b/docs/src/ai/external-agents.md @@ -98,6 +98,11 @@ However, the SDK doesn't yet expose everything needed to fully support all of th > Also note that some [first-party agent](./agent-panel.md) features don't yet work with Claude Code: editing past messages, resuming threads from history, checkpointing, and using the agent in SSH projects. > We hope to add these features in the near future. +#### CLAUDE.md + +If you already have a `CLAUDE.md` file in your project (either at the root of it or in subdirectories) or at your root `.claude` directory, Claude Code in Zed will use it. +If you don't have one, you can make Claude Code create one for you through through the `init` slash command. + ## Add Custom Agents {#add-custom-agents} You can run any agent speaking ACP in Zed by changing your settings as follows: From bf1ae1d196d9ddd0c90663f118bd3019b56fd5df Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:46:35 -0300 Subject: [PATCH 562/744] docs: Fix typo in the `CLAUDE.md` section (#37497) Follow-up to https://github.com/zed-industries/zed/pull/37496. Fix a typo and improves writing overall. Release Notes: - N/A --- docs/src/ai/external-agents.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/src/ai/external-agents.md b/docs/src/ai/external-agents.md index c0794a6a0181369e97d50cca64c9a711fbd8e5f4..e8f348b32a2e020603d10f92a507743e69480400 100644 --- a/docs/src/ai/external-agents.md +++ b/docs/src/ai/external-agents.md @@ -100,8 +100,9 @@ However, the SDK doesn't yet expose everything needed to fully support all of th #### CLAUDE.md -If you already have a `CLAUDE.md` file in your project (either at the root of it or in subdirectories) or at your root `.claude` directory, Claude Code in Zed will use it. -If you don't have one, you can make Claude Code create one for you through through the `init` slash command. +Claude Code in Zed will automatically use any `CLAUDE.md` file found in your project root, project subdirectories, or root `.claude` directory. + +If you don't have a `CLAUDE.md` file, you can ask Claude Code to create one for you through the `init` slash command. ## Add Custom Agents {#add-custom-agents} From be0bb4a56b0b495b895c99b27ce54e36b76aa9a7 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 3 Sep 2025 17:10:14 -0500 Subject: [PATCH 563/744] Centralize `ZED_STATELESS` (#37492) Closes #ISSUE Centralizes the references to the `ZED_STATELESS` env var into a single location in a new crate named `zed_env_vars` Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 12 ++++++++++++ Cargo.toml | 2 ++ crates/agent/Cargo.toml | 1 + crates/agent/src/thread_store.rs | 3 +-- crates/agent2/Cargo.toml | 1 + crates/agent2/src/db.rs | 4 +--- crates/assistant_context/Cargo.toml | 3 ++- crates/assistant_context/src/context_store.rs | 3 +-- crates/db/Cargo.toml | 1 + crates/db/src/db.rs | 6 ++---- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 2 +- crates/zed_env_vars/Cargo.toml | 18 ++++++++++++++++++ crates/zed_env_vars/LICENSE-GPL | 1 + crates/zed_env_vars/src/zed_env_vars.rs | 6 ++++++ script/new-crate | 1 + 16 files changed, 52 insertions(+), 13 deletions(-) create mode 100644 crates/zed_env_vars/Cargo.toml create mode 120000 crates/zed_env_vars/LICENSE-GPL create mode 100644 crates/zed_env_vars/src/zed_env_vars.rs diff --git a/Cargo.lock b/Cargo.lock index 239323517c01045b3ddb0d23b1d99f0904d137a6..1e237d8438f319348a1408c5b82d74360ace09a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,6 +190,7 @@ dependencies = [ "uuid", "workspace", "workspace-hack", + "zed_env_vars", "zstd", ] @@ -278,6 +279,7 @@ dependencies = [ "web_search", "workspace-hack", "worktree", + "zed_env_vars", "zlog", "zstd", ] @@ -848,6 +850,7 @@ dependencies = [ "uuid", "workspace", "workspace-hack", + "zed_env_vars", ] [[package]] @@ -4489,6 +4492,7 @@ dependencies = [ "tempfile", "util", "workspace-hack", + "zed_env_vars", ] [[package]] @@ -20549,6 +20553,7 @@ dependencies = [ "workspace", "workspace-hack", "zed_actions", + "zed_env_vars", "zeta", "zlog", "zlog_settings", @@ -20565,6 +20570,13 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "zed_env_vars" +version = "0.1.0" +dependencies = [ + "workspace-hack", +] + [[package]] name = "zed_extension_api" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 08551ef75a315563f9cdb22c0ed92742d550df61..3e90af94c56aa7035dc82c1222d2152a6a8e09f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -193,6 +193,7 @@ members = [ "crates/x_ai", "crates/zed", "crates/zed_actions", + "crates/zed_env_vars", "crates/zeta", "crates/zeta_cli", "crates/zlog", @@ -420,6 +421,7 @@ worktree = { path = "crates/worktree" } x_ai = { path = "crates/x_ai" } zed = { path = "crates/zed" } zed_actions = { path = "crates/zed_actions" } +zed_env_vars = { path = "crates/zed_env_vars" } zeta = { path = "crates/zeta" } zlog = { path = "crates/zlog" } zlog_settings = { path = "crates/zlog_settings" } diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 391abb38fe826923b06646511c0dd6c5ce5c6ca4..76f96647c7af5692ca9b4b146e27f9f7c19c7995 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -63,6 +63,7 @@ time.workspace = true util.workspace = true uuid.workspace = true workspace-hack.workspace = true +zed_env_vars.workspace = true zstd.workspace = true [dev-dependencies] diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index cba2457566709d5664270a8239495aaac3fec6fb..2eae758b835d5d79ccf86f18be032f2d9bb87c2b 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -41,8 +41,7 @@ use std::{ }; use util::ResultExt as _; -pub static ZED_STATELESS: std::sync::LazyLock = - std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); +use zed_env_vars::ZED_STATELESS; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum DataType { diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 0e9c8fcf7237627d2cb7b17b977b68322160e6d5..b712bed258dfb69ddf81a1ba431ec7a3566b9baf 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -68,6 +68,7 @@ uuid.workspace = true watch.workspace = true web_search.workspace = true workspace-hack.workspace = true +zed_env_vars.workspace = true zstd.workspace = true [dev-dependencies] diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index e7d31c0c7ac4dd2327931e2d888ec29f6ca96e73..c78725138ffa081cc5b75c883d883b7a155d482c 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent2/src/db.rs @@ -18,6 +18,7 @@ use sqlez::{ }; use std::sync::Arc; use ui::{App, SharedString}; +use zed_env_vars::ZED_STATELESS; pub type DbMessage = crate::Message; pub type DbSummary = DetailedSummaryState; @@ -201,9 +202,6 @@ impl DbThread { } } -pub static ZED_STATELESS: std::sync::LazyLock = - std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum DataType { #[serde(rename = "json")] diff --git a/crates/assistant_context/Cargo.toml b/crates/assistant_context/Cargo.toml index 45c0072418782909829ba3186138f0c6a9456654..3e2761a84674c6c4201165edf856b675843315d9 100644 --- a/crates/assistant_context/Cargo.toml +++ b/crates/assistant_context/Cargo.toml @@ -50,8 +50,9 @@ text.workspace = true ui.workspace = true util.workspace = true uuid.workspace = true -workspace-hack.workspace = true workspace.workspace = true +workspace-hack.workspace = true +zed_env_vars.workspace = true [dev-dependencies] indoc.workspace = true diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index 6960d9db7948e8f09ea65ede91702d98e6bc99be..5fac44e31f4cc073af8fe6bbb57f75fc03b27f45 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -24,6 +24,7 @@ use rpc::AnyProtoClient; use std::sync::LazyLock; use std::{cmp::Reverse, ffi::OsStr, mem, path::Path, sync::Arc, time::Duration}; use util::{ResultExt, TryFutureExt}; +use zed_env_vars::ZED_STATELESS; pub(crate) fn init(client: &AnyProtoClient) { client.add_entity_message_handler(ContextStore::handle_advertise_contexts); @@ -788,8 +789,6 @@ impl ContextStore { fn reload(&mut self, cx: &mut Context) -> Task> { let fs = self.fs.clone(); cx.spawn(async move |this, cx| { - pub static ZED_STATELESS: LazyLock = - LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); if *ZED_STATELESS { return Ok(()); } diff --git a/crates/db/Cargo.toml b/crates/db/Cargo.toml index c53b2988b94dd5b355e132024c2677b61a83d071..de449cd38f77d062eda906cced3e3b697a370d15 100644 --- a/crates/db/Cargo.toml +++ b/crates/db/Cargo.toml @@ -27,6 +27,7 @@ sqlez.workspace = true sqlez_macros.workspace = true util.workspace = true workspace-hack.workspace = true +zed_env_vars.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 0802bd8bb7ec738b948d0dbf14c24863833e3ba1..eab2f115d8e5c3db51541544a8dbc95f34713741 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -17,9 +17,10 @@ use sqlez::thread_safe_connection::ThreadSafeConnection; use sqlez_macros::sql; use std::future::Future; use std::path::Path; +use std::sync::atomic::AtomicBool; use std::sync::{LazyLock, atomic::Ordering}; -use std::{env, sync::atomic::AtomicBool}; use util::{ResultExt, maybe}; +use zed_env_vars::ZED_STATELESS; const CONNECTION_INITIALIZE_QUERY: &str = sql!( PRAGMA foreign_keys=TRUE; @@ -36,9 +37,6 @@ const FALLBACK_DB_NAME: &str = "FALLBACK_MEMORY_DB"; const DB_FILE_NAME: &str = "db.sqlite"; -pub static ZED_STATELESS: LazyLock = - LazyLock::new(|| env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); - pub static ALL_FILE_DB_FAILED: LazyLock = LazyLock::new(|| AtomicBool::new(false)); /// Open or create a database at the given directory path. diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 9aceec1fbebefd2cd87f83a8546064f376d198b1..f2295d5fa732d9e36e2b37cf346199f35cabc803 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -165,6 +165,7 @@ web_search_providers.workspace = true workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true +zed_env_vars.workspace = true zeta.workspace = true zlog.workspace = true zlog_settings.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 52e475edf8620a1ce97ce732f12777d9bb0cad1a..9582e7a2ab541243a768370eb08ed1f4f1c465a3 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -288,7 +288,7 @@ pub fn main() { let (open_listener, mut open_rx) = OpenListener::new(); - let failed_single_instance_check = if *db::ZED_STATELESS + let failed_single_instance_check = if *zed_env_vars::ZED_STATELESS || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev { false diff --git a/crates/zed_env_vars/Cargo.toml b/crates/zed_env_vars/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..9abfc410e7e74774c4e9e7608e8c1c3824ebc3c1 --- /dev/null +++ b/crates/zed_env_vars/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "zed_env_vars" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/zed_env_vars.rs" + +[features] +default = [] + +[dependencies] +workspace-hack.workspace = true diff --git a/crates/zed_env_vars/LICENSE-GPL b/crates/zed_env_vars/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/zed_env_vars/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zed_env_vars/src/zed_env_vars.rs b/crates/zed_env_vars/src/zed_env_vars.rs new file mode 100644 index 0000000000000000000000000000000000000000..d1679a0518f2bae857364b0035b6184350ffca55 --- /dev/null +++ b/crates/zed_env_vars/src/zed_env_vars.rs @@ -0,0 +1,6 @@ +use std::sync::LazyLock; + +/// Whether Zed is running in stateless mode. +/// When true, Zed will use in-memory databases instead of persistent storage. +pub static ZED_STATELESS: LazyLock = + LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); diff --git a/script/new-crate b/script/new-crate index 52ee900b30837cbf77fa1e3145e0282fa5e19b7c..1ac2d9262133c788969fe594b5b06480f1293fa7 100755 --- a/script/new-crate +++ b/script/new-crate @@ -63,6 +63,7 @@ anyhow.workspace = true gpui.workspace = true ui.workspace = true util.workspace = true +workspace-hack.workspace = true # Uncomment other workspace dependencies as needed # assistant.workspace = true From d6f0811dabb9a5428780893598e0075c4829387d Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 3 Sep 2025 19:24:59 -0300 Subject: [PATCH 564/744] acp: Receive available commands over notifications (#37499) See: https://github.com/zed-industries/agent-client-protocol/pull/62 Release Notes: - Agent Panel: Fixes an issue where Claude Code would timeout waiting for slash commands to be loaded Co-authored-by: Cole Miller --- Cargo.lock | 4 +-- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 12 +++---- crates/acp_thread/src/connection.rs | 1 - crates/agent2/src/agent.rs | 1 - crates/agent_servers/src/acp.rs | 1 - crates/agent_servers/src/claude.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 47 ++++++++++++++------------ crates/agent_ui/src/agent_diff.rs | 1 + 9 files changed, 34 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e237d8438f319348a1408c5b82d74360ace09a2..58d01da63372431e107ea9c0b17fde0700f9050f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,9 +196,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.2.0-alpha.4" +version = "0.2.0-alpha.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "603941db1d130ee275840c465b73a2312727d4acef97449550ccf033de71301f" +checksum = "6d02292efd75080932b6466471d428c70e2ac06908ae24792fc7c36ecbaf67ca" dependencies = [ "anyhow", "async-broadcast", diff --git a/Cargo.toml b/Cargo.toml index 3e90af94c56aa7035dc82c1222d2152a6a8e09f0..941c364e0dd85def66ebbc4e310ef0a90458fe44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -430,7 +430,7 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agent-client-protocol = { version = "0.2.0-alpha.4", features = ["unstable"]} +agent-client-protocol = { version = "0.2.0-alpha.6", features = ["unstable"]} 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 804e4683a7cc20ac2bcd80f10139d641aa864b98..dc295369cce2b8fda596e3917724187bd35b7377 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -785,7 +785,6 @@ pub struct AcpThread { session_id: acp::SessionId, token_usage: Option, prompt_capabilities: acp::PromptCapabilities, - available_commands: Vec, _observe_prompt_capabilities: Task>, determine_shell: Shared>, terminals: HashMap>, @@ -805,6 +804,7 @@ pub enum AcpThreadEvent { LoadError(LoadError), PromptCapabilitiesUpdated, Refusal, + AvailableCommandsUpdated(Vec), } impl EventEmitter for AcpThread {} @@ -860,7 +860,6 @@ impl AcpThread { action_log: Entity, session_id: acp::SessionId, mut prompt_capabilities_rx: watch::Receiver, - available_commands: Vec, cx: &mut Context, ) -> Self { let prompt_capabilities = *prompt_capabilities_rx.borrow(); @@ -900,7 +899,6 @@ impl AcpThread { session_id, token_usage: None, prompt_capabilities, - available_commands, _observe_prompt_capabilities: task, terminals: HashMap::default(), determine_shell, @@ -911,10 +909,6 @@ impl AcpThread { self.prompt_capabilities } - pub fn available_commands(&self) -> Vec { - self.available_commands.clone() - } - pub fn connection(&self) -> &Rc { &self.connection } @@ -1010,6 +1004,9 @@ impl AcpThread { acp::SessionUpdate::Plan(plan) => { self.update_plan(plan, cx); } + acp::SessionUpdate::AvailableCommandsUpdate { available_commands } => { + cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands)) + } } Ok(()) } @@ -3080,7 +3077,6 @@ mod tests { audio: true, embedded_context: true, }), - vec![], cx, ) }); diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 7901b08c907811ac1b6b74b975ca66b6b901868f..1c465a4cdd466e34dcb8fc31ed910f84a4469582 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -338,7 +338,6 @@ mod test_support { audio: true, embedded_context: true, }), - vec![], cx, ) }); diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 241e3d389f96a320d8a43e23493c4738a76802d6..e96b4c0cfa32be910a7a77e58a1911deb7e5357a 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -292,7 +292,6 @@ impl NativeAgent { action_log.clone(), session_id.clone(), prompt_capabilities_rx, - vec![], cx, ) }); diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 7907083e144c3d6188120a8ae8e24aa0ddbd765b..7991c1e3ccedafe8891ef80c57c4939bb19d2fb1 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -224,7 +224,6 @@ impl AgentConnection for AcpConnection { session_id.clone(), // ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically. watch::Receiver::constant(self.agent_capabilities.prompt_capabilities), - response.available_commands, cx, ) })?; diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index a02d8c37c1ebaa87acdbeee4ce384787758f12b7..194867241baf86cf7b3d3ab168318a00d64d6e25 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -40,7 +40,7 @@ impl ClaudeCode { Self::PACKAGE_NAME.into(), "node_modules/@anthropic-ai/claude-code/cli.js".into(), true, - None, + Some("0.2.5".parse().unwrap()), cx, ) })? diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index e277caf58da8bfdf97c5991d69a67cbd8006fc5e..50da44e430fd684d0e91d43ee82a0ccb0117111d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -430,6 +430,7 @@ impl AcpThreadView { window, cx, ); + self.available_commands.replace(vec![]); self.new_server_version_available.take(); cx.notify(); } @@ -535,26 +536,6 @@ impl AcpThreadView { Ok(thread) => { let action_log = thread.read(cx).action_log().clone(); - let mut available_commands = thread.read(cx).available_commands(); - - if connection - .auth_methods() - .iter() - .any(|method| method.id.0.as_ref() == "claude-login") - { - available_commands.push(acp::AvailableCommand { - name: "login".to_owned(), - description: "Authenticate".to_owned(), - input: None, - }); - available_commands.push(acp::AvailableCommand { - name: "logout".to_owned(), - description: "Authenticate".to_owned(), - input: None, - }); - } - this.available_commands.replace(available_commands); - this.prompt_capabilities .set(thread.read(cx).prompt_capabilities()); @@ -1343,6 +1324,30 @@ impl AcpThreadView { .set(thread.read(cx).prompt_capabilities()); } AcpThreadEvent::TokenUsageUpdated => {} + AcpThreadEvent::AvailableCommandsUpdated(available_commands) => { + let mut available_commands = available_commands.clone(); + + if thread + .read(cx) + .connection() + .auth_methods() + .iter() + .any(|method| method.id.0.as_ref() == "claude-login") + { + available_commands.push(acp::AvailableCommand { + name: "login".to_owned(), + description: "Authenticate".to_owned(), + input: None, + }); + available_commands.push(acp::AvailableCommand { + name: "logout".to_owned(), + description: "Authenticate".to_owned(), + input: None, + }); + } + + self.available_commands.replace(available_commands); + } } cx.notify(); } @@ -5745,7 +5750,6 @@ pub(crate) mod tests { audio: true, embedded_context: true, }), - vec![], cx, ) }))) @@ -5805,7 +5809,6 @@ pub(crate) mod tests { audio: true, embedded_context: true, }), - Vec::new(), cx, ) }))) diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index f9d7321ca8dd72b791a462d50f262ce0f5531fd5..e3688dccce87ab9fb563aa3129fb94c1390d003f 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1528,6 +1528,7 @@ impl AgentDiff { | AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::ToolAuthorizationRequired | AcpThreadEvent::PromptCapabilitiesUpdated + | AcpThreadEvent::AvailableCommandsUpdated(_) | AcpThreadEvent::Retry(_) => {} } } From da2d791127872bf596af3fc39a3b761e5a3e8609 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 3 Sep 2025 21:02:34 -0300 Subject: [PATCH 565/744] Update external agents installation docs (#37500) --- docs/src/ai/external-agents.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/src/ai/external-agents.md b/docs/src/ai/external-agents.md index e8f348b32a2e020603d10f92a507743e69480400..963e41d42f53ad68ef70de3466913b71b11bd38e 100644 --- a/docs/src/ai/external-agents.md +++ b/docs/src/ai/external-agents.md @@ -30,12 +30,19 @@ If you'd like to bind this to a keyboard shortcut, you can do so by editing your #### Installation -If you don't yet have Gemini CLI installed, then Zed will install a version for you. If you do, then we will use the version of Gemini CLI on your path. +The first time you create a Gemini CLI thread, Zed will install [@google/gemini-cli](https://github.com/zed-industries/claude-code-acp). This installation is only available to Zed and is kept up to date as you use the agent. -You need to be running at least Gemini version `0.2.0`, and if your version of Gemini is too old you will see an -error message. +By default, Zed will use this managed version of Gemini CLI even if you have it installed globally. However, you can configure it to use a version in your `PATH` by adding this to your settings: -The instructions to upgrade Gemini depend on how you originally installed it, but typically, running `npm install -g @google/gemini-cli@latest` should work. +```json +{ + "agent_servers": { + "gemini": { + "ignore_system_version": false + } + } +} +``` #### Authentication @@ -80,8 +87,9 @@ If you'd like to bind this to a keyboard shortcut, you can do so by editing your #### Installation -If you don't yet have Claude Code installed, then Zed will install a version for you. -If you do, then we will use the version of Claude Code on your path. +The first time you create a Claude Code thread, Zed will install [@zed-industries/claude-code-acp](https://github.com/zed-industries/claude-code-acp). This installation is only available to Zed and is kept up to date as you use the agent. + +Zed will always use this managed version of Claude Code even if you have it installed globally. ### Usage @@ -122,6 +130,8 @@ You can run any agent speaking ACP in Zed by changing your settings as follows: This can also be useful if you're in the middle of developing a new agent that speaks the protocol and you want to debug it. +You can also specify a custom path, arguments, or environment for the builtin integrations by using the `claude` and `gemini` names. + ## Debugging Agents When using external agents in Zed, you can access the debug view via with `dev: open acp logs` from the Command Palette. This lets you see the messages being sent and received between Zed and the agent. From 9eeeda1330e814c57e71e317a14b3f357d549642 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 3 Sep 2025 20:02:57 -0400 Subject: [PATCH 566/744] onboarding: Add telemetry to Basics page (#37502) - Welcome Keymap Changed - Welcome Theme Changed - Welcome Theme mode Changed - Welcome Page Telemetry Diagnostics Toggled - Welcome Page Telemetry Metrics Toggled - Welcome Vim Mode Toggled - Welcome Keymap Changed - Welcome Sign In Clicked cc: @katie-z-geer Release Notes: - N/A --- crates/onboarding/src/basics_page.rs | 56 +++++++++++++++++++++++++--- crates/onboarding/src/onboarding.rs | 1 + crates/sum_tree/src/tree_map.rs | 2 +- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 991386cb389b1ac5bdeb2e76bae4a210fe3b2cce..d98db03be8a02c9e5dd7b36fa7e4fae4b2a320d3 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -68,6 +68,12 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement MODE_NAMES[mode as usize].clone(), move |_, _, cx| { write_mode_change(mode, cx); + + telemetry::event!( + "Welcome Theme mode Changed", + from = theme_mode, + to = mode + ); }, ) }), @@ -105,7 +111,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement ThemeMode::Dark => Appearance::Dark, ThemeMode::System => *system_appearance, }; - let current_theme_name = theme_selection.theme(appearance); + let current_theme_name = SharedString::new(theme_selection.theme(appearance)); let theme_names = match appearance { Appearance::Light => LIGHT_THEMES, @@ -149,8 +155,15 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement }) .on_click({ let theme_name = theme.name.clone(); + let current_theme_name = current_theme_name.clone(); + move |_, _, cx| { write_theme_change(theme_name.clone(), theme_mode, cx); + telemetry::event!( + "Welcome Theme Changed", + from = current_theme_name, + to = theme_name + ); } }) .map(|this| { @@ -239,6 +252,17 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement cx, move |setting, _| setting.metrics = Some(enabled), ); + + // This telemetry event shouldn't fire when it's off. If it does we're be alerted + // and can fix it in a timely manner to respect a user's choice. + telemetry::event!("Welcome Page Telemetry Metrics Toggled", + options = if enabled { + "on" + } else { + "off" + } + ); + }}, ).tab_index({ *tab_index += 1; @@ -267,6 +291,16 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement cx, move |setting, _| setting.diagnostics = Some(enabled), ); + + // This telemetry event shouldn't fire when it's off. If it does we're be alerted + // and can fix it in a timely manner to respect a user's choice. + telemetry::event!("Welcome Page Telemetry Diagnostics Toggled", + options = if enabled { + "on" + } else { + "off" + } + ); } } ).tab_index({ @@ -327,6 +361,8 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE update_settings_file::(fs, cx, move |setting, _| { setting.base_keymap = Some(keymap_base); }); + + telemetry::event!("Welcome Keymap Changed", keymap = keymap_base); } } @@ -344,13 +380,21 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme { let fs = ::global(cx); move |&selection, _, cx| { - update_settings_file::(fs.clone(), cx, move |setting, _| { - *setting = match selection { - ToggleState::Selected => Some(true), - ToggleState::Unselected => Some(false), - ToggleState::Indeterminate => None, + let vim_mode = match selection { + ToggleState::Selected => true, + ToggleState::Unselected => false, + ToggleState::Indeterminate => { + return; } + }; + update_settings_file::(fs.clone(), cx, move |setting, _| { + *setting = Some(vim_mode); }); + + telemetry::event!( + "Welcome Vim Mode Toggled", + options = if vim_mode { "on" } else { "off" }, + ); } }, ) diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 873dd63201423bba8995136e2fde82551966b3dd..7f1bb81d4d7486ea85e23dedcb763e238d53b2f3 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -476,6 +476,7 @@ impl Onboarding { .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(|_, window, cx| { + telemetry::event!("Welcome Sign In Clicked"); window.dispatch_action(SignIn.boxed_clone(), cx); }) .into_any_element() diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index fc93d40ae5021165cefdf9130a65af221806ee6d..818214e4024497e20f1f7cc208421ffbbb1d0401 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -2,7 +2,7 @@ use std::{cmp::Ordering, fmt::Debug}; use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary}; -/// A cheaply-clonable ordered map based on a [SumTree](crate::SumTree). +/// A cheaply-cloneable ordered map based on a [SumTree](crate::SumTree). #[derive(Clone, PartialEq, Eq)] pub struct TreeMap(SumTree>) where From f36a545a86469e4bbd0726b363a742d4879de756 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 3 Sep 2025 19:03:32 -0500 Subject: [PATCH 567/744] onboarding: Improve performance of AI upsell card (#37504) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/ai_onboarding/src/ai_upsell_card.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index efe6e4165e445c4cd92f4d08dfc0c1e1947acd55..d6e7a0bbad321148495b37292c3d17f4321c0a6e 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -86,10 +86,16 @@ impl RenderOnce for AiUpsellCard { ) .child(plan_definitions.free_plan()); - let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child( - Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.)) - .color(Color::Custom(cx.theme().colors().border.opacity(0.05))), - ); + let grid_bg = h_flex() + .absolute() + .inset_0() + .w_full() + .h(px(240.)) + .bg(gpui::pattern_slash( + cx.theme().colors().border.opacity(0.1), + 2., + 25., + )); let gradient_bg = div() .absolute() From 3c021d089041d5de0e38d18de514c2a4b304fd22 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:32:13 +0530 Subject: [PATCH 568/744] language_models: Fix beta_headers for Anthropic custom models (#37306) Closes #37289 The current implementation has a problem. The **`from_id` method** in the Anthropic crate works well for predefined models, but not for custom models that are defined in the settings. This is because it fallbacks to using default beta headers, which are incorrect for custom models. The issue is that the model instance for custom models lives within the `language_models` provider, so I've updated the **`stream_completion`** method to explicitly accept beta headers from its caller. Now, the beta headers are passed from the `language_models` provider all the way to `anthropic.stream_completion`, which resolves the issue. Release Notes: - Fixed a bug where extra_beta_headers defined in settings for Anthropic custom models were being ignored. --------- Signed-off-by: Umesh Yadav --- crates/anthropic/src/anthropic.rs | 12 +++++------- crates/language_models/src/provider/anthropic.rs | 11 +++++++++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/anthropic/src/anthropic.rs b/crates/anthropic/src/anthropic.rs index 773bb557de1895e57bdeb5612e01e2839af3244b..7fd0fb4bc5abd983c57507522c2a37dffcbfa258 100644 --- a/crates/anthropic/src/anthropic.rs +++ b/crates/anthropic/src/anthropic.rs @@ -363,11 +363,9 @@ pub async fn complete( api_url: &str, api_key: &str, request: Request, + beta_headers: String, ) -> Result { let uri = format!("{api_url}/v1/messages"); - let beta_headers = Model::from_id(&request.model) - .map(|model| model.beta_headers()) - .unwrap_or_else(|_| Model::DEFAULT_BETA_HEADERS.join(",")); let request_builder = HttpRequest::builder() .method(Method::POST) .uri(uri) @@ -409,8 +407,9 @@ pub async fn stream_completion( api_url: &str, api_key: &str, request: Request, + beta_headers: String, ) -> Result>, AnthropicError> { - stream_completion_with_rate_limit_info(client, api_url, api_key, request) + stream_completion_with_rate_limit_info(client, api_url, api_key, request, beta_headers) .await .map(|output| output.0) } @@ -506,6 +505,7 @@ pub async fn stream_completion_with_rate_limit_info( api_url: &str, api_key: &str, request: Request, + beta_headers: String, ) -> Result< ( BoxStream<'static, Result>, @@ -518,9 +518,7 @@ pub async fn stream_completion_with_rate_limit_info( stream: true, }; let uri = format!("{api_url}/v1/messages"); - let beta_headers = Model::from_id(&request.base.model) - .map(|model| model.beta_headers()) - .unwrap_or_else(|_| Model::DEFAULT_BETA_HEADERS.join(",")); + let request_builder = HttpRequest::builder() .method(Method::POST) .uri(uri) diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 6c003c4c3919a9f553024c6b1b56d03d410d984b..d246976cda4eb46a46c857f1e94757697ddf5f65 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -424,14 +424,21 @@ impl AnthropicModel { return futures::future::ready(Err(anyhow!("App state dropped").into())).boxed(); }; + let beta_headers = self.model.beta_headers(); + async move { let Some(api_key) = api_key else { return Err(LanguageModelCompletionError::NoApiKey { provider: PROVIDER_NAME, }); }; - let request = - anthropic::stream_completion(http_client.as_ref(), &api_url, &api_key, request); + let request = anthropic::stream_completion( + http_client.as_ref(), + &api_url, + &api_key, + request, + beta_headers, + ); request.await.map_err(Into::into) } .boxed() From ce362864dba541002b88a779def5e7db0cbcca5d Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 3 Sep 2025 21:39:06 -0700 Subject: [PATCH 569/744] docs: Update OpenAI-compatible provider config format (#37517) The example was still showing how we used to setup openai compatible providers, but that format should only be used for changing the url for your actual OpenAI provider. If you are doing a compatible provider, it should be using the new format. Closes #37093 Release Notes: - N/A --- docs/src/ai/llm-providers.md | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index ecc4cb004befc199cf77708367e639a6dd6b029d..2846ab2f2ffda3645dea07e3c7e51803b6177018 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -435,21 +435,24 @@ To do it via your `settings.json`, add the following snippet under `language_mod ```json { "language_models": { - "openai": { - "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, - "capabilities": { - "tools": true, - "images": false, - "parallel_tool_calls": false, - "prompt_cache_key": false + "openai_compatible": { + // Using Together AI as an example + "Together AI": { + "api_url": "https://api.together.xyz/v1", + "available_models": [ + { + "name": "mistralai/Mixtral-8x7B-Instruct-v0.1", + "display_name": "Together Mixtral 8x7B", + "max_tokens": 32768, + "capabilities": { + "tools": true, + "images": false, + "parallel_tool_calls": false, + "prompt_cache_key": false + } } - } - ] + ] + } } } } @@ -463,7 +466,7 @@ By default, OpenAI-compatible models inherit the following capabilities: - `prompt_cache_key`: false (does not support `prompt_cache_key` parameter) 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. +So, ensure you have it set in your environment variables (`_API_KEY=`) so your settings can pick it up. In the example above, it would be `TOGETHER_AI_API_KEY=`. ### OpenRouter {#openrouter} From d677c98f437c920fd954d166b0d7dd01b7b489d2 Mon Sep 17 00:00:00 2001 From: Francis <75153730+fbo25@users.noreply.github.com> Date: Thu, 4 Sep 2025 07:39:55 +0200 Subject: [PATCH 570/744] agent2: Use inline enums in `now` and `edit_file` tools JSON schema (#37397) Added schemars annotations to generate inline enums instead of references ($ref) in the JSON schema passed to LLMs. Concerns : - "timezeone" parameter for "now" tool function - "mode" parameter for "edit_file" tool function Should be the same for futures tools/functions enums. This is easier for LLMs to understand the schema since many of them don't use JSON references correctly. Tested with : - local GPT-OSS-120b with llama.cpp server (openai compatible) - remote Claude Sonnet 4.0 with Zed pro subscription Thanks in advance for the merge. (notice this is my first PR ever on Github, I hope I'm doing things well, please let me know if you have any comment - edit: just noticed my username/email were not correctly setup on my local git, sorry, it's been 5 years I've not used git) Closes #37389 Release Notes: - agent: Improve "now" and "edit_file" tool schemas to work with more models. --- crates/agent2/src/tools/edit_file_tool.rs | 1 + crates/agent2/src/tools/now_tool.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index f86bfd25f74556118c827050837ec5beda37d471..ae37dc1f1340f9aa25789930b8f792ed8c3c8356 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -83,6 +83,7 @@ struct EditFileToolPartialInput { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "lowercase")] +#[schemars(inline)] pub enum EditFileMode { Edit, Create, diff --git a/crates/agent2/src/tools/now_tool.rs b/crates/agent2/src/tools/now_tool.rs index 9467e7db68b7338d2f0ccd941aeaa551368eb1e1..49068be0dd91993ae7cc4d866617d399d754d529 100644 --- a/crates/agent2/src/tools/now_tool.rs +++ b/crates/agent2/src/tools/now_tool.rs @@ -11,6 +11,7 @@ use crate::{AgentTool, ToolCallEventStream}; #[derive(Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] +#[schemars(inline)] pub enum Timezone { /// Use UTC for the datetime. Utc, From d0aaf046736312fe3c7555586b54c185c6bc4c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E7=91=97=E6=9D=B0?= <3695888@qq.com> Date: Thu, 4 Sep 2025 13:51:48 +0800 Subject: [PATCH 571/744] Change DeepSeek max token count to 128k (#36864) https://api-docs.deepseek.com/zh-cn/news/news250821 Now the official API supports 128k token content and have modify the name to v3.1/v3.1 thinking Release Notes: - N/A --------- Co-authored-by: Ben Brandt --- crates/deepseek/src/deepseek.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/deepseek/src/deepseek.rs b/crates/deepseek/src/deepseek.rs index e09a9e0f7a19642253245b381abdc9fa05d0af00..64a1cbe5d96354260c2bf84a43ed70be7336aa7a 100644 --- a/crates/deepseek/src/deepseek.rs +++ b/crates/deepseek/src/deepseek.rs @@ -96,7 +96,7 @@ impl Model { pub fn max_token_count(&self) -> u64 { match self { - Self::Chat | Self::Reasoner => 64_000, + Self::Chat | Self::Reasoner => 128_000, Self::Custom { max_tokens, .. } => *max_tokens, } } @@ -104,7 +104,7 @@ impl Model { pub fn max_output_tokens(&self) -> Option { match self { Self::Chat => Some(8_192), - Self::Reasoner => Some(8_192), + Self::Reasoner => Some(64_000), Self::Custom { max_output_tokens, .. } => *max_output_tokens, From 69a5c45672fa599f1a5239a942724231fa36a427 Mon Sep 17 00:00:00 2001 From: James Tucker Date: Wed, 3 Sep 2025 23:18:23 -0700 Subject: [PATCH 572/744] gpui: Fix out-of-bounds node indices in dispatch_path (#37252) Observed in a somewhat regular startup crash on Windows at head (~50% of launches in release mode). Closes #37212 Release Notes: - N/A --- crates/gpui/src/key_dispatch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index 95374e579fa5cc11d84c2ba7e9ec88f261d8d2b2..63cfa680c0d158811a92cddfda390ff82fb7db5c 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -552,7 +552,7 @@ impl DispatchTree { let mut current_node_id = Some(target); while let Some(node_id) = current_node_id { dispatch_path.push(node_id); - current_node_id = self.nodes[node_id.0].parent; + current_node_id = self.nodes.get(node_id.0).and_then(|node| node.parent); } dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node. dispatch_path From aa1629b544c4a6c3cf3aacf12fd7ff05d22b6740 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Thu, 4 Sep 2025 09:09:28 +0200 Subject: [PATCH 573/744] Remove some unused events (#37498) This PR cleans up some emitted events around the codebase. These events are either never emitted or never listened for. It seems better to re-implement these at some point should they again be needed - this ensures that they will actually be fired in the cases where they are needed as opposed to being there and getting unreliable and stale (which is already the case for the majority of the events removed here). Lastly, this ensures the `CapabilitiesChanged` event is not fired too often. Release Notes: - N/A --- .../src/activity_indicator.rs | 15 --------- crates/debugger_ui/src/debugger_panel.rs | 23 +------------ crates/debugger_ui/src/session.rs | 18 ++-------- crates/debugger_ui/src/session/running.rs | 3 -- crates/editor/src/editor.rs | 4 --- crates/editor/src/items.rs | 8 ----- crates/language/src/buffer.rs | 16 +++------ crates/multi_buffer/src/multi_buffer.rs | 33 +++++++------------ crates/workspace/src/item.rs | 5 --- crates/workspace/src/pane.rs | 7 +--- crates/workspace/src/workspace.rs | 3 -- 11 files changed, 21 insertions(+), 114 deletions(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index b65d1472a7552d56ec319e12295088a2973796d5..1f4c10b060aebfaf4931cda1020c3ca8cc9cf79f 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -84,7 +84,6 @@ impl ActivityIndicator { ) -> Entity { let project = workspace.project().clone(); let auto_updater = AutoUpdater::get(cx); - let workspace_handle = cx.entity(); let this = cx.new(|cx| { let mut status_events = languages.language_server_binary_statuses(); cx.spawn(async move |this, cx| { @@ -102,20 +101,6 @@ impl ActivityIndicator { }) .detach(); - cx.subscribe_in( - &workspace_handle, - window, - |activity_indicator, _, event, window, cx| { - if let workspace::Event::ClearActivityIndicator = event - && activity_indicator.statuses.pop().is_some() - { - activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx); - cx.notify(); - } - }, - ) - .detach(); - cx.subscribe( &project.read(cx).lsp_store(), |activity_indicator, _, event, cx| { diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index f81c1fff89a965ae99205d405d1afa52bdde813a..ef714a1f6710f54c5673eac097e7530b3c605b58 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -13,11 +13,8 @@ use anyhow::{Context as _, Result, anyhow}; use collections::IndexMap; use dap::adapters::DebugAdapterName; use dap::debugger_settings::DebugPanelDockPosition; -use dap::{ - ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent, - client::SessionId, debugger_settings::DebuggerSettings, -}; use dap::{DapRegistry, StartDebuggingRequestArguments}; +use dap::{client::SessionId, debugger_settings::DebuggerSettings}; use editor::Editor; use gpui::{ Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId, @@ -46,23 +43,6 @@ use workspace::{ }; use zed_actions::ToggleFocus; -pub enum DebugPanelEvent { - Exited(SessionId), - Terminated(SessionId), - Stopped { - client_id: SessionId, - event: StoppedEvent, - go_to_stack_frame: bool, - }, - Thread((SessionId, ThreadEvent)), - Continued((SessionId, ContinuedEvent)), - Output((SessionId, OutputEvent)), - Module((SessionId, ModuleEvent)), - LoadedSource((SessionId, LoadedSourceEvent)), - ClientShutdown(SessionId), - CapabilitiesChanged(SessionId), -} - pub struct DebugPanel { size: Pixels, active_session: Option>, @@ -1407,7 +1387,6 @@ async fn register_session_inner( } impl EventEmitter for DebugPanel {} -impl EventEmitter for DebugPanel {} impl Focusable for DebugPanel { fn focus_handle(&self, _: &App) -> FocusHandle { diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 0fc003a14dd9ac51c2608df86644a628a44b3e8e..40c9bd810f9c5c9691f51f3d38957a98c9f037a2 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -2,9 +2,7 @@ pub mod running; use crate::{StackTraceView, persistence::SerializedLayout, session::running::DebugTerminal}; use dap::client::SessionId; -use gpui::{ - App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, -}; +use gpui::{App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity}; use project::debugger::session::Session; use project::worktree_store::WorktreeStore; use project::{Project, debugger::session::SessionQuirks}; @@ -24,13 +22,6 @@ pub struct DebugSession { stack_trace_view: OnceCell>, _worktree_store: WeakEntity, workspace: WeakEntity, - _subscriptions: [Subscription; 1], -} - -#[derive(Debug)] -pub enum DebugPanelItemEvent { - Close, - Stopped { go_to_stack_frame: bool }, } impl DebugSession { @@ -59,9 +50,6 @@ impl DebugSession { let quirks = session.read(cx).quirks(); cx.new(|cx| Self { - _subscriptions: [cx.subscribe(&running_state, |_, _, _, cx| { - cx.notify(); - })], remote_id: None, running_state, quirks, @@ -133,7 +121,7 @@ impl DebugSession { } } -impl EventEmitter for DebugSession {} +impl EventEmitter<()> for DebugSession {} impl Focusable for DebugSession { fn focus_handle(&self, cx: &App) -> FocusHandle { @@ -142,7 +130,7 @@ impl Focusable for DebugSession { } impl Item for DebugSession { - type Event = DebugPanelItemEvent; + type Event = (); fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { "Debugger".into() } diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 46e5f35aecb0ad13e55ceb8d2dd12e7ae791a2c5..a18a186469a0aaaf5f3d061830446f5ba27dec72 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -14,7 +14,6 @@ use crate::{ session::running::memory_view::MemoryView, }; -use super::DebugPanelItemEvent; use anyhow::{Context as _, Result, anyhow}; use breakpoint_list::BreakpointList; use collections::{HashMap, IndexMap}; @@ -1826,8 +1825,6 @@ impl RunningState { } } -impl EventEmitter for RunningState {} - impl Focusable for RunningState { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5edc7f3c061efe05bd4112bcbf152695ab56c50d..fe5b2f83c2034822d4f36d3b66bbcea3b6b7322c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -20326,7 +20326,6 @@ impl Editor { multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded | multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged), - multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed), multi_buffer::Event::DiagnosticsUpdated => { self.update_diagnostics_state(window, cx); } @@ -23024,7 +23023,6 @@ pub enum EditorEvent { DirtyChanged, Saved, TitleChanged, - DiffBaseChanged, SelectionsChanged { local: bool, }, @@ -23032,14 +23030,12 @@ pub enum EditorEvent { local: bool, autoscroll: bool, }, - Closed, TransactionUndone { transaction_id: clock::Lamport, }, TransactionBegun { transaction_id: clock::Lamport, }, - Reloaded, CursorShapeChanged, BreadcrumbsChanged, PushedToNavHistory { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b7110190fd8931ed9c1b4ee075b47f89d7f1e992..8a07939cf47529d6a7d94b20bd22d7278b3e9d24 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -775,12 +775,6 @@ impl Item for Editor { self.nav_history = Some(history); } - fn discarded(&self, _project: Entity, _: &mut Window, cx: &mut Context) { - for buffer in self.buffer().clone().read(cx).all_buffers() { - buffer.update(cx, |buffer, cx| buffer.discarded(cx)) - } - } - fn on_removed(&self, cx: &App) { self.report_editor_event(ReportEditorEvent::Closed, None, cx); } @@ -1022,8 +1016,6 @@ impl Item for Editor { fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) { match event { - EditorEvent::Closed => f(ItemEvent::CloseItem), - EditorEvent::Saved | EditorEvent::TitleChanged => { f(ItemEvent::UpdateTab); f(ItemEvent::UpdateBreadcrumbs); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index c978f6c4ef9f60e092d67a655adc6d95693788a8..1f056aacc57338d65705e5b7f4bd91085c6142b4 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -315,10 +315,6 @@ pub enum BufferEvent { DiagnosticsUpdated, /// The buffer gained or lost editing capabilities. CapabilityChanged, - /// The buffer was explicitly requested to close. - Closed, - /// The buffer was discarded when closing. - Discarded, } /// The file associated with a buffer. @@ -1246,8 +1242,10 @@ impl Buffer { /// Assign the buffer a new [`Capability`]. pub fn set_capability(&mut self, capability: Capability, cx: &mut Context) { - self.capability = capability; - cx.emit(BufferEvent::CapabilityChanged) + if self.capability != capability { + self.capability = capability; + cx.emit(BufferEvent::CapabilityChanged) + } } /// This method is called to signal that the buffer has been saved. @@ -1267,12 +1265,6 @@ impl Buffer { cx.notify(); } - /// This method is called to signal that the buffer has been discarded. - pub fn discarded(&self, cx: &mut Context) { - cx.emit(BufferEvent::Discarded); - cx.notify(); - } - /// Reloads the contents of the buffer from disk. pub fn reload(&mut self, cx: &Context) -> oneshot::Receiver> { let (tx, rx) = futures::channel::oneshot::channel(); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 874e58d2a354628958ef160fdae2836b563d3c7b..a2f28215b4655b12095da96c033d23cb3f13eb77 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -113,15 +113,10 @@ pub enum Event { transaction_id: TransactionId, }, Reloaded, - ReloadNeeded, - LanguageChanged(BufferId), - CapabilityChanged, Reparsed(BufferId), Saved, FileHandleChanged, - Closed, - Discarded, DirtyChanged, DiagnosticsUpdated, BufferDiffChanged, @@ -2433,28 +2428,24 @@ impl MultiBuffer { event: &language::BufferEvent, cx: &mut Context, ) { + use language::BufferEvent; cx.emit(match event { - language::BufferEvent::Edited => Event::Edited { + BufferEvent::Edited => Event::Edited { singleton_buffer_edited: true, edited_buffer: Some(buffer), }, - language::BufferEvent::DirtyChanged => Event::DirtyChanged, - language::BufferEvent::Saved => Event::Saved, - language::BufferEvent::FileHandleChanged => Event::FileHandleChanged, - language::BufferEvent::Reloaded => Event::Reloaded, - language::BufferEvent::ReloadNeeded => Event::ReloadNeeded, - language::BufferEvent::LanguageChanged => { - Event::LanguageChanged(buffer.read(cx).remote_id()) - } - language::BufferEvent::Reparsed => Event::Reparsed(buffer.read(cx).remote_id()), - language::BufferEvent::DiagnosticsUpdated => Event::DiagnosticsUpdated, - language::BufferEvent::Closed => Event::Closed, - language::BufferEvent::Discarded => Event::Discarded, - language::BufferEvent::CapabilityChanged => { + BufferEvent::DirtyChanged => Event::DirtyChanged, + BufferEvent::Saved => Event::Saved, + BufferEvent::FileHandleChanged => Event::FileHandleChanged, + BufferEvent::Reloaded => Event::Reloaded, + BufferEvent::LanguageChanged => Event::LanguageChanged(buffer.read(cx).remote_id()), + BufferEvent::Reparsed => Event::Reparsed(buffer.read(cx).remote_id()), + BufferEvent::DiagnosticsUpdated => Event::DiagnosticsUpdated, + BufferEvent::CapabilityChanged => { self.capability = buffer.read(cx).capability(); - Event::CapabilityChanged + return; } - language::BufferEvent::Operation { .. } => return, + BufferEvent::Operation { .. } | BufferEvent::ReloadNeeded => return, }); } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 731e1691479ad7eec1388c174d85081a177127cc..f37be0f154f736b021b0fcf5f29cf26074e3299f 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -541,7 +541,6 @@ pub trait ItemHandle: 'static + Send { cx: &mut Context, ); fn deactivated(&self, window: &mut Window, cx: &mut App); - fn discarded(&self, project: Entity, window: &mut Window, cx: &mut App); fn on_removed(&self, cx: &App); fn workspace_deactivated(&self, window: &mut Window, cx: &mut App); fn navigate(&self, data: Box, window: &mut Window, cx: &mut App) -> bool; @@ -975,10 +974,6 @@ impl ItemHandle for Entity { }); } - fn discarded(&self, project: Entity, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.discarded(project, window, cx)); - } - fn deactivated(&self, window: &mut Window, cx: &mut App) { self.update(cx, |this, cx| this.deactivated(window, cx)); } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index fe8014d9f7b8ba8a85d4a9c97f0d99ff2dc669eb..b3b16acd4fea54a63dc6398c70c52e93ec780023 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -254,9 +254,6 @@ pub enum Event { Remove { focus_on_pane: Option>, }, - RemoveItem { - idx: usize, - }, RemovedItem { item: Box, }, @@ -287,7 +284,6 @@ impl fmt::Debug for Event { .field("local", local) .finish(), Event::Remove { .. } => f.write_str("Remove"), - Event::RemoveItem { idx } => f.debug_struct("RemoveItem").field("idx", idx).finish(), Event::RemovedItem { item } => f .debug_struct("RemovedItem") .field("item", &item.item_id()) @@ -2096,11 +2092,10 @@ impl Pane { Ok(0) => {} Ok(1) => { // Don't save this file - pane.update_in(cx, |pane, window, cx| { + pane.update_in(cx, |pane, _, cx| { if pane.is_tab_pinned(item_ix) && !item.can_save(cx) { pane.pinned_tab_count -= 1; } - item.discarded(project, window, cx) }) .log_err(); return Ok(true); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index bd19f37c1e0fd8653f5d73dea365f1148fd2e91d..af86517bb452c1cea77a72f2cf2350ef1e2eb030 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1030,7 +1030,6 @@ pub enum Event { ItemAdded { item: Box, }, - ItemRemoved, ActiveItemChanged, UserSavedItem { pane: WeakEntity, @@ -1046,7 +1045,6 @@ pub enum Event { }, ZoomChanged, ModalOpened, - ClearActivityIndicator, } #[derive(Debug)] @@ -3939,7 +3937,6 @@ impl Workspace { } serialize_workspace = false; } - pane::Event::RemoveItem { .. } => {} pane::Event::RemovedItem { item } => { cx.emit(Event::ActiveItemChanged); self.update_window_edited(window, cx); From b7ad20773c41cccbf5d761d2b7b58c4eb2197c0f Mon Sep 17 00:00:00 2001 From: "Mitch (a.k.a Voz)" Date: Thu, 4 Sep 2025 03:25:47 -0500 Subject: [PATCH 574/744] worktree: Create parent directories on rename (#37437) Closes https://github.com/zed-industries/zed/issues/37357 Release Notes: - Allow creating sub-directories when renaming a file in file finder --------- Co-authored-by: Kirill Bulatov --- crates/worktree/src/worktree.rs | 59 ++++++++++++----- crates/worktree/src/worktree_tests.rs | 95 +++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 18 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 711c99ce28bbbc557a293d8b644ac6594f31ad7f..7af86d3364f6e07116ae701da83b897869bb905e 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1775,36 +1775,54 @@ impl LocalWorktree { }; absolutize_path }; - let abs_path = abs_new_path.clone(); + let fs = self.fs.clone(); + let abs_path = abs_new_path.clone(); let case_sensitive = self.fs_case_sensitive; - let rename = cx.background_spawn(async move { - let abs_old_path = abs_old_path?; - let abs_new_path = abs_new_path; - - let abs_old_path_lower = abs_old_path.to_str().map(|p| p.to_lowercase()); - let abs_new_path_lower = abs_new_path.to_str().map(|p| p.to_lowercase()); - - // If we're on a case-insensitive FS and we're doing a case-only rename (i.e. `foobar` to `FOOBAR`) - // we want to overwrite, because otherwise we run into a file-already-exists error. - let overwrite = !case_sensitive - && abs_old_path != abs_new_path - && abs_old_path_lower == abs_new_path_lower; + let do_rename = async move |fs: &dyn Fs, old_path: &Path, new_path: &Path, overwrite| { fs.rename( - &abs_old_path, - &abs_new_path, + &old_path, + &new_path, fs::RenameOptions { overwrite, - ..Default::default() + ..fs::RenameOptions::default() }, ) .await - .with_context(|| format!("Renaming {abs_old_path:?} into {abs_new_path:?}")) + .with_context(|| format!("renaming {old_path:?} into {new_path:?}")) + }; + + let rename_task = cx.background_spawn(async move { + let abs_old_path = abs_old_path?; + + // If we're on a case-insensitive FS and we're doing a case-only rename (i.e. `foobar` to `FOOBAR`) + // we want to overwrite, because otherwise we run into a file-already-exists error. + let overwrite = !case_sensitive + && abs_old_path != abs_new_path + && abs_old_path.to_str().map(|p| p.to_lowercase()) + == abs_new_path.to_str().map(|p| p.to_lowercase()); + + // The directory we're renaming into might not exist yet + if let Err(e) = do_rename(fs.as_ref(), &abs_old_path, &abs_new_path, overwrite).await { + if let Some(err) = e.downcast_ref::() + && err.kind() == std::io::ErrorKind::NotFound + { + if let Some(parent) = abs_new_path.parent() { + fs.create_dir(parent) + .await + .with_context(|| format!("creating parent directory {parent:?}"))?; + return do_rename(fs.as_ref(), &abs_old_path, &abs_new_path, overwrite) + .await; + } + } + return Err(e); + } + Ok(()) }); cx.spawn(async move |this, cx| { - rename.await?; + rename_task.await?; Ok(this .update(cx, |this, cx| { let local = this.as_local_mut().unwrap(); @@ -1818,6 +1836,11 @@ impl LocalWorktree { ); Task::ready(Ok(this.root_entry().cloned())) } else { + // First refresh the parent directory (in case it was newly created) + if let Some(parent) = new_path.parent() { + let _ = local.refresh_entries_for_paths(vec![parent.into()]); + } + // Then refresh the new path local.refresh_entry(new_path.clone(), Some(old_path), cx) } })? diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index c46e14f077e6f4f527b1fcc616a6560cf9654b18..1783ba317c9927bb79ebdb91b1f57f13d200b60f 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -1924,6 +1924,101 @@ fn random_filename(rng: &mut impl Rng) -> String { .collect() } +#[gpui::test] +async fn test_rename_file_to_new_directory(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + let expected_contents = "content"; + fs.as_fake() + .insert_tree( + "/root", + json!({ + "test.txt": expected_contents + }), + ) + .await; + let worktree = Worktree::local( + Path::new("/root"), + true, + fs.clone(), + Arc::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete()) + .await; + + let entry_id = worktree.read_with(cx, |worktree, _| { + worktree.entry_for_path("test.txt").unwrap().id + }); + let _result = worktree + .update(cx, |worktree, cx| { + worktree.rename_entry(entry_id, Path::new("dir1/dir2/dir3/test.txt"), cx) + }) + .await + .unwrap(); + worktree.read_with(cx, |worktree, _| { + assert!( + worktree.entry_for_path("test.txt").is_none(), + "Old file should have been removed" + ); + assert!( + worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_some(), + "Whole directory hierarchy and the new file should have been created" + ); + }); + assert_eq!( + worktree + .update(cx, |worktree, cx| { + worktree.load_file("dir1/dir2/dir3/test.txt".as_ref(), cx) + }) + .await + .unwrap() + .text, + expected_contents, + "Moved file's contents should be preserved" + ); + + let entry_id = worktree.read_with(cx, |worktree, _| { + worktree + .entry_for_path("dir1/dir2/dir3/test.txt") + .unwrap() + .id + }); + let _result = worktree + .update(cx, |worktree, cx| { + worktree.rename_entry(entry_id, Path::new("dir1/dir2/test.txt"), cx) + }) + .await + .unwrap(); + worktree.read_with(cx, |worktree, _| { + assert!( + worktree.entry_for_path("test.txt").is_none(), + "First file should not reappear" + ); + assert!( + worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_none(), + "Old file should have been removed" + ); + assert!( + worktree.entry_for_path("dir1/dir2/test.txt").is_some(), + "No error should have occurred after moving into existing directory" + ); + }); + assert_eq!( + worktree + .update(cx, |worktree, cx| { + worktree.load_file("dir1/dir2/test.txt".as_ref(), cx) + }) + .await + .unwrap() + .text, + expected_contents, + "Moved file's contents should be preserved" + ); +} + #[gpui::test] async fn test_private_single_file_worktree(cx: &mut TestAppContext) { init_test(cx); From fca44f89c11070fad20bc365bd9e8ab484be1f21 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 4 Sep 2025 11:22:19 +0200 Subject: [PATCH 575/744] languages: Allow installing pre-release of rust-analyzer and clangd (#37530) Release Notes: - Added lsp binary config to allow fetching nightly rust-analyzer and clangd releases --- crates/editor/src/editor_tests.rs | 4 +++ crates/language/src/language.rs | 4 ++- .../src/extension_lsp_adapter.rs | 1 + crates/languages/src/c.rs | 17 +++++++++-- crates/languages/src/css.rs | 1 + crates/languages/src/go.rs | 1 + crates/languages/src/json.rs | 2 ++ crates/languages/src/python.rs | 3 ++ crates/languages/src/rust.rs | 7 ++++- crates/languages/src/tailwind.rs | 1 + crates/languages/src/typescript.rs | 2 ++ crates/languages/src/vtsls.rs | 1 + crates/languages/src/yaml.rs | 1 + crates/project/src/lsp_store.rs | 1 + crates/project/src/project_settings.rs | 8 ++++++ docs/src/languages/cpp.md | 28 +++++++++++++++---- docs/src/languages/rust.md | 16 ++++++++++- 17 files changed, 87 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 5efa3908256531e40845096187d44353ae140bbc..90e488368f99ea50bdcbfc671a359fa5e899f59e 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -16783,6 +16783,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon "some other init value": false })), enable_lsp_tasks: false, + fetch: None, }, ); }); @@ -16803,6 +16804,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon "anotherInitValue": false })), enable_lsp_tasks: false, + fetch: None, }, ); }); @@ -16823,6 +16825,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon "anotherInitValue": false })), enable_lsp_tasks: false, + fetch: None, }, ); }); @@ -16841,6 +16844,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon settings: None, initialization_options: None, enable_lsp_tasks: false, + fetch: None, }, ); }); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 0606ae3de9be5800401787a852bafd5cfd9051be..e4a1510d7df128158691842206a27844304b3237 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -395,6 +395,7 @@ pub trait LspAdapter: 'static + Send + Sync { async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, + cx: &AsyncApp, ) -> Result>; fn will_fetch_server( @@ -605,7 +606,7 @@ async fn try_fetch_server_binary delegate.update_status(name.clone(), BinaryStatus::CheckingForUpdate); let latest_version = adapter - .fetch_latest_server_version(delegate.as_ref()) + .fetch_latest_server_version(delegate.as_ref(), cx) .await?; if let Some(binary) = adapter @@ -2222,6 +2223,7 @@ impl LspAdapter for FakeLspAdapter { async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { unreachable!(); } diff --git a/crates/language_extension/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs index e465a8dd0a0404e76b19942dd15bf19c4f204fdc..9b6e467f2f2dfddccaa96d7aaf5d5550d72fe904 100644 --- a/crates/language_extension/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -204,6 +204,7 @@ impl LspAdapter for ExtensionLspAdapter { async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { unreachable!("get_language_server_command is overridden") } diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index 2820f55a497c8b77b679a3ab5368cae6901c89e6..afdf49e66e59b78c82f234160a9c4bc1efa83574 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -5,8 +5,9 @@ use gpui::{App, AsyncApp}; use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release}; pub use language::*; use lsp::{InitializeParams, LanguageServerBinary, LanguageServerName}; -use project::lsp_store::clangd_ext; +use project::{lsp_store::clangd_ext, project_settings::ProjectSettings}; use serde_json::json; +use settings::Settings as _; use smol::fs; use std::{any::Any, env::consts, path::PathBuf, sync::Arc}; use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into}; @@ -42,9 +43,19 @@ impl super::LspAdapter for CLspAdapter { async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, + cx: &AsyncApp, ) -> Result> { - let release = - latest_github_release("clangd/clangd", true, false, delegate.http_client()).await?; + let release = latest_github_release( + "clangd/clangd", + true, + ProjectSettings::try_read_global(cx, |s| { + s.lsp.get(&Self::SERVER_NAME)?.fetch.as_ref()?.pre_release + }) + .flatten() + .unwrap_or(false), + delegate.http_client(), + ) + .await?; let os_suffix = match consts::OS { "macos" => "mac", "linux" => "linux", diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index 2480d4026883ab5b6e3af7f8a4d8bbbb59757879..5cea35084d54f86c6a2b47385bce628b73aa29c7 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -61,6 +61,7 @@ impl LspAdapter for CssLspAdapter { async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { Ok(Box::new( self.node diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 86f8e1faaa969449f45f38ee5cf8e8cde9ccff29..8c116c899f6456d1d3a12a741ca391a2cbaec41d 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -59,6 +59,7 @@ impl super::LspAdapter for GoLspAdapter { async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { let release = latest_github_release("golang/tools", false, false, delegate.http_client()).await?; diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 4fcf865568b11ea0f8b40d6a7d57cc354e85a680..a33f5c9836f621a59b45aba7a249f7b1c2d1489d 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -321,6 +321,7 @@ impl LspAdapter for JsonLspAdapter { async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { Ok(Box::new( self.node @@ -494,6 +495,7 @@ impl LspAdapter for NodeVersionAdapter { async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { let release = latest_github_release( "zed-industries/package-version-server", diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index bd3a7b34cf873328e480012ca96fcbe9fc3eff95..5e6f5e414f001209d3b4447ae8326a12953c45ac 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -157,6 +157,7 @@ impl LspAdapter for PythonLspAdapter { async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { Ok(Box::new( self.node @@ -1111,6 +1112,7 @@ impl LspAdapter for PyLspAdapter { async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { Ok(Box::new(()) as Box<_>) } @@ -1422,6 +1424,7 @@ impl LspAdapter for BasedPyrightLspAdapter { async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { Ok(Box::new(()) as Box<_>) } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index a5acc0043298cab49264dac75d51e6a69e5149fe..3d5ff1cd06149594e54de03d454bef64483b7e47 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -147,11 +147,16 @@ impl LspAdapter for RustLspAdapter { async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, + cx: &AsyncApp, ) -> Result> { let release = latest_github_release( "rust-lang/rust-analyzer", true, - false, + ProjectSettings::try_read_global(cx, |s| { + s.lsp.get(&SERVER_NAME)?.fetch.as_ref()?.pre_release + }) + .flatten() + .unwrap_or(false), delegate.http_client(), ) .await?; diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 7215dc0d591daae93ca0ee37043bc1372fa32cd2..af7653ea9e3059a40e0023e5d5d2ccd3c8b02556 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -66,6 +66,7 @@ impl LspAdapter for TailwindLspAdapter { async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { Ok(Box::new( self.node diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 77cf1a64f1f5586e727ca2b7ccc55dbe8a6183cf..15adea6070ada89e21de7d7d347f183c1aa010ab 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -563,6 +563,7 @@ impl LspAdapter for TypeScriptLspAdapter { async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { Ok(Box::new(TypeScriptVersions { typescript_version: self.node.npm_package_latest_version("typescript").await?, @@ -885,6 +886,7 @@ impl LspAdapter for EsLintLspAdapter { async fn fetch_latest_server_version( &self, _delegate: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { let url = build_asset_url( "zed-industries/vscode-eslint", diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index f7152b0b5df8e2249c43b1f2ffa2b490bf001961..1cf3c8aa52db7bbfc1e784afecc62972884a3d47 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -73,6 +73,7 @@ impl LspAdapter for VtslsLspAdapter { async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { Ok(Box::new(TypeScriptVersions { typescript_version: self.node.npm_package_latest_version("typescript").await?, diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index b9197b12ae85af5ee2a9aa2b5807a4f0410a1dda..bf634aafbab9fb3312f63fa818a55ddae90a05f3 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -44,6 +44,7 @@ impl LspAdapter for YamlLspAdapter { async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { Ok(Box::new( self.node diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 6e06c2dd95fa9bf8d3b1a0670fb119a0ef552de1..36ec338fb71ca1a130657dca1db037051691ad9d 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -13070,6 +13070,7 @@ impl LspAdapter for SshLspAdapter { async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, + _: &AsyncApp, ) -> Result> { anyhow::bail!("SshLspAdapter does not support fetch_latest_server_version") } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 40874638111eb3b85d4f65ad0b531072ab082624..c98065116e00fd6c643a2c809cf6e8fb1c51532b 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -516,6 +516,12 @@ pub struct BinarySettings { pub ignore_system_version: Option, } +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] +pub struct FetchSettings { + // Whether to consider pre-releases for fetching + pub pre_release: Option, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] #[serde(rename_all = "snake_case")] pub struct LspSettings { @@ -527,6 +533,7 @@ pub struct LspSettings { /// Default: true #[serde(default = "default_true")] pub enable_lsp_tasks: bool, + pub fetch: Option, } impl Default for LspSettings { @@ -536,6 +543,7 @@ impl Default for LspSettings { initialization_options: None, settings: None, enable_lsp_tasks: true, + fetch: None, } } } diff --git a/docs/src/languages/cpp.md b/docs/src/languages/cpp.md index e84bb6ea507f264240a40e986f41c5cd3a23610d..91395b5a94ca3e84699edcb731209ca9e260753c 100644 --- a/docs/src/languages/cpp.md +++ b/docs/src/languages/cpp.md @@ -9,22 +9,23 @@ C++ support is available natively in Zed. You can configure which `clangd` binary Zed should use. -To use a binary in a custom location, add the following to your `settings.json`: +By default, Zed will try to find a `clangd` in your `$PATH` and try to use that. If that binary successfully executes, it's used. Otherwise, Zed will fall back to installing its own `clangd` version and use that. + +If you want to install a pre-release `clangd` version instead you can instruct Zed to do so by setting `pre_release` to `true` in your `settings.json`: ```json { "lsp": { "clangd": { - "binary": { - "path": "/path/to/clangd", - "arguments": [] + "fetch": { + "pre_release": true } } } } ``` -If you want to disable Zed looking for a `clangd` binary, you can set `ignore_system_version` to `true`: +If you want to disable Zed looking for a `clangd` binary, you can set `ignore_system_version` to `true` in your `settings.json`: ```json { @@ -38,6 +39,23 @@ If you want to disable Zed looking for a `clangd` binary, you can set `ignore_sy } ``` +If you want to use a binary in a custom location, you can specify a `path` and optional `arguments`: + +```json +{ + "lsp": { + "cangd": { + "binary": { + "path": "/path/to/clangd", + "arguments": [] + } + } + } +} +``` + +This `"path"` has to be an absolute path. + ## Arguments You can pass any number of arguments to clangd. To see a full set of available options, run `clangd --help` from the command line. For example with `--function-arg-placeholders=0` completions contain only parentheses for function calls, while the default (`--function-arg-placeholders=1`) completions also contain placeholders for method parameters. diff --git a/docs/src/languages/rust.md b/docs/src/languages/rust.md index 0bfa3ecac75f1371b38e1609090315841ea97a4c..c8dd1ac550150573a6e476b75b1cee4645a49619 100644 --- a/docs/src/languages/rust.md +++ b/docs/src/languages/rust.md @@ -63,7 +63,21 @@ A `true` setting will set the target directory to `target/rust-analyzer`. You ca You can configure which `rust-analyzer` binary Zed should use. -By default, Zed will try to find a `rust-analyzer` in your `$PATH` and try to use that. If that binary successfully executes `rust-analyzer --help`, it's used. Otherwise, Zed will fall back to installing its own `rust-analyzer` version and using that. +By default, Zed will try to find a `rust-analyzer` in your `$PATH` and try to use that. If that binary successfully executes `rust-analyzer --help`, it's used. Otherwise, Zed will fall back to installing its own stable `rust-analyzer` version and use that. + +If you want to install pre-release `rust-analyzer` version instead you can instruct Zed to do so by setting `pre_release` to `true` in your `settings.json`: + +```json +{ + "lsp": { + "rust-analyzer": { + "fetch": { + "pre_release": true + } + } + } +} +``` If you want to disable Zed looking for a `rust-analyzer` binary, you can set `ignore_system_version` to `true` in your `settings.json`: From 28c78d2d85c81d7ea9a0e3dbab5db8a8bf0e9f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Thu, 4 Sep 2025 21:31:12 +0800 Subject: [PATCH 576/744] windows: Keep just one copy of GPU instance (#37445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now we only keep a single copy of the GPU device. The GPU lost handling got broken after #35376, but it’s properly handled again now. Release Notes: - N/A --- crates/gpui/src/platform/windows.rs | 2 + .../gpui/src/platform/windows/direct_write.rs | 34 ++- .../src/platform/windows/directx_devices.rs | 180 ++++++++++++ .../src/platform/windows/directx_renderer.rs | 268 +++++------------- crates/gpui/src/platform/windows/events.rs | 29 +- crates/gpui/src/platform/windows/platform.rs | 139 ++++++++- crates/gpui/src/platform/windows/window.rs | 9 +- 7 files changed, 429 insertions(+), 232 deletions(-) create mode 100644 crates/gpui/src/platform/windows/directx_devices.rs diff --git a/crates/gpui/src/platform/windows.rs b/crates/gpui/src/platform/windows.rs index 77e0ca41bf8b394dc8bdd75e521aab3ba63dce2c..9cd1a7d05f4bcc6aa097db5dad64bdbc502575fc 100644 --- a/crates/gpui/src/platform/windows.rs +++ b/crates/gpui/src/platform/windows.rs @@ -2,6 +2,7 @@ mod clipboard; mod destination_list; mod direct_write; mod directx_atlas; +mod directx_devices; mod directx_renderer; mod dispatcher; mod display; @@ -18,6 +19,7 @@ pub(crate) use clipboard::*; pub(crate) use destination_list::*; pub(crate) use direct_write::*; pub(crate) use directx_atlas::*; +pub(crate) use directx_devices::*; pub(crate) use directx_renderer::*; pub(crate) use dispatcher::*; pub(crate) use display::*; diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index ec6d008e7b9614c146056da7adba2607ea9be909..df3161bf079a8eb0cb04908e586f5d344519821e 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, sync::Arc}; use ::util::ResultExt; -use anyhow::Result; +use anyhow::{Context, Result}; use collections::HashMap; use itertools::Itertools; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; @@ -70,7 +70,7 @@ struct FontIdentifier { } impl DirectWriteComponent { - pub fn new(gpu_context: &DirectXDevices) -> Result { + pub fn new(directx_devices: &DirectXDevices) -> Result { // todo: ideally this would not be a large unsafe block but smaller isolated ones for easier auditing unsafe { let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)?; @@ -85,7 +85,7 @@ impl DirectWriteComponent { let locale = String::from_utf16_lossy(&locale_vec); let text_renderer = Arc::new(TextRendererWrapper::new(&locale)); - let gpu_state = GPUState::new(gpu_context)?; + let gpu_state = GPUState::new(directx_devices)?; Ok(DirectWriteComponent { locale, @@ -100,9 +100,9 @@ impl DirectWriteComponent { } impl GPUState { - fn new(gpu_context: &DirectXDevices) -> Result { - let device = gpu_context.device.clone(); - let device_context = gpu_context.device_context.clone(); + fn new(directx_devices: &DirectXDevices) -> Result { + let device = directx_devices.device.clone(); + let device_context = directx_devices.device_context.clone(); let blend_state = { let mut blend_state = None; @@ -183,8 +183,8 @@ impl GPUState { } impl DirectWriteTextSystem { - pub(crate) fn new(gpu_context: &DirectXDevices) -> Result { - let components = DirectWriteComponent::new(gpu_context)?; + pub(crate) fn new(directx_devices: &DirectXDevices) -> Result { + let components = DirectWriteComponent::new(directx_devices)?; let system_font_collection = unsafe { let mut result = std::mem::zeroed(); components @@ -210,6 +210,10 @@ impl DirectWriteTextSystem { font_id_by_identifier: HashMap::default(), }))) } + + pub(crate) fn handle_gpu_lost(&self, directx_devices: &DirectXDevices) { + self.0.write().handle_gpu_lost(directx_devices); + } } impl PlatformTextSystem for DirectWriteTextSystem { @@ -1211,6 +1215,20 @@ impl DirectWriteState { )); result } + + fn handle_gpu_lost(&mut self, directx_devices: &DirectXDevices) { + try_to_recover_from_device_lost( + || GPUState::new(directx_devices).context("Recreating GPU state for DirectWrite"), + |gpu_state| self.components.gpu_state = gpu_state, + || { + log::error!( + "Failed to recreate GPU state for DirectWrite after multiple attempts." + ); + // Do something here? + // At this point, the device loss is considered unrecoverable. + }, + ); + } } impl Drop for DirectWriteState { diff --git a/crates/gpui/src/platform/windows/directx_devices.rs b/crates/gpui/src/platform/windows/directx_devices.rs new file mode 100644 index 0000000000000000000000000000000000000000..005737ca2070ab8a30656493b548c6f3c6e9a3dc --- /dev/null +++ b/crates/gpui/src/platform/windows/directx_devices.rs @@ -0,0 +1,180 @@ +use anyhow::{Context, Result}; +use util::ResultExt; +use windows::Win32::{ + Foundation::HMODULE, + Graphics::{ + Direct3D::{ + D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL, D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_11_1, + }, + Direct3D11::{ + D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_DEBUG, D3D11_SDK_VERSION, + D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, + }, + Dxgi::{ + CreateDXGIFactory2, DXGI_CREATE_FACTORY_DEBUG, DXGI_CREATE_FACTORY_FLAGS, + DXGI_GPU_PREFERENCE_MINIMUM_POWER, IDXGIAdapter1, IDXGIFactory6, + }, + }, +}; + +pub(crate) fn try_to_recover_from_device_lost( + mut f: impl FnMut() -> Result, + on_success: impl FnOnce(T), + on_error: impl FnOnce(), +) { + let result = (0..5).find_map(|i| { + if i > 0 { + // Add a small delay before retrying + std::thread::sleep(std::time::Duration::from_millis(100)); + } + f().log_err() + }); + + if let Some(result) = result { + on_success(result); + } else { + on_error(); + } +} + +#[derive(Clone)] +pub(crate) struct DirectXDevices { + pub(crate) adapter: IDXGIAdapter1, + pub(crate) dxgi_factory: IDXGIFactory6, + pub(crate) device: ID3D11Device, + pub(crate) device_context: ID3D11DeviceContext, +} + +impl DirectXDevices { + pub(crate) fn new() -> Result { + let debug_layer_available = check_debug_layer_available(); + let dxgi_factory = + get_dxgi_factory(debug_layer_available).context("Creating DXGI factory")?; + let adapter = + get_adapter(&dxgi_factory, debug_layer_available).context("Getting DXGI adapter")?; + let (device, device_context) = { + let mut device: Option = None; + let mut context: Option = None; + let mut feature_level = D3D_FEATURE_LEVEL::default(); + get_device( + &adapter, + Some(&mut device), + Some(&mut context), + Some(&mut feature_level), + debug_layer_available, + ) + .context("Creating Direct3D device")?; + match feature_level { + D3D_FEATURE_LEVEL_11_1 => { + log::info!("Created device with Direct3D 11.1 feature level.") + } + D3D_FEATURE_LEVEL_11_0 => { + log::info!("Created device with Direct3D 11.0 feature level.") + } + D3D_FEATURE_LEVEL_10_1 => { + log::info!("Created device with Direct3D 10.1 feature level.") + } + _ => unreachable!(), + } + (device.unwrap(), context.unwrap()) + }; + + Ok(Self { + adapter, + dxgi_factory, + device, + device_context, + }) + } +} + +#[inline] +fn check_debug_layer_available() -> bool { + #[cfg(debug_assertions)] + { + use windows::Win32::Graphics::Dxgi::{DXGIGetDebugInterface1, IDXGIInfoQueue}; + + unsafe { DXGIGetDebugInterface1::(0) } + .log_err() + .is_some() + } + #[cfg(not(debug_assertions))] + { + false + } +} + +#[inline] +fn get_dxgi_factory(debug_layer_available: bool) -> Result { + let factory_flag = if debug_layer_available { + DXGI_CREATE_FACTORY_DEBUG + } else { + #[cfg(debug_assertions)] + log::warn!( + "Failed to get DXGI debug interface. DirectX debugging features will be disabled." + ); + DXGI_CREATE_FACTORY_FLAGS::default() + }; + unsafe { Ok(CreateDXGIFactory2(factory_flag)?) } +} + +#[inline] +fn get_adapter(dxgi_factory: &IDXGIFactory6, debug_layer_available: bool) -> Result { + for adapter_index in 0.. { + let adapter: IDXGIAdapter1 = unsafe { + dxgi_factory + .EnumAdapterByGpuPreference(adapter_index, DXGI_GPU_PREFERENCE_MINIMUM_POWER) + }?; + if let Ok(desc) = unsafe { adapter.GetDesc1() } { + let gpu_name = String::from_utf16_lossy(&desc.Description) + .trim_matches(char::from(0)) + .to_string(); + log::info!("Using GPU: {}", gpu_name); + } + // Check to see whether the adapter supports Direct3D 11, but don't + // create the actual device yet. + if get_device(&adapter, None, None, None, debug_layer_available) + .log_err() + .is_some() + { + return Ok(adapter); + } + } + + unreachable!() +} + +#[inline] +fn get_device( + adapter: &IDXGIAdapter1, + device: Option<*mut Option>, + context: Option<*mut Option>, + feature_level: Option<*mut D3D_FEATURE_LEVEL>, + debug_layer_available: bool, +) -> Result<()> { + let device_flags = if debug_layer_available { + D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_DEBUG + } else { + D3D11_CREATE_DEVICE_BGRA_SUPPORT + }; + unsafe { + D3D11CreateDevice( + adapter, + D3D_DRIVER_TYPE_UNKNOWN, + HMODULE::default(), + device_flags, + // 4x MSAA is required for Direct3D Feature Level 10.1 or better + Some(&[ + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + ]), + D3D11_SDK_VERSION, + device, + feature_level, + context, + )?; + } + Ok(()) +} diff --git a/crates/gpui/src/platform/windows/directx_renderer.rs b/crates/gpui/src/platform/windows/directx_renderer.rs index 0c092e22283d29ba1b522012a51f6cab77f51865..2baa237cdaa196da225070c241232fc6af0f0ff4 100644 --- a/crates/gpui/src/platform/windows/directx_renderer.rs +++ b/crates/gpui/src/platform/windows/directx_renderer.rs @@ -7,7 +7,7 @@ use ::util::ResultExt; use anyhow::{Context, Result}; use windows::{ Win32::{ - Foundation::{HMODULE, HWND}, + Foundation::HWND, Graphics::{ Direct3D::*, Direct3D11::*, @@ -39,7 +39,7 @@ pub(crate) struct FontInfo { pub(crate) struct DirectXRenderer { hwnd: HWND, atlas: Arc, - devices: ManuallyDrop, + devices: ManuallyDrop, resources: ManuallyDrop, globals: DirectXGlobalElements, pipelines: DirectXRenderPipelines, @@ -49,9 +49,9 @@ pub(crate) struct DirectXRenderer { /// Direct3D objects #[derive(Clone)] -pub(crate) struct DirectXDevices { - adapter: IDXGIAdapter1, - dxgi_factory: IDXGIFactory6, +pub(crate) struct DirectXRendererDevices { + pub(crate) adapter: IDXGIAdapter1, + pub(crate) dxgi_factory: IDXGIFactory6, pub(crate) device: ID3D11Device, pub(crate) device_context: ID3D11DeviceContext, dxgi_device: Option, @@ -96,39 +96,17 @@ struct DirectComposition { comp_visual: IDCompositionVisual, } -impl DirectXDevices { - pub(crate) fn new(disable_direct_composition: bool) -> Result> { - let debug_layer_available = check_debug_layer_available(); - let dxgi_factory = - get_dxgi_factory(debug_layer_available).context("Creating DXGI factory")?; - let adapter = - get_adapter(&dxgi_factory, debug_layer_available).context("Getting DXGI adapter")?; - let (device, device_context) = { - let mut device: Option = None; - let mut context: Option = None; - let mut feature_level = D3D_FEATURE_LEVEL::default(); - get_device( - &adapter, - Some(&mut device), - Some(&mut context), - Some(&mut feature_level), - debug_layer_available, - ) - .context("Creating Direct3D device")?; - match feature_level { - D3D_FEATURE_LEVEL_11_1 => { - log::info!("Created device with Direct3D 11.1 feature level.") - } - D3D_FEATURE_LEVEL_11_0 => { - log::info!("Created device with Direct3D 11.0 feature level.") - } - D3D_FEATURE_LEVEL_10_1 => { - log::info!("Created device with Direct3D 10.1 feature level.") - } - _ => unreachable!(), - } - (device.unwrap(), context.unwrap()) - }; +impl DirectXRendererDevices { + pub(crate) fn new( + directx_devices: &DirectXDevices, + disable_direct_composition: bool, + ) -> Result> { + let DirectXDevices { + adapter, + dxgi_factory, + device, + device_context, + } = directx_devices; let dxgi_device = if disable_direct_composition { None } else { @@ -136,23 +114,27 @@ impl DirectXDevices { }; Ok(ManuallyDrop::new(Self { - adapter, - dxgi_factory, + adapter: adapter.clone(), + dxgi_factory: dxgi_factory.clone(), + device: device.clone(), + device_context: device_context.clone(), dxgi_device, - device, - device_context, })) } } impl DirectXRenderer { - pub(crate) fn new(hwnd: HWND, disable_direct_composition: bool) -> Result { + pub(crate) fn new( + hwnd: HWND, + directx_devices: &DirectXDevices, + disable_direct_composition: bool, + ) -> Result { if disable_direct_composition { log::info!("Direct Composition is disabled."); } - let devices = - DirectXDevices::new(disable_direct_composition).context("Creating DirectX devices")?; + let devices = DirectXRendererDevices::new(directx_devices, disable_direct_composition) + .context("Creating DirectX devices")?; let atlas = Arc::new(DirectXAtlas::new(&devices.device, &devices.device_context)); let resources = DirectXResources::new(&devices, 1, 1, hwnd, disable_direct_composition) @@ -218,28 +200,30 @@ impl DirectXRenderer { Ok(()) } + #[inline] fn present(&mut self) -> Result<()> { - unsafe { - let result = self.resources.swap_chain.Present(0, DXGI_PRESENT(0)); - // Presenting the swap chain can fail if the DirectX device was removed or reset. - if result == DXGI_ERROR_DEVICE_REMOVED || result == DXGI_ERROR_DEVICE_RESET { - let reason = self.devices.device.GetDeviceRemovedReason(); + let result = unsafe { self.resources.swap_chain.Present(0, DXGI_PRESENT(0)) }; + result.ok().context("Presenting swap chain failed") + } + + pub(crate) fn handle_device_lost(&mut self, directx_devices: &DirectXDevices) { + try_to_recover_from_device_lost( + || { + self.handle_device_lost_impl(directx_devices) + .context("DirectXRenderer handling device lost") + }, + |_| {}, + || { log::error!( - "DirectX device removed or reset when drawing. Reason: {:?}", - reason + "DirectXRenderer failed to recover from device lost after multiple attempts" ); - self.handle_device_lost()?; - } else { - result.ok()?; - } - } - Ok(()) + // Do something here? + // At this point, the device loss is considered unrecoverable. + }, + ); } - fn handle_device_lost(&mut self) -> Result<()> { - // Here we wait a bit to ensure the the system has time to recover from the device lost state. - // If we don't wait, the final drawing result will be blank. - std::thread::sleep(std::time::Duration::from_millis(300)); + fn handle_device_lost_impl(&mut self, directx_devices: &DirectXDevices) -> Result<()> { let disable_direct_composition = self.direct_composition.is_none(); unsafe { @@ -262,7 +246,7 @@ impl DirectXRenderer { ManuallyDrop::drop(&mut self.devices); } - let devices = DirectXDevices::new(disable_direct_composition) + let devices = DirectXRendererDevices::new(directx_devices, disable_direct_composition) .context("Recreating DirectX devices")?; let resources = DirectXResources::new( &devices, @@ -337,49 +321,39 @@ impl DirectXRenderer { if self.resources.width == width && self.resources.height == height { return Ok(()); } + self.resources.width = width; + self.resources.height = height; + + // Clear the render target before resizing + unsafe { self.devices.device_context.OMSetRenderTargets(None, None) }; + unsafe { ManuallyDrop::drop(&mut self.resources.render_target) }; + drop(self.resources.render_target_view[0].take().unwrap()); + + // Resizing the swap chain requires a call to the underlying DXGI adapter, which can return the device removed error. + // The app might have moved to a monitor that's attached to a different graphics device. + // When a graphics device is removed or reset, the desktop resolution often changes, resulting in a window size change. + // But here we just return the error, because we are handling device lost scenarios elsewhere. unsafe { - // Clear the render target before resizing - self.devices.device_context.OMSetRenderTargets(None, None); - ManuallyDrop::drop(&mut self.resources.render_target); - drop(self.resources.render_target_view[0].take().unwrap()); - - let result = self.resources.swap_chain.ResizeBuffers( - BUFFER_COUNT as u32, - width, - height, - RENDER_TARGET_FORMAT, - DXGI_SWAP_CHAIN_FLAG(0), - ); - // Resizing the swap chain requires a call to the underlying DXGI adapter, which can return the device removed error. - // The app might have moved to a monitor that's attached to a different graphics device. - // When a graphics device is removed or reset, the desktop resolution often changes, resulting in a window size change. - match result { - Ok(_) => {} - Err(e) => { - if e.code() == DXGI_ERROR_DEVICE_REMOVED || e.code() == DXGI_ERROR_DEVICE_RESET - { - let reason = self.devices.device.GetDeviceRemovedReason(); - log::error!( - "DirectX device removed or reset when resizing. Reason: {:?}", - reason - ); - self.resources.width = width; - self.resources.height = height; - self.handle_device_lost()?; - return Ok(()); - } else { - log::error!("Failed to resize swap chain: {:?}", e); - return Err(e.into()); - } - } - } - self.resources - .recreate_resources(&self.devices, width, height)?; + .swap_chain + .ResizeBuffers( + BUFFER_COUNT as u32, + width, + height, + RENDER_TARGET_FORMAT, + DXGI_SWAP_CHAIN_FLAG(0), + ) + .context("Failed to resize swap chain")?; + } + + self.resources + .recreate_resources(&self.devices, width, height)?; + unsafe { self.devices .device_context .OMSetRenderTargets(Some(&self.resources.render_target_view), None); } + Ok(()) } @@ -680,7 +654,7 @@ impl DirectXRenderer { impl DirectXResources { pub fn new( - devices: &DirectXDevices, + devices: &DirectXRendererDevices, width: u32, height: u32, hwnd: HWND, @@ -725,7 +699,7 @@ impl DirectXResources { #[inline] fn recreate_resources( &mut self, - devices: &DirectXDevices, + devices: &DirectXRendererDevices, width: u32, height: u32, ) -> Result<()> { @@ -745,8 +719,6 @@ impl DirectXResources { self.path_intermediate_msaa_view = path_intermediate_msaa_view; self.path_intermediate_srv = path_intermediate_srv; self.viewport = viewport; - self.width = width; - self.height = height; Ok(()) } } @@ -1041,92 +1013,6 @@ impl Drop for DirectXResources { } } -#[inline] -fn check_debug_layer_available() -> bool { - #[cfg(debug_assertions)] - { - unsafe { DXGIGetDebugInterface1::(0) } - .log_err() - .is_some() - } - #[cfg(not(debug_assertions))] - { - false - } -} - -#[inline] -fn get_dxgi_factory(debug_layer_available: bool) -> Result { - let factory_flag = if debug_layer_available { - DXGI_CREATE_FACTORY_DEBUG - } else { - #[cfg(debug_assertions)] - log::warn!( - "Failed to get DXGI debug interface. DirectX debugging features will be disabled." - ); - DXGI_CREATE_FACTORY_FLAGS::default() - }; - unsafe { Ok(CreateDXGIFactory2(factory_flag)?) } -} - -fn get_adapter(dxgi_factory: &IDXGIFactory6, debug_layer_available: bool) -> Result { - for adapter_index in 0.. { - let adapter: IDXGIAdapter1 = unsafe { - dxgi_factory - .EnumAdapterByGpuPreference(adapter_index, DXGI_GPU_PREFERENCE_MINIMUM_POWER) - }?; - if let Ok(desc) = unsafe { adapter.GetDesc1() } { - let gpu_name = String::from_utf16_lossy(&desc.Description) - .trim_matches(char::from(0)) - .to_string(); - log::info!("Using GPU: {}", gpu_name); - } - // Check to see whether the adapter supports Direct3D 11, but don't - // create the actual device yet. - if get_device(&adapter, None, None, None, debug_layer_available) - .log_err() - .is_some() - { - return Ok(adapter); - } - } - - unreachable!() -} - -fn get_device( - adapter: &IDXGIAdapter1, - device: Option<*mut Option>, - context: Option<*mut Option>, - feature_level: Option<*mut D3D_FEATURE_LEVEL>, - debug_layer_available: bool, -) -> Result<()> { - let device_flags = if debug_layer_available { - D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_DEBUG - } else { - D3D11_CREATE_DEVICE_BGRA_SUPPORT - }; - unsafe { - D3D11CreateDevice( - adapter, - D3D_DRIVER_TYPE_UNKNOWN, - HMODULE::default(), - device_flags, - // 4x MSAA is required for Direct3D Feature Level 10.1 or better - Some(&[ - D3D_FEATURE_LEVEL_11_1, - D3D_FEATURE_LEVEL_11_0, - D3D_FEATURE_LEVEL_10_1, - ]), - D3D11_SDK_VERSION, - device, - feature_level, - context, - )?; - } - Ok(()) -} - #[inline] fn get_comp_device(dxgi_device: &IDXGIDevice) -> Result { Ok(unsafe { DCompositionCreateDevice(dxgi_device)? }) @@ -1191,7 +1077,7 @@ fn create_swap_chain( #[inline] fn create_resources( - devices: &DirectXDevices, + devices: &DirectXRendererDevices, swap_chain: &IDXGISwapChain1, width: u32, height: u32, diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 06b242465f71ffe6e5a0c6c90132d548f0e3d8ff..c1e2040d377da814b261682eb93321fda4ebdb2d 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -25,6 +25,7 @@ pub(crate) const WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD: u32 = WM_USER + 3; pub(crate) const WM_GPUI_DOCK_MENU_ACTION: u32 = WM_USER + 4; pub(crate) const WM_GPUI_FORCE_UPDATE_WINDOW: u32 = WM_USER + 5; pub(crate) const WM_GPUI_KEYBOARD_LAYOUT_CHANGED: u32 = WM_USER + 6; +pub(crate) const WM_GPUI_GPU_DEVICE_LOST: u32 = WM_USER + 7; const SIZE_MOVE_LOOP_TIMER_ID: usize = 1; const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1; @@ -40,7 +41,6 @@ impl WindowsWindowInner { 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), @@ -104,6 +104,7 @@ impl WindowsWindowInner { WM_SHOWWINDOW => self.handle_window_visibility_changed(handle, wparam), WM_GPUI_CURSOR_STYLE_CHANGED => self.handle_cursor_changed(lparam), WM_GPUI_FORCE_UPDATE_WINDOW => self.draw_window(handle, true), + WM_GPUI_GPU_DEVICE_LOST => self.handle_device_lost(lparam), _ => None, }; if let Some(n) = handled { @@ -1167,26 +1168,12 @@ impl WindowsWindowInner { None } - 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 - } + fn handle_device_lost(&self, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let devices = lparam.0 as *const DirectXDevices; + let devices = unsafe { &*devices }; + lock.renderer.handle_device_lost(&devices); + Some(0) } #[inline] diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index e12a74b966e11a15fe1fe1c8479dfa42d97ee446..2df357b09199275dc94de435dee0386818eb0731 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -1,6 +1,7 @@ use std::{ cell::RefCell, ffi::OsStr, + mem::ManuallyDrop, path::{Path, PathBuf}, rc::{Rc, Weak}, sync::Arc, @@ -17,7 +18,7 @@ use windows::{ UI::ViewManagement::UISettings, Win32::{ Foundation::*, - Graphics::Gdi::*, + Graphics::{Direct3D11::ID3D11Device, Gdi::*}, Security::Credentials::*, System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*}, UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*}, @@ -55,6 +56,7 @@ pub(crate) struct WindowsPlatformState { jump_list: JumpList, // NOTE: standard cursor handles don't need to close. pub(crate) current_cursor: Option, + directx_devices: ManuallyDrop, } #[derive(Default)] @@ -69,15 +71,17 @@ struct PlatformCallbacks { } impl WindowsPlatformState { - fn new() -> Self { + fn new(directx_devices: DirectXDevices) -> Self { let callbacks = PlatformCallbacks::default(); let jump_list = JumpList::new(); let current_cursor = load_cursor(CursorStyle::Arrow); + let directx_devices = ManuallyDrop::new(directx_devices); Self { callbacks, jump_list, current_cursor, + directx_devices, menus: Vec::new(), } } @@ -88,15 +92,21 @@ impl WindowsPlatform { unsafe { OleInitialize(None).context("unable to initialize Windows OLE")?; } + let directx_devices = DirectXDevices::new().context("Creating DirectX devices")?; let (main_sender, main_receiver) = flume::unbounded::(); let validation_number = rand::random::(); let raw_window_handles = Arc::new(RwLock::new(SmallVec::new())); + let text_system = Arc::new( + DirectWriteTextSystem::new(&directx_devices) + .context("Error creating DirectWriteTextSystem")?, + ); register_platform_window_class(); let mut context = PlatformWindowCreateContext { inner: None, raw_window_handles: Arc::downgrade(&raw_window_handles), validation_number, main_receiver: Some(main_receiver), + directx_devices: Some(directx_devices), }; let result = unsafe { CreateWindowExW( @@ -125,12 +135,7 @@ impl WindowsPlatform { .is_ok_and(|value| value == "true" || value == "1"); let background_executor = BackgroundExecutor::new(dispatcher.clone()); let foreground_executor = ForegroundExecutor::new(dispatcher); - let directx_devices = DirectXDevices::new(disable_direct_composition) - .context("Unable to init directx devices.")?; - let text_system = Arc::new( - DirectWriteTextSystem::new(&directx_devices) - .context("Error creating DirectWriteTextSystem")?, - ); + let drop_target_helper: IDropTargetHelper = unsafe { CoCreateInstance(&CLSID_DragDropHelper, None, CLSCTX_INPROC_SERVER) .context("Error creating drop target helper.")? @@ -181,6 +186,7 @@ impl WindowsPlatform { main_receiver: self.inner.main_receiver.clone(), platform_window_handle: self.handle, disable_direct_composition: self.disable_direct_composition, + directx_devices: (*self.inner.state.borrow().directx_devices).clone(), } } @@ -228,11 +234,24 @@ impl WindowsPlatform { } fn begin_vsync_thread(&self) { + let mut directx_device = (*self.inner.state.borrow().directx_devices).clone(); + let platform_window: SafeHwnd = self.handle.into(); + let validation_number = self.inner.validation_number; let all_windows = Arc::downgrade(&self.raw_window_handles); + let text_system = Arc::downgrade(&self.text_system); std::thread::spawn(move || { let vsync_provider = VSyncProvider::new(); loop { vsync_provider.wait_for_vsync(); + if check_device_lost(&directx_device.device) { + handle_gpu_device_lost( + &mut directx_device, + platform_window.as_raw(), + validation_number, + &all_windows, + &text_system, + ); + } let Some(all_windows) = all_windows.upgrade() else { break; }; @@ -647,7 +666,9 @@ impl Platform for WindowsPlatform { impl WindowsPlatformInner { fn new(context: &mut PlatformWindowCreateContext) -> Result> { - let state = RefCell::new(WindowsPlatformState::new()); + let state = RefCell::new(WindowsPlatformState::new( + context.directx_devices.take().unwrap(), + )); Ok(Rc::new(Self { state, raw_window_handles: context.raw_window_handles.clone(), @@ -667,7 +688,8 @@ impl WindowsPlatformInner { WM_GPUI_CLOSE_ONE_WINDOW | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD | WM_GPUI_DOCK_MENU_ACTION - | WM_GPUI_KEYBOARD_LAYOUT_CHANGED => self.handle_gpui_events(msg, wparam, lparam), + | WM_GPUI_KEYBOARD_LAYOUT_CHANGED + | WM_GPUI_GPU_DEVICE_LOST => self.handle_gpui_events(msg, wparam, lparam), _ => None, }; if let Some(result) = handled { @@ -692,6 +714,7 @@ impl WindowsPlatformInner { WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD => self.run_foreground_task(), WM_GPUI_DOCK_MENU_ACTION => self.handle_dock_action_event(lparam.0 as _), WM_GPUI_KEYBOARD_LAYOUT_CHANGED => self.handle_keyboard_layout_change(), + WM_GPUI_GPU_DEVICE_LOST => self.handle_device_lost(lparam), _ => unreachable!(), } } @@ -749,6 +772,18 @@ impl WindowsPlatformInner { self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback); Some(0) } + + fn handle_device_lost(&self, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let directx_devices = lparam.0 as *const DirectXDevices; + let directx_devices = unsafe { &*directx_devices }; + unsafe { + ManuallyDrop::drop(&mut lock.directx_devices); + } + lock.directx_devices = ManuallyDrop::new(directx_devices.clone()); + + Some(0) + } } impl Drop for WindowsPlatform { @@ -762,6 +797,14 @@ impl Drop for WindowsPlatform { } } +impl Drop for WindowsPlatformState { + fn drop(&mut self) { + unsafe { + ManuallyDrop::drop(&mut self.directx_devices); + } + } +} + pub(crate) struct WindowCreationInfo { pub(crate) icon: HICON, pub(crate) executor: ForegroundExecutor, @@ -772,6 +815,7 @@ pub(crate) struct WindowCreationInfo { pub(crate) main_receiver: flume::Receiver, pub(crate) platform_window_handle: HWND, pub(crate) disable_direct_composition: bool, + pub(crate) directx_devices: DirectXDevices, } struct PlatformWindowCreateContext { @@ -779,6 +823,7 @@ struct PlatformWindowCreateContext { raw_window_handles: std::sync::Weak>>, validation_number: usize, main_receiver: Option>, + directx_devices: Option, } fn open_target(target: impl AsRef) -> Result<()> { @@ -951,6 +996,80 @@ fn should_auto_hide_scrollbars() -> Result { Ok(ui_settings.AutoHideScrollBars()?) } +fn check_device_lost(device: &ID3D11Device) -> bool { + let device_state = unsafe { device.GetDeviceRemovedReason() }; + match device_state { + Ok(_) => false, + Err(err) => { + log::error!("DirectX device lost detected: {:?}", err); + true + } + } +} + +fn handle_gpu_device_lost( + directx_devices: &mut DirectXDevices, + platform_window: HWND, + validation_number: usize, + all_windows: &std::sync::Weak>>, + text_system: &std::sync::Weak, +) { + // Here we wait a bit to ensure the the system has time to recover from the device lost state. + // If we don't wait, the final drawing result will be blank. + std::thread::sleep(std::time::Duration::from_millis(350)); + + try_to_recover_from_device_lost( + || { + DirectXDevices::new() + .context("Failed to recreate new DirectX devices after device lost") + }, + |new_devices| *directx_devices = new_devices, + || { + log::error!("Failed to recover DirectX devices after multiple attempts."); + // Do something here? + // At this point, the device loss is considered unrecoverable. + // std::process::exit(1); + }, + ); + log::info!("DirectX devices successfully recreated."); + + unsafe { + SendMessageW( + platform_window, + WM_GPUI_GPU_DEVICE_LOST, + Some(WPARAM(validation_number)), + Some(LPARAM(directx_devices as *const _ as _)), + ); + } + + if let Some(text_system) = text_system.upgrade() { + text_system.handle_gpu_lost(&directx_devices); + } + if let Some(all_windows) = all_windows.upgrade() { + for window in all_windows.read().iter() { + unsafe { + SendMessageW( + window.as_raw(), + WM_GPUI_GPU_DEVICE_LOST, + Some(WPARAM(validation_number)), + Some(LPARAM(directx_devices as *const _ as _)), + ); + } + } + std::thread::sleep(std::time::Duration::from_millis(200)); + for window in all_windows.read().iter() { + unsafe { + SendMessageW( + window.as_raw(), + WM_GPUI_FORCE_UPDATE_WINDOW, + Some(WPARAM(validation_number)), + None, + ); + } + } + } +} + const PLATFORM_WINDOW_CLASS_NAME: PCWSTR = w!("Zed::PlatformWindow"); fn register_platform_window_class() { diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 7fd4aff3c69ea5ba1c99dc018dc21e35748beeb3..9d001da822315c76aa9a16b010a38407c5730386 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -79,6 +79,7 @@ pub(crate) struct WindowsWindowInner { impl WindowsWindowState { fn new( hwnd: HWND, + directx_devices: &DirectXDevices, window_params: &CREATESTRUCTW, current_cursor: Option, display: WindowsDisplay, @@ -104,7 +105,7 @@ impl WindowsWindowState { }; let border_offset = WindowBorderOffset::default(); let restore_from_minimized = None; - let renderer = DirectXRenderer::new(hwnd, disable_direct_composition) + let renderer = DirectXRenderer::new(hwnd, directx_devices, disable_direct_composition) .context("Creating DirectX renderer")?; let callbacks = Callbacks::default(); let input_handler = None; @@ -205,9 +206,10 @@ impl WindowsWindowState { } impl WindowsWindowInner { - fn new(context: &WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result> { + fn new(context: &mut WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result> { let state = RefCell::new(WindowsWindowState::new( hwnd, + &context.directx_devices, cs, context.current_cursor, context.display, @@ -345,6 +347,7 @@ struct WindowCreateContext { platform_window_handle: HWND, appearance: WindowAppearance, disable_direct_composition: bool, + directx_devices: DirectXDevices, } impl WindowsWindow { @@ -363,6 +366,7 @@ impl WindowsWindow { main_receiver, platform_window_handle, disable_direct_composition, + directx_devices, } = creation_info; register_window_class(icon); let hide_title_bar = params @@ -422,6 +426,7 @@ impl WindowsWindow { platform_window_handle, appearance, disable_direct_composition, + directx_devices, }; let creation_result = unsafe { CreateWindowExW( From 473bbd78cca3b81c833453955d56a7666203cd39 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 4 Sep 2025 09:46:40 -0400 Subject: [PATCH 577/744] onboarding: Fix typos in comments (#37541) This PR fixes some grammatical typos in some comments in the `onboarding` crate. Release Notes: - N/A --- crates/onboarding/src/basics_page.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index d98db03be8a02c9e5dd7b36fa7e4fae4b2a320d3..59ec437dcf8d11209e9c73020f1b51e40aa56cce 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -253,7 +253,7 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement move |setting, _| setting.metrics = Some(enabled), ); - // This telemetry event shouldn't fire when it's off. If it does we're be alerted + // This telemetry event shouldn't fire when it's off. If it does we'll be alerted // and can fix it in a timely manner to respect a user's choice. telemetry::event!("Welcome Page Telemetry Metrics Toggled", options = if enabled { @@ -292,7 +292,7 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement move |setting, _| setting.diagnostics = Some(enabled), ); - // This telemetry event shouldn't fire when it's off. If it does we're be alerted + // This telemetry event shouldn't fire when it's off. If it does we'll be alerted // and can fix it in a timely manner to respect a user's choice. telemetry::event!("Welcome Page Telemetry Diagnostics Toggled", options = if enabled { From a05f86f97bd69569d256229007bc51daacf1be22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Thu, 4 Sep 2025 22:47:17 +0800 Subject: [PATCH 578/744] windows: Don't log error when `RedrawWindow` (#37542) Release Notes: - N/A --- crates/gpui/src/platform/windows/platform.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 2df357b09199275dc94de435dee0386818eb0731..96db8077c4b7b139bf2c724a3502a6e4bd194f9f 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -257,9 +257,7 @@ impl WindowsPlatform { }; for hwnd in all_windows.read().iter() { unsafe { - RedrawWindow(Some(hwnd.as_raw()), None, None, RDW_INVALIDATE) - .ok() - .log_err(); + let _ = RedrawWindow(Some(hwnd.as_raw()), None, None, RDW_INVALIDATE); } } } From 1ae326432e7bda2e89fd63e59a56c082277106b4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 4 Sep 2025 09:14:53 -0600 Subject: [PATCH 579/744] Extract a scheduler crate from GPUI to enable unified integration testing of client and server code (#37326) Extracts and cleans up GPUI's scheduler code into a new `scheduler` crate, making it pluggable by external runtimes. This will enable deterministic integration testing with cloud components by providing a unified test scheduler across Zed and backend code. In Zed, it will replace the existing GPUI scheduler for consistent async task management across platforms. ## Changes - **Core Implementation**: `TestScheduler` with seed-based randomization, session tracking (`SessionId`), and foreground/background task separation for reproducible testing. - **Executors**: `ForegroundExecutor` (!Send, thread-local) and `BackgroundExecutor` (Send, with blocking/timeout support) as GPUI-compatible wrappers. - **Clock and Timer**: Controllable `TestClock` and future-based `Timer` for time-sensitive tests. - **Testing APIs**: `once()`, `with_seed()`, and `many()` methods for configurable test runs. - **Dependencies**: Added `async-task`, `chrono`, `futures`, etc., with updates to `Cargo.toml` and lock file. ## Benefits - **Integration Testing**: Facilitates reliable async tests involving cloud sessions, reducing flakiness via deterministic execution. - **Pluggability**: Trait-based design (`Scheduler`) allows easy integration into non-GPUI runtimes while maintaining GPUI compatibility. - **Cleanup**: Refactors GPUI scheduler logic for clarity, correctness (no `unwrap()`, proper error handling), and extensibility. Follows Rust guidelines; run `./script/clippy` for verification. - [x] Define and test a core scheduler that we think can power our cloud code and GPUI - [ ] Replace GPUI's scheduler Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- .rules | 13 + Cargo.lock | 73 ++-- Cargo.toml | 6 +- crates/acp_thread/src/acp_thread.rs | 6 +- crates/action_log/src/action_log.rs | 6 +- crates/agent_ui/src/buffer_codegen.rs | 6 +- .../src/assistant_context_tests.rs | 14 +- crates/assistant_tools/src/edit_agent.rs | 8 +- .../src/edit_agent/create_file_parser.rs | 2 +- .../src/edit_agent/edit_parser.rs | 2 +- .../assistant_tools/src/edit_agent/evals.rs | 4 +- .../src/edit_agent/streaming_fuzzy_matcher.rs | 2 +- crates/buffer_diff/src/buffer_diff.rs | 17 +- crates/channel/src/channel_chat.rs | 6 +- crates/client/src/client.rs | 7 +- crates/collab/src/auth.rs | 37 +- crates/collab/src/db.rs | 2 +- crates/collab/src/db/tests.rs | 4 +- crates/collab/src/tests/integration_tests.rs | 2 +- .../src/tests/random_channel_buffer_tests.rs | 2 +- .../random_project_collaboration_tests.rs | 83 +++-- .../src/tests/randomized_test_helpers.rs | 18 +- crates/diagnostics/src/diagnostics_tests.rs | 45 +-- crates/editor/src/display_map.rs | 59 ++- crates/editor/src/display_map/block_map.rs | 56 +-- crates/editor/src/display_map/fold_map.rs | 48 +-- crates/editor/src/display_map/inlay_map.rs | 36 +- crates/editor/src/display_map/tab_map.rs | 14 +- crates/editor/src/display_map/wrap_map.rs | 24 +- crates/editor/src/editor.rs | 4 +- crates/editor/src/git/blame.rs | 8 +- crates/gpui/examples/data_table.rs | 54 +-- crates/gpui/src/app/test_context.rs | 2 +- crates/gpui/src/bounds_tree.rs | 10 +- crates/gpui/src/platform/test/dispatcher.rs | 10 +- crates/gpui/src/platform/windows/platform.rs | 6 +- crates/language/src/buffer.rs | 4 +- crates/language/src/buffer_tests.rs | 32 +- crates/multi_buffer/src/multi_buffer.rs | 39 +- crates/multi_buffer/src/multi_buffer_tests.rs | 48 +-- crates/project/src/lsp_store.rs | 4 +- crates/project/src/project_tests.rs | 6 +- crates/rope/benches/rope_benchmark.rs | 6 +- crates/rope/src/chunk.rs | 8 +- crates/rope/src/rope.rs | 22 +- crates/rpc/src/auth.rs | 40 +- crates/scheduler/Cargo.toml | 25 ++ crates/scheduler/LICENSE-APACHE | 1 + crates/scheduler/src/clock.rs | 34 ++ crates/scheduler/src/executor.rs | 137 +++++++ crates/scheduler/src/scheduler.rs | 63 ++++ crates/scheduler/src/test_scheduler.rs | 352 ++++++++++++++++++ crates/scheduler/src/tests.rs | 348 +++++++++++++++++ crates/streaming_diff/src/streaming_diff.rs | 12 +- crates/sum_tree/src/sum_tree.rs | 46 ++- crates/terminal/src/terminal.rs | 17 +- crates/text/src/locator.rs | 8 +- crates/text/src/network.rs | 6 +- crates/text/src/patch.rs | 12 +- crates/text/src/tests.rs | 21 +- crates/text/src/text.rs | 8 +- crates/util/src/util.rs | 13 +- crates/worktree/src/worktree_tests.rs | 30 +- crates/zeta/src/input_excerpt.rs | 4 +- 64 files changed, 1569 insertions(+), 473 deletions(-) create mode 100644 crates/scheduler/Cargo.toml create mode 120000 crates/scheduler/LICENSE-APACHE create mode 100644 crates/scheduler/src/clock.rs create mode 100644 crates/scheduler/src/executor.rs create mode 100644 crates/scheduler/src/scheduler.rs create mode 100644 crates/scheduler/src/test_scheduler.rs create mode 100644 crates/scheduler/src/tests.rs diff --git a/.rules b/.rules index da009f1877b4c6ef2f0613995391852d4bf1dc8a..2f2b9cd705d95775bedf092bc4e6254136da6117 100644 --- a/.rules +++ b/.rules @@ -12,6 +12,19 @@ - Example: avoid `let _ = client.request(...).await?;` - use `client.request(...).await?;` instead * When implementing async operations that may fail, ensure errors propagate to the UI layer so users get meaningful feedback. * Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`. +* When creating new crates, prefer specifying the library root path in `Cargo.toml` using `[lib] path = "...rs"` instead of the default `lib.rs`, to maintain consistent and descriptive naming (e.g., `gpui.rs` or `main.rs`). +* Avoid creative additions unless explicitly requested +* Use full words for variable names (no abbreviations like "q" for "queue") +* Use variable shadowing to scope clones in async contexts for clarity, minimizing the lifetime of borrowed references. + Example: + ```rust + executor.spawn({ + let task_ran = task_ran.clone(); + async move { + *task_ran.borrow_mut() = true; + } + }); + ``` # GPUI diff --git a/Cargo.lock b/Cargo.lock index 58d01da63372431e107ea9c0b17fde0700f9050f..ee80d59006f50c321e80bbe6fca9288b345524be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,7 +26,7 @@ dependencies = [ "portable-pty", "project", "prompt_store", - "rand 0.8.5", + "rand 0.9.1", "serde", "serde_json", "settings", @@ -79,7 +79,7 @@ dependencies = [ "log", "pretty_assertions", "project", - "rand 0.8.5", + "rand 0.9.1", "serde_json", "settings", "text", @@ -172,7 +172,7 @@ dependencies = [ "pretty_assertions", "project", "prompt_store", - "rand 0.8.5", + "rand 0.9.1", "ref-cast", "rope", "schemars", @@ -408,7 +408,7 @@ dependencies = [ "project", "prompt_store", "proto", - "rand 0.8.5", + "rand 0.9.1", "release_channel", "rope", "rules_library", @@ -834,7 +834,7 @@ dependencies = [ "project", "prompt_store", "proto", - "rand 0.8.5", + "rand 0.9.1", "regex", "rpc", "serde", @@ -933,7 +933,7 @@ dependencies = [ "parking_lot", "pretty_assertions", "project", - "rand 0.8.5", + "rand 0.9.1", "regex", "serde", "serde_json", @@ -985,7 +985,7 @@ dependencies = [ "pretty_assertions", "project", "prompt_store", - "rand 0.8.5", + "rand 0.9.1", "regex", "reqwest_client", "rust-embed", @@ -2478,7 +2478,7 @@ dependencies = [ "language", "log", "pretty_assertions", - "rand 0.8.5", + "rand 0.9.1", "rope", "serde_json", "sum_tree", @@ -2899,7 +2899,7 @@ dependencies = [ "language", "log", "postage", - "rand 0.8.5", + "rand 0.9.1", "release_channel", "rpc", "settings", @@ -3086,7 +3086,7 @@ dependencies = [ "parking_lot", "paths", "postage", - "rand 0.8.5", + "rand 0.9.1", "regex", "release_channel", "rpc", @@ -3335,7 +3335,7 @@ dependencies = [ "prometheus", "prompt_store", "prost 0.9.0", - "rand 0.8.5", + "rand 0.9.1", "recent_projects", "release_channel", "remote", @@ -4697,7 +4697,7 @@ dependencies = [ "markdown", "pretty_assertions", "project", - "rand 0.8.5", + "rand 0.9.1", "serde", "serde_json", "settings", @@ -5068,7 +5068,7 @@ dependencies = [ "parking_lot", "pretty_assertions", "project", - "rand 0.8.5", + "rand 0.9.1", "regex", "release_channel", "rpc", @@ -5563,7 +5563,7 @@ dependencies = [ "parking_lot", "paths", "project", - "rand 0.8.5", + "rand 0.9.1", "release_channel", "remote", "reqwest_client", @@ -6412,7 +6412,7 @@ dependencies = [ "log", "parking_lot", "pretty_assertions", - "rand 0.8.5", + "rand 0.9.1", "regex", "rope", "schemars", @@ -7465,7 +7465,7 @@ dependencies = [ "pathfinder_geometry", "postage", "profiling", - "rand 0.8.5", + "rand 0.9.1", "raw-window-handle", "refineable", "reqwest_client", @@ -9078,7 +9078,7 @@ dependencies = [ "parking_lot", "postage", "pretty_assertions", - "rand 0.8.5", + "rand 0.9.1", "regex", "rpc", "schemars", @@ -10392,7 +10392,7 @@ dependencies = [ "parking_lot", "pretty_assertions", "project", - "rand 0.8.5", + "rand 0.9.1", "rope", "serde", "settings", @@ -12618,7 +12618,7 @@ dependencies = [ "postage", "prettier", "pretty_assertions", - "rand 0.8.5", + "rand 0.9.1", "regex", "release_channel", "remote", @@ -13892,7 +13892,7 @@ dependencies = [ "ctor", "gpui", "log", - "rand 0.8.5", + "rand 0.9.1", "rayon", "smallvec", "sum_tree", @@ -13921,7 +13921,7 @@ dependencies = [ "gpui", "parking_lot", "proto", - "rand 0.8.5", + "rand 0.9.1", "rsa", "serde", "serde_json", @@ -14356,6 +14356,19 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "scheduler" +version = "0.1.0" +dependencies = [ + "async-task", + "chrono", + "futures 0.3.31", + "parking", + "parking_lot", + "rand 0.9.1", + "workspace-hack", +] + [[package]] name = "schema_generator" version = "0.1.0" @@ -15655,7 +15668,7 @@ name = "streaming_diff" version = "0.1.0" dependencies = [ "ordered-float 2.10.1", - "rand 0.8.5", + "rand 0.9.1", "rope", "util", "workspace-hack", @@ -15769,7 +15782,7 @@ dependencies = [ "arrayvec", "ctor", "log", - "rand 0.8.5", + "rand 0.9.1", "rayon", "workspace-hack", "zlog", @@ -16360,7 +16373,7 @@ dependencies = [ "futures 0.3.31", "gpui", "libc", - "rand 0.8.5", + "rand 0.9.1", "regex", "release_channel", "schemars", @@ -16408,7 +16421,7 @@ dependencies = [ "language", "log", "project", - "rand 0.8.5", + "rand 0.9.1", "regex", "schemars", "search", @@ -16440,7 +16453,7 @@ dependencies = [ "log", "parking_lot", "postage", - "rand 0.8.5", + "rand 0.9.1", "regex", "rope", "smallvec", @@ -17797,7 +17810,7 @@ dependencies = [ "libc", "log", "nix 0.29.0", - "rand 0.8.5", + "rand 0.9.1", "regex", "rust-embed", "schemars", @@ -18588,7 +18601,7 @@ dependencies = [ "futures 0.3.31", "gpui", "parking_lot", - "rand 0.8.5", + "rand 0.9.1", "workspace-hack", "zlog", ] @@ -20047,7 +20060,7 @@ dependencies = [ "paths", "postage", "pretty_assertions", - "rand 0.8.5", + "rand 0.9.1", "rpc", "schemars", "serde", @@ -20812,7 +20825,7 @@ dependencies = [ "menu", "postage", "project", - "rand 0.8.5", + "rand 0.9.1", "regex", "release_channel", "reqwest_client", diff --git a/Cargo.toml b/Cargo.toml index 941c364e0dd85def66ebbc4e310ef0a90458fe44..8a487b612a18dc837d3cd75697f13bf92b5b28b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,6 +131,7 @@ members = [ "crates/refineable", "crates/refineable/derive_refineable", "crates/release_channel", + "crates/scheduler", "crates/remote", "crates/remote_server", "crates/repl", @@ -360,6 +361,7 @@ proto = { path = "crates/proto" } recent_projects = { path = "crates/recent_projects" } refineable = { path = "crates/refineable" } release_channel = { path = "crates/release_channel" } +scheduler = { path = "crates/scheduler" } remote = { path = "crates/remote" } remote_server = { path = "crates/remote_server" } repl = { path = "crates/repl" } @@ -444,6 +446,7 @@ async-fs = "2.1" async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" } async-recursion = "1.0.0" async-tar = "0.5.0" +async-task = "4.7" async-trait = "0.1" async-tungstenite = "0.29.1" async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] } @@ -538,6 +541,7 @@ objc = "0.2" open = "5.0.0" ordered-float = "2.1.1" palette = { version = "0.7.5", default-features = false, features = ["std"] } +parking = "2.0" parking_lot = "0.12.1" partial-json-fixer = "0.5.3" parse_int = "0.9" @@ -560,7 +564,7 @@ prost-build = "0.9" prost-types = "0.9" pulldown-cmark = { version = "0.12.0", default-features = false } quote = "1.0.9" -rand = "0.8.5" +rand = "0.9" rayon = "1.8" ref-cast = "1.0.24" regex = "1.5" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index dc295369cce2b8fda596e3917724187bd35b7377..a3a8e31230b749b7b774a380030aab4600d78a07 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -2114,7 +2114,7 @@ mod tests { use gpui::{App, AsyncApp, TestAppContext, WeakEntity}; use indoc::indoc; use project::{FakeFs, Fs}; - use rand::Rng as _; + use rand::{distr, prelude::*}; use serde_json::json; use settings::SettingsStore; use smol::stream::StreamExt as _; @@ -3057,8 +3057,8 @@ mod tests { cx: &mut App, ) -> Task>> { let session_id = acp::SessionId( - rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) + rand::rng() + .sample_iter(&distr::Alphanumeric) .take(7) .map(char::from) .collect::() diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index 9ec10f4dbb0e670bf20d9c033db9cec02e5fda67..11ba596ac5a0ecd4ed49744d0eafa9defcde20c1 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -2218,7 +2218,7 @@ mod tests { action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); for _ in 0..operations { - match rng.gen_range(0..100) { + match rng.random_range(0..100) { 0..25 => { action_log.update(cx, |log, cx| { let range = buffer.read(cx).random_byte_range(0, &mut rng); @@ -2237,7 +2237,7 @@ mod tests { .unwrap(); } _ => { - let is_agent_edit = rng.gen_bool(0.5); + let is_agent_edit = rng.random_bool(0.5); if is_agent_edit { log::info!("agent edit"); } else { @@ -2252,7 +2252,7 @@ mod tests { } } - if rng.gen_bool(0.2) { + if rng.random_bool(0.2) { quiesce(&action_log, &buffer, cx); } } diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index 04eb41793f2257a9dccfdd089594d2f90d0ce513..2309aad754aee55af5ad040c39d22304486446a4 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -1139,7 +1139,7 @@ mod tests { ); while !new_text.is_empty() { let max_len = cmp::min(new_text.len(), 10); - let len = rng.gen_range(1..=max_len); + let len = rng.random_range(1..=max_len); let (chunk, suffix) = new_text.split_at(len); chunks_tx.unbounded_send(chunk.to_string()).unwrap(); new_text = suffix; @@ -1208,7 +1208,7 @@ mod tests { ); while !new_text.is_empty() { let max_len = cmp::min(new_text.len(), 10); - let len = rng.gen_range(1..=max_len); + let len = rng.random_range(1..=max_len); let (chunk, suffix) = new_text.split_at(len); chunks_tx.unbounded_send(chunk.to_string()).unwrap(); new_text = suffix; @@ -1277,7 +1277,7 @@ mod tests { ); while !new_text.is_empty() { let max_len = cmp::min(new_text.len(), 10); - let len = rng.gen_range(1..=max_len); + let len = rng.random_range(1..=max_len); let (chunk, suffix) = new_text.split_at(len); chunks_tx.unbounded_send(chunk.to_string()).unwrap(); new_text = suffix; diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_context/src/assistant_context_tests.rs index 61d748cbddb0858dda2f181ea6c943426393e087..8b182685cfeb4e3ae1b9df8c532b8f0c5ad91235 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_context/src/assistant_context_tests.rs @@ -764,7 +764,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std let network = Arc::new(Mutex::new(Network::new(rng.clone()))); let mut contexts = Vec::new(); - let num_peers = rng.gen_range(min_peers..=max_peers); + let num_peers = rng.random_range(min_peers..=max_peers); let context_id = ContextId::new(); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); for i in 0..num_peers { @@ -806,10 +806,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std || !network.lock().is_idle() || network.lock().contains_disconnected_peers() { - let context_index = rng.gen_range(0..contexts.len()); + let context_index = rng.random_range(0..contexts.len()); let context = &contexts[context_index]; - match rng.gen_range(0..100) { + match rng.random_range(0..100) { 0..=29 if mutation_count > 0 => { log::info!("Context {}: edit buffer", context_index); context.update(cx, |context, cx| { @@ -874,10 +874,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std merge_same_roles: true, })]; - let num_sections = rng.gen_range(0..=3); + let num_sections = rng.random_range(0..=3); let mut section_start = 0; for _ in 0..num_sections { - let mut section_end = rng.gen_range(section_start..=output_text.len()); + let mut section_end = rng.random_range(section_start..=output_text.len()); while !output_text.is_char_boundary(section_end) { section_end += 1; } @@ -924,7 +924,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std 75..=84 if mutation_count > 0 => { context.update(cx, |context, cx| { if let Some(message) = context.messages(cx).choose(&mut rng) { - let new_status = match rng.gen_range(0..3) { + let new_status = match rng.random_range(0..3) { 0 => MessageStatus::Done, 1 => MessageStatus::Pending, _ => MessageStatus::Error(SharedString::from("Random error")), @@ -971,7 +971,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std network.lock().broadcast(replica_id, ops_to_send); context.update(cx, |context, cx| context.apply_ops(ops_to_receive, cx)); - } else if rng.gen_bool(0.1) && replica_id != 0 { + } else if rng.random_bool(0.1) && replica_id != 0 { log::info!("Context {}: disconnecting", context_index); network.lock().disconnect_peer(replica_id); } else if network.lock().has_unreceived(replica_id) { diff --git a/crates/assistant_tools/src/edit_agent.rs b/crates/assistant_tools/src/edit_agent.rs index 665ece2baaeed0dac32e5c0153ec1d79fef47f12..29ac53e2a606d63873f515aff25326debf0486f1 100644 --- a/crates/assistant_tools/src/edit_agent.rs +++ b/crates/assistant_tools/src/edit_agent.rs @@ -1315,17 +1315,17 @@ mod tests { #[gpui::test(iterations = 100)] async fn test_random_indents(mut rng: StdRng) { - let len = rng.gen_range(1..=100); + let len = rng.random_range(1..=100); let new_text = util::RandomCharIter::new(&mut rng) .with_simple_text() .take(len) .collect::(); let new_text = new_text .split('\n') - .map(|line| format!("{}{}", " ".repeat(rng.gen_range(0..=8)), line)) + .map(|line| format!("{}{}", " ".repeat(rng.random_range(0..=8)), line)) .collect::>() .join("\n"); - let delta = IndentDelta::Spaces(rng.gen_range(-4..=4)); + let delta = IndentDelta::Spaces(rng.random_range(-4i8..=4i8) as isize); let chunks = to_random_chunks(&mut rng, &new_text); let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| { @@ -1357,7 +1357,7 @@ mod tests { } fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec { - let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50)); + let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50)); let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count); chunk_indices.sort(); chunk_indices.push(input.len()); diff --git a/crates/assistant_tools/src/edit_agent/create_file_parser.rs b/crates/assistant_tools/src/edit_agent/create_file_parser.rs index 0aad9ecb87c1426486b531ac4291913cd0d74092..5126f9c6b1fe4ee5cc600ae93b7300b7af09451f 100644 --- a/crates/assistant_tools/src/edit_agent/create_file_parser.rs +++ b/crates/assistant_tools/src/edit_agent/create_file_parser.rs @@ -204,7 +204,7 @@ mod tests { } fn parse_random_chunks(input: &str, parser: &mut CreateFileParser, rng: &mut StdRng) -> String { - let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50)); + let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50)); let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count); chunk_indices.sort(); chunk_indices.push(input.len()); diff --git a/crates/assistant_tools/src/edit_agent/edit_parser.rs b/crates/assistant_tools/src/edit_agent/edit_parser.rs index db58c2bf3685030abfa6cfdd506c068c6643dce8..8411171ba4ea491d2603014a0715ce471b34e36f 100644 --- a/crates/assistant_tools/src/edit_agent/edit_parser.rs +++ b/crates/assistant_tools/src/edit_agent/edit_parser.rs @@ -996,7 +996,7 @@ mod tests { } fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec { - let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50)); + let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50)); let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count); chunk_indices.sort(); chunk_indices.push(input.len()); diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index 4f182b31481c5d855b59f4398e104d0eea05bc74..e78d43f56b2f13f90b83968dadd5ff79e1a96658 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -1399,7 +1399,7 @@ fn eval( } fn run_eval(eval: EvalInput, tx: mpsc::Sender>) { - let dispatcher = gpui::TestDispatcher::new(StdRng::from_entropy()); + let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng()); let mut cx = TestAppContext::build(dispatcher, None); let output = cx.executor().block_test(async { let test = EditAgentTest::new(&mut cx).await; @@ -1707,7 +1707,7 @@ async fn retry_on_rate_limit(mut request: impl AsyncFnMut() -> Result) -> }; if let Some(retry_after) = retry_delay { - let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0)); + let jitter = retry_after.mul_f64(rand::rng().random_range(0.0..1.0)); eprintln!("Attempt #{attempt}: Retry after {retry_after:?} + jitter of {jitter:?}"); Timer::after(retry_after + jitter).await; } else { diff --git a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs b/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs index 33b37679f0a345ef070942057b307bd377012d05..386b8204400a157b37b2f356829fa27df3abca92 100644 --- a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs +++ b/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs @@ -771,7 +771,7 @@ mod tests { } fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec { - let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50)); + let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50)); let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count); chunk_indices.sort(); chunk_indices.push(input.len()); diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index b20dad4ebbcc5990bd0a6a165375ca62481e609f..22ee20e0db2810610dc2e7a4cae86dca90681337 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -2044,10 +2044,10 @@ mod tests { #[gpui::test(iterations = 100)] async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) { fn gen_line(rng: &mut StdRng) -> String { - if rng.gen_bool(0.2) { + if rng.random_bool(0.2) { "\n".to_owned() } else { - let c = rng.gen_range('A'..='Z'); + let c = rng.random_range('A'..='Z'); format!("{c}{c}{c}\n") } } @@ -2066,7 +2066,7 @@ mod tests { old_lines.into_iter() }; let mut result = String::new(); - let unchanged_count = rng.gen_range(0..=old_lines.len()); + let unchanged_count = rng.random_range(0..=old_lines.len()); result += &old_lines .by_ref() @@ -2076,14 +2076,14 @@ mod tests { s }); while old_lines.len() > 0 { - let deleted_count = rng.gen_range(0..=old_lines.len()); + let deleted_count = rng.random_range(0..=old_lines.len()); let _advance = old_lines .by_ref() .take(deleted_count) .map(|line| line.len() + 1) .sum::(); let minimum_added = if deleted_count == 0 { 1 } else { 0 }; - let added_count = rng.gen_range(minimum_added..=5); + let added_count = rng.random_range(minimum_added..=5); let addition = (0..added_count).map(|_| gen_line(rng)).collect::(); result += &addition; @@ -2092,7 +2092,8 @@ mod tests { if blank_lines == old_lines.len() { break; }; - let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len()); + let unchanged_count = + rng.random_range((blank_lines + 1).max(1)..=old_lines.len()); result += &old_lines.by_ref().take(unchanged_count).fold( String::new(), |mut s, line| { @@ -2149,7 +2150,7 @@ mod tests { ) }); let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot()); - let mut index_text = if rng.r#gen() { + let mut index_text = if rng.random() { Rope::from(head_text.as_str()) } else { working_copy.as_rope().clone() @@ -2165,7 +2166,7 @@ mod tests { } for _ in 0..operations { - let i = rng.gen_range(0..hunks.len()); + let i = rng.random_range(0..hunks.len()); let hunk = &mut hunks[i]; let hunk_to_change = hunk.clone(); let stage = match hunk.secondary_status { diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index baf23ac39f983c018da2f291bec7879913f12a58..776499c8760f13fbd2903780b1e234f8755d9860 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -129,7 +129,7 @@ impl ChannelChat { loaded_all_messages: false, next_pending_message_id: 0, last_acknowledged_id: None, - rng: StdRng::from_entropy(), + rng: StdRng::from_os_rng(), first_loaded_message_id: None, _subscription: subscription.set_entity(&cx.entity(), &cx.to_async()), } @@ -183,7 +183,7 @@ impl ChannelChat { let channel_id = self.channel_id; let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id)); - let nonce = self.rng.r#gen(); + let nonce = self.rng.random(); self.insert_messages( SumTree::from_item( ChannelMessage { @@ -257,7 +257,7 @@ impl ChannelChat { cx, ); - let nonce: u128 = self.rng.r#gen(); + let nonce: u128 = self.rng.random(); let request = self.rpc.request(proto::UpdateChannelMessage { channel_id: self.channel_id.0, diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 1287b4563c99cbd387b3a18d98fbbc734e55e4db..85f6aeade69cc04c5f58b72258ac062157094460 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -691,7 +691,7 @@ impl Client { #[cfg(any(test, feature = "test-support"))] let mut rng = StdRng::seed_from_u64(0); #[cfg(not(any(test, feature = "test-support")))] - let mut rng = StdRng::from_entropy(); + let mut rng = StdRng::from_os_rng(); let mut delay = INITIAL_RECONNECTION_DELAY; loop { @@ -721,8 +721,9 @@ impl Client { }, cx, ); - let jitter = - Duration::from_millis(rng.gen_range(0..delay.as_millis() as u64)); + let jitter = Duration::from_millis( + rng.random_range(0..delay.as_millis() as u64), + ); cx.background_executor().timer(delay + jitter).await; delay = cmp::min(delay * 2, MAX_RECONNECTION_DELAY); } else { diff --git a/crates/collab/src/auth.rs b/crates/collab/src/auth.rs index e484d6b510f444e764ac38210d6a5cfc42142807..13296b79ae8b3df97753e7adf4f2078990c187b0 100644 --- a/crates/collab/src/auth.rs +++ b/crates/collab/src/auth.rs @@ -227,7 +227,7 @@ pub async fn verify_access_token( #[cfg(test)] mod test { - use rand::thread_rng; + use rand::prelude::*; use scrypt::password_hash::{PasswordHasher, SaltString}; use sea_orm::EntityTrait; @@ -358,9 +358,42 @@ mod test { None, None, params, - &SaltString::generate(thread_rng()), + &SaltString::generate(PasswordHashRngCompat::new()), ) .map_err(anyhow::Error::new)? .to_string()) } + + // TODO: remove once we password_hash v0.6 is released. + struct PasswordHashRngCompat(rand::rngs::ThreadRng); + + impl PasswordHashRngCompat { + fn new() -> Self { + Self(rand::rng()) + } + } + + impl scrypt::password_hash::rand_core::RngCore for PasswordHashRngCompat { + fn next_u32(&mut self) -> u32 { + self.0.next_u32() + } + + fn next_u64(&mut self) -> u64 { + self.0.next_u64() + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + self.0.fill_bytes(dest); + } + + fn try_fill_bytes( + &mut self, + dest: &mut [u8], + ) -> Result<(), scrypt::password_hash::rand_core::Error> { + self.fill_bytes(dest); + Ok(()) + } + } + + impl scrypt::password_hash::rand_core::CryptoRng for PasswordHashRngCompat {} } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 95a485305ca31bde351f4962d1678e30801d8a01..f39da309dde4d4f9b2bebe4d117869f78225112d 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -256,7 +256,7 @@ impl Database { let test_options = self.test_options.as_ref().unwrap(); test_options.executor.simulate_random_delay().await; let fail_probability = *test_options.query_failure_probability.lock(); - if test_options.executor.rng().gen_bool(fail_probability) { + if test_options.executor.rng().random_bool(fail_probability) { return Err(anyhow!("simulated query failure"))?; } diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 2eb8d377acaba9f8fe5ea558a29cc028c2aa11fd..f8560edda78217e6a5e09a5c2e66e0f436ca0477 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -75,10 +75,10 @@ impl TestDb { static LOCK: Mutex<()> = Mutex::new(()); let _guard = LOCK.lock(); - let mut rng = StdRng::from_entropy(); + let mut rng = StdRng::from_os_rng(); let url = format!( "postgres://postgres@localhost/zed-test-{}", - rng.r#gen::() + rng.random::() ); let runtime = tokio::runtime::Builder::new_current_thread() .enable_io() diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 6bb2db05201ea464053a758b390e84ccdfc6527a..07bd162e66f686c425dc441a57644b52141586e3 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -5746,7 +5746,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( let definitions; let buffer_b2; - if rng.r#gen() { + if rng.random() { cx_a.run_until_parked(); cx_b.run_until_parked(); definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx)); diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index 6fcd6d75cd0d827296f555bfa54c18dac518a3be..9451090af2198117ddb20241b99be5b208daa729 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -84,7 +84,7 @@ impl RandomizedTest for RandomChannelBufferTest { } loop { - match rng.gen_range(0..100_u32) { + match rng.random_range(0..100_u32) { 0..=29 => { let channel_name = client.channel_store().read_with(cx, |store, cx| { store.ordered_channels().find_map(|(_, channel)| { diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index bfe05c4a1d600bb280d3821350204d0b2d0d6e08..326f64cb244b88a64728f4347e3cfc31a8c252bf 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -17,7 +17,7 @@ use project::{ DEFAULT_COMPLETION_CONTEXT, Project, ProjectPath, search::SearchQuery, search::SearchResult, }; use rand::{ - distributions::{Alphanumeric, DistString}, + distr::{self, SampleString}, prelude::*, }; use serde::{Deserialize, Serialize}; @@ -168,19 +168,19 @@ impl RandomizedTest for ProjectCollaborationTest { ) -> ClientOperation { let call = cx.read(ActiveCall::global); loop { - match rng.gen_range(0..100_u32) { + match rng.random_range(0..100_u32) { // Mutate the call 0..=29 => { // Respond to an incoming call if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) { - break if rng.gen_bool(0.7) { + break if rng.random_bool(0.7) { ClientOperation::AcceptIncomingCall } else { ClientOperation::RejectIncomingCall }; } - match rng.gen_range(0..100_u32) { + match rng.random_range(0..100_u32) { // Invite a contact to the current call 0..=70 => { let available_contacts = @@ -212,7 +212,7 @@ impl RandomizedTest for ProjectCollaborationTest { } // Mutate projects - 30..=59 => match rng.gen_range(0..100_u32) { + 30..=59 => match rng.random_range(0..100_u32) { // Open a new project 0..=70 => { // Open a remote project @@ -270,7 +270,7 @@ impl RandomizedTest for ProjectCollaborationTest { } // Mutate project worktrees - 81.. => match rng.gen_range(0..100_u32) { + 81.. => match rng.random_range(0..100_u32) { // Add a worktree to a local project 0..=50 => { let Some(project) = client.local_projects().choose(rng).cloned() else { @@ -279,7 +279,7 @@ impl RandomizedTest for ProjectCollaborationTest { let project_root_name = root_name_for_project(&project, cx); let mut paths = client.fs().paths(false); paths.remove(0); - let new_root_path = if paths.is_empty() || rng.r#gen() { + let new_root_path = if paths.is_empty() || rng.random() { Path::new(path!("/")).join(plan.next_root_dir_name()) } else { paths.choose(rng).unwrap().clone() @@ -309,7 +309,7 @@ impl RandomizedTest for ProjectCollaborationTest { .choose(rng) }); let Some(worktree) = worktree else { continue }; - let is_dir = rng.r#gen::(); + let is_dir = rng.random::(); let mut full_path = worktree.read_with(cx, |w, _| PathBuf::from(w.root_name())); full_path.push(gen_file_name(rng)); @@ -334,7 +334,7 @@ impl RandomizedTest for ProjectCollaborationTest { let project_root_name = root_name_for_project(&project, cx); let is_local = project.read_with(cx, |project, _| project.is_local()); - match rng.gen_range(0..100_u32) { + match rng.random_range(0..100_u32) { // Manipulate an existing buffer 0..=70 => { let Some(buffer) = client @@ -349,7 +349,7 @@ impl RandomizedTest for ProjectCollaborationTest { let full_path = buffer .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx)); - match rng.gen_range(0..100_u32) { + match rng.random_range(0..100_u32) { // Close the buffer 0..=15 => { break ClientOperation::CloseBuffer { @@ -360,7 +360,7 @@ impl RandomizedTest for ProjectCollaborationTest { } // Save the buffer 16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => { - let detach = rng.gen_bool(0.3); + let detach = rng.random_bool(0.3); break ClientOperation::SaveBuffer { project_root_name, is_local, @@ -383,17 +383,17 @@ impl RandomizedTest for ProjectCollaborationTest { _ => { let offset = buffer.read_with(cx, |buffer, _| { buffer.clip_offset( - rng.gen_range(0..=buffer.len()), + rng.random_range(0..=buffer.len()), language::Bias::Left, ) }); - let detach = rng.r#gen(); + let detach = rng.random(); break ClientOperation::RequestLspDataInBuffer { project_root_name, full_path, offset, is_local, - kind: match rng.gen_range(0..5_u32) { + kind: match rng.random_range(0..5_u32) { 0 => LspRequestKind::Rename, 1 => LspRequestKind::Highlights, 2 => LspRequestKind::Definition, @@ -407,8 +407,8 @@ impl RandomizedTest for ProjectCollaborationTest { } 71..=80 => { - let query = rng.gen_range('a'..='z').to_string(); - let detach = rng.gen_bool(0.3); + let query = rng.random_range('a'..='z').to_string(); + let detach = rng.random_bool(0.3); break ClientOperation::SearchProject { project_root_name, is_local, @@ -460,7 +460,7 @@ impl RandomizedTest for ProjectCollaborationTest { // Create or update a file or directory 96.. => { - let is_dir = rng.r#gen::(); + let is_dir = rng.random::(); let content; let mut path; let dir_paths = client.fs().directories(false); @@ -470,11 +470,11 @@ impl RandomizedTest for ProjectCollaborationTest { path = dir_paths.choose(rng).unwrap().clone(); path.push(gen_file_name(rng)); } else { - content = Alphanumeric.sample_string(rng, 16); + content = distr::Alphanumeric.sample_string(rng, 16); // Create a new file or overwrite an existing file let file_paths = client.fs().files(); - if file_paths.is_empty() || rng.gen_bool(0.5) { + if file_paths.is_empty() || rng.random_bool(0.5) { path = dir_paths.choose(rng).unwrap().clone(); path.push(gen_file_name(rng)); path.set_extension("rs"); @@ -1090,7 +1090,7 @@ impl RandomizedTest for ProjectCollaborationTest { move |_, cx| { let background = cx.background_executor(); let mut rng = background.rng(); - let count = rng.gen_range::(1..3); + let count = rng.random_range::(1..3); let files = fs.as_fake().files(); let files = (0..count) .map(|_| files.choose(&mut rng).unwrap().clone()) @@ -1117,12 +1117,12 @@ impl RandomizedTest for ProjectCollaborationTest { let background = cx.background_executor(); let mut rng = background.rng(); - let highlight_count = rng.gen_range(1..=5); + let highlight_count = rng.random_range(1..=5); for _ in 0..highlight_count { - let start_row = rng.gen_range(0..100); - let start_column = rng.gen_range(0..100); - let end_row = rng.gen_range(0..100); - let end_column = rng.gen_range(0..100); + let start_row = rng.random_range(0..100); + let start_column = rng.random_range(0..100); + let end_row = rng.random_range(0..100); + let end_column = rng.random_range(0..100); let start = PointUtf16::new(start_row, start_column); let end = PointUtf16::new(end_row, end_column); let range = @@ -1219,8 +1219,8 @@ impl RandomizedTest for ProjectCollaborationTest { guest_project.remote_id(), ); assert_eq!( - guest_snapshot.entries(false, 0).collect::>(), - host_snapshot.entries(false, 0).collect::>(), + guest_snapshot.entries(false, 0).map(null_out_entry_size).collect::>(), + host_snapshot.entries(false, 0).map(null_out_entry_size).collect::>(), "{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}", client.username, host_snapshot.abs_path(), @@ -1248,6 +1248,18 @@ impl RandomizedTest for ProjectCollaborationTest { ); } }); + + // A hack to work around a hack in + // https://github.com/zed-industries/zed/pull/16696 that wasn't + // detected until we upgraded the rng crate. This whole crate is + // going away with DeltaDB soon, so we hold our nose and + // continue. + fn null_out_entry_size(entry: &project::Entry) -> project::Entry { + project::Entry { + size: 0, + ..entry.clone() + } + } } let buffers = client.buffers().clone(); @@ -1422,7 +1434,7 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation .filter(|path| path.starts_with(repo_path)) .collect::>(); - let count = rng.gen_range(0..=paths.len()); + let count = rng.random_range(0..=paths.len()); paths.shuffle(rng); paths.truncate(count); @@ -1434,13 +1446,13 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation let repo_path = client.fs().directories(false).choose(rng).unwrap().clone(); - match rng.gen_range(0..100_u32) { + match rng.random_range(0..100_u32) { 0..=25 => { let file_paths = generate_file_paths(&repo_path, rng, client); let contents = file_paths .into_iter() - .map(|path| (path, Alphanumeric.sample_string(rng, 16))) + .map(|path| (path, distr::Alphanumeric.sample_string(rng, 16))) .collect(); GitOperation::WriteGitIndex { @@ -1449,7 +1461,8 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation } } 26..=63 => { - let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8)); + let new_branch = + (rng.random_range(0..10) > 3).then(|| distr::Alphanumeric.sample_string(rng, 8)); GitOperation::WriteGitBranch { repo_path, @@ -1596,7 +1609,7 @@ fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option String { let mut name = String::new(); for _ in 0..10 { - let letter = rng.gen_range('a'..='z'); + let letter = rng.random_range('a'..='z'); name.push(letter); } name @@ -1604,7 +1617,7 @@ fn gen_file_name(rng: &mut StdRng) -> String { fn gen_status(rng: &mut StdRng) -> FileStatus { fn gen_tracked_status(rng: &mut StdRng) -> TrackedStatus { - match rng.gen_range(0..3) { + match rng.random_range(0..3) { 0 => TrackedStatus { index_status: StatusCode::Unmodified, worktree_status: StatusCode::Unmodified, @@ -1626,7 +1639,7 @@ fn gen_status(rng: &mut StdRng) -> FileStatus { } fn gen_unmerged_status_code(rng: &mut StdRng) -> UnmergedStatusCode { - match rng.gen_range(0..3) { + match rng.random_range(0..3) { 0 => UnmergedStatusCode::Updated, 1 => UnmergedStatusCode::Added, 2 => UnmergedStatusCode::Deleted, @@ -1634,7 +1647,7 @@ fn gen_status(rng: &mut StdRng) -> FileStatus { } } - match rng.gen_range(0..2) { + match rng.random_range(0..2) { 0 => FileStatus::Unmerged(UnmergedStatus { first_head: gen_unmerged_status_code(rng), second_head: gen_unmerged_status_code(rng), diff --git a/crates/collab/src/tests/randomized_test_helpers.rs b/crates/collab/src/tests/randomized_test_helpers.rs index d6c299a6a9ed4e0439573e9b33fabe8ff122963d..9a372017e34f575f780d56f3936fefec832e160c 100644 --- a/crates/collab/src/tests/randomized_test_helpers.rs +++ b/crates/collab/src/tests/randomized_test_helpers.rs @@ -208,9 +208,9 @@ pub fn save_randomized_test_plan() { impl TestPlan { pub async fn new(server: &mut TestServer, mut rng: StdRng) -> Arc> { - let allow_server_restarts = rng.gen_bool(0.7); - let allow_client_reconnection = rng.gen_bool(0.7); - let allow_client_disconnection = rng.gen_bool(0.1); + let allow_server_restarts = rng.random_bool(0.7); + let allow_client_reconnection = rng.random_bool(0.7); + let allow_client_disconnection = rng.random_bool(0.1); let mut users = Vec::new(); for ix in 0..max_peers() { @@ -407,7 +407,7 @@ impl TestPlan { } Some(loop { - break match self.rng.gen_range(0..100) { + break match self.rng.random_range(0..100) { 0..=29 if clients.len() < self.users.len() => { let user = self .users @@ -421,13 +421,13 @@ impl TestPlan { } } 30..=34 if clients.len() > 1 && self.allow_client_disconnection => { - let (client, cx) = &clients[self.rng.gen_range(0..clients.len())]; + let (client, cx) = &clients[self.rng.random_range(0..clients.len())]; let user_id = client.current_user_id(cx); self.operation_ix += 1; ServerOperation::RemoveConnection { user_id } } 35..=39 if clients.len() > 1 && self.allow_client_reconnection => { - let (client, cx) = &clients[self.rng.gen_range(0..clients.len())]; + let (client, cx) = &clients[self.rng.random_range(0..clients.len())]; let user_id = client.current_user_id(cx); self.operation_ix += 1; ServerOperation::BounceConnection { user_id } @@ -439,12 +439,12 @@ impl TestPlan { _ if !clients.is_empty() => { let count = self .rng - .gen_range(1..10) + .random_range(1..10) .min(self.max_operations - self.operation_ix); let batch_id = util::post_inc(&mut self.next_batch_id); let mut user_ids = (0..count) .map(|_| { - let ix = self.rng.gen_range(0..clients.len()); + let ix = self.rng.random_range(0..clients.len()); let (client, cx) = &clients[ix]; client.current_user_id(cx) }) @@ -453,7 +453,7 @@ impl TestPlan { ServerOperation::MutateClients { user_ids, batch_id, - quiesce: self.rng.gen_bool(0.7), + quiesce: self.rng.random_bool(0.7), } } _ => continue, diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index fdca32520d1e08d562ac6f533968c146b5ec0673..6a8baecdb3513754683cc748717bb94e863df509 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -682,7 +682,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng Default::default(); for _ in 0..operations { - match rng.gen_range(0..100) { + match rng.random_range(0..100) { // language server completes its diagnostic check 0..=20 if !updated_language_servers.is_empty() => { let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap(); @@ -691,7 +691,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng lsp_store.disk_based_diagnostics_finished(server_id, cx) }); - if rng.gen_bool(0.5) { + if rng.random_bool(0.5) { cx.run_until_parked(); } } @@ -701,7 +701,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng let (path, server_id, diagnostics) = match current_diagnostics.iter_mut().choose(&mut rng) { // update existing set of diagnostics - Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => { + Some(((path, server_id), diagnostics)) if rng.random_bool(0.5) => { (path.clone(), *server_id, diagnostics) } @@ -709,13 +709,13 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng _ => { let path: PathBuf = format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into(); - let len = rng.gen_range(128..256); + let len = rng.random_range(128..256); let content = RandomCharIter::new(&mut rng).take(len).collect::(); fs.insert_file(&path, content.into_bytes()).await; let server_id = match language_server_ids.iter().choose(&mut rng) { - Some(server_id) if rng.gen_bool(0.5) => *server_id, + Some(server_id) if rng.random_bool(0.5) => *server_id, _ => { let id = LanguageServerId(language_server_ids.len()); language_server_ids.push(id); @@ -846,7 +846,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S let mut next_inlay_id = 0; for _ in 0..operations { - match rng.gen_range(0..100) { + match rng.random_range(0..100) { // language server completes its diagnostic check 0..=20 if !updated_language_servers.is_empty() => { let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap(); @@ -855,7 +855,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S lsp_store.disk_based_diagnostics_finished(server_id, cx) }); - if rng.gen_bool(0.5) { + if rng.random_bool(0.5) { cx.run_until_parked(); } } @@ -864,7 +864,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S diagnostics.editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(window, cx); if !snapshot.buffer_snapshot.is_empty() { - let position = rng.gen_range(0..snapshot.buffer_snapshot.len()); + let position = rng.random_range(0..snapshot.buffer_snapshot.len()); let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left); log::info!( "adding inlay at {position}/{}: {:?}", @@ -890,7 +890,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S let (path, server_id, diagnostics) = match current_diagnostics.iter_mut().choose(&mut rng) { // update existing set of diagnostics - Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => { + Some(((path, server_id), diagnostics)) if rng.random_bool(0.5) => { (path.clone(), *server_id, diagnostics) } @@ -898,13 +898,13 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S _ => { let path: PathBuf = format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into(); - let len = rng.gen_range(128..256); + let len = rng.random_range(128..256); let content = RandomCharIter::new(&mut rng).take(len).collect::(); fs.insert_file(&path, content.into_bytes()).await; let server_id = match language_server_ids.iter().choose(&mut rng) { - Some(server_id) if rng.gen_bool(0.5) => *server_id, + Some(server_id) if rng.random_bool(0.5) => *server_id, _ => { let id = LanguageServerId(language_server_ids.len()); language_server_ids.push(id); @@ -1589,10 +1589,10 @@ fn randomly_update_diagnostics_for_path( next_id: &mut usize, rng: &mut impl Rng, ) { - let mutation_count = rng.gen_range(1..=3); + let mutation_count = rng.random_range(1..=3); for _ in 0..mutation_count { - if rng.gen_bool(0.3) && !diagnostics.is_empty() { - let idx = rng.gen_range(0..diagnostics.len()); + if rng.random_bool(0.3) && !diagnostics.is_empty() { + let idx = rng.random_range(0..diagnostics.len()); log::info!(" removing diagnostic at index {idx}"); diagnostics.remove(idx); } else { @@ -1601,7 +1601,7 @@ fn randomly_update_diagnostics_for_path( let new_diagnostic = random_lsp_diagnostic(rng, fs, path, unique_id); - let ix = rng.gen_range(0..=diagnostics.len()); + let ix = rng.random_range(0..=diagnostics.len()); log::info!( " inserting {} at index {ix}. {},{}..{},{}", new_diagnostic.message, @@ -1638,8 +1638,8 @@ fn random_lsp_diagnostic( let file_content = fs.read_file_sync(path).unwrap(); let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref()); - let start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN)); - let end = rng.gen_range(start..file_text.len().saturating_add(ERROR_MARGIN)); + let start = rng.random_range(0..file_text.len().saturating_add(ERROR_MARGIN)); + let end = rng.random_range(start..file_text.len().saturating_add(ERROR_MARGIN)); let start_point = file_text.offset_to_point_utf16(start); let end_point = file_text.offset_to_point_utf16(end); @@ -1649,7 +1649,7 @@ fn random_lsp_diagnostic( lsp::Position::new(end_point.row, end_point.column), ); - let severity = if rng.gen_bool(0.5) { + let severity = if rng.random_bool(0.5) { Some(lsp::DiagnosticSeverity::ERROR) } else { Some(lsp::DiagnosticSeverity::WARNING) @@ -1657,13 +1657,14 @@ fn random_lsp_diagnostic( let message = format!("diagnostic {unique_id}"); - let related_information = if rng.gen_bool(0.3) { - let info_count = rng.gen_range(1..=3); + let related_information = if rng.random_bool(0.3) { + let info_count = rng.random_range(1..=3); let mut related_info = Vec::with_capacity(info_count); for i in 0..info_count { - let info_start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN)); - let info_end = rng.gen_range(info_start..file_text.len().saturating_add(ERROR_MARGIN)); + let info_start = rng.random_range(0..file_text.len().saturating_add(ERROR_MARGIN)); + let info_end = + rng.random_range(info_start..file_text.len().saturating_add(ERROR_MARGIN)); let info_start_point = file_text.offset_to_point_utf16(info_start); let info_end_point = file_text.offset_to_point_utf16(info_end); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index c16e4a6ddbb971b44d71421d6ad868e6423eb035..3a07ee45aff60a7ffc28e76ce5f7d4f79641d4b2 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1552,15 +1552,15 @@ pub mod tests { .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); - let mut tab_size = rng.gen_range(1..=4); - let buffer_start_excerpt_header_height = rng.gen_range(1..=5); - let excerpt_header_height = rng.gen_range(1..=5); + let mut tab_size = rng.random_range(1..=4); + let buffer_start_excerpt_header_height = rng.random_range(1..=5); + let excerpt_header_height = rng.random_range(1..=5); let font_size = px(14.0); let max_wrap_width = 300.0; - let mut wrap_width = if rng.gen_bool(0.1) { + let mut wrap_width = if rng.random_bool(0.1) { None } else { - Some(px(rng.gen_range(0.0..=max_wrap_width))) + Some(px(rng.random_range(0.0..=max_wrap_width))) }; log::info!("tab size: {}", tab_size); @@ -1571,8 +1571,8 @@ pub mod tests { }); let buffer = cx.update(|cx| { - if rng.r#gen() { - let len = rng.gen_range(0..10); + if rng.random() { + let len = rng.random_range(0..10); let text = util::RandomCharIter::new(&mut rng) .take(len) .collect::(); @@ -1609,12 +1609,12 @@ pub mod tests { log::info!("display text: {:?}", snapshot.text()); for _i in 0..operations { - match rng.gen_range(0..100) { + match rng.random_range(0..100) { 0..=19 => { - wrap_width = if rng.gen_bool(0.2) { + wrap_width = if rng.random_bool(0.2) { None } else { - Some(px(rng.gen_range(0.0..=max_wrap_width))) + Some(px(rng.random_range(0.0..=max_wrap_width))) }; log::info!("setting wrap width to {:?}", wrap_width); map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); @@ -1634,28 +1634,27 @@ pub mod tests { } 30..=44 => { map.update(cx, |map, cx| { - if rng.r#gen() || blocks.is_empty() { + if rng.random() || blocks.is_empty() { let buffer = map.snapshot(cx).buffer_snapshot; - let block_properties = (0..rng.gen_range(1..=1)) + let block_properties = (0..rng.random_range(1..=1)) .map(|_| { - let position = - buffer.anchor_after(buffer.clip_offset( - rng.gen_range(0..=buffer.len()), - Bias::Left, - )); + let position = buffer.anchor_after(buffer.clip_offset( + rng.random_range(0..=buffer.len()), + Bias::Left, + )); - let placement = if rng.r#gen() { + let placement = if rng.random() { BlockPlacement::Above(position) } else { BlockPlacement::Below(position) }; - let height = rng.gen_range(1..5); + let height = rng.random_range(1..5); log::info!( "inserting block {:?} with height {}", placement.as_ref().map(|p| p.to_point(&buffer)), height ); - let priority = rng.gen_range(1..100); + let priority = rng.random_range(1..100); BlockProperties { placement, style: BlockStyle::Fixed, @@ -1668,9 +1667,9 @@ pub mod tests { blocks.extend(map.insert_blocks(block_properties, cx)); } else { blocks.shuffle(&mut rng); - let remove_count = rng.gen_range(1..=4.min(blocks.len())); + let remove_count = rng.random_range(1..=4.min(blocks.len())); let block_ids_to_remove = (0..remove_count) - .map(|_| blocks.remove(rng.gen_range(0..blocks.len()))) + .map(|_| blocks.remove(rng.random_range(0..blocks.len()))) .collect(); log::info!("removing block ids {:?}", block_ids_to_remove); map.remove_blocks(block_ids_to_remove, cx); @@ -1679,16 +1678,16 @@ pub mod tests { } 45..=79 => { let mut ranges = Vec::new(); - for _ in 0..rng.gen_range(1..=3) { + for _ in 0..rng.random_range(1..=3) { buffer.read_with(cx, |buffer, cx| { let buffer = buffer.read(cx); - let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); - let start = buffer.clip_offset(rng.gen_range(0..=end), Left); + let end = buffer.clip_offset(rng.random_range(0..=buffer.len()), Right); + let start = buffer.clip_offset(rng.random_range(0..=end), Left); ranges.push(start..end); }); } - if rng.r#gen() && fold_count > 0 { + if rng.random() && fold_count > 0 { log::info!("unfolding ranges: {:?}", ranges); map.update(cx, |map, cx| { map.unfold_intersecting(ranges, true, cx); @@ -1727,8 +1726,8 @@ pub mod tests { // Line boundaries let buffer = &snapshot.buffer_snapshot; for _ in 0..5 { - let row = rng.gen_range(0..=buffer.max_point().row); - let column = rng.gen_range(0..=buffer.line_len(MultiBufferRow(row))); + let row = rng.random_range(0..=buffer.max_point().row); + let column = rng.random_range(0..=buffer.line_len(MultiBufferRow(row))); let point = buffer.clip_point(Point::new(row, column), Left); let (prev_buffer_bound, prev_display_bound) = snapshot.prev_line_boundary(point); @@ -1776,8 +1775,8 @@ pub mod tests { let min_point = snapshot.clip_point(DisplayPoint::new(DisplayRow(0), 0), Left); let max_point = snapshot.clip_point(snapshot.max_point(), Right); for _ in 0..5 { - let row = rng.gen_range(0..=snapshot.max_point().row().0); - let column = rng.gen_range(0..=snapshot.line_len(DisplayRow(row))); + let row = rng.random_range(0..=snapshot.max_point().row().0); + let column = rng.random_range(0..=snapshot.line_len(DisplayRow(row))); let point = snapshot.clip_point(DisplayPoint::new(DisplayRow(row), column), Left); log::info!("Moving from point {:?}", point); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index b073fe7be75c82754de6ca7773b68073b213c49c..de734e5ea62f23d2396fb03393c32e55d0e1fc7b 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -128,10 +128,10 @@ impl BlockPlacement { } } - fn sort_order(&self) -> u8 { + fn tie_break(&self) -> u8 { match self { - BlockPlacement::Above(_) => 0, - BlockPlacement::Replace(_) => 1, + BlockPlacement::Replace(_) => 0, + BlockPlacement::Above(_) => 1, BlockPlacement::Near(_) => 2, BlockPlacement::Below(_) => 3, } @@ -143,7 +143,7 @@ impl BlockPlacement { self.start() .cmp(other.start(), buffer) .then_with(|| other.end().cmp(self.end(), buffer)) - .then_with(|| self.sort_order().cmp(&other.sort_order())) + .then_with(|| self.tie_break().cmp(&other.tie_break())) } fn to_wrap_row(&self, wrap_snapshot: &WrapSnapshot) -> Option> { @@ -847,6 +847,7 @@ impl BlockMap { .start() .cmp(placement_b.start()) .then_with(|| placement_b.end().cmp(placement_a.end())) + .then_with(|| placement_a.tie_break().cmp(&placement_b.tie_break())) .then_with(|| { if block_a.is_header() { Ordering::Less @@ -856,7 +857,6 @@ impl BlockMap { Ordering::Equal } }) - .then_with(|| placement_a.sort_order().cmp(&placement_b.sort_order())) .then_with(|| match (block_a, block_b) { ( Block::ExcerptBoundary { @@ -2922,21 +2922,21 @@ mod tests { .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); - let wrap_width = if rng.gen_bool(0.2) { + let wrap_width = if rng.random_bool(0.2) { None } else { - Some(px(rng.gen_range(0.0..=100.0))) + Some(px(rng.random_range(0.0..=100.0))) }; let tab_size = 1.try_into().unwrap(); let font_size = px(14.0); - let buffer_start_header_height = rng.gen_range(1..=5); - let excerpt_header_height = rng.gen_range(1..=5); + let buffer_start_header_height = rng.random_range(1..=5); + let excerpt_header_height = rng.random_range(1..=5); log::info!("Wrap width: {:?}", wrap_width); log::info!("Excerpt Header Height: {:?}", excerpt_header_height); - let is_singleton = rng.r#gen(); + let is_singleton = rng.random(); let buffer = if is_singleton { - let len = rng.gen_range(0..10); + let len = rng.random_range(0..10); let text = RandomCharIter::new(&mut rng).take(len).collect::(); log::info!("initial singleton buffer text: {:?}", text); cx.update(|cx| MultiBuffer::build_simple(&text, cx)) @@ -2966,30 +2966,30 @@ mod tests { for _ in 0..operations { let mut buffer_edits = Vec::new(); - match rng.gen_range(0..=100) { + match rng.random_range(0..=100) { 0..=19 => { - let wrap_width = if rng.gen_bool(0.2) { + let wrap_width = if rng.random_bool(0.2) { None } else { - Some(px(rng.gen_range(0.0..=100.0))) + Some(px(rng.random_range(0.0..=100.0))) }; log::info!("Setting wrap width to {:?}", wrap_width); wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); } 20..=39 => { - let block_count = rng.gen_range(1..=5); + let block_count = rng.random_range(1..=5); let block_properties = (0..block_count) .map(|_| { let buffer = cx.update(|cx| buffer.read(cx).read(cx).clone()); let offset = - buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left); + buffer.clip_offset(rng.random_range(0..=buffer.len()), Bias::Left); let mut min_height = 0; - let placement = match rng.gen_range(0..3) { + let placement = match rng.random_range(0..3) { 0 => { min_height = 1; let start = buffer.anchor_after(offset); let end = buffer.anchor_after(buffer.clip_offset( - rng.gen_range(offset..=buffer.len()), + rng.random_range(offset..=buffer.len()), Bias::Left, )); BlockPlacement::Replace(start..=end) @@ -2998,7 +2998,7 @@ mod tests { _ => BlockPlacement::Below(buffer.anchor_after(offset)), }; - let height = rng.gen_range(min_height..5); + let height = rng.random_range(min_height..5); BlockProperties { style: BlockStyle::Fixed, placement, @@ -3040,7 +3040,7 @@ mod tests { } } 40..=59 if !block_map.custom_blocks.is_empty() => { - let block_count = rng.gen_range(1..=4.min(block_map.custom_blocks.len())); + let block_count = rng.random_range(1..=4.min(block_map.custom_blocks.len())); let block_ids_to_remove = block_map .custom_blocks .choose_multiple(&mut rng, block_count) @@ -3095,8 +3095,8 @@ mod tests { let mut folded_count = folded_buffers.len(); let mut unfolded_count = unfolded_buffers.len(); - let fold = !unfolded_buffers.is_empty() && rng.gen_bool(0.5); - let unfold = !folded_buffers.is_empty() && rng.gen_bool(0.5); + let fold = !unfolded_buffers.is_empty() && rng.random_bool(0.5); + let unfold = !folded_buffers.is_empty() && rng.random_bool(0.5); if !fold && !unfold { log::info!( "Noop fold/unfold operation. Unfolded buffers: {unfolded_count}, folded buffers: {folded_count}" @@ -3107,7 +3107,7 @@ mod tests { buffer.update(cx, |buffer, cx| { if fold { let buffer_to_fold = - unfolded_buffers[rng.gen_range(0..unfolded_buffers.len())]; + unfolded_buffers[rng.random_range(0..unfolded_buffers.len())]; log::info!("Folding {buffer_to_fold:?}"); let related_excerpts = buffer_snapshot .excerpts() @@ -3133,7 +3133,7 @@ mod tests { } if unfold { let buffer_to_unfold = - folded_buffers[rng.gen_range(0..folded_buffers.len())]; + folded_buffers[rng.random_range(0..folded_buffers.len())]; log::info!("Unfolding {buffer_to_unfold:?}"); unfolded_count += 1; folded_count -= 1; @@ -3146,7 +3146,7 @@ mod tests { } _ => { buffer.update(cx, |buffer, cx| { - let mutation_count = rng.gen_range(1..=5); + let mutation_count = rng.random_range(1..=5); let subscription = buffer.subscribe(); buffer.randomly_mutate(&mut rng, mutation_count, cx); buffer_snapshot = buffer.snapshot(cx); @@ -3331,7 +3331,7 @@ mod tests { ); for start_row in 0..expected_row_count { - let end_row = rng.gen_range(start_row + 1..=expected_row_count); + let end_row = rng.random_range(start_row + 1..=expected_row_count); let mut expected_text = expected_lines[start_row..end_row].join("\n"); if end_row < expected_row_count { expected_text.push('\n'); @@ -3426,8 +3426,8 @@ mod tests { ); for _ in 0..10 { - let end_row = rng.gen_range(1..=expected_lines.len()); - let start_row = rng.gen_range(0..end_row); + let end_row = rng.random_range(1..=expected_lines.len()); + let start_row = rng.random_range(0..end_row); let mut expected_longest_rows_in_range = vec![]; let mut longest_line_len_in_range = 0; diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 42f46fb74969301007d19032f1b96377d141a724..6d160d0d6d58dbeeac89749aeabcedef6010c1c3 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1771,9 +1771,9 @@ mod tests { .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); - let len = rng.gen_range(0..10); + let len = rng.random_range(0..10); let text = RandomCharIter::new(&mut rng).take(len).collect::(); - let buffer = if rng.r#gen() { + let buffer = if rng.random() { MultiBuffer::build_simple(&text, cx) } else { MultiBuffer::build_random(&mut rng, cx) @@ -1790,7 +1790,7 @@ mod tests { log::info!("text: {:?}", buffer_snapshot.text()); let mut buffer_edits = Vec::new(); let mut inlay_edits = Vec::new(); - match rng.gen_range(0..=100) { + match rng.random_range(0..=100) { 0..=39 => { snapshot_edits.extend(map.randomly_mutate(&mut rng)); } @@ -1800,7 +1800,7 @@ mod tests { } _ => buffer.update(cx, |buffer, cx| { let subscription = buffer.subscribe(); - let edit_count = rng.gen_range(1..=5); + let edit_count = rng.random_range(1..=5); buffer.randomly_mutate(&mut rng, edit_count, cx); buffer_snapshot = buffer.snapshot(cx); let edits = subscription.consume().into_inner(); @@ -1917,10 +1917,14 @@ mod tests { } for _ in 0..5 { - let mut start = snapshot - .clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Left); - let mut end = snapshot - .clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Right); + let mut start = snapshot.clip_offset( + FoldOffset(rng.random_range(0..=snapshot.len().0)), + Bias::Left, + ); + let mut end = snapshot.clip_offset( + FoldOffset(rng.random_range(0..=snapshot.len().0)), + Bias::Right, + ); if start > end { mem::swap(&mut start, &mut end); } @@ -1975,8 +1979,8 @@ mod tests { for _ in 0..5 { let end = - buffer_snapshot.clip_offset(rng.gen_range(0..=buffer_snapshot.len()), Right); - let start = buffer_snapshot.clip_offset(rng.gen_range(0..=end), Left); + buffer_snapshot.clip_offset(rng.random_range(0..=buffer_snapshot.len()), Right); + let start = buffer_snapshot.clip_offset(rng.random_range(0..=end), Left); let expected_folds = map .snapshot .folds @@ -2001,10 +2005,10 @@ mod tests { let text = snapshot.text(); for _ in 0..5 { - let start_row = rng.gen_range(0..=snapshot.max_point().row()); - let start_column = rng.gen_range(0..=snapshot.line_len(start_row)); - let end_row = rng.gen_range(0..=snapshot.max_point().row()); - let end_column = rng.gen_range(0..=snapshot.line_len(end_row)); + let start_row = rng.random_range(0..=snapshot.max_point().row()); + let start_column = rng.random_range(0..=snapshot.line_len(start_row)); + let end_row = rng.random_range(0..=snapshot.max_point().row()); + let end_column = rng.random_range(0..=snapshot.line_len(end_row)); let mut start = snapshot.clip_point(FoldPoint::new(start_row, start_column), Bias::Left); let mut end = snapshot.clip_point(FoldPoint::new(end_row, end_column), Bias::Right); @@ -2109,17 +2113,17 @@ mod tests { rng: &mut impl Rng, ) -> Vec<(FoldSnapshot, Vec)> { let mut snapshot_edits = Vec::new(); - match rng.gen_range(0..=100) { + match rng.random_range(0..=100) { 0..=39 if !self.snapshot.folds.is_empty() => { let inlay_snapshot = self.snapshot.inlay_snapshot.clone(); let buffer = &inlay_snapshot.buffer; let mut to_unfold = Vec::new(); - for _ in 0..rng.gen_range(1..=3) { - let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); - let start = buffer.clip_offset(rng.gen_range(0..=end), Left); + for _ in 0..rng.random_range(1..=3) { + let end = buffer.clip_offset(rng.random_range(0..=buffer.len()), Right); + let start = buffer.clip_offset(rng.random_range(0..=end), Left); to_unfold.push(start..end); } - let inclusive = rng.r#gen(); + let inclusive = rng.random(); log::info!("unfolding {:?} (inclusive: {})", to_unfold, inclusive); let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]); snapshot_edits.push((snapshot, edits)); @@ -2130,9 +2134,9 @@ mod tests { let inlay_snapshot = self.snapshot.inlay_snapshot.clone(); let buffer = &inlay_snapshot.buffer; let mut to_fold = Vec::new(); - for _ in 0..rng.gen_range(1..=2) { - let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); - let start = buffer.clip_offset(rng.gen_range(0..=end), Left); + for _ in 0..rng.random_range(1..=2) { + let end = buffer.clip_offset(rng.random_range(0..=buffer.len()), Right); + let start = buffer.clip_offset(rng.random_range(0..=end), Left); to_fold.push((start..end, FoldPlaceholder::test())); } log::info!("folding {:?}", to_fold); diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 3db9d10fdc74f418ecd4ea682dde91185130cd46..e00ffdbf2c6ed7ee7288b2371481cb1f1b28bc92 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -719,14 +719,18 @@ impl InlayMap { let mut to_remove = Vec::new(); let mut to_insert = Vec::new(); let snapshot = &mut self.snapshot; - for i in 0..rng.gen_range(1..=5) { - if self.inlays.is_empty() || rng.r#gen() { + for i in 0..rng.random_range(1..=5) { + if self.inlays.is_empty() || rng.random() { let position = snapshot.buffer.random_byte_range(0, rng).start; - let bias = if rng.r#gen() { Bias::Left } else { Bias::Right }; - let len = if rng.gen_bool(0.01) { + let bias = if rng.random() { + Bias::Left + } else { + Bias::Right + }; + let len = if rng.random_bool(0.01) { 0 } else { - rng.gen_range(1..=5) + rng.random_range(1..=5) }; let text = util::RandomCharIter::new(&mut *rng) .filter(|ch| *ch != '\r') @@ -1665,8 +1669,8 @@ mod tests { .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); - let len = rng.gen_range(0..30); - let buffer = if rng.r#gen() { + let len = rng.random_range(0..30); + let buffer = if rng.random() { let text = util::RandomCharIter::new(&mut rng) .take(len) .collect::(); @@ -1683,7 +1687,7 @@ mod tests { let mut prev_inlay_text = inlay_snapshot.text(); let mut buffer_edits = Vec::new(); - match rng.gen_range(0..=100) { + match rng.random_range(0..=100) { 0..=50 => { let (snapshot, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); log::info!("mutated text: {:?}", snapshot.text()); @@ -1691,7 +1695,7 @@ mod tests { } _ => buffer.update(cx, |buffer, cx| { let subscription = buffer.subscribe(); - let edit_count = rng.gen_range(1..=5); + let edit_count = rng.random_range(1..=5); buffer.randomly_mutate(&mut rng, edit_count, cx); buffer_snapshot = buffer.snapshot(cx); let edits = subscription.consume().into_inner(); @@ -1740,7 +1744,7 @@ mod tests { } let mut text_highlights = TextHighlights::default(); - let text_highlight_count = rng.gen_range(0_usize..10); + let text_highlight_count = rng.random_range(0_usize..10); let mut text_highlight_ranges = (0..text_highlight_count) .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) .collect::>(); @@ -1762,10 +1766,10 @@ mod tests { let mut inlay_highlights = InlayHighlights::default(); if !inlays.is_empty() { - let inlay_highlight_count = rng.gen_range(0..inlays.len()); + let inlay_highlight_count = rng.random_range(0..inlays.len()); let mut inlay_indices = BTreeSet::default(); while inlay_indices.len() < inlay_highlight_count { - inlay_indices.insert(rng.gen_range(0..inlays.len())); + inlay_indices.insert(rng.random_range(0..inlays.len())); } let new_highlights = TreeMap::from_ordered_entries( inlay_indices @@ -1782,8 +1786,8 @@ mod tests { }), n => { let inlay_text = inlay.text.to_string(); - let mut highlight_end = rng.gen_range(1..n); - let mut highlight_start = rng.gen_range(0..highlight_end); + let mut highlight_end = rng.random_range(1..n); + let mut highlight_start = rng.random_range(0..highlight_end); while !inlay_text.is_char_boundary(highlight_end) { highlight_end += 1; } @@ -1805,9 +1809,9 @@ mod tests { } for _ in 0..5 { - let mut end = rng.gen_range(0..=inlay_snapshot.len().0); + let mut end = rng.random_range(0..=inlay_snapshot.len().0); end = expected_text.clip_offset(end, Bias::Right); - let mut start = rng.gen_range(0..=end); + let mut start = rng.random_range(0..=end); start = expected_text.clip_offset(start, Bias::Right); let range = InlayOffset(start)..InlayOffset(end); diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 6f5df9bb8e658b95260dde4feb2b00c177c98520..523e777d9113b203dafbb5e151ba22a01394c956 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -736,9 +736,9 @@ mod tests { #[gpui::test(iterations = 100)] fn test_random_tabs(cx: &mut gpui::App, mut rng: StdRng) { - let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); - let len = rng.gen_range(0..30); - let buffer = if rng.r#gen() { + let tab_size = NonZeroU32::new(rng.random_range(1..=4)).unwrap(); + let len = rng.random_range(0..30); + let buffer = if rng.random() { let text = util::RandomCharIter::new(&mut rng) .take(len) .collect::(); @@ -769,11 +769,11 @@ mod tests { ); for _ in 0..5 { - let end_row = rng.gen_range(0..=text.max_point().row); - let end_column = rng.gen_range(0..=text.line_len(end_row)); + let end_row = rng.random_range(0..=text.max_point().row); + let end_column = rng.random_range(0..=text.line_len(end_row)); let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right)); - let start_row = rng.gen_range(0..=text.max_point().row); - let start_column = rng.gen_range(0..=text.line_len(start_row)); + let start_row = rng.random_range(0..=text.max_point().row); + let start_column = rng.random_range(0..=text.line_len(start_row)); let mut start = TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left)); if start > end { diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 500ec3a0bb77f8a8332e86485b81b357644e6d23..127293726a59d1945e8f9dcbfcd2eb3da0cc2290 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1215,12 +1215,12 @@ mod tests { .unwrap_or(10); let text_system = cx.read(|cx| cx.text_system().clone()); - let mut wrap_width = if rng.gen_bool(0.1) { + let mut wrap_width = if rng.random_bool(0.1) { None } else { - Some(px(rng.gen_range(0.0..=1000.0))) + Some(px(rng.random_range(0.0..=1000.0))) }; - let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); + let tab_size = NonZeroU32::new(rng.random_range(1..=4)).unwrap(); let font = test_font(); let _font_id = text_system.resolve_font(&font); @@ -1230,10 +1230,10 @@ mod tests { log::info!("Wrap width: {:?}", wrap_width); let buffer = cx.update(|cx| { - if rng.r#gen() { + if rng.random() { MultiBuffer::build_random(&mut rng, cx) } else { - let len = rng.gen_range(0..10); + let len = rng.random_range(0..10); let text = util::RandomCharIter::new(&mut rng) .take(len) .collect::(); @@ -1281,12 +1281,12 @@ mod tests { log::info!("{} ==============================================", _i); let mut buffer_edits = Vec::new(); - match rng.gen_range(0..=100) { + match rng.random_range(0..=100) { 0..=19 => { - wrap_width = if rng.gen_bool(0.2) { + wrap_width = if rng.random_bool(0.2) { None } else { - Some(px(rng.gen_range(0.0..=1000.0))) + Some(px(rng.random_range(0.0..=1000.0))) }; log::info!("Setting wrap width to {:?}", wrap_width); wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); @@ -1317,7 +1317,7 @@ mod tests { _ => { buffer.update(cx, |buffer, cx| { let subscription = buffer.subscribe(); - let edit_count = rng.gen_range(1..=5); + let edit_count = rng.random_range(1..=5); buffer.randomly_mutate(&mut rng, edit_count, cx); buffer_snapshot = buffer.snapshot(cx); buffer_edits.extend(subscription.consume()); @@ -1341,7 +1341,7 @@ mod tests { snapshot.verify_chunks(&mut rng); edits.push((snapshot, wrap_edits)); - if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.random_bool(0.4) { log::info!("Waiting for wrapping to finish"); while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { notifications.next().await.unwrap(); @@ -1479,8 +1479,8 @@ mod tests { impl WrapSnapshot { fn verify_chunks(&mut self, rng: &mut impl Rng) { for _ in 0..5 { - let mut end_row = rng.gen_range(0..=self.max_point().row()); - let start_row = rng.gen_range(0..=end_row); + let mut end_row = rng.random_range(0..=self.max_point().row()); + let start_row = rng.random_range(0..=end_row); end_row += 1; let mut expected_text = self.text_chunks(start_row).collect::(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fe5b2f83c2034822d4f36d3b66bbcea3b6b7322c..37951074d15bbb8f34bcbaba9d839eae5d34cf1e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -164,7 +164,7 @@ use project::{ DiagnosticSeverity, GitGutterSetting, GoToDiagnosticSeverityFilter, ProjectSettings, }, }; -use rand::{seq::SliceRandom, thread_rng}; +use rand::seq::SliceRandom; use rpc::{ErrorCode, ErrorExt, proto::PeerId}; use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; use selections_collection::{ @@ -10971,7 +10971,7 @@ impl Editor { } pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context) { - self.manipulate_immutable_lines(window, cx, |lines| lines.shuffle(&mut thread_rng())) + self.manipulate_immutable_lines(window, cx, |lines| lines.shuffle(&mut rand::rng())) } fn manipulate_lines( diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 27a9b8870383b7f1136e31028bacedc8744e0650..51719048ef81cf273bc58e7d810d66d454a04805 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -1107,7 +1107,7 @@ mod tests { init_test(cx); let fs = FakeFs::new(cx.executor()); - let buffer_initial_text_len = rng.gen_range(5..15); + let buffer_initial_text_len = rng.random_range(5..15); let mut buffer_initial_text = Rope::from( RandomCharIter::new(&mut rng) .take(buffer_initial_text_len) @@ -1159,7 +1159,7 @@ mod tests { git_blame.update(cx, |blame, cx| blame.check_invariants(cx)); for _ in 0..operations { - match rng.gen_range(0..100) { + match rng.random_range(0..100) { 0..=19 => { log::info!("quiescing"); cx.executor().run_until_parked(); @@ -1202,8 +1202,8 @@ mod tests { let mut blame_entries = Vec::new(); for ix in 0..5 { if last_row < max_row { - let row_start = rng.gen_range(last_row..max_row); - let row_end = rng.gen_range(row_start + 1..cmp::min(row_start + 3, max_row) + 1); + let row_start = rng.random_range(last_row..max_row); + let row_end = rng.random_range(row_start + 1..cmp::min(row_start + 3, max_row) + 1); blame_entries.push(blame_entry(&ix.to_string(), row_start..row_end)); last_row = row_end; } else { diff --git a/crates/gpui/examples/data_table.rs b/crates/gpui/examples/data_table.rs index 5e82b08839de5f3b98ec3267b22a3bb8586fa02c..10e22828a8e8f5c8778cbcb06a087d4bdfac3adc 100644 --- a/crates/gpui/examples/data_table.rs +++ b/crates/gpui/examples/data_table.rs @@ -38,58 +38,58 @@ pub struct Quote { impl Quote { pub fn random() -> Self { use rand::Rng; - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // simulate a base price in a realistic range - let prev_close = rng.gen_range(100.0..200.0); - let change = rng.gen_range(-5.0..5.0); + let prev_close = rng.random_range(100.0..200.0); + let change = rng.random_range(-5.0..5.0); let last_done = prev_close + change; - let open = prev_close + rng.gen_range(-3.0..3.0); - let high = (prev_close + rng.gen_range::(0.0..10.0)).max(open); - let low = (prev_close - rng.gen_range::(0.0..10.0)).min(open); - let timestamp = Duration::from_secs(rng.gen_range(0..86400)); - let volume = rng.gen_range(1_000_000..100_000_000); + let open = prev_close + rng.random_range(-3.0..3.0); + let high = (prev_close + rng.random_range::(0.0..10.0)).max(open); + let low = (prev_close - rng.random_range::(0.0..10.0)).min(open); + let timestamp = Duration::from_secs(rng.random_range(0..86400)); + let volume = rng.random_range(1_000_000..100_000_000); let turnover = last_done * volume as f64; let symbol = { let mut ticker = String::new(); - if rng.gen_bool(0.5) { + if rng.random_bool(0.5) { ticker.push_str(&format!( "{:03}.{}", - rng.gen_range(100..1000), - rng.gen_range(0..10) + rng.random_range(100..1000), + rng.random_range(0..10) )); } else { ticker.push_str(&format!( "{}{}", - rng.gen_range('A'..='Z'), - rng.gen_range('A'..='Z') + rng.random_range('A'..='Z'), + rng.random_range('A'..='Z') )); } - ticker.push_str(&format!(".{}", rng.gen_range('A'..='Z'))); + ticker.push_str(&format!(".{}", rng.random_range('A'..='Z'))); ticker }; let name = format!( "{} {} - #{}", symbol, - rng.gen_range(1..100), - rng.gen_range(10000..100000) + rng.random_range(1..100), + rng.random_range(10000..100000) ); - let ttm = rng.gen_range(0.0..10.0); - let market_cap = rng.gen_range(1_000_000.0..10_000_000.0); - let float_cap = market_cap + rng.gen_range(1_000.0..10_000.0); - let shares = rng.gen_range(100.0..1000.0); + let ttm = rng.random_range(0.0..10.0); + let market_cap = rng.random_range(1_000_000.0..10_000_000.0); + let float_cap = market_cap + rng.random_range(1_000.0..10_000.0); + let shares = rng.random_range(100.0..1000.0); let pb = market_cap / shares; let pe = market_cap / shares; let eps = market_cap / shares; - let dividend = rng.gen_range(0.0..10.0); - let dividend_yield = rng.gen_range(0.0..10.0); - let dividend_per_share = rng.gen_range(0.0..10.0); + let dividend = rng.random_range(0.0..10.0); + let dividend_yield = rng.random_range(0.0..10.0); + let dividend_per_share = rng.random_range(0.0..10.0); let dividend_date = SharedString::new(format!( "{}-{}-{}", - rng.gen_range(2000..2023), - rng.gen_range(1..12), - rng.gen_range(1..28) + rng.random_range(2000..2023), + rng.random_range(1..12), + rng.random_range(1..28) )); - let dividend_payment = rng.gen_range(0.0..10.0); + let dividend_payment = rng.random_range(0.0..10.0); Self { name: name.into(), diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index c65c045f6bc16310d3772825147ad570f209fd99..b3d342b09bf1dceb27413d3ec24fbcc0d2f541e9 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -144,7 +144,7 @@ impl TestAppContext { /// Create a single TestAppContext, for non-multi-client tests pub fn single() -> Self { - let dispatcher = TestDispatcher::new(StdRng::from_entropy()); + let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0)); Self::build(dispatcher, None) } diff --git a/crates/gpui/src/bounds_tree.rs b/crates/gpui/src/bounds_tree.rs index 03f83b95035489bd86201c4d64c15f5a12ed50ea..a96bfe55b9ff431a96da7bf42692288264eb184c 100644 --- a/crates/gpui/src/bounds_tree.rs +++ b/crates/gpui/src/bounds_tree.rs @@ -309,12 +309,12 @@ mod tests { let mut expected_quads: Vec<(Bounds, u32)> = Vec::new(); // Insert a random number of random AABBs into the tree. - let num_bounds = rng.gen_range(1..=max_bounds); + let num_bounds = rng.random_range(1..=max_bounds); for _ in 0..num_bounds { - let min_x: f32 = rng.gen_range(-100.0..100.0); - let min_y: f32 = rng.gen_range(-100.0..100.0); - let width: f32 = rng.gen_range(0.0..50.0); - let height: f32 = rng.gen_range(0.0..50.0); + let min_x: f32 = rng.random_range(-100.0..100.0); + let min_y: f32 = rng.random_range(-100.0..100.0); + let width: f32 = rng.random_range(0.0..50.0); + let height: f32 = rng.random_range(0.0..50.0); let bounds = Bounds { origin: Point { x: min_x, y: min_y }, size: Size { width, height }, diff --git a/crates/gpui/src/platform/test/dispatcher.rs b/crates/gpui/src/platform/test/dispatcher.rs index 4ce62c4bdcae60d517dd88501cb89af8fee2c9bc..e19710effda9299c6eb72e8c4acc2f615ac077ee 100644 --- a/crates/gpui/src/platform/test/dispatcher.rs +++ b/crates/gpui/src/platform/test/dispatcher.rs @@ -118,7 +118,7 @@ impl TestDispatcher { } YieldNow { - count: self.state.lock().random.gen_range(0..10), + count: self.state.lock().random.random_range(0..10), } } @@ -151,11 +151,11 @@ impl TestDispatcher { if deprioritized_background_len == 0 { return false; } - let ix = state.random.gen_range(0..deprioritized_background_len); + let ix = state.random.random_range(0..deprioritized_background_len); main_thread = false; runnable = state.deprioritized_background.swap_remove(ix); } else { - main_thread = state.random.gen_ratio( + main_thread = state.random.random_ratio( foreground_len as u32, (foreground_len + background_len) as u32, ); @@ -170,7 +170,7 @@ impl TestDispatcher { .pop_front() .unwrap(); } else { - let ix = state.random.gen_range(0..background_len); + let ix = state.random.random_range(0..background_len); runnable = state.background.swap_remove(ix); }; }; @@ -241,7 +241,7 @@ impl TestDispatcher { pub fn gen_block_on_ticks(&self) -> usize { let mut lock = self.state.lock(); let block_on_ticks = lock.block_on_ticks.clone(); - lock.random.gen_range(block_on_ticks) + lock.random.random_range(block_on_ticks) } } diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 96db8077c4b7b139bf2c724a3502a6e4bd194f9f..4d0e6ea56f7d90f303f6634de1239a6a4542429a 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -94,7 +94,11 @@ impl WindowsPlatform { } let directx_devices = DirectXDevices::new().context("Creating DirectX devices")?; let (main_sender, main_receiver) = flume::unbounded::(); - let validation_number = rand::random::(); + let validation_number = if usize::BITS == 64 { + rand::random::() as usize + } else { + rand::random::() as usize + }; let raw_window_handles = Arc::new(RwLock::new(SmallVec::new())); let text_system = Arc::new( DirectWriteTextSystem::new(&directx_devices) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1f056aacc57338d65705e5b7f4bd91085c6142b4..c86787e1f9de8cf31037187dc667e2a7e428cea9 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2842,12 +2842,12 @@ impl Buffer { let new_start = last_end.map_or(0, |last_end| last_end + 1); let mut range = self.random_byte_range(new_start, rng); - if rng.gen_bool(0.2) { + if rng.random_bool(0.2) { mem::swap(&mut range.start, &mut range.end); } last_end = Some(range.end); - let new_text_len = rng.gen_range(0..10); + let new_text_len = rng.random_range(0..10); let mut new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect(); new_text = new_text.to_uppercase(); diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index ce65afa6288767766fa9a1da5c3a24f9ca86e580..5b88112c956e5466748fc349825a78f6232e540e 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -3013,7 +3013,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); - let base_text_len = rng.gen_range(0..10); + let base_text_len = rng.random_range(0..10); let base_text = RandomCharIter::new(&mut rng) .take(base_text_len) .collect::(); @@ -3022,7 +3022,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { let network = Arc::new(Mutex::new(Network::new(rng.clone()))); let base_buffer = cx.new(|cx| Buffer::local(base_text.as_str(), cx)); - for i in 0..rng.gen_range(min_peers..=max_peers) { + for i in 0..rng.random_range(min_peers..=max_peers) { let buffer = cx.new(|cx| { let state = base_buffer.read(cx).to_proto(cx); let ops = cx @@ -3035,7 +3035,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { .map(|op| proto::deserialize_operation(op).unwrap()), cx, ); - buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); + buffer.set_group_interval(Duration::from_millis(rng.random_range(0..=200))); let network = network.clone(); cx.subscribe(&cx.entity(), move |buffer, _, event, _| { if let BufferEvent::Operation { @@ -3066,11 +3066,11 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { let mut next_diagnostic_id = 0; let mut active_selections = BTreeMap::default(); loop { - let replica_index = rng.gen_range(0..replica_ids.len()); + let replica_index = rng.random_range(0..replica_ids.len()); let replica_id = replica_ids[replica_index]; let buffer = &mut buffers[replica_index]; let mut new_buffer = None; - match rng.gen_range(0..100) { + match rng.random_range(0..100) { 0..=29 if mutation_count != 0 => { buffer.update(cx, |buffer, cx| { buffer.start_transaction_at(now); @@ -3082,13 +3082,13 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { } 30..=39 if mutation_count != 0 => { buffer.update(cx, |buffer, cx| { - if rng.gen_bool(0.2) { + if rng.random_bool(0.2) { log::info!("peer {} clearing active selections", replica_id); active_selections.remove(&replica_id); buffer.remove_active_selections(cx); } else { let mut selections = Vec::new(); - for id in 0..rng.gen_range(1..=5) { + for id in 0..rng.random_range(1..=5) { let range = buffer.random_byte_range(0, &mut rng); selections.push(Selection { id, @@ -3111,7 +3111,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { mutation_count -= 1; } 40..=49 if mutation_count != 0 && replica_id == 0 => { - let entry_count = rng.gen_range(1..=5); + let entry_count = rng.random_range(1..=5); buffer.update(cx, |buffer, cx| { let diagnostics = DiagnosticSet::new( (0..entry_count).map(|_| { @@ -3166,7 +3166,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { new_buffer.replica_id(), new_buffer.text() ); - new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); + new_buffer.set_group_interval(Duration::from_millis(rng.random_range(0..=200))); let network = network.clone(); cx.subscribe(&cx.entity(), move |buffer, _, event, _| { if let BufferEvent::Operation { @@ -3238,7 +3238,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { _ => {} } - now += Duration::from_millis(rng.gen_range(0..=200)); + now += Duration::from_millis(rng.random_range(0..=200)); buffers.extend(new_buffer); for buffer in &buffers { @@ -3320,23 +3320,23 @@ fn test_trailing_whitespace_ranges(mut rng: StdRng) { // Generate a random multi-line string containing // some lines with trailing whitespace. let mut text = String::new(); - for _ in 0..rng.gen_range(0..16) { - for _ in 0..rng.gen_range(0..36) { - text.push(match rng.gen_range(0..10) { + for _ in 0..rng.random_range(0..16) { + for _ in 0..rng.random_range(0..36) { + text.push(match rng.random_range(0..10) { 0..=1 => ' ', 3 => '\t', - _ => rng.gen_range('a'..='z'), + _ => rng.random_range('a'..='z'), }); } text.push('\n'); } - match rng.gen_range(0..10) { + match rng.random_range(0..10) { // sometimes remove the last newline 0..=1 => drop(text.pop()), // // sometimes add extra newlines - 2..=3 => text.push_str(&"\n".repeat(rng.gen_range(1..5))), + 2..=3 => text.push_str(&"\n".repeat(rng.random_range(1..5))), _ => {} } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index a2f28215b4655b12095da96c033d23cb3f13eb77..4535d57d7747cbe747cf55a0a0f0cd30540e6af7 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -3580,7 +3580,7 @@ impl MultiBuffer { pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::App) -> Entity { cx.new(|cx| { let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); - let mutation_count = rng.gen_range(1..=5); + let mutation_count = rng.random_range(1..=5); multibuffer.randomly_edit_excerpts(rng, mutation_count, cx); multibuffer }) @@ -3603,16 +3603,17 @@ impl MultiBuffer { } let new_start = last_end.map_or(0, |last_end| last_end + 1); - let end = snapshot.clip_offset(rng.gen_range(new_start..=snapshot.len()), Bias::Right); - let start = snapshot.clip_offset(rng.gen_range(new_start..=end), Bias::Right); + let end = + snapshot.clip_offset(rng.random_range(new_start..=snapshot.len()), Bias::Right); + let start = snapshot.clip_offset(rng.random_range(new_start..=end), Bias::Right); last_end = Some(end); let mut range = start..end; - if rng.gen_bool(0.2) { + if rng.random_bool(0.2) { mem::swap(&mut range.start, &mut range.end); } - let new_text_len = rng.gen_range(0..10); + let new_text_len = rng.random_range(0..10); let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect(); edits.push((range, new_text.into())); @@ -3639,18 +3640,18 @@ impl MultiBuffer { let mut buffers = Vec::new(); for _ in 0..mutation_count { - if rng.gen_bool(0.05) { + if rng.random_bool(0.05) { log::info!("Clearing multi-buffer"); self.clear(cx); continue; - } else if rng.gen_bool(0.1) && !self.excerpt_ids().is_empty() { + } else if rng.random_bool(0.1) && !self.excerpt_ids().is_empty() { let ids = self.excerpt_ids(); let mut excerpts = HashSet::default(); - for _ in 0..rng.gen_range(0..ids.len()) { + for _ in 0..rng.random_range(0..ids.len()) { excerpts.extend(ids.choose(rng).copied()); } - let line_count = rng.gen_range(0..5); + let line_count = rng.random_range(0..5); log::info!("Expanding excerpts {excerpts:?} by {line_count} lines"); @@ -3664,8 +3665,8 @@ impl MultiBuffer { } let excerpt_ids = self.excerpt_ids(); - if excerpt_ids.is_empty() || (rng.r#gen() && excerpt_ids.len() < max_excerpts) { - let buffer_handle = if rng.r#gen() || self.buffers.borrow().is_empty() { + if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) { + let buffer_handle = if rng.random() || self.buffers.borrow().is_empty() { let text = RandomCharIter::new(&mut *rng).take(10).collect::(); buffers.push(cx.new(|cx| Buffer::local(text, cx))); let buffer = buffers.last().unwrap().read(cx); @@ -3687,11 +3688,11 @@ impl MultiBuffer { let buffer = buffer_handle.read(cx); let buffer_text = buffer.text(); - let ranges = (0..rng.gen_range(0..5)) + let ranges = (0..rng.random_range(0..5)) .map(|_| { let end_ix = - buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right); - let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + buffer.clip_offset(rng.random_range(0..=buffer.len()), Bias::Right); + let start_ix = buffer.clip_offset(rng.random_range(0..=end_ix), Bias::Left); ExcerptRange::new(start_ix..end_ix) }) .collect::>(); @@ -3708,7 +3709,7 @@ impl MultiBuffer { let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx); log::info!("Inserted with ids: {:?}", excerpt_id); } else { - let remove_count = rng.gen_range(1..=excerpt_ids.len()); + let remove_count = rng.random_range(1..=excerpt_ids.len()); let mut excerpts_to_remove = excerpt_ids .choose_multiple(rng, remove_count) .cloned() @@ -3730,7 +3731,7 @@ impl MultiBuffer { ) { use rand::prelude::*; - if rng.gen_bool(0.7) || self.singleton { + if rng.random_bool(0.7) || self.singleton { let buffer = self .buffers .borrow() @@ -3740,7 +3741,7 @@ impl MultiBuffer { if let Some(buffer) = buffer { buffer.update(cx, |buffer, cx| { - if rng.r#gen() { + if rng.random() { buffer.randomly_edit(rng, mutation_count, cx); } else { buffer.randomly_undo_redo(rng, cx); @@ -6388,8 +6389,8 @@ impl MultiBufferSnapshot { #[cfg(any(test, feature = "test-support"))] impl MultiBufferSnapshot { pub fn random_byte_range(&self, start_offset: usize, rng: &mut impl rand::Rng) -> Range { - let end = self.clip_offset(rng.gen_range(start_offset..=self.len()), Bias::Right); - let start = self.clip_offset(rng.gen_range(start_offset..=end), Bias::Right); + let end = self.clip_offset(rng.random_range(start_offset..=self.len()), Bias::Right); + let start = self.clip_offset(rng.random_range(start_offset..=end), Bias::Right); start..end } diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 61b4b0520f23ed50b3b36374710b52c78c37080f..efc622b0172a13ae9a6ad3bf366904706a36580f 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -2491,12 +2491,12 @@ async fn test_random_set_ranges(cx: &mut TestAppContext, mut rng: StdRng) { for _ in 0..operations { let snapshot = buf.update(cx, |buf, _| buf.snapshot()); - let num_ranges = rng.gen_range(0..=10); + let num_ranges = rng.random_range(0..=10); let max_row = snapshot.max_point().row; let mut ranges = (0..num_ranges) .map(|_| { - let start = rng.gen_range(0..max_row); - let end = rng.gen_range(start + 1..max_row + 1); + let start = rng.random_range(0..max_row); + let end = rng.random_range(start + 1..max_row + 1); Point::row_range(start..end) }) .collect::>(); @@ -2562,11 +2562,11 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { let mut needs_diff_calculation = false; for _ in 0..operations { - match rng.gen_range(0..100) { + match rng.random_range(0..100) { 0..=14 if !buffers.is_empty() => { let buffer = buffers.choose(&mut rng).unwrap(); buffer.update(cx, |buf, cx| { - let edit_count = rng.gen_range(1..5); + let edit_count = rng.random_range(1..5); buf.randomly_edit(&mut rng, edit_count, cx); log::info!("buffer text:\n{}", buf.text()); needs_diff_calculation = true; @@ -2577,11 +2577,11 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { multibuffer.update(cx, |multibuffer, cx| { let ids = multibuffer.excerpt_ids(); let mut excerpts = HashSet::default(); - for _ in 0..rng.gen_range(0..ids.len()) { + for _ in 0..rng.random_range(0..ids.len()) { excerpts.extend(ids.choose(&mut rng).copied()); } - let line_count = rng.gen_range(0..5); + let line_count = rng.random_range(0..5); let excerpt_ixs = excerpts .iter() @@ -2600,7 +2600,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { } 20..=29 if !reference.excerpts.is_empty() => { let mut ids_to_remove = vec![]; - for _ in 0..rng.gen_range(1..=3) { + for _ in 0..rng.random_range(1..=3) { let Some(excerpt) = reference.excerpts.choose(&mut rng) else { break; }; @@ -2620,8 +2620,12 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { let multibuffer = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); let offset = - multibuffer.clip_offset(rng.gen_range(0..=multibuffer.len()), Bias::Left); - let bias = if rng.r#gen() { Bias::Left } else { Bias::Right }; + multibuffer.clip_offset(rng.random_range(0..=multibuffer.len()), Bias::Left); + let bias = if rng.random() { + Bias::Left + } else { + Bias::Right + }; log::info!("Creating anchor at {} with bias {:?}", offset, bias); anchors.push(multibuffer.anchor_at(offset, bias)); anchors.sort_by(|a, b| a.cmp(b, &multibuffer)); @@ -2654,7 +2658,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { 45..=55 if !reference.excerpts.is_empty() => { multibuffer.update(cx, |multibuffer, cx| { let snapshot = multibuffer.snapshot(cx); - let excerpt_ix = rng.gen_range(0..reference.excerpts.len()); + let excerpt_ix = rng.random_range(0..reference.excerpts.len()); let excerpt = &reference.excerpts[excerpt_ix]; let start = excerpt.range.start; let end = excerpt.range.end; @@ -2691,7 +2695,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { }); } _ => { - let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) { + let buffer_handle = if buffers.is_empty() || rng.random_bool(0.4) { let mut base_text = util::RandomCharIter::new(&mut rng) .take(256) .collect::(); @@ -2708,7 +2712,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { buffers.choose(&mut rng).unwrap() }; - let prev_excerpt_ix = rng.gen_range(0..=reference.excerpts.len()); + let prev_excerpt_ix = rng.random_range(0..=reference.excerpts.len()); let prev_excerpt_id = reference .excerpts .get(prev_excerpt_ix) @@ -2716,8 +2720,8 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { let excerpt_ix = (prev_excerpt_ix + 1).min(reference.excerpts.len()); let (range, anchor_range) = buffer_handle.read_with(cx, |buffer, _| { - let end_row = rng.gen_range(0..=buffer.max_point().row); - let start_row = rng.gen_range(0..=end_row); + let end_row = rng.random_range(0..=buffer.max_point().row); + let start_row = rng.random_range(0..=end_row); let end_ix = buffer.point_to_offset(Point::new(end_row, 0)); let start_ix = buffer.point_to_offset(Point::new(start_row, 0)); let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix); @@ -2766,7 +2770,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { } } - if rng.gen_bool(0.3) { + if rng.random_bool(0.3) { multibuffer.update(cx, |multibuffer, cx| { old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe())); }) @@ -2815,7 +2819,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { pretty_assertions::assert_eq!(actual_row_infos, expected_row_infos); for _ in 0..5 { - let start_row = rng.gen_range(0..=expected_row_infos.len()); + let start_row = rng.random_range(0..=expected_row_infos.len()); assert_eq!( snapshot .row_infos(MultiBufferRow(start_row as u32)) @@ -2872,8 +2876,8 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { let text_rope = Rope::from(expected_text.as_str()); for _ in 0..10 { - let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right); - let start_ix = text_rope.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right); + let start_ix = text_rope.clip_offset(rng.random_range(0..=end_ix), Bias::Left); let text_for_range = snapshot .text_for_range(start_ix..end_ix) @@ -2908,7 +2912,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { } for _ in 0..10 { - let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right); + let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right); assert_eq!( snapshot.reversed_chars_at(end_ix).collect::(), expected_text[..end_ix].chars().rev().collect::(), @@ -2916,8 +2920,8 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { } for _ in 0..10 { - let end_ix = rng.gen_range(0..=text_rope.len()); - let start_ix = rng.gen_range(0..=end_ix); + let end_ix = rng.random_range(0..=text_rope.len()); + let start_ix = rng.random_range(0..=end_ix); assert_eq!( snapshot .bytes_in_range(start_ix..end_ix) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 36ec338fb71ca1a130657dca1db037051691ad9d..7f7e759b275baadfe3b2d3931955ad39b03fdb05 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3761,7 +3761,7 @@ impl LspStore { worktree_store, languages: languages.clone(), language_server_statuses: Default::default(), - nonce: StdRng::from_entropy().r#gen(), + nonce: StdRng::from_os_rng().random(), diagnostic_summaries: HashMap::default(), lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), @@ -3823,7 +3823,7 @@ impl LspStore { worktree_store, languages: languages.clone(), language_server_statuses: Default::default(), - nonce: StdRng::from_entropy().r#gen(), + nonce: StdRng::from_os_rng().random(), diagnostic_summaries: HashMap::default(), lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index a07f94fb737745b22bf6eaf685e1a4f2874a4dae..969e18f6d40346aa86d83bd0beb77d6652ff0763 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -7661,7 +7661,7 @@ async fn test_staging_random_hunks( .unwrap_or(20); // Try to induce races between diff recalculation and index writes. - if rng.gen_bool(0.5) { + if rng.random_bool(0.5) { executor.deprioritize(*CALCULATE_DIFF_TASK); } @@ -7717,7 +7717,7 @@ async fn test_staging_random_hunks( assert_eq!(hunks.len(), 6); for _i in 0..operations { - let hunk_ix = rng.gen_range(0..hunks.len()); + let hunk_ix = rng.random_range(0..hunks.len()); let hunk = &mut hunks[hunk_ix]; let row = hunk.range.start.row; @@ -7735,7 +7735,7 @@ async fn test_staging_random_hunks( hunk.secondary_status = SecondaryHunkAdditionPending; } - for _ in 0..rng.gen_range(0..10) { + for _ in 0..rng.random_range(0..10) { log::info!("yielding"); cx.executor().simulate_random_delay().await; } diff --git a/crates/rope/benches/rope_benchmark.rs b/crates/rope/benches/rope_benchmark.rs index 2233708525a8a060c78e66340537317cc6694d18..cb741fc78481e7d03a7c18dbf0d8919359b06436 100644 --- a/crates/rope/benches/rope_benchmark.rs +++ b/crates/rope/benches/rope_benchmark.rs @@ -28,11 +28,11 @@ fn generate_random_rope_ranges(mut rng: StdRng, rope: &Rope) -> Vec let mut start = 0; for _ in 0..num_ranges { let range_start = rope.clip_offset( - rng.gen_range(start..=(start + range_max_len)), + rng.random_range(start..=(start + range_max_len)), sum_tree::Bias::Left, ); let range_end = rope.clip_offset( - rng.gen_range(range_start..(range_start + range_max_len)), + rng.random_range(range_start..(range_start + range_max_len)), sum_tree::Bias::Right, ); @@ -52,7 +52,7 @@ fn generate_random_rope_points(mut rng: StdRng, rope: &Rope) -> Vec { let mut points = Vec::new(); for _ in 0..num_points { - points.push(rope.offset_to_point(rng.gen_range(0..rope.len()))); + points.push(rope.offset_to_point(rng.random_range(0..rope.len()))); } points } diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index 00679d8cf539af5759250dfe6fc7406e192407fb..689875274a460abafb808ab7db7db3f5e0487a03 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -612,7 +612,7 @@ mod tests { #[gpui::test(iterations = 100)] fn test_random_chunks(mut rng: StdRng) { - let chunk_len = rng.gen_range(0..=MAX_BASE); + let chunk_len = rng.random_range(0..=MAX_BASE); let text = RandomCharIter::new(&mut rng) .take(chunk_len) .collect::(); @@ -627,8 +627,8 @@ mod tests { verify_chunk(chunk.as_slice(), text); for _ in 0..10 { - let mut start = rng.gen_range(0..=chunk.text.len()); - let mut end = rng.gen_range(start..=chunk.text.len()); + let mut start = rng.random_range(0..=chunk.text.len()); + let mut end = rng.random_range(start..=chunk.text.len()); while !chunk.text.is_char_boundary(start) { start -= 1; } @@ -645,7 +645,7 @@ mod tests { #[gpui::test(iterations = 1000)] fn test_nth_set_bit_random(mut rng: StdRng) { - let set_count = rng.gen_range(0..=128); + let set_count = rng.random_range(0..=128); let mut set_bits = (0..128).choose_multiple(&mut rng, set_count); set_bits.sort(); let mut n = 0; diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 41b2a2d033eb49a1851c02e7066be22d807bca4b..33886854220862c60153dc3ea1f02180c62212a3 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -1610,9 +1610,9 @@ mod tests { let mut expected = String::new(); let mut actual = Rope::new(); for _ in 0..operations { - let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right); - let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left); - let len = rng.gen_range(0..=64); + let end_ix = clip_offset(&expected, rng.random_range(0..=expected.len()), Right); + let start_ix = clip_offset(&expected, rng.random_range(0..=end_ix), Left); + let len = rng.random_range(0..=64); let new_text: String = RandomCharIter::new(&mut rng).take(len).collect(); let mut new_actual = Rope::new(); @@ -1629,8 +1629,8 @@ mod tests { log::info!("text: {:?}", expected); for _ in 0..5 { - let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right); - let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left); + let end_ix = clip_offset(&expected, rng.random_range(0..=expected.len()), Right); + let start_ix = clip_offset(&expected, rng.random_range(0..=end_ix), Left); let actual_text = actual.chunks_in_range(start_ix..end_ix).collect::(); assert_eq!(actual_text, &expected[start_ix..end_ix]); @@ -1695,14 +1695,14 @@ mod tests { ); // Check that next_line/prev_line work correctly from random positions - let mut offset = rng.gen_range(start_ix..=end_ix); + let mut offset = rng.random_range(start_ix..=end_ix); while !expected.is_char_boundary(offset) { offset -= 1; } chunks.seek(offset); for _ in 0..5 { - if rng.r#gen() { + if rng.random() { let expected_next_line_start = expected[offset..end_ix] .find('\n') .map(|newline_ix| offset + newline_ix + 1); @@ -1791,8 +1791,8 @@ mod tests { } assert!((start_ix..=end_ix).contains(&chunks.offset())); - if rng.r#gen() { - offset = rng.gen_range(start_ix..=end_ix); + if rng.random() { + offset = rng.random_range(start_ix..=end_ix); while !expected.is_char_boundary(offset) { offset -= 1; } @@ -1876,8 +1876,8 @@ mod tests { } for _ in 0..5 { - let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right); - let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left); + let end_ix = clip_offset(&expected, rng.random_range(0..=expected.len()), Right); + let start_ix = clip_offset(&expected, rng.random_range(0..=end_ix), Left); assert_eq!( actual.cursor(start_ix).summary::(end_ix), TextSummary::from(&expected[start_ix..end_ix]) diff --git a/crates/rpc/src/auth.rs b/crates/rpc/src/auth.rs index 2e3546289d6bbc476ea7dd6002cb70d466a53d3f..3829f3d36b7cbddd00678f815d08053c864c2010 100644 --- a/crates/rpc/src/auth.rs +++ b/crates/rpc/src/auth.rs @@ -1,6 +1,6 @@ use anyhow::{Context as _, Result}; use base64::prelude::*; -use rand::{Rng as _, thread_rng}; +use rand::prelude::*; use rsa::pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey}; use rsa::traits::PaddingScheme; use rsa::{Oaep, Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey}; @@ -31,7 +31,7 @@ pub struct PrivateKey(RsaPrivateKey); /// Generate a public and private key for asymmetric encryption. pub fn keypair() -> Result<(PublicKey, PrivateKey)> { - let mut rng = thread_rng(); + let mut rng = RsaRngCompat::new(); let bits = 2048; let private_key = RsaPrivateKey::new(&mut rng, bits)?; let public_key = RsaPublicKey::from(&private_key); @@ -40,10 +40,10 @@ pub fn keypair() -> Result<(PublicKey, PrivateKey)> { /// Generate a random 64-character base64 string. pub fn random_token() -> String { - let mut rng = thread_rng(); + let mut rng = rand::rng(); let mut token_bytes = [0; 48]; for byte in token_bytes.iter_mut() { - *byte = rng.r#gen(); + *byte = rng.random(); } BASE64_URL_SAFE.encode(token_bytes) } @@ -52,7 +52,7 @@ impl PublicKey { /// Convert a string to a base64-encoded string that can only be decoded with the corresponding /// private key. pub fn encrypt_string(&self, string: &str, format: EncryptionFormat) -> Result { - let mut rng = thread_rng(); + let mut rng = RsaRngCompat::new(); let bytes = string.as_bytes(); let encrypted_bytes = match format { EncryptionFormat::V0 => self.0.encrypt(&mut rng, Pkcs1v15Encrypt, bytes), @@ -107,6 +107,36 @@ impl TryFrom for PublicKey { } } +// TODO: remove once we rsa v0.10 is released. +struct RsaRngCompat(rand::rngs::ThreadRng); + +impl RsaRngCompat { + fn new() -> Self { + Self(rand::rng()) + } +} + +impl rsa::signature::rand_core::RngCore for RsaRngCompat { + fn next_u32(&mut self) -> u32 { + self.0.next_u32() + } + + fn next_u64(&mut self) -> u64 { + self.0.next_u64() + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + self.0.fill_bytes(dest); + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rsa::signature::rand_core::Error> { + self.fill_bytes(dest); + Ok(()) + } +} + +impl rsa::signature::rand_core::CryptoRng for RsaRngCompat {} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/scheduler/Cargo.toml b/crates/scheduler/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..0446c67914541964f01514865ddc363c60f837c8 --- /dev/null +++ b/crates/scheduler/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "scheduler" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/scheduler.rs" +doctest = false + +[features] +test-support = [] + +[dependencies] +async-task.workspace = true +chrono.workspace = true +futures.workspace = true +parking.workspace = true +parking_lot.workspace = true +rand.workspace = true +workspace-hack.workspace = true diff --git a/crates/scheduler/LICENSE-APACHE b/crates/scheduler/LICENSE-APACHE new file mode 120000 index 0000000000000000000000000000000000000000..1cd601d0a3affae83854be02a0afdec3b7a9ec4d --- /dev/null +++ b/crates/scheduler/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/scheduler/src/clock.rs b/crates/scheduler/src/clock.rs new file mode 100644 index 0000000000000000000000000000000000000000..c035c6b7dbcbabeaeeb2a952974cc4bf777c1f92 --- /dev/null +++ b/crates/scheduler/src/clock.rs @@ -0,0 +1,34 @@ +use chrono::{DateTime, Duration, Utc}; +use parking_lot::Mutex; + +pub trait Clock { + fn now(&self) -> DateTime; +} + +pub struct TestClock { + now: Mutex>, +} + +impl TestClock { + pub fn new() -> Self { + const START_TIME: &str = "2025-07-01T23:59:58-00:00"; + let now = DateTime::parse_from_rfc3339(START_TIME).unwrap().to_utc(); + Self { + now: Mutex::new(now), + } + } + + pub fn set_now(&self, now: DateTime) { + *self.now.lock() = now; + } + + pub fn advance(&self, duration: Duration) { + *self.now.lock() += duration; + } +} + +impl Clock for TestClock { + fn now(&self) -> DateTime { + *self.now.lock() + } +} diff --git a/crates/scheduler/src/executor.rs b/crates/scheduler/src/executor.rs new file mode 100644 index 0000000000000000000000000000000000000000..03f91ae551ff086f56e089bd53d690a2c5345949 --- /dev/null +++ b/crates/scheduler/src/executor.rs @@ -0,0 +1,137 @@ +use crate::{Scheduler, SessionId, Timer}; +use std::{ + future::Future, + marker::PhantomData, + pin::Pin, + rc::Rc, + sync::Arc, + task::{Context, Poll}, + time::Duration, +}; + +#[derive(Clone)] +pub struct ForegroundExecutor { + session_id: SessionId, + scheduler: Arc, + not_send: PhantomData>, +} + +impl ForegroundExecutor { + pub fn spawn(&self, future: F) -> Task + where + F: Future + 'static, + F::Output: 'static, + { + let session_id = self.session_id; + let scheduler = Arc::clone(&self.scheduler); + let (runnable, task) = async_task::spawn_local(future, move |runnable| { + scheduler.schedule_foreground(session_id, runnable); + }); + runnable.schedule(); + Task(TaskState::Spawned(task)) + } + + pub fn timer(&self, duration: Duration) -> Timer { + self.scheduler.timer(duration) + } +} + +impl ForegroundExecutor { + pub fn new(session_id: SessionId, scheduler: Arc) -> Self { + assert!( + scheduler.is_main_thread(), + "ForegroundExecutor must be created on the same thread as the Scheduler" + ); + Self { + session_id, + scheduler, + not_send: PhantomData, + } + } +} + +impl BackgroundExecutor { + pub fn new(scheduler: Arc) -> Self { + Self { scheduler } + } +} + +pub struct BackgroundExecutor { + scheduler: Arc, +} + +impl BackgroundExecutor { + pub fn spawn(&self, future: F) -> Task + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + let scheduler = Arc::clone(&self.scheduler); + let (runnable, task) = async_task::spawn(future, move |runnable| { + scheduler.schedule_background(runnable); + }); + runnable.schedule(); + Task(TaskState::Spawned(task)) + } + + pub fn block_on(&self, future: Fut) -> Fut::Output { + self.scheduler.block_on(future) + } + + pub fn block_with_timeout( + &self, + future: &mut Fut, + timeout: Duration, + ) -> Option { + self.scheduler.block_with_timeout(future, timeout) + } + + pub fn timer(&self, duration: Duration) -> Timer { + self.scheduler.timer(duration) + } +} + +/// Task is a primitive that allows work to happen in the background. +/// +/// It implements [`Future`] so you can `.await` on it. +/// +/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows +/// the task to continue running, but with no way to return a value. +#[must_use] +#[derive(Debug)] +pub struct Task(TaskState); + +#[derive(Debug)] +enum TaskState { + /// A task that is ready to return a value + Ready(Option), + + /// A task that is currently running. + Spawned(async_task::Task), +} + +impl Task { + /// Creates a new task that will resolve with the value + pub fn ready(val: T) -> Self { + Task(TaskState::Ready(Some(val))) + } + + /// Detaching a task runs it to completion in the background + pub fn detach(self) { + match self { + Task(TaskState::Ready(_)) => {} + Task(TaskState::Spawned(task)) => task.detach(), + } + } +} + +impl Future for Task { + type Output = T; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + match unsafe { self.get_unchecked_mut() } { + Task(TaskState::Ready(val)) => Poll::Ready(val.take().unwrap()), + Task(TaskState::Spawned(task)) => Pin::new(task).poll(cx), + } + } +} diff --git a/crates/scheduler/src/scheduler.rs b/crates/scheduler/src/scheduler.rs new file mode 100644 index 0000000000000000000000000000000000000000..ee1964784565266aba2fcc1efd1cd8de0a7fd5e7 --- /dev/null +++ b/crates/scheduler/src/scheduler.rs @@ -0,0 +1,63 @@ +mod clock; +mod executor; +mod test_scheduler; +#[cfg(test)] +mod tests; + +pub use clock::*; +pub use executor::*; +pub use test_scheduler::*; + +use async_task::Runnable; +use futures::{FutureExt as _, channel::oneshot, future::LocalBoxFuture}; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; + +pub trait Scheduler: Send + Sync { + fn block(&self, future: LocalBoxFuture<()>, timeout: Option); + fn schedule_foreground(&self, session_id: SessionId, runnable: Runnable); + fn schedule_background(&self, runnable: Runnable); + fn timer(&self, timeout: Duration) -> Timer; + fn is_main_thread(&self) -> bool; +} + +impl dyn Scheduler { + pub fn block_on(&self, future: Fut) -> Fut::Output { + let mut output = None; + self.block(async { output = Some(future.await) }.boxed_local(), None); + output.unwrap() + } + + pub fn block_with_timeout( + &self, + future: &mut Fut, + timeout: Duration, + ) -> Option { + let mut output = None; + self.block( + async { output = Some(future.await) }.boxed_local(), + Some(timeout), + ); + output + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct SessionId(u16); + +pub struct Timer(oneshot::Receiver<()>); + +impl Future for Timer { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> { + match self.0.poll_unpin(cx) { + Poll::Ready(_) => Poll::Ready(()), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/crates/scheduler/src/test_scheduler.rs b/crates/scheduler/src/test_scheduler.rs new file mode 100644 index 0000000000000000000000000000000000000000..479759d9bdb775a3d2a71bae586fba9d658e71ce --- /dev/null +++ b/crates/scheduler/src/test_scheduler.rs @@ -0,0 +1,352 @@ +use crate::{ + BackgroundExecutor, Clock as _, ForegroundExecutor, Scheduler, SessionId, TestClock, Timer, +}; +use async_task::Runnable; +use chrono::{DateTime, Duration as ChronoDuration, Utc}; +use futures::{FutureExt as _, channel::oneshot, future::LocalBoxFuture}; +use parking_lot::Mutex; +use rand::prelude::*; +use std::{ + collections::VecDeque, + future::Future, + panic::{self, AssertUnwindSafe}, + pin::Pin, + sync::{ + Arc, + atomic::{AtomicBool, Ordering::SeqCst}, + }, + task::{Context, Poll, Wake, Waker}, + thread, + time::{Duration, Instant}, +}; + +pub struct TestScheduler { + clock: Arc, + rng: Arc>, + state: Mutex, + pub thread_id: thread::ThreadId, + pub config: SchedulerConfig, +} + +impl TestScheduler { + /// Run a test once with default configuration (seed 0) + pub fn once(f: impl AsyncFnOnce(Arc) -> R) -> R { + Self::with_seed(0, f) + } + + /// Run a test multiple times with sequential seeds (0, 1, 2, ...) + pub fn many(iterations: usize, mut f: impl AsyncFnMut(Arc) -> R) -> Vec { + (0..iterations as u64) + .map(|seed| { + let mut unwind_safe_f = AssertUnwindSafe(&mut f); + match panic::catch_unwind(move || Self::with_seed(seed, &mut *unwind_safe_f)) { + Ok(result) => result, + Err(error) => { + eprintln!("Failing Seed: {seed}"); + panic::resume_unwind(error); + } + } + }) + .collect() + } + + /// Run a test once with a specific seed + pub fn with_seed(seed: u64, f: impl AsyncFnOnce(Arc) -> R) -> R { + let scheduler = Arc::new(TestScheduler::new(SchedulerConfig::with_seed(seed))); + let future = f(scheduler.clone()); + let result = scheduler.block_on(future); + scheduler.run(); + result + } + + pub fn new(config: SchedulerConfig) -> Self { + Self { + rng: Arc::new(Mutex::new(StdRng::seed_from_u64(config.seed))), + state: Mutex::new(SchedulerState { + runnables: VecDeque::new(), + timers: Vec::new(), + randomize_order: config.randomize_order, + allow_parking: config.allow_parking, + next_session_id: SessionId(0), + }), + thread_id: thread::current().id(), + clock: Arc::new(TestClock::new()), + config, + } + } + + pub fn clock(&self) -> Arc { + self.clock.clone() + } + + pub fn rng(&self) -> Arc> { + self.rng.clone() + } + + /// Create a foreground executor for this scheduler + pub fn foreground(self: &Arc) -> ForegroundExecutor { + let session_id = { + let mut state = self.state.lock(); + state.next_session_id.0 += 1; + state.next_session_id + }; + ForegroundExecutor::new(session_id, self.clone()) + } + + /// Create a background executor for this scheduler + pub fn background(self: &Arc) -> BackgroundExecutor { + BackgroundExecutor::new(self.clone()) + } + + pub fn block_on(&self, future: Fut) -> Fut::Output { + (self as &dyn Scheduler).block_on(future) + } + + pub fn yield_random(&self) -> Yield { + Yield(self.rng.lock().random_range(0..20)) + } + + pub fn run(&self) { + while self.step() || self.advance_clock() { + // Continue until no work remains + } + } + + fn step(&self) -> bool { + let elapsed_timers = { + let mut state = self.state.lock(); + let end_ix = state + .timers + .partition_point(|timer| timer.expiration <= self.clock.now()); + state.timers.drain(..end_ix).collect::>() + }; + + if !elapsed_timers.is_empty() { + return true; + } + + let runnable = self.state.lock().runnables.pop_front(); + if let Some(runnable) = runnable { + runnable.run(); + return true; + } + + false + } + + fn advance_clock(&self) -> bool { + if let Some(timer) = self.state.lock().timers.first() { + self.clock.set_now(timer.expiration); + true + } else { + false + } + } +} + +impl Scheduler for TestScheduler { + fn is_main_thread(&self) -> bool { + thread::current().id() == self.thread_id + } + + fn schedule_foreground(&self, session_id: SessionId, runnable: Runnable) { + let mut state = self.state.lock(); + let ix = if state.randomize_order { + let start_ix = state + .runnables + .iter() + .rposition(|task| task.session_id == Some(session_id)) + .map_or(0, |ix| ix + 1); + self.rng + .lock() + .random_range(start_ix..=state.runnables.len()) + } else { + state.runnables.len() + }; + state.runnables.insert( + ix, + ScheduledRunnable { + session_id: Some(session_id), + runnable, + }, + ); + } + + fn schedule_background(&self, runnable: Runnable) { + let mut state = self.state.lock(); + let ix = if state.randomize_order { + self.rng.lock().random_range(0..=state.runnables.len()) + } else { + state.runnables.len() + }; + state.runnables.insert( + ix, + ScheduledRunnable { + session_id: None, + runnable, + }, + ); + } + + fn timer(&self, duration: Duration) -> Timer { + let (tx, rx) = oneshot::channel(); + let expiration = self.clock.now() + ChronoDuration::from_std(duration).unwrap(); + let state = &mut *self.state.lock(); + state.timers.push(ScheduledTimer { + expiration, + _notify: tx, + }); + state.timers.sort_by_key(|timer| timer.expiration); + Timer(rx) + } + + /// Block until the given future completes, with an optional timeout. If the + /// future is unable to make progress at any moment before the timeout and + /// no other tasks or timers remain, we panic unless parking is allowed. If + /// parking is allowed, we block up to the timeout or indefinitely if none + /// is provided. This is to allow testing a mix of deterministic and + /// non-deterministic async behavior, such as when interacting with I/O in + /// an otherwise deterministic test. + fn block(&self, mut future: LocalBoxFuture<()>, timeout: Option) { + let (parker, unparker) = parking::pair(); + let deadline = timeout.map(|timeout| Instant::now() + timeout); + let awoken = Arc::new(AtomicBool::new(false)); + let waker = Waker::from(Arc::new(WakerFn::new({ + let awoken = awoken.clone(); + move || { + awoken.store(true, SeqCst); + unparker.unpark(); + } + }))); + let max_ticks = if timeout.is_some() { + self.rng + .lock() + .random_range(0..=self.config.max_timeout_ticks) + } else { + usize::MAX + }; + let mut cx = Context::from_waker(&waker); + + for _ in 0..max_ticks { + let Poll::Pending = future.poll_unpin(&mut cx) else { + break; + }; + + let mut stepped = None; + while self.rng.lock().random() && stepped.unwrap_or(true) { + *stepped.get_or_insert(false) |= self.step(); + } + + let stepped = stepped.unwrap_or(true); + let awoken = awoken.swap(false, SeqCst); + if !stepped && !awoken && !self.advance_clock() { + if self.state.lock().allow_parking { + if !park(&parker, deadline) { + break; + } + } else if deadline.is_some() { + break; + } else { + panic!("Parking forbidden"); + } + } + } + } +} + +#[derive(Clone, Debug)] +pub struct SchedulerConfig { + pub seed: u64, + pub randomize_order: bool, + pub allow_parking: bool, + pub max_timeout_ticks: usize, +} + +impl SchedulerConfig { + pub fn with_seed(seed: u64) -> Self { + Self { + seed, + ..Default::default() + } + } +} + +impl Default for SchedulerConfig { + fn default() -> Self { + Self { + seed: 0, + randomize_order: true, + allow_parking: false, + max_timeout_ticks: 1000, + } + } +} + +struct ScheduledRunnable { + session_id: Option, + runnable: Runnable, +} + +impl ScheduledRunnable { + fn run(self) { + self.runnable.run(); + } +} + +struct ScheduledTimer { + expiration: DateTime, + _notify: oneshot::Sender<()>, +} + +struct SchedulerState { + runnables: VecDeque, + timers: Vec, + randomize_order: bool, + allow_parking: bool, + next_session_id: SessionId, +} + +struct WakerFn { + f: F, +} + +impl WakerFn { + fn new(f: F) -> Self { + Self { f } + } +} + +impl Wake for WakerFn { + fn wake(self: Arc) { + (self.f)(); + } + + fn wake_by_ref(self: &Arc) { + (self.f)(); + } +} + +pub struct Yield(usize); + +impl Future for Yield { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + if self.0 == 0 { + Poll::Ready(()) + } else { + self.0 -= 1; + cx.waker().wake_by_ref(); + Poll::Pending + } + } +} + +fn park(parker: &parking::Parker, deadline: Option) -> bool { + if let Some(deadline) = deadline { + parker.park_deadline(deadline) + } else { + parker.park(); + true + } +} diff --git a/crates/scheduler/src/tests.rs b/crates/scheduler/src/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..19eb354e979083b1ec070bd5d09e5871001a8c4f --- /dev/null +++ b/crates/scheduler/src/tests.rs @@ -0,0 +1,348 @@ +use super::*; +use futures::{ + FutureExt, + channel::{mpsc, oneshot}, + executor::block_on, + future, + sink::SinkExt, + stream::{FuturesUnordered, StreamExt}, +}; +use std::{ + cell::RefCell, + collections::{BTreeSet, HashSet}, + pin::Pin, + rc::Rc, + sync::Arc, + task::{Context, Poll}, +}; + +#[test] +fn test_foreground_executor_spawn() { + let result = TestScheduler::once(async |scheduler| { + let task = scheduler.foreground().spawn(async move { 42 }); + task.await + }); + assert_eq!(result, 42); +} + +#[test] +fn test_background_executor_spawn() { + TestScheduler::once(async |scheduler| { + let task = scheduler.background().spawn(async move { 42 }); + let result = task.await; + assert_eq!(result, 42); + }); +} + +#[test] +fn test_foreground_ordering() { + let mut traces = HashSet::new(); + + TestScheduler::many(100, async |scheduler| { + #[derive(Hash, PartialEq, Eq)] + struct TraceEntry { + session: usize, + task: usize, + } + + let trace = Rc::new(RefCell::new(Vec::new())); + + let foreground_1 = scheduler.foreground(); + for task in 0..10 { + foreground_1 + .spawn({ + let trace = trace.clone(); + async move { + trace.borrow_mut().push(TraceEntry { session: 0, task }); + } + }) + .detach(); + } + + let foreground_2 = scheduler.foreground(); + for task in 0..10 { + foreground_2 + .spawn({ + let trace = trace.clone(); + async move { + trace.borrow_mut().push(TraceEntry { session: 1, task }); + } + }) + .detach(); + } + + scheduler.run(); + + assert_eq!( + trace + .borrow() + .iter() + .filter(|entry| entry.session == 0) + .map(|entry| entry.task) + .collect::>(), + (0..10).collect::>() + ); + assert_eq!( + trace + .borrow() + .iter() + .filter(|entry| entry.session == 1) + .map(|entry| entry.task) + .collect::>(), + (0..10).collect::>() + ); + + traces.insert(trace.take()); + }); + + assert!(traces.len() > 1, "Expected at least two traces"); +} + +#[test] +fn test_timer_ordering() { + TestScheduler::many(1, async |scheduler| { + let background = scheduler.background(); + let futures = FuturesUnordered::new(); + futures.push( + async { + background.timer(Duration::from_millis(100)).await; + 2 + } + .boxed(), + ); + futures.push( + async { + background.timer(Duration::from_millis(50)).await; + 1 + } + .boxed(), + ); + futures.push( + async { + background.timer(Duration::from_millis(150)).await; + 3 + } + .boxed(), + ); + assert_eq!(futures.collect::>().await, vec![1, 2, 3]); + }); +} + +#[test] +fn test_send_from_bg_to_fg() { + TestScheduler::once(async |scheduler| { + let foreground = scheduler.foreground(); + let background = scheduler.background(); + + let (sender, receiver) = oneshot::channel::(); + + background + .spawn(async move { + sender.send(42).unwrap(); + }) + .detach(); + + let task = foreground.spawn(async move { receiver.await.unwrap() }); + let result = task.await; + assert_eq!(result, 42); + }); +} + +#[test] +fn test_randomize_order() { + // Test deterministic mode: different seeds should produce same execution order + let mut deterministic_results = HashSet::new(); + for seed in 0..10 { + let config = SchedulerConfig { + seed, + randomize_order: false, + ..Default::default() + }; + let order = block_on(capture_execution_order(config)); + assert_eq!(order.len(), 6); + deterministic_results.insert(order); + } + + // All deterministic runs should produce the same result + assert_eq!( + deterministic_results.len(), + 1, + "Deterministic mode should always produce same execution order" + ); + + // Test randomized mode: different seeds can produce different execution orders + let mut randomized_results = HashSet::new(); + for seed in 0..20 { + let config = SchedulerConfig::with_seed(seed); + let order = block_on(capture_execution_order(config)); + assert_eq!(order.len(), 6); + randomized_results.insert(order); + } + + // Randomized mode should produce multiple different execution orders + assert!( + randomized_results.len() > 1, + "Randomized mode should produce multiple different orders" + ); +} + +async fn capture_execution_order(config: SchedulerConfig) -> Vec { + let scheduler = Arc::new(TestScheduler::new(config)); + let foreground = scheduler.foreground(); + let background = scheduler.background(); + + let (sender, receiver) = mpsc::unbounded::(); + + // Spawn foreground tasks + for i in 0..3 { + let mut sender = sender.clone(); + foreground + .spawn(async move { + sender.send(format!("fg-{}", i)).await.ok(); + }) + .detach(); + } + + // Spawn background tasks + for i in 0..3 { + let mut sender = sender.clone(); + background + .spawn(async move { + sender.send(format!("bg-{}", i)).await.ok(); + }) + .detach(); + } + + drop(sender); // Close sender to signal no more messages + scheduler.run(); + + receiver.collect().await +} + +#[test] +fn test_block() { + let scheduler = Arc::new(TestScheduler::new(SchedulerConfig::default())); + let executor = BackgroundExecutor::new(scheduler); + let (tx, rx) = oneshot::channel(); + + // Spawn background task to send value + let _ = executor + .spawn(async move { + tx.send(42).unwrap(); + }) + .detach(); + + // Block on receiving the value + let result = executor.block_on(async { rx.await.unwrap() }); + assert_eq!(result, 42); +} + +#[test] +#[should_panic(expected = "Parking forbidden")] +fn test_parking_panics() { + let scheduler = Arc::new(TestScheduler::new(SchedulerConfig::default())); + let executor = BackgroundExecutor::new(scheduler); + executor.block_on(future::pending::<()>()); +} + +#[test] +fn test_block_with_parking() { + let config = SchedulerConfig { + allow_parking: true, + ..Default::default() + }; + let scheduler = Arc::new(TestScheduler::new(config)); + let executor = BackgroundExecutor::new(scheduler); + let (tx, rx) = oneshot::channel(); + + // Spawn background task to send value + let _ = executor + .spawn(async move { + tx.send(42).unwrap(); + }) + .detach(); + + // Block on receiving the value (will park if needed) + let result = executor.block_on(async { rx.await.unwrap() }); + assert_eq!(result, 42); +} + +#[test] +fn test_helper_methods() { + // Test the once method + let result = TestScheduler::once(async |scheduler: Arc| { + let background = scheduler.background(); + background.spawn(async { 42 }).await + }); + assert_eq!(result, 42); + + // Test the many method + let results = TestScheduler::many(3, async |scheduler: Arc| { + let background = scheduler.background(); + background.spawn(async { 10 }).await + }); + assert_eq!(results, vec![10, 10, 10]); + + // Test the with_seed method + let result = TestScheduler::with_seed(123, async |scheduler: Arc| { + let background = scheduler.background(); + + // Spawn a background task and wait for its result + let task = background.spawn(async { 99 }); + task.await + }); + assert_eq!(result, 99); +} + +#[test] +fn test_block_with_timeout() { + // Test case: future completes within timeout + TestScheduler::once(async |scheduler| { + let background = scheduler.background(); + let mut future = future::ready(42); + let output = background.block_with_timeout(&mut future, Duration::from_millis(100)); + assert_eq!(output, Some(42)); + }); + + // Test case: future times out + TestScheduler::once(async |scheduler| { + let background = scheduler.background(); + let mut future = future::pending::<()>(); + let output = background.block_with_timeout(&mut future, Duration::from_millis(50)); + assert_eq!(output, None); + }); + + // Test case: future makes progress via timer but still times out + let mut results = BTreeSet::new(); + TestScheduler::many(100, async |scheduler| { + let background = scheduler.background(); + let mut task = background.spawn(async move { + Yield { polls: 10 }.await; + 42 + }); + let output = background.block_with_timeout(&mut task, Duration::from_millis(50)); + results.insert(output); + }); + assert_eq!( + results.into_iter().collect::>(), + vec![None, Some(42)] + ); +} + +struct Yield { + polls: usize, +} + +impl Future for Yield { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.polls -= 1; + if self.polls == 0 { + Poll::Ready(()) + } else { + cx.waker().wake_by_ref(); + Poll::Pending + } + } +} diff --git a/crates/streaming_diff/src/streaming_diff.rs b/crates/streaming_diff/src/streaming_diff.rs index 704164e01eedc64cac9a1e8e4e82f584a0b4fdb9..5677981b0dc9878963e01d09e7281749d6603c8f 100644 --- a/crates/streaming_diff/src/streaming_diff.rs +++ b/crates/streaming_diff/src/streaming_diff.rs @@ -945,7 +945,7 @@ mod tests { let mut new_len = 0; while new_len < new.len() { - let mut chunk_len = rng.gen_range(1..=new.len() - new_len); + let mut chunk_len = rng.random_range(1..=new.len() - new_len); while !new.is_char_boundary(new_len + chunk_len) { chunk_len += 1; } @@ -1034,14 +1034,14 @@ mod tests { fn randomly_edit(text: &str, rng: &mut impl Rng) -> String { let mut result = String::from(text); - let edit_count = rng.gen_range(1..=5); + let edit_count = rng.random_range(1..=5); fn random_char_range(text: &str, rng: &mut impl Rng) -> (usize, usize) { - let mut start = rng.gen_range(0..=text.len()); + let mut start = rng.random_range(0..=text.len()); while !text.is_char_boundary(start) { start -= 1; } - let mut end = rng.gen_range(start..=text.len()); + let mut end = rng.random_range(start..=text.len()); while !text.is_char_boundary(end) { end += 1; } @@ -1049,11 +1049,11 @@ mod tests { } for _ in 0..edit_count { - match rng.gen_range(0..3) { + match rng.random_range(0..3) { 0 => { // Insert let (pos, _) = random_char_range(&result, rng); - let insert_len = rng.gen_range(1..=5); + let insert_len = rng.random_range(1..=5); let insert_text: String = random_text(rng, insert_len); result.insert_str(pos, &insert_text); } diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 710fdd4fbf12ccc2b60998207d964bd31550b345..64814ad09148cc0eb318c306132f2e296fcb3cab 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -909,7 +909,7 @@ where #[cfg(test)] mod tests { use super::*; - use rand::{distributions, prelude::*}; + use rand::{distr::StandardUniform, prelude::*}; use std::cmp; #[ctor::ctor] @@ -951,24 +951,24 @@ mod tests { let rng = &mut rng; let mut tree = SumTree::::default(); - let count = rng.gen_range(0..10); - if rng.r#gen() { - tree.extend(rng.sample_iter(distributions::Standard).take(count), &()); + let count = rng.random_range(0..10); + if rng.random() { + tree.extend(rng.sample_iter(StandardUniform).take(count), &()); } else { let items = rng - .sample_iter(distributions::Standard) + .sample_iter(StandardUniform) .take(count) .collect::>(); tree.par_extend(items, &()); } for _ in 0..num_operations { - let splice_end = rng.gen_range(0..tree.extent::(&()).0 + 1); - let splice_start = rng.gen_range(0..splice_end + 1); - let count = rng.gen_range(0..10); + let splice_end = rng.random_range(0..tree.extent::(&()).0 + 1); + let splice_start = rng.random_range(0..splice_end + 1); + let count = rng.random_range(0..10); let tree_end = tree.extent::(&()); let new_items = rng - .sample_iter(distributions::Standard) + .sample_iter(StandardUniform) .take(count) .collect::>(); @@ -978,7 +978,7 @@ mod tests { tree = { let mut cursor = tree.cursor::(&()); let mut new_tree = cursor.slice(&Count(splice_start), Bias::Right); - if rng.r#gen() { + if rng.random() { new_tree.extend(new_items, &()); } else { new_tree.par_extend(new_items, &()); @@ -1005,7 +1005,7 @@ mod tests { .filter(|(_, item)| (item & 1) == 0) .collect::>(); - let mut item_ix = if rng.r#gen() { + let mut item_ix = if rng.random() { filter_cursor.next(); 0 } else { @@ -1022,12 +1022,12 @@ mod tests { filter_cursor.next(); item_ix += 1; - while item_ix > 0 && rng.gen_bool(0.2) { + while item_ix > 0 && rng.random_bool(0.2) { log::info!("prev"); filter_cursor.prev(); item_ix -= 1; - if item_ix == 0 && rng.gen_bool(0.2) { + if item_ix == 0 && rng.random_bool(0.2) { filter_cursor.prev(); assert_eq!(filter_cursor.item(), None); assert_eq!(filter_cursor.start().0, 0); @@ -1039,9 +1039,9 @@ mod tests { let mut before_start = false; let mut cursor = tree.cursor::(&()); - let start_pos = rng.gen_range(0..=reference_items.len()); + let start_pos = rng.random_range(0..=reference_items.len()); cursor.seek(&Count(start_pos), Bias::Right); - let mut pos = rng.gen_range(start_pos..=reference_items.len()); + let mut pos = rng.random_range(start_pos..=reference_items.len()); cursor.seek_forward(&Count(pos), Bias::Right); for i in 0..10 { @@ -1084,10 +1084,18 @@ mod tests { } for _ in 0..10 { - let end = rng.gen_range(0..tree.extent::(&()).0 + 1); - let start = rng.gen_range(0..end + 1); - let start_bias = if rng.r#gen() { Bias::Left } else { Bias::Right }; - let end_bias = if rng.r#gen() { Bias::Left } else { Bias::Right }; + let end = rng.random_range(0..tree.extent::(&()).0 + 1); + let start = rng.random_range(0..end + 1); + let start_bias = if rng.random() { + Bias::Left + } else { + Bias::Right + }; + let end_bias = if rng.random() { + Bias::Left + } else { + Bias::Right + }; let mut cursor = tree.cursor::(&()); cursor.seek(&Count(start), start_bias); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index a8b1fcf0f2a31cbd80612d2e19506d38d52fe0af..96271ea771e3fdbe42b03504797ba78170d79096 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -2198,7 +2198,7 @@ mod tests { }; use collections::HashMap; use gpui::{Pixels, Point, TestAppContext, bounds, point, size}; - use rand::{Rng, distributions::Alphanumeric, rngs::ThreadRng, thread_rng}; + use rand::{Rng, distr, rngs::ThreadRng}; #[ignore = "Test is flaky on macOS, and doesn't run on Windows"] #[gpui::test] @@ -2249,13 +2249,14 @@ mod tests { #[test] fn test_mouse_to_cell_test() { - let mut rng = thread_rng(); + let mut rng = rand::rng(); const ITERATIONS: usize = 10; const PRECISION: usize = 1000; for _ in 0..ITERATIONS { - let viewport_cells = rng.gen_range(15..20); - let cell_size = rng.gen_range(5 * PRECISION..20 * PRECISION) as f32 / PRECISION as f32; + let viewport_cells = rng.random_range(15..20); + let cell_size = + rng.random_range(5 * PRECISION..20 * PRECISION) as f32 / PRECISION as f32; let size = crate::TerminalBounds { cell_width: Pixels::from(cell_size), @@ -2277,8 +2278,8 @@ mod tests { for col in 0..(viewport_cells - 1) { let col = col as usize; - let row_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32; - let col_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32; + let row_offset = rng.random_range(0..PRECISION) as f32 / PRECISION as f32; + let col_offset = rng.random_range(0..PRECISION) as f32 / PRECISION as f32; let mouse_pos = point( Pixels::from(col as f32 * cell_size + col_offset), @@ -2298,7 +2299,7 @@ mod tests { #[test] fn test_mouse_to_cell_clamp() { - let mut rng = thread_rng(); + let mut rng = rand::rng(); let size = crate::TerminalBounds { cell_width: Pixels::from(10.), @@ -2336,7 +2337,7 @@ mod tests { for _ in 0..((size.height() / size.line_height()) as usize) { let mut row_vec = Vec::new(); for _ in 0..((size.width() / size.cell_width()) as usize) { - let cell_char = rng.sample(Alphanumeric) as char; + let cell_char = rng.sample(distr::Alphanumeric) as char; row_vec.push(cell_char) } cells.push(row_vec) diff --git a/crates/text/src/locator.rs b/crates/text/src/locator.rs index d529e60d48ed520b518ed9beee789860eb84860a..9b89cf21c74eccfe6cbb93fd2dec5bc849f2170d 100644 --- a/crates/text/src/locator.rs +++ b/crates/text/src/locator.rs @@ -106,13 +106,13 @@ mod tests { let mut rhs = Default::default(); while lhs == rhs { lhs = Locator( - (0..rng.gen_range(1..=5)) - .map(|_| rng.gen_range(0..=100)) + (0..rng.random_range(1..=5)) + .map(|_| rng.random_range(0..=100)) .collect(), ); rhs = Locator( - (0..rng.gen_range(1..=5)) - .map(|_| rng.gen_range(0..=100)) + (0..rng.random_range(1..=5)) + .map(|_| rng.random_range(0..=100)) .collect(), ); } diff --git a/crates/text/src/network.rs b/crates/text/src/network.rs index f22bb52d205ba9505d9f2dc168628734346d81f5..d0d1b650ad92f8ab258cdd37e2bfc662855d6a97 100644 --- a/crates/text/src/network.rs +++ b/crates/text/src/network.rs @@ -65,8 +65,8 @@ impl Network { for message in &messages { // Insert one or more duplicates of this message, potentially *before* the previous // message sent by this peer to simulate out-of-order delivery. - for _ in 0..self.rng.gen_range(1..4) { - let insertion_index = self.rng.gen_range(0..inbox.len() + 1); + for _ in 0..self.rng.random_range(1..4) { + let insertion_index = self.rng.random_range(0..inbox.len() + 1); inbox.insert( insertion_index, Envelope { @@ -85,7 +85,7 @@ impl Network { pub fn receive(&mut self, receiver: ReplicaId) -> Vec { let inbox = self.inboxes.get_mut(&receiver).unwrap(); - let count = self.rng.gen_range(0..inbox.len() + 1); + let count = self.rng.random_range(0..inbox.len() + 1); inbox .drain(0..count) .map(|envelope| envelope.message) diff --git a/crates/text/src/patch.rs b/crates/text/src/patch.rs index dcb35e9a921538134b94e2870011eb3b341f01de..b8bb904052be44d7b67ba51215896f6f308c39c9 100644 --- a/crates/text/src/patch.rs +++ b/crates/text/src/patch.rs @@ -497,8 +497,8 @@ mod tests { .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(20); - let initial_chars = (0..rng.gen_range(0..=100)) - .map(|_| rng.gen_range(b'a'..=b'z') as char) + let initial_chars = (0..rng.random_range(0..=100)) + .map(|_| rng.random_range(b'a'..=b'z') as char) .collect::>(); log::info!("initial chars: {:?}", initial_chars); @@ -517,11 +517,11 @@ mod tests { break; } - let end = rng.gen_range(last_edit_end..=expected_chars.len()); - let start = rng.gen_range(last_edit_end..=end); + let end = rng.random_range(last_edit_end..=expected_chars.len()); + let start = rng.random_range(last_edit_end..=end); let old_len = end - start; - let mut new_len = rng.gen_range(0..=3); + let mut new_len = rng.random_range(0..=3); if start == end && new_len == 0 { new_len += 1; } @@ -529,7 +529,7 @@ mod tests { last_edit_end = start + new_len + 1; let new_chars = (0..new_len) - .map(|_| rng.gen_range(b'A'..=b'Z') as char) + .map(|_| rng.random_range(b'A'..=b'Z') as char) .collect::>(); log::info!( " editing {:?}: {:?}", diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index a096f1281f592babf7900891a6412451bdc362d0..4298e704ab5f8fbe57af363379395ef23624cfcf 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -36,14 +36,14 @@ fn test_random_edits(mut rng: StdRng) { .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); - let reference_string_len = rng.gen_range(0..3); + let reference_string_len = rng.random_range(0..3); let mut reference_string = RandomCharIter::new(&mut rng) .take(reference_string_len) .collect::(); let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), reference_string.clone()); LineEnding::normalize(&mut reference_string); - buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200))); + buffer.set_group_interval(Duration::from_millis(rng.random_range(0..=200))); let mut buffer_versions = Vec::new(); log::info!( "buffer text {:?}, version: {:?}", @@ -64,7 +64,7 @@ fn test_random_edits(mut rng: StdRng) { buffer.version() ); - if rng.gen_bool(0.25) { + if rng.random_bool(0.25) { buffer.randomly_undo_redo(&mut rng); reference_string = buffer.text(); log::info!( @@ -82,7 +82,7 @@ fn test_random_edits(mut rng: StdRng) { buffer.check_invariants(); - if rng.gen_bool(0.3) { + if rng.random_bool(0.3) { buffer_versions.push((buffer.clone(), buffer.subscribe())); } } @@ -112,8 +112,9 @@ fn test_random_edits(mut rng: StdRng) { ); for _ in 0..5 { - let end_ix = old_buffer.clip_offset(rng.gen_range(0..=old_buffer.len()), Bias::Right); - let start_ix = old_buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + let end_ix = + old_buffer.clip_offset(rng.random_range(0..=old_buffer.len()), Bias::Right); + let start_ix = old_buffer.clip_offset(rng.random_range(0..=end_ix), Bias::Left); let range = old_buffer.anchor_before(start_ix)..old_buffer.anchor_after(end_ix); let mut old_text = old_buffer.text_for_range(range.clone()).collect::(); let edits = buffer @@ -731,7 +732,7 @@ fn test_random_concurrent_edits(mut rng: StdRng) { .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); - let base_text_len = rng.gen_range(0..10); + let base_text_len = rng.random_range(0..10); let base_text = RandomCharIter::new(&mut rng) .take(base_text_len) .collect::(); @@ -741,7 +742,7 @@ fn test_random_concurrent_edits(mut rng: StdRng) { for i in 0..peers { let mut buffer = Buffer::new(i as ReplicaId, BufferId::new(1).unwrap(), base_text.clone()); - buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200)); + buffer.history.group_interval = Duration::from_millis(rng.random_range(0..=200)); buffers.push(buffer); replica_ids.push(i as u16); network.add_peer(i as u16); @@ -751,10 +752,10 @@ fn test_random_concurrent_edits(mut rng: StdRng) { let mut mutation_count = operations; loop { - let replica_index = rng.gen_range(0..peers); + let replica_index = rng.random_range(0..peers); let replica_id = replica_ids[replica_index]; let buffer = &mut buffers[replica_index]; - match rng.gen_range(0..=100) { + match rng.random_range(0..=100) { 0..=50 if mutation_count != 0 => { let op = buffer.randomly_edit(&mut rng, 5).1; network.broadcast(buffer.replica_id, vec![op]); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 705d3f1788288eb67a0b3b756ba545dc99b031d3..8fb6f56222b503360a3d2dd6f4a6b27d1ac728e3 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1818,8 +1818,8 @@ impl Buffer { } pub fn random_byte_range(&self, start_offset: usize, rng: &mut impl rand::Rng) -> Range { - let end = self.clip_offset(rng.gen_range(start_offset..=self.len()), Bias::Right); - let start = self.clip_offset(rng.gen_range(start_offset..=end), Bias::Right); + let end = self.clip_offset(rng.random_range(start_offset..=self.len()), Bias::Right); + let start = self.clip_offset(rng.random_range(start_offset..=end), Bias::Right); start..end } @@ -1841,7 +1841,7 @@ impl Buffer { let range = self.random_byte_range(new_start, rng); last_end = Some(range.end); - let new_text_len = rng.gen_range(0..10); + let new_text_len = rng.random_range(0..10); let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect(); edits.push((range, new_text.into())); @@ -1877,7 +1877,7 @@ impl Buffer { use rand::prelude::*; let mut ops = Vec::new(); - for _ in 0..rng.gen_range(1..=5) { + for _ in 0..rng.random_range(1..=5) { if let Some(entry) = self.history.undo_stack.choose(rng) { let transaction = entry.transaction.clone(); log::info!( diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index c66adb8b3a7ef93828e95683596f43b91f96f994..db44e3945186842990f7ef8d7b2794b023324d56 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -815,7 +815,8 @@ pub fn defer(f: F) -> Deferred { #[cfg(any(test, feature = "test-support"))] mod rng { - use rand::{Rng, seq::SliceRandom}; + use rand::prelude::*; + pub struct RandomCharIter { rng: T, simple_text: bool, @@ -840,18 +841,18 @@ mod rng { fn next(&mut self) -> Option { if self.simple_text { - return if self.rng.gen_range(0..100) < 5 { + return if self.rng.random_range(0..100) < 5 { Some('\n') } else { - Some(self.rng.gen_range(b'a'..b'z' + 1).into()) + Some(self.rng.random_range(b'a'..b'z' + 1).into()) }; } - match self.rng.gen_range(0..100) { + match self.rng.random_range(0..100) { // whitespace 0..=19 => [' ', '\n', '\r', '\t'].choose(&mut self.rng).copied(), // two-byte greek letters - 20..=32 => char::from_u32(self.rng.gen_range(('α' as u32)..('ω' as u32 + 1))), + 20..=32 => char::from_u32(self.rng.random_range(('α' as u32)..('ω' as u32 + 1))), // // three-byte characters 33..=45 => ['✋', '✅', '❌', '❎', '⭐'] .choose(&mut self.rng) @@ -859,7 +860,7 @@ mod rng { // // four-byte characters 46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.rng).copied(), // ascii letters - _ => Some(self.rng.gen_range(b'a'..b'z' + 1).into()), + _ => Some(self.rng.random_range(b'a'..b'z' + 1).into()), } } } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 1783ba317c9927bb79ebdb91b1f57f13d200b60f..92569e0f8177ea2886271e2a39580076effc4e8b 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -1464,7 +1464,7 @@ async fn test_random_worktree_operations_during_initial_scan( tree.as_local().unwrap().snapshot().check_invariants(true) }); - if rng.gen_bool(0.6) { + if rng.random_bool(0.6) { snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())); } } @@ -1551,7 +1551,7 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) let mut snapshots = Vec::new(); let mut mutations_len = operations; while mutations_len > 1 { - if rng.gen_bool(0.2) { + if rng.random_bool(0.2) { worktree .update(cx, |worktree, cx| { randomly_mutate_worktree(worktree, &mut rng, cx) @@ -1563,8 +1563,8 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) } let buffered_event_count = fs.as_fake().buffered_event_count(); - if buffered_event_count > 0 && rng.gen_bool(0.3) { - let len = rng.gen_range(0..=buffered_event_count); + if buffered_event_count > 0 && rng.random_bool(0.3) { + let len = rng.random_range(0..=buffered_event_count); log::info!("flushing {} events", len); fs.as_fake().flush_events(len); } else { @@ -1573,7 +1573,7 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) } cx.executor().run_until_parked(); - if rng.gen_bool(0.2) { + if rng.random_bool(0.2) { log::info!("storing snapshot {}", snapshots.len()); let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); snapshots.push(snapshot); @@ -1701,7 +1701,7 @@ fn randomly_mutate_worktree( let snapshot = worktree.snapshot(); let entry = snapshot.entries(false, 0).choose(rng).unwrap(); - match rng.gen_range(0_u32..100) { + match rng.random_range(0_u32..100) { 0..=33 if entry.path.as_ref() != Path::new("") => { log::info!("deleting entry {:?} ({})", entry.path, entry.id.0); worktree.delete_entry(entry.id, false, cx).unwrap() @@ -1733,7 +1733,7 @@ fn randomly_mutate_worktree( _ => { if entry.is_dir() { let child_path = entry.path.join(random_filename(rng)); - let is_dir = rng.gen_bool(0.3); + let is_dir = rng.random_bool(0.3); log::info!( "creating {} at {:?}", if is_dir { "dir" } else { "file" }, @@ -1776,11 +1776,11 @@ async fn randomly_mutate_fs( } } - if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) { + if (files.is_empty() && dirs.len() == 1) || rng.random_bool(insertion_probability) { let path = dirs.choose(rng).unwrap(); let new_path = path.join(random_filename(rng)); - if rng.r#gen() { + if rng.random() { log::info!( "creating dir {:?}", new_path.strip_prefix(root_path).unwrap() @@ -1793,7 +1793,7 @@ async fn randomly_mutate_fs( ); fs.create_file(&new_path, Default::default()).await.unwrap(); } - } else if rng.gen_bool(0.05) { + } else if rng.random_bool(0.05) { let ignore_dir_path = dirs.choose(rng).unwrap(); let ignore_path = ignore_dir_path.join(*GITIGNORE); @@ -1808,11 +1808,11 @@ async fn randomly_mutate_fs( .cloned() .collect::>(); let files_to_ignore = { - let len = rng.gen_range(0..=subfiles.len()); + let len = rng.random_range(0..=subfiles.len()); subfiles.choose_multiple(rng, len) }; let dirs_to_ignore = { - let len = rng.gen_range(0..subdirs.len()); + let len = rng.random_range(0..subdirs.len()); subdirs.choose_multiple(rng, len) }; @@ -1848,7 +1848,7 @@ async fn randomly_mutate_fs( file_path.into_iter().chain(dir_path).choose(rng).unwrap() }; - let is_rename = rng.r#gen(); + let is_rename = rng.random(); if is_rename { let new_path_parent = dirs .iter() @@ -1857,7 +1857,7 @@ async fn randomly_mutate_fs( .unwrap(); let overwrite_existing_dir = - !old_path.starts_with(new_path_parent) && rng.gen_bool(0.3); + !old_path.starts_with(new_path_parent) && rng.random_bool(0.3); let new_path = if overwrite_existing_dir { fs.remove_dir( new_path_parent, @@ -1919,7 +1919,7 @@ async fn randomly_mutate_fs( fn random_filename(rng: &mut impl Rng) -> String { (0..6) - .map(|_| rng.sample(rand::distributions::Alphanumeric)) + .map(|_| rng.sample(rand::distr::Alphanumeric)) .map(char::from) .collect() } diff --git a/crates/zeta/src/input_excerpt.rs b/crates/zeta/src/input_excerpt.rs index f4add6593e9a2b15679b5b0e6e660b4ce6a52f87..dd1bbed1d72e8668e9ed55c9b66b911addfcdd43 100644 --- a/crates/zeta/src/input_excerpt.rs +++ b/crates/zeta/src/input_excerpt.rs @@ -149,7 +149,7 @@ mod tests { let mut rng = rand::thread_rng(); let mut numbers = Vec::new(); for _ in 0..5 { - numbers.push(rng.gen_range(1..101)); + numbers.push(rng.random_range(1..101)); } numbers } @@ -208,7 +208,7 @@ mod tests { <|editable_region_end|> let mut numbers = Vec::new(); for _ in 0..5 { - numbers.push(rng.gen_range(1..101)); + numbers.push(rng.random_range(1..101)); ```"#} ); } From e37efc1e9b313ae4ac28322334db464a2b84c8c4 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 4 Sep 2025 17:30:23 +0200 Subject: [PATCH 580/744] diagnostics: Fix diagnostics pane clearing up too eagerly on typing (#37546) Closes https://github.com/zed-industries/zed/issues/30494 Release Notes: - Fixed diagnostics pane closing buffers too eagerly when typing inside it --- crates/diagnostics/src/diagnostics.rs | 74 ++++++++++++++------------- crates/diagnostics/src/items.rs | 63 ++++++++++------------- 2 files changed, 64 insertions(+), 73 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 53d03718475da1eeaf2b6b3faa22baabb1695f2d..20e8a861334ac764db921d706e86605aed00c175 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -94,43 +94,44 @@ impl Render for ProjectDiagnosticsEditor { 0 }; - let child = if warning_count + self.summary.error_count == 0 { - let label = if self.summary.warning_count == 0 { - SharedString::new_static("No problems in workspace") + let child = + if warning_count + self.summary.error_count == 0 && self.editor.read(cx).is_empty(cx) { + let label = if self.summary.warning_count == 0 { + SharedString::new_static("No problems in workspace") + } else { + SharedString::new_static("No errors in workspace") + }; + v_flex() + .key_context("EmptyPane") + .size_full() + .gap_1() + .justify_center() + .items_center() + .text_center() + .bg(cx.theme().colors().editor_background) + .child(Label::new(label).color(Color::Muted)) + .when(self.summary.warning_count > 0, |this| { + let plural_suffix = if self.summary.warning_count > 1 { + "s" + } else { + "" + }; + let label = format!( + "Show {} warning{}", + self.summary.warning_count, plural_suffix + ); + this.child( + Button::new("diagnostics-show-warning-label", label).on_click( + cx.listener(|this, _, window, cx| { + this.toggle_warnings(&Default::default(), window, cx); + cx.notify(); + }), + ), + ) + }) } else { - SharedString::new_static("No errors in workspace") + div().size_full().child(self.editor.clone()) }; - v_flex() - .key_context("EmptyPane") - .size_full() - .gap_1() - .justify_center() - .items_center() - .text_center() - .bg(cx.theme().colors().editor_background) - .child(Label::new(label).color(Color::Muted)) - .when(self.summary.warning_count > 0, |this| { - let plural_suffix = if self.summary.warning_count > 1 { - "s" - } else { - "" - }; - let label = format!( - "Show {} warning{}", - self.summary.warning_count, plural_suffix - ); - this.child( - Button::new("diagnostics-show-warning-label", label).on_click(cx.listener( - |this, _, window, cx| { - this.toggle_warnings(&Default::default(), window, cx); - cx.notify(); - }, - )), - ) - }) - } else { - div().size_full().child(self.editor.clone()) - }; div() .key_context("Diagnostics") @@ -233,6 +234,7 @@ impl ProjectDiagnosticsEditor { } } EditorEvent::Blurred => this.update_stale_excerpts(window, cx), + EditorEvent::Saved => this.update_stale_excerpts(window, cx), _ => {} } }, @@ -277,7 +279,7 @@ impl ProjectDiagnosticsEditor { } fn update_stale_excerpts(&mut self, window: &mut Window, cx: &mut Context) { - if self.update_excerpts_task.is_some() { + if self.update_excerpts_task.is_some() || self.multibuffer.read(cx).is_dirty(cx) { return; } diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 7ac6d101f315674cec4fd07f4ad2df0830284124..11ee4ece96d0c4646714d808037e7a2789bcdf85 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -32,49 +32,38 @@ impl Render for DiagnosticIndicator { } let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) { - (0, 0) => h_flex().map(|this| { - this.child( - Icon::new(IconName::Check) - .size(IconSize::Small) - .color(Color::Default), - ) - }), - (0, warning_count) => h_flex() - .gap_1() - .child( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .color(Color::Warning), - ) - .child(Label::new(warning_count.to_string()).size(LabelSize::Small)), - (error_count, 0) => h_flex() - .gap_1() - .child( - Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error), - ) - .child(Label::new(error_count.to_string()).size(LabelSize::Small)), + (0, 0) => h_flex().child( + Icon::new(IconName::Check) + .size(IconSize::Small) + .color(Color::Default), + ), (error_count, warning_count) => h_flex() .gap_1() - .child( - Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error), - ) - .child(Label::new(error_count.to_string()).size(LabelSize::Small)) - .child( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .color(Color::Warning), - ) - .child(Label::new(warning_count.to_string()).size(LabelSize::Small)), + .when(error_count > 0, |this| { + this.child( + Icon::new(IconName::XCircle) + .size(IconSize::Small) + .color(Color::Error), + ) + .child(Label::new(error_count.to_string()).size(LabelSize::Small)) + }) + .when(warning_count > 0, |this| { + this.child( + Icon::new(IconName::Warning) + .size(IconSize::Small) + .color(Color::Warning), + ) + .child(Label::new(warning_count.to_string()).size(LabelSize::Small)) + }), }; let status = if let Some(diagnostic) = &self.current_diagnostic { - let message = diagnostic.message.split('\n').next().unwrap().to_string(); + let message = diagnostic + .message + .split_once('\n') + .map_or(&*diagnostic.message, |(first, _)| first); Some( - Button::new("diagnostic_message", message) + Button::new("diagnostic_message", SharedString::new(message)) .label_size(LabelSize::Small) .tooltip(|window, cx| { Tooltip::for_action( From 0870a1fe80b0724ddaae1408aa402761540131de Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 4 Sep 2025 09:01:50 -0700 Subject: [PATCH 581/744] acp: Don't share API key with Anthropic provider (#37543) Since Claude Code has it's own preferred method of grabbing API keys, we don't want to reuse this one. Release Notes: - acp: Don't share Anthropic API key from the Anthropic provider to allow default Claude Code login options --------- Co-authored-by: Agus Zubiaga --- crates/agent_servers/src/claude.rs | 15 ++--- crates/agent_ui/src/acp/thread_view.rs | 92 +++++++++++++------------- 2 files changed, 49 insertions(+), 58 deletions(-) diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 194867241baf86cf7b3d3ab168318a00d64d6e25..48d3e33775d98dfe89801813c6926ff40f48ed87 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1,4 +1,3 @@ -use language_models::provider::anthropic::AnthropicLanguageModelProvider; use settings::SettingsStore; use std::path::Path; use std::rc::Rc; @@ -99,16 +98,10 @@ impl AgentServer for ClaudeCode { .await? }; - if let Some(api_key) = cx - .update(AnthropicLanguageModelProvider::api_key)? - .await - .ok() - { - command - .env - .get_or_insert_default() - .insert("ANTHROPIC_API_KEY".to_owned(), api_key.key); - } + command + .env + .get_or_insert_default() + .insert("ANTHROPIC_API_KEY".to_owned(), "".to_owned()); let root_dir_exists = fs.is_dir(&root_dir).await; anyhow::ensure!( diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 50da44e430fd684d0e91d43ee82a0ccb0117111d..3407f4e878e6452322aba1b5009b582f322db4b4 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -983,7 +983,7 @@ impl AcpThreadView { this, AuthRequired { description: None, - provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID), + provider_id: None, }, agent, connection, @@ -3010,6 +3010,8 @@ impl AcpThreadView { let show_description = configuration_view.is_none() && description.is_none() && pending_auth_method.is_none(); + let auth_methods = connection.auth_methods(); + v_flex().flex_1().size_full().justify_end().child( v_flex() .p_2() @@ -3040,21 +3042,23 @@ impl AcpThreadView { .cloned() .map(|view| div().w_full().child(view)), ) - .when( - show_description, - |el| { - el.child( - Label::new(format!( - "You are not currently authenticated with {}. Please choose one of the following options:", - self.agent.name() - )) - .size(LabelSize::Small) - .color(Color::Muted) - .mb_1() - .ml_5(), - ) - }, - ) + .when(show_description, |el| { + el.child( + Label::new(format!( + "You are not currently authenticated with {}.{}", + self.agent.name(), + if auth_methods.len() > 1 { + " Please choose one of the following options:" + } else { + "" + } + )) + .size(LabelSize::Small) + .color(Color::Muted) + .mb_1() + .ml_5(), + ) + }) .when_some(pending_auth_method, |el, _| { el.child( h_flex() @@ -3066,12 +3070,12 @@ impl AcpThreadView { Icon::new(IconName::ArrowCircle) .size(IconSize::Small) .color(Color::Muted) - .with_rotate_animation(2) + .with_rotate_animation(2), ) .child(Label::new("Authenticating…").size(LabelSize::Small)), ) }) - .when(!connection.auth_methods().is_empty(), |this| { + .when(!auth_methods.is_empty(), |this| { this.child( h_flex() .justify_end() @@ -3083,38 +3087,32 @@ impl AcpThreadView { .pt_2() .border_color(cx.theme().colors().border.opacity(0.8)) }) - .children( - connection - .auth_methods() - .iter() - .enumerate() - .rev() - .map(|(ix, method)| { - Button::new( - SharedString::from(method.id.0.clone()), - method.name.clone(), - ) - .when(ix == 0, |el| { - el.style(ButtonStyle::Tinted(ui::TintColor::Warning)) - }) - .label_size(LabelSize::Small) - .on_click({ - let method_id = method.id.clone(); - cx.listener(move |this, _, window, cx| { - telemetry::event!( - "Authenticate Agent Started", - agent = this.agent.telemetry_id(), - method = method_id - ); + .children(connection.auth_methods().iter().enumerate().rev().map( + |(ix, method)| { + Button::new( + SharedString::from(method.id.0.clone()), + method.name.clone(), + ) + .when(ix == 0, |el| { + el.style(ButtonStyle::Tinted(ui::TintColor::Warning)) + }) + .label_size(LabelSize::Small) + .on_click({ + let method_id = method.id.clone(); + cx.listener(move |this, _, window, cx| { + telemetry::event!( + "Authenticate Agent Started", + agent = this.agent.telemetry_id(), + method = method_id + ); - this.authenticate(method_id.clone(), window, cx) - }) + this.authenticate(method_id.clone(), window, cx) }) - }), - ), + }) + }, + )), ) - }) - + }), ) } From 25ee9b1013fe10a04b429576006b88fb34bdcd85 Mon Sep 17 00:00:00 2001 From: Jiqing Yang <73824809+WERDXZ@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:21:44 -0700 Subject: [PATCH 582/744] Fix Wayland crash on AMD GPUs by updating Blade (#37516) Updates blade-graphics from e0ec4e7 to bfa594e to fix GPU crashes on Wayland with AMD graphics cards. The crash was caused by incorrect BLAS scratch buffer alignment - the old version hardcoded 256-byte alignment, but AMD GPUs require different alignment values. The newer Blade version uses the GPU's actual alignment requirements instead of hardcoding. Closes #37448 Release Notes: - Migrate to newer version of Blade upstream --- Cargo.lock | 6 +++--- Cargo.toml | 6 +++--- crates/gpui/src/platform/blade/blade_renderer.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ee80d59006f50c321e80bbe6fca9288b345524be..d31c8ecd88713f939293d022533715e39a48ed43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2291,7 +2291,7 @@ dependencies = [ [[package]] name = "blade-graphics" version = "0.6.0" -source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5" +source = "git+https://github.com/kvark/blade?rev=bfa594e#bfa594ea697d4b6326ea29f747525c85ecf933b9" dependencies = [ "ash", "ash-window", @@ -2324,7 +2324,7 @@ dependencies = [ [[package]] name = "blade-macros" version = "0.3.0" -source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5" +source = "git+https://github.com/kvark/blade?rev=bfa594e#bfa594ea697d4b6326ea29f747525c85ecf933b9" dependencies = [ "proc-macro2", "quote", @@ -2334,7 +2334,7 @@ dependencies = [ [[package]] name = "blade-util" version = "0.2.0" -source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5" +source = "git+https://github.com/kvark/blade?rev=bfa594e#bfa594ea697d4b6326ea29f747525c85ecf933b9" dependencies = [ "blade-graphics", "bytemuck", diff --git a/Cargo.toml b/Cargo.toml index 8a487b612a18dc837d3cd75697f13bf92b5b28b7..1cce7701c01bc2391c7ec7b505bd945226d7ce11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -462,9 +462,9 @@ aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] } base64 = "0.22" bincode = "1.2.1" bitflags = "2.6.0" -blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } -blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } -blade-util = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } +blade-graphics = { git = "https://github.com/kvark/blade", rev = "bfa594e" } +blade-macros = { git = "https://github.com/kvark/blade", rev = "bfa594e" } +blade-util = { git = "https://github.com/kvark/blade", rev = "bfa594e" } blake3 = "1.5.3" bytes = "1.0" cargo_metadata = "0.19" diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index cc1df7748ba6b7947ab53a86baa8ab31644ac05d..1f60920bcc928c97c1f2b2c06e22ed235217c87e 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/crates/gpui/src/platform/blade/blade_renderer.rs @@ -371,7 +371,7 @@ impl BladeRenderer { .or_else(|| { [4, 2, 1] .into_iter() - .find(|count| context.gpu.supports_texture_sample_count(*count)) + .find(|&n| (context.gpu.capabilities().sample_count_mask & n) != 0) }) .unwrap_or(1); let pipelines = BladePipelines::new(&context.gpu, surface.info(), path_sample_count); From 6e2922367c16344c44d56181ba6f7348869501b7 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 4 Sep 2025 13:41:47 -0400 Subject: [PATCH 583/744] Use full SHA for `blade` dependency (#37554) In https://github.com/zed-industries/zed/pull/37516 we updated the `blade` dependency, but used a short SHA. No reason to not use the full SHA. Release Notes: - N/A --- Cargo.lock | 6 +++--- Cargo.toml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d31c8ecd88713f939293d022533715e39a48ed43..50632ef0a4b8f23b9301c8525fe83235e49ff5f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2291,7 +2291,7 @@ dependencies = [ [[package]] name = "blade-graphics" version = "0.6.0" -source = "git+https://github.com/kvark/blade?rev=bfa594e#bfa594ea697d4b6326ea29f747525c85ecf933b9" +source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9" dependencies = [ "ash", "ash-window", @@ -2324,7 +2324,7 @@ dependencies = [ [[package]] name = "blade-macros" version = "0.3.0" -source = "git+https://github.com/kvark/blade?rev=bfa594e#bfa594ea697d4b6326ea29f747525c85ecf933b9" +source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9" dependencies = [ "proc-macro2", "quote", @@ -2334,7 +2334,7 @@ dependencies = [ [[package]] name = "blade-util" version = "0.2.0" -source = "git+https://github.com/kvark/blade?rev=bfa594e#bfa594ea697d4b6326ea29f747525c85ecf933b9" +source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9" dependencies = [ "blade-graphics", "bytemuck", diff --git a/Cargo.toml b/Cargo.toml index 1cce7701c01bc2391c7ec7b505bd945226d7ce11..6c9ca3b4a6e636adc32a6e1f48386bd240055c12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -462,9 +462,9 @@ aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] } base64 = "0.22" bincode = "1.2.1" bitflags = "2.6.0" -blade-graphics = { git = "https://github.com/kvark/blade", rev = "bfa594e" } -blade-macros = { git = "https://github.com/kvark/blade", rev = "bfa594e" } -blade-util = { git = "https://github.com/kvark/blade", rev = "bfa594e" } +blade-graphics = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" } +blade-macros = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" } +blade-util = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" } blake3 = "1.5.3" bytes = "1.0" cargo_metadata = "0.19" From caebd0cc4ddfd4a0838378d8ace632a92f682328 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:55:20 -0400 Subject: [PATCH 584/744] debugger: Fix stack frame filter crash (#37555) The crash was caused by not accounting for the fact that a range of collapse frames only counts as one entry. Causing the filter indices to overshoot for indices after collapse frames (it was counting all collapse frames instead of just one). The test missed this because it all happened in one `cx.update` closure and didn't render the stack frame list when the filter was applied. The test has been updated to account for this. Release Notes: - N/A Co-authored-by: Cole Miller --- .../src/session/running/stack_frame_list.rs | 60 ++++-- .../debugger_ui/src/tests/stack_frame_list.rs | 194 ++++++++++++------ 2 files changed, 165 insertions(+), 89 deletions(-) diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index f80173c365a047da39733c94964c473bef579e1c..e51b8da362a581c96d2872a213a8be32ff31b097 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -28,8 +28,8 @@ pub enum StackFrameListEvent { } /// Represents the filter applied to the stack frame list -#[derive(PartialEq, Eq, Copy, Clone)] -enum StackFrameFilter { +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +pub(crate) enum StackFrameFilter { /// Show all frames All, /// Show only frames from the user's code @@ -174,19 +174,29 @@ impl StackFrameList { #[cfg(test)] pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec { - self.stack_frames(cx) - .unwrap_or_default() - .into_iter() - .enumerate() - .filter(|(ix, _)| { - self.list_filter == StackFrameFilter::All - || self - .filter_entries_indices - .binary_search_by_key(&ix, |ix| ix) - .is_ok() - }) - .map(|(_, stack_frame)| stack_frame.dap) - .collect() + match self.list_filter { + StackFrameFilter::All => self + .stack_frames(cx) + .unwrap_or_default() + .into_iter() + .map(|stack_frame| stack_frame.dap) + .collect(), + StackFrameFilter::OnlyUserFrames => self + .filter_entries_indices + .iter() + .map(|ix| match &self.entries[*ix] { + StackFrameEntry::Label(label) => label, + StackFrameEntry::Collapsed(_) => panic!("Collapsed tabs should not be visible"), + StackFrameEntry::Normal(frame) => frame, + }) + .cloned() + .collect(), + } + } + + #[cfg(test)] + pub(crate) fn list_filter(&self) -> StackFrameFilter { + self.list_filter } pub fn opened_stack_frame_id(&self) -> Option { @@ -246,6 +256,7 @@ impl StackFrameList { self.entries.clear(); self.selected_ix = None; self.list_state.reset(0); + self.filter_entries_indices.clear(); cx.emit(StackFrameListEvent::BuiltEntries); cx.notify(); return; @@ -263,7 +274,7 @@ impl StackFrameList { .unwrap_or_default(); let mut filter_entries_indices = Vec::default(); - for (ix, stack_frame) in stack_frames.iter().enumerate() { + for stack_frame in stack_frames.iter() { let frame_in_visible_worktree = stack_frame.dap.source.as_ref().is_some_and(|source| { source.path.as_ref().is_some_and(|path| { worktree_prefixes @@ -273,10 +284,6 @@ impl StackFrameList { }) }); - if frame_in_visible_worktree { - filter_entries_indices.push(ix); - } - match stack_frame.dap.presentation_hint { Some(dap::StackFramePresentationHint::Deemphasize) | Some(dap::StackFramePresentationHint::Subtle) => { @@ -302,6 +309,9 @@ impl StackFrameList { first_stack_frame_with_path.get_or_insert(entries.len()); } entries.push(StackFrameEntry::Normal(stack_frame.dap.clone())); + if frame_in_visible_worktree { + filter_entries_indices.push(entries.len() - 1); + } } } } @@ -309,7 +319,6 @@ impl StackFrameList { let collapsed_entries = std::mem::take(&mut collapsed_entries); if !collapsed_entries.is_empty() { entries.push(StackFrameEntry::Collapsed(collapsed_entries)); - self.filter_entries_indices.push(entries.len() - 1); } self.entries = entries; self.filter_entries_indices = filter_entries_indices; @@ -612,7 +621,16 @@ impl StackFrameList { let entries = std::mem::take(stack_frames) .into_iter() .map(StackFrameEntry::Normal); + // HERE + let entries_len = entries.len(); self.entries.splice(ix..ix + 1, entries); + let (Ok(filtered_indices_start) | Err(filtered_indices_start)) = + self.filter_entries_indices.binary_search(&ix); + + for idx in &mut self.filter_entries_indices[filtered_indices_start..] { + *idx += entries_len - 1; + } + self.selected_ix = Some(ix); self.list_state.reset(self.entries.len()); cx.emit(StackFrameListEvent::BuiltEntries); diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 023056224e177bb053f5188ced59c059c9c8ad32..a61a31d270c9d599f30185d7da3c825c51bb7898 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -1,6 +1,6 @@ use crate::{ debugger_panel::DebugPanel, - session::running::stack_frame_list::StackFrameEntry, + session::running::stack_frame_list::{StackFrameEntry, StackFrameFilter}, tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session}, }; use dap::{ @@ -867,6 +867,28 @@ async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppC }, StackFrame { id: 4, + name: "node:internal/modules/run_main2".into(), + source: Some(dap::Source { + name: Some("run_main.js".into()), + path: Some(path!("/usr/lib/node/internal/modules/run_main2.js").into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 50, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize), + }, + StackFrame { + id: 5, name: "doSomething".into(), source: Some(dap::Source { name: Some("test.js".into()), @@ -957,83 +979,119 @@ async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppC cx.run_until_parked(); - active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| { - let stack_frame_list = debug_panel_item - .running_state() - .update(cx, |state, _| state.stack_frame_list().clone()); + let stack_frame_list = + active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| { + let stack_frame_list = debug_panel_item + .running_state() + .update(cx, |state, _| state.stack_frame_list().clone()); + + stack_frame_list.update(cx, |stack_frame_list, cx| { + stack_frame_list.build_entries(true, window, cx); + + // Verify we have the expected collapsed structure + assert_eq!( + stack_frame_list.entries(), + &vec![ + StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()), + StackFrameEntry::Collapsed(vec![ + stack_frames_for_assertions[1].clone(), + stack_frames_for_assertions[2].clone(), + stack_frames_for_assertions[3].clone() + ]), + StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()), + ] + ); + }); - stack_frame_list.update(cx, |stack_frame_list, cx| { - stack_frame_list.build_entries(true, window, cx); + stack_frame_list + }); - // Verify we have the expected collapsed structure - assert_eq!( - stack_frame_list.entries(), - &vec![ - StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()), - StackFrameEntry::Collapsed(vec![ - stack_frames_for_assertions[1].clone(), - stack_frames_for_assertions[2].clone() - ]), - StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()), - ] - ); + stack_frame_list.update(cx, |stack_frame_list, cx| { + let all_frames = stack_frame_list.flatten_entries(true, false); + assert_eq!(all_frames.len(), 5, "Should see all 5 frames initially"); - // Test 1: Verify filtering works - let all_frames = stack_frame_list.flatten_entries(true, false); - assert_eq!(all_frames.len(), 4, "Should see all 4 frames initially"); + stack_frame_list + .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx); + assert_eq!( + stack_frame_list.list_filter(), + StackFrameFilter::OnlyUserFrames + ); + }); - // Toggle to user frames only - stack_frame_list - .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx); + stack_frame_list.update(cx, |stack_frame_list, cx| { + let user_frames = stack_frame_list.dap_stack_frames(cx); + assert_eq!(user_frames.len(), 2, "Should only see 2 user frames"); + assert_eq!(user_frames[0].name, "main"); + assert_eq!(user_frames[1].name, "doSomething"); + + // Toggle back to all frames + stack_frame_list + .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx); + assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All); + }); - let user_frames = stack_frame_list.dap_stack_frames(cx); - assert_eq!(user_frames.len(), 2, "Should only see 2 user frames"); - assert_eq!(user_frames[0].name, "main"); - assert_eq!(user_frames[1].name, "doSomething"); + stack_frame_list.update(cx, |stack_frame_list, cx| { + let all_frames_again = stack_frame_list.flatten_entries(true, false); + assert_eq!( + all_frames_again.len(), + 5, + "Should see all 5 frames after toggling back" + ); - // Test 2: Verify filtering toggles correctly - // Check we can toggle back and see all frames again + // Test 3: Verify collapsed entries stay expanded + stack_frame_list.expand_collapsed_entry(1, cx); + assert_eq!( + stack_frame_list.entries(), + &vec![ + StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()), + StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()), + StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()), + StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()), + StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()), + ] + ); - // Toggle back to all frames - stack_frame_list - .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx); + stack_frame_list + .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx); + assert_eq!( + stack_frame_list.list_filter(), + StackFrameFilter::OnlyUserFrames + ); + }); - let all_frames_again = stack_frame_list.flatten_entries(true, false); - assert_eq!( - all_frames_again.len(), - 4, - "Should see all 4 frames after toggling back" - ); + stack_frame_list.update(cx, |stack_frame_list, cx| { + stack_frame_list + .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx); + assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All); + }); - // Test 3: Verify collapsed entries stay expanded - stack_frame_list.expand_collapsed_entry(1, cx); - assert_eq!( - stack_frame_list.entries(), - &vec![ - StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()), - StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()), - StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()), - StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()), - ] - ); + stack_frame_list.update(cx, |stack_frame_list, cx| { + stack_frame_list + .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx); + assert_eq!( + stack_frame_list.list_filter(), + StackFrameFilter::OnlyUserFrames + ); - // Toggle filter twice - stack_frame_list - .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx); - stack_frame_list - .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx); + assert_eq!( + stack_frame_list.dap_stack_frames(cx).as_slice(), + &[ + stack_frames_for_assertions[0].clone(), + stack_frames_for_assertions[4].clone() + ] + ); - // Verify entries remain expanded - assert_eq!( - stack_frame_list.entries(), - &vec![ - StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()), - StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()), - StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()), - StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()), - ], - "Expanded entries should remain expanded after toggling filter" - ); - }); + // Verify entries remain expanded + assert_eq!( + stack_frame_list.entries(), + &vec![ + StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()), + StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()), + StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()), + StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()), + StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()), + ], + "Expanded entries should remain expanded after toggling filter" + ); }); } From 9e111054837ef27e3a95709867215e54d3e52ac6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 4 Sep 2025 14:07:50 -0400 Subject: [PATCH 585/744] toml: Extract to zed-extensions/toml repository (#37558) This PR extracts the TOML extension to the [zed-extensions/toml](https://github.com/zed-extensions/toml) repository. Release Notes: - N/A --- .config/hakari.toml | 1 - Cargo.lock | 7 - Cargo.toml | 1 - docs/src/languages/toml.md | 2 +- extensions/toml/Cargo.toml | 16 -- extensions/toml/LICENSE-APACHE | 1 - extensions/toml/extension.toml | 18 --- extensions/toml/languages/toml/brackets.scm | 3 - extensions/toml/languages/toml/config.toml | 11 -- extensions/toml/languages/toml/highlights.scm | 38 ----- extensions/toml/languages/toml/indents.scm | 0 extensions/toml/languages/toml/outline.scm | 15 -- extensions/toml/languages/toml/overrides.scm | 2 - extensions/toml/languages/toml/redactions.scm | 1 - .../toml/languages/toml/textobjects.scm | 6 - extensions/toml/src/toml.rs | 152 ------------------ 16 files changed, 1 insertion(+), 273 deletions(-) delete mode 100644 extensions/toml/Cargo.toml delete mode 120000 extensions/toml/LICENSE-APACHE delete mode 100644 extensions/toml/extension.toml delete mode 100644 extensions/toml/languages/toml/brackets.scm delete mode 100644 extensions/toml/languages/toml/config.toml delete mode 100644 extensions/toml/languages/toml/highlights.scm delete mode 100644 extensions/toml/languages/toml/indents.scm delete mode 100644 extensions/toml/languages/toml/outline.scm delete mode 100644 extensions/toml/languages/toml/overrides.scm delete mode 100644 extensions/toml/languages/toml/redactions.scm delete mode 100644 extensions/toml/languages/toml/textobjects.scm delete mode 100644 extensions/toml/src/toml.rs diff --git a/.config/hakari.toml b/.config/hakari.toml index 8ce0b77490482ab5ff2d781fb78fd86b56959a6a..e8f094e618b39138df95bbdb58e5800cd396fad5 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -41,5 +41,4 @@ workspace-members = [ "slash_commands_example", "zed_snippets", "zed_test_extension", - "zed_toml", ] diff --git a/Cargo.lock b/Cargo.lock index 50632ef0a4b8f23b9301c8525fe83235e49ff5f8..1a15d11e664f808d4ba68d61a3ad7a6c557c2420 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20653,13 +20653,6 @@ dependencies = [ "zed_extension_api 0.6.0", ] -[[package]] -name = "zed_toml" -version = "0.1.4" -dependencies = [ - "zed_extension_api 0.1.0", -] - [[package]] name = "zeno" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index 6c9ca3b4a6e636adc32a6e1f48386bd240055c12..f389153efe9d0719187d14bb554042fcf2888376 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -211,7 +211,6 @@ members = [ "extensions/slash-commands-example", "extensions/snippets", "extensions/test-extension", - "extensions/toml", # # Tooling diff --git a/docs/src/languages/toml.md b/docs/src/languages/toml.md index eb51dbb93bf3031449744ccd4617992f46d31351..40a6b880fccce87c20a61029418490021719fb98 100644 --- a/docs/src/languages/toml.md +++ b/docs/src/languages/toml.md @@ -1,6 +1,6 @@ # TOML -TOML support is available through the [TOML extension](https://github.com/zed-industries/zed/tree/main/extensions/toml). +TOML support is available through the [TOML extension](https://github.com/zed-extensions/toml). - Tree-sitter: [tree-sitter/tree-sitter-toml](https://github.com/tree-sitter/tree-sitter-toml) - Language Server: [tamasfe/taplo](https://github.com/tamasfe/taplo) diff --git a/extensions/toml/Cargo.toml b/extensions/toml/Cargo.toml deleted file mode 100644 index 25c2c418084dc89fe4c402c1abe13d5535bf6447..0000000000000000000000000000000000000000 --- a/extensions/toml/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "zed_toml" -version = "0.1.4" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/toml.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.1.0" diff --git a/extensions/toml/LICENSE-APACHE b/extensions/toml/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3affae83854be02a0afdec3b7a9ec4d..0000000000000000000000000000000000000000 --- a/extensions/toml/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/toml/extension.toml b/extensions/toml/extension.toml deleted file mode 100644 index 5be7213c40362ec4bbeba8cb0846a507d9ec9e7e..0000000000000000000000000000000000000000 --- a/extensions/toml/extension.toml +++ /dev/null @@ -1,18 +0,0 @@ -id = "toml" -name = "TOML" -description = "TOML support." -version = "0.1.4" -schema_version = 1 -authors = [ - "Max Brunsfeld ", - "Ammar Arif " -] -repository = "https://github.com/zed-industries/zed" - -[language_servers.taplo] -name = "Taplo" -language = "TOML" - -[grammars.toml] -repository = "https://github.com/tree-sitter/tree-sitter-toml" -commit = "342d9be207c2dba869b9967124c679b5e6fd0ebe" diff --git a/extensions/toml/languages/toml/brackets.scm b/extensions/toml/languages/toml/brackets.scm deleted file mode 100644 index 9e8c9cd93c30f7697ead2161295b4583ffdfb93b..0000000000000000000000000000000000000000 --- a/extensions/toml/languages/toml/brackets.scm +++ /dev/null @@ -1,3 +0,0 @@ -("[" @open "]" @close) -("{" @open "}" @close) -("\"" @open "\"" @close) diff --git a/extensions/toml/languages/toml/config.toml b/extensions/toml/languages/toml/config.toml deleted file mode 100644 index f62290d9e9244603eaa22dc98297f84f694635e4..0000000000000000000000000000000000000000 --- a/extensions/toml/languages/toml/config.toml +++ /dev/null @@ -1,11 +0,0 @@ -name = "TOML" -grammar = "toml" -path_suffixes = ["Cargo.lock", "toml", "Pipfile", "uv.lock"] -line_comments = ["# "] -autoclose_before = ",]}" -brackets = [ - { start = "{", end = "}", close = true, newline = true }, - { start = "[", end = "]", close = true, newline = true }, - { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, - { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] }, -] diff --git a/extensions/toml/languages/toml/highlights.scm b/extensions/toml/languages/toml/highlights.scm deleted file mode 100644 index 4be265cce74b3d8916e96f428550ea405db915e0..0000000000000000000000000000000000000000 --- a/extensions/toml/languages/toml/highlights.scm +++ /dev/null @@ -1,38 +0,0 @@ -; Properties -;----------- - -(bare_key) @property -(quoted_key) @property - -; Literals -;--------- - -(boolean) @constant -(comment) @comment -(integer) @number -(float) @number -(string) @string -(escape_sequence) @string.escape -(offset_date_time) @string.special -(local_date_time) @string.special -(local_date) @string.special -(local_time) @string.special - -; Punctuation -;------------ - -[ - "." - "," -] @punctuation.delimiter - -"=" @operator - -[ - "[" - "]" - "[[" - "]]" - "{" - "}" -] @punctuation.bracket diff --git a/extensions/toml/languages/toml/indents.scm b/extensions/toml/languages/toml/indents.scm deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/extensions/toml/languages/toml/outline.scm b/extensions/toml/languages/toml/outline.scm deleted file mode 100644 index 0b3794962835a6c993e212aef5607bc859196fe9..0000000000000000000000000000000000000000 --- a/extensions/toml/languages/toml/outline.scm +++ /dev/null @@ -1,15 +0,0 @@ -(table - . - "[" - . - (_) @name) @item - -(table_array_element - . - "[[" - . - (_) @name) @item - -(pair - . - (_) @name) @item diff --git a/extensions/toml/languages/toml/overrides.scm b/extensions/toml/languages/toml/overrides.scm deleted file mode 100644 index 81fec9a5f57b28fc67b4781ec37df43559e21dc9..0000000000000000000000000000000000000000 --- a/extensions/toml/languages/toml/overrides.scm +++ /dev/null @@ -1,2 +0,0 @@ -(comment) @comment.inclusive -(string) @string diff --git a/extensions/toml/languages/toml/redactions.scm b/extensions/toml/languages/toml/redactions.scm deleted file mode 100644 index a906e9ac7b3e6561937ec7642e851a71fa2e3fec..0000000000000000000000000000000000000000 --- a/extensions/toml/languages/toml/redactions.scm +++ /dev/null @@ -1 +0,0 @@ -(pair (bare_key) "=" (_) @redact) diff --git a/extensions/toml/languages/toml/textobjects.scm b/extensions/toml/languages/toml/textobjects.scm deleted file mode 100644 index f5b4856e27a76a90d577f54fdd6104ec6bce795f..0000000000000000000000000000000000000000 --- a/extensions/toml/languages/toml/textobjects.scm +++ /dev/null @@ -1,6 +0,0 @@ -(comment)+ @comment -(table "[" (_) "]" - (_)* @class.inside) @class.around - -(table_array_element "[[" (_) "]]" - (_)* @class.inside) @class.around diff --git a/extensions/toml/src/toml.rs b/extensions/toml/src/toml.rs deleted file mode 100644 index c9b96aecacd17d192fad9b6801973c2f2389cf98..0000000000000000000000000000000000000000 --- a/extensions/toml/src/toml.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::fs; -use zed::LanguageServerId; -use zed_extension_api::settings::LspSettings; -use zed_extension_api::{self as zed, Result}; - -struct TaploBinary { - path: String, - args: Option>, -} - -struct TomlExtension { - cached_binary_path: Option, -} - -impl TomlExtension { - fn language_server_binary( - &mut self, - language_server_id: &LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - let binary_settings = LspSettings::for_worktree("taplo", worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.binary); - let binary_args = binary_settings - .as_ref() - .and_then(|binary_settings| binary_settings.arguments.clone()); - - if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) { - return Ok(TaploBinary { - path, - args: binary_args, - }); - } - - if let Some(path) = worktree.which("taplo") { - return Ok(TaploBinary { - path, - args: binary_args, - }); - } - - if let Some(path) = &self.cached_binary_path - && fs::metadata(path).is_ok_and(|stat| stat.is_file()) - { - return Ok(TaploBinary { - path: path.clone(), - args: binary_args, - }); - } - - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::CheckingForUpdate, - ); - let release = zed::latest_github_release( - "tamasfe/taplo", - zed::GithubReleaseOptions { - require_assets: true, - pre_release: false, - }, - )?; - - let (platform, arch) = zed::current_platform(); - let asset_name = format!( - "taplo-{os}-{arch}.gz", - arch = match arch { - zed::Architecture::Aarch64 => "aarch64", - zed::Architecture::X86 => "x86", - zed::Architecture::X8664 => "x86_64", - }, - os = match platform { - zed::Os::Mac => "darwin", - zed::Os::Linux => "linux", - zed::Os::Windows => "windows", - }, - ); - - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; - - let version_dir = format!("taplo-{}", release.version); - fs::create_dir_all(&version_dir) - .map_err(|err| format!("failed to create directory '{version_dir}': {err}"))?; - - let binary_path = format!( - "{version_dir}/{bin_name}", - bin_name = match platform { - zed::Os::Windows => "taplo.exe", - zed::Os::Mac | zed::Os::Linux => "taplo", - } - ); - - if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::Downloading, - ); - - zed::download_file( - &asset.download_url, - &binary_path, - zed::DownloadedFileType::Gzip, - ) - .map_err(|err| format!("failed to download file: {err}"))?; - - zed::make_file_executable(&binary_path)?; - - let entries = fs::read_dir(".") - .map_err(|err| format!("failed to list working directory {err}"))?; - for entry in entries { - let entry = entry.map_err(|err| format!("failed to load directory entry {err}"))?; - if entry.file_name().to_str() != Some(&version_dir) { - fs::remove_dir_all(entry.path()).ok(); - } - } - } - - self.cached_binary_path = Some(binary_path.clone()); - Ok(TaploBinary { - path: binary_path, - args: binary_args, - }) - } -} - -impl zed::Extension for TomlExtension { - fn new() -> Self { - Self { - cached_binary_path: None, - } - } - - fn language_server_command( - &mut self, - language_server_id: &LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - let taplo_binary = self.language_server_binary(language_server_id, worktree)?; - Ok(zed::Command { - command: taplo_binary.path, - args: taplo_binary - .args - .unwrap_or_else(|| vec!["lsp".to_string(), "stdio".to_string()]), - env: Default::default(), - }) - } -} - -zed::register_extension!(TomlExtension); From 9d943589713f20767a355ba6f0d108d59bc31482 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 4 Sep 2025 14:33:56 -0400 Subject: [PATCH 586/744] acp: Keep diff editors in sync with `AgentFontSize` global (#37559) Release Notes: - agent: Fixed `cmd-+` and `cmd--` not affecting the font size of diffs. --- crates/agent_ui/src/acp/entry_view_state.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 11 ++++++----- crates/theme/src/settings.rs | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index e60b923ca78c4613e9b8d8063a280f560d788d44..ec57ea7e6df3244b6ea1bcb99212d845fa68c457 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -207,7 +207,7 @@ impl EntryViewState { self.entries.drain(range); } - pub fn settings_changed(&mut self, cx: &mut App) { + pub fn agent_font_size_changed(&mut self, cx: &mut App) { for entry in self.entries.iter() { match entry { Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {} diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 3407f4e878e6452322aba1b5009b582f322db4b4..b4d56ad05be1a66e9740c2432a9bd08b1adfee0e 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -43,7 +43,7 @@ use std::{collections::BTreeMap, rc::Rc, time::Duration}; use task::SpawnInTerminal; use terminal_view::terminal_panel::TerminalPanel; use text::Anchor; -use theme::ThemeSettings; +use theme::{AgentFontSize, ThemeSettings}; use ui::{ Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*, @@ -290,7 +290,7 @@ pub struct AcpThreadView { is_loading_contents: bool, new_server_version_available: Option, _cancel_task: Option>, - _subscriptions: [Subscription; 3], + _subscriptions: [Subscription; 4], } enum ThreadState { @@ -380,7 +380,8 @@ impl AcpThreadView { }); let subscriptions = [ - cx.observe_global_in::(window, Self::settings_changed), + cx.observe_global_in::(window, Self::agent_font_size_changed), + cx.observe_global_in::(window, Self::agent_font_size_changed), cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event), cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event), ]; @@ -4735,9 +4736,9 @@ impl AcpThreadView { ) } - fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context) { + fn agent_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context) { self.entry_view_state.update(cx, |entry_view_state, cx| { - entry_view_state.settings_changed(cx); + entry_view_state.agent_font_size_changed(cx); }); } diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 11db22d97485f5d400abdd8638da501abd55a192..825176a2a0b5e35c60606d0922cef37fe91caea7 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -253,8 +253,9 @@ pub(crate) struct UiFontSize(Pixels); impl Global for UiFontSize {} +/// In-memory override for the font size in the agent panel. #[derive(Default)] -pub(crate) struct AgentFontSize(Pixels); +pub struct AgentFontSize(Pixels); impl Global for AgentFontSize {} From a85946eba8cd2791a716f90b49df71c81002c3d3 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 4 Sep 2025 14:54:32 -0400 Subject: [PATCH 587/744] docs: Update TOML docs (#37561) This PR updates the TOML docs to remove references to Taplo and suggest the Tombi extension for users wanting language server support. Relates to https://github.com/zed-industries/zed/issues/36766. Release Notes: - N/A --- docs/src/languages/toml.md | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/docs/src/languages/toml.md b/docs/src/languages/toml.md index 40a6b880fccce87c20a61029418490021719fb98..46b93b67eb4ba85dea0c297adbfe1a261b6a22dc 100644 --- a/docs/src/languages/toml.md +++ b/docs/src/languages/toml.md @@ -1,22 +1,7 @@ # TOML -TOML support is available through the [TOML extension](https://github.com/zed-extensions/toml). +TOML support is available through the [TOML extension](https://zed.dev/extensions/toml). - Tree-sitter: [tree-sitter/tree-sitter-toml](https://github.com/tree-sitter/tree-sitter-toml) -- Language Server: [tamasfe/taplo](https://github.com/tamasfe/taplo) -## Configuration - -You can control the behavior of the Taplo TOML language server by adding a `.taplo.toml` file to the root of your project. See the [Taplo Configuration File](https://taplo.tamasfe.dev/configuration/file.html#configuration-file) and [Taplo Formatter Options](https://taplo.tamasfe.dev/configuration/formatter-options.html) documentation for more. - -```toml -# .taplo.toml -[formatting] -align_comments = false -reorder_keys = true - -include = ["Cargo.toml", "some_directory/**/*.toml"] -# exclude = ["vendor/**/*.toml"] -``` - -Note: The taplo language server will not automatically pickup changes to `.taplo.toml`. You must manually trigger {#action editor::RestartLanguageServer} or reload Zed for it to pickup changes. +A TOML language server is available in the [Tombi extension](https://zed.dev/extensions/tombi). From 223fda2fe221e5c6b3bd90b61b8e1f444203a6f6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 4 Sep 2025 22:05:21 +0300 Subject: [PATCH 588/744] Make remote projects to sync in local user settings (#37560) Closes https://github.com/zed-industries/zed/issues/20024 Closes https://github.com/zed-industries/zed/issues/23489 https://github.com/user-attachments/assets/6466e0c1-4188-4980-8bb6-52ef6e7591c9 Release Notes: - Made remote projects to sync in local user settings --- crates/project/src/project.rs | 9 ++- crates/project/src/project_settings.rs | 55 +++++++++++++++++-- crates/proto/proto/worktree.proto | 5 ++ crates/proto/proto/zed.proto | 4 +- crates/proto/src/proto.rs | 2 + .../remote_server/src/remote_editing_tests.rs | 6 +- crates/remote_server/src/unix.rs | 48 ++++++++-------- crates/settings/src/settings_store.rs | 21 +++---- 8 files changed, 105 insertions(+), 45 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 46dd3b7d9e51aa06aa45b9cccb87533f2b90f58c..4adebabc5a03636ca81fbc3b04a277c2d6d03a66 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1271,6 +1271,7 @@ impl Project { fs.clone(), worktree_store.clone(), task_store.clone(), + Some(remote_proto.clone()), cx, ) }); @@ -1521,7 +1522,13 @@ impl Project { })?; let settings_observer = cx.new(|cx| { - SettingsObserver::new_remote(fs.clone(), worktree_store.clone(), task_store.clone(), cx) + SettingsObserver::new_remote( + fs.clone(), + worktree_store.clone(), + task_store.clone(), + None, + cx, + ) })?; let git_store = cx.new(|cx| { diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index c98065116e00fd6c643a2c809cf6e8fb1c51532b..57969ec9938602b477293aa3033a31bc8b3deae1 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -4,7 +4,7 @@ use context_server::ContextServerCommand; use dap::adapters::DebugAdapterName; use fs::Fs; use futures::StreamExt as _; -use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Task}; +use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Subscription, Task}; use lsp::LanguageServerName; use paths::{ EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path, @@ -13,7 +13,7 @@ use paths::{ }; use rpc::{ AnyProtoClient, TypedEnvelope, - proto::{self, FromProto, ToProto}, + proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto}, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -658,6 +658,7 @@ pub struct SettingsObserver { worktree_store: Entity, project_id: u64, task_store: Entity, + _user_settings_watcher: Option, _global_task_config_watcher: Task<()>, _global_debug_config_watcher: Task<()>, } @@ -670,6 +671,7 @@ pub struct SettingsObserver { impl SettingsObserver { pub fn init(client: &AnyProtoClient) { client.add_entity_message_handler(Self::handle_update_worktree_settings); + client.add_entity_message_handler(Self::handle_update_user_settings); } pub fn new_local( @@ -686,7 +688,8 @@ impl SettingsObserver { task_store, mode: SettingsObserverMode::Local(fs.clone()), downstream_client: None, - project_id: 0, + _user_settings_watcher: None, + project_id: REMOTE_SERVER_PROJECT_ID, _global_task_config_watcher: Self::subscribe_to_global_task_file_changes( fs.clone(), paths::tasks_file().clone(), @@ -704,14 +707,38 @@ impl SettingsObserver { fs: Arc, worktree_store: Entity, task_store: Entity, + upstream_client: Option, cx: &mut Context, ) -> Self { + let mut user_settings_watcher = None; + if cx.try_global::().is_some() { + if let Some(upstream_client) = upstream_client { + let mut user_settings = None; + user_settings_watcher = Some(cx.observe_global::(move |_, cx| { + let new_settings = cx.global::().raw_user_settings(); + if Some(new_settings) != user_settings.as_ref() { + if let Some(new_settings_string) = serde_json::to_string(new_settings).ok() + { + user_settings = Some(new_settings.clone()); + upstream_client + .send(proto::UpdateUserSettings { + project_id: REMOTE_SERVER_PROJECT_ID, + contents: new_settings_string, + }) + .log_err(); + } + } + })); + } + }; + Self { worktree_store, task_store, mode: SettingsObserverMode::Remote, downstream_client: None, - project_id: 0, + project_id: REMOTE_SERVER_PROJECT_ID, + _user_settings_watcher: user_settings_watcher, _global_task_config_watcher: Self::subscribe_to_global_task_file_changes( fs.clone(), paths::tasks_file().clone(), @@ -803,6 +830,24 @@ impl SettingsObserver { Ok(()) } + async fn handle_update_user_settings( + _: Entity, + envelope: TypedEnvelope, + cx: AsyncApp, + ) -> anyhow::Result<()> { + let new_settings = serde_json::from_str::(&envelope.payload.contents) + .with_context(|| { + format!("deserializing {} user settings", envelope.payload.contents) + })?; + cx.update_global(|settings_store: &mut SettingsStore, cx| { + settings_store + .set_raw_user_settings(new_settings, cx) + .context("setting new user settings")?; + anyhow::Ok(()) + })??; + Ok(()) + } + fn on_worktree_store_event( &mut self, _: Entity, @@ -1089,7 +1134,7 @@ impl SettingsObserver { project_id: self.project_id, worktree_id: remote_worktree_id.to_proto(), path: directory.to_proto(), - content: file_content, + content: file_content.clone(), kind: Some(local_settings_kind_to_proto(kind).into()), }) .log_err(); diff --git a/crates/proto/proto/worktree.proto b/crates/proto/proto/worktree.proto index 67bd1925b509c6fc7727fa5cf6338e6cc00a4ae0..19a61cc4bc8d3b04103afe3a6c6b799ab92461e3 100644 --- a/crates/proto/proto/worktree.proto +++ b/crates/proto/proto/worktree.proto @@ -150,3 +150,8 @@ enum LocalSettingsKind { Editorconfig = 2; Debug = 3; } + +message UpdateUserSettings { + uint64 project_id = 1; + string contents = 2; +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 2222bdec082759cb75ffcdb2c7a95435f36eba11..4133b4b5eea6f14e2c9359f7318f192a8566d809 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -397,7 +397,9 @@ message Envelope { LspQuery lsp_query = 365; LspQueryResponse lsp_query_response = 366; - ToggleLspLogs toggle_lsp_logs = 367; // current max + ToggleLspLogs toggle_lsp_logs = 367; + + UpdateUserSettings update_user_settings = 368; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 04495fb898b1d9bdbf229bb69e1e44b8afa6d1fb..8f4e836b20ae5bae43617e10391f75c3a069a82f 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -278,6 +278,7 @@ messages!( (UpdateUserChannels, Foreground), (UpdateWorktree, Foreground), (UpdateWorktreeSettings, Foreground), + (UpdateUserSettings, Background), (UpdateRepository, Foreground), (RemoveRepository, Foreground), (UsersResponse, Foreground), @@ -583,6 +584,7 @@ entity_messages!( UpdateRepository, RemoveRepository, UpdateWorktreeSettings, + UpdateUserSettings, LspExtExpandMacro, LspExtOpenDocs, LspExtRunnables, diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 353857f5871551a20315f638aa3d9653b3ed2848..c0ccaf900d18ee176bab7193c2bfb65b8555318d 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -280,7 +280,8 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo AllLanguageSettings::get_global(cx) .language(None, Some(&"Rust".into()), cx) .language_servers, - ["..."] // local settings are ignored + ["from-local-settings"], + "User language settings should be synchronized with the server settings" ) }); @@ -300,7 +301,8 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo AllLanguageSettings::get_global(cx) .language(None, Some(&"Rust".into()), cx) .language_servers, - ["from-server-settings".to_string()] + ["from-server-settings".to_string()], + "Server language settings should take precedence over the user settings" ) }); diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index cb671a72d9beab0983536571e81fcd78f3df21c8..4aef536f0a45b5ea943f861da2be94ab7c2c21c4 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -918,29 +918,33 @@ fn initialize_settings( }); let (mut tx, rx) = watch::channel(None); + let mut node_settings = None; cx.observe_global::(move |cx| { - let settings = &ProjectSettings::get_global(cx).node; - log::info!("Got new node settings: {:?}", settings); - let options = NodeBinaryOptions { - allow_path_lookup: !settings.ignore_system_version, - // TODO: Implement this setting - allow_binary_download: true, - use_paths: settings.path.as_ref().map(|node_path| { - let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref()); - let npm_path = settings - .npm_path - .as_ref() - .map(|path| PathBuf::from(shellexpand::tilde(&path).as_ref())); - ( - node_path.clone(), - npm_path.unwrap_or_else(|| { - let base_path = PathBuf::new(); - node_path.parent().unwrap_or(&base_path).join("npm") - }), - ) - }), - }; - tx.send(Some(options)).log_err(); + let new_node_settings = &ProjectSettings::get_global(cx).node; + if Some(new_node_settings) != node_settings.as_ref() { + log::info!("Got new node settings: {new_node_settings:?}"); + let options = NodeBinaryOptions { + allow_path_lookup: !new_node_settings.ignore_system_version, + // TODO: Implement this setting + allow_binary_download: true, + use_paths: new_node_settings.path.as_ref().map(|node_path| { + let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref()); + let npm_path = new_node_settings + .npm_path + .as_ref() + .map(|path| PathBuf::from(shellexpand::tilde(&path).as_ref())); + ( + node_path.clone(), + npm_path.unwrap_or_else(|| { + let base_path = PathBuf::new(); + node_path.parent().unwrap_or(&base_path).join("npm") + }), + ) + }), + }; + node_settings = Some(new_node_settings.clone()); + tx.send(Some(options)).ok(); + } }) .detach(); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 60eb132ad8b4f6419f463f32b1874ea97be07ec1..72df08d14fb61536d147b4d1fb8b9a2466f5f0aa 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -467,6 +467,13 @@ impl SettingsStore { &self.raw_user_settings } + /// Replaces current settings with the values from the given JSON. + pub fn set_raw_user_settings(&mut self, new_settings: Value, cx: &mut App) -> Result<()> { + self.raw_user_settings = new_settings; + self.recompute_values(None, cx)?; + Ok(()) + } + /// Get the configured settings profile names. pub fn configured_settings_profiles(&self) -> impl Iterator { self.raw_user_settings @@ -525,20 +532,6 @@ impl SettingsStore { } } - pub async fn load_global_settings(fs: &Arc) -> Result { - match fs.load(paths::global_settings_file()).await { - result @ Ok(_) => result, - Err(err) => { - if let Some(e) = err.downcast_ref::() - && e.kind() == std::io::ErrorKind::NotFound - { - return Ok("{}".to_string()); - } - Err(err) - } - } - } - fn update_settings_file_inner( &self, fs: Arc, From 5f03202b5cda0baff80212af07f80454c8aca1cd Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 4 Sep 2025 15:19:02 -0400 Subject: [PATCH 589/744] settings ui: Create settings key trait (#37489) This PR separates out the associated constant `KEY` from the `Settings` trait into a new trait `SettingsKey`. This allows for the key trait to be derived using attributes to specify the path so that the new `SettingsUi` derive macro can use the same attributes to determine top level settings paths thereby removing the need to duplicate the path in both `Settings::KEY` and `#[settings_ui(path = "...")]` Co-authored-by: Ben Kunkle Release Notes: - N/A --------- Co-authored-by: Ben Kunkle --- Cargo.lock | 2 + crates/agent_servers/src/settings.rs | 7 +- crates/agent_settings/src/agent_settings.rs | 9 +- crates/agent_ui/src/slash_command_settings.rs | 7 +- crates/audio/src/audio_settings.rs | 9 +- crates/auto_update/src/auto_update.rs | 7 +- crates/call/src/call_settings.rs | 7 +- crates/client/src/client.rs | 17 +-- crates/collab_ui/src/panel_settings.rs | 22 ++- crates/dap/src/debugger_settings.rs | 9 +- crates/editor/src/editor_settings.rs | 7 +- .../extension_host/src/extension_settings.rs | 7 +- crates/extensions_ui/src/extensions_ui.rs | 2 +- .../file_finder/src/file_finder_settings.rs | 7 +- crates/git_hosting_providers/src/settings.rs | 7 +- crates/git_ui/src/git_panel_settings.rs | 7 +- crates/go_to_line/src/cursor_position.rs | 7 +- .../image_viewer/src/image_viewer_settings.rs | 7 +- crates/journal/src/journal.rs | 7 +- crates/language/src/language_settings.rs | 10 +- crates/language_models/src/settings.rs | 9 +- crates/onboarding/src/ai_setup_page.rs | 10 +- crates/onboarding/src/basics_page.rs | 2 +- .../src/outline_panel_settings.rs | 7 +- crates/project/src/project.rs | 79 +++++++---- crates/project/src/project_settings.rs | 9 +- .../src/project_panel_settings.rs | 7 +- .../recent_projects/src/remote_connections.rs | 7 +- crates/repl/src/jupyter_settings.rs | 7 +- crates/settings/src/base_keymap_setting.rs | 19 ++- crates/settings/src/settings.rs | 6 +- crates/settings/src/settings_store.rs | 97 +++++++++---- .../src/settings_ui_macros.rs | 134 +++++++++++++++++- crates/terminal/src/terminal_settings.rs | 7 +- crates/theme/src/settings.rs | 7 +- crates/title_bar/src/title_bar_settings.rs | 11 +- crates/vim/src/test/vim_test_context.rs | 8 +- crates/vim/src/vim.rs | 11 +- crates/vim_mode_setting/Cargo.toml | 2 + .../vim_mode_setting/src/vim_mode_setting.rs | 78 +++++++--- crates/workspace/src/item.rs | 12 +- crates/workspace/src/workspace_settings.rs | 12 +- crates/worktree/src/worktree_settings.rs | 7 +- crates/zlog_settings/src/zlog_settings.rs | 18 ++- 44 files changed, 474 insertions(+), 256 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a15d11e664f808d4ba68d61a3ad7a6c557c2420..a99c59a1890080ac220b669b26864d859d2ad377 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17995,6 +17995,8 @@ version = "0.1.0" dependencies = [ "anyhow", "gpui", + "schemars", + "serde", "settings", "workspace-hack", ] diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 693d7d7b7014b3abbecfbe592bac67210b336872..167753296a1a489128ba916f114f4895c15afcf9 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -6,13 +6,14 @@ use collections::HashMap; use gpui::{App, SharedString}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; pub fn init(cx: &mut App) { AllAgentServersSettings::register(cx); } -#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi)] +#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)] +#[settings_key(key = "agent_servers")] pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, @@ -75,8 +76,6 @@ pub struct CustomAgentServerSettings { } impl settings::Settings for AllAgentServersSettings { - const KEY: Option<&'static str> = Some("agent_servers"); - type FileContent = Self; fn load(sources: SettingsSources, _: &mut App) -> Result { diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 8aebdcd288c8451d9bc391f1fc1598d6098d55af..8c4a190e1c3135b5bbfbc90544bb92db7a6bdd22 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -8,7 +8,7 @@ use gpui::{App, Pixels, SharedString}; use language_model::LanguageModel; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; use std::borrow::Cow; pub use crate::agent_profile::*; @@ -223,7 +223,8 @@ impl AgentSettingsContent { } } -#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi)] +#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)] +#[settings_key(key = "agent", fallback_key = "assistant")] pub struct AgentSettingsContent { /// Whether the Agent is enabled. /// @@ -399,10 +400,6 @@ pub struct ContextServerPresetContent { } impl Settings for AgentSettings { - const KEY: Option<&'static str> = Some("agent"); - - const FALLBACK_KEY: Option<&'static str> = Some("assistant"); - const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]); type FileContent = AgentSettingsContent; diff --git a/crates/agent_ui/src/slash_command_settings.rs b/crates/agent_ui/src/slash_command_settings.rs index c54a10ed49a77d395c4968e551b1cd30ad1c6e07..9580ffef0f317fbe726c57041fad4f0fa438e143 100644 --- a/crates/agent_ui/src/slash_command_settings.rs +++ b/crates/agent_ui/src/slash_command_settings.rs @@ -2,10 +2,11 @@ use anyhow::Result; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; /// Settings for slash commands. -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)] +#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(key = "slash_commands")] pub struct SlashCommandSettings { /// Settings for the `/cargo-workspace` slash command. #[serde(default)] @@ -21,8 +22,6 @@ pub struct CargoWorkspaceCommandSettings { } impl Settings for SlashCommandSettings { - const KEY: Option<&'static str> = Some("slash_commands"); - type FileContent = Self; fn load(sources: SettingsSources, _cx: &mut App) -> Result { diff --git a/crates/audio/src/audio_settings.rs b/crates/audio/src/audio_settings.rs index d30d950273f2138f3bd54c573513373574f1ce43..168519030bcbd4a422965580ddbe01121934278d 100644 --- a/crates/audio/src/audio_settings.rs +++ b/crates/audio/src/audio_settings.rs @@ -2,9 +2,9 @@ use anyhow::Result; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] pub struct AudioSettings { /// Opt into the new audio system. #[serde(rename = "experimental.rodio_audio", default)] @@ -12,8 +12,9 @@ pub struct AudioSettings { } /// Configuration of audio in Zed. -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] #[serde(default)] +#[settings_key(key = "audio")] pub struct AudioSettingsContent { /// Whether to use the experimental audio system #[serde(rename = "experimental.rodio_audio", default)] @@ -21,8 +22,6 @@ pub struct AudioSettingsContent { } impl Settings for AudioSettings { - const KEY: Option<&'static str> = Some("audio"); - type FileContent = AudioSettingsContent; fn load(sources: SettingsSources, _cx: &mut App) -> Result { diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index f0ae3fdb1cfef667a9f737aa6545a42046a9d322..f5d4533a9ee042e62752f26b989bc75561c534ae 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -10,7 +10,7 @@ use paths::remote_servers_dir; use release_channel::{AppCommitSha, ReleaseChannel}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsStore, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi}; use smol::{fs, io::AsyncReadExt}; use smol::{fs::File, process::Command}; use std::{ @@ -118,13 +118,12 @@ struct AutoUpdateSetting(bool); /// Whether or not to automatically check for updates. /// /// Default: true -#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi)] +#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey)] #[serde(transparent)] +#[settings_key(key = "auto_update")] struct AutoUpdateSettingContent(bool); impl Settings for AutoUpdateSetting { - const KEY: Option<&'static str> = Some("auto_update"); - type FileContent = AutoUpdateSettingContent; fn load(sources: SettingsSources, _: &mut App) -> Result { diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs index 7b0838e3a96185c1e4c33b8116fbd6a41b35f3dc..b0677e3c3bcb5112fdd9ad2abc4bf188b225aeac 100644 --- a/crates/call/src/call_settings.rs +++ b/crates/call/src/call_settings.rs @@ -2,7 +2,7 @@ use anyhow::Result; use gpui::App; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; #[derive(Deserialize, Debug)] pub struct CallSettings { @@ -11,7 +11,8 @@ pub struct CallSettings { } /// Configuration of voice calls in Zed. -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] +#[settings_key(key = "calls")] pub struct CallSettingsContent { /// Whether the microphone should be muted when joining a channel or a call. /// @@ -25,8 +26,6 @@ pub struct CallSettingsContent { } impl Settings for CallSettings { - const KEY: Option<&'static str> = Some("calls"); - type FileContent = CallSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> Result { diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 85f6aeade69cc04c5f58b72258ac062157094460..cb8185c7ed326ed7d45726a99077c53903118316 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -31,7 +31,7 @@ use release_channel::{AppVersion, ReleaseChannel}; use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; use std::{ any::TypeId, convert::TryFrom, @@ -96,7 +96,8 @@ actions!( ] ); -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(None)] pub struct ClientSettingsContent { server_url: Option, } @@ -107,8 +108,6 @@ pub struct ClientSettings { } impl Settings for ClientSettings { - const KEY: Option<&'static str> = None; - type FileContent = ClientSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> Result { @@ -122,7 +121,8 @@ impl Settings for ClientSettings { fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } -#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(None)] pub struct ProxySettingsContent { proxy: Option, } @@ -133,8 +133,6 @@ pub struct ProxySettings { } impl Settings for ProxySettings { - const KEY: Option<&'static str> = None; - type FileContent = ProxySettingsContent; fn load(sources: SettingsSources, _: &mut App) -> Result { @@ -527,7 +525,8 @@ pub struct TelemetrySettings { } /// Control what info is collected by Zed. -#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] +#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] +#[settings_key(key = "telemetry")] pub struct TelemetrySettingsContent { /// Send debug info like crash reports. /// @@ -540,8 +539,6 @@ pub struct TelemetrySettingsContent { } impl settings::Settings for TelemetrySettings { - const KEY: Option<&'static str> = Some("telemetry"); - type FileContent = TelemetrySettingsContent; fn load(sources: SettingsSources, _: &mut App) -> Result { diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index 64f0a9366df7cdef1f2c05809752fb1cf912111b..bae118d819c2e38e7b77e5aa841c084e4c45d6e8 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/crates/collab_ui/src/panel_settings.rs @@ -1,7 +1,7 @@ use gpui::Pixels; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; use workspace::dock::DockPosition; #[derive(Deserialize, Debug)] @@ -27,7 +27,8 @@ pub struct ChatPanelSettings { pub default_width: Pixels, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] +#[settings_key(key = "chat_panel")] pub struct ChatPanelSettingsContent { /// When to show the panel button in the status bar. /// @@ -43,14 +44,16 @@ pub struct ChatPanelSettingsContent { pub default_width: Option, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, SettingsKey)] +#[settings_key(key = "notification_panel")] pub struct NotificationPanelSettings { pub button: bool, pub dock: DockPosition, pub default_width: Pixels, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] +#[settings_key(key = "collaboration_panel")] pub struct PanelSettingsContent { /// Whether to show the panel button in the status bar. /// @@ -66,7 +69,8 @@ pub struct PanelSettingsContent { pub default_width: Option, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] +#[settings_key(key = "message_editor")] pub struct MessageEditorSettings { /// Whether to automatically replace emoji shortcodes with emoji characters. /// For example: typing `:wave:` gets replaced with `👋`. @@ -76,8 +80,6 @@ pub struct MessageEditorSettings { } impl Settings for CollaborationPanelSettings { - const KEY: Option<&'static str> = Some("collaboration_panel"); - type FileContent = PanelSettingsContent; fn load( @@ -91,8 +93,6 @@ impl Settings for CollaborationPanelSettings { } impl Settings for ChatPanelSettings { - const KEY: Option<&'static str> = Some("chat_panel"); - type FileContent = ChatPanelSettingsContent; fn load( @@ -106,8 +106,6 @@ impl Settings for ChatPanelSettings { } impl Settings for NotificationPanelSettings { - const KEY: Option<&'static str> = Some("notification_panel"); - type FileContent = PanelSettingsContent; fn load( @@ -121,8 +119,6 @@ impl Settings for NotificationPanelSettings { } impl Settings for MessageEditorSettings { - const KEY: Option<&'static str> = Some("message_editor"); - type FileContent = MessageEditorSettings; fn load( diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs index 929bff747e8685ec9a4b36fa9db63d12a769faa2..8d53fdea8649f1c62fa74cc6f0ddd6aec6ecff6d 100644 --- a/crates/dap/src/debugger_settings.rs +++ b/crates/dap/src/debugger_settings.rs @@ -2,7 +2,7 @@ use dap_types::SteppingGranularity; use gpui::{App, Global}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)] #[serde(rename_all = "snake_case")] @@ -12,11 +12,12 @@ pub enum DebugPanelDockPosition { Right, } -#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi)] +#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi, SettingsKey)] #[serde(default)] // todo(settings_ui) @ben: I'm pretty sure not having the fields be optional here is a bug, // it means the defaults will override previously set values if a single key is missing -#[settings_ui(group = "Debugger", path = "debugger")] +#[settings_ui(group = "Debugger")] +#[settings_key(key = "debugger")] pub struct DebuggerSettings { /// Determines the stepping granularity. /// @@ -64,8 +65,6 @@ impl Default for DebuggerSettings { } impl Settings for DebuggerSettings { - const KEY: Option<&'static str> = Some("debugger"); - type FileContent = Self; fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 44cb0749760e9e3af91bc837df0ef0589e251703..d74244131e6635c7b9eda6ace0723ced96b0e041 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -6,7 +6,7 @@ use language::CursorShape; use project::project_settings::DiagnosticSeverity; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi, VsCodeSettings}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi, VsCodeSettings}; use util::serde::default_true; /// Imports from the VSCode settings at @@ -431,8 +431,9 @@ pub enum SnippetSortOrder { None, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] #[settings_ui(group = "Editor")] +#[settings_key(None)] pub struct EditorSettingsContent { /// Whether the cursor blinks in the editor. /// @@ -777,8 +778,6 @@ impl EditorSettings { } impl Settings for EditorSettings { - const KEY: Option<&'static str> = None; - type FileContent = EditorSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { diff --git a/crates/extension_host/src/extension_settings.rs b/crates/extension_host/src/extension_settings.rs index 6bd760795cec6d1c4208770f1355e8ac7a34eb95..fa5a613c55a76a0b5660b114d49acc17fcf79120 100644 --- a/crates/extension_host/src/extension_settings.rs +++ b/crates/extension_host/src/extension_settings.rs @@ -3,10 +3,11 @@ use collections::HashMap; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; use std::sync::Arc; -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)] +#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(None)] pub struct ExtensionSettings { /// The extensions that should be automatically installed by Zed. /// @@ -38,8 +39,6 @@ impl ExtensionSettings { } impl Settings for ExtensionSettings { - const KEY: Option<&'static str> = None; - type FileContent = Self; fn load(sources: SettingsSources, _cx: &mut App) -> Result { diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index fd504764b65826ea74e092ea4c11d5576fa51524..0b925dceb1544d97a77082881626bc1e97f3d1b0 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -1345,7 +1345,7 @@ impl ExtensionsPage { this.update_settings::( selection, cx, - |setting, value| *setting = Some(value), + |setting, value| setting.vim_mode = Some(value), ); }), )), diff --git a/crates/file_finder/src/file_finder_settings.rs b/crates/file_finder/src/file_finder_settings.rs index 007af53b1144ed4caa7985d75cdf4707f13ed13e..6a6b98b8ea3e1c7e7f0e3cc0385fdd7f413b659f 100644 --- a/crates/file_finder/src/file_finder_settings.rs +++ b/crates/file_finder/src/file_finder_settings.rs @@ -1,7 +1,7 @@ use anyhow::Result; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; #[derive(Deserialize, Debug, Clone, Copy, PartialEq)] pub struct FileFinderSettings { @@ -11,7 +11,8 @@ pub struct FileFinderSettings { pub include_ignored: Option, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] +#[settings_key(key = "file_finder")] pub struct FileFinderSettingsContent { /// Whether to show file icons in the file finder. /// @@ -42,8 +43,6 @@ pub struct FileFinderSettingsContent { } impl Settings for FileFinderSettings { - const KEY: Option<&'static str> = Some("file_finder"); - type FileContent = FileFinderSettingsContent; fn load(sources: SettingsSources, _: &mut gpui::App) -> Result { diff --git a/crates/git_hosting_providers/src/settings.rs b/crates/git_hosting_providers/src/settings.rs index 34e3805a39ea8a13a6a2f79552a6a917c4597692..3249981db91015479bab728484341519db357683 100644 --- a/crates/git_hosting_providers/src/settings.rs +++ b/crates/git_hosting_providers/src/settings.rs @@ -5,7 +5,7 @@ use git::GitHostingProviderRegistry; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsStore, SettingsUi}; use url::Url; use util::ResultExt as _; @@ -78,7 +78,8 @@ pub struct GitHostingProviderConfig { pub name: String, } -#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(None)] pub struct GitHostingProviderSettings { /// The list of custom Git hosting providers. #[serde(default)] @@ -86,8 +87,6 @@ pub struct GitHostingProviderSettings { } impl Settings for GitHostingProviderSettings { - const KEY: Option<&'static str> = None; - type FileContent = Self; fn load(sources: settings::SettingsSources, _: &mut App) -> Result { diff --git a/crates/git_ui/src/git_panel_settings.rs b/crates/git_ui/src/git_panel_settings.rs index 39d6540db52046845521a23c0290be4e6e595492..be207314acd82446566dffd2eb58339974f177ff 100644 --- a/crates/git_ui/src/git_panel_settings.rs +++ b/crates/git_ui/src/git_panel_settings.rs @@ -2,7 +2,7 @@ use editor::ShowScrollbar; use gpui::Pixels; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; use workspace::dock::DockPosition; #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -36,7 +36,8 @@ pub enum StatusStyle { LabelColor, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] +#[settings_key(key = "git_panel")] pub struct GitPanelSettingsContent { /// Whether to show the panel button in the status bar. /// @@ -90,8 +91,6 @@ pub struct GitPanelSettings { } impl Settings for GitPanelSettings { - const KEY: Option<&'static str> = Some("git_panel"); - type FileContent = GitPanelSettingsContent; fn load( diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 5840993ece84b1e8098ee341395e7f77fb791ace..6af8c79fe9cc4ed0be0d7cb466753fa939355eec 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -2,7 +2,7 @@ use editor::{Editor, EditorSettings, MultiBufferSnapshot}; use gpui::{App, Entity, FocusHandle, Focusable, Subscription, Task, WeakEntity}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; use std::{fmt::Write, num::NonZeroU32, time::Duration}; use text::{Point, Selection}; use ui::{ @@ -301,13 +301,12 @@ pub(crate) enum LineIndicatorFormat { Long, } -#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi)] +#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey)] #[serde(transparent)] +#[settings_key(key = "line_indicator_format")] pub(crate) struct LineIndicatorFormatContent(LineIndicatorFormat); impl Settings for LineIndicatorFormat { - const KEY: Option<&'static str> = Some("line_indicator_format"); - type FileContent = LineIndicatorFormatContent; fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { diff --git a/crates/image_viewer/src/image_viewer_settings.rs b/crates/image_viewer/src/image_viewer_settings.rs index 4949b266b4e03c7089d4bc25e2a223a0ce64a081..510de69b522fbb07cb8eedba43edfe3a95e4a591 100644 --- a/crates/image_viewer/src/image_viewer_settings.rs +++ b/crates/image_viewer/src/image_viewer_settings.rs @@ -1,10 +1,11 @@ use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; /// The settings for the image viewer. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, SettingsUi)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, SettingsUi, SettingsKey)] +#[settings_key(key = "image_viewer")] pub struct ImageViewerSettings { /// The unit to use for displaying image file sizes. /// @@ -24,8 +25,6 @@ pub enum ImageFileSizeUnit { } impl Settings for ImageViewerSettings { - const KEY: Option<&'static str> = Some("image_viewer"); - type FileContent = Self; fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index ffa24571c88a0f0e06252565261b1a6d285d098c..5cdfa6c1df034deaf06e1c99ea99415757b84c29 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -5,7 +5,7 @@ use editor::{Editor, SelectionEffects}; use gpui::{App, AppContext as _, Context, Window, actions}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; use std::{ fs::OpenOptions, path::{Path, PathBuf}, @@ -22,7 +22,8 @@ actions!( ); /// Settings specific to journaling -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(key = "journal")] pub struct JournalSettings { /// The path of the directory where journal entries are stored. /// @@ -52,8 +53,6 @@ pub enum HourFormat { } impl settings::Settings for JournalSettings { - const KEY: Option<&'static str> = Some("journal"); - type FileContent = Self; fn load(sources: SettingsSources, _: &mut App) -> Result { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index f04b83bc7336143672647a07107fa27bc55f5823..3443ccf592a4138edb61959f0dd82bdb8cc8d418 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -17,7 +17,8 @@ use serde::{ }; use settings::{ - ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore, SettingsUi, + ParameterizedJsonSchema, Settings, SettingsKey, SettingsLocation, SettingsSources, + SettingsStore, SettingsUi, }; use shellexpand; use std::{borrow::Cow, num::NonZeroU32, path::Path, slice, sync::Arc}; @@ -292,7 +293,10 @@ pub struct CopilotSettings { } /// The settings for all languages. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey, +)] +#[settings_key(None)] pub struct AllLanguageSettingsContent { /// The settings for enabling/disabling features. #[serde(default)] @@ -1213,8 +1217,6 @@ impl InlayHintKind { } impl settings::Settings for AllLanguageSettings { - const KEY: Option<&'static str> = None; - type FileContent = AllLanguageSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> Result { diff --git a/crates/language_models/src/settings.rs b/crates/language_models/src/settings.rs index 8b7ab5fc2547bd0b014238739f1b940dad831f66..cfe66c91a36d4da562cba84363f79bd1d5b4e1ce 100644 --- a/crates/language_models/src/settings.rs +++ b/crates/language_models/src/settings.rs @@ -5,7 +5,7 @@ use collections::HashMap; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; use crate::provider::{ self, @@ -46,7 +46,10 @@ pub struct AllLanguageModelSettings { pub zed_dot_dev: ZedDotDevSettings, } -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, SettingsUi)] +#[derive( + Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, SettingsUi, SettingsKey, +)] +#[settings_key(key = "language_models")] pub struct AllLanguageModelSettingsContent { pub anthropic: Option, pub bedrock: Option, @@ -145,8 +148,6 @@ pub struct OpenRouterSettingsContent { } impl settings::Settings for AllLanguageModelSettings { - const KEY: Option<&'static str> = Some("language_models"); - const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]); type FileContent = AllLanguageModelSettingsContent; diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 54c49bc72a49309002421c4f8ac3544c86e4dc69..3631ad00dfb8662d5d4142a4cbd11186c1b1b137 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -264,13 +264,9 @@ pub(crate) fn render_ai_setup_page( ); let fs = ::global(cx); - update_settings_file::( - fs, - cx, - move |ai_settings: &mut Option, _| { - *ai_settings = Some(enabled); - }, - ); + update_settings_file::(fs, cx, move |ai_settings, _| { + ai_settings.disable_ai = Some(enabled); + }); }, ) .tab_index({ diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 59ec437dcf8d11209e9c73020f1b51e40aa56cce..aef9dcca86ce49a70f1a508c0a43614737a653c7 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -388,7 +388,7 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme } }; update_settings_file::(fs.clone(), cx, move |setting, _| { - *setting = Some(vim_mode); + setting.vim_mode = Some(vim_mode); }); telemetry::event!( diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index 48c6621e3509c1eda69a6a5e92602ba2ab12a484..dc123f2ba5fb38dd80b72aee8fc6ad6a000be23d 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -2,7 +2,7 @@ use editor::ShowScrollbar; use gpui::Pixels; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[serde(rename_all = "snake_case")] @@ -61,7 +61,8 @@ pub struct IndentGuidesSettingsContent { pub show: Option, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] +#[settings_key(key = "outline_panel")] pub struct OutlinePanelSettingsContent { /// Whether to show the outline panel button in the status bar. /// @@ -116,8 +117,6 @@ pub struct OutlinePanelSettingsContent { } impl Settings for OutlinePanelSettings { - const KEY: Option<&'static str> = Some("outline_panel"); - type FileContent = OutlinePanelSettingsContent; fn load( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4adebabc5a03636ca81fbc3b04a277c2d6d03a66..1e2e52c120f95a7c7540cd6f916d2d401f411af2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -28,6 +28,7 @@ use context_server_store::ContextServerStore; pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent}; use git::repository::get_git_committer; use git_store::{Repository, RepositoryId}; +use schemars::JsonSchema; pub mod search_history; mod yarn; @@ -94,7 +95,10 @@ use rpc::{ }; use search::{SearchInputKind, SearchQuery, SearchResult}; use search_history::SearchHistory; -use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsSources, SettingsStore}; +use settings::{ + InvalidSettingsError, Settings, SettingsKey, SettingsLocation, SettingsSources, SettingsStore, + SettingsUi, +}; use smol::channel::Receiver; use snippet::Snippet; use snippet_provider::SnippetProvider; @@ -968,10 +972,26 @@ pub struct DisableAiSettings { pub disable_ai: bool, } -impl settings::Settings for DisableAiSettings { - const KEY: Option<&'static str> = Some("disable_ai"); +#[derive( + Copy, + Clone, + PartialEq, + Eq, + Debug, + Default, + serde::Serialize, + serde::Deserialize, + SettingsUi, + SettingsKey, + JsonSchema, +)] +#[settings_key(None)] +pub struct DisableAiSettingContent { + pub disable_ai: Option, +} - type FileContent = Option; +impl settings::Settings for DisableAiSettings { + type FileContent = DisableAiSettingContent; fn load(sources: SettingsSources, _: &mut App) -> Result { // For security reasons, settings can only make AI restrictions MORE strict, not less. @@ -984,7 +1004,7 @@ impl settings::Settings for DisableAiSettings { .iter() .chain(sources.user.iter()) .chain(sources.server.iter()) - .any(|disabled| **disabled == Some(true)); + .any(|disabled| disabled.disable_ai == Some(true)); Ok(Self { disable_ai }) } @@ -5550,10 +5570,15 @@ mod disable_ai_settings_tests { #[gpui::test] async fn test_disable_ai_settings_security(cx: &mut TestAppContext) { + fn disable_setting(value: Option) -> DisableAiSettingContent { + DisableAiSettingContent { disable_ai: value } + } cx.update(|cx| { // Test 1: Default is false (AI enabled) let sources = SettingsSources { - default: &Some(false), + default: &DisableAiSettingContent { + disable_ai: Some(false), + }, global: None, extensions: None, user: None, @@ -5567,10 +5592,10 @@ mod disable_ai_settings_tests { assert!(!settings.disable_ai, "Default should allow AI"); // Test 2: Global true, local false -> still disabled (local cannot re-enable) - let global_true = Some(true); - let local_false = Some(false); + let global_true = disable_setting(Some(true)); + let local_false = disable_setting(Some(false)); let sources = SettingsSources { - default: &Some(false), + default: &disable_setting(Some(false)), global: None, extensions: None, user: Some(&global_true), @@ -5587,10 +5612,10 @@ mod disable_ai_settings_tests { ); // Test 3: Global false, local true -> disabled (local can make more restrictive) - let global_false = Some(false); - let local_true = Some(true); + let global_false = disable_setting(Some(false)); + let local_true = disable_setting(Some(true)); let sources = SettingsSources { - default: &Some(false), + default: &disable_setting(Some(false)), global: None, extensions: None, user: Some(&global_false), @@ -5604,10 +5629,10 @@ mod disable_ai_settings_tests { assert!(settings.disable_ai, "Local true can override global false"); // Test 4: Server can only make more restrictive (set to true) - let user_false = Some(false); - let server_true = Some(true); + let user_false = disable_setting(Some(false)); + let server_true = disable_setting(Some(true)); let sources = SettingsSources { - default: &Some(false), + default: &disable_setting(Some(false)), global: None, extensions: None, user: Some(&user_false), @@ -5624,10 +5649,10 @@ mod disable_ai_settings_tests { ); // Test 5: Server false cannot override user true - let user_true = Some(true); - let server_false = Some(false); + let user_true = disable_setting(Some(true)); + let server_false = disable_setting(Some(false)); let sources = SettingsSources { - default: &Some(false), + default: &disable_setting(Some(false)), global: None, extensions: None, user: Some(&user_true), @@ -5644,12 +5669,12 @@ mod disable_ai_settings_tests { ); // Test 6: Multiple local settings, any true disables AI - let global_false = Some(false); - let local_false3 = Some(false); - let local_true2 = Some(true); - let local_false4 = Some(false); + let global_false = disable_setting(Some(false)); + let local_false3 = disable_setting(Some(false)); + let local_true2 = disable_setting(Some(true)); + let local_false4 = disable_setting(Some(false)); let sources = SettingsSources { - default: &Some(false), + default: &disable_setting(Some(false)), global: None, extensions: None, user: Some(&global_false), @@ -5663,11 +5688,11 @@ mod disable_ai_settings_tests { assert!(settings.disable_ai, "Any local true should disable AI"); // Test 7: All three sources can independently disable AI - let user_false2 = Some(false); - let server_false2 = Some(false); - let local_true3 = Some(true); + let user_false2 = disable_setting(Some(false)); + let server_false2 = disable_setting(Some(false)); + let local_true3 = disable_setting(Some(true)); let sources = SettingsSources { - default: &Some(false), + default: &disable_setting(Some(false)), global: None, extensions: None, user: Some(&user_false2), diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 57969ec9938602b477293aa3033a31bc8b3deae1..694e244e63e2b2861d640ec32ce0a1f5c50be52f 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -18,8 +18,8 @@ use rpc::{ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ - InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, - SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file, + InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation, + SettingsSources, SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file, }; use std::{ collections::BTreeMap, @@ -36,7 +36,8 @@ use crate::{ worktree_store::{WorktreeStore, WorktreeStoreEvent}, }; -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(None)] pub struct ProjectSettings { /// Configuration for language servers. /// @@ -568,8 +569,6 @@ impl Default for SessionSettings { } impl Settings for ProjectSettings { - const KEY: Option<&'static str> = None; - type FileContent = Self; fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index db9b2b85d545e85a0cff3ec13a8f75e28dac88fa..6c812c294663d1d6fe7915d201f9e8925fa943ab 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -2,7 +2,7 @@ use editor::ShowScrollbar; use gpui::Pixels; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[serde(rename_all = "snake_case")] @@ -92,7 +92,8 @@ pub enum ShowDiagnostics { All, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] +#[settings_key(key = "project_panel")] pub struct ProjectPanelSettingsContent { /// Whether to show the project panel button in the status bar. /// @@ -168,8 +169,6 @@ pub struct ProjectPanelSettingsContent { } impl Settings for ProjectPanelSettings { - const KEY: Option<&'static str> = Some("project_panel"); - type FileContent = ProjectPanelSettingsContent; fn load( diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs index a7f915301f42850b03be951f596a8542842a6877..3e6810239c80c72d74624bcc243157290fcd93fa 100644 --- a/crates/recent_projects/src/remote_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -21,7 +21,7 @@ use remote::{ }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; use theme::ThemeSettings; use ui::{ ActiveTheme, Color, CommonAnimationExt, Context, Icon, IconName, IconSize, InteractiveElement, @@ -121,15 +121,14 @@ pub struct SshProject { pub paths: Vec, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(None)] pub struct RemoteSettingsContent { pub ssh_connections: Option>, pub read_ssh_config: Option, } impl Settings for SshSettings { - const KEY: Option<&'static str> = None; - type FileContent = RemoteSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> Result { diff --git a/crates/repl/src/jupyter_settings.rs b/crates/repl/src/jupyter_settings.rs index 6f3d6b1db631267e9b41ae7598d6e573387f2ac6..c89736a03dc6d77dd89bb33c4990b25149189a41 100644 --- a/crates/repl/src/jupyter_settings.rs +++ b/crates/repl/src/jupyter_settings.rs @@ -4,7 +4,7 @@ use editor::EditorSettings; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; #[derive(Debug, Default)] pub struct JupyterSettings { @@ -20,7 +20,8 @@ impl JupyterSettings { } } -#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] +#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] +#[settings_key(key = "jupyter")] pub struct JupyterSettingsContent { /// Default kernels to select for each language. /// @@ -37,8 +38,6 @@ impl Default for JupyterSettingsContent { } impl Settings for JupyterSettings { - const KEY: Option<&'static str> = Some("jupyter"); - type FileContent = JupyterSettingsContent; fn load( diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs index fb5b445b49a1fdbfac34ce8bc1a3d17d8241e009..a6bfeecbc3c01eb5309221443d1b9905b99dcd5b 100644 --- a/crates/settings/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -1,10 +1,10 @@ use std::fmt::{Display, Formatter}; -use crate as settings; +use crate::{self as settings}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources, VsCodeSettings}; -use settings_ui_macros::SettingsUi; +use settings_ui_macros::{SettingsKey, SettingsUi}; /// Base key bindings scheme. Base keymaps can be overridden with user keymaps. /// @@ -101,16 +101,25 @@ impl BaseKeymap { } #[derive( - Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default, SettingsUi, + Copy, + Clone, + Debug, + Serialize, + Deserialize, + JsonSchema, + PartialEq, + Eq, + Default, + SettingsUi, + SettingsKey, )] // extracted so that it can be an option, and still work with derive(SettingsUi) +#[settings_key(None)] pub struct BaseKeymapSetting { pub base_keymap: Option, } impl Settings for BaseKeymap { - const KEY: Option<&'static str> = None; - type FileContent = BaseKeymapSetting; fn load( diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 7e567cc085101713b0f6b100d0b47f6bf4c3531f..8a50b1afe5d0c68365efe0652421937f6dad2783 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -21,12 +21,12 @@ pub use keymap_file::{ pub use settings_file::*; pub use settings_json::*; pub use settings_store::{ - InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, - SettingsStore, + InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation, + SettingsSources, SettingsStore, }; pub use settings_ui_core::*; // Re-export the derive macro -pub use settings_ui_macros::SettingsUi; +pub use settings_ui_macros::{SettingsKey, SettingsUi}; pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource}; #[derive(Clone, Debug, PartialEq)] diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 72df08d14fb61536d147b4d1fb8b9a2466f5f0aa..cc0ebf10cd004ce660572d7ea3a44ec945a47432 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -36,17 +36,19 @@ use crate::{ settings_ui_core::SettingsUi, update_value_in_json_text, }; -/// A value that can be defined as a user setting. -/// -/// Settings can be loaded from a combination of multiple JSON files. -pub trait Settings: 'static + Send + Sync { +pub trait SettingsKey: 'static + Send + Sync { /// The name of a key within the JSON file from which this setting should /// be deserialized. If this is `None`, then the setting will be deserialized /// from the root object. const KEY: Option<&'static str>; const FALLBACK_KEY: Option<&'static str> = None; +} +/// A value that can be defined as a user setting. +/// +/// Settings can be loaded from a combination of multiple JSON files. +pub trait Settings: 'static + Send + Sync { /// The name of the keys in the [`FileContent`](Self::FileContent) that should /// always be written to a settings file, even if their value matches the default /// value. @@ -57,8 +59,19 @@ pub trait Settings: 'static + Send + Sync { const PRESERVED_KEYS: Option<&'static [&'static str]> = None; /// The type that is stored in an individual JSON file. - type FileContent: Clone + Default + Serialize + DeserializeOwned + JsonSchema + SettingsUi; - + type FileContent: Clone + + Default + + Serialize + + DeserializeOwned + + JsonSchema + + SettingsUi + + SettingsKey; + + /* + * let path = Settings + * + * + */ /// The logic for combining together values from one or more JSON files into the /// final value for this setting. /// @@ -71,7 +84,7 @@ pub trait Settings: 'static + Send + Sync { Self: Sized; fn missing_default() -> anyhow::Error { - anyhow::anyhow!("missing default") + anyhow::anyhow!("missing default for: {}", std::any::type_name::()) } /// Use [the helpers in the vscode_import module](crate::vscode_import) to apply known @@ -1393,7 +1406,7 @@ impl Debug for SettingsStore { impl AnySettingValue for SettingValue { fn key(&self) -> Option<&'static str> { - T::KEY + T::FileContent::KEY } fn setting_type_name(&self) -> &'static str { @@ -1445,16 +1458,21 @@ impl AnySettingValue for SettingValue { mut json: &Value, ) -> (Option<&'static str>, Result) { let mut key = None; - if let Some(k) = T::KEY { + if let Some(k) = T::FileContent::KEY { if let Some(value) = json.get(k) { json = value; key = Some(k); - } else if let Some((k, value)) = T::FALLBACK_KEY.and_then(|k| Some((k, json.get(k)?))) { + } else if let Some((k, value)) = + T::FileContent::FALLBACK_KEY.and_then(|k| Some((k, json.get(k)?))) + { json = value; key = Some(k); } else { let value = T::FileContent::default(); - return (T::KEY, Ok(DeserializedSetting(Box::new(value)))); + return ( + T::FileContent::KEY, + Ok(DeserializedSetting(Box::new(value))), + ); } } let value = serde_path_to_error::deserialize::<_, T::FileContent>(json) @@ -1498,6 +1516,7 @@ impl AnySettingValue for SettingValue { } } } + self.global_value .as_ref() .unwrap_or_else(|| panic!("no default value for setting {}", self.setting_type_name())) @@ -1570,7 +1589,7 @@ mod tests { // This is so the SettingsUi macro can still work properly use crate as settings; use serde_derive::Deserialize; - use settings_ui_macros::SettingsUi; + use settings_ui_macros::{SettingsKey, SettingsUi}; use unindent::Unindent; #[gpui::test] @@ -2120,7 +2139,8 @@ mod tests { staff: bool, } - #[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)] + #[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] + #[settings_key(key = "user")] struct UserSettingsContent { name: Option, age: Option, @@ -2128,7 +2148,6 @@ mod tests { } impl Settings for UserSettings { - const KEY: Option<&'static str> = Some("user"); type FileContent = UserSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> Result { @@ -2143,12 +2162,37 @@ mod tests { #[derive(Debug, Deserialize, PartialEq)] struct TurboSetting(bool); + #[derive( + Copy, + Clone, + PartialEq, + Eq, + Debug, + Default, + serde::Serialize, + serde::Deserialize, + SettingsUi, + SettingsKey, + JsonSchema, + )] + #[serde(default)] + #[settings_key(None)] + pub struct TurboSettingContent { + turbo: Option, + } + impl Settings for TurboSetting { - const KEY: Option<&'static str> = Some("turbo"); - type FileContent = bool; + type FileContent = TurboSettingContent; fn load(sources: SettingsSources, _: &mut App) -> Result { - sources.json_merge() + Ok(Self( + sources + .user + .or(sources.server) + .unwrap_or(sources.default) + .turbo + .unwrap_or_default(), + )) } fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut Self::FileContent) {} @@ -2162,15 +2206,14 @@ mod tests { key2: String, } - #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] + #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] + #[settings_key(None)] struct MultiKeySettingsJson { key1: Option, key2: Option, } impl Settings for MultiKeySettings { - const KEY: Option<&'static str> = None; - type FileContent = MultiKeySettingsJson; fn load(sources: SettingsSources, _: &mut App) -> Result { @@ -2200,15 +2243,16 @@ mod tests { Hour24, } - #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)] + #[derive( + Clone, Default, Debug, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey, + )] + #[settings_key(key = "journal")] struct JournalSettingsJson { pub path: Option, pub hour_format: Option, } impl Settings for JournalSettings { - const KEY: Option<&'static str> = Some("journal"); - type FileContent = JournalSettingsJson; fn load(sources: SettingsSources, _: &mut App) -> Result { @@ -2288,7 +2332,10 @@ mod tests { ); } - #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] + #[derive( + Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey, + )] + #[settings_key(None)] struct LanguageSettings { #[serde(default)] languages: HashMap, @@ -2301,8 +2348,6 @@ mod tests { } impl Settings for LanguageSettings { - const KEY: Option<&'static str> = None; - type FileContent = Self; fn load(sources: SettingsSources, _: &mut App) -> Result { diff --git a/crates/settings_ui_macros/src/settings_ui_macros.rs b/crates/settings_ui_macros/src/settings_ui_macros.rs index c98705d5f8d4de3f42b4756a32353123f5779fbc..1895083508a6a606f4dd9889529719aa12ea0b10 100644 --- a/crates/settings_ui_macros/src/settings_ui_macros.rs +++ b/crates/settings_ui_macros/src/settings_ui_macros.rs @@ -43,10 +43,9 @@ pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenSt let lit: LitStr = meta.input.parse()?; group_name = Some(lit.value()); } else if meta.path.is_ident("path") { - // todo(settings_ui) try get KEY from Settings if possible, and once we do, - // if can get key from settings, throw error if path also passed + // todo(settings_ui) rely entirely on settings_key, remove path attribute if path_name.is_some() { - return Err(meta.error("Only one 'path' can be specified")); + return Err(meta.error("Only one 'path' can be specified, either with `path` in `settings_ui` or with `settings_key`")); } meta.input.parse::()?; let lit: LitStr = meta.input.parse()?; @@ -55,6 +54,12 @@ pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenSt Ok(()) }) .unwrap_or_else(|e| panic!("in #[settings_ui] attribute: {}", e)); + } else if let Some(settings_key) = parse_setting_key_attr(attr) { + // todo(settings_ui) either remove fallback key or handle it here + if path_name.is_some() && settings_key.key.is_some() { + panic!("Both 'path' and 'settings_key' are specified. Must specify only one"); + } + path_name = settings_key.key; } } @@ -212,3 +217,126 @@ fn generate_ui_item_body( }, } } + +struct SettingsKey { + key: Option, + fallback_key: Option, +} + +fn parse_setting_key_attr(attr: &syn::Attribute) -> Option { + if !attr.path().is_ident("settings_key") { + return None; + } + + let mut settings_key = SettingsKey { + key: None, + fallback_key: None, + }; + + let mut found_none = false; + + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("None") { + found_none = true; + } else if meta.path.is_ident("key") { + if settings_key.key.is_some() { + return Err(meta.error("Only one 'group' path can be specified")); + } + meta.input.parse::()?; + let lit: LitStr = meta.input.parse()?; + settings_key.key = Some(lit.value()); + } else if meta.path.is_ident("fallback_key") { + if found_none { + return Err(meta.error("Cannot specify 'fallback_key' and 'None'")); + } + + if settings_key.fallback_key.is_some() { + return Err(meta.error("Only one 'fallback_key' can be specified")); + } + + meta.input.parse::()?; + let lit: LitStr = meta.input.parse()?; + settings_key.fallback_key = Some(lit.value()); + } + Ok(()) + }) + .unwrap_or_else(|e| panic!("in #[settings_key] attribute: {}", e)); + + if found_none && settings_key.fallback_key.is_some() { + panic!("in #[settings_key] attribute: Cannot specify 'None' and 'fallback_key'"); + } + if found_none && settings_key.key.is_some() { + panic!("in #[settings_key] attribute: Cannot specify 'None' and 'key'"); + } + if !found_none && settings_key.key.is_none() { + panic!("in #[settings_key] attribute: 'key' must be specified"); + } + + return Some(settings_key); +} + +#[proc_macro_derive(SettingsKey, attributes(settings_key))] +pub fn derive_settings_key(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + // Handle generic parameters if present + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let mut settings_key = Option::::None; + + for attr in &input.attrs { + let parsed_settings_key = parse_setting_key_attr(attr); + if parsed_settings_key.is_some() && settings_key.is_some() { + panic!("Duplicate #[settings_key] attribute"); + } + settings_key = parsed_settings_key; + } + + let Some(SettingsKey { key, fallback_key }) = settings_key else { + panic!("Missing #[settings_key] attribute"); + }; + + let key = key.map_or_else(|| quote! {None}, |key| quote! {Some(#key)}); + let fallback_key = fallback_key.map_or_else( + || quote! {None}, + |fallback_key| quote! {Some(#fallback_key)}, + ); + + let expanded = quote! { + impl #impl_generics settings::SettingsKey for #name #ty_generics #where_clause { + const KEY: Option<&'static str> = #key; + + const FALLBACK_KEY: Option<&'static str> = #fallback_key; + }; + }; + + proc_macro::TokenStream::from(expanded) +} + +#[cfg(test)] +mod tests { + use syn::{Attribute, parse_quote}; + + use super::*; + + #[test] + fn test_extract_key() { + let input: Attribute = parse_quote!( + #[settings_key(key = "my_key")] + ); + let settings_key = parse_setting_key_attr(&input).unwrap(); + assert_eq!(settings_key.key, Some("my_key".to_string())); + assert_eq!(settings_key.fallback_key, None); + } + + #[test] + fn test_empty_key() { + let input: Attribute = parse_quote!( + #[settings_key(None)] + ); + let settings_key = parse_setting_key_attr(&input).unwrap(); + assert_eq!(settings_key.key, None); + assert_eq!(settings_key.fallback_key, None); + } +} diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index c3051e089c68e3df0733c9e6cf7c8a42f56e742d..0ab92a0f26d35710da7fd0a2e88542a98c7affed 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -6,7 +6,7 @@ use gpui::{AbsoluteLength, App, FontFallbacks, FontFeatures, FontWeight, Pixels, use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::{SettingsSources, SettingsUi}; +use settings::{SettingsKey, SettingsSources, SettingsUi}; use std::path::PathBuf; use task::Shell; use theme::FontFamilyName; @@ -135,7 +135,8 @@ pub enum ActivateScript { Pyenv, } -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(key = "terminal")] pub struct TerminalSettingsContent { /// What shell to use when opening a terminal. /// @@ -253,8 +254,6 @@ pub struct TerminalSettingsContent { } impl settings::Settings for TerminalSettings { - const KEY: Option<&'static str> = Some("terminal"); - type FileContent = TerminalSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 825176a2a0b5e35c60606d0922cef37fe91caea7..8409c60b22b03b8d917b84ae20229dc2db63fe4a 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -13,7 +13,7 @@ use gpui::{ use refineable::Refineable; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; -use settings::{ParameterizedJsonSchema, Settings, SettingsSources, SettingsUi}; +use settings::{ParameterizedJsonSchema, Settings, SettingsKey, SettingsSources, SettingsUi}; use std::sync::Arc; use util::ResultExt as _; use util::schemars::replace_subschema; @@ -366,7 +366,8 @@ impl IconThemeSelection { } /// Settings for rendering text in UI and text buffers. -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(None)] pub struct ThemeSettingsContent { /// The default font size for text in the UI. #[serde(default)] @@ -818,8 +819,6 @@ fn clamp_font_weight(weight: f32) -> FontWeight { } impl settings::Settings for ThemeSettings { - const KEY: Option<&'static str> = None; - type FileContent = ThemeSettingsContent; fn load(sources: SettingsSources, cx: &mut App) -> Result { diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs index 0dc301f7eef6789bf1c0a2ad51cb63dff77d0337..38e529098bd3e97a11ecefac684c1734302f4261 100644 --- a/crates/title_bar/src/title_bar_settings.rs +++ b/crates/title_bar/src/title_bar_settings.rs @@ -1,7 +1,7 @@ use db::anyhow; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; #[derive(Copy, Clone, Deserialize, Debug)] pub struct TitleBarSettings { @@ -14,8 +14,11 @@ pub struct TitleBarSettings { pub show_menus: bool, } -#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] -#[settings_ui(group = "Title Bar", path = "title_bar")] +#[derive( + Copy, Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey, +)] +#[settings_ui(group = "Title Bar")] +#[settings_key(key = "title_bar")] pub struct TitleBarSettingsContent { /// Whether to show the branch icon beside branch switcher in the title bar. /// @@ -48,8 +51,6 @@ pub struct TitleBarSettingsContent { } impl Settings for TitleBarSettings { - const KEY: Option<&'static str> = Some("title_bar"); - type FileContent = TitleBarSettingsContent; fn load(sources: SettingsSources, _: &mut gpui::App) -> anyhow::Result diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index ef9588acae181bad2b079d7c89458458bb851a64..4f1173a188b6d3113234c79f02a55d2c34cf12d9 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -68,7 +68,7 @@ impl VimTestContext { pub fn init_keybindings(enabled: bool, cx: &mut App) { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |s| *s = Some(enabled)); + store.update_user_settings::(cx, |s| s.vim_mode = Some(enabled)); }); let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure( "keymaps/default-macos.json", @@ -134,7 +134,7 @@ impl VimTestContext { pub fn enable_vim(&mut self) { self.cx.update(|_, cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |s| *s = Some(true)); + store.update_user_settings::(cx, |s| s.vim_mode = Some(true)); }); }) } @@ -142,7 +142,7 @@ impl VimTestContext { pub fn disable_vim(&mut self) { self.cx.update(|_, cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |s| *s = Some(false)); + store.update_user_settings::(cx, |s| s.vim_mode = Some(false)); }); }) } @@ -151,7 +151,7 @@ impl VimTestContext { self.cx.update(|_, cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings::(cx, |s| { - *s = Some(true) + s.helix_mode = Some(true) }); }); }) diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 5a4ac425183e1843db7075c0f5054a16f82948f9..f4f8de2e7800732bb0a278bbc37928c58002ec7d 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -39,7 +39,9 @@ use object::Object; use schemars::JsonSchema; use serde::Deserialize; use serde_derive::Serialize; -use settings::{Settings, SettingsSources, SettingsStore, SettingsUi, update_settings_file}; +use settings::{ + Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi, update_settings_file, +}; use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; use std::{mem, ops::Range, sync::Arc}; use surrounds::SurroundsType; @@ -247,7 +249,7 @@ pub fn init(cx: &mut App) { let fs = workspace.app_state().fs.clone(); let currently_enabled = Vim::enabled(cx); update_settings_file::(fs, cx, move |setting, _| { - *setting = Some(!currently_enabled) + setting.vim_mode = Some(!currently_enabled) }) }); @@ -1785,7 +1787,8 @@ struct VimSettings { pub cursor_shape: CursorShapeSettings, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(key = "vim")] struct VimSettingsContent { pub default_mode: Option, pub toggle_relative_line_numbers: Option, @@ -1824,8 +1827,6 @@ impl From for Mode { } impl Settings for VimSettings { - const KEY: Option<&'static str> = Some("vim"); - type FileContent = VimSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> Result { diff --git a/crates/vim_mode_setting/Cargo.toml b/crates/vim_mode_setting/Cargo.toml index fbb7f30b4c2a03aca48ad5db26283c33aedb885b..61d265b958b10fac700bd78577ac5fefb19b7d09 100644 --- a/crates/vim_mode_setting/Cargo.toml +++ b/crates/vim_mode_setting/Cargo.toml @@ -14,5 +14,7 @@ path = "src/vim_mode_setting.rs" [dependencies] anyhow.workspace = true gpui.workspace = true +schemars.workspace = true +serde.workspace = true settings.workspace = true workspace-hack.workspace = true diff --git a/crates/vim_mode_setting/src/vim_mode_setting.rs b/crates/vim_mode_setting/src/vim_mode_setting.rs index 7fb39ef4f6f10370f1a0fb2cf83dcb3a88b80d81..660520a307dbef1e73174aa5449417d766c04235 100644 --- a/crates/vim_mode_setting/src/vim_mode_setting.rs +++ b/crates/vim_mode_setting/src/vim_mode_setting.rs @@ -6,7 +6,8 @@ use anyhow::Result; use gpui::App; -use settings::{Settings, SettingsSources, SettingsUi}; +use schemars::JsonSchema; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; /// Initializes the `vim_mode_setting` crate. pub fn init(cx: &mut App) { @@ -14,25 +15,40 @@ pub fn init(cx: &mut App) { HelixModeSetting::register(cx); } -/// Whether or not to enable Vim mode. -/// -/// Default: false -#[derive(SettingsUi)] pub struct VimModeSetting(pub bool); -impl Settings for VimModeSetting { - const KEY: Option<&'static str> = Some("vim_mode"); +#[derive( + Copy, + Clone, + PartialEq, + Eq, + Debug, + Default, + serde::Serialize, + serde::Deserialize, + SettingsUi, + SettingsKey, + JsonSchema, +)] +#[settings_key(None)] +pub struct VimModeSettingContent { + /// Whether or not to enable Vim mode. + /// + /// Default: false + pub vim_mode: Option, +} - type FileContent = Option; +impl Settings for VimModeSetting { + type FileContent = VimModeSettingContent; fn load(sources: SettingsSources, _: &mut App) -> Result { Ok(Self( sources .user - .or(sources.server) - .copied() - .flatten() - .unwrap_or(sources.default.ok_or_else(Self::missing_default)?), + .and_then(|mode| mode.vim_mode) + .or(sources.server.and_then(|mode| mode.vim_mode)) + .or(sources.default.vim_mode) + .ok_or_else(Self::missing_default)?, )) } @@ -41,25 +57,41 @@ impl Settings for VimModeSetting { } } -/// Whether or not to enable Helix mode. -/// -/// Default: false -#[derive(SettingsUi)] +#[derive(Debug)] pub struct HelixModeSetting(pub bool); -impl Settings for HelixModeSetting { - const KEY: Option<&'static str> = Some("helix_mode"); +#[derive( + Copy, + Clone, + PartialEq, + Eq, + Debug, + Default, + serde::Serialize, + serde::Deserialize, + SettingsUi, + SettingsKey, + JsonSchema, +)] +#[settings_key(None)] +pub struct HelixModeSettingContent { + /// Whether or not to enable Helix mode. + /// + /// Default: false + pub helix_mode: Option, +} - type FileContent = Option; +impl Settings for HelixModeSetting { + type FileContent = HelixModeSettingContent; fn load(sources: SettingsSources, _: &mut App) -> Result { Ok(Self( sources .user - .or(sources.server) - .copied() - .flatten() - .unwrap_or(sources.default.ok_or_else(Self::missing_default)?), + .and_then(|mode| mode.helix_mode) + .or(sources.server.and_then(|mode| mode.helix_mode)) + .or(sources.default.helix_mode) + .ok_or_else(Self::missing_default)?, )) } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index f37be0f154f736b021b0fcf5f29cf26074e3299f..23fbec470c4d2e305bf7b51679bbe56f6dfeaa95 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -17,7 +17,7 @@ use gpui::{ use project::{Project, ProjectEntryId, ProjectPath}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsLocation, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsLocation, SettingsSources, SettingsUi}; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -101,7 +101,8 @@ pub enum ActivateOnClose { LeftNeighbour, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(key = "tabs")] pub struct ItemSettingsContent { /// Whether to show the Git file status on a tab item. /// @@ -130,7 +131,8 @@ pub struct ItemSettingsContent { show_close_button: Option, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(key = "preview_tabs")] pub struct PreviewTabsSettingsContent { /// Whether to show opened editors as preview tabs. /// Preview tabs do not stay open, are reused until explicitly set to be kept open opened (via double-click or editing) and show file names in italic. @@ -148,8 +150,6 @@ pub struct PreviewTabsSettingsContent { } impl Settings for ItemSettings { - const KEY: Option<&'static str> = Some("tabs"); - type FileContent = ItemSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> Result { @@ -187,8 +187,6 @@ impl Settings for ItemSettings { } impl Settings for PreviewTabsSettings { - const KEY: Option<&'static str> = Some("preview_tabs"); - type FileContent = PreviewTabsSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> Result { diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 1a7e548e4eda1f41e36c6ad0883cdd57be8828d7..8868f3190575ac4b861e0619732890f477d83b69 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -6,7 +6,7 @@ use collections::HashMap; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; #[derive(Deserialize)] pub struct WorkspaceSettings { @@ -118,7 +118,8 @@ pub enum RestoreOnStartupBehavior { LastSession, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(None)] pub struct WorkspaceSettingsContent { /// Active pane styling settings. pub active_pane_modifiers: Option, @@ -223,7 +224,8 @@ pub struct TabBarSettings { pub show_tab_bar_buttons: bool, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(key = "tab_bar")] pub struct TabBarSettingsContent { /// Whether or not to show the tab bar in the editor. /// @@ -282,8 +284,6 @@ pub struct CenteredLayoutSettings { } impl Settings for WorkspaceSettings { - const KEY: Option<&'static str> = None; - type FileContent = WorkspaceSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> Result { @@ -373,8 +373,6 @@ impl Settings for WorkspaceSettings { } impl Settings for TabBarSettings { - const KEY: Option<&'static str> = Some("tab_bar"); - type FileContent = TabBarSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> Result { diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index 6a8e2b5d89b0201b81f45817adb439fe85e24d91..41eb3ab6f6aa971d44009c1cbb00567a4f3448ea 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -4,7 +4,7 @@ use anyhow::Context as _; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; use util::paths::PathMatcher; #[derive(Clone, PartialEq, Eq)] @@ -31,7 +31,8 @@ impl WorktreeSettings { } } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] +#[settings_key(None)] pub struct WorktreeSettingsContent { /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides /// `file_scan_inclusions`. @@ -65,8 +66,6 @@ pub struct WorktreeSettingsContent { } impl Settings for WorktreeSettings { - const KEY: Option<&'static str> = None; - type FileContent = WorktreeSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { diff --git a/crates/zlog_settings/src/zlog_settings.rs b/crates/zlog_settings/src/zlog_settings.rs index 0cdc784489b47d89388edc9ed20aed6f3c2f9959..dd74fc574ff23dc78beca1feafeb34d874a68c22 100644 --- a/crates/zlog_settings/src/zlog_settings.rs +++ b/crates/zlog_settings/src/zlog_settings.rs @@ -3,7 +3,7 @@ use anyhow::Result; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsStore, SettingsUi}; pub fn init(cx: &mut App) { ZlogSettings::register(cx); @@ -15,15 +15,25 @@ pub fn init(cx: &mut App) { .detach(); } -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)] +#[derive( + Clone, + Debug, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + SettingsUi, + SettingsKey, +)] +#[settings_key(key = "log")] pub struct ZlogSettings { #[serde(default, flatten)] pub scopes: std::collections::HashMap, } impl Settings for ZlogSettings { - const KEY: Option<&'static str> = Some("log"); - type FileContent = Self; fn load(sources: settings::SettingsSources, _: &mut App) -> Result From c2fa9d79814c2124da0d4f8a0c3dfcf075505ac0 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 4 Sep 2025 15:25:52 -0400 Subject: [PATCH 590/744] docs: Add configuration example for `simple-completion-language-server` (#37566) This PR adds a configuration example for the `simple-completion-language-server`. We show the user how to re-enable the `feature_paths` option, as we're now disabling it by default (https://github.com/zed-industries/zed/pull/37565). Release Notes: - N/A --- docs/src/snippets.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/src/snippets.md b/docs/src/snippets.md index 3514d08340e8f1d04287ffde0150281149625476..b5d09c6c37b6f869c11dbbc524a8515fde7d4142 100644 --- a/docs/src/snippets.md +++ b/docs/src/snippets.md @@ -40,4 +40,20 @@ To create JSX snippets you have to use `javascript.json` snippets file, instead ## See also +The `feature_paths` option in `simple-completion-language-server` is disabled by default. + +If you want to enable it you can add the following to your `settings.json`: + +```json +{ + "lsp": { + "snippet-completion-server": { + "settings": { + "feature_paths": true + } + } + } +} +``` + For more configuration information, see the [`simple-completion-language-server` instructions](https://github.com/zed-industries/simple-completion-language-server/tree/main). From ccae033d8519cffa7f61a265f05837f2ecf599de Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 4 Sep 2025 22:34:23 +0300 Subject: [PATCH 591/744] Make fallback open picker more intuitive (#37564) Closes https://github.com/zed-industries/zed/issues/34991 Before, the picker did not allow to open the current directory that was just completed: image pressing `enter` here would open `assets`; pressing `tab` would append the `assets/` segment to the query. Only backspace, removing `/` would allow to open the current directory. After: image The first item is now a placeholder for opening the current directory with `enter`. Any time a fuzzy query is appended, the placeholder goes away; `tab` selects the entry below the placeholder. Release Notes: - Made fallback open picker more intuitive --------- Co-authored-by: Peter Tripp Co-authored-by: David Kleingeld --- crates/file_finder/src/open_path_prompt.rs | 66 +++++++++- .../file_finder/src/open_path_prompt_tests.rs | 118 ++++++++++++------ 2 files changed, 146 insertions(+), 38 deletions(-) diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 4625872e46c690701b304351c6648a8e380f181a..51e8f5c437ab1aa86433f91022a01e8a2e09f664 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -1,7 +1,7 @@ use crate::file_finder_settings::FileFinderSettings; use file_icons::FileIcons; use futures::channel::oneshot; -use fuzzy::{StringMatch, StringMatchCandidate}; +use fuzzy::{CharBag, StringMatch, StringMatchCandidate}; use gpui::{HighlightStyle, StyledText, Task}; use picker::{Picker, PickerDelegate}; use project::{DirectoryItem, DirectoryLister}; @@ -125,6 +125,13 @@ impl OpenPathDelegate { DirectoryState::None { .. } => Vec::new(), } } + + fn current_dir(&self) -> &'static str { + match self.path_style { + PathStyle::Posix => "./", + PathStyle::Windows => ".\\", + } + } } #[derive(Debug)] @@ -233,6 +240,7 @@ impl PickerDelegate for OpenPathDelegate { cx: &mut Context>, ) -> Task<()> { let lister = &self.lister; + let input_is_empty = query.is_empty(); let (dir, suffix) = get_dir_and_suffix(query, self.path_style); let query = match &self.directory_state { @@ -263,6 +271,7 @@ impl PickerDelegate for OpenPathDelegate { let cancel_flag = self.cancel_flag.clone(); let parent_path_is_root = self.prompt_root == dir; + let current_dir = self.current_dir(); cx.spawn_in(window, async move |this, cx| { if let Some(query) = query { let paths = query.await; @@ -353,10 +362,38 @@ impl PickerDelegate for OpenPathDelegate { return; }; + let mut max_id = 0; if !suffix.starts_with('.') { - new_entries.retain(|entry| !entry.path.string.starts_with('.')); + new_entries.retain(|entry| { + max_id = max_id.max(entry.path.id); + !entry.path.string.starts_with('.') + }); } + if suffix.is_empty() { + let should_prepend_with_current_dir = this + .read_with(cx, |picker, _| { + !input_is_empty + && !matches!( + picker.delegate.directory_state, + DirectoryState::Create { .. } + ) + }) + .unwrap_or(false); + if should_prepend_with_current_dir { + new_entries.insert( + 0, + CandidateInfo { + path: StringMatchCandidate { + id: max_id + 1, + string: current_dir.to_string(), + char_bag: CharBag::from(current_dir), + }, + is_dir: true, + }, + ); + } + this.update(cx, |this, cx| { this.delegate.selected_index = 0; this.delegate.string_matches = new_entries @@ -485,6 +522,10 @@ impl PickerDelegate for OpenPathDelegate { _: &mut Context>, ) -> Option { let candidate = self.get_entry(self.selected_index)?; + if candidate.path.string.is_empty() || candidate.path.string == self.current_dir() { + return None; + } + let path_style = self.path_style; Some( maybe!({ @@ -629,12 +670,18 @@ impl PickerDelegate for OpenPathDelegate { DirectoryState::None { .. } => Vec::new(), }; + let is_current_dir_candidate = candidate.path.string == self.current_dir(); + let file_icon = maybe!({ if !settings.file_icons { return None; } let icon = if candidate.is_dir { - FileIcons::get_folder_icon(false, cx)? + if is_current_dir_candidate { + return Some(Icon::new(IconName::ReplyArrowRight).color(Color::Muted)); + } else { + FileIcons::get_folder_icon(false, cx)? + } } else { let path = path::Path::new(&candidate.path.string); FileIcons::get_icon(path, cx)? @@ -652,6 +699,8 @@ impl PickerDelegate for OpenPathDelegate { .child(HighlightedLabel::new( if parent_path == &self.prompt_root { format!("{}{}", self.prompt_root, candidate.path.string) + } else if is_current_dir_candidate { + "open this directory".to_string() } else { candidate.path.string }, @@ -747,6 +796,17 @@ impl PickerDelegate for OpenPathDelegate { fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { Arc::from(format!("[directory{MAIN_SEPARATOR_STR}]filename.ext")) } + + fn separators_after_indices(&self) -> Vec { + let Some(m) = self.string_matches.first() else { + return Vec::new(); + }; + if m.string == self.current_dir() { + vec![0] + } else { + Vec::new() + } + } } fn path_candidates( diff --git a/crates/file_finder/src/open_path_prompt_tests.rs b/crates/file_finder/src/open_path_prompt_tests.rs index a69ac6992dc280fd6537b16087302c2fbb9f8f4c..1f47c4e80adc598c505cf130519623fd6e578035 100644 --- a/crates/file_finder/src/open_path_prompt_tests.rs +++ b/crates/file_finder/src/open_path_prompt_tests.rs @@ -43,12 +43,17 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) { insert_query(query, &picker, cx).await; assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]); + #[cfg(not(windows))] + let expected_separator = "./"; + #[cfg(windows)] + let expected_separator = ".\\"; + // If the query ends with a slash, the picker should show the contents of the directory. let query = path!("/root/"); insert_query(query, &picker, cx).await; assert_eq!( collect_match_candidates(&picker, cx), - vec!["a1", "a2", "a3", "dir1", "dir2"] + vec![expected_separator, "a1", "a2", "a3", "dir1", "dir2"] ); // Show candidates for the query "a". @@ -72,7 +77,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) { insert_query(query, &picker, cx).await; assert_eq!( collect_match_candidates(&picker, cx), - vec!["c", "d1", "d2", "d3", "dir3", "dir4"] + vec![expected_separator, "c", "d1", "d2", "d3", "dir3", "dir4"] ); // Show candidates for the query "d". @@ -116,71 +121,86 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) { // Confirm completion for the query "/root", since it's a directory, it should add a trailing slash. let query = path!("/root"); insert_query(query, &picker, cx).await; - assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/")); + assert_eq!( + confirm_completion(query, 0, &picker, cx).unwrap(), + path!("/root/") + ); // Confirm completion for the query "/root/", selecting the first candidate "a", since it's a file, it should not add a trailing slash. let query = path!("/root/"); insert_query(query, &picker, cx).await; - assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/a")); + assert_eq!( + confirm_completion(query, 0, &picker, cx), + None, + "First entry is `./` and when we confirm completion, it is tabbed below" + ); + assert_eq!( + confirm_completion(query, 1, &picker, cx).unwrap(), + path!("/root/a"), + "Second entry is the first entry of a directory that we want to be completed" + ); // Confirm completion for the query "/root/", selecting the second candidate "dir1", since it's a directory, it should add a trailing slash. let query = path!("/root/"); insert_query(query, &picker, cx).await; assert_eq!( - confirm_completion(query, 1, &picker, cx), + confirm_completion(query, 2, &picker, cx).unwrap(), path!("/root/dir1/") ); let query = path!("/root/a"); insert_query(query, &picker, cx).await; - assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/a")); + assert_eq!( + confirm_completion(query, 0, &picker, cx).unwrap(), + path!("/root/a") + ); let query = path!("/root/d"); insert_query(query, &picker, cx).await; assert_eq!( - confirm_completion(query, 1, &picker, cx), + confirm_completion(query, 1, &picker, cx).unwrap(), path!("/root/dir2/") ); let query = path!("/root/dir2"); insert_query(query, &picker, cx).await; assert_eq!( - confirm_completion(query, 0, &picker, cx), + confirm_completion(query, 0, &picker, cx).unwrap(), path!("/root/dir2/") ); let query = path!("/root/dir2/"); insert_query(query, &picker, cx).await; assert_eq!( - confirm_completion(query, 0, &picker, cx), + confirm_completion(query, 1, &picker, cx).unwrap(), path!("/root/dir2/c") ); let query = path!("/root/dir2/"); insert_query(query, &picker, cx).await; assert_eq!( - confirm_completion(query, 2, &picker, cx), + confirm_completion(query, 3, &picker, cx).unwrap(), path!("/root/dir2/dir3/") ); let query = path!("/root/dir2/d"); insert_query(query, &picker, cx).await; assert_eq!( - confirm_completion(query, 0, &picker, cx), + confirm_completion(query, 0, &picker, cx).unwrap(), path!("/root/dir2/d") ); let query = path!("/root/dir2/d"); insert_query(query, &picker, cx).await; assert_eq!( - confirm_completion(query, 1, &picker, cx), + confirm_completion(query, 1, &picker, cx).unwrap(), path!("/root/dir2/dir3/") ); let query = path!("/root/dir2/di"); insert_query(query, &picker, cx).await; assert_eq!( - confirm_completion(query, 1, &picker, cx), + confirm_completion(query, 1, &picker, cx).unwrap(), path!("/root/dir2/dir4/") ); } @@ -211,42 +231,63 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) { insert_query(query, &picker, cx).await; assert_eq!( collect_match_candidates(&picker, cx), - vec!["a", "dir1", "dir2"] + vec![".\\", "a", "dir1", "dir2"] + ); + assert_eq!( + confirm_completion(query, 0, &picker, cx), + None, + "First entry is `.\\` and when we confirm completion, it is tabbed below" + ); + assert_eq!( + confirm_completion(query, 1, &picker, cx).unwrap(), + "C:/root/a", + "Second entry is the first entry of a directory that we want to be completed" ); - assert_eq!(confirm_completion(query, 0, &picker, cx), "C:/root/a"); let query = "C:\\root/"; insert_query(query, &picker, cx).await; assert_eq!( collect_match_candidates(&picker, cx), - vec!["a", "dir1", "dir2"] + vec![".\\", "a", "dir1", "dir2"] + ); + assert_eq!( + confirm_completion(query, 1, &picker, cx).unwrap(), + "C:\\root/a" ); - assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root/a"); let query = "C:\\root\\"; insert_query(query, &picker, cx).await; assert_eq!( collect_match_candidates(&picker, cx), - vec!["a", "dir1", "dir2"] + vec![".\\", "a", "dir1", "dir2"] + ); + assert_eq!( + confirm_completion(query, 1, &picker, cx).unwrap(), + "C:\\root\\a" ); - assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root\\a"); // Confirm completion for the query "C:/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash. let query = "C:/root/d"; insert_query(query, &picker, cx).await; assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]); - assert_eq!(confirm_completion(query, 1, &picker, cx), "C:/root/dir2\\"); + assert_eq!( + confirm_completion(query, 1, &picker, cx).unwrap(), + "C:/root/dir2\\" + ); let query = "C:\\root/d"; insert_query(query, &picker, cx).await; assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]); - assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root/dir1\\"); + assert_eq!( + confirm_completion(query, 0, &picker, cx).unwrap(), + "C:\\root/dir1\\" + ); let query = "C:\\root\\d"; insert_query(query, &picker, cx).await; assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]); assert_eq!( - confirm_completion(query, 0, &picker, cx), + confirm_completion(query, 0, &picker, cx).unwrap(), "C:\\root\\dir1\\" ); } @@ -276,20 +317,29 @@ async fn test_open_path_prompt_on_windows_with_remote(cx: &mut TestAppContext) { insert_query(query, &picker, cx).await; assert_eq!( collect_match_candidates(&picker, cx), - vec!["a", "dir1", "dir2"] + vec!["./", "a", "dir1", "dir2"] + ); + assert_eq!( + confirm_completion(query, 1, &picker, cx).unwrap(), + "/root/a" ); - assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/a"); // Confirm completion for the query "/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash. let query = "/root/d"; insert_query(query, &picker, cx).await; assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]); - assert_eq!(confirm_completion(query, 1, &picker, cx), "/root/dir2/"); + assert_eq!( + confirm_completion(query, 1, &picker, cx).unwrap(), + "/root/dir2/" + ); let query = "/root/d"; insert_query(query, &picker, cx).await; assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]); - assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/dir1/"); + assert_eq!( + confirm_completion(query, 0, &picker, cx).unwrap(), + "/root/dir1/" + ); } #[gpui::test] @@ -396,15 +446,13 @@ fn confirm_completion( select: usize, picker: &Entity>, cx: &mut VisualTestContext, -) -> String { - picker - .update_in(cx, |f, window, cx| { - if f.delegate.selected_index() != select { - f.delegate.set_selected_index(select, window, cx); - } - f.delegate.confirm_completion(query.to_string(), window, cx) - }) - .unwrap() +) -> Option { + picker.update_in(cx, |f, window, cx| { + if f.delegate.selected_index() != select { + f.delegate.set_selected_index(select, window, cx); + } + f.delegate.confirm_completion(query.to_string(), window, cx) + }) } fn collect_match_candidates( From 4c32d5bf138171a52b00dc5f2be233c718c800ae Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 4 Sep 2025 15:35:48 -0400 Subject: [PATCH 592/744] snippets: Disable `feature_paths` by default (#37565) This PR updates the default configuration of the `snippets` extension to disable suggesting paths (`feature_paths`). If users want to enable it, it can be done via the settings: ```json { "lsp": { "snippet-completion-server": { "settings": { "feature_paths": true } } } } ``` Release Notes: - N/A --- extensions/snippets/src/snippets.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/snippets/src/snippets.rs b/extensions/snippets/src/snippets.rs index 05e1ebca38ddfa576795e6040ccd2b3dde20cc3e..1efe4c234002b5c8b3d26b9bdb0b30095e212ea6 100644 --- a/extensions/snippets/src/snippets.rs +++ b/extensions/snippets/src/snippets.rs @@ -120,7 +120,9 @@ impl zed::Extension for SnippetExtension { "snippets_first": true, "feature_words": false, "feature_snippets": true, - "feature_paths": true + // We disable `feature_paths` by default, because it's bad UX to assume that any `/` that is typed + // is the start of a path. + "feature_paths": false }) }); Ok(Some(settings)) From 1b865a60f854eb7dbc0aad98718d1785adceb8b6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 4 Sep 2025 16:08:49 -0400 Subject: [PATCH 593/744] snippets: Bump to v0.0.6 (#37567) This PR bumps the snippets extension to v0.0.6. Changes: - https://github.com/zed-industries/zed/pull/37565 Release Notes: - N/A --- Cargo.lock | 2 +- extensions/snippets/Cargo.toml | 2 +- extensions/snippets/extension.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a99c59a1890080ac220b669b26864d859d2ad377..c5e6c8588137b87e00b15e0655a53cdefc518d4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20642,7 +20642,7 @@ dependencies = [ [[package]] name = "zed_snippets" -version = "0.0.5" +version = "0.0.6" dependencies = [ "serde_json", "zed_extension_api 0.1.0", diff --git a/extensions/snippets/Cargo.toml b/extensions/snippets/Cargo.toml index 80a3d4f31ebd628f03b077c727527b5aa0057ebf..ab5ac7244a3acbe25246588f4fe4ad1a35f1964f 100644 --- a/extensions/snippets/Cargo.toml +++ b/extensions/snippets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_snippets" -version = "0.0.5" +version = "0.0.6" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/snippets/extension.toml b/extensions/snippets/extension.toml index c2b4178a614220872bca37e4c2a12f4b16bba82f..01dc587d77af8a9ca074b49a247ef8f6cfffb516 100644 --- a/extensions/snippets/extension.toml +++ b/extensions/snippets/extension.toml @@ -1,9 +1,9 @@ id = "snippets" name = "Snippets" description = "Support for language-agnostic snippets, provided by simple-completion-language-server" -version = "0.0.5" +version = "0.0.6" schema_version = 1 -authors = [] +authors = ["Zed Industries "] repository = "https://github.com/zed-industries/zed" [language_servers.snippet-completion-server] From e982cb824a94abc32392861c0753373a8df1684e Mon Sep 17 00:00:00 2001 From: morgankrey Date: Thu, 4 Sep 2025 15:57:00 -0500 Subject: [PATCH 594/744] docs: Claude Authentication (#37573) Release Notes: - N/A --- docs/src/ai/agent-panel.md | 2 +- docs/src/ai/external-agents.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/src/ai/agent-panel.md b/docs/src/ai/agent-panel.md index ce91ca3401d07aba552b1ca007b3809e301071de..b6b748e2f58493cd62abbd3c6e7dc443182e992f 100644 --- a/docs/src/ai/agent-panel.md +++ b/docs/src/ai/agent-panel.md @@ -23,7 +23,7 @@ From this point on, you can interact with the many supported features outlined b > Note that for external agents, like [Gemini CLI](./external-agents.md#gemini-cli) or [Claude Code](./external-agents.md#claude-code), some of the features outlined below are _not_ currently supported—for example, _restoring threads from history_, _checkpoints_, _token usage display_, _model selection_, and others. All of them should hopefully be supported in the future. -### Creating New Threads +### Creating New Threads {#new-thread} By default, the Agent Panel uses Zed's first-party agent. diff --git a/docs/src/ai/external-agents.md b/docs/src/ai/external-agents.md index 963e41d42f53ad68ef70de3466913b71b11bd38e..e05849ef1aa54c8ea2c3fff09c8008f58b8a01b7 100644 --- a/docs/src/ai/external-agents.md +++ b/docs/src/ai/external-agents.md @@ -85,6 +85,12 @@ If you'd like to bind this to a keyboard shortcut, you can do so by editing your ] ``` +### Authentication + +As of version `0.202.7` (stable) and `0.203.2` (preview), authentication to Zed's Claude Code installation is decoupled entirely from Zed's agent. That is to say, an Anthropic API key added via the [Zed Agent's settings](./llm-providers.md#anthropic) will _not_ be utilized by Claude Code for authentication and billing. + +To ensure you're using your billing method of choice, [open a new Claude Code thread](./agent-panel.md#new-thread)`. Then, run `/login`, and authenticate either via API key, or via `Log in with Claude Code` to use a Claude Pro/Max subscription. + #### Installation The first time you create a Claude Code thread, Zed will install [@zed-industries/claude-code-acp](https://github.com/zed-industries/claude-code-acp). This installation is only available to Zed and is kept up to date as you use the agent. From 3c0183fa5e8fda26d300974a19ae229e251ed4fa Mon Sep 17 00:00:00 2001 From: morgankrey Date: Thu, 4 Sep 2025 16:14:57 -0500 Subject: [PATCH 595/744] Extraneous backtick (#37576) Release Notes: - N/A --- docs/src/ai/external-agents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/ai/external-agents.md b/docs/src/ai/external-agents.md index e05849ef1aa54c8ea2c3fff09c8008f58b8a01b7..bc7768c6081ad7a32eb1fd780750a48c4b9200f0 100644 --- a/docs/src/ai/external-agents.md +++ b/docs/src/ai/external-agents.md @@ -89,7 +89,7 @@ If you'd like to bind this to a keyboard shortcut, you can do so by editing your As of version `0.202.7` (stable) and `0.203.2` (preview), authentication to Zed's Claude Code installation is decoupled entirely from Zed's agent. That is to say, an Anthropic API key added via the [Zed Agent's settings](./llm-providers.md#anthropic) will _not_ be utilized by Claude Code for authentication and billing. -To ensure you're using your billing method of choice, [open a new Claude Code thread](./agent-panel.md#new-thread)`. Then, run `/login`, and authenticate either via API key, or via `Log in with Claude Code` to use a Claude Pro/Max subscription. +To ensure you're using your billing method of choice, [open a new Claude Code thread](./agent-panel.md#new-thread). Then, run `/login`, and authenticate either via API key, or via `Log in with Claude Code` to use a Claude Pro/Max subscription. #### Installation From c7902478c18a42138c6129aa3fc50aa0165337c8 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 4 Sep 2025 18:16:25 -0400 Subject: [PATCH 596/744] acp: Pass project environment to external agent servers (#37568) Closes #37469 Release Notes: - agent: The project shell environment is now passed to external agent processes. Co-authored-by: Richard Feldman Co-authored-by: Nia Espera --- crates/agent_servers/src/claude.rs | 9 +++++++++ crates/agent_servers/src/gemini.rs | 14 ++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 48d3e33775d98dfe89801813c6926ff40f48ed87..15352ce216f52dfd7a9f372a43c0ec401eb540af 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -80,8 +80,15 @@ impl AgentServer for ClaudeCode { let settings = cx.read_global(|settings: &SettingsStore, _| { settings.get::(None).claude.clone() }); + let project = delegate.project().clone(); cx.spawn(async move |cx| { + let mut project_env = project + .update(cx, |project, cx| { + project.directory_environment(root_dir.as_path().into(), cx) + })? + .await + .unwrap_or_default(); let mut command = if let Some(settings) = settings { settings.command } else { @@ -97,6 +104,8 @@ impl AgentServer for ClaudeCode { })? .await? }; + project_env.extend(command.env.take().unwrap_or_default()); + command.env = Some(project_env); command .env diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index b58ad703cda496c4413f30decbfa5e0b1d1b0735..7e40d85767b7ed407a22ece55580bee7317a5e6d 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -41,12 +41,19 @@ impl AgentServer for Gemini { let settings = cx.read_global(|settings: &SettingsStore, _| { settings.get::(None).gemini.clone() }); + let project = delegate.project().clone(); cx.spawn(async move |cx| { let ignore_system_version = settings .as_ref() .and_then(|settings| settings.ignore_system_version) .unwrap_or(true); + let mut project_env = project + .update(cx, |project, cx| { + project.directory_environment(root_dir.as_path().into(), cx) + })? + .await + .unwrap_or_default(); let mut command = if let Some(settings) = settings && let Some(command) = settings.custom_command() { @@ -67,13 +74,12 @@ impl AgentServer for Gemini { if !command.args.contains(&ACP_ARG.into()) { command.args.push(ACP_ARG.into()); } - if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() { - command - .env - .get_or_insert_default() + project_env .insert("GEMINI_API_KEY".to_owned(), api_key.key); } + project_env.extend(command.env.take().unwrap_or_default()); + command.env = Some(project_env); let root_dir_exists = fs.is_dir(&root_dir).await; anyhow::ensure!( From 0cb8a8983cef1f3e015fa0f2fc37e8325f3d201d Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 4 Sep 2025 18:30:48 -0400 Subject: [PATCH 597/744] settings ui: Improve setting proc macro and add scroll to UI (#37581) This PR improves the settings_ui proc macro by taking into account more serde attributes 1. rename_all 2. rename 3. flatten We also pass field documentation to the UI layer now too. This allows ui elements to have more information like the switch field description. We got the scrollbar working and started getting language settings to show up. Release Notes: - N/A --------- Co-authored-by: Ben Kunkle --- crates/editor/src/editor_settings.rs | 1 + crates/language/src/language_settings.rs | 38 ++-- crates/settings/src/settings_ui_core.rs | 9 + crates/settings_ui/src/settings_ui.rs | 123 ++++++++----- .../src/settings_ui_macros.rs | 170 +++++++++++++++--- 5 files changed, 258 insertions(+), 83 deletions(-) diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index d74244131e6635c7b9eda6ace0723ced96b0e041..7f4d024e57c4831aa4c512e6dcb3a9ab35d4f610 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -748,6 +748,7 @@ pub struct ScrollbarAxesContent { #[derive( Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi, )] +#[settings_ui(group = "Gutter")] pub struct GutterContent { /// Whether to show line numbers in the gutter. /// diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 3443ccf592a4138edb61959f0dd82bdb8cc8d418..cb519e32eca964cee4a742085714b233a424dd3c 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -208,7 +208,9 @@ impl LanguageSettings { } /// The provider that supplies edit predictions. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive( + Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi, +)] #[serde(rename_all = "snake_case")] pub enum EditPredictionProvider { None, @@ -231,13 +233,14 @@ impl EditPredictionProvider { /// The settings for edit predictions, such as [GitHub Copilot](https://github.com/features/copilot) /// or [Supermaven](https://supermaven.com). -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, SettingsUi)] pub struct EditPredictionSettings { /// The provider that supplies edit predictions. pub provider: EditPredictionProvider, /// A list of globs representing files that edit predictions should be disabled for. /// This list adds to a pre-existing, sensible default set of globs. /// Any additional ones you add are combined with them. + #[settings_ui(skip)] pub disabled_globs: Vec, /// Configures how edit predictions are displayed in the buffer. pub mode: EditPredictionsMode, @@ -269,7 +272,9 @@ pub struct DisabledGlob { } /// The mode in which edit predictions should be displayed. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive( + Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi, +)] #[serde(rename_all = "snake_case")] pub enum EditPredictionsMode { /// If provider supports it, display inline when holding modifier key (e.g., alt). @@ -282,13 +287,15 @@ pub enum EditPredictionsMode { Eager, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, SettingsUi)] pub struct CopilotSettings { /// HTTP/HTTPS proxy to use for Copilot. + #[settings_ui(skip)] pub proxy: Option, /// Disable certificate verification for proxy (not recommended). pub proxy_no_verify: Option, /// Enterprise URI for Copilot. + #[settings_ui(skip)] pub enterprise_uri: Option, } @@ -297,6 +304,7 @@ pub struct CopilotSettings { Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey, )] #[settings_key(None)] +#[settings_ui(group = "Default Language Settings")] pub struct AllLanguageSettingsContent { /// The settings for enabling/disabling features. #[serde(default)] @@ -309,10 +317,12 @@ pub struct AllLanguageSettingsContent { pub defaults: LanguageSettingsContent, /// The settings for individual languages. #[serde(default)] + #[settings_ui(skip)] pub languages: LanguageToSettingsMap, /// Settings for associating file extensions and filenames /// with languages. #[serde(default)] + #[settings_ui(skip)] pub file_types: HashMap, Vec>, } @@ -345,7 +355,7 @@ inventory::submit! { } /// Controls how completions are processed for this language. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)] #[serde(rename_all = "snake_case")] pub struct CompletionSettings { /// Controls how words are completed. @@ -420,7 +430,7 @@ fn default_3() -> usize { } /// The settings for a particular language. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct LanguageSettingsContent { /// How many columns a tab should occupy. /// @@ -617,12 +627,13 @@ pub enum RewrapBehavior { } /// The contents of the edit prediction settings. -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi)] pub struct EditPredictionSettingsContent { /// A list of globs representing files that edit predictions should be disabled for. /// This list adds to a pre-existing, sensible default set of globs. /// Any additional ones you add are combined with them. #[serde(default)] + #[settings_ui(skip)] pub disabled_globs: Option>, /// The mode used to display edit predictions in the buffer. /// Provider support required. @@ -637,12 +648,13 @@ pub struct EditPredictionSettingsContent { pub enabled_in_text_threads: bool, } -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi)] pub struct CopilotSettingsContent { /// HTTP/HTTPS proxy to use for Copilot. /// /// Default: none #[serde(default)] + #[settings_ui(skip)] pub proxy: Option, /// Disable certificate verification for the proxy (not recommended). /// @@ -653,19 +665,21 @@ pub struct CopilotSettingsContent { /// /// Default: none #[serde(default)] + #[settings_ui(skip)] pub enterprise_uri: Option, } /// The settings for enabling/disabling features. -#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] #[serde(rename_all = "snake_case")] +#[settings_ui(group = "Features")] pub struct FeaturesContent { /// Determines which edit prediction provider to use. pub edit_prediction_provider: Option, } /// Controls the soft-wrapping behavior in the editor. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)] #[serde(rename_all = "snake_case")] pub enum SoftWrap { /// Prefer a single line generally, unless an overly long line is encountered. @@ -934,7 +948,9 @@ pub enum Formatter { } /// The settings for indent guides. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive( + Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi, +)] pub struct IndentGuideSettings { /// Whether to display indent guides in the editor. /// diff --git a/crates/settings/src/settings_ui_core.rs b/crates/settings/src/settings_ui_core.rs index 9086d3c7454465e8abcaf2d30d01a4f928e4ddef..896a8bc038bdd8e495cdb6161212f9c722d54f14 100644 --- a/crates/settings/src/settings_ui_core.rs +++ b/crates/settings/src/settings_ui_core.rs @@ -19,6 +19,7 @@ pub trait SettingsUi { path: None, title: "None entry", item: SettingsUiItem::None, + documentation: None, } } } @@ -29,6 +30,8 @@ pub struct SettingsUiEntry { pub path: Option<&'static str>, /// What is displayed for the text for this entry pub title: &'static str, + /// documentation for this entry. Constructed from the documentation comment above the struct or field + pub documentation: Option<&'static str>, pub item: SettingsUiItem, } @@ -54,6 +57,7 @@ pub enum SettingsUiItemSingle { pub struct SettingsValue { pub title: &'static str, + pub documentation: Option<&'static str>, pub path: SmallVec<[&'static str; 1]>, pub value: Option, pub default_value: T, @@ -128,7 +132,9 @@ pub enum NumType { U64 = 0, U32 = 1, F32 = 2, + USIZE = 3, } + pub static NUM_TYPE_NAMES: std::sync::LazyLock<[&'static str; NumType::COUNT]> = std::sync::LazyLock::new(|| NumType::ALL.map(NumType::type_name)); pub static NUM_TYPE_IDS: std::sync::LazyLock<[TypeId; NumType::COUNT]> = @@ -143,6 +149,7 @@ impl NumType { NumType::U64 => TypeId::of::(), NumType::U32 => TypeId::of::(), NumType::F32 => TypeId::of::(), + NumType::USIZE => TypeId::of::(), } } @@ -151,6 +158,7 @@ impl NumType { NumType::U64 => std::any::type_name::(), NumType::U32 => std::any::type_name::(), NumType::F32 => std::any::type_name::(), + NumType::USIZE => std::any::type_name::(), } } } @@ -175,3 +183,4 @@ numeric_stepper_for_num_type!(u64, U64); numeric_stepper_for_num_type!(u32, U32); // todo(settings_ui) is there a better ui for f32? numeric_stepper_for_num_type!(f32, F32); +numeric_stepper_for_num_type!(usize, USIZE); diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index f316a318785c7f56d465c2d39e6b6ea9bbbd1bfa..d736f0e174ba13d368794d8f5b623a44845d561b 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -1,14 +1,13 @@ mod appearance_settings_controls; use std::any::TypeId; -use std::collections::VecDeque; use std::ops::{Not, Range}; use anyhow::Context as _; use command_palette_hooks::CommandPaletteFilter; use editor::EditorSettingsControls; use feature_flags::{FeatureFlag, FeatureFlagViewExt}; -use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, actions}; +use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions}; use settings::{ NumType, SettingsStore, SettingsUiEntry, SettingsUiItem, SettingsUiItemDynamic, SettingsUiItemGroup, SettingsUiItemSingle, SettingsValue, @@ -138,6 +137,7 @@ impl Item for SettingsPage { struct UiEntry { title: &'static str, path: Option<&'static str>, + documentation: Option<&'static str>, _depth: usize, // a // b < a descendant range < a total descendant range @@ -195,6 +195,7 @@ fn build_tree_item( tree.push(UiEntry { title: entry.title, path: entry.path, + documentation: entry.documentation, _depth: depth, descendant_range: index + 1..index + 1, total_descendant_range: index + 1..index + 1, @@ -354,32 +355,29 @@ fn render_content( tree: &SettingsUiTree, window: &mut Window, cx: &mut Context, -) -> impl IntoElement { - let Some(active_entry) = tree.entries.get(tree.active_entry_index) else { - return div() - .size_full() - .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error)); - }; - let mut content = v_flex().size_full().gap_4().overflow_hidden(); +) -> Div { + let content = v_flex().size_full().gap_4(); let mut path = smallvec::smallvec![]; - if let Some(active_entry_path) = active_entry.path { - path.push(active_entry_path); - } - let mut entry_index_queue = VecDeque::new(); - - if let Some(child_index) = active_entry.first_descendant_index() { - entry_index_queue.push_back(child_index); - let mut index = child_index; - while let Some(next_sibling_index) = tree.entries[index].next_sibling { - entry_index_queue.push_back(next_sibling_index); - index = next_sibling_index; - } - }; - while let Some(index) = entry_index_queue.pop_front() { + fn render_recursive( + tree: &SettingsUiTree, + index: usize, + path: &mut SmallVec<[&'static str; 1]>, + mut element: Div, + window: &mut Window, + cx: &mut App, + ) -> Div { + let Some(child) = tree.entries.get(index) else { + return element.child( + Label::new(SharedString::new_static("No settings found")).color(Color::Error), + ); + }; + + element = + element.child(Label::new(SharedString::new_static(child.title)).size(LabelSize::Large)); + // todo(settings_ui): subgroups? - let child = &tree.entries[index]; let mut pushed_path = false; if let Some(child_path) = child.path { path.push(child_path); @@ -388,37 +386,56 @@ fn render_content( let settings_value = settings_value_from_settings_and_path( path.clone(), child.title, + child.documentation, // PERF: how to structure this better? There feels like there's a way to avoid the clone // and every value lookup SettingsStore::global(cx).raw_user_settings(), SettingsStore::global(cx).raw_default_settings(), ); if let Some(select_descendant) = child.select_descendant { - let selected_descendant = select_descendant(settings_value.read(), cx); - if let Some(descendant_index) = - child.nth_descendant_index(&tree.entries, selected_descendant) - { - entry_index_queue.push_front(descendant_index); + let selected_descendant = child + .nth_descendant_index(&tree.entries, select_descendant(settings_value.read(), cx)); + if let Some(descendant_index) = selected_descendant { + element = render_recursive(&tree, descendant_index, path, element, window, cx); } } + if let Some(child_render) = child.render.as_ref() { + element = element.child(div().child(render_item_single( + settings_value, + child_render, + window, + cx, + ))); + } else if let Some(child_index) = child.first_descendant_index() { + let mut index = Some(child_index); + while let Some(sub_child_index) = index { + element = render_recursive(tree, sub_child_index, path, element, window, cx); + index = tree.entries[sub_child_index].next_sibling; + } + } else { + element = + element.child(div().child(Label::new("// skipped (for now)").color(Color::Muted))) + } + if pushed_path { path.pop(); } - let Some(child_render) = child.render.as_ref() else { - continue; - }; - content = content.child( - div() - .child(Label::new(SharedString::new_static(child.title)).size(LabelSize::Large)) - .child(render_item_single(settings_value, child_render, window, cx)), - ); + return element; } - return content; + return render_recursive( + tree, + tree.active_entry_index, + &mut path, + content, + window, + cx, + ); } impl Render for SettingsPage { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let scroll_handle = window.use_state(cx, |_, _| ScrollHandle::new()); div() .grid() .grid_cols(16) @@ -427,15 +444,19 @@ impl Render for SettingsPage { .size_full() .child( div() + .id("settings-ui-nav") .col_span(2) .h_full() .child(render_nav(&self.settings_tree, window, cx)), ) - .child(div().col_span(4).h_full().child(render_content( - &self.settings_tree, - window, - cx, - ))) + .child( + div().col_span(6).h_full().child( + render_content(&self.settings_tree, window, cx) + .id("settings-ui-content") + .track_scroll(scroll_handle.read(cx)) + .overflow_y_scroll(), + ), + ) } } @@ -530,6 +551,7 @@ fn downcast_any_item( let deserialized_setting_value = SettingsValue { title: settings_value.title, path: settings_value.path, + documentation: settings_value.documentation, value, default_value, }; @@ -586,6 +608,17 @@ fn render_any_numeric_stepper( window, cx, ), + NumType::USIZE => render_numeric_stepper::( + downcast_any_item(settings_value), + usize::saturating_sub, + usize::saturating_add, + |n| { + serde_json::Number::try_from(n) + .context("Failed to convert usize to serde_json::Number") + }, + window, + cx, + ), } } @@ -640,7 +673,7 @@ fn render_switch_field( SwitchField::new( id, SharedString::new_static(value.title), - None, + value.documentation.map(SharedString::new_static), match value.read() { true => ToggleState::Selected, false => ToggleState::Unselected, @@ -731,6 +764,7 @@ fn render_toggle_button_group( fn settings_value_from_settings_and_path( path: SmallVec<[&'static str; 1]>, title: &'static str, + documentation: Option<&'static str>, user_settings: &serde_json::Value, default_settings: &serde_json::Value, ) -> SettingsValue { @@ -743,6 +777,7 @@ fn settings_value_from_settings_and_path( let settings_value = SettingsValue { default_value, value, + documentation, path: path.clone(), // todo(settings_ui) is title required inside SettingsValue? title, diff --git a/crates/settings_ui_macros/src/settings_ui_macros.rs b/crates/settings_ui_macros/src/settings_ui_macros.rs index 1895083508a6a606f4dd9889529719aa12ea0b10..076f9c0f04e2963e9f4732a1fc7177f9ab85c723 100644 --- a/crates/settings_ui_macros/src/settings_ui_macros.rs +++ b/crates/settings_ui_macros/src/settings_ui_macros.rs @@ -1,3 +1,5 @@ +use std::ops::Not; + use heck::{ToSnakeCase as _, ToTitleCase as _}; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; @@ -63,12 +65,19 @@ pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenSt } } + let doc_str = parse_documentation_from_attrs(&input.attrs); + let ui_item_fn_body = generate_ui_item_body(group_name.as_ref(), path_name.as_ref(), &input); // todo(settings_ui): make group name optional, repurpose group as tag indicating item is group, and have "title" tag for custom title let title = group_name.unwrap_or(input.ident.to_string().to_title_case()); - let ui_entry_fn_body = map_ui_item_to_entry(path_name.as_deref(), &title, quote! { Self }); + let ui_entry_fn_body = map_ui_item_to_entry( + path_name.as_deref(), + &title, + doc_str.as_deref(), + quote! { Self }, + ); let expanded = quote! { impl #impl_generics settings::SettingsUi for #name #ty_generics #where_clause { @@ -111,14 +120,22 @@ fn option_inner_type(ty: TokenStream) -> Option { return Some(ty.to_token_stream()); } -fn map_ui_item_to_entry(path: Option<&str>, title: &str, ty: TokenStream) -> TokenStream { +fn map_ui_item_to_entry( + path: Option<&str>, + title: &str, + doc_str: Option<&str>, + ty: TokenStream, +) -> TokenStream { let ty = extract_type_from_option(ty); + // todo(settings_ui): does quote! just work with options? let path = path.map_or_else(|| quote! {None}, |path| quote! {Some(#path)}); + let doc_str = doc_str.map_or_else(|| quote! {None}, |doc_str| quote! {Some(#doc_str)}); quote! { settings::SettingsUiEntry { title: #title, path: #path, item: #ty::settings_ui_item(), + documentation: #doc_str, } } } @@ -134,6 +151,7 @@ fn generate_ui_item_body( settings::SettingsUiItem::None }, (Some(_), _, Data::Struct(data_struct)) => { + let struct_serde_attrs = parse_serde_attributes(&input.attrs); let fields = data_struct .fields .iter() @@ -153,48 +171,37 @@ fn generate_ui_item_body( }) }) .map(|field| { + let field_serde_attrs = parse_serde_attributes(&field.attrs); + let name = field.ident.clone().expect("tuple fields").to_string(); + let doc_str = parse_documentation_from_attrs(&field.attrs); + ( - field.ident.clone().expect("tuple fields").to_string(), + name.to_title_case(), + doc_str, + field_serde_attrs.flatten.not().then(|| { + struct_serde_attrs.apply_rename_to_field(&field_serde_attrs, &name) + }), field.ty.to_token_stream(), ) }) // todo(settings_ui): Re-format field name as nice title, and support setting different title with attr - .map(|(name, ty)| map_ui_item_to_entry(Some(&name), &name.to_title_case(), ty)); + .map(|(title, doc_str, path, ty)| { + map_ui_item_to_entry(path.as_deref(), &title, doc_str.as_deref(), ty) + }); quote! { settings::SettingsUiItem::Group(settings::SettingsUiItemGroup{ items: vec![#(#fields),*] }) } } (None, _, Data::Enum(data_enum)) => { - let mut lowercase = false; - let mut snake_case = false; - for attr in &input.attrs { - if attr.path().is_ident("serde") { - attr.parse_nested_meta(|meta| { - if meta.path.is_ident("rename_all") { - meta.input.parse::()?; - let lit = meta.input.parse::()?.value(); - lowercase = lit == "lowercase"; - snake_case = lit == "snake_case"; - } - Ok(()) - }) - .ok(); - } - } + let serde_attrs = parse_serde_attributes(&input.attrs); let length = data_enum.variants.len(); let variants = data_enum.variants.iter().map(|variant| { let string = variant.ident.clone().to_string(); let title = string.to_title_case(); - let string = if lowercase { - string.to_lowercase() - } else if snake_case { - string.to_snake_case() - } else { - string - }; + let string = serde_attrs.rename_all.apply(&string); (string, title) }); @@ -218,6 +225,113 @@ fn generate_ui_item_body( } } +struct SerdeOptions { + rename_all: SerdeRenameAll, + rename: Option, + flatten: bool, + _alias: Option, // todo(settings_ui) +} + +#[derive(PartialEq)] +enum SerdeRenameAll { + Lowercase, + SnakeCase, + None, +} + +impl SerdeRenameAll { + fn apply(&self, name: &str) -> String { + match self { + SerdeRenameAll::Lowercase => name.to_lowercase(), + SerdeRenameAll::SnakeCase => name.to_snake_case(), + SerdeRenameAll::None => name.to_string(), + } + } +} + +impl SerdeOptions { + fn apply_rename_to_field(&self, field_options: &Self, name: &str) -> String { + // field renames take precedence over struct rename all cases + if let Some(rename) = &field_options.rename { + return rename.clone(); + } + return self.rename_all.apply(name); + } +} + +fn parse_serde_attributes(attrs: &[syn::Attribute]) -> SerdeOptions { + let mut options = SerdeOptions { + rename_all: SerdeRenameAll::None, + rename: None, + flatten: false, + _alias: None, + }; + + for attr in attrs { + if !attr.path().is_ident("serde") { + continue; + } + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("rename_all") { + meta.input.parse::()?; + let lit = meta.input.parse::()?.value(); + + if options.rename_all != SerdeRenameAll::None { + return Err(meta.error("duplicate `rename_all` attribute")); + } else if lit == "lowercase" { + options.rename_all = SerdeRenameAll::Lowercase; + } else if lit == "snake_case" { + options.rename_all = SerdeRenameAll::SnakeCase; + } else { + return Err(meta.error(format!("invalid `rename_all` attribute: {}", lit))); + } + // todo(settings_ui): Other options? + } else if meta.path.is_ident("flatten") { + options.flatten = true; + } else if meta.path.is_ident("rename") { + if options.rename.is_some() { + return Err(meta.error("Can only have one rename attribute")); + } + + meta.input.parse::()?; + let lit = meta.input.parse::()?.value(); + options.rename = Some(lit); + } + Ok(()) + }) + .unwrap(); + } + + return options; +} + +fn parse_documentation_from_attrs(attrs: &[syn::Attribute]) -> Option { + let mut doc_str = Option::::None; + for attr in attrs { + if attr.path().is_ident("doc") { + // /// ... + // becomes + // #[doc = "..."] + use syn::{Expr::Lit, ExprLit, Lit::Str, Meta, MetaNameValue}; + if let Meta::NameValue(MetaNameValue { + value: + Lit(ExprLit { + lit: Str(ref lit_str), + .. + }), + .. + }) = attr.meta + { + let doc = lit_str.value(); + let doc_str = doc_str.get_or_insert_default(); + doc_str.push_str(doc.trim()); + doc_str.push('\n'); + } + } + } + return doc_str; +} + struct SettingsKey { key: Option, fallback_key: Option, @@ -290,7 +404,7 @@ pub fn derive_settings_key(input: proc_macro::TokenStream) -> proc_macro::TokenS if parsed_settings_key.is_some() && settings_key.is_some() { panic!("Duplicate #[settings_key] attribute"); } - settings_key = parsed_settings_key; + settings_key = settings_key.or(parsed_settings_key); } let Some(SettingsKey { key, fallback_key }) = settings_key else { From a6605270365e7db0ced654c290d62643d6d84672 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Thu, 4 Sep 2025 18:26:37 -0600 Subject: [PATCH 598/744] Make entry_for_path return a reference instead of cloning (#37591) Release Notes: - N/A --- crates/agent_ui/src/acp/message_editor.rs | 7 +++---- crates/agent_ui/src/acp/thread_view.rs | 14 ++++++-------- crates/agent_ui/src/context_picker.rs | 3 ++- crates/project/src/project.rs | 2 +- crates/project/src/worktree_store.rs | 3 +-- crates/repl/src/notebook/notebook_ui.rs | 7 ++++--- crates/search/src/project_search.rs | 1 + 7 files changed, 18 insertions(+), 19 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index da121bb7a486d80f15125d2ecc526b3b01e059d3..4f57c6161d8f5fae3aa0b5762ed85e49dfd20b43 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -493,14 +493,13 @@ impl MessageEditor { let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else { return Task::ready(Err(anyhow!("project entry not found"))); }; - let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else { + let directory_path = entry.path.clone(); + let worktree_id = project_path.worktree_id; + let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) else { return Task::ready(Err(anyhow!("worktree not found"))); }; let project = self.project.clone(); cx.spawn(async move |_, cx| { - let directory_path = entry.path.clone(); - - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?; let file_paths = worktree.read_with(cx, |worktree, _cx| { collect_files_in_path(worktree, &directory_path) })?; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index b4d56ad05be1a66e9740c2432a9bd08b1adfee0e..441b4aa06fdad65fce079ea36dc3d2e59cf4644f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4070,15 +4070,15 @@ impl AcpThreadView { MentionUri::PastedImage => {} MentionUri::Directory { abs_path } => { let project = workspace.project(); - let Some(entry) = project.update(cx, |project, cx| { + let Some(entry_id) = project.update(cx, |project, cx| { let path = project.find_project_path(abs_path, cx)?; - project.entry_for_path(&path, cx) + project.entry_for_path(&path, cx).map(|entry| entry.id) }) else { return; }; project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(entry.id)); + cx.emit(project::Event::RevealInProjectPanel(entry_id)); }); } MentionUri::Symbol { @@ -4091,11 +4091,9 @@ impl AcpThreadView { line_range, } => { let project = workspace.project(); - let Some((path, _)) = project.update(cx, |project, cx| { - let path = project.find_project_path(path, cx)?; - let entry = project.entry_for_path(&path, cx)?; - Some((path, entry)) - }) else { + let Some(path) = + project.update(cx, |project, cx| project.find_project_path(path, cx)) + else { return; }; diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 405b5ed90ba1606ef97b8b048b959bfc354bc5cd..b225fbf34058604cfb3f306a9cee14f69bb5edaa 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -987,7 +987,8 @@ impl MentionLink { .read(cx) .project() .read(cx) - .entry_for_path(&project_path, cx)?; + .entry_for_path(&project_path, cx)? + .clone(); Some(MentionLink::File(project_path, entry)) } Self::SYMBOL => { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1e2e52c120f95a7c7540cd6f916d2d401f411af2..66924f159a0a97dce558d742ca3ee80456542305 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4352,7 +4352,7 @@ impl Project { self.active_entry } - pub fn entry_for_path(&self, path: &ProjectPath, cx: &App) -> Option { + pub fn entry_for_path<'a>(&'a self, path: &ProjectPath, cx: &'a App) -> Option<&'a Entry> { self.worktree_store.read(cx).entry_for_path(path, cx) } diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 1eeeefc40ad09012e5d280c0821052cd6f8db098..b37e1ef8026b643444b3ca0ba67cdb953a959a36 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -203,11 +203,10 @@ impl WorktreeStore { }) } - pub fn entry_for_path(&self, path: &ProjectPath, cx: &App) -> Option { + pub fn entry_for_path<'a>(&'a self, path: &ProjectPath, cx: &'a App) -> Option<&'a Entry> { self.worktree_for_id(path.worktree_id, cx)? .read(cx) .entry_for_path(&path.path) - .cloned() } pub fn create_worktree( diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 325d262d9eddc164093f088d0e4790d0fa581167..081c474cdad86a5340520ef09345bd456f55b5ba 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -594,9 +594,10 @@ impl project::ProjectItem for NotebookItem { }; let id = project - .update(cx, |project, cx| project.entry_for_path(&path, cx))? - .context("Entry not found")? - .id; + .update(cx, |project, cx| { + project.entry_for_path(&path, cx).map(|entry| entry.id) + })? + .context("Entry not found")?; cx.new(|_| NotebookItem { path: abs_path, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 4a2dbf31fc96b43db34bd9977fafb09cc5ad60d1..33458a3a88fb717ba047b57564c8804f7ebea928 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -3201,6 +3201,7 @@ pub mod tests { .read(cx) .entry_for_path(&(worktree_id, "a").into(), cx) .expect("no entry for /a/ directory") + .clone() }); assert!(a_dir_entry.is_dir()); window From fded3fbcdb6861e0913bda8c86adaab6256ed254 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Thu, 4 Sep 2025 19:15:59 -0600 Subject: [PATCH 599/744] zeta: Scope edit prediction event history to current project (#37595) This change also causes Zeta to not do anything for editors that are not associated with a project. In practice, this shouldn't affect any behavior - those editors shouldn't have edit predictions anyway. Release Notes: - Edit Prediction: Requests no longer include recent edits from other projects (other Zed windows). --- .../zed/src/zed/edit_prediction_registry.rs | 3 +- crates/zeta/src/zeta.rs | 347 ++++++------------ crates/zeta_cli/src/main.rs | 58 +-- 3 files changed, 149 insertions(+), 259 deletions(-) diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index 7b8b98018e6d6c608574ab81e912e8a98e363046..4f009ccb0b1197f11b034ac48b89dd37b6f41278 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -207,9 +207,10 @@ fn assign_edit_prediction_provider( if let Some(buffer) = &singleton_buffer && buffer.read(cx).file().is_some() + && let Some(project) = editor.project() { zeta.update(cx, |zeta, cx| { - zeta.register_buffer(buffer, cx); + zeta.register_buffer(buffer, project, cx); }); } diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index e0cfd23dd26cd7ea49181b5aabc16f00f4fd826a..3851d16755783209fd9da4f468a494779a7d9fe7 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -35,12 +35,13 @@ use language_model::{LlmApiToken, RefreshLlmTokenListener}; use project::{Project, ProjectPath}; use release_channel::AppVersion; use settings::WorktreeId; +use std::collections::hash_map; +use std::mem; use std::str::FromStr; use std::{ cmp, fmt::Write, future::Future, - mem, ops::Range, path::Path, rc::Rc, @@ -211,9 +212,8 @@ impl std::fmt::Debug for EditPrediction { } pub struct Zeta { + projects: HashMap, client: Arc, - events: VecDeque, - registered_buffers: HashMap, shown_completions: VecDeque, rated_completions: HashSet, data_collection_choice: Entity, @@ -225,6 +225,11 @@ pub struct Zeta { license_detection_watchers: HashMap>, } +struct ZetaProject { + events: VecDeque, + registered_buffers: HashMap, +} + impl Zeta { pub fn global(cx: &mut App) -> Option> { cx.try_global::().map(|global| global.0.clone()) @@ -255,7 +260,9 @@ impl Zeta { } pub fn clear_history(&mut self) { - self.events.clear(); + for zeta_project in self.projects.values_mut() { + zeta_project.events.clear(); + } } pub fn usage(&self, cx: &App) -> Option { @@ -269,11 +276,10 @@ impl Zeta { let data_collection_choice = cx.new(|_| data_collection_choice); Self { + projects: HashMap::default(), client, - events: VecDeque::new(), shown_completions: VecDeque::new(), rated_completions: HashSet::default(), - registered_buffers: HashMap::default(), data_collection_choice, llm_token: LlmApiToken::default(), _llm_token_subscription: cx.subscribe( @@ -294,12 +300,35 @@ impl Zeta { } } - fn push_event(&mut self, event: Event) { + fn get_or_init_zeta_project( + &mut self, + project: &Entity, + cx: &mut Context, + ) -> &mut ZetaProject { + let project_id = project.entity_id(); + match self.projects.entry(project_id) { + hash_map::Entry::Occupied(entry) => entry.into_mut(), + hash_map::Entry::Vacant(entry) => { + cx.observe_release(project, move |this, _, _cx| { + this.projects.remove(&project_id); + }) + .detach(); + entry.insert(ZetaProject { + events: VecDeque::with_capacity(MAX_EVENT_COUNT), + registered_buffers: HashMap::default(), + }) + } + } + } + + fn push_event(zeta_project: &mut ZetaProject, event: Event) { + let events = &mut zeta_project.events; + if let Some(Event::BufferChange { new_snapshot: last_new_snapshot, timestamp: last_timestamp, .. - }) = self.events.back_mut() + }) = events.back_mut() { // Coalesce edits for the same buffer when they happen one after the other. let Event::BufferChange { @@ -318,50 +347,65 @@ impl Zeta { } } - self.events.push_back(event); - if self.events.len() >= MAX_EVENT_COUNT { + if events.len() >= MAX_EVENT_COUNT { // These are halved instead of popping to improve prompt caching. - self.events.drain(..MAX_EVENT_COUNT / 2); + events.drain(..MAX_EVENT_COUNT / 2); } - } - - pub fn register_buffer(&mut self, buffer: &Entity, cx: &mut Context) { - let buffer_id = buffer.entity_id(); - let weak_buffer = buffer.downgrade(); - - if let std::collections::hash_map::Entry::Vacant(entry) = - self.registered_buffers.entry(buffer_id) - { - let snapshot = buffer.read(cx).snapshot(); - entry.insert(RegisteredBuffer { - snapshot, - _subscriptions: [ - cx.subscribe(buffer, move |this, buffer, event, cx| { - this.handle_buffer_event(buffer, event, cx); - }), - cx.observe_release(buffer, move |this, _buffer, _cx| { - this.registered_buffers.remove(&weak_buffer.entity_id()); - }), - ], - }); - }; + events.push_back(event); } - fn handle_buffer_event( + pub fn register_buffer( &mut self, - buffer: Entity, - event: &language::BufferEvent, + buffer: &Entity, + project: &Entity, cx: &mut Context, ) { - if let language::BufferEvent::Edited = event { - self.report_changes_for_buffer(&buffer, cx); + let zeta_project = self.get_or_init_zeta_project(project, cx); + Self::register_buffer_impl(zeta_project, buffer, project, cx); + } + + fn register_buffer_impl<'a>( + zeta_project: &'a mut ZetaProject, + buffer: &Entity, + project: &Entity, + cx: &mut Context, + ) -> &'a mut RegisteredBuffer { + let buffer_id = buffer.entity_id(); + match zeta_project.registered_buffers.entry(buffer_id) { + hash_map::Entry::Occupied(entry) => entry.into_mut(), + hash_map::Entry::Vacant(entry) => { + let snapshot = buffer.read(cx).snapshot(); + let project_entity_id = project.entity_id(); + entry.insert(RegisteredBuffer { + snapshot, + _subscriptions: [ + cx.subscribe(buffer, { + let project = project.downgrade(); + move |this, buffer, event, cx| { + if let language::BufferEvent::Edited = event + && let Some(project) = project.upgrade() + { + this.report_changes_for_buffer(&buffer, &project, cx); + } + } + }), + cx.observe_release(buffer, move |this, _buffer, _cx| { + let Some(zeta_project) = this.projects.get_mut(&project_entity_id) + else { + return; + }; + zeta_project.registered_buffers.remove(&buffer_id); + }), + ], + }) + } } } fn request_completion_impl( &mut self, - project: Option<&Entity>, + project: &Entity, buffer: &Entity, cursor: language::Anchor, can_collect_data: bool, @@ -376,16 +420,14 @@ impl Zeta { { let buffer = buffer.clone(); let buffer_snapshotted_at = Instant::now(); - let snapshot = self.report_changes_for_buffer(&buffer, cx); + let snapshot = self.report_changes_for_buffer(&buffer, project, cx); let zeta = cx.entity(); - let events = self.events.clone(); + let events = self.get_or_init_zeta_project(project, cx).events.clone(); let client = self.client.clone(); 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()) - { + let git_info = if let (true, Some(file)) = (can_collect_data, snapshot.file()) { git_info_for_file(project, &ProjectPath::from_file(file.as_ref(), cx), cx) } else { None @@ -512,163 +554,10 @@ impl Zeta { }) } - // Generates several example completions of various states to fill the Zeta completion modal - #[cfg(any(test, feature = "test-support"))] - pub fn fill_with_fake_completions(&mut self, cx: &mut Context) -> Task<()> { - use language::Point; - - let test_buffer_text = indoc::indoc! {r#"a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line - And maybe a short line - - Then a few lines - - and then another - "#}; - - let project = None; - let buffer = cx.new(|cx| Buffer::local(test_buffer_text, cx)); - let position = buffer.read(cx).anchor_before(Point::new(1, 0)); - - let completion_tasks = vec![ - self.fake_completion( - project, - &buffer, - position, - PredictEditsResponse { - request_id: Uuid::parse_str("e7861db5-0cea-4761-b1c5-ad083ac53a80").unwrap(), - output_excerpt: format!("{EDITABLE_REGION_START_MARKER} -a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line -[here's an edit] -And maybe a short line -Then a few lines -and then another -{EDITABLE_REGION_END_MARKER} - ", ), - }, - cx, - ), - self.fake_completion( - project, - &buffer, - position, - PredictEditsResponse { - request_id: Uuid::parse_str("077c556a-2c49-44e2-bbc6-dafc09032a5e").unwrap(), - output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER} -a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line -And maybe a short line -[and another edit] -Then a few lines -and then another -{EDITABLE_REGION_END_MARKER} - "#), - }, - cx, - ), - self.fake_completion( - project, - &buffer, - position, - PredictEditsResponse { - request_id: Uuid::parse_str("df8c7b23-3d1d-4f99-a306-1f6264a41277").unwrap(), - output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER} -a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line -And maybe a short line - -Then a few lines - -and then another -{EDITABLE_REGION_END_MARKER} - "#), - }, - cx, - ), - self.fake_completion( - project, - &buffer, - position, - PredictEditsResponse { - request_id: Uuid::parse_str("c743958d-e4d8-44a8-aa5b-eb1e305c5f5c").unwrap(), - output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER} -a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line -And maybe a short line - -Then a few lines - -and then another -{EDITABLE_REGION_END_MARKER} - "#), - }, - cx, - ), - self.fake_completion( - project, - &buffer, - position, - PredictEditsResponse { - request_id: Uuid::parse_str("ff5cd7ab-ad06-4808-986e-d3391e7b8355").unwrap(), - output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER} -a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line -And maybe a short line -Then a few lines -[a third completion] -and then another -{EDITABLE_REGION_END_MARKER} - "#), - }, - cx, - ), - self.fake_completion( - project, - &buffer, - position, - PredictEditsResponse { - request_id: Uuid::parse_str("83cafa55-cdba-4b27-8474-1865ea06be94").unwrap(), - output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER} -a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line -And maybe a short line -and then another -[fourth completion example] -{EDITABLE_REGION_END_MARKER} - "#), - }, - cx, - ), - self.fake_completion( - project, - &buffer, - position, - PredictEditsResponse { - request_id: Uuid::parse_str("d5bd3afd-8723-47c7-bd77-15a3a926867b").unwrap(), - output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER} -a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line -And maybe a short line -Then a few lines -and then another -[fifth and final completion] -{EDITABLE_REGION_END_MARKER} - "#), - }, - cx, - ), - ]; - - cx.spawn(async move |zeta, cx| { - for task in completion_tasks { - task.await.unwrap(); - } - - zeta.update(cx, |zeta, _cx| { - zeta.shown_completions.get_mut(2).unwrap().edits = Arc::new([]); - zeta.shown_completions.get_mut(3).unwrap().edits = Arc::new([]); - }) - .ok(); - }) - } - #[cfg(any(test, feature = "test-support"))] pub fn fake_completion( &mut self, - project: Option<&Entity>, + project: &Entity, buffer: &Entity, position: language::Anchor, response: PredictEditsResponse, @@ -683,7 +572,7 @@ and then another pub fn request_completion( &mut self, - project: Option<&Entity>, + project: &Entity, buffer: &Entity, position: language::Anchor, can_collect_data: bool, @@ -1043,23 +932,23 @@ and then another fn report_changes_for_buffer( &mut self, buffer: &Entity, + project: &Entity, cx: &mut Context, ) -> BufferSnapshot { - self.register_buffer(buffer, cx); + let zeta_project = self.get_or_init_zeta_project(project, cx); + let registered_buffer = Self::register_buffer_impl(zeta_project, buffer, project, cx); - let registered_buffer = self - .registered_buffers - .get_mut(&buffer.entity_id()) - .unwrap(); let new_snapshot = buffer.read(cx).snapshot(); - if new_snapshot.version != registered_buffer.snapshot.version { let old_snapshot = mem::replace(&mut registered_buffer.snapshot, new_snapshot.clone()); - self.push_event(Event::BufferChange { - old_snapshot, - new_snapshot: new_snapshot.clone(), - timestamp: Instant::now(), - }); + Self::push_event( + zeta_project, + Event::BufferChange { + old_snapshot, + new_snapshot: new_snapshot.clone(), + timestamp: Instant::now(), + }, + ); } new_snapshot @@ -1140,7 +1029,7 @@ pub struct GatherContextOutput { } pub fn gather_context( - project: Option<&Entity>, + project: &Entity, full_path_str: String, snapshot: &BufferSnapshot, cursor_point: language::Point, @@ -1149,8 +1038,7 @@ pub fn gather_context( git_info: Option, cx: &App, ) -> Task> { - let local_lsp_store = - project.and_then(|project| project.read(cx).lsp_store().read(cx).as_local()); + let local_lsp_store = project.read(cx).lsp_store().read(cx).as_local(); let diagnostic_groups: Vec<(String, serde_json::Value)> = if can_collect_data && let Some(local_lsp_store) = local_lsp_store { snapshot @@ -1540,6 +1428,9 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { if self.zeta.read(cx).update_required { return; } + let Some(project) = project else { + return; + }; if self .zeta @@ -1578,13 +1469,7 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { let completion_request = this.update(cx, |this, cx| { this.last_request_timestamp = Instant::now(); this.zeta.update(cx, |zeta, cx| { - zeta.request_completion( - project.as_ref(), - &buffer, - position, - can_collect_data, - cx, - ) + zeta.request_completion(&project, &buffer, position, can_collect_data, cx) }) }); @@ -1762,7 +1647,6 @@ fn tokens_for_bytes(bytes: usize) -> usize { #[cfg(test)] mod tests { - use client::UserStore; use client::test::FakeServer; use clock::FakeSystemClock; use cloud_api_types::{CreateLlmTokenResponse, LlmToken}; @@ -1771,6 +1655,7 @@ mod tests { use indoc::indoc; use language::Point; use settings::SettingsStore; + use util::path; use super::*; @@ -1897,6 +1782,7 @@ mod tests { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); client::init_settings(cx); + Project::init_settings(cx); }); let edits = edits_for_prediction( @@ -1961,6 +1847,7 @@ mod tests { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); client::init_settings(cx); + Project::init_settings(cx); }); let buffer_content = "lorem\n"; @@ -2010,13 +1897,14 @@ mod tests { }); // Construct the fake server to authenticate. let _server = FakeServer::for_client(42, &client, cx).await; - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let zeta = cx.new(|cx| Zeta::new(client, user_store.clone(), cx)); - + let fs = project::FakeFs::new(cx.executor()); + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let buffer = cx.new(|cx| Buffer::local(buffer_content, cx)); let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0))); + + let zeta = cx.new(|cx| Zeta::new(client, project.read(cx).user_store(), cx)); let completion_task = zeta.update(cx, |zeta, cx| { - zeta.request_completion(None, &buffer, cursor, false, cx) + zeta.request_completion(&project, &buffer, cursor, false, cx) }); let completion = completion_task.await.unwrap().unwrap(); @@ -2074,14 +1962,15 @@ mod tests { }); // Construct the fake server to authenticate. let _server = FakeServer::for_client(42, &client, cx).await; - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let zeta = cx.new(|cx| Zeta::new(client, user_store.clone(), cx)); - + let fs = project::FakeFs::new(cx.executor()); + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let buffer = cx.new(|cx| Buffer::local(buffer_content, cx)); let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0))); + + let zeta = cx.new(|cx| Zeta::new(client, project.read(cx).user_store(), cx)); let completion_task = zeta.update(cx, |zeta, cx| { - zeta.request_completion(None, &buffer, cursor, false, cx) + zeta.request_completion(&project, &buffer, cursor, false, cx) }); let completion = completion_task.await.unwrap().unwrap(); diff --git a/crates/zeta_cli/src/main.rs b/crates/zeta_cli/src/main.rs index 5b2d4cf615be67d9493d617ae7de38fdc8fa4b2f..e66eeed80920a0c31c5c06e119e17d418fbc294c 100644 --- a/crates/zeta_cli/src/main.rs +++ b/crates/zeta_cli/src/main.rs @@ -10,7 +10,7 @@ use language::Bias; use language::Buffer; use language::Point; use language_model::LlmApiToken; -use project::{Project, ProjectPath}; +use project::{Project, ProjectPath, Worktree}; use release_channel::AppVersion; use reqwest_client::ReqwestClient; use std::path::{Path, PathBuf}; @@ -129,15 +129,33 @@ async fn get_context( return Err(anyhow!("Absolute paths are not supported in --cursor")); } - let (project, _lsp_open_handle, buffer) = if use_language_server { - let (project, lsp_open_handle, buffer) = - open_buffer_with_language_server(&worktree_path, &cursor.path, app_state, cx).await?; - (Some(project), Some(lsp_open_handle), buffer) + let project = cx.update(|cx| { + Project::local( + app_state.client.clone(), + app_state.node_runtime.clone(), + app_state.user_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + None, + cx, + ) + })?; + + let worktree = project + .update(cx, |project, cx| { + project.create_worktree(&worktree_path, true, cx) + })? + .await?; + + let (_lsp_open_handle, buffer) = if use_language_server { + let (lsp_open_handle, buffer) = + open_buffer_with_language_server(&project, &worktree, &cursor.path, cx).await?; + (Some(lsp_open_handle), buffer) } else { let abs_path = worktree_path.join(&cursor.path); let content = smol::fs::read_to_string(&abs_path).await?; let buffer = cx.new(|cx| Buffer::local(content, cx))?; - (None, None, buffer) + (None, buffer) }; let worktree_name = worktree_path @@ -177,7 +195,7 @@ async fn get_context( let mut gather_context_output = cx .update(|cx| { gather_context( - project.as_ref(), + &project, full_path_str, &snapshot, clipped_cursor, @@ -198,29 +216,11 @@ async fn get_context( } pub async fn open_buffer_with_language_server( - worktree_path: &Path, + project: &Entity, + worktree: &Entity, path: &Path, - app_state: &Arc, cx: &mut AsyncApp, -) -> Result<(Entity, Entity>, Entity)> { - let project = cx.update(|cx| { - Project::local( - app_state.client.clone(), - app_state.node_runtime.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - None, - cx, - ) - })?; - - let worktree = project - .update(cx, |project, cx| { - project.create_worktree(worktree_path, true, cx) - })? - .await?; - +) -> Result<(Entity>, Entity)> { let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath { worktree_id: worktree.id(), path: path.to_path_buf().into(), @@ -237,7 +237,7 @@ pub async fn open_buffer_with_language_server( let log_prefix = path.to_string_lossy().to_string(); wait_for_lang_server(&project, &buffer, log_prefix, cx).await?; - Ok((project, lsp_open_handle, buffer)) + Ok((lsp_open_handle, buffer)) } // TODO: Dedupe with similar function in crates/eval/src/instance.rs From 57c6dbd71e483646cd0409894547e97f664ebed3 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Fri, 5 Sep 2025 09:10:50 +0530 Subject: [PATCH 600/744] linux: Fix IME positioning on scaled display on Wayland (#37600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes IME bounds scaling on Wayland since it uses logical pixels, unlike X11. We now scale only on X11. Windows and macOS don’t use these bounds for IME anyway. Release Notes: - Fixed an issue where the IME popover could appear outside the window or fail to show on Wayland. --- crates/gpui/src/platform.rs | 8 +++---- .../gpui/src/platform/linux/wayland/client.rs | 6 ++--- .../gpui/src/platform/linux/wayland/window.rs | 7 +++--- crates/gpui/src/platform/linux/x11/client.rs | 22 +++++++++---------- crates/gpui/src/platform/linux/x11/window.rs | 7 +++--- crates/gpui/src/platform/mac/window.rs | 9 ++++---- crates/gpui/src/platform/test/window.rs | 6 ++--- crates/gpui/src/platform/windows/window.rs | 2 +- crates/gpui/src/window.rs | 4 +--- 9 files changed, 34 insertions(+), 37 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index d3425c8835bb474ffbed6bc79371340d569d1bfb..444b60ac154424c423c3cd6a827b22cd7024694f 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -39,9 +39,9 @@ use crate::{ Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput, - Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene, - ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, SystemWindowTab, Task, - TaskLabel, Window, WindowControlArea, hash, point, px, size, + Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph, + ShapedRun, SharedString, Size, SvgRenderer, SvgSize, SystemWindowTab, Task, TaskLabel, Window, + WindowControlArea, hash, point, px, size, }; use anyhow::Result; use async_task::Runnable; @@ -548,7 +548,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn set_client_inset(&self, _inset: Pixels) {} fn gpu_specs(&self) -> Option; - fn update_ime_position(&self, _bounds: Bounds); + fn update_ime_position(&self, _bounds: Bounds); #[cfg(any(test, feature = "test-support"))] fn as_test(&mut self) -> Option<&mut TestWindow> { diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 189cfa19545f052cf8ebc75b89c1f955d3396859..8596bddc8dd821426982d618f661d6da621096bb 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -75,8 +75,8 @@ use crate::{ FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon, LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, - PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScrollDelta, - ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size, + PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScrollDelta, ScrollWheelEvent, + Size, TouchPhase, WindowParams, point, px, size, }; use crate::{ SharedString, @@ -323,7 +323,7 @@ impl WaylandClientStatePtr { } } - pub fn update_ime_position(&self, bounds: Bounds) { + pub fn update_ime_position(&self, bounds: Bounds) { let client = self.get_client(); let mut state = client.borrow_mut(); if state.composing || state.text_input.is_none() || state.pre_edit_text.is_some() { diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 7570c58c09e8d5c63091174fa51bc30c54c005e1..76dd89c940c615d726af1cf5922be226d91dfd41 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -25,9 +25,8 @@ use crate::scene::Scene; use crate::{ AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels, PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions, - ResizeEdge, ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance, - WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, WindowDecorations, - WindowParams, px, size, + ResizeEdge, Size, Tiling, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance, + WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, px, size, }; use crate::{ Capslock, @@ -1078,7 +1077,7 @@ impl PlatformWindow for WaylandWindow { } } - fn update_ime_position(&self, bounds: Bounds) { + fn update_ime_position(&self, bounds: Bounds) { let state = self.borrow(); state.client.update_ime_position(bounds); } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 9a43bd64706ec21905b18b8837af2ddc785cba87..42c59701d3ee644b99bc8bb58002b429265c1a45 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -62,8 +62,7 @@ use crate::{ AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke, LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, Pixels, Platform, PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point, RequestFrameOptions, - ScaledPixels, ScrollDelta, Size, TouchPhase, WindowParams, X11Window, - modifiers_from_xinput_info, point, px, + ScrollDelta, Size, TouchPhase, WindowParams, X11Window, modifiers_from_xinput_info, point, px, }; /// Value for DeviceId parameters which selects all devices. @@ -252,7 +251,7 @@ impl X11ClientStatePtr { } } - pub fn update_ime_position(&self, bounds: Bounds) { + pub fn update_ime_position(&self, bounds: Bounds) { let Some(client) = self.get_client() else { return; }; @@ -270,6 +269,7 @@ impl X11ClientStatePtr { state.ximc = Some(ximc); return; }; + let scaled_bounds = bounds.scale(state.scale_factor); let ic_attributes = ximc .build_ic_attributes() .push( @@ -282,8 +282,8 @@ impl X11ClientStatePtr { b.push( xim::AttributeName::SpotLocation, xim::Point { - x: u32::from(bounds.origin.x + bounds.size.width) as i16, - y: u32::from(bounds.origin.y + bounds.size.height) as i16, + x: u32::from(scaled_bounds.origin.x + scaled_bounds.size.width) as i16, + y: u32::from(scaled_bounds.origin.y + scaled_bounds.size.height) as i16, }, ); }) @@ -703,14 +703,14 @@ impl X11Client { state.xim_handler = Some(xim_handler); return; }; - if let Some(area) = window.get_ime_area() { + if let Some(scaled_area) = window.get_ime_area() { ic_attributes = ic_attributes.nested_list(xim::AttributeName::PreeditAttributes, |b| { b.push( xim::AttributeName::SpotLocation, xim::Point { - x: u32::from(area.origin.x + area.size.width) as i16, - y: u32::from(area.origin.y + area.size.height) as i16, + x: u32::from(scaled_area.origin.x + scaled_area.size.width) as i16, + y: u32::from(scaled_area.origin.y + scaled_area.size.height) as i16, }, ); }); @@ -1351,7 +1351,7 @@ impl X11Client { drop(state); window.handle_ime_preedit(text); - if let Some(area) = window.get_ime_area() { + if let Some(scaled_area) = window.get_ime_area() { let ic_attributes = ximc .build_ic_attributes() .push( @@ -1364,8 +1364,8 @@ impl X11Client { b.push( xim::AttributeName::SpotLocation, xim::Point { - x: u32::from(area.origin.x + area.size.width) as i16, - y: u32::from(area.origin.y + area.size.height) as i16, + x: u32::from(scaled_area.origin.x + scaled_area.size.width) as i16, + y: u32::from(scaled_area.origin.y + scaled_area.size.height) as i16, }, ); }) diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 6af943b31761dc26b2cde4090cad4ce6574dd5c9..79a43837252f7dc702b43176d2f06172a3acec18 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -1019,8 +1019,9 @@ impl X11WindowStatePtr { } } - pub fn get_ime_area(&self) -> Option> { + pub fn get_ime_area(&self) -> Option> { let mut state = self.state.borrow_mut(); + let scale_factor = state.scale_factor; let mut bounds: Option> = None; if let Some(mut input_handler) = state.input_handler.take() { drop(state); @@ -1030,7 +1031,7 @@ impl X11WindowStatePtr { let mut state = self.state.borrow_mut(); state.input_handler = Some(input_handler); }; - bounds + bounds.map(|b| b.scale(scale_factor)) } pub fn set_bounds(&self, bounds: Bounds) -> anyhow::Result<()> { @@ -1618,7 +1619,7 @@ impl PlatformWindow for X11Window { } } - fn update_ime_position(&self, bounds: Bounds) { + fn update_ime_position(&self, bounds: Bounds) { let mut state = self.0.state.borrow_mut(); let client = state.client.clone(); drop(state); diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 686cfb314e58c4e10e916a07931fb5f4248ea54e..1230a704062ba835bceb5db5d2ecf05b688e34df 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -4,10 +4,9 @@ use crate::{ ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, - ScaledPixels, SharedString, Size, SystemWindowTab, Timer, WindowAppearance, - WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowKind, WindowParams, - dispatch_get_main_queue, dispatch_sys::dispatch_async_f, platform::PlatformInputHandler, point, - px, size, + SharedString, Size, SystemWindowTab, Timer, WindowAppearance, WindowBackgroundAppearance, + WindowBounds, WindowControlArea, WindowKind, WindowParams, dispatch_get_main_queue, + dispatch_sys::dispatch_async_f, platform::PlatformInputHandler, point, px, size, }; use block::ConcreteBlock; use cocoa::{ @@ -1480,7 +1479,7 @@ impl PlatformWindow for MacWindow { None } - fn update_ime_position(&self, _bounds: Bounds) { + fn update_ime_position(&self, _bounds: Bounds) { let executor = self.0.lock().executor.clone(); executor .spawn(async move { diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index e15bd7aeecec5932eb6386bd47d168eda906dd63..9e87f4504ddd61e34b645ea69ea394c4940f9d55 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -1,8 +1,8 @@ use crate::{ AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DispatchEventResult, GpuSpecs, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, - Point, PromptButton, RequestFrameOptions, ScaledPixels, Size, TestPlatform, TileId, - WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams, + Point, PromptButton, RequestFrameOptions, Size, TestPlatform, TileId, WindowAppearance, + WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams, }; use collections::HashMap; use parking_lot::Mutex; @@ -289,7 +289,7 @@ impl PlatformWindow for TestWindow { unimplemented!() } - fn update_ime_position(&self, _bounds: Bounds) {} + fn update_ime_position(&self, _bounds: Bounds) {} fn gpu_specs(&self) -> Option { None diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 9d001da822315c76aa9a16b010a38407c5730386..aa907c8d734973fc4fc795b6d8ebf7654d1b40de 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -839,7 +839,7 @@ impl PlatformWindow for WindowsWindow { self.0.state.borrow().renderer.gpu_specs().log_err() } - fn update_ime_position(&self, _bounds: Bounds) { + fn update_ime_position(&self, _bounds: Bounds) { // There is no such thing on Windows. } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 0ec73c4b0040e6c65cd8819ecf5d20a9ec1900d0..61d15cb3ed41751ce08c00599bbe28fc0c0cadb2 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4096,9 +4096,7 @@ impl Window { self.on_next_frame(|window, cx| { if let Some(mut input_handler) = window.platform_window.take_input_handler() { if let Some(bounds) = input_handler.selected_bounds(window, cx) { - window - .platform_window - .update_ime_position(bounds.scale(window.scale_factor())); + window.platform_window.update_ime_position(bounds); } window.platform_window.set_input_handler(input_handler); } From 4124bedab796d2ac0a1e57f8b94f72500969797a Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 5 Sep 2025 08:54:08 +0200 Subject: [PATCH 601/744] gpui: Skip `test` attribute expansion for rust-analyzer (#37611) The `test` attribute doesn't really matter to rust-analyzer, so we can make use of its cfg to have it think its just the standard test attribute which should make rust-analyzer slightly less resource intensive in zed. It also should prevent some IDE features from possibly failing within tests. Notably this has no effect outside of this repo, as the `rust-analyzer` cfg only takes effect on workspace member crates. Ideally we'd use the ignored proc macro config here but rust-analyzer still doesn't have toml configs working unfortunately. Release Notes: - N/A --- crates/gpui/src/gpui.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 3c4ee41c16ab7cfc5e42007291e330282b330ecb..0858cb014e33da354eb8a6488982b913b76d2b52 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -121,6 +121,14 @@ mod seal { pub trait Sealed {} } +// This allows r-a to skip expanding the gpui test macro which should +// reduce resource usage a bit as the test attribute is special cased +// to be treated as a no-op. +#[cfg(rust_analyzer)] +pub use core::prelude::v1::test; +#[cfg(not(rust_analyzer))] +pub use gpui_macros::test; + pub use action::*; pub use anyhow::Result; pub use app::*; @@ -134,7 +142,7 @@ pub use elements::*; pub use executor::*; pub use geometry::*; pub use global::*; -pub use gpui_macros::{AppContext, IntoElement, Render, VisualContext, register_action, test}; +pub use gpui_macros::{AppContext, IntoElement, Render, VisualContext, register_action}; pub use http_client; pub use input::*; pub use inspector::*; From bed358718b6693fb32e63ea5d6f3c4d41cbf1277 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 5 Sep 2025 09:56:53 +0200 Subject: [PATCH 602/744] agent_ui: Fix index panic in `SlashCommandCompletion::try_parse` (#37612) Release Notes: - N/A --- .../agent_ui/src/acp/completion_provider.rs | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 44e81433ab5a9d904f329e238b24960e2d568750..ecaa9cd45072191177d3fe15ec16d500b34fb489 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1025,43 +1025,31 @@ impl SlashCommandCompletion { return None; } - let last_command_start = line.rfind('/')?; - if last_command_start >= line.len() { - return Some(Self::default()); - } - if last_command_start > 0 - && line - .chars() - .nth(last_command_start - 1) - .is_some_and(|c| !c.is_whitespace()) + let (prefix, last_command) = line.rsplit_once('/')?; + if prefix.chars().last().is_some_and(|c| !c.is_whitespace()) + || last_command.starts_with(char::is_whitespace) { return None; } - let rest_of_line = &line[last_command_start + 1..]; - - let mut command = None; let mut argument = None; - let mut end = last_command_start + 1; - - if let Some(command_text) = rest_of_line.split_whitespace().next() { - command = Some(command_text.to_string()); - end += command_text.len(); - - // Find the start of arguments after the command - if let Some(args_start) = - rest_of_line[command_text.len()..].find(|c: char| !c.is_whitespace()) - { - let args = &rest_of_line[command_text.len() + args_start..].trim_end(); - if !args.is_empty() { - argument = Some(args.to_string()); - end += args.len() + 1; - } + let mut command = None; + if let Some((command_text, args)) = last_command.split_once(char::is_whitespace) { + if !args.is_empty() { + argument = Some(args.trim_end().to_string()); } - } + command = Some(command_text.to_string()); + } else if !last_command.is_empty() { + command = Some(last_command.to_string()); + }; Some(Self { - source_range: last_command_start + offset_to_line..end + offset_to_line, + source_range: prefix.len() + offset_to_line + ..line + .rfind(|c: char| !c.is_whitespace()) + .unwrap_or_else(|| line.len()) + + 1 + + offset_to_line, command, argument, }) @@ -1180,6 +1168,15 @@ mod tests { }) ); + assert_eq!( + SlashCommandCompletion::try_parse("/拿不到命令 拿不到命令 ", 0), + Some(SlashCommandCompletion { + source_range: 0..30, + command: Some("拿不到命令".to_string()), + argument: Some("拿不到命令".to_string()), + }) + ); + assert_eq!(SlashCommandCompletion::try_parse("Lorem Ipsum", 0), None); assert_eq!(SlashCommandCompletion::try_parse("Lorem /", 0), None); @@ -1187,6 +1184,8 @@ mod tests { assert_eq!(SlashCommandCompletion::try_parse("Lorem /help", 0), None); assert_eq!(SlashCommandCompletion::try_parse("Lorem/", 0), None); + + assert_eq!(SlashCommandCompletion::try_parse("/ ", 0), None); } #[test] From ec58adca131ce2232ccd186947213e6255e6987d Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 5 Sep 2025 13:16:15 +0200 Subject: [PATCH 603/744] languages: Invoke conda activate in conda environments (#37627) This isn't quite right, but using the env manager path causes conda to scream and I am not yet sure why, either way this is an improvement over the status quo Release Notes: - N/A\ --- crates/languages/src/python.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 5e6f5e414f001209d3b4447ae8326a12953c45ac..06fb49293f838fca2d54de076139ac8c4ebacfc2 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -902,6 +902,13 @@ impl ToolchainLister for PythonToolchainProvider { let env = toolchain.name.as_deref().unwrap_or("default"); activation_script.push(format!("pixi shell -e {env}")) } + Some(PythonEnvironmentKind::Conda) => { + if let Some(name) = &toolchain.name { + activation_script.push(format!("conda activate {name}")); + } else { + activation_script.push("conda activate".to_string()); + } + } Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => { if let Some(prefix) = &toolchain.prefix { let activate_keyword = match shell { From 16c4fd4fc563eeedc645a50931129908bc3bfb07 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Fri, 5 Sep 2025 13:19:57 +0200 Subject: [PATCH 604/744] gpui: move Option -> Result conversion out of closure in App::update_window_id (#37624) Doesn't fix anything, but it seems that we do not need to assert and convert into an error until after the closure run to completion, especially since this is the only error we throw. Release Notes: - N/A --- crates/gpui/src/app.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 69d5c0ee4375443ad42a7b25a64a138406ac95a2..8b0b404d1dffbf8a27de1f29437ce9cc2ba63f0f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1358,12 +1358,7 @@ impl App { F: FnOnce(AnyView, &mut Window, &mut App) -> T, { self.update(|cx| { - let mut window = cx - .windows - .get_mut(id) - .context("window not found")? - .take() - .context("window not found")?; + let mut window = cx.windows.get_mut(id)?.take()?; let root_view = window.root.clone().unwrap(); @@ -1380,15 +1375,14 @@ impl App { true }); } else { - cx.windows - .get_mut(id) - .context("window not found")? - .replace(window); + cx.windows.get_mut(id)?.replace(window); } - Ok(result) + Some(result) }) + .context("window not found") } + /// Creates an `AsyncApp`, which can be cloned and has a static lifetime /// so it can be held across `await` points. pub fn to_async(&self) -> AsyncApp { From e30f45cf64dcac1943cf726fad2ff55f8018057b Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 5 Sep 2025 14:22:32 +0200 Subject: [PATCH 605/744] Syntax tree view improvements (#37570) In an effort to improve the experience while developing extensions and improving themes, this PR updates the syntax tree views behavior slightly. Before, the view would always update to the current active editor whilst being used. This was quite painful for improving extension scheme files, as you would always have to change back and forth between editors to have a view at the relevant syntax tree. With this PR, the syntax tree view will now stay attached to the editor it was opened in, similar to preview views. Once the view is shown, the `UseActiveEditor` will become available in the command palette and enable the user to update the view to the last focused editor. On file close, the view will also be updated accordingly. https://github.com/user-attachments/assets/922075e5-9da0-4c1d-9e1a-51e024bf41ea A button is also shown whenever switching is possible. Futhermore, improved the empty state of the view. Lastly, a drive-by cleanup of the `show_action_types` method so there is no need to call `iter()` when calling the method. Release Notes: - The syntax tree view will now stay attached to the buffer it was opened in, similar to the Markdown preview. Use the `UseActiveEditor` action when the view is shown to change it to the last focused editor. --- Cargo.lock | 1 + crates/agent_ui/src/agent_ui.rs | 3 +- .../src/command_palette_hooks.rs | 4 +- crates/copilot/src/copilot.rs | 2 +- crates/language_tools/Cargo.toml | 1 + crates/language_tools/src/syntax_tree_view.rs | 395 +++++++++++++----- crates/settings_ui/src/settings_ui.rs | 2 +- crates/workspace/src/workspace.rs | 6 + crates/zed/src/zed.rs | 3 +- crates/zeta/src/init.rs | 4 +- 10 files changed, 302 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c5e6c8588137b87e00b15e0655a53cdefc518d4f..b0fb3b6f49a90bc92d4dff35a6e76574625cc531 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9247,6 +9247,7 @@ dependencies = [ "anyhow", "client", "collections", + "command_palette_hooks", "copilot", "editor", "futures 0.3.31", diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 93a4a8f748eefc933f809669af841f443888f7ed..e60c0baff99d1f615cbe439aed754a35f2a5c8db 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -337,8 +337,7 @@ fn update_command_palette_filter(cx: &mut App) { ]; filter.show_action_types(edit_prediction_actions.iter()); - filter - .show_action_types([TypeId::of::()].iter()); + filter.show_action_types(&[TypeId::of::()]); } }); } diff --git a/crates/command_palette_hooks/src/command_palette_hooks.rs b/crates/command_palette_hooks/src/command_palette_hooks.rs index df64d53874b4907b3bf586ee7935302c2e6979ae..f1344c5ba6d46fce966ace60d483e3c0fc717f80 100644 --- a/crates/command_palette_hooks/src/command_palette_hooks.rs +++ b/crates/command_palette_hooks/src/command_palette_hooks.rs @@ -76,7 +76,7 @@ impl CommandPaletteFilter { } /// Hides all actions with the given types. - pub fn hide_action_types(&mut self, action_types: &[TypeId]) { + pub fn hide_action_types<'a>(&mut self, action_types: impl IntoIterator) { for action_type in action_types { self.hidden_action_types.insert(*action_type); self.shown_action_types.remove(action_type); @@ -84,7 +84,7 @@ impl CommandPaletteFilter { } /// Shows all actions with the given types. - pub fn show_action_types<'a>(&mut self, action_types: impl Iterator) { + pub fn show_action_types<'a>(&mut self, action_types: impl IntoIterator) { for action_type in action_types { self.shown_action_types.insert(*action_type); self.hidden_action_types.remove(action_type); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index d0a57735ab5a0342b245aa8db72e6b021b3943de..61b7a4e18e4e679c29e26185735352737983c4d1 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1095,7 +1095,7 @@ impl Copilot { _ => { filter.hide_action_types(&signed_in_actions); filter.hide_action_types(&auth_actions); - filter.show_action_types(no_auth_actions.iter()); + filter.show_action_types(&no_auth_actions); } } } diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index b8f85d8d90068be9ad6849528f28522a96206cc8..bbac900cded75e9ca680a1813734f57423ce0ee9 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -16,6 +16,7 @@ doctest = false anyhow.workspace = true client.workspace = true collections.workspace = true +command_palette_hooks.workspace = true copilot.workspace = true editor.workspace = true futures.workspace = true diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index cf84ac34c4af6d04895ba5d1e22c262a1ef8f03c..5700d8d487e990937597295fb5bab761a46f2ba3 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -1,17 +1,22 @@ +use command_palette_hooks::CommandPaletteFilter; use editor::{Anchor, Editor, ExcerptId, SelectionEffects, scroll::Autoscroll}; use gpui::{ - App, AppContext as _, Context, Div, Entity, EventEmitter, FocusHandle, Focusable, Hsla, - InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, - Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle, WeakEntity, Window, - actions, div, rems, uniform_list, + App, AppContext as _, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, Focusable, + Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, + ParentElement, Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle, + WeakEntity, Window, actions, div, rems, uniform_list, }; use language::{Buffer, OwnedSyntaxLayer}; -use std::{mem, ops::Range}; +use std::{any::TypeId, mem, ops::Range}; use theme::ActiveTheme; use tree_sitter::{Node, TreeCursor}; -use ui::{ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex}; +use ui::{ + ButtonCommon, ButtonLike, Clickable, Color, ContextMenu, FluentBuilder as _, IconButton, + IconName, Label, LabelCommon, LabelSize, PopoverMenu, StyledExt, Tooltip, h_flex, v_flex, +}; use workspace::{ - SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, + Event as WorkspaceEvent, SplitDirection, ToolbarItemEvent, ToolbarItemLocation, + ToolbarItemView, Workspace, item::{Item, ItemHandle}, }; @@ -19,17 +24,51 @@ actions!( dev, [ /// Opens the syntax tree view for the current file. - OpenSyntaxTreeView + OpenSyntaxTreeView, + ] +); + +actions!( + syntax_tree_view, + [ + /// Update the syntax tree view to show the last focused file. + UseActiveEditor ] ); pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action(|workspace, _: &OpenSyntaxTreeView, window, cx| { + let syntax_tree_actions = [TypeId::of::()]; + + CommandPaletteFilter::update_global(cx, |this, _| { + this.hide_action_types(&syntax_tree_actions); + }); + + cx.observe_new(move |workspace: &mut Workspace, _, _| { + workspace.register_action(move |workspace, _: &OpenSyntaxTreeView, window, cx| { + CommandPaletteFilter::update_global(cx, |this, _| { + this.show_action_types(&syntax_tree_actions); + }); + let active_item = workspace.active_item(cx); let workspace_handle = workspace.weak_handle(); - let syntax_tree_view = - cx.new(|cx| SyntaxTreeView::new(workspace_handle, active_item, window, cx)); + let syntax_tree_view = cx.new(|cx| { + cx.on_release(move |view: &mut SyntaxTreeView, cx| { + if view + .workspace_handle + .read_with(cx, |workspace, cx| { + workspace.item_of_type::(cx).is_none() + }) + .unwrap_or_default() + { + CommandPaletteFilter::update_global(cx, |this, _| { + this.hide_action_types(&syntax_tree_actions); + }); + } + }) + .detach(); + + SyntaxTreeView::new(workspace_handle, active_item, window, cx) + }); workspace.split_item( SplitDirection::Right, Box::new(syntax_tree_view), @@ -37,6 +76,13 @@ pub fn init(cx: &mut App) { cx, ) }); + workspace.register_action(|workspace, _: &UseActiveEditor, window, cx| { + if let Some(tree_view) = workspace.item_of_type::(cx) { + tree_view.update(cx, |view, cx| { + view.update_active_editor(&Default::default(), window, cx) + }) + } + }); }) .detach(); } @@ -45,6 +91,9 @@ pub struct SyntaxTreeView { workspace_handle: WeakEntity, editor: Option, list_scroll_handle: UniformListScrollHandle, + /// The last active editor in the workspace. Note that this is specifically not the + /// currently shown editor. + last_active_editor: Option>, selected_descendant_ix: Option, hovered_descendant_ix: Option, focus_handle: FocusHandle, @@ -61,6 +110,14 @@ struct EditorState { _subscription: gpui::Subscription, } +impl EditorState { + fn has_language(&self) -> bool { + self.active_buffer + .as_ref() + .is_some_and(|buffer| buffer.active_layer.is_some()) + } +} + #[derive(Clone)] struct BufferState { buffer: Entity, @@ -79,17 +136,25 @@ impl SyntaxTreeView { workspace_handle: workspace_handle.clone(), list_scroll_handle: UniformListScrollHandle::new(), editor: None, + last_active_editor: None, hovered_descendant_ix: None, selected_descendant_ix: None, focus_handle: cx.focus_handle(), }; - this.workspace_updated(active_item, window, cx); - cx.observe_in( + this.handle_item_updated(active_item, window, cx); + + cx.subscribe_in( &workspace_handle.upgrade().unwrap(), window, - |this, workspace, window, cx| { - this.workspace_updated(workspace.read(cx).active_item(cx), window, cx); + move |this, workspace, event, window, cx| match event { + WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::ActiveItemChanged => { + this.handle_item_updated(workspace.read(cx).active_item(cx), window, cx) + } + WorkspaceEvent::ItemRemoved { item_id } => { + this.handle_item_removed(item_id, window, cx); + } + _ => {} }, ) .detach(); @@ -97,20 +162,56 @@ impl SyntaxTreeView { this } - fn workspace_updated( + fn handle_item_updated( &mut self, active_item: Option>, window: &mut Window, cx: &mut Context, ) { - if let Some(item) = active_item - && item.item_id() != cx.entity_id() - && let Some(editor) = item.act_as::(cx) - { + let Some(editor) = active_item + .filter(|item| item.item_id() != cx.entity_id()) + .and_then(|item| item.act_as::(cx)) + else { + return; + }; + + if let Some(editor_state) = self.editor.as_ref().filter(|state| state.has_language()) { + self.last_active_editor = (editor_state.editor != editor).then_some(editor); + } else { self.set_editor(editor, window, cx); } } + fn handle_item_removed( + &mut self, + item_id: &EntityId, + window: &mut Window, + cx: &mut Context, + ) { + if self + .editor + .as_ref() + .is_some_and(|state| state.editor.entity_id() == *item_id) + { + self.editor = None; + // Try activating the last active editor if there is one + self.update_active_editor(&Default::default(), window, cx); + cx.notify(); + } + } + + fn update_active_editor( + &mut self, + _: &UseActiveEditor, + window: &mut Window, + cx: &mut Context, + ) { + let Some(editor) = self.last_active_editor.take() else { + return; + }; + self.set_editor(editor, window, cx); + } + fn set_editor(&mut self, editor: Entity, window: &mut Window, cx: &mut Context) { if let Some(state) = &self.editor { if state.editor == editor { @@ -294,101 +395,153 @@ impl SyntaxTreeView { .pl(rems(depth as f32)) .hover(|style| style.bg(colors.element_hover)) } -} - -impl Render for SyntaxTreeView { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let mut rendered = div().flex_1().bg(cx.theme().colors().editor_background); - if let Some(layer) = self - .editor - .as_ref() - .and_then(|editor| editor.active_buffer.as_ref()) - .and_then(|buffer| buffer.active_layer.as_ref()) - { - let layer = layer.clone(); - rendered = rendered.child(uniform_list( - "SyntaxTreeView", - layer.node().descendant_count(), - cx.processor(move |this, range: Range, _, cx| { - let mut items = Vec::new(); - let mut cursor = layer.node().walk(); - let mut descendant_ix = range.start; - cursor.goto_descendant(descendant_ix); - let mut depth = cursor.depth(); - let mut visited_children = false; - while descendant_ix < range.end { - if visited_children { - if cursor.goto_next_sibling() { - visited_children = false; - } else if cursor.goto_parent() { - depth -= 1; - } else { - break; - } - } else { - items.push( - Self::render_node( - &cursor, - depth, - Some(descendant_ix) == this.selected_descendant_ix, + fn compute_items( + &mut self, + layer: &OwnedSyntaxLayer, + range: Range, + cx: &Context, + ) -> Vec

{ + let mut items = Vec::new(); + let mut cursor = layer.node().walk(); + let mut descendant_ix = range.start; + cursor.goto_descendant(descendant_ix); + let mut depth = cursor.depth(); + let mut visited_children = false; + while descendant_ix < range.end { + if visited_children { + if cursor.goto_next_sibling() { + visited_children = false; + } else if cursor.goto_parent() { + depth -= 1; + } else { + break; + } + } else { + items.push( + Self::render_node( + &cursor, + depth, + Some(descendant_ix) == self.selected_descendant_ix, + cx, + ) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| { + tree_view.update_editor_with_range_for_descendant_ix( + descendant_ix, + window, + cx, + |editor, mut range, window, cx| { + // Put the cursor at the beginning of the node. + mem::swap(&mut range.start, &mut range.end); + + editor.change_selections( + SelectionEffects::scroll(Autoscroll::newest()), + window, + cx, + |selections| { + selections.select_ranges(vec![range]); + }, + ); + }, + ); + }), + ) + .on_mouse_move(cx.listener( + move |tree_view, _: &MouseMoveEvent, window, cx| { + if tree_view.hovered_descendant_ix != Some(descendant_ix) { + tree_view.hovered_descendant_ix = Some(descendant_ix); + tree_view.update_editor_with_range_for_descendant_ix( + descendant_ix, + window, cx, - ) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| { - tree_view.update_editor_with_range_for_descendant_ix( - descendant_ix, - window, cx, - |editor, mut range, window, cx| { - // Put the cursor at the beginning of the node. - mem::swap(&mut range.start, &mut range.end); - - editor.change_selections( - SelectionEffects::scroll(Autoscroll::newest()), - window, cx, - |selections| { - selections.select_ranges(vec![range]); - }, - ); + |editor, range, _, cx| { + editor.clear_background_highlights::(cx); + editor.highlight_background::( + &[range], + |theme| { + theme + .colors() + .editor_document_highlight_write_background }, + cx, ); - }), - ) - .on_mouse_move(cx.listener( - move |tree_view, _: &MouseMoveEvent, window, cx| { - if tree_view.hovered_descendant_ix != Some(descendant_ix) { - tree_view.hovered_descendant_ix = Some(descendant_ix); - tree_view.update_editor_with_range_for_descendant_ix(descendant_ix, window, cx, |editor, range, _, cx| { - editor.clear_background_highlights::( cx); - editor.highlight_background::( - &[range], - |theme| theme.colors().editor_document_highlight_write_background, - cx, - ); - }); - cx.notify(); - } }, - )), - ); - descendant_ix += 1; - if cursor.goto_first_child() { - depth += 1; - } else { - visited_children = true; + ); + cx.notify(); } - } - } - items - }), - ) - .size_full() - .track_scroll(self.list_scroll_handle.clone()) - .text_bg(cx.theme().colors().background).into_any_element()); + }, + )), + ); + descendant_ix += 1; + if cursor.goto_first_child() { + depth += 1; + } else { + visited_children = true; + } + } } + items + } +} - rendered +impl Render for SyntaxTreeView { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .flex_1() + .bg(cx.theme().colors().editor_background) + .map(|this| { + let editor_state = self.editor.as_ref(); + + if let Some(layer) = editor_state + .and_then(|editor| editor.active_buffer.as_ref()) + .and_then(|buffer| buffer.active_layer.as_ref()) + { + let layer = layer.clone(); + this.child( + uniform_list( + "SyntaxTreeView", + layer.node().descendant_count(), + cx.processor(move |this, range: Range, _, cx| { + this.compute_items(&layer, range, cx) + }), + ) + .size_full() + .track_scroll(self.list_scroll_handle.clone()) + .text_bg(cx.theme().colors().background) + .into_any_element(), + ) + } else { + let inner_content = v_flex() + .items_center() + .text_center() + .gap_2() + .max_w_3_5() + .map(|this| { + if editor_state.is_some_and(|state| !state.has_language()) { + this.child(Label::new("Current editor has no associated language")) + .child( + Label::new(concat!( + "Try assigning a language or", + "switching to a different buffer" + )) + .size(LabelSize::Small), + ) + } else { + this.child(Label::new("Not attached to an editor")).child( + Label::new("Focus an editor to show a new tree view") + .size(LabelSize::Small), + ) + } + }); + + this.h_flex() + .size_full() + .justify_center() + .child(inner_content) + } + }) } } @@ -506,6 +659,26 @@ impl SyntaxTreeToolbarItemView { .child(Label::new(active_layer.language.name())) .child(Label::new(format_node_range(active_layer.node()))) } + + fn render_update_button(&mut self, cx: &mut Context) -> Option { + self.tree_view.as_ref().and_then(|view| { + view.update(cx, |view, cx| { + view.last_active_editor.as_ref().map(|editor| { + IconButton::new("syntax-view-update", IconName::RotateCw) + .tooltip({ + let active_tab_name = editor.read_with(cx, |editor, cx| { + editor.tab_content_text(Default::default(), cx) + }); + + Tooltip::text(format!("Update view to '{active_tab_name}'")) + }) + .on_click(cx.listener(|this, _, window, cx| { + this.update_active_editor(&Default::default(), window, cx); + })) + }) + }) + }) + } } fn format_node_range(node: Node) -> String { @@ -522,8 +695,10 @@ fn format_node_range(node: Node) -> String { impl Render for SyntaxTreeToolbarItemView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - self.render_menu(cx) - .unwrap_or_else(|| PopoverMenu::new("Empty Syntax Tree")) + h_flex() + .gap_1() + .children(self.render_menu(cx)) + .children(self.render_update_button(cx)) } } diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index d736f0e174ba13d368794d8f5b623a44845d561b..5fea6dfcebb21be4351172ed4d8a17452b5601ba 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -70,7 +70,7 @@ pub fn init(cx: &mut App) { move |is_enabled, _workspace, _, cx| { if is_enabled { CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.show_action_types(settings_ui_actions.iter()); + filter.show_action_types(&settings_ui_actions); }); } else { CommandPaletteFilter::update_global(cx, |filter, _cx| { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index af86517bb452c1cea77a72f2cf2350ef1e2eb030..0bfcaaf593eca73baa2a6a57def5af17b6ee93b3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1031,6 +1031,9 @@ pub enum Event { item: Box, }, ActiveItemChanged, + ItemRemoved { + item_id: EntityId, + }, UserSavedItem { pane: WeakEntity, item: Box, @@ -3945,6 +3948,9 @@ impl Workspace { { entry.remove(); } + cx.emit(Event::ItemRemoved { + item_id: item.item_id(), + }); } pane::Event::Focus => { window.invalidate_character_coordinates(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 96f0f261dcce9268976f92ec028f0581fb648913..864f6badeb6941aa2d6bd17a43977f84a77461b1 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4502,6 +4502,7 @@ mod tests { "snippets", "supermaven", "svg", + "syntax_tree_view", "tab_switcher", "task", "terminal", @@ -4511,11 +4512,11 @@ mod tests { "toolchain", "variable_list", "vim", + "window", "workspace", "zed", "zed_predict_onboarding", "zeta", - "window", ]; assert_eq!( all_namespaces, diff --git a/crates/zeta/src/init.rs b/crates/zeta/src/init.rs index 6e5b31f99a76cb0e066348150e962396cf1ad9c6..f27667de6332bf4c3b8d2d705f281c9e3ba96a83 100644 --- a/crates/zeta/src/init.rs +++ b/crates/zeta/src/init.rs @@ -86,7 +86,7 @@ fn feature_gate_predict_edits_actions(cx: &mut App) { if is_ai_disabled { filter.hide_action_types(&zeta_all_action_types); } else if has_feature_flag { - filter.show_action_types(rate_completion_action_types.iter()); + filter.show_action_types(&rate_completion_action_types); } else { filter.hide_action_types(&rate_completion_action_types); } @@ -98,7 +98,7 @@ fn feature_gate_predict_edits_actions(cx: &mut App) { if !DisableAiSettings::get_global(cx).disable_ai { if is_enabled { CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.show_action_types(rate_completion_action_types.iter()); + filter.show_action_types(&rate_completion_action_types); }); } else { CommandPaletteFilter::update_global(cx, |filter, _cx| { From 74e8afe9a8f72b1ff0a1f5fd62d78f2eb15f7e15 Mon Sep 17 00:00:00 2001 From: Isaac Hales Date: Fri, 5 Sep 2025 08:57:58 -0600 Subject: [PATCH 606/744] Fix logic for default values for task variables (#37588) This is a small fix for default values in task variables. The [documentation](https://zed.dev/docs/tasks) states > You can also use verbose syntax that allows specifying a default if a given variable is not available: ${ZED_FILE:default_value} I found, however, that this doesn't actually work. Instead, the Zed variable and the default value are just appended in the output. For example, if I run a task `echo ${ZED_ROW:100}` the result I get is `447:100` (in this case it should just be `447`). This PR fixes that. I also added a new test case for handling default values. I also tested the fix in a dev build and it seems to work. There are no UI adjustments. AI disclosure: I used Claude Code to write the code, including the fix and the tests. This is actually my first open-source PR ever, so if I did something wrong, I'd appreciate any tips and I'll make it right! Release Notes: - Fixed task variable substitution always appending the default --- crates/task/src/task_template.rs | 92 +++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 3d1d180557fc457e4200a5b246f2a08e2f5dfcf0..a57f5a175af3fd79ce6b8ef818e3fb97acdc32c2 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -333,15 +333,16 @@ fn substitute_all_template_variables_in_str>( if let Some(substituted_variable) = variable_names.get(variable_name) { substituted_variables.insert(substituted_variable.clone()); } - - let mut name = name.as_ref().to_owned(); - // Got a task variable hit + // Got a task variable hit - use the variable value, ignore default + return Ok(Some(name.as_ref().to_owned())); + } else if variable_name.starts_with(ZED_VARIABLE_NAME_PREFIX) { + // Unknown ZED variable - use default if available if !default.is_empty() { - name.push_str(default); + // Strip the colon and return the default value + return Ok(Some(default[1..].to_owned())); + } else { + bail!("Unknown variable name: {variable_name}"); } - return Ok(Some(name)); - } else if variable_name.starts_with(ZED_VARIABLE_NAME_PREFIX) { - bail!("Unknown variable name: {variable_name}"); } // This is an unknown variable. // We should not error out, as they may come from user environment (e.g. $PATH). That means that the variable substitution might not be perfect. @@ -892,4 +893,81 @@ mod tests { "overwritten" ); } + + #[test] + fn test_variable_default_values() { + let task_with_defaults = TaskTemplate { + label: "test with defaults".to_string(), + command: format!( + "echo ${{{}}}", + VariableName::File.to_string() + ":fallback.txt" + ), + args: vec![ + "${ZED_MISSING_VAR:default_value}".to_string(), + format!("${{{}}}", VariableName::Row.to_string() + ":42"), + ], + ..TaskTemplate::default() + }; + + // Test 1: When ZED_FILE exists, should use actual value and ignore default + let context_with_file = TaskContext { + cwd: None, + task_variables: TaskVariables::from_iter(vec![ + (VariableName::File, "actual_file.rs".to_string()), + (VariableName::Row, "123".to_string()), + ]), + project_env: HashMap::default(), + }; + + let resolved = task_with_defaults + .resolve_task(TEST_ID_BASE, &context_with_file) + .expect("Should resolve task with existing variables"); + + assert_eq!( + resolved.resolved.command.unwrap(), + "echo actual_file.rs", + "Should use actual ZED_FILE value, not default" + ); + assert_eq!( + resolved.resolved.args, + vec!["default_value", "123"], + "Should use default for missing var, actual value for existing var" + ); + + // Test 2: When ZED_FILE doesn't exist, should use default value + let context_without_file = TaskContext { + cwd: None, + task_variables: TaskVariables::from_iter(vec![(VariableName::Row, "456".to_string())]), + project_env: HashMap::default(), + }; + + let resolved = task_with_defaults + .resolve_task(TEST_ID_BASE, &context_without_file) + .expect("Should resolve task using default values"); + + assert_eq!( + resolved.resolved.command.unwrap(), + "echo fallback.txt", + "Should use default value when ZED_FILE is missing" + ); + assert_eq!( + resolved.resolved.args, + vec!["default_value", "456"], + "Should use defaults for missing vars" + ); + + // Test 3: Missing ZED variable without default should fail + let task_no_default = TaskTemplate { + label: "test no default".to_string(), + command: "${ZED_MISSING_NO_DEFAULT}".to_string(), + ..TaskTemplate::default() + }; + + assert!( + task_no_default + .resolve_task(TEST_ID_BASE, &TaskContext::default()) + .is_none(), + "Should fail when ZED variable has no default and doesn't exist" + ); + } } From 360e372b57d2913ed670bd0edbca73adfe5956f4 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 5 Sep 2025 11:09:32 -0400 Subject: [PATCH 607/744] linux: Restore ctrl-escape to keymap (#37636) Closes: https://github.com/zed-industries/zed/issues/37628 Follow-up to: https://github.com/zed-industries/zed/pull/36712 Release Notes: - linux: Fix for ctrl-escape not escaping the tab switcher. --- assets/keymaps/default-linux.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 28518490ccbe9d3a4e8161ffbc32ed5c27ae0d84..44234b819abdf10231e4cb4e4fb7dfe335d19778 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -16,6 +16,7 @@ "up": "menu::SelectPrevious", "enter": "menu::Confirm", "ctrl-enter": "menu::SecondaryConfirm", + "ctrl-escape": "menu::Cancel", "ctrl-c": "menu::Cancel", "escape": "menu::Cancel", "alt-shift-enter": "menu::Restart", From 3d37611b6f20b158454014b5d886805a06902e71 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Fri, 5 Sep 2025 17:43:39 +0200 Subject: [PATCH 608/744] cli: Rename script zed-wsl to zed, and enable on non-WSL (#37631) Closes #23026 With this hotfix, git committing from the built-in Zed terminal (well, PowerShell), now works. Release Notes: - N/A --- .gitattributes | 2 +- crates/zed/resources/windows/{zed-wsl => zed.sh} | 4 ++-- script/bundle-windows.ps1 | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename crates/zed/resources/windows/{zed-wsl => zed.sh} (88%) diff --git a/.gitattributes b/.gitattributes index 0dedc2d567dac982b217453c266a046b09ea4830..37d28993301fef9c7eb4da0847cc9f4b7a5f1fbb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,4 +2,4 @@ *.json linguist-language=JSON-with-Comments # Ensure the WSL script always has LF line endings, even on Windows -crates/zed/resources/windows/zed-wsl text eol=lf +crates/zed/resources/windows/zed text eol=lf diff --git a/crates/zed/resources/windows/zed-wsl b/crates/zed/resources/windows/zed.sh similarity index 88% rename from crates/zed/resources/windows/zed-wsl rename to crates/zed/resources/windows/zed.sh index d3cbb93af6f5979508229656deadeab0dbf21661..734b1a7eb00dc304786a58674171fdb5872b90c8 100644 --- a/crates/zed/resources/windows/zed-wsl +++ b/crates/zed/resources/windows/zed.sh @@ -20,6 +20,6 @@ if [ $IN_WSL = true ]; then "$ZED_PATH/zed.exe" --wsl "$WSL_USER@$WSL_DISTRO_NAME" "$@" exit $? else - echo "Only WSL is supported for now" >&2 - exit 1 + "$ZED_PATH/zed.exe" "$@" + exit $? fi diff --git a/script/bundle-windows.ps1 b/script/bundle-windows.ps1 index 84ad39fb706f9d3e0e4af73a68b468e0bea33ee1..a26abf8413f375b611d01d57b61ac3f91a960dd7 100644 --- a/script/bundle-windows.ps1 +++ b/script/bundle-windows.ps1 @@ -150,7 +150,7 @@ function CollectFiles { Move-Item -Path "$innoDir\zed_explorer_command_injector.appx" -Destination "$innoDir\appx\zed_explorer_command_injector.appx" -Force Move-Item -Path "$innoDir\zed_explorer_command_injector.dll" -Destination "$innoDir\appx\zed_explorer_command_injector.dll" -Force Move-Item -Path "$innoDir\cli.exe" -Destination "$innoDir\bin\zed.exe" -Force - Move-Item -Path "$innoDir\zed-wsl" -Destination "$innoDir\bin\zed" -Force + Move-Item -Path "$innoDir\zed.sh" -Destination "$innoDir\bin\zed" -Force Move-Item -Path "$innoDir\auto_update_helper.exe" -Destination "$innoDir\tools\auto_update_helper.exe" -Force Move-Item -Path ".\AGS_SDK-6.3.0\ags_lib\lib\amd_ags_x64.dll" -Destination "$innoDir\amd_ags_x64.dll" -Force } From fb6cc8794f360acf0c0671502bb456ae7233fc88 Mon Sep 17 00:00:00 2001 From: Yacine Hmito <6893840+yacinehmito@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:56:40 +0200 Subject: [PATCH 609/744] Fix typo in development docs for macOS (#37607) Release Notes: - N/A --- docs/src/development/macos.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/development/macos.md b/docs/src/development/macos.md index c7e92623d4e226cb575da524fd8241fba3730fd6..851e2efdd7cdf15b9617445fe065149da8a5721f 100644 --- a/docs/src/development/macos.md +++ b/docs/src/development/macos.md @@ -33,7 +33,7 @@ Clone down the [Zed repository](https://github.com/zed-industries/zed). ### Backend Dependencies (optional) {#backend-dependencies} -If you are looking to develop Zed collaboration features using a local collabortation server, please see: [Local Collaboration](./local-collaboration.md) docs. +If you are looking to develop Zed collaboration features using a local collaboration server, please see: [Local Collaboration](./local-collaboration.md) docs. ## Building Zed from Source From 91ab0636ec6ed5a3df39a61bb56d24f715865c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Sat, 6 Sep 2025 00:25:55 +0800 Subject: [PATCH 610/744] windows: Make sure `zed.sh` using the correct line ending (#37650) This got missed in the changes from #37631 Release Notes: - N/A --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 37d28993301fef9c7eb4da0847cc9f4b7a5f1fbb..57afd4ea6942bd3985fb7395101800706d7b4ae6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,4 +2,4 @@ *.json linguist-language=JSON-with-Comments # Ensure the WSL script always has LF line endings, even on Windows -crates/zed/resources/windows/zed text eol=lf +crates/zed/resources/windows/zed.sh text eol=lf From 638320b21e8a893a8da0ee23c438127cf82f5f85 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 5 Sep 2025 12:40:47 -0400 Subject: [PATCH 611/744] Improve macOS version information in telemetry (#37185) macOS versions are currently reported as `macOS 26.0.0`. But this makes it impossible to differentiate amongst macOS Beta releases which have the same version number (`X.0.0`) but are different builds. This PR adds build number info to `os_version` for macOS Betas and [Rapid Security Response](https://support.apple.com/en-us/102657) release that have identical version numbers to stable release, but have different builds numbers. We can differentiate them because the build numbers end with a letter. | Version | Before | After | | - | - | - | | macOS Sonoma 14.7.8 | 14.7.8 | 14.7.8 | | macOS Sequoia 15.6.1 | 15.6.1 | 15.6.1 | | mcOS Ventura 13.3.1 | 13.3.1 | 13.3.1 | | macOS Ventura 13.3.1 (a) | 13.3.1 | 13.3.1 (Build 22E772610a) | | macOS Tahoe 26.0.0 (Beta1) | 26.0.0 | 26.0.0 (Build 25A5316a) | | macOS Tahoe 26.0.0 (Beta5) | 26.0.0 | 26.0.0 (Build 25A5349a) | This should cause minimal telemetry changes and only impacting a macOS betas and a couple specific older macOS versions, but will allow differentiation between macOS beta releases in GitHub issues. Alternatives: 1. Leave as-is (can't differentiate between macOS beta builds) 2. Always include build number info (impacts telemetry; more consistent going forward; differentiates non-final Release Candidates which don't include a trailing letter) I couldn't find a cocoa method to retrieve macOS build number, so I switched dependencies from `cocoa` to `objc2-foundation` in the client crate. We already depend upon this crate as a dependency of `blade-graphics` so I matched the features of that and so workspace-hack doesn't change. https://github.com/zed-industries/zed/blob/1ebc69a44708f344449c0c9d47e33b414277adec/tooling/workspace-hack/Cargo.toml#L355 Release Notes: - N/A --- Cargo.lock | 2 +- Cargo.toml | 25 +++++++++++++++++++++++++ crates/client/Cargo.toml | 2 +- crates/client/src/telemetry.rs | 25 +++++++++++++------------ 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b0fb3b6f49a90bc92d4dff35a6e76574625cc531..975e762dddefa6d2c67f8957e4356a69c903f187 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3070,7 +3070,6 @@ dependencies = [ "clock", "cloud_api_client", "cloud_llm_client", - "cocoa 0.26.0", "collections", "credentials_provider", "derive_more", @@ -3083,6 +3082,7 @@ dependencies = [ "http_client_tls", "httparse", "log", + "objc2-foundation", "parking_lot", "paths", "postage", diff --git a/Cargo.toml b/Cargo.toml index f389153efe9d0719187d14bb554042fcf2888376..1de877334fe6cf7c5d4c84649e27b0633579723e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -537,6 +537,31 @@ nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c80421 nix = "0.29" num-format = "0.4.4" objc = "0.2" +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" +] } open = "5.0.0" ordered-float = "2.1.1" palette = { version = "0.7.5", default-features = false, features = ["std"] } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 5c6d1157fd710de0e1dd160b611c0bd7c6667c4d..01007cdc6618996735c859284e3860b936f540e8 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -75,7 +75,7 @@ util = { workspace = true, features = ["test-support"] } windows.workspace = true [target.'cfg(target_os = "macos")'.dependencies] -cocoa.workspace = true +objc2-foundation.workspace = true [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] tokio-native-tls = "0.3" diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index a5c1532c7563ab4bcb5f8826dcc18f3d52daf222..e3123400866516bda26b071e288bdad9dd5964e0 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -84,6 +84,10 @@ static DOTNET_PROJECT_FILES_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"^(global\.json|Directory\.Build\.props|.*\.(csproj|fsproj|vbproj|sln))$").unwrap() }); +#[cfg(target_os = "macos")] +static MACOS_VERSION_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"(\s*\(Build [^)]*[0-9]\))").unwrap()); + pub fn os_name() -> String { #[cfg(target_os = "macos")] { @@ -108,19 +112,16 @@ pub fn os_name() -> String { pub fn os_version() -> String { #[cfg(target_os = "macos")] { - use cocoa::base::nil; - use cocoa::foundation::NSProcessInfo; - - unsafe { - let process_info = cocoa::foundation::NSProcessInfo::processInfo(nil); - let version = process_info.operatingSystemVersion(); - gpui::SemanticVersion::new( - version.majorVersion as usize, - version.minorVersion as usize, - version.patchVersion as usize, - ) + use objc2_foundation::NSProcessInfo; + let process_info = NSProcessInfo::processInfo(); + let version_nsstring = unsafe { process_info.operatingSystemVersionString() }; + // "Version 15.6.1 (Build 24G90)" -> "15.6.1 (Build 24G90)" + let version_string = version_nsstring.to_string().replace("Version ", ""); + // "15.6.1 (Build 24G90)" -> "15.6.1" + // "26.0.0 (Build 25A5349a)" -> unchanged (Beta or Rapid Security Response; ends with letter) + MACOS_VERSION_REGEX + .replace_all(&version_string, "") .to_string() - } } #[cfg(any(target_os = "linux", target_os = "freebsd"))] { From b3405c3bd18749f3f7acda52670ee03528d655b8 Mon Sep 17 00:00:00 2001 From: Matin Aniss <76515905+MatinAniss@users.noreply.github.com> Date: Sat, 6 Sep 2025 02:52:57 +1000 Subject: [PATCH 612/744] Add line ending selector (#35392) Partially addresses this issue #5294 Adds a selector between `LF` and `CRLF` for the buffer's line endings, the checkmark denotes the currently selected line ending. Selector image Release Notes: - Added line ending selector. --------- Co-authored-by: Conrad Irwin --- Cargo.lock | 16 ++ Cargo.toml | 2 + crates/language/src/buffer.rs | 35 +++- crates/language/src/buffer_tests.rs | 72 +++++++ crates/language/src/proto.rs | 25 +++ crates/line_ending_selector/Cargo.toml | 24 +++ crates/line_ending_selector/LICENSE-GPL | 1 + .../src/line_ending_selector.rs | 192 ++++++++++++++++++ crates/proto/proto/buffer.proto | 7 + crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + crates/zed/src/zed.rs | 1 + 12 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 crates/line_ending_selector/Cargo.toml create mode 120000 crates/line_ending_selector/LICENSE-GPL create mode 100644 crates/line_ending_selector/src/line_ending_selector.rs diff --git a/Cargo.lock b/Cargo.lock index 975e762dddefa6d2c67f8957e4356a69c903f187..fbdf0e848c356620f2a2cca800cf40ef850c3b13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9518,6 +9518,21 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "line_ending_selector" +version = "0.1.0" +dependencies = [ + "editor", + "gpui", + "language", + "picker", + "project", + "ui", + "util", + "workspace", + "workspace-hack", +] + [[package]] name = "link-cplusplus" version = "1.0.10" @@ -20492,6 +20507,7 @@ dependencies = [ "language_tools", "languages", "libc", + "line_ending_selector", "livekit_client", "log", "markdown", diff --git a/Cargo.toml b/Cargo.toml index 1de877334fe6cf7c5d4c84649e27b0633579723e..d8e8040cd920e1f6b5a561c80a4a205d030cbb49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,7 @@ members = [ "crates/language_selector", "crates/language_tools", "crates/languages", + "crates/line_ending_selector", "crates/livekit_api", "crates/livekit_client", "crates/lmstudio", @@ -323,6 +324,7 @@ language_models = { path = "crates/language_models" } language_selector = { path = "crates/language_selector" } language_tools = { path = "crates/language_tools" } languages = { path = "crates/languages" } +line_ending_selector = { path = "crates/line_ending_selector" } livekit_api = { path = "crates/livekit_api" } livekit_client = { path = "crates/livekit_client" } lmstudio = { path = "crates/lmstudio" } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index c86787e1f9de8cf31037187dc667e2a7e428cea9..2a303bb9a0ff44981def92f593595e92629be1e5 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -284,6 +284,14 @@ pub enum Operation { /// The language server ID. server_id: LanguageServerId, }, + + /// An update to the line ending type of this buffer. + UpdateLineEnding { + /// The line ending type. + line_ending: LineEnding, + /// The buffer's lamport timestamp. + lamport_timestamp: clock::Lamport, + }, } /// An event that occurs in a buffer. @@ -1240,6 +1248,21 @@ impl Buffer { self.syntax_map.lock().language_registry() } + /// Assign the line ending type to the buffer. + pub fn set_line_ending(&mut self, line_ending: LineEnding, cx: &mut Context) { + self.text.set_line_ending(line_ending); + + let lamport_timestamp = self.text.lamport_clock.tick(); + self.send_operation( + Operation::UpdateLineEnding { + line_ending, + lamport_timestamp, + }, + true, + cx, + ); + } + /// Assign the buffer a new [`Capability`]. pub fn set_capability(&mut self, capability: Capability, cx: &mut Context) { if self.capability != capability { @@ -2557,7 +2580,7 @@ impl Buffer { Operation::UpdateSelections { selections, .. } => selections .iter() .all(|s| self.can_resolve(&s.start) && self.can_resolve(&s.end)), - Operation::UpdateCompletionTriggers { .. } => true, + Operation::UpdateCompletionTriggers { .. } | Operation::UpdateLineEnding { .. } => true, } } @@ -2623,6 +2646,13 @@ impl Buffer { } self.text.lamport_clock.observe(lamport_timestamp); } + Operation::UpdateLineEnding { + line_ending, + lamport_timestamp, + } => { + self.text.set_line_ending(line_ending); + self.text.lamport_clock.observe(lamport_timestamp); + } } } @@ -4814,6 +4844,9 @@ impl operation_queue::Operation for Operation { } | Operation::UpdateCompletionTriggers { lamport_timestamp, .. + } + | Operation::UpdateLineEnding { + lamport_timestamp, .. } => *lamport_timestamp, } } diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 5b88112c956e5466748fc349825a78f6232e540e..050ec457dfe6e83d420206b381d5524b9c583441 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -67,6 +67,78 @@ fn test_line_endings(cx: &mut gpui::App) { }); } +#[gpui::test] +fn test_set_line_ending(cx: &mut TestAppContext) { + let base = cx.new(|cx| Buffer::local("one\ntwo\nthree\n", cx)); + let base_replica = cx.new(|cx| { + Buffer::from_proto(1, Capability::ReadWrite, base.read(cx).to_proto(cx), None).unwrap() + }); + base.update(cx, |_buffer, cx| { + cx.subscribe(&base_replica, |this, _, event, cx| { + if let BufferEvent::Operation { + operation, + is_local: true, + } = event + { + this.apply_ops([operation.clone()], cx); + } + }) + .detach(); + }); + base_replica.update(cx, |_buffer, cx| { + cx.subscribe(&base, |this, _, event, cx| { + if let BufferEvent::Operation { + operation, + is_local: true, + } = event + { + this.apply_ops([operation.clone()], cx); + } + }) + .detach(); + }); + + // Base + base_replica.read_with(cx, |buffer, _| { + assert_eq!(buffer.line_ending(), LineEnding::Unix); + }); + base.update(cx, |buffer, cx| { + assert_eq!(buffer.line_ending(), LineEnding::Unix); + buffer.set_line_ending(LineEnding::Windows, cx); + assert_eq!(buffer.line_ending(), LineEnding::Windows); + }); + base_replica.read_with(cx, |buffer, _| { + assert_eq!(buffer.line_ending(), LineEnding::Windows); + }); + base.update(cx, |buffer, cx| { + buffer.set_line_ending(LineEnding::Unix, cx); + assert_eq!(buffer.line_ending(), LineEnding::Unix); + }); + base_replica.read_with(cx, |buffer, _| { + assert_eq!(buffer.line_ending(), LineEnding::Unix); + }); + + // Replica + base.read_with(cx, |buffer, _| { + assert_eq!(buffer.line_ending(), LineEnding::Unix); + }); + base_replica.update(cx, |buffer, cx| { + assert_eq!(buffer.line_ending(), LineEnding::Unix); + buffer.set_line_ending(LineEnding::Windows, cx); + assert_eq!(buffer.line_ending(), LineEnding::Windows); + }); + base.read_with(cx, |buffer, _| { + assert_eq!(buffer.line_ending(), LineEnding::Windows); + }); + base_replica.update(cx, |buffer, cx| { + buffer.set_line_ending(LineEnding::Unix, cx); + assert_eq!(buffer.line_ending(), LineEnding::Unix); + }); + base.read_with(cx, |buffer, _| { + assert_eq!(buffer.line_ending(), LineEnding::Unix); + }); +} + #[gpui::test] fn test_select_language(cx: &mut App) { init_settings(cx, |_| {}); diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 0d5a8e916c8712733dcc7a26faa984453cdd30fd..bc85b10859632fc3e2cf61c663b7159a023f4f3a 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -90,6 +90,15 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { language_server_id: server_id.to_proto(), }, ), + + crate::Operation::UpdateLineEnding { + line_ending, + lamport_timestamp, + } => proto::operation::Variant::UpdateLineEnding(proto::operation::UpdateLineEnding { + replica_id: lamport_timestamp.replica_id as u32, + lamport_timestamp: lamport_timestamp.value, + line_ending: serialize_line_ending(*line_ending) as i32, + }), }), } } @@ -341,6 +350,18 @@ pub fn deserialize_operation(message: proto::Operation) -> Result { + crate::Operation::UpdateLineEnding { + lamport_timestamp: clock::Lamport { + replica_id: message.replica_id as ReplicaId, + value: message.lamport_timestamp, + }, + line_ending: deserialize_line_ending( + proto::LineEnding::from_i32(message.line_ending) + .context("missing line_ending")?, + ), + } + } }, ) } @@ -496,6 +517,10 @@ pub fn lamport_timestamp_for_operation(operation: &proto::Operation) -> Option { + replica_id = op.replica_id; + value = op.lamport_timestamp; + } } Some(clock::Lamport { diff --git a/crates/line_ending_selector/Cargo.toml b/crates/line_ending_selector/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..7c5c8f6d8f3996771f832c28d5d71b857bb0b3b6 --- /dev/null +++ b/crates/line_ending_selector/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "line_ending_selector" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/line_ending_selector.rs" +doctest = false + +[dependencies] +editor.workspace = true +gpui.workspace = true +language.workspace = true +picker.workspace = true +project.workspace = true +ui.workspace = true +util.workspace = true +workspace.workspace = true +workspace-hack.workspace = true diff --git a/crates/line_ending_selector/LICENSE-GPL b/crates/line_ending_selector/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/line_ending_selector/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/line_ending_selector/src/line_ending_selector.rs b/crates/line_ending_selector/src/line_ending_selector.rs new file mode 100644 index 0000000000000000000000000000000000000000..532f0b051d79e25229d7cb72419ca557edd5b477 --- /dev/null +++ b/crates/line_ending_selector/src/line_ending_selector.rs @@ -0,0 +1,192 @@ +use editor::Editor; +use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, actions}; +use language::{Buffer, LineEnding}; +use picker::{Picker, PickerDelegate}; +use project::Project; +use std::sync::Arc; +use ui::{ListItem, ListItemSpacing, prelude::*}; +use util::ResultExt; +use workspace::ModalView; + +actions!( + line_ending, + [ + /// Toggles the line ending selector modal. + Toggle + ] +); + +pub fn init(cx: &mut App) { + cx.observe_new(LineEndingSelector::register).detach(); +} + +pub struct LineEndingSelector { + picker: Entity>, +} + +impl LineEndingSelector { + fn register(editor: &mut Editor, _window: Option<&mut Window>, cx: &mut Context) { + let editor_handle = cx.weak_entity(); + editor + .register_action(move |_: &Toggle, window, cx| { + Self::toggle(&editor_handle, window, cx); + }) + .detach(); + } + + fn toggle(editor: &WeakEntity, window: &mut Window, cx: &mut App) { + let Some((workspace, buffer)) = editor + .update(cx, |editor, cx| { + Some((editor.workspace()?, editor.active_excerpt(cx)?.1)) + }) + .ok() + .flatten() + else { + return; + }; + + workspace.update(cx, |workspace, cx| { + let project = workspace.project().clone(); + workspace.toggle_modal(window, cx, move |window, cx| { + LineEndingSelector::new(buffer, project, window, cx) + }); + }) + } + + fn new( + buffer: Entity, + project: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let line_ending = buffer.read(cx).line_ending(); + let delegate = + LineEndingSelectorDelegate::new(cx.entity().downgrade(), buffer, project, line_ending); + let picker = cx.new(|cx| Picker::nonsearchable_uniform_list(delegate, window, cx)); + Self { picker } + } +} + +impl Render for LineEndingSelector { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + v_flex().w(rems(34.)).child(self.picker.clone()) + } +} + +impl Focusable for LineEndingSelector { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl EventEmitter for LineEndingSelector {} +impl ModalView for LineEndingSelector {} + +struct LineEndingSelectorDelegate { + line_ending_selector: WeakEntity, + buffer: Entity, + project: Entity, + line_ending: LineEnding, + matches: Vec, + selected_index: usize, +} + +impl LineEndingSelectorDelegate { + fn new( + line_ending_selector: WeakEntity, + buffer: Entity, + project: Entity, + line_ending: LineEnding, + ) -> Self { + Self { + line_ending_selector, + buffer, + project, + line_ending, + matches: vec![LineEnding::Unix, LineEnding::Windows], + selected_index: 0, + } + } +} + +impl PickerDelegate for LineEndingSelectorDelegate { + type ListItem = ListItem; + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + "Select a line ending…".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { + if let Some(line_ending) = self.matches.get(self.selected_index) { + self.buffer.update(cx, |this, cx| { + this.set_line_ending(*line_ending, cx); + }); + let buffer = self.buffer.clone(); + let project = self.project.clone(); + cx.defer(move |cx| { + project.update(cx, |this, cx| { + this.save_buffer(buffer, cx).detach(); + }); + }); + } + self.dismissed(window, cx); + } + + fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { + self.line_ending_selector + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _: &mut Context>, + ) { + self.selected_index = ix; + } + + fn update_matches( + &mut self, + _query: String, + _window: &mut Window, + _cx: &mut Context>, + ) -> gpui::Task<()> { + return Task::ready(()); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _: &mut Window, + _: &mut Context>, + ) -> Option { + let line_ending = self.matches[ix]; + let label = match line_ending { + LineEnding::Unix => "LF", + LineEnding::Windows => "CRLF", + }; + + let mut list_item = ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .toggle_state(selected) + .child(Label::new(label)); + + if self.line_ending == line_ending { + list_item = list_item.end_slot(Icon::new(IconName::Check).color(Color::Muted)); + } + + Some(list_item) + } +} diff --git a/crates/proto/proto/buffer.proto b/crates/proto/proto/buffer.proto index f4dacf2fdca97bf9766c8de348a67cd18f8fb973..4580fd8e9db80e7dc54b1c997f8df108e3bf9330 100644 --- a/crates/proto/proto/buffer.proto +++ b/crates/proto/proto/buffer.proto @@ -143,6 +143,7 @@ message Operation { UpdateSelections update_selections = 3; UpdateDiagnostics update_diagnostics = 4; UpdateCompletionTriggers update_completion_triggers = 5; + UpdateLineEnding update_line_ending = 6; } message Edit { @@ -174,6 +175,12 @@ message Operation { repeated string triggers = 3; uint64 language_server_id = 4; } + + message UpdateLineEnding { + uint32 replica_id = 1; + uint32 lamport_timestamp = 2; + LineEnding line_ending = 3; + } } message ProjectTransaction { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index f2295d5fa732d9e36e2b37cf346199f35cabc803..bee6c87670c87a08945918a3dd49b26463a3a3ef 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -93,6 +93,7 @@ language_models.workspace = true language_selector.workspace = true language_tools.workspace = true languages = { workspace = true, features = ["load-grammars"] } +line_ending_selector.workspace = true libc.workspace = true log.workspace = true markdown.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 9582e7a2ab541243a768370eb08ed1f4f1c465a3..3287e866e48058a763c7db6633c1db4252fc0bec 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -620,6 +620,7 @@ pub fn main() { terminal_view::init(cx); journal::init(app_state.clone(), cx); language_selector::init(cx); + line_ending_selector::init(cx); toolchain_selector::init(cx); theme_selector::init(cx); settings_profile_selector::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 864f6badeb6941aa2d6bd17a43977f84a77461b1..fda43a10bad9acc6ae2864519cac5def08fb2f84 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4480,6 +4480,7 @@ mod tests { "keymap_editor", "keystroke_input", "language_selector", + "line_ending", "lsp_tool", "markdown", "menu", From b65fb0626491be66a26718ea43f29e7d89f74c9f Mon Sep 17 00:00:00 2001 From: Dino Date: Fri, 5 Sep 2025 18:12:51 +0100 Subject: [PATCH 613/744] editor: Fix text manipulation on line mode selections (#37646) This commit updates the implementation for `editor::Editor.manipulate_text` to use `editor::selections_collection::SelectionsCollection.all_adjusted` instead of `editor::selections_collection::SelectionsCollection.all`, as the former takes into account the selection's `line_mode`, fixing the issue where, if an user was in vim's visual line mode, running the `editor: convert to upper case` command would not work as expected. Closes #36953 Release Notes: - Fixed bug where using the editor's convert case commands while in vim's Visual Line mode would not work as expected --- crates/editor/src/editor.rs | 10 +++++++--- crates/editor/src/editor_tests.rs | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 37951074d15bbb8f34bcbaba9d839eae5d34cf1e..fd2299f37dfc91c4a1d287c549269a7a77fc07e7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11391,14 +11391,17 @@ impl Editor { let mut edits = Vec::new(); let mut selection_adjustment = 0i32; - for selection in self.selections.all::(cx) { + for selection in self.selections.all_adjusted(cx) { let selection_is_empty = selection.is_empty(); let (start, end) = if selection_is_empty { let (word_range, _) = buffer.surrounding_word(selection.start, false); (word_range.start, word_range.end) } else { - (selection.start, selection.end) + ( + buffer.point_to_offset(selection.start), + buffer.point_to_offset(selection.end), + ) }; let text = buffer.text_for_range(start..end).collect::(); @@ -11409,7 +11412,8 @@ impl Editor { start: (start as i32 - selection_adjustment) as usize, end: ((start + text.len()) as i32 - selection_adjustment) as usize, goal: SelectionGoal::None, - ..selection + id: selection.id, + reversed: selection.reversed, }); selection_adjustment += old_length - text.len() as i32; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 90e488368f99ea50bdcbfc671a359fa5e899f59e..f4569b436488728f197183b27c63b2706881c8cb 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5363,6 +5363,20 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { cx.assert_editor_state(indoc! {" «HeLlO, wOrLD!ˇ» "}); + + // Test selections with `line_mode = true`. + cx.update_editor(|editor, _window, _cx| editor.selections.line_mode = true); + cx.set_state(indoc! {" + «The quick brown + fox jumps over + tˇ»he lazy dog + "}); + cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx)); + cx.assert_editor_state(indoc! {" + «THE QUICK BROWN + FOX JUMPS OVER + THE LAZY DOGˇ» + "}); } #[gpui::test] From 5d374193bb7493b993b661ea1231f113946b784b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 5 Sep 2025 10:34:39 -0700 Subject: [PATCH 614/744] Add terminal::Toggle (#37585) Co-Authored-By: Brandan Release Notes: - Added a new action `terminal::Toggle` that is by default bound to 'ctrl-\`'. This copies the default behaviour from VSCode and Jetbrains where the terminal opens and closes correctly. If you'd like the old behaviour you can rebind 'ctrl-\`' to `terminal::ToggleFocus` Co-authored-by: Brandan --- assets/keymaps/default-linux.json | 2 +- assets/keymaps/default-macos.json | 2 +- assets/keymaps/default-windows.json | 2 +- assets/keymaps/linux/jetbrains.json | 2 +- assets/keymaps/macos/jetbrains.json | 2 +- crates/terminal_view/src/terminal_panel.rs | 9 +++++++++ crates/vim/src/command.rs | 4 ++-- crates/workspace/src/workspace.rs | 10 ++++++++++ 8 files changed, 26 insertions(+), 7 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 44234b819abdf10231e4cb4e4fb7dfe335d19778..70a002cf081deaf5df66a2173dc17e7f02ce3aeb 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -583,7 +583,7 @@ "ctrl-n": "workspace::NewFile", "shift-new": "workspace::NewWindow", "ctrl-shift-n": "workspace::NewWindow", - "ctrl-`": "terminal_panel::ToggleFocus", + "ctrl-`": "terminal_panel::Toggle", "f10": ["app_menu::OpenApplicationMenu", "Zed"], "alt-1": ["workspace::ActivatePane", 0], "alt-2": ["workspace::ActivatePane", 1], diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 954684c826b18828857c6411e2413aa514aeec45..21504c7e623583017459baaac7d25191d7a08b68 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -649,7 +649,7 @@ "alt-shift-enter": "toast::RunAction", "cmd-shift-s": "workspace::SaveAs", "cmd-shift-n": "workspace::NewWindow", - "ctrl-`": "terminal_panel::ToggleFocus", + "ctrl-`": "terminal_panel::Toggle", "cmd-1": ["workspace::ActivatePane", 0], "cmd-2": ["workspace::ActivatePane", 1], "cmd-3": ["workspace::ActivatePane", 2], diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 728907e60ca3361270f15b20f66aaf7571be6ac2..1c9f1281882dc136daa7a3912d3d92b3516a4441 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -599,7 +599,7 @@ "ctrl-n": "workspace::NewFile", "shift-new": "workspace::NewWindow", "ctrl-shift-n": "workspace::NewWindow", - "ctrl-`": "terminal_panel::ToggleFocus", + "ctrl-`": "terminal_panel::Toggle", "f10": ["app_menu::OpenApplicationMenu", "Zed"], "alt-1": ["workspace::ActivatePane", 0], "alt-2": ["workspace::ActivatePane", 1], diff --git a/assets/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json index 3df1243feda88680a4ce03cd0b25ab9ea9a36edd..59a182a968a849edb3359927e7647f611bcd44da 100644 --- a/assets/keymaps/linux/jetbrains.json +++ b/assets/keymaps/linux/jetbrains.json @@ -125,7 +125,7 @@ { "context": "Workspace || Editor", "bindings": { - "alt-f12": "terminal_panel::ToggleFocus", + "alt-f12": "terminal_panel::Toggle", "ctrl-shift-k": "git::Push" } }, diff --git a/assets/keymaps/macos/jetbrains.json b/assets/keymaps/macos/jetbrains.json index 66962811f48a429f2f5d036241c64d6549f60334..2c757c3a30a08eb55e8344945ab66baf91ce0c6b 100644 --- a/assets/keymaps/macos/jetbrains.json +++ b/assets/keymaps/macos/jetbrains.json @@ -127,7 +127,7 @@ { "context": "Workspace || Editor", "bindings": { - "alt-f12": "terminal_panel::ToggleFocus", + "alt-f12": "terminal_panel::Toggle", "cmd-shift-k": "git::Push" } }, diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 2ba7f617bf407299b2b0e670f66432ce053718be..44d64c5fe3351d4c3e2a9342bfaf818445d78736 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -49,6 +49,8 @@ const TERMINAL_PANEL_KEY: &str = "TerminalPanel"; actions!( terminal_panel, [ + /// Toggles the terminal panel. + Toggle, /// Toggles focus on the terminal panel. ToggleFocus ] @@ -64,6 +66,13 @@ pub fn init(cx: &mut App) { workspace.toggle_panel_focus::(window, cx); } }); + workspace.register_action(|workspace, _: &Toggle, window, cx| { + if is_enabled_in_workspace(workspace, cx) { + if !workspace.toggle_panel_focus::(window, cx) { + workspace.close_panel::(window, cx); + } + } + }); }, ) .detach(); diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 29fe6aae0252bcc1ca5767f71b7c668ecae1b9a8..eda483988b4e8a01affa9c85d0cad7657def61eb 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1265,8 +1265,8 @@ fn generate_commands(_: &App) -> Vec { VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"), VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"), VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"), - VimCommand::str(("te", "rm"), "terminal_panel::ToggleFocus"), - VimCommand::str(("T", "erm"), "terminal_panel::ToggleFocus"), + VimCommand::str(("te", "rm"), "terminal_panel::Toggle"), + VimCommand::str(("T", "erm"), "terminal_panel::Toggle"), VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"), VimCommand::str(("Ch", "at"), "chat_panel::ToggleFocus"), VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0bfcaaf593eca73baa2a6a57def5af17b6ee93b3..6b4e7c1731b23e2e35086431d4d83bda4958d33f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3093,6 +3093,16 @@ impl Workspace { } } + pub fn close_panel(&self, window: &mut Window, cx: &mut Context) { + for dock in self.all_docks().iter() { + dock.update(cx, |dock, cx| { + if dock.panel::().is_some() { + dock.set_open(false, window, cx) + } + }) + } + } + pub fn panel(&self, cx: &App) -> Option> { self.all_docks() .iter() From 1c5c8552f2d00d442a0975a76d3231ab94004ea4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 5 Sep 2025 12:03:26 -0700 Subject: [PATCH 615/744] Show actual error in InvalidBufferView (#37657) Release Notes: - Update error view to show the error --- crates/workspace/src/invalid_buffer_view.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/workspace/src/invalid_buffer_view.rs b/crates/workspace/src/invalid_buffer_view.rs index b8c0db29d3ab95497fc5e850b0738b762f42b28b..05f409653b69e76654fa11d70b57d61fd6c0b73b 100644 --- a/crates/workspace/src/invalid_buffer_view.rs +++ b/crates/workspace/src/invalid_buffer_view.rs @@ -3,7 +3,8 @@ use std::{path::Path, sync::Arc}; use gpui::{EventEmitter, FocusHandle, Focusable}; use ui::{ App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement, - KeyBinding, ParentElement, Render, SharedString, Styled as _, Window, h_flex, v_flex, + KeyBinding, Label, LabelCommon, LabelSize, ParentElement, Render, SharedString, Styled as _, + Window, h_flex, v_flex, }; use zed_actions::workspace::OpenWithSystem; @@ -30,7 +31,7 @@ impl InvalidBufferView { Self { is_local, abs_path: Arc::from(abs_path), - error: format!("{e}").into(), + error: format!("{}", e.root_cause()).into(), focus_handle: cx.focus_handle(), } } @@ -88,7 +89,12 @@ impl Render for InvalidBufferView { v_flex() .justify_center() .gap_2() - .child(h_flex().justify_center().child("Unsupported file type")) + .child(h_flex().justify_center().child("Could not open file")) + .child( + h_flex() + .justify_center() + .child(Label::new(self.error.clone()).size(LabelSize::Small)), + ) .when(self.is_local, |contents| { contents.child( h_flex().justify_center().child( From 45fa03410796db8ac488b781b915e4c80a588f1a Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 5 Sep 2025 21:50:51 +0200 Subject: [PATCH 616/744] Restore notification panel settings (#37661) Follow-up to https://github.com/zed-industries/zed/pull/37489 Notification panel settings were always missing the content, hence this PR adds it. After #37489, the use of the same content twice broke things, which currently makes the notification panel non-configurable on Nightly. This PR fixes this. There once was an issue about the documentation for the panel being wrong as well. However, I was just unable to find that sadly. Release Notes: - N/A --- crates/collab_ui/src/panel_settings.rs | 30 ++++++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index bae118d819c2e38e7b77e5aa841c084e4c45d6e8..81d441167c06ef75c7e251dffefc55ff099a48e8 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/crates/collab_ui/src/panel_settings.rs @@ -44,8 +44,24 @@ pub struct ChatPanelSettingsContent { pub default_width: Option, } -#[derive(Deserialize, Debug, SettingsKey)] -#[settings_key(key = "notification_panel")] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] +#[settings_key(key = "collaboration_panel")] +pub struct PanelSettingsContent { + /// Whether to show the panel button in the status bar. + /// + /// Default: true + pub button: Option, + /// Where to dock the panel. + /// + /// Default: left + pub dock: Option, + /// Default width of the panel in pixels. + /// + /// Default: 240 + pub default_width: Option, +} + +#[derive(Deserialize, Debug)] pub struct NotificationPanelSettings { pub button: bool, pub dock: DockPosition, @@ -53,19 +69,19 @@ pub struct NotificationPanelSettings { } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] -#[settings_key(key = "collaboration_panel")] -pub struct PanelSettingsContent { +#[settings_key(key = "notification_panel")] +pub struct NotificationPanelSettingsContent { /// Whether to show the panel button in the status bar. /// /// Default: true pub button: Option, /// Where to dock the panel. /// - /// Default: left + /// Default: right pub dock: Option, /// Default width of the panel in pixels. /// - /// Default: 240 + /// Default: 300 pub default_width: Option, } @@ -106,7 +122,7 @@ impl Settings for ChatPanelSettings { } impl Settings for NotificationPanelSettings { - type FileContent = PanelSettingsContent; + type FileContent = NotificationPanelSettingsContent; fn load( sources: SettingsSources, From c45177e2963c285a19db2cc2424df53041d48640 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Sat, 6 Sep 2025 02:05:42 +0530 Subject: [PATCH 617/744] editor: Fix fold placeholder hover width smaller than marker (#37663) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: Screenshot 2025-09-06 at 1 21 39 AM The fold marker we use, `⋯`, isn’t rendered at the same size as the editor’s font. Notice how the fold marker appears larger than the same character typed directly in the editor buffer. image When we shape the line, we use the editor’s font size, and it ends up determining the element’s width. To fix this, we should treat the ellipsis as a UI element rather than a buffer character, since current visual size looks good to me. Screenshot 2025-09-06 at 1 29 28 AM Release Notes: - Fixed an issue where the fold placeholder’s hover area was smaller than the marker. --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fd2299f37dfc91c4a1d287c549269a7a77fc07e7..2374c8d6875f05608aa800de660fb3602ed35988 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1794,7 +1794,7 @@ impl Editor { let font_size = style.font_size.to_pixels(window.rem_size()); let editor = cx.entity().downgrade(); let fold_placeholder = FoldPlaceholder { - constrain_width: true, + constrain_width: false, render: Arc::new(move |fold_id, fold_range, cx| { let editor = editor.clone(); div() From ea363466aa8e5ca9553820c3bde2746c56dfc6ea Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:03:42 -0400 Subject: [PATCH 618/744] Fix attach modal showing local processes in SSH sessions (#37608) Closes #37520 This change makes the attach modal load processes from the remote server when connecting via SSH, rather than showing local processes from the client machine. This works by using the new GetProcessesRequest RPC message to allow downstream clients to get the correct processes to display. It also only works with downstream ssh clients because the message handler is only registered on headless projects. Release Notes: - debugger: Fix bug where SSH attach modal showed local processes instead of processes from the server --- crates/debugger_ui/src/attach_modal.rs | 92 +++++++++++++++----- crates/debugger_ui/src/new_process_modal.rs | 9 +- crates/proto/proto/debugger.proto | 14 +++ crates/proto/proto/zed.proto | 5 +- crates/proto/src/proto.rs | 4 + crates/remote_server/src/headless_project.rs | 30 +++++++ 6 files changed, 130 insertions(+), 24 deletions(-) diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index 662a98c82075cd6e936988959c855eadb5138092..3e3bc3ec27c3d1dbf0bacd445b883a50370d5b6f 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -1,8 +1,10 @@ use dap::{DapRegistry, DebugRequest}; use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render}; +use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task}; use gpui::{Subscription, WeakEntity}; use picker::{Picker, PickerDelegate}; +use project::Project; +use rpc::proto; use task::ZedDebugConfig; use util::debug_panic; @@ -56,29 +58,28 @@ impl AttachModal { pub fn new( definition: ZedDebugConfig, workspace: WeakEntity, + project: Entity, modal: bool, window: &mut Window, cx: &mut Context, ) -> Self { - let mut processes: Box<[_]> = System::new_all() - .processes() - .values() - .map(|process| { - let name = process.name().to_string_lossy().into_owned(); - Candidate { - name: name.into(), - pid: process.pid().as_u32(), - command: process - .cmd() - .iter() - .map(|s| s.to_string_lossy().to_string()) - .collect::>(), - } - }) - .collect(); - processes.sort_by_key(|k| k.name.clone()); - let processes = processes.into_iter().collect(); - Self::with_processes(workspace, definition, processes, modal, window, cx) + let processes_task = get_processes_for_project(&project, cx); + + let modal = Self::with_processes(workspace, definition, Arc::new([]), modal, window, cx); + + cx.spawn_in(window, async move |this, cx| { + let processes = processes_task.await; + this.update_in(cx, |modal, window, cx| { + modal.picker.update(cx, |picker, cx| { + picker.delegate.candidates = processes; + picker.refresh(window, cx); + }); + })?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + modal } pub(super) fn with_processes( @@ -332,6 +333,57 @@ impl PickerDelegate for AttachModalDelegate { } } +fn get_processes_for_project(project: &Entity, cx: &mut App) -> Task> { + let project = project.read(cx); + + if let Some(remote_client) = project.remote_client() { + let proto_client = remote_client.read(cx).proto_client(); + cx.spawn(async move |_cx| { + let response = proto_client + .request(proto::GetProcesses { + project_id: proto::REMOTE_SERVER_PROJECT_ID, + }) + .await + .unwrap_or_else(|_| proto::GetProcessesResponse { + processes: Vec::new(), + }); + + let mut processes: Vec = response + .processes + .into_iter() + .map(|p| Candidate { + pid: p.pid, + name: p.name.into(), + command: p.command, + }) + .collect(); + + processes.sort_by_key(|k| k.name.clone()); + Arc::from(processes.into_boxed_slice()) + }) + } else { + let mut processes: Box<[_]> = System::new_all() + .processes() + .values() + .map(|process| { + let name = process.name().to_string_lossy().into_owned(); + Candidate { + name: name.into(), + pid: process.pid().as_u32(), + command: process + .cmd() + .iter() + .map(|s| s.to_string_lossy().to_string()) + .collect::>(), + } + }) + .collect(); + processes.sort_by_key(|k| k.name.clone()); + let processes = processes.into_iter().collect(); + Task::ready(processes) + } +} + #[cfg(any(test, feature = "test-support"))] pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context) -> Vec { modal.picker.read_with(cx, |picker, _| { diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index 68770bc8b15fbf95824de167dbc8d7fada2b5075..ee6289187ba990d5bbaa040631a1c32619857e53 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -20,7 +20,7 @@ use gpui::{ }; use itertools::Itertools as _; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; -use project::{DebugScenarioContext, TaskContexts, TaskSourceKind, task_store::TaskStore}; +use project::{DebugScenarioContext, Project, TaskContexts, TaskSourceKind, task_store::TaskStore}; use settings::Settings; use task::{DebugScenario, RevealTarget, ZedDebugConfig}; use theme::ThemeSettings; @@ -88,8 +88,10 @@ impl NewProcessModal { })?; workspace.update_in(cx, |workspace, window, cx| { let workspace_handle = workspace.weak_handle(); + let project = workspace.project().clone(); workspace.toggle_modal(window, cx, |window, cx| { - let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx); + let attach_mode = + AttachMode::new(None, workspace_handle.clone(), project, window, cx); let debug_picker = cx.new(|cx| { let delegate = @@ -940,6 +942,7 @@ impl AttachMode { pub(super) fn new( debugger: Option, workspace: WeakEntity, + project: Entity, window: &mut Window, cx: &mut Context, ) -> Entity { @@ -950,7 +953,7 @@ impl AttachMode { stop_on_entry: Some(false), }; let attach_picker = cx.new(|cx| { - let modal = AttachModal::new(definition.clone(), workspace, false, window, cx); + let modal = AttachModal::new(definition.clone(), workspace, project, false, window, cx); window.focus(&modal.focus_handle(cx)); modal diff --git a/crates/proto/proto/debugger.proto b/crates/proto/proto/debugger.proto index c6f9c9f1342336c36ab8dfd0ec70a24ff6564476..e3cb5ebbce0ceb87a7197f19a133bbb92a572085 100644 --- a/crates/proto/proto/debugger.proto +++ b/crates/proto/proto/debugger.proto @@ -546,3 +546,17 @@ message LogToDebugConsole { uint64 session_id = 2; string message = 3; } + +message GetProcesses { + uint64 project_id = 1; +} + +message GetProcessesResponse { + repeated ProcessInfo processes = 1; +} + +message ProcessInfo { + uint32 pid = 1; + string name = 2; + repeated string command = 3; +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 4133b4b5eea6f14e2c9359f7318f192a8566d809..3763671a7a1f29949194d61c70866f96ca6ad972 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -399,7 +399,10 @@ message Envelope { LspQueryResponse lsp_query_response = 366; ToggleLspLogs toggle_lsp_logs = 367; - UpdateUserSettings update_user_settings = 368; // current max + UpdateUserSettings update_user_settings = 368; + + GetProcesses get_processes = 369; + GetProcessesResponse get_processes_response = 370; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 8f4e836b20ae5bae43617e10391f75c3a069a82f..3c98ae62e7a4b1489c071a0ac673d23b394c28d5 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -102,6 +102,8 @@ messages!( (GetPathMetadata, Background), (GetPathMetadataResponse, Background), (GetPermalinkToLine, Foreground), + (GetProcesses, Background), + (GetProcessesResponse, Background), (GetPermalinkToLineResponse, Foreground), (GetProjectSymbols, Background), (GetProjectSymbolsResponse, Background), @@ -485,6 +487,7 @@ request_messages!( (GetDefaultBranch, GetDefaultBranchResponse), (GitClone, GitCloneResponse), (ToggleLspLogs, Ack), + (GetProcesses, GetProcessesResponse), ); lsp_messages!( @@ -610,6 +613,7 @@ entity_messages!( ActivateToolchain, ActiveToolchain, GetPathMetadata, + GetProcesses, CancelLanguageServerWork, RegisterBufferWithLanguageServers, GitShow, diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index f55826631b46b4f9eaaa17d8a9f4b0603a07fcc3..7fb5ac8498c67863783b1944c912f0ad9767fed5 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -32,6 +32,7 @@ use std::{ path::{Path, PathBuf}, sync::{Arc, atomic::AtomicUsize}, }; +use sysinfo::System; use util::ResultExt; use worktree::Worktree; @@ -230,6 +231,7 @@ impl HeadlessProject { session.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata); session.add_request_handler(cx.weak_entity(), Self::handle_shutdown_remote_server); session.add_request_handler(cx.weak_entity(), Self::handle_ping); + session.add_request_handler(cx.weak_entity(), Self::handle_get_processes); session.add_entity_request_handler(Self::handle_add_worktree); session.add_request_handler(cx.weak_entity(), Self::handle_remove_worktree); @@ -719,6 +721,34 @@ impl HeadlessProject { log::debug!("Received ping from client"); Ok(proto::Ack {}) } + + async fn handle_get_processes( + _this: Entity, + _envelope: TypedEnvelope, + _cx: AsyncApp, + ) -> Result { + let mut processes = Vec::new(); + let system = System::new_all(); + + for (_pid, process) in system.processes() { + let name = process.name().to_string_lossy().into_owned(); + let command = process + .cmd() + .iter() + .map(|s| s.to_string_lossy().to_string()) + .collect::>(); + + processes.push(proto::ProcessInfo { + pid: process.pid().as_u32(), + name, + command, + }); + } + + processes.sort_by_key(|p| p.name.clone()); + + Ok(proto::GetProcessesResponse { processes }) + } } fn prompt_to_proto( From 236b3e546e484647c491323d733c299af518c7a0 Mon Sep 17 00:00:00 2001 From: morgankrey Date: Fri, 5 Sep 2025 14:34:13 -0700 Subject: [PATCH 619/744] Update Link (#37671) Documentation fix Release Notes: - N/A --- docs/src/configuring-languages.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/configuring-languages.md b/docs/src/configuring-languages.md index 52b7a3f7b82aeb3f2f19dcd63ef64c34251f1cd8..9da44fb53dba0ea044ce01ddb2d9ef3d90133adb 100644 --- a/docs/src/configuring-languages.md +++ b/docs/src/configuring-languages.md @@ -251,7 +251,7 @@ You can toggle language server support globally or per-language: } ``` -This disables the language server for Markdown files, which can be useful for performance in large documentation projects. You can configure this globally in your `~/.zed/settings.json` or inside a `.zed/settings.json` in your project directory. +This disables the language server for Markdown files, which can be useful for performance in large documentation projects. You can configure this globally in your `~/.config/zed/settings.json` or inside a `.zed/settings.json` in your project directory. ## Formatting and Linting From 64b6e8ba0fd47828b6cf2917f520ca9962c15df5 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 5 Sep 2025 23:35:28 +0200 Subject: [PATCH 620/744] debugger: Fix allow showing more than 1 compact session item (#37036) Closes #36978 This PR fixes an issue that we would only show the first `root -> child` session in compact mode, but the session that came after it, we would only show the child session label instead of also adding the parent label due to compact mode. ## Before Screenshot 2025-08-27 at 22 18 39 ## After Screenshot 2025-08-27 at 21 57 16 With 3 parent + child sessions and one parent session only. Screenshot 2025-08-27 at 22 22 13 cc @cole-miller I know we hacked on this some while ago, so figured you might be the best guy to ask for a review. Release Notes: - Debugger: Fix to allow showing more than 1 compact session item --------- Co-authored-by: Anthony --- crates/debugger_ui/src/dropdown_menus.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/crates/debugger_ui/src/dropdown_menus.rs b/crates/debugger_ui/src/dropdown_menus.rs index c611d5d44f36b4eafb578a400da615bbd96b4cd2..376a4a41ce7b03cd07f578d85f641a6ddfc4ebe8 100644 --- a/crates/debugger_ui/src/dropdown_menus.rs +++ b/crates/debugger_ui/src/dropdown_menus.rs @@ -113,23 +113,6 @@ impl DebugPanel { } }; session_entries.push(root_entry); - - session_entries.extend( - sessions_with_children - .by_ref() - .take_while(|(session, _)| { - session - .read(cx) - .session(cx) - .read(cx) - .parent_id(cx) - .is_some() - }) - .map(|(session, _)| SessionListEntry { - leaf: session.clone(), - ancestors: vec![], - }), - ); } let weak = cx.weak_entity(); From 59bdbf5a5dfc465f6327da4048d72f6536fdd841 Mon Sep 17 00:00:00 2001 From: Nia Date: Sat, 6 Sep 2025 00:27:14 +0200 Subject: [PATCH 621/744] Various fixups to unsafe code (#37651) A collection of fixups of possibly-unsound code and removing some small useless writes. Release Notes: - N/A --- crates/fs/src/fs.rs | 15 ++- crates/gpui/src/arena.rs | 27 +++--- crates/gpui/src/util.rs | 6 +- crates/sqlez/src/connection.rs | 170 +++++++++++++++++---------------- crates/sqlez/src/statement.rs | 78 ++++++++------- crates/util/src/util.rs | 4 +- crates/zlog/src/filter.rs | 14 +-- crates/zlog/src/sink.rs | 38 ++++---- 8 files changed, 181 insertions(+), 171 deletions(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index a5cf9b88254deff5b9a07402207f19875827d7f0..98c8dc9054984c49732bec57a9604a14ceb5ee72 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -20,6 +20,9 @@ use std::os::fd::{AsFd, AsRawFd}; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; +#[cfg(any(target_os = "macos", target_os = "freebsd"))] +use std::mem::MaybeUninit; + use async_tar::Archive; use futures::{AsyncRead, Stream, StreamExt, future::BoxFuture}; use git::repository::{GitRepository, RealGitRepository}; @@ -261,14 +264,15 @@ impl FileHandle for std::fs::File { }; let fd = self.as_fd(); - let mut path_buf: [libc::c_char; libc::PATH_MAX as usize] = [0; libc::PATH_MAX as usize]; + let mut path_buf = MaybeUninit::<[u8; libc::PATH_MAX as usize]>::uninit(); let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETPATH, path_buf.as_mut_ptr()) }; if result == -1 { anyhow::bail!("fcntl returned -1".to_string()); } - let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr()) }; + // SAFETY: `fcntl` will initialize the path buffer. + let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr().cast()) }; let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes())); Ok(path) } @@ -296,15 +300,16 @@ impl FileHandle for std::fs::File { }; let fd = self.as_fd(); - let mut kif: libc::kinfo_file = unsafe { std::mem::zeroed() }; + let mut kif = MaybeUninit::::uninit(); kif.kf_structsize = libc::KINFO_FILE_SIZE; - let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_KINFO, &mut kif) }; + let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_KINFO, kif.as_mut_ptr()) }; if result == -1 { anyhow::bail!("fcntl returned -1".to_string()); } - let c_str = unsafe { CStr::from_ptr(kif.kf_path.as_ptr()) }; + // SAFETY: `fcntl` will initialize the kif. + let c_str = unsafe { CStr::from_ptr(kif.assume_init().kf_path.as_ptr()) }; let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes())); Ok(path) } diff --git a/crates/gpui/src/arena.rs b/crates/gpui/src/arena.rs index 0983bd23454c9a3a921ed721ecd32561387f9049..a0d0c23987472de46d5b23129adb5a4ec8ee00cb 100644 --- a/crates/gpui/src/arena.rs +++ b/crates/gpui/src/arena.rs @@ -1,8 +1,9 @@ use std::{ alloc::{self, handle_alloc_error}, cell::Cell, + num::NonZeroUsize, ops::{Deref, DerefMut}, - ptr, + ptr::{self, NonNull}, rc::Rc, }; @@ -30,23 +31,23 @@ impl Drop for Chunk { fn drop(&mut self) { unsafe { let chunk_size = self.end.offset_from_unsigned(self.start); - // this never fails as it succeeded during allocation - let layout = alloc::Layout::from_size_align(chunk_size, 1).unwrap(); + // SAFETY: This succeeded during allocation. + let layout = alloc::Layout::from_size_align_unchecked(chunk_size, 1); alloc::dealloc(self.start, layout); } } } impl Chunk { - fn new(chunk_size: usize) -> Self { + fn new(chunk_size: NonZeroUsize) -> Self { unsafe { // this only fails if chunk_size is unreasonably huge - let layout = alloc::Layout::from_size_align(chunk_size, 1).unwrap(); + let layout = alloc::Layout::from_size_align(chunk_size.get(), 1).unwrap(); let start = alloc::alloc(layout); if start.is_null() { handle_alloc_error(layout); } - let end = start.add(chunk_size); + let end = start.add(chunk_size.get()); Self { start, end, @@ -55,14 +56,14 @@ impl Chunk { } } - fn allocate(&mut self, layout: alloc::Layout) -> Option<*mut u8> { + fn allocate(&mut self, layout: alloc::Layout) -> Option> { unsafe { let aligned = self.offset.add(self.offset.align_offset(layout.align())); let next = aligned.add(layout.size()); if next <= self.end { self.offset = next; - Some(aligned) + NonNull::new(aligned) } else { None } @@ -79,7 +80,7 @@ pub struct Arena { elements: Vec, valid: Rc>, current_chunk_index: usize, - chunk_size: usize, + chunk_size: NonZeroUsize, } impl Drop for Arena { @@ -90,7 +91,7 @@ impl Drop for Arena { impl Arena { pub fn new(chunk_size: usize) -> Self { - assert!(chunk_size > 0); + let chunk_size = NonZeroUsize::try_from(chunk_size).unwrap(); Self { chunks: vec![Chunk::new(chunk_size)], elements: Vec::new(), @@ -101,7 +102,7 @@ impl Arena { } pub fn capacity(&self) -> usize { - self.chunks.len() * self.chunk_size + self.chunks.len() * self.chunk_size.get() } pub fn clear(&mut self) { @@ -136,7 +137,7 @@ impl Arena { let layout = alloc::Layout::new::(); let mut current_chunk = &mut self.chunks[self.current_chunk_index]; let ptr = if let Some(ptr) = current_chunk.allocate(layout) { - ptr + ptr.as_ptr() } else { self.current_chunk_index += 1; if self.current_chunk_index >= self.chunks.len() { @@ -149,7 +150,7 @@ impl Arena { } current_chunk = &mut self.chunks[self.current_chunk_index]; if let Some(ptr) = current_chunk.allocate(layout) { - ptr + ptr.as_ptr() } else { panic!( "Arena chunk_size of {} is too small to allocate {} bytes", diff --git a/crates/gpui/src/util.rs b/crates/gpui/src/util.rs index 3d7fa06e6ca013ae38b1c63d1bfd624d46cdf4f1..3704784a954f14b8317202e227ffb1b17092d70d 100644 --- a/crates/gpui/src/util.rs +++ b/crates/gpui/src/util.rs @@ -99,9 +99,9 @@ impl Future for WithTimeout { fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> task::Poll { // SAFETY: the fields of Timeout are private and we never move the future ourselves // And its already pinned since we are being polled (all futures need to be pinned to be polled) - let this = unsafe { self.get_unchecked_mut() }; - let future = unsafe { Pin::new_unchecked(&mut this.future) }; - let timer = unsafe { Pin::new_unchecked(&mut this.timer) }; + let this = unsafe { &raw mut *self.get_unchecked_mut() }; + let future = unsafe { Pin::new_unchecked(&mut (*this).future) }; + let timer = unsafe { Pin::new_unchecked(&mut (*this).timer) }; if let task::Poll::Ready(output) = future.poll(cx) { task::Poll::Ready(Ok(output)) diff --git a/crates/sqlez/src/connection.rs b/crates/sqlez/src/connection.rs index 228bd4c6a2df31f41dc1988596fc87323063d78c..53f0d4e2614f340cc0563d5cd9374bdc3626d9bb 100644 --- a/crates/sqlez/src/connection.rs +++ b/crates/sqlez/src/connection.rs @@ -92,91 +92,97 @@ impl Connection { let mut remaining_sql = sql.as_c_str(); let sql_start = remaining_sql.as_ptr(); - unsafe { - let mut alter_table = None; - while { - let remaining_sql_str = remaining_sql.to_str().unwrap().trim(); - let any_remaining_sql = remaining_sql_str != ";" && !remaining_sql_str.is_empty(); - if any_remaining_sql { - alter_table = parse_alter_table(remaining_sql_str); + let mut alter_table = None; + while { + let remaining_sql_str = remaining_sql.to_str().unwrap().trim(); + let any_remaining_sql = remaining_sql_str != ";" && !remaining_sql_str.is_empty(); + if any_remaining_sql { + alter_table = parse_alter_table(remaining_sql_str); + } + any_remaining_sql + } { + let mut raw_statement = ptr::null_mut::(); + let mut remaining_sql_ptr = ptr::null(); + + let (res, offset, message, _conn) = if let Some((table_to_alter, column)) = alter_table + { + // ALTER TABLE is a weird statement. When preparing the statement the table's + // existence is checked *before* syntax checking any other part of the statement. + // Therefore, we need to make sure that the table has been created before calling + // prepare. As we don't want to trash whatever database this is connected to, we + // create a new in-memory DB to test. + + let temp_connection = Connection::open_memory(None); + //This should always succeed, if it doesn't then you really should know about it + temp_connection + .exec(&format!("CREATE TABLE {table_to_alter}({column})")) + .unwrap()() + .unwrap(); + + unsafe { + sqlite3_prepare_v2( + temp_connection.sqlite3, + remaining_sql.as_ptr(), + -1, + &mut raw_statement, + &mut remaining_sql_ptr, + ) + }; + + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + let offset = unsafe { sqlite3_error_offset(temp_connection.sqlite3) }; + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + let offset = 0; + + unsafe { + ( + sqlite3_errcode(temp_connection.sqlite3), + offset, + sqlite3_errmsg(temp_connection.sqlite3), + Some(temp_connection), + ) } - any_remaining_sql - } { - let mut raw_statement = ptr::null_mut::(); - let mut remaining_sql_ptr = ptr::null(); - - let (res, offset, message, _conn) = - if let Some((table_to_alter, column)) = alter_table { - // ALTER TABLE is a weird statement. When preparing the statement the table's - // existence is checked *before* syntax checking any other part of the statement. - // Therefore, we need to make sure that the table has been created before calling - // prepare. As we don't want to trash whatever database this is connected to, we - // create a new in-memory DB to test. - - let temp_connection = Connection::open_memory(None); - //This should always succeed, if it doesn't then you really should know about it - temp_connection - .exec(&format!("CREATE TABLE {table_to_alter}({column})")) - .unwrap()() - .unwrap(); - - sqlite3_prepare_v2( - temp_connection.sqlite3, - remaining_sql.as_ptr(), - -1, - &mut raw_statement, - &mut remaining_sql_ptr, - ); - - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - let offset = sqlite3_error_offset(temp_connection.sqlite3); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - let offset = 0; - - ( - sqlite3_errcode(temp_connection.sqlite3), - offset, - sqlite3_errmsg(temp_connection.sqlite3), - Some(temp_connection), - ) - } else { - sqlite3_prepare_v2( - self.sqlite3, - remaining_sql.as_ptr(), - -1, - &mut raw_statement, - &mut remaining_sql_ptr, - ); - - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - let offset = sqlite3_error_offset(self.sqlite3); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - let offset = 0; - - ( - sqlite3_errcode(self.sqlite3), - offset, - sqlite3_errmsg(self.sqlite3), - None, - ) - }; - - sqlite3_finalize(raw_statement); - - if res == 1 && offset >= 0 { - let sub_statement_correction = - remaining_sql.as_ptr() as usize - sql_start as usize; - let err_msg = - String::from_utf8_lossy(CStr::from_ptr(message as *const _).to_bytes()) - .into_owned(); - - return Some((err_msg, offset as usize + sub_statement_correction)); + } else { + unsafe { + sqlite3_prepare_v2( + self.sqlite3, + remaining_sql.as_ptr(), + -1, + &mut raw_statement, + &mut remaining_sql_ptr, + ) + }; + + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + let offset = unsafe { sqlite3_error_offset(self.sqlite3) }; + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + let offset = 0; + + unsafe { + ( + sqlite3_errcode(self.sqlite3), + offset, + sqlite3_errmsg(self.sqlite3), + None, + ) } - remaining_sql = CStr::from_ptr(remaining_sql_ptr); - alter_table = None; + }; + + unsafe { sqlite3_finalize(raw_statement) }; + + if res == 1 && offset >= 0 { + let sub_statement_correction = remaining_sql.as_ptr() as usize - sql_start as usize; + let err_msg = String::from_utf8_lossy(unsafe { + CStr::from_ptr(message as *const _).to_bytes() + }) + .into_owned(); + + return Some((err_msg, offset as usize + sub_statement_correction)); } + remaining_sql = unsafe { CStr::from_ptr(remaining_sql_ptr) }; + alter_table = None; } None } diff --git a/crates/sqlez/src/statement.rs b/crates/sqlez/src/statement.rs index eb7553f862b0a291bf08345606ff22317d3eec60..d08e58a6f93344d4bb52c35c8c76406724a230b4 100644 --- a/crates/sqlez/src/statement.rs +++ b/crates/sqlez/src/statement.rs @@ -44,41 +44,41 @@ impl<'a> Statement<'a> { connection, phantom: PhantomData, }; - unsafe { - let sql = CString::new(query.as_ref()).context("Error creating cstr")?; - let mut remaining_sql = sql.as_c_str(); - while { - let remaining_sql_str = remaining_sql - .to_str() - .context("Parsing remaining sql")? - .trim(); - remaining_sql_str != ";" && !remaining_sql_str.is_empty() - } { - let mut raw_statement = ptr::null_mut::(); - let mut remaining_sql_ptr = ptr::null(); + let sql = CString::new(query.as_ref()).context("Error creating cstr")?; + let mut remaining_sql = sql.as_c_str(); + while { + let remaining_sql_str = remaining_sql + .to_str() + .context("Parsing remaining sql")? + .trim(); + remaining_sql_str != ";" && !remaining_sql_str.is_empty() + } { + let mut raw_statement = ptr::null_mut::(); + let mut remaining_sql_ptr = ptr::null(); + unsafe { sqlite3_prepare_v2( connection.sqlite3, remaining_sql.as_ptr(), -1, &mut raw_statement, &mut remaining_sql_ptr, - ); + ) + }; - connection.last_error().with_context(|| { - format!("Prepare call failed for query:\n{}", query.as_ref()) - })?; + connection + .last_error() + .with_context(|| format!("Prepare call failed for query:\n{}", query.as_ref()))?; - remaining_sql = CStr::from_ptr(remaining_sql_ptr); - statement.raw_statements.push(raw_statement); + remaining_sql = unsafe { CStr::from_ptr(remaining_sql_ptr) }; + statement.raw_statements.push(raw_statement); - if !connection.can_write() && sqlite3_stmt_readonly(raw_statement) == 0 { - let sql = CStr::from_ptr(sqlite3_sql(raw_statement)); + if !connection.can_write() && unsafe { sqlite3_stmt_readonly(raw_statement) == 0 } { + let sql = unsafe { CStr::from_ptr(sqlite3_sql(raw_statement)) }; - bail!( - "Write statement prepared with connection that is not write capable. SQL:\n{} ", - sql.to_str()? - ) - } + bail!( + "Write statement prepared with connection that is not write capable. SQL:\n{} ", + sql.to_str()? + ) } } @@ -271,23 +271,21 @@ impl<'a> Statement<'a> { } fn step(&mut self) -> Result { - unsafe { - match sqlite3_step(self.current_statement()) { - SQLITE_ROW => Ok(StepResult::Row), - SQLITE_DONE => { - if self.current_statement >= self.raw_statements.len() - 1 { - Ok(StepResult::Done) - } else { - self.current_statement += 1; - self.step() - } - } - SQLITE_MISUSE => anyhow::bail!("Statement step returned SQLITE_MISUSE"), - _other_error => { - self.connection.last_error()?; - unreachable!("Step returned error code and last error failed to catch it"); + match unsafe { sqlite3_step(self.current_statement()) } { + SQLITE_ROW => Ok(StepResult::Row), + SQLITE_DONE => { + if self.current_statement >= self.raw_statements.len() - 1 { + Ok(StepResult::Done) + } else { + self.current_statement += 1; + self.step() } } + SQLITE_MISUSE => anyhow::bail!("Statement step returned SQLITE_MISUSE"), + _other_error => { + self.connection.last_error()?; + unreachable!("Step returned error code and last error failed to catch it"); + } } } diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index db44e3945186842990f7ef8d7b2794b023324d56..90f5be1c92875ac0b9b2d3e7352ae858371b3686 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -256,6 +256,9 @@ fn load_shell_from_passwd() -> Result<()> { &mut result, ) }; + anyhow::ensure!(!result.is_null(), "passwd entry for uid {} not found", uid); + + // SAFETY: If `getpwuid_r` doesn't error, we have the entry here. let entry = unsafe { pwd.assume_init() }; anyhow::ensure!( @@ -264,7 +267,6 @@ fn load_shell_from_passwd() -> Result<()> { uid, status ); - anyhow::ensure!(!result.is_null(), "passwd entry for uid {} not found", uid); anyhow::ensure!( entry.pw_uid == uid, "passwd entry has different uid ({}) than getuid ({}) returned", diff --git a/crates/zlog/src/filter.rs b/crates/zlog/src/filter.rs index ee3c2410798b286795b9fd78b89502a2a7894987..31a58894774e6c0d08ea22b585350eb26ff09907 100644 --- a/crates/zlog/src/filter.rs +++ b/crates/zlog/src/filter.rs @@ -22,7 +22,7 @@ pub const LEVEL_ENABLED_MAX_DEFAULT: log::LevelFilter = log::LevelFilter::Info; /// crate that the max level is everything, so that we can dynamically enable /// logs that are more verbose than this level without the `log` crate throwing /// them away before we see them -static mut LEVEL_ENABLED_MAX_STATIC: log::LevelFilter = LEVEL_ENABLED_MAX_DEFAULT; +static LEVEL_ENABLED_MAX_STATIC: AtomicU8 = AtomicU8::new(LEVEL_ENABLED_MAX_DEFAULT as u8); /// A cache of the true maximum log level that _could_ be printed. This is based /// on the maximally verbose level that is configured by the user, and is used @@ -46,7 +46,7 @@ const DEFAULT_FILTERS: &[(&str, log::LevelFilter)] = &[ pub fn init_env_filter(filter: env_config::EnvFilter) { if let Some(level_max) = filter.level_global { - unsafe { LEVEL_ENABLED_MAX_STATIC = level_max } + LEVEL_ENABLED_MAX_STATIC.store(level_max as u8, Ordering::Release) } if ENV_FILTER.set(filter).is_err() { panic!("Environment filter cannot be initialized twice"); @@ -54,7 +54,7 @@ pub fn init_env_filter(filter: env_config::EnvFilter) { } pub fn is_possibly_enabled_level(level: log::Level) -> bool { - level as u8 <= LEVEL_ENABLED_MAX_CONFIG.load(Ordering::Relaxed) + level as u8 <= LEVEL_ENABLED_MAX_CONFIG.load(Ordering::Acquire) } pub fn is_scope_enabled(scope: &Scope, module_path: Option<&str>, level: log::Level) -> bool { @@ -66,7 +66,7 @@ pub fn is_scope_enabled(scope: &Scope, module_path: Option<&str>, level: log::Le // scope map return false; } - let is_enabled_by_default = level <= unsafe { LEVEL_ENABLED_MAX_STATIC }; + let is_enabled_by_default = level as u8 <= LEVEL_ENABLED_MAX_STATIC.load(Ordering::Acquire); let global_scope_map = SCOPE_MAP.read().unwrap_or_else(|err| { SCOPE_MAP.clear_poison(); err.into_inner() @@ -92,13 +92,13 @@ pub fn is_scope_enabled(scope: &Scope, module_path: Option<&str>, level: log::Le pub fn refresh_from_settings(settings: &HashMap) { let env_config = ENV_FILTER.get(); let map_new = ScopeMap::new_from_settings_and_env(settings, env_config, DEFAULT_FILTERS); - let mut level_enabled_max = unsafe { LEVEL_ENABLED_MAX_STATIC }; + let mut level_enabled_max = LEVEL_ENABLED_MAX_STATIC.load(Ordering::Acquire); for entry in &map_new.entries { if let Some(level) = entry.enabled { - level_enabled_max = level_enabled_max.max(level); + level_enabled_max = level_enabled_max.max(level as u8); } } - LEVEL_ENABLED_MAX_CONFIG.store(level_enabled_max as u8, Ordering::Release); + LEVEL_ENABLED_MAX_CONFIG.store(level_enabled_max, Ordering::Release); { let mut global_map = SCOPE_MAP.write().unwrap_or_else(|err| { diff --git a/crates/zlog/src/sink.rs b/crates/zlog/src/sink.rs index 3ac85d4bbfc8aaa5d8568cb14b50e04a94708f1c..afbdf37bf9c74860a3b56b706ffc6d64338fd275 100644 --- a/crates/zlog/src/sink.rs +++ b/crates/zlog/src/sink.rs @@ -4,7 +4,7 @@ use std::{ path::PathBuf, sync::{ Mutex, OnceLock, - atomic::{AtomicU64, Ordering}, + atomic::{AtomicBool, AtomicU64, Ordering}, }, }; @@ -19,17 +19,17 @@ const ANSI_GREEN: &str = "\x1b[32m"; const ANSI_BLUE: &str = "\x1b[34m"; const ANSI_MAGENTA: &str = "\x1b[35m"; -/// Whether stdout output is enabled. -static mut ENABLED_SINKS_STDOUT: bool = false; -/// Whether stderr output is enabled. -static mut ENABLED_SINKS_STDERR: bool = false; - /// Is Some(file) if file output is enabled. static ENABLED_SINKS_FILE: Mutex> = Mutex::new(None); static SINK_FILE_PATH: OnceLock<&'static PathBuf> = OnceLock::new(); static SINK_FILE_PATH_ROTATE: OnceLock<&'static PathBuf> = OnceLock::new(); + +// NB: Since this can be accessed in tests, we probably should stick to atomics here. +/// Whether stdout output is enabled. +static ENABLED_SINKS_STDOUT: AtomicBool = AtomicBool::new(false); +/// Whether stderr output is enabled. +static ENABLED_SINKS_STDERR: AtomicBool = AtomicBool::new(false); /// Atomic counter for the size of the log file in bytes. -// TODO: make non-atomic if writing single threaded static SINK_FILE_SIZE_BYTES: AtomicU64 = AtomicU64::new(0); /// Maximum size of the log file before it will be rotated, in bytes. const SINK_FILE_SIZE_BYTES_MAX: u64 = 1024 * 1024; // 1 MB @@ -42,15 +42,13 @@ pub struct Record<'a> { } pub fn init_output_stdout() { - unsafe { - ENABLED_SINKS_STDOUT = true; - } + // Use atomics here instead of just a `static mut`, since in the context + // of tests these accesses can be multi-threaded. + ENABLED_SINKS_STDOUT.store(true, Ordering::Release); } pub fn init_output_stderr() { - unsafe { - ENABLED_SINKS_STDERR = true; - } + ENABLED_SINKS_STDERR.store(true, Ordering::Release); } pub fn init_output_file( @@ -79,7 +77,7 @@ pub fn init_output_file( if size_bytes >= SINK_FILE_SIZE_BYTES_MAX { rotate_log_file(&mut file, Some(path), path_rotate, &SINK_FILE_SIZE_BYTES); } else { - SINK_FILE_SIZE_BYTES.store(size_bytes, Ordering::Relaxed); + SINK_FILE_SIZE_BYTES.store(size_bytes, Ordering::Release); } *enabled_sinks_file = Some(file); @@ -108,7 +106,7 @@ static LEVEL_ANSI_COLORS: [&str; 6] = [ // PERF: batching pub fn submit(record: Record) { - if unsafe { ENABLED_SINKS_STDOUT } { + if ENABLED_SINKS_STDOUT.load(Ordering::Acquire) { let mut stdout = std::io::stdout().lock(); _ = writeln!( &mut stdout, @@ -123,7 +121,7 @@ pub fn submit(record: Record) { }, record.message ); - } else if unsafe { ENABLED_SINKS_STDERR } { + } else if ENABLED_SINKS_STDERR.load(Ordering::Acquire) { let mut stdout = std::io::stderr().lock(); _ = writeln!( &mut stdout, @@ -173,7 +171,7 @@ pub fn submit(record: Record) { }, record.message ); - SINK_FILE_SIZE_BYTES.fetch_add(writer.written, Ordering::Relaxed) + writer.written + SINK_FILE_SIZE_BYTES.fetch_add(writer.written, Ordering::AcqRel) + writer.written }; if file_size_bytes > SINK_FILE_SIZE_BYTES_MAX { rotate_log_file( @@ -187,7 +185,7 @@ pub fn submit(record: Record) { } pub fn flush() { - if unsafe { ENABLED_SINKS_STDOUT } { + if ENABLED_SINKS_STDOUT.load(Ordering::Acquire) { _ = std::io::stdout().lock().flush(); } let mut file = ENABLED_SINKS_FILE.lock().unwrap_or_else(|handle| { @@ -265,7 +263,7 @@ fn rotate_log_file( // according to the documentation, it only fails if: // - the file is not writeable: should never happen, // - the size would cause an overflow (implementation specific): 0 should never cause an overflow - atomic_size.store(0, Ordering::Relaxed); + atomic_size.store(0, Ordering::Release); } #[cfg(test)] @@ -298,7 +296,7 @@ mod tests { std::fs::read_to_string(&rotation_log_file_path).unwrap(), contents, ); - assert_eq!(size.load(Ordering::Relaxed), 0); + assert_eq!(size.load(Ordering::Acquire), 0); } /// Regression test, ensuring that if log level values change we are made aware From 6a7b84eb87ab6764b2ee1152a714c4f00aced8f2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 6 Sep 2025 00:47:39 +0200 Subject: [PATCH 622/744] toolchains: Allow users to provide custom paths to toolchains (#37009) - **toolchains: Add new state to toolchain selector** - **Use toolchain term for Add Toolchain button** - **Hoist out a meta function for toolchain listers** Closes #27332 Release Notes: - python: Users can now specify a custom path to their virtual environment from within the picker. --------- Co-authored-by: Danilo Leal --- Cargo.lock | 5 + assets/keymaps/default-linux.json | 8 + assets/keymaps/default-macos.json | 8 + crates/file_finder/src/open_path_prompt.rs | 32 +- crates/language/src/language.rs | 1 + crates/language/src/toolchain.rs | 62 +- crates/languages/src/lib.rs | 2 +- crates/languages/src/python.rs | 100 ++- crates/project/src/lsp_store.rs | 4 +- crates/project/src/project.rs | 74 +- crates/project/src/project_tests.rs | 29 +- crates/project/src/toolchain_store.rs | 284 ++++++- crates/proto/proto/toolchain.proto | 13 + crates/proto/proto/zed.proto | 5 +- crates/proto/src/proto.rs | 4 + crates/repl/src/kernels/mod.rs | 89 +- crates/toolchain_selector/Cargo.toml | 5 + .../src/active_toolchain.rs | 34 +- .../src/toolchain_selector.rs | 802 +++++++++++++++++- crates/workspace/src/persistence.rs | 148 +++- crates/workspace/src/persistence/model.rs | 3 + crates/workspace/src/workspace.rs | 31 + 22 files changed, 1508 insertions(+), 235 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbdf0e848c356620f2a2cca800cf40ef850c3b13..295c3a83c52e3b355a8e43e9d36c09149fdc694f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17000,10 +17000,15 @@ checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" name = "toolchain_selector" version = "0.1.0" dependencies = [ + "anyhow", + "convert_case 0.8.0", "editor", + "file_finder", + "futures 0.3.31", "fuzzy", "gpui", "language", + "menu", "picker", "project", "ui", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 70a002cf081deaf5df66a2173dc17e7f02ce3aeb..ac44b3f1ae55feb11b0027efea14c6afed8cb62a 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -628,6 +628,7 @@ "alt-save": "workspace::SaveAll", "ctrl-alt-s": "workspace::SaveAll", "ctrl-k m": "language_selector::Toggle", + "ctrl-k ctrl-m": "toolchain::AddToolchain", "escape": "workspace::Unfollow", "ctrl-k ctrl-left": "workspace::ActivatePaneLeft", "ctrl-k ctrl-right": "workspace::ActivatePaneRight", @@ -1028,6 +1029,13 @@ "tab": "channel_modal::ToggleMode" } }, + { + "context": "ToolchainSelector", + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-a": "toolchain::AddToolchain" + } + }, { "context": "FileFinder || (FileFinder > Picker > Editor)", "bindings": { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 21504c7e623583017459baaac7d25191d7a08b68..337915527ca22f04afc8450cf6a366d1f2995551 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -690,6 +690,7 @@ "cmd-?": "agent::ToggleFocus", "cmd-alt-s": "workspace::SaveAll", "cmd-k m": "language_selector::Toggle", + "cmd-k cmd-m": "toolchain::AddToolchain", "escape": "workspace::Unfollow", "cmd-k cmd-left": "workspace::ActivatePaneLeft", "cmd-k cmd-right": "workspace::ActivatePaneRight", @@ -1094,6 +1095,13 @@ "tab": "channel_modal::ToggleMode" } }, + { + "context": "ToolchainSelector", + "use_key_equivalents": true, + "bindings": { + "cmd-shift-a": "toolchain::AddToolchain" + } + }, { "context": "FileFinder || (FileFinder > Picker > Editor)", "use_key_equivalents": true, diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 51e8f5c437ab1aa86433f91022a01e8a2e09f664..c0abb372b28ff817853e9dc7b6523f676359e157 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -23,7 +23,6 @@ use workspace::Workspace; pub(crate) struct OpenPathPrompt; -#[derive(Debug)] pub struct OpenPathDelegate { tx: Option>>>, lister: DirectoryLister, @@ -35,6 +34,9 @@ pub struct OpenPathDelegate { prompt_root: String, path_style: PathStyle, replace_prompt: Task<()>, + render_footer: + Arc>) -> Option + 'static>, + hidden_entries: bool, } impl OpenPathDelegate { @@ -60,9 +62,25 @@ impl OpenPathDelegate { }, path_style, replace_prompt: Task::ready(()), + render_footer: Arc::new(|_, _| None), + hidden_entries: false, } } + pub fn with_footer( + mut self, + footer: Arc< + dyn Fn(&mut Window, &mut Context>) -> Option + 'static, + >, + ) -> Self { + self.render_footer = footer; + self + } + + pub fn show_hidden(mut self) -> Self { + self.hidden_entries = true; + self + } fn get_entry(&self, selected_match_index: usize) -> Option { match &self.directory_state { DirectoryState::List { entries, .. } => { @@ -269,7 +287,7 @@ impl PickerDelegate for OpenPathDelegate { self.cancel_flag.store(true, atomic::Ordering::Release); self.cancel_flag = Arc::new(AtomicBool::new(false)); let cancel_flag = self.cancel_flag.clone(); - + let hidden_entries = self.hidden_entries; let parent_path_is_root = self.prompt_root == dir; let current_dir = self.current_dir(); cx.spawn_in(window, async move |this, cx| { @@ -363,7 +381,7 @@ impl PickerDelegate for OpenPathDelegate { }; let mut max_id = 0; - if !suffix.starts_with('.') { + if !suffix.starts_with('.') && !hidden_entries { new_entries.retain(|entry| { max_id = max_id.max(entry.path.id); !entry.path.string.starts_with('.') @@ -781,6 +799,14 @@ impl PickerDelegate for OpenPathDelegate { } } + fn render_footer( + &self, + window: &mut Window, + cx: &mut Context>, + ) -> Option { + (self.render_footer)(window, cx) + } + fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option { Some(match &self.directory_state { DirectoryState::Create { .. } => SharedString::from("Type a path…"), diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index e4a1510d7df128158691842206a27844304b3237..86faf2b9d316dd068c400c48c5b0b99196cfc191 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -69,6 +69,7 @@ pub use text_diff::{ use theme::SyntaxTheme; pub use toolchain::{ LanguageToolchainStore, LocalLanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister, + ToolchainMetadata, ToolchainScope, }; use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime}; use util::serde::default_true; diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 84b10c7961eddb130f88b24c9e3438ff2882f8d3..2cc86881fbd515317d4d6f5949e82eb3da63a1bb 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -29,6 +29,40 @@ pub struct Toolchain { pub as_json: serde_json::Value, } +/// Declares a scope of a toolchain added by user. +/// +/// When the user adds a toolchain, we give them an option to see that toolchain in: +/// - All of their projects +/// - A project they're currently in. +/// - Only in the subproject they're currently in. +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub enum ToolchainScope { + Subproject(WorktreeId, Arc), + Project, + /// Available in all projects on this box. It wouldn't make sense to show suggestions across machines. + Global, +} + +impl ToolchainScope { + pub fn label(&self) -> &'static str { + match self { + ToolchainScope::Subproject(_, _) => "Subproject", + ToolchainScope::Project => "Project", + ToolchainScope::Global => "Global", + } + } + + pub fn description(&self) -> &'static str { + match self { + ToolchainScope::Subproject(_, _) => { + "Available only in the subproject you're currently in." + } + ToolchainScope::Project => "Available in all locations in your current project.", + ToolchainScope::Global => "Available in all of your projects on this machine.", + } + } +} + impl std::hash::Hash for Toolchain { fn hash(&self, state: &mut H) { let Self { @@ -58,23 +92,41 @@ impl PartialEq for Toolchain { } #[async_trait] -pub trait ToolchainLister: Send + Sync { +pub trait ToolchainLister: Send + Sync + 'static { + /// List all available toolchains for a given path. async fn list( &self, worktree_root: PathBuf, subroot_relative_path: Arc, project_env: Option>, ) -> ToolchainList; - // Returns a term which we should use in UI to refer to a toolchain. - fn term(&self) -> SharedString; - /// Returns the name of the manifest file for this toolchain. - fn manifest_name(&self) -> ManifestName; + + /// Given a user-created toolchain, resolve lister-specific details. + /// Put another way: fill in the details of the toolchain so the user does not have to. + async fn resolve( + &self, + path: PathBuf, + project_env: Option>, + ) -> anyhow::Result; + async fn activation_script( &self, toolchain: &Toolchain, shell: ShellKind, fs: &dyn Fs, ) -> Vec; + /// Returns various "static" bits of information about this toolchain lister. This function should be pure. + fn meta(&self) -> ToolchainMetadata; +} + +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct ToolchainMetadata { + /// Returns a term which we should use in UI to refer to toolchains produced by a given `[ToolchainLister]`. + pub term: SharedString, + /// A user-facing placeholder describing the semantic meaning of a path to a new toolchain. + pub new_toolchain_placeholder: SharedString, + /// The name of the manifest file for this toolchain. + pub manifest_name: ManifestName, } #[async_trait(?Send)] diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 168cf8f57ca25444e54c11bb8e594faa94726b5d..33fb2af0612a203b45276bb8e7f580c5a86a90b6 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -97,7 +97,7 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { let python_context_provider = Arc::new(python::PythonContextProvider); let python_lsp_adapter = Arc::new(python::PythonLspAdapter::new(node.clone())); let basedpyright_lsp_adapter = Arc::new(BasedPyrightLspAdapter::new()); - let python_toolchain_provider = Arc::new(python::PythonToolchainProvider::default()); + let python_toolchain_provider = Arc::new(python::PythonToolchainProvider); let rust_context_provider = Arc::new(rust::RustContextProvider); let rust_lsp_adapter = Arc::new(rust::RustLspAdapter); let tailwind_adapter = Arc::new(tailwind::TailwindLspAdapter::new(node.clone())); diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 06fb49293f838fca2d54de076139ac8c4ebacfc2..d1f40a8233a3590b382bc1e0edbe5dd69b3317d8 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -5,19 +5,19 @@ use collections::HashMap; use futures::AsyncBufReadExt; use gpui::{App, Task}; use gpui::{AsyncApp, SharedString}; -use language::Toolchain; use language::ToolchainList; use language::ToolchainLister; use language::language_settings::language_settings; use language::{ContextLocation, LanguageToolchainStore}; use language::{ContextProvider, LspAdapter, LspAdapterDelegate}; use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery}; +use language::{Toolchain, ToolchainMetadata}; use lsp::LanguageServerBinary; use lsp::LanguageServerName; use node_runtime::{NodeRuntime, VersionStrategy}; use pet_core::Configuration; use pet_core::os_environment::Environment; -use pet_core::python_environment::PythonEnvironmentKind; +use pet_core::python_environment::{PythonEnvironment, PythonEnvironmentKind}; use project::Fs; use project::lsp_store::language_server_settings; use serde_json::{Value, json}; @@ -688,17 +688,7 @@ fn python_env_kind_display(k: &PythonEnvironmentKind) -> &'static str { } } -pub(crate) struct PythonToolchainProvider { - term: SharedString, -} - -impl Default for PythonToolchainProvider { - fn default() -> Self { - Self { - term: SharedString::new_static("Virtual Environment"), - } - } -} +pub(crate) struct PythonToolchainProvider; static ENV_PRIORITY_LIST: &[PythonEnvironmentKind] = &[ // Prioritize non-Conda environments. @@ -744,9 +734,6 @@ async fn get_worktree_venv_declaration(worktree_root: &Path) -> Option { #[async_trait] impl ToolchainLister for PythonToolchainProvider { - fn manifest_name(&self) -> language::ManifestName { - ManifestName::from(SharedString::new_static("pyproject.toml")) - } async fn list( &self, worktree_root: PathBuf, @@ -847,32 +834,7 @@ impl ToolchainLister for PythonToolchainProvider { let mut toolchains: Vec<_> = toolchains .into_iter() - .filter_map(|toolchain| { - let mut name = String::from("Python"); - if let Some(version) = &toolchain.version { - _ = write!(name, " {version}"); - } - - let name_and_kind = match (&toolchain.name, &toolchain.kind) { - (Some(name), Some(kind)) => { - Some(format!("({name}; {})", python_env_kind_display(kind))) - } - (Some(name), None) => Some(format!("({name})")), - (None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))), - (None, None) => None, - }; - - if let Some(nk) = name_and_kind { - _ = write!(name, " {nk}"); - } - - Some(Toolchain { - name: name.into(), - path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(), - language_name: LanguageName::new("Python"), - as_json: serde_json::to_value(toolchain.clone()).ok()?, - }) - }) + .filter_map(venv_to_toolchain) .collect(); toolchains.dedup(); ToolchainList { @@ -881,9 +843,34 @@ impl ToolchainLister for PythonToolchainProvider { groups: Default::default(), } } - fn term(&self) -> SharedString { - self.term.clone() + fn meta(&self) -> ToolchainMetadata { + ToolchainMetadata { + term: SharedString::new_static("Virtual Environment"), + new_toolchain_placeholder: SharedString::new_static( + "A path to the python3 executable within a virtual environment, or path to virtual environment itself", + ), + manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")), + } + } + + async fn resolve( + &self, + path: PathBuf, + env: Option>, + ) -> anyhow::Result { + let env = env.unwrap_or_default(); + let environment = EnvironmentApi::from_env(&env); + let locators = pet::locators::create_locators( + Arc::new(pet_conda::Conda::from(&environment)), + Arc::new(pet_poetry::Poetry::from(&environment)), + &environment, + ); + let toolchain = pet::resolve::resolve_environment(&path, &locators, &environment) + .context("Could not find a virtual environment in provided path")?; + let venv = toolchain.resolved.unwrap_or(toolchain.discovered); + venv_to_toolchain(venv).context("Could not convert a venv into a toolchain") } + async fn activation_script( &self, toolchain: &Toolchain, @@ -956,6 +943,31 @@ impl ToolchainLister for PythonToolchainProvider { } } +fn venv_to_toolchain(venv: PythonEnvironment) -> Option { + let mut name = String::from("Python"); + if let Some(ref version) = venv.version { + _ = write!(name, " {version}"); + } + + let name_and_kind = match (&venv.name, &venv.kind) { + (Some(name), Some(kind)) => Some(format!("({name}; {})", python_env_kind_display(kind))), + (Some(name), None) => Some(format!("({name})")), + (None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))), + (None, None) => None, + }; + + if let Some(nk) = name_and_kind { + _ = write!(name, " {nk}"); + } + + Some(Toolchain { + name: name.into(), + path: venv.executable.as_ref()?.to_str()?.to_owned().into(), + language_name: LanguageName::new("Python"), + as_json: serde_json::to_value(venv).ok()?, + }) +} + pub struct EnvironmentApi<'a> { global_search_locations: Arc>>, project_env: &'a HashMap, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 7f7e759b275baadfe3b2d3931955ad39b03fdb05..a247c07c910c135b46714123c9dec8b452cbc60b 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3933,8 +3933,8 @@ impl LspStore { event: &ToolchainStoreEvent, _: &mut Context, ) { - match event { - ToolchainStoreEvent::ToolchainActivated => self.request_workspace_config_refresh(), + if let ToolchainStoreEvent::ToolchainActivated = event { + self.request_workspace_config_refresh() } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 66924f159a0a97dce558d742ca3ee80456542305..0ebfd83f4e414763e0f99d473e3b60dab159f743 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -48,7 +48,7 @@ use clock::ReplicaId; use dap::client::DebugAdapterClient; -use collections::{BTreeSet, HashMap, HashSet}; +use collections::{BTreeSet, HashMap, HashSet, IndexSet}; use debounced_delay::DebouncedDelay; pub use debugger::breakpoint_store::BreakpointWithPosition; use debugger::{ @@ -74,8 +74,9 @@ use gpui::{ }; use language::{ Buffer, BufferEvent, Capability, CodeLabel, CursorShape, Language, LanguageName, - LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction, - Unclipped, language_settings::InlayHintKind, proto::split_operations, + LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainMetadata, + ToolchainScope, Transaction, Unclipped, language_settings::InlayHintKind, + proto::split_operations, }; use lsp::{ CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, InsertTextMode, @@ -104,6 +105,7 @@ use snippet::Snippet; use snippet_provider::SnippetProvider; use std::{ borrow::Cow, + collections::BTreeMap, ops::Range, path::{Component, Path, PathBuf}, pin::pin, @@ -117,7 +119,7 @@ use terminals::Terminals; use text::{Anchor, BufferId, OffsetRangeExt, Point, Rope}; use toolchain_store::EmptyToolchainStore; use util::{ - ResultExt as _, + ResultExt as _, maybe, paths::{PathStyle, RemotePathBuf, SanitizedPath, compare_paths}, }; use worktree::{CreatedEntry, Snapshot, Traversal}; @@ -142,7 +144,7 @@ pub use lsp_store::{ LanguageServerStatus, LanguageServerToQuery, LspStore, LspStoreEvent, SERVER_PROGRESS_THROTTLE_TIMEOUT, }; -pub use toolchain_store::ToolchainStore; +pub use toolchain_store::{ToolchainStore, Toolchains}; const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500; const MAX_SEARCH_RESULT_FILES: usize = 5_000; const MAX_SEARCH_RESULT_RANGES: usize = 10_000; @@ -3370,7 +3372,7 @@ impl Project { path: ProjectPath, language_name: LanguageName, cx: &App, - ) -> Task)>> { + ) -> Task> { if let Some(toolchain_store) = self.toolchain_store.as_ref().map(Entity::downgrade) { cx.spawn(async move |cx| { toolchain_store @@ -3383,16 +3385,70 @@ impl Project { } } - pub async fn toolchain_term( + pub async fn toolchain_metadata( languages: Arc, language_name: LanguageName, - ) -> Option { + ) -> Option { languages .language_for_name(language_name.as_ref()) .await .ok()? .toolchain_lister() - .map(|lister| lister.term()) + .map(|lister| lister.meta()) + } + + pub fn add_toolchain( + &self, + toolchain: Toolchain, + scope: ToolchainScope, + cx: &mut Context, + ) { + maybe!({ + self.toolchain_store.as_ref()?.update(cx, |this, cx| { + this.add_toolchain(toolchain, scope, cx); + }); + Some(()) + }); + } + + pub fn remove_toolchain( + &self, + toolchain: Toolchain, + scope: ToolchainScope, + cx: &mut Context, + ) { + maybe!({ + self.toolchain_store.as_ref()?.update(cx, |this, cx| { + this.remove_toolchain(toolchain, scope, cx); + }); + Some(()) + }); + } + + pub fn user_toolchains( + &self, + cx: &App, + ) -> Option>> { + Some(self.toolchain_store.as_ref()?.read(cx).user_toolchains()) + } + + pub fn resolve_toolchain( + &self, + path: PathBuf, + language_name: LanguageName, + cx: &App, + ) -> Task> { + if let Some(toolchain_store) = self.toolchain_store.as_ref().map(Entity::downgrade) { + cx.spawn(async move |cx| { + toolchain_store + .update(cx, |this, cx| { + this.resolve_toolchain(path, language_name, cx) + })? + .await + }) + } else { + Task::ready(Err(anyhow!("This project does not support toolchains"))) + } } pub fn toolchain_store(&self) -> Option> { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 969e18f6d40346aa86d83bd0beb77d6652ff0763..e65da3acd41e7ce4db06821da58fe0969a74217f 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -22,7 +22,7 @@ use itertools::Itertools; use language::{ Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider, - ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainLister, + ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainList, ToolchainLister, language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings}, tree_sitter_rust, tree_sitter_typescript, }; @@ -727,7 +727,12 @@ async fn test_running_multiple_instances_of_a_single_server_in_one_worktree( // We're not using venvs at all here, so both folders should fall under the same root. assert_eq!(server.server_id(), LanguageServerId(0)); // Now, let's select a different toolchain for one of subprojects. - let (available_toolchains_for_b, root_path) = project + + let Toolchains { + toolchains: available_toolchains_for_b, + root_path, + .. + } = project .update(cx, |this, cx| { let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id(); this.available_toolchains( @@ -9213,13 +9218,21 @@ fn python_lang(fs: Arc) -> Arc { ..Default::default() } } - // Returns a term which we should use in UI to refer to a toolchain. - fn term(&self) -> SharedString { - SharedString::new_static("virtual environment") + async fn resolve( + &self, + _: PathBuf, + _: Option>, + ) -> anyhow::Result { + Err(anyhow::anyhow!("Not implemented")) } - /// Returns the name of the manifest file for this toolchain. - fn manifest_name(&self) -> ManifestName { - SharedString::new_static("pyproject.toml").into() + fn meta(&self) -> ToolchainMetadata { + ToolchainMetadata { + term: SharedString::new_static("Virtual Environment"), + new_toolchain_placeholder: SharedString::new_static( + "A path to the python3 executable within a virtual environment, or path to virtual environment itself", + ), + manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")), + } } async fn activation_script(&self, _: &Toolchain, _: ShellKind, _: &dyn Fs) -> Vec { vec![] diff --git a/crates/project/src/toolchain_store.rs b/crates/project/src/toolchain_store.rs index 57d492e26fc7b59df02df0128ed6b9ade132c6d9..e76b98f697768c987f527eaf444c159334b12c96 100644 --- a/crates/project/src/toolchain_store.rs +++ b/crates/project/src/toolchain_store.rs @@ -4,20 +4,23 @@ use std::{ sync::Arc, }; -use anyhow::{Result, bail}; +use anyhow::{Context as _, Result, bail}; use async_trait::async_trait; -use collections::BTreeMap; +use collections::{BTreeMap, IndexSet}; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, }; use language::{ LanguageName, LanguageRegistry, LanguageToolchainStore, ManifestDelegate, Toolchain, - ToolchainList, + ToolchainList, ToolchainScope, }; use rpc::{ AnyProtoClient, TypedEnvelope, - proto::{self, FromProto, ToProto}, + proto::{ + self, FromProto, ResolveToolchainResponse, ToProto, + resolve_toolchain_response::Response as ResolveResponsePayload, + }, }; use settings::WorktreeId; use util::ResultExt as _; @@ -28,24 +31,31 @@ use crate::{ worktree_store::WorktreeStore, }; -pub struct ToolchainStore(ToolchainStoreInner); +pub struct ToolchainStore { + mode: ToolchainStoreInner, + user_toolchains: BTreeMap>, + _sub: Subscription, +} + enum ToolchainStoreInner { - Local( - Entity, - #[allow(dead_code)] Subscription, - ), - Remote( - Entity, - #[allow(dead_code)] Subscription, - ), + Local(Entity), + Remote(Entity), } +pub struct Toolchains { + /// Auto-detected toolchains. + pub toolchains: ToolchainList, + /// Path of the project root at which we ran the automatic toolchain detection. + pub root_path: Arc, + pub user_toolchains: BTreeMap>, +} impl EventEmitter for ToolchainStore {} impl ToolchainStore { pub fn init(client: &AnyProtoClient) { client.add_entity_request_handler(Self::handle_activate_toolchain); client.add_entity_request_handler(Self::handle_list_toolchains); client.add_entity_request_handler(Self::handle_active_toolchain); + client.add_entity_request_handler(Self::handle_resolve_toolchain); } pub fn local( @@ -62,18 +72,26 @@ impl ToolchainStore { active_toolchains: Default::default(), manifest_tree, }); - let subscription = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| { + let _sub = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| { cx.emit(e.clone()) }); - Self(ToolchainStoreInner::Local(entity, subscription)) + Self { + mode: ToolchainStoreInner::Local(entity), + user_toolchains: Default::default(), + _sub, + } } pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut Context) -> Self { let entity = cx.new(|_| RemoteToolchainStore { client, project_id }); - let _subscription = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| { + let _sub = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| { cx.emit(e.clone()) }); - Self(ToolchainStoreInner::Remote(entity, _subscription)) + Self { + mode: ToolchainStoreInner::Remote(entity), + user_toolchains: Default::default(), + _sub, + } } pub(crate) fn activate_toolchain( &self, @@ -81,43 +99,130 @@ impl ToolchainStore { toolchain: Toolchain, cx: &mut App, ) -> Task> { - match &self.0 { - ToolchainStoreInner::Local(local, _) => { + match &self.mode { + ToolchainStoreInner::Local(local) => { local.update(cx, |this, cx| this.activate_toolchain(path, toolchain, cx)) } - ToolchainStoreInner::Remote(remote, _) => { + ToolchainStoreInner::Remote(remote) => { remote.update(cx, |this, cx| this.activate_toolchain(path, toolchain, cx)) } } } + + pub(crate) fn user_toolchains(&self) -> BTreeMap> { + self.user_toolchains.clone() + } + pub(crate) fn add_toolchain( + &mut self, + toolchain: Toolchain, + scope: ToolchainScope, + cx: &mut Context, + ) { + let did_insert = self + .user_toolchains + .entry(scope) + .or_default() + .insert(toolchain); + if did_insert { + cx.emit(ToolchainStoreEvent::CustomToolchainsModified); + } + } + + pub(crate) fn remove_toolchain( + &mut self, + toolchain: Toolchain, + scope: ToolchainScope, + cx: &mut Context, + ) { + let mut did_remove = false; + self.user_toolchains + .entry(scope) + .and_modify(|toolchains| did_remove = toolchains.shift_remove(&toolchain)); + if did_remove { + cx.emit(ToolchainStoreEvent::CustomToolchainsModified); + } + } + + pub(crate) fn resolve_toolchain( + &self, + abs_path: PathBuf, + language_name: LanguageName, + cx: &mut Context, + ) -> Task> { + debug_assert!(abs_path.is_absolute()); + match &self.mode { + ToolchainStoreInner::Local(local) => local.update(cx, |this, cx| { + this.resolve_toolchain(abs_path, language_name, cx) + }), + ToolchainStoreInner::Remote(remote) => remote.update(cx, |this, cx| { + this.resolve_toolchain(abs_path, language_name, cx) + }), + } + } pub(crate) fn list_toolchains( &self, path: ProjectPath, language_name: LanguageName, cx: &mut Context, - ) -> Task)>> { - match &self.0 { - ToolchainStoreInner::Local(local, _) => { + ) -> Task> { + let user_toolchains = self + .user_toolchains + .iter() + .filter(|(scope, _)| { + if let ToolchainScope::Subproject(worktree_id, relative_path) = scope { + path.worktree_id == *worktree_id && relative_path.starts_with(&path.path) + } else { + true + } + }) + .map(|(scope, toolchains)| { + ( + scope.clone(), + toolchains + .iter() + .filter(|toolchain| toolchain.language_name == language_name) + .cloned() + .collect::>(), + ) + }) + .collect::>(); + let task = match &self.mode { + ToolchainStoreInner::Local(local) => { local.update(cx, |this, cx| this.list_toolchains(path, language_name, cx)) } - ToolchainStoreInner::Remote(remote, _) => { + ToolchainStoreInner::Remote(remote) => { remote.read(cx).list_toolchains(path, language_name, cx) } - } + }; + cx.spawn(async move |_, _| { + let (mut toolchains, root_path) = task.await?; + toolchains.toolchains.retain(|toolchain| { + !user_toolchains + .values() + .any(|toolchains| toolchains.contains(toolchain)) + }); + + Some(Toolchains { + toolchains, + root_path, + user_toolchains, + }) + }) } + pub(crate) fn active_toolchain( &self, path: ProjectPath, language_name: LanguageName, cx: &App, ) -> Task> { - match &self.0 { - ToolchainStoreInner::Local(local, _) => Task::ready(local.read(cx).active_toolchain( + match &self.mode { + ToolchainStoreInner::Local(local) => Task::ready(local.read(cx).active_toolchain( path.worktree_id, &path.path, language_name, )), - ToolchainStoreInner::Remote(remote, _) => { + ToolchainStoreInner::Remote(remote) => { remote.read(cx).active_toolchain(path, language_name, cx) } } @@ -197,7 +302,7 @@ impl ToolchainStore { })? .await; let has_values = toolchains.is_some(); - let groups = if let Some((toolchains, _)) = &toolchains { + let groups = if let Some(Toolchains { toolchains, .. }) = &toolchains { toolchains .groups .iter() @@ -211,7 +316,12 @@ impl ToolchainStore { } else { vec![] }; - let (toolchains, relative_path) = if let Some((toolchains, relative_path)) = toolchains { + let (toolchains, relative_path) = if let Some(Toolchains { + toolchains, + root_path: relative_path, + .. + }) = toolchains + { let toolchains = toolchains .toolchains .into_iter() @@ -236,16 +346,45 @@ impl ToolchainStore { relative_worktree_path: Some(relative_path.to_string_lossy().into_owned()), }) } + + async fn handle_resolve_toolchain( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let toolchain = this + .update(&mut cx, |this, cx| { + let language_name = LanguageName::from_proto(envelope.payload.language_name); + let path = PathBuf::from(envelope.payload.abs_path); + this.resolve_toolchain(path, language_name, cx) + })? + .await; + let response = match toolchain { + Ok(toolchain) => { + let toolchain = proto::Toolchain { + name: toolchain.name.to_string(), + path: toolchain.path.to_string(), + raw_json: toolchain.as_json.to_string(), + }; + ResolveResponsePayload::Toolchain(toolchain) + } + Err(e) => ResolveResponsePayload::Error(e.to_string()), + }; + Ok(ResolveToolchainResponse { + response: Some(response), + }) + } + pub fn as_language_toolchain_store(&self) -> Arc { - match &self.0 { - ToolchainStoreInner::Local(local, _) => Arc::new(LocalStore(local.downgrade())), - ToolchainStoreInner::Remote(remote, _) => Arc::new(RemoteStore(remote.downgrade())), + match &self.mode { + ToolchainStoreInner::Local(local) => Arc::new(LocalStore(local.downgrade())), + ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())), } } pub fn as_local_store(&self) -> Option<&Entity> { - match &self.0 { - ToolchainStoreInner::Local(local, _) => Some(local), - ToolchainStoreInner::Remote(_, _) => None, + match &self.mode { + ToolchainStoreInner::Local(local) => Some(local), + ToolchainStoreInner::Remote(_) => None, } } } @@ -311,6 +450,7 @@ struct RemoteStore(WeakEntity); #[derive(Clone)] pub enum ToolchainStoreEvent { ToolchainActivated, + CustomToolchainsModified, } impl EventEmitter for LocalToolchainStore {} @@ -351,7 +491,7 @@ impl LocalToolchainStore { .await .ok()?; let toolchains = language.toolchain_lister()?; - let manifest_name = toolchains.manifest_name(); + let manifest_name = toolchains.meta().manifest_name; let (snapshot, worktree) = this .update(cx, |this, cx| { this.worktree_store @@ -414,6 +554,33 @@ impl LocalToolchainStore { }) .cloned() } + + fn resolve_toolchain( + &self, + path: PathBuf, + language_name: LanguageName, + cx: &mut Context, + ) -> Task> { + let registry = self.languages.clone(); + let environment = self.project_environment.clone(); + cx.spawn(async move |_, cx| { + let language = cx + .background_spawn(registry.language_for_name(&language_name.0)) + .await + .with_context(|| format!("Language {} not found", language_name.0))?; + let toolchain_lister = language.toolchain_lister().with_context(|| { + format!("Language {} does not support toolchains", language_name.0) + })?; + + let project_env = environment + .update(cx, |environment, cx| { + environment.get_directory_environment(path.as_path().into(), cx) + })? + .await; + cx.background_spawn(async move { toolchain_lister.resolve(path, project_env).await }) + .await + }) + } } impl EventEmitter for RemoteToolchainStore {} @@ -556,4 +723,47 @@ impl RemoteToolchainStore { }) }) } + + fn resolve_toolchain( + &self, + abs_path: PathBuf, + language_name: LanguageName, + cx: &mut Context, + ) -> Task> { + let project_id = self.project_id; + let client = self.client.clone(); + cx.background_spawn(async move { + let response: proto::ResolveToolchainResponse = client + .request(proto::ResolveToolchain { + project_id, + language_name: language_name.clone().into(), + abs_path: abs_path.to_string_lossy().into_owned(), + }) + .await?; + + let response = response + .response + .context("Failed to resolve toolchain via RPC")?; + use proto::resolve_toolchain_response::Response; + match response { + Response::Toolchain(toolchain) => { + Ok(Toolchain { + language_name: language_name.clone(), + name: toolchain.name.into(), + // todo(windows) + // Do we need to convert path to native string? + path: PathBuf::from_proto(toolchain.path) + .to_string_lossy() + .to_string() + .into(), + as_json: serde_json::Value::from_str(&toolchain.raw_json) + .context("Deserializing ResolveToolchain LSP response")?, + }) + } + Response::Error(error) => { + anyhow::bail!("{error}"); + } + } + }) + } } diff --git a/crates/proto/proto/toolchain.proto b/crates/proto/proto/toolchain.proto index 08844a307a2c44cf2a30405b3202f10c72db579d..b190322ca0602078ea28d00fe970e4958fb17fb0 100644 --- a/crates/proto/proto/toolchain.proto +++ b/crates/proto/proto/toolchain.proto @@ -44,3 +44,16 @@ message ActiveToolchain { message ActiveToolchainResponse { optional Toolchain toolchain = 1; } + +message ResolveToolchain { + uint64 project_id = 1; + string abs_path = 2; + string language_name = 3; +} + +message ResolveToolchainResponse { + oneof response { + Toolchain toolchain = 1; + string error = 2; + } +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 3763671a7a1f29949194d61c70866f96ca6ad972..39fa1fdd53d140cb5d88da751d843e6a7ad1db70 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -402,7 +402,10 @@ message Envelope { UpdateUserSettings update_user_settings = 368; GetProcesses get_processes = 369; - GetProcessesResponse get_processes_response = 370; // current max + GetProcessesResponse get_processes_response = 370; + + ResolveToolchain resolve_toolchain = 371; + ResolveToolchainResponse resolve_toolchain_response = 372; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 3c98ae62e7a4b1489c071a0ac673d23b394c28d5..4c0fc3dc98e22029cf167c0506916d71f3e93602 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -26,6 +26,8 @@ messages!( (ActivateToolchain, Foreground), (ActiveToolchain, Foreground), (ActiveToolchainResponse, Foreground), + (ResolveToolchain, Background), + (ResolveToolchainResponse, Background), (AddNotification, Foreground), (AddProjectCollaborator, Foreground), (AddWorktree, Foreground), @@ -459,6 +461,7 @@ request_messages!( (ListToolchains, ListToolchainsResponse), (ActivateToolchain, Ack), (ActiveToolchain, ActiveToolchainResponse), + (ResolveToolchain, ResolveToolchainResponse), (GetPathMetadata, GetPathMetadataResponse), (GetCrashFiles, GetCrashFilesResponse), (CancelLanguageServerWork, Ack), @@ -612,6 +615,7 @@ entity_messages!( ListToolchains, ActivateToolchain, ActiveToolchain, + ResolveToolchain, GetPathMetadata, GetProcesses, CancelLanguageServerWork, diff --git a/crates/repl/src/kernels/mod.rs b/crates/repl/src/kernels/mod.rs index 52188a39c48f5fc07a1f4a64949a82d205f75f9f..fb16cb1ea3b093b0592cb114a1224dc4858630fe 100644 --- a/crates/repl/src/kernels/mod.rs +++ b/crates/repl/src/kernels/mod.rs @@ -11,7 +11,7 @@ use language::LanguageName; pub use native_kernel::*; mod remote_kernels; -use project::{Project, ProjectPath, WorktreeId}; +use project::{Project, ProjectPath, Toolchains, WorktreeId}; pub use remote_kernels::*; use anyhow::Result; @@ -92,49 +92,58 @@ pub fn python_env_kernel_specifications( let background_executor = cx.background_executor().clone(); async move { - let toolchains = if let Some((toolchains, _)) = toolchains.await { - toolchains + let (toolchains, user_toolchains) = if let Some(Toolchains { + toolchains, + root_path: _, + user_toolchains, + }) = toolchains.await + { + (toolchains, user_toolchains) } else { return Ok(Vec::new()); }; - let kernelspecs = toolchains.toolchains.into_iter().map(|toolchain| { - background_executor.spawn(async move { - let python_path = toolchain.path.to_string(); - - // Check if ipykernel is installed - let ipykernel_check = util::command::new_smol_command(&python_path) - .args(&["-c", "import ipykernel"]) - .output() - .await; - - if ipykernel_check.is_ok() && ipykernel_check.unwrap().status.success() { - // Create a default kernelspec for this environment - let default_kernelspec = JupyterKernelspec { - argv: vec![ - python_path.clone(), - "-m".to_string(), - "ipykernel_launcher".to_string(), - "-f".to_string(), - "{connection_file}".to_string(), - ], - display_name: toolchain.name.to_string(), - language: "python".to_string(), - interrupt_mode: None, - metadata: None, - env: None, - }; - - Some(KernelSpecification::PythonEnv(LocalKernelSpecification { - name: toolchain.name.to_string(), - path: PathBuf::from(&python_path), - kernelspec: default_kernelspec, - })) - } else { - None - } - }) - }); + let kernelspecs = user_toolchains + .into_values() + .flatten() + .chain(toolchains.toolchains) + .map(|toolchain| { + background_executor.spawn(async move { + let python_path = toolchain.path.to_string(); + + // Check if ipykernel is installed + let ipykernel_check = util::command::new_smol_command(&python_path) + .args(&["-c", "import ipykernel"]) + .output() + .await; + + if ipykernel_check.is_ok() && ipykernel_check.unwrap().status.success() { + // Create a default kernelspec for this environment + let default_kernelspec = JupyterKernelspec { + argv: vec![ + python_path.clone(), + "-m".to_string(), + "ipykernel_launcher".to_string(), + "-f".to_string(), + "{connection_file}".to_string(), + ], + display_name: toolchain.name.to_string(), + language: "python".to_string(), + interrupt_mode: None, + metadata: None, + env: None, + }; + + Some(KernelSpecification::PythonEnv(LocalKernelSpecification { + name: toolchain.name.to_string(), + path: PathBuf::from(&python_path), + kernelspec: default_kernelspec, + })) + } else { + None + } + }) + }); let kernel_specs = futures::future::join_all(kernelspecs) .await diff --git a/crates/toolchain_selector/Cargo.toml b/crates/toolchain_selector/Cargo.toml index 46b88594fdda8979a861fb33317cae81a32d2ea1..a17f82564093e2ae17f95ec82559f308b910b2dd 100644 --- a/crates/toolchain_selector/Cargo.toml +++ b/crates/toolchain_selector/Cargo.toml @@ -6,10 +6,15 @@ publish.workspace = true license = "GPL-3.0-or-later" [dependencies] +anyhow.workspace = true +convert_case.workspace = true editor.workspace = true +file_finder.workspace = true +futures.workspace = true fuzzy.workspace = true gpui.workspace = true language.workspace = true +menu.workspace = true picker.workspace = true project.workspace = true ui.workspace = true diff --git a/crates/toolchain_selector/src/active_toolchain.rs b/crates/toolchain_selector/src/active_toolchain.rs index bf45bffea30791a062e4a130b0f742f3d47c1342..3e26f3ad6c3d23c4b0e00c4c9f67e37fd9c33d32 100644 --- a/crates/toolchain_selector/src/active_toolchain.rs +++ b/crates/toolchain_selector/src/active_toolchain.rs @@ -5,8 +5,8 @@ use gpui::{ AsyncWindowContext, Context, Entity, IntoElement, ParentElement, Render, Subscription, Task, WeakEntity, Window, div, }; -use language::{Buffer, BufferEvent, LanguageName, Toolchain}; -use project::{Project, ProjectPath, WorktreeId, toolchain_store::ToolchainStoreEvent}; +use language::{Buffer, BufferEvent, LanguageName, Toolchain, ToolchainScope}; +use project::{Project, ProjectPath, Toolchains, WorktreeId, toolchain_store::ToolchainStoreEvent}; use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip}; use util::maybe; use workspace::{StatusItemView, Workspace, item::ItemHandle}; @@ -69,15 +69,15 @@ impl ActiveToolchain { .read_with(cx, |this, _| Some(this.language()?.name())) .ok() .flatten()?; - let term = workspace + let meta = workspace .update(cx, |workspace, cx| { let languages = workspace.project().read(cx).languages(); - Project::toolchain_term(languages.clone(), language_name.clone()) + Project::toolchain_metadata(languages.clone(), language_name.clone()) }) .ok()? .await?; let _ = this.update(cx, |this, cx| { - this.term = term; + this.term = meta.term; cx.notify(); }); let (worktree_id, path) = active_file @@ -170,7 +170,11 @@ impl ActiveToolchain { let project = workspace .read_with(cx, |this, _| this.project().clone()) .ok()?; - let (toolchains, relative_path) = cx + let Toolchains { + toolchains, + root_path: relative_path, + user_toolchains, + } = cx .update(|_, cx| { project.read(cx).available_toolchains( ProjectPath { @@ -183,8 +187,20 @@ impl ActiveToolchain { }) .ok()? .await?; - if let Some(toolchain) = toolchains.toolchains.first() { - // Since we don't have a selected toolchain, pick one for user here. + // Since we don't have a selected toolchain, pick one for user here. + let default_choice = user_toolchains + .iter() + .find_map(|(scope, toolchains)| { + if scope == &ToolchainScope::Global { + // Ignore global toolchains when making a default choice. They're unlikely to be the right choice. + None + } else { + toolchains.first() + } + }) + .or_else(|| toolchains.toolchains.first()) + .cloned(); + if let Some(toolchain) = &default_choice { workspace::WORKSPACE_DB .set_toolchain( workspace_id, @@ -209,7 +225,7 @@ impl ActiveToolchain { .await; } - toolchains.toolchains.first().cloned() + default_choice } }) } diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs index feeca8cf52a5116d53562826da72a0bb304d16ce..2f946a69152f76912a1da996e429c48e3ec3be10 100644 --- a/crates/toolchain_selector/src/toolchain_selector.rs +++ b/crates/toolchain_selector/src/toolchain_selector.rs @@ -1,25 +1,39 @@ mod active_toolchain; pub use active_toolchain::ActiveToolchain; +use convert_case::Casing as _; use editor::Editor; +use file_finder::OpenPathDelegate; +use futures::channel::oneshot; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use gpui::{ - App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ParentElement, - Render, Styled, Task, WeakEntity, Window, actions, + Action, Animation, AnimationExt, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, + Focusable, KeyContext, ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, + actions, pulsating_between, }; -use language::{LanguageName, Toolchain, ToolchainList}; +use language::{Language, LanguageName, Toolchain, ToolchainScope}; use picker::{Picker, PickerDelegate}; -use project::{Project, ProjectPath, WorktreeId}; -use std::{borrow::Cow, path::Path, sync::Arc}; -use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; -use util::ResultExt; +use project::{DirectoryLister, Project, ProjectPath, Toolchains, WorktreeId}; +use std::{ + borrow::Cow, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; +use ui::{ + Divider, HighlightedLabel, KeyBinding, List, ListItem, ListItemSpacing, Navigable, + NavigableEntry, prelude::*, +}; +use util::{ResultExt, maybe, paths::PathStyle}; use workspace::{ModalView, Workspace}; actions!( toolchain, [ /// Selects a toolchain for the current project. - Select + Select, + /// Adds a new toolchain for the current project. + AddToolchain ] ); @@ -28,9 +42,513 @@ pub fn init(cx: &mut App) { } pub struct ToolchainSelector { + state: State, + create_search_state: Arc) -> SearchState + 'static>, + language: Option>, + project: Entity, + language_name: LanguageName, + worktree_id: WorktreeId, + relative_path: Arc, +} + +#[derive(Clone)] +struct SearchState { picker: Entity>, } +struct AddToolchainState { + state: AddState, + project: Entity, + language_name: LanguageName, + root_path: ProjectPath, + weak: WeakEntity, +} + +struct ScopePickerState { + entries: [NavigableEntry; 3], + selected_scope: ToolchainScope, +} + +#[expect( + dead_code, + reason = "These tasks have to be kept alive to run to completion" +)] +enum PathInputState { + WaitingForPath(Task<()>), + Resolving(Task<()>), +} + +enum AddState { + Path { + picker: Entity>, + error: Option>, + input_state: PathInputState, + _subscription: Subscription, + }, + Name { + toolchain: Toolchain, + editor: Entity, + scope_picker: ScopePickerState, + }, +} + +impl AddToolchainState { + fn new( + project: Entity, + language_name: LanguageName, + root_path: ProjectPath, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + let weak = cx.weak_entity(); + + cx.new(|cx| { + let (lister, rx) = Self::create_path_browser_delegate(project.clone(), cx); + let picker = cx.new(|cx| Picker::uniform_list(lister, window, cx)); + Self { + state: AddState::Path { + _subscription: cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| { + cx.stop_propagation(); + }), + picker, + error: None, + input_state: Self::wait_for_path(rx, window, cx), + }, + project, + language_name, + root_path, + weak, + } + }) + } + + fn create_path_browser_delegate( + project: Entity, + cx: &mut Context, + ) -> (OpenPathDelegate, oneshot::Receiver>>) { + let (tx, rx) = oneshot::channel(); + let weak = cx.weak_entity(); + let lister = OpenPathDelegate::new( + tx, + DirectoryLister::Project(project), + false, + PathStyle::current(), + ) + .show_hidden() + .with_footer(Arc::new(move |_, cx| { + let error = weak + .read_with(cx, |this, _| { + if let AddState::Path { error, .. } = &this.state { + error.clone() + } else { + None + } + }) + .ok() + .flatten(); + let is_loading = weak + .read_with(cx, |this, _| { + matches!( + this.state, + AddState::Path { + input_state: PathInputState::Resolving(_), + .. + } + ) + }) + .unwrap_or_default(); + Some( + v_flex() + .child(Divider::horizontal()) + .child( + h_flex() + .p_1() + .justify_between() + .gap_2() + .child(Label::new("Select Toolchain Path").color(Color::Muted).map( + |this| { + if is_loading { + this.with_animation( + "select-toolchain-label", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 0.8)), + |label, delta| label.alpha(delta), + ) + .into_any() + } else { + this.into_any_element() + } + }, + )) + .when_some(error, |this, error| { + this.child(Label::new(error).color(Color::Error)) + }), + ) + .into_any(), + ) + })); + + (lister, rx) + } + fn resolve_path( + path: PathBuf, + root_path: ProjectPath, + language_name: LanguageName, + project: Entity, + window: &mut Window, + cx: &mut Context, + ) -> PathInputState { + PathInputState::Resolving(cx.spawn_in(window, async move |this, cx| { + _ = maybe!(async move { + let toolchain = project + .update(cx, |this, cx| { + this.resolve_toolchain(path.clone(), language_name, cx) + })? + .await; + let Ok(toolchain) = toolchain else { + // Go back to the path input state + _ = this.update_in(cx, |this, window, cx| { + if let AddState::Path { + input_state, + picker, + error, + .. + } = &mut this.state + && matches!(input_state, PathInputState::Resolving(_)) + { + let Err(e) = toolchain else { unreachable!() }; + *error = Some(Arc::from(e.to_string())); + let (delegate, rx) = + Self::create_path_browser_delegate(this.project.clone(), cx); + picker.update(cx, |picker, cx| { + *picker = Picker::uniform_list(delegate, window, cx); + picker.set_query( + Arc::from(path.to_string_lossy().as_ref()), + window, + cx, + ); + }); + *input_state = Self::wait_for_path(rx, window, cx); + this.focus_handle(cx).focus(window); + } + }); + return Err(anyhow::anyhow!("Failed to resolve toolchain")); + }; + let resolved_toolchain_path = project.read_with(cx, |this, cx| { + this.find_project_path(&toolchain.path.as_ref(), cx) + })?; + + // Suggest a default scope based on the applicability. + let scope = if let Some(project_path) = resolved_toolchain_path { + if root_path.path.as_ref() != Path::new("") + && project_path.starts_with(&root_path) + { + ToolchainScope::Subproject(root_path.worktree_id, root_path.path) + } else { + ToolchainScope::Project + } + } else { + // This path lies outside of the project. + ToolchainScope::Global + }; + + _ = this.update_in(cx, |this, window, cx| { + let scope_picker = ScopePickerState { + entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), + selected_scope: scope, + }; + this.state = AddState::Name { + editor: cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_text(toolchain.name.as_ref(), window, cx); + editor + }), + toolchain, + scope_picker, + }; + this.focus_handle(cx).focus(window); + }); + + Result::<_, anyhow::Error>::Ok(()) + }) + .await; + })) + } + + fn wait_for_path( + rx: oneshot::Receiver>>, + window: &mut Window, + cx: &mut Context, + ) -> PathInputState { + let task = cx.spawn_in(window, async move |this, cx| { + maybe!(async move { + let result = rx.await.log_err()?; + + let path = result + .into_iter() + .flat_map(|paths| paths.into_iter()) + .next()?; + this.update_in(cx, |this, window, cx| { + if let AddState::Path { + input_state, error, .. + } = &mut this.state + && matches!(input_state, PathInputState::WaitingForPath(_)) + { + error.take(); + *input_state = Self::resolve_path( + path, + this.root_path.clone(), + this.language_name.clone(), + this.project.clone(), + window, + cx, + ); + } + }) + .ok()?; + Some(()) + }) + .await; + }); + PathInputState::WaitingForPath(task) + } + + fn confirm_toolchain( + &mut self, + _: &menu::Confirm, + window: &mut Window, + cx: &mut Context, + ) { + let AddState::Name { + toolchain, + editor, + scope_picker, + } = &mut self.state + else { + return; + }; + + let text = editor.read(cx).text(cx); + if text.is_empty() { + return; + } + + toolchain.name = SharedString::from(text); + self.project.update(cx, |this, cx| { + this.add_toolchain(toolchain.clone(), scope_picker.selected_scope.clone(), cx); + }); + _ = self.weak.update(cx, |this, cx| { + this.state = State::Search((this.create_search_state)(window, cx)); + this.focus_handle(cx).focus(window); + cx.notify(); + }); + } +} +impl Focusable for AddToolchainState { + fn focus_handle(&self, cx: &App) -> FocusHandle { + match &self.state { + AddState::Path { picker, .. } => picker.focus_handle(cx), + AddState::Name { editor, .. } => editor.focus_handle(cx), + } + } +} + +impl AddToolchainState { + fn select_scope(&mut self, scope: ToolchainScope, cx: &mut Context) { + if let AddState::Name { scope_picker, .. } = &mut self.state { + scope_picker.selected_scope = scope; + cx.notify(); + } + } +} + +impl Focusable for State { + fn focus_handle(&self, cx: &App) -> FocusHandle { + match self { + State::Search(state) => state.picker.focus_handle(cx), + State::AddToolchain(state) => state.focus_handle(cx), + } + } +} +impl Render for AddToolchainState { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let theme = cx.theme().clone(); + let weak = self.weak.upgrade(); + let label = SharedString::new_static("Add"); + + v_flex() + .size_full() + // todo: These modal styles shouldn't be needed as the modal picker already has `elevation_3` + // They get duplicated in the middle state of adding a virtual env, but then are needed for this last state + .bg(cx.theme().colors().elevated_surface_background) + .border_1() + .border_color(cx.theme().colors().border_variant) + .rounded_lg() + .when_some(weak, |this, weak| { + this.on_action(window.listener_for( + &weak, + |this: &mut ToolchainSelector, _: &menu::Cancel, window, cx| { + this.state = State::Search((this.create_search_state)(window, cx)); + this.state.focus_handle(cx).focus(window); + cx.notify(); + }, + )) + }) + .on_action(cx.listener(Self::confirm_toolchain)) + .map(|this| match &self.state { + AddState::Path { picker, .. } => this.child(picker.clone()), + AddState::Name { + editor, + scope_picker, + .. + } => { + let scope_options = [ + ToolchainScope::Global, + ToolchainScope::Project, + ToolchainScope::Subproject( + self.root_path.worktree_id, + self.root_path.path.clone(), + ), + ]; + + let mut navigable_scope_picker = Navigable::new( + v_flex() + .child( + h_flex() + .w_full() + .p_2() + .border_b_1() + .border_color(theme.colors().border) + .child(editor.clone()), + ) + .child( + v_flex() + .child( + Label::new("Scope") + .size(LabelSize::Small) + .color(Color::Muted) + .mt_1() + .ml_2(), + ) + .child(List::new().children( + scope_options.iter().enumerate().map(|(i, scope)| { + let is_selected = *scope == scope_picker.selected_scope; + let label = scope.label(); + let description = scope.description(); + let scope_clone_for_action = scope.clone(); + let scope_clone_for_click = scope.clone(); + + div() + .id(SharedString::from(format!("scope-option-{i}"))) + .track_focus(&scope_picker.entries[i].focus_handle) + .on_action(cx.listener( + move |this, _: &menu::Confirm, _, cx| { + this.select_scope( + scope_clone_for_action.clone(), + cx, + ); + }, + )) + .child( + ListItem::new(SharedString::from(format!( + "scope-{i}" + ))) + .toggle_state( + is_selected + || scope_picker.entries[i] + .focus_handle + .contains_focused(window, cx), + ) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .child( + h_flex() + .gap_2() + .child(Label::new(label)) + .child( + Label::new(description) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + .on_click(cx.listener(move |this, _, _, cx| { + this.select_scope( + scope_clone_for_click.clone(), + cx, + ); + })), + ) + }), + )) + .child(Divider::horizontal()) + .child(h_flex().p_1p5().justify_end().map(|this| { + let is_disabled = editor.read(cx).is_empty(cx); + let handle = self.focus_handle(cx); + this.child( + Button::new("add-toolchain", label) + .disabled(is_disabled) + .key_binding(KeyBinding::for_action_in( + &menu::Confirm, + &handle, + window, + cx, + )) + .on_click(cx.listener(|this, _, window, cx| { + this.confirm_toolchain( + &menu::Confirm, + window, + cx, + ); + })) + .map(|this| { + if false { + this.with_animation( + "inspecting-user-toolchain", + Animation::new(Duration::from_millis( + 500, + )) + .repeat() + .with_easing(pulsating_between( + 0.4, 0.8, + )), + |label, delta| label.alpha(delta), + ) + .into_any() + } else { + this.into_any_element() + } + }), + ) + })), + ) + .into_any_element(), + ); + + for entry in &scope_picker.entries { + navigable_scope_picker = navigable_scope_picker.entry(entry.clone()); + } + + this.child(navigable_scope_picker.render(window, cx)) + } + }) + } +} + +#[derive(Clone)] +enum State { + Search(SearchState), + AddToolchain(Entity), +} + +impl RenderOnce for State { + fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { + match self { + State::Search(state) => state.picker.into_any_element(), + State::AddToolchain(state) => state.into_any_element(), + } + } +} impl ToolchainSelector { fn register( workspace: &mut Workspace, @@ -40,6 +558,16 @@ impl ToolchainSelector { workspace.register_action(move |workspace, _: &Select, window, cx| { Self::toggle(workspace, window, cx); }); + workspace.register_action(move |workspace, _: &AddToolchain, window, cx| { + let Some(toolchain_selector) = workspace.active_modal::(cx) else { + Self::toggle(workspace, window, cx); + return; + }; + + toolchain_selector.update(cx, |toolchain_selector, cx| { + toolchain_selector.handle_add_toolchain(&AddToolchain, window, cx); + }); + }); } fn toggle( @@ -105,35 +633,100 @@ impl ToolchainSelector { window: &mut Window, cx: &mut Context, ) -> Self { - let toolchain_selector = cx.entity().downgrade(); - let picker = cx.new(|cx| { - let delegate = ToolchainSelectorDelegate::new( - active_toolchain, - toolchain_selector, - workspace, - worktree_id, - worktree_root, - project, - relative_path, - language_name, + let language_registry = project.read(cx).languages().clone(); + cx.spawn({ + let language_name = language_name.clone(); + async move |this, cx| { + let language = language_registry + .language_for_name(&language_name.0) + .await + .ok(); + this.update(cx, |this, cx| { + this.language = language; + cx.notify(); + }) + .ok(); + } + }) + .detach(); + let project_clone = project.clone(); + let language_name_clone = language_name.clone(); + let relative_path_clone = relative_path.clone(); + + let create_search_state = Arc::new(move |window: &mut Window, cx: &mut Context| { + let toolchain_selector = cx.entity().downgrade(); + let picker = cx.new(|cx| { + let delegate = ToolchainSelectorDelegate::new( + active_toolchain.clone(), + toolchain_selector, + workspace.clone(), + worktree_id, + worktree_root.clone(), + project_clone.clone(), + relative_path_clone.clone(), + language_name_clone.clone(), + window, + cx, + ); + Picker::uniform_list(delegate, window, cx) + }); + let picker_focus_handle = picker.focus_handle(cx); + picker.update(cx, |picker, _| { + picker.delegate.focus_handle = picker_focus_handle.clone(); + }); + SearchState { picker } + }); + + Self { + state: State::Search(create_search_state(window, cx)), + create_search_state, + language: None, + project, + language_name, + worktree_id, + relative_path, + } + } + + fn handle_add_toolchain( + &mut self, + _: &AddToolchain, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.state, State::Search(_)) { + self.state = State::AddToolchain(AddToolchainState::new( + self.project.clone(), + self.language_name.clone(), + ProjectPath { + worktree_id: self.worktree_id, + path: self.relative_path.clone(), + }, window, cx, - ); - Picker::uniform_list(delegate, window, cx) - }); - Self { picker } + )); + self.state.focus_handle(cx).focus(window); + cx.notify(); + } } } impl Render for ToolchainSelector { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex().w(rems(34.)).child(self.picker.clone()) + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let mut key_context = KeyContext::new_with_defaults(); + key_context.add("ToolchainSelector"); + + v_flex() + .key_context(key_context) + .w(rems(34.)) + .on_action(cx.listener(Self::handle_add_toolchain)) + .child(self.state.clone().render(window, cx)) } } impl Focusable for ToolchainSelector { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.picker.focus_handle(cx) + self.state.focus_handle(cx) } } @@ -142,7 +735,7 @@ impl ModalView for ToolchainSelector {} pub struct ToolchainSelectorDelegate { toolchain_selector: WeakEntity, - candidates: ToolchainList, + candidates: Arc<[(Toolchain, Option)]>, matches: Vec, selected_index: usize, workspace: WeakEntity, @@ -150,6 +743,9 @@ pub struct ToolchainSelectorDelegate { worktree_abs_path_root: Arc, relative_path: Arc, placeholder_text: Arc, + add_toolchain_text: Arc, + project: Entity, + focus_handle: FocusHandle, _fetch_candidates_task: Task>, } @@ -166,19 +762,33 @@ impl ToolchainSelectorDelegate { window: &mut Window, cx: &mut Context>, ) -> Self { + let _project = project.clone(); + let _fetch_candidates_task = cx.spawn_in(window, { async move |this, cx| { - let term = project + let meta = _project .read_with(cx, |this, _| { - Project::toolchain_term(this.languages().clone(), language_name.clone()) + Project::toolchain_metadata(this.languages().clone(), language_name.clone()) }) .ok()? .await?; let relative_path = this - .read_with(cx, |this, _| this.delegate.relative_path.clone()) + .update(cx, |this, cx| { + this.delegate.add_toolchain_text = format!( + "Add {}", + meta.term.as_ref().to_case(convert_case::Case::Title) + ) + .into(); + cx.notify(); + this.delegate.relative_path.clone() + }) .ok()?; - let (available_toolchains, relative_path) = project + let Toolchains { + toolchains: available_toolchains, + root_path: relative_path, + user_toolchains, + } = _project .update(cx, |this, cx| { this.available_toolchains( ProjectPath { @@ -200,7 +810,7 @@ impl ToolchainSelectorDelegate { } }; let placeholder_text = - format!("Select a {} for {pretty_path}…", term.to_lowercase(),).into(); + format!("Select a {} for {pretty_path}…", meta.term.to_lowercase(),).into(); let _ = this.update_in(cx, move |this, window, cx| { this.delegate.relative_path = relative_path; this.delegate.placeholder_text = placeholder_text; @@ -208,15 +818,27 @@ impl ToolchainSelectorDelegate { }); let _ = this.update_in(cx, move |this, window, cx| { - this.delegate.candidates = available_toolchains; + this.delegate.candidates = user_toolchains + .into_iter() + .flat_map(|(scope, toolchains)| { + toolchains + .into_iter() + .map(move |toolchain| (toolchain, Some(scope.clone()))) + }) + .chain( + available_toolchains + .toolchains + .into_iter() + .map(|toolchain| (toolchain, None)), + ) + .collect(); if let Some(active_toolchain) = active_toolchain && let Some(position) = this .delegate .candidates - .toolchains .iter() - .position(|toolchain| *toolchain == active_toolchain) + .position(|(toolchain, _)| *toolchain == active_toolchain) { this.delegate.set_selected_index(position, window, cx); } @@ -238,6 +860,9 @@ impl ToolchainSelectorDelegate { placeholder_text, relative_path, _fetch_candidates_task, + project, + focus_handle: cx.focus_handle(), + add_toolchain_text: Arc::from("Add Toolchain"), } } fn relativize_path(path: SharedString, worktree_root: &Path) -> SharedString { @@ -263,7 +888,7 @@ impl PickerDelegate for ToolchainSelectorDelegate { fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { if let Some(string_match) = self.matches.get(self.selected_index) { - let toolchain = self.candidates.toolchains[string_match.candidate_id].clone(); + let (toolchain, _) = self.candidates[string_match.candidate_id].clone(); if let Some(workspace_id) = self .workspace .read_with(cx, |this, _| this.database_id()) @@ -330,11 +955,11 @@ impl PickerDelegate for ToolchainSelectorDelegate { cx.spawn_in(window, async move |this, cx| { let matches = if query.is_empty() { candidates - .toolchains .into_iter() .enumerate() - .map(|(index, candidate)| { - let path = Self::relativize_path(candidate.path, &worktree_root_path); + .map(|(index, (candidate, _))| { + let path = + Self::relativize_path(candidate.path.clone(), &worktree_root_path); let string = format!("{}{}", candidate.name, path); StringMatch { candidate_id: index, @@ -346,11 +971,11 @@ impl PickerDelegate for ToolchainSelectorDelegate { .collect() } else { let candidates = candidates - .toolchains .into_iter() .enumerate() - .map(|(candidate_id, toolchain)| { - let path = Self::relativize_path(toolchain.path, &worktree_root_path); + .map(|(candidate_id, (toolchain, _))| { + let path = + Self::relativize_path(toolchain.path.clone(), &worktree_root_path); let string = format!("{}{}", toolchain.name, path); StringMatchCandidate::new(candidate_id, &string) }) @@ -383,11 +1008,11 @@ impl PickerDelegate for ToolchainSelectorDelegate { &self, ix: usize, selected: bool, - _window: &mut Window, - _: &mut Context>, + _: &mut Window, + cx: &mut Context>, ) -> Option { let mat = &self.matches[ix]; - let toolchain = &self.candidates.toolchains[mat.candidate_id]; + let (toolchain, scope) = &self.candidates[mat.candidate_id]; let label = toolchain.name.clone(); let path = Self::relativize_path(toolchain.path.clone(), &self.worktree_abs_path_root); @@ -399,8 +1024,9 @@ impl PickerDelegate for ToolchainSelectorDelegate { path_highlights.iter_mut().for_each(|index| { *index -= label.len(); }); + let id: SharedString = format!("toolchain-{ix}",).into(); Some( - ListItem::new(ix) + ListItem::new(id) .inset(true) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) @@ -409,7 +1035,89 @@ impl PickerDelegate for ToolchainSelectorDelegate { HighlightedLabel::new(path, path_highlights) .size(LabelSize::Small) .color(Color::Muted), - ), + ) + .when_some(scope.as_ref(), |this, scope| { + let id: SharedString = format!( + "delete-custom-toolchain-{}-{}", + toolchain.name, toolchain.path + ) + .into(); + let toolchain = toolchain.clone(); + let scope = scope.clone(); + + this.end_slot(IconButton::new(id, IconName::Trash)) + .on_click(cx.listener(move |this, _, _, cx| { + this.delegate.project.update(cx, |this, cx| { + this.remove_toolchain(toolchain.clone(), scope.clone(), cx) + }); + + this.delegate.matches.retain_mut(|m| { + if m.candidate_id == ix { + return false; + } else if m.candidate_id > ix { + m.candidate_id -= 1; + } + true + }); + + this.delegate.candidates = this + .delegate + .candidates + .iter() + .enumerate() + .filter_map(|(i, toolchain)| (ix != i).then_some(toolchain.clone())) + .collect(); + + if this.delegate.selected_index >= ix { + this.delegate.selected_index = + this.delegate.selected_index.saturating_sub(1); + } + cx.stop_propagation(); + cx.notify(); + })) + }), + ) + } + fn render_footer( + &self, + _window: &mut Window, + cx: &mut Context>, + ) -> Option { + Some( + v_flex() + .rounded_b_md() + .child(Divider::horizontal()) + .child( + h_flex() + .p_1p5() + .gap_0p5() + .justify_end() + .child( + Button::new("xd", self.add_toolchain_text.clone()) + .key_binding(KeyBinding::for_action_in( + &AddToolchain, + &self.focus_handle, + _window, + cx, + )) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(AddToolchain), cx) + }), + ) + .child( + Button::new("select", "Select") + .key_binding(KeyBinding::for_action_in( + &menu::Confirm, + &self.focus_handle, + _window, + cx, + )) + .on_click(|_, window, cx| { + window.dispatch_action(menu::Confirm.boxed_clone(), cx) + }), + ), + ) + .into_any_element(), ) } } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index ef5a86a2762510fbea6f6a1a5172953a0ea20f7d..d674f6dd4d56ba95a664ac7d9e4ebf25969e2125 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -9,7 +9,7 @@ use std::{ }; use anyhow::{Context as _, Result, bail}; -use collections::HashMap; +use collections::{HashMap, IndexSet}; use db::{ query, sqlez::{connection::Connection, domain::Domain}, @@ -18,16 +18,16 @@ use db::{ use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; -use language::{LanguageName, Toolchain}; +use language::{LanguageName, Toolchain, ToolchainScope}; use project::WorktreeId; use remote::{RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions}; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, - statement::{SqlType, Statement}, + statement::Statement, thread_safe_connection::ThreadSafeConnection, }; -use ui::{App, px}; +use ui::{App, SharedString, px}; use util::{ResultExt, maybe}; use uuid::Uuid; @@ -169,6 +169,7 @@ impl From for BreakpointStateWrapper<'static> { BreakpointStateWrapper(Cow::Owned(kind)) } } + impl StaticColumnCount for BreakpointStateWrapper<'_> { fn column_count() -> usize { 1 @@ -193,11 +194,6 @@ impl Column for BreakpointStateWrapper<'_> { } } -/// This struct is used to implement traits on Vec -#[derive(Debug)] -#[allow(dead_code)] -struct Breakpoints(Vec); - impl sqlez::bindable::StaticColumnCount for Breakpoint { fn column_count() -> usize { // Position, log message, condition message, and hit condition message @@ -246,26 +242,6 @@ impl Column for Breakpoint { } } -impl Column for Breakpoints { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let mut breakpoints = Vec::new(); - let mut index = start_index; - - loop { - match statement.column_type(index) { - Ok(SqlType::Null) => break, - _ => { - let (breakpoint, next_index) = Breakpoint::column(statement, index)?; - - breakpoints.push(breakpoint); - index = next_index; - } - } - } - Ok((Breakpoints(breakpoints), index)) - } -} - #[derive(Clone, Debug, PartialEq)] struct SerializedPixels(gpui::Pixels); impl sqlez::bindable::StaticColumnCount for SerializedPixels {} @@ -711,6 +687,18 @@ impl Domain for WorkspaceDb { CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(remote_connection_id, paths); ), + sql!(CREATE TABLE user_toolchains ( + remote_connection_id INTEGER, + workspace_id INTEGER NOT NULL, + worktree_id INTEGER NOT NULL, + relative_worktree_path TEXT NOT NULL, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + raw_json TEXT NOT NULL, + + PRIMARY KEY (workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) + ) STRICT;), ]; // Allow recovering from bad migration that was initially shipped to nightly @@ -831,6 +819,7 @@ impl WorkspaceDb { session_id: None, breakpoints: self.breakpoints(workspace_id), window_id, + user_toolchains: self.user_toolchains(workspace_id, remote_connection_id), }) } @@ -880,6 +869,73 @@ impl WorkspaceDb { } } + fn user_toolchains( + &self, + workspace_id: WorkspaceId, + remote_connection_id: Option, + ) -> BTreeMap> { + type RowKind = (WorkspaceId, u64, String, String, String, String, String); + + let toolchains: Vec = self + .select_bound(sql! { + SELECT workspace_id, worktree_id, relative_worktree_path, + language_name, name, path, raw_json + FROM user_toolchains WHERE remote_connection_id IS ?1 AND ( + workspace_id IN (0, ?2) + ) + }) + .and_then(|mut statement| { + (statement)((remote_connection_id.map(|id| id.0), workspace_id)) + }) + .unwrap_or_default(); + let mut ret = BTreeMap::<_, IndexSet<_>>::default(); + + for ( + _workspace_id, + worktree_id, + relative_worktree_path, + language_name, + name, + path, + raw_json, + ) in toolchains + { + // INTEGER's that are primary keys (like workspace ids, remote connection ids and such) start at 1, so we're safe to + let scope = if _workspace_id == WorkspaceId(0) { + debug_assert_eq!(worktree_id, u64::MAX); + debug_assert_eq!(relative_worktree_path, String::default()); + ToolchainScope::Global + } else { + debug_assert_eq!(workspace_id, _workspace_id); + debug_assert_eq!( + worktree_id == u64::MAX, + relative_worktree_path == String::default() + ); + + if worktree_id != u64::MAX && relative_worktree_path != String::default() { + ToolchainScope::Subproject( + WorktreeId::from_usize(worktree_id as usize), + Arc::from(relative_worktree_path.as_ref()), + ) + } else { + ToolchainScope::Project + } + }; + let Ok(as_json) = serde_json::from_str(&raw_json) else { + continue; + }; + let toolchain = Toolchain { + name: SharedString::from(name), + path: SharedString::from(path), + language_name: LanguageName::from_proto(language_name), + as_json, + }; + ret.entry(scope).or_default().insert(toolchain); + } + + ret + } + /// Saves a workspace using the worktree roots. Will garbage collect any workspaces /// that used this workspace previously pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) { @@ -935,6 +991,22 @@ impl WorkspaceDb { } } } + for (scope, toolchains) in workspace.user_toolchains { + for toolchain in toolchains { + let query = sql!(INSERT OR REPLACE INTO user_toolchains(remote_connection_id, workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)); + let (workspace_id, worktree_id, relative_worktree_path) = match scope { + ToolchainScope::Subproject(worktree_id, ref path) => (Some(workspace.id), Some(worktree_id), Some(path.to_string_lossy().into_owned())), + ToolchainScope::Project => (Some(workspace.id), None, None), + ToolchainScope::Global => (None, None, None), + }; + let args = (remote_connection_id, workspace_id.unwrap_or(WorkspaceId(0)), worktree_id.map_or(usize::MAX,|id| id.to_usize()), relative_worktree_path.unwrap_or_default(), + toolchain.language_name.as_ref().to_owned(), toolchain.name.to_string(), toolchain.path.to_string(), toolchain.as_json.to_string()); + if let Err(err) = conn.exec_bound(query)?(args) { + log::error!("{err}"); + continue; + } + } + } conn.exec_bound(sql!( DELETE @@ -1797,6 +1869,7 @@ mod tests { }, session_id: None, window_id: None, + user_toolchains: Default::default(), }; db.save_workspace(workspace.clone()).await; @@ -1917,6 +1990,7 @@ mod tests { }, session_id: None, window_id: None, + user_toolchains: Default::default(), }; db.save_workspace(workspace.clone()).await; @@ -1950,6 +2024,7 @@ mod tests { breakpoints: collections::BTreeMap::default(), session_id: None, window_id: None, + user_toolchains: Default::default(), }; db.save_workspace(workspace_without_breakpoint.clone()) @@ -2047,6 +2122,7 @@ mod tests { breakpoints: Default::default(), session_id: None, window_id: None, + user_toolchains: Default::default(), }; let workspace_2 = SerializedWorkspace { @@ -2061,6 +2137,7 @@ mod tests { breakpoints: Default::default(), session_id: None, window_id: None, + user_toolchains: Default::default(), }; db.save_workspace(workspace_1.clone()).await; @@ -2167,6 +2244,7 @@ mod tests { centered_layout: false, session_id: None, window_id: Some(999), + user_toolchains: Default::default(), }; db.save_workspace(workspace.clone()).await; @@ -2200,6 +2278,7 @@ mod tests { centered_layout: false, session_id: None, window_id: Some(1), + user_toolchains: Default::default(), }; let mut workspace_2 = SerializedWorkspace { @@ -2214,6 +2293,7 @@ mod tests { breakpoints: Default::default(), session_id: None, window_id: Some(2), + user_toolchains: Default::default(), }; db.save_workspace(workspace_1.clone()).await; @@ -2255,6 +2335,7 @@ mod tests { centered_layout: false, session_id: None, window_id: Some(3), + user_toolchains: Default::default(), }; db.save_workspace(workspace_3.clone()).await; @@ -2292,6 +2373,7 @@ mod tests { breakpoints: Default::default(), session_id: Some("session-id-1".to_owned()), window_id: Some(10), + user_toolchains: Default::default(), }; let workspace_2 = SerializedWorkspace { @@ -2306,6 +2388,7 @@ mod tests { breakpoints: Default::default(), session_id: Some("session-id-1".to_owned()), window_id: Some(20), + user_toolchains: Default::default(), }; let workspace_3 = SerializedWorkspace { @@ -2320,6 +2403,7 @@ mod tests { breakpoints: Default::default(), session_id: Some("session-id-2".to_owned()), window_id: Some(30), + user_toolchains: Default::default(), }; let workspace_4 = SerializedWorkspace { @@ -2334,6 +2418,7 @@ mod tests { breakpoints: Default::default(), session_id: None, window_id: None, + user_toolchains: Default::default(), }; let connection_id = db @@ -2359,6 +2444,7 @@ mod tests { breakpoints: Default::default(), session_id: Some("session-id-2".to_owned()), window_id: Some(50), + user_toolchains: Default::default(), }; let workspace_6 = SerializedWorkspace { @@ -2373,6 +2459,7 @@ mod tests { centered_layout: false, session_id: Some("session-id-3".to_owned()), window_id: Some(60), + user_toolchains: Default::default(), }; db.save_workspace(workspace_1.clone()).await; @@ -2424,6 +2511,7 @@ mod tests { centered_layout: false, session_id: None, window_id: None, + user_toolchains: Default::default(), } } @@ -2458,6 +2546,7 @@ mod tests { session_id: Some("one-session".to_owned()), breakpoints: Default::default(), window_id: Some(window_id), + user_toolchains: Default::default(), }) .collect::>(); @@ -2555,6 +2644,7 @@ mod tests { session_id: Some("one-session".to_owned()), breakpoints: Default::default(), window_id: Some(window_id), + user_toolchains: Default::default(), }) .collect::>(); diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 005a1ba2347f8ac3847199ad4564d8ca45420f4a..08a2f2e38dd142848f8a9c07652e147b58bee233 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -5,12 +5,14 @@ use crate::{ }; use anyhow::Result; use async_recursion::async_recursion; +use collections::IndexSet; use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use gpui::{AsyncWindowContext, Entity, WeakEntity}; +use language::{Toolchain, ToolchainScope}; use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; use remote::RemoteConnectionOptions; use std::{ @@ -57,6 +59,7 @@ pub(crate) struct SerializedWorkspace { pub(crate) docks: DockStructure, pub(crate) session_id: Option, pub(crate) breakpoints: BTreeMap, Vec>, + pub(crate) user_toolchains: BTreeMap>, pub(crate) window_id: Option, } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6b4e7c1731b23e2e35086431d4d83bda4958d33f..58373b5d1a30a431106282d26589aa09694d3382 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -73,6 +73,7 @@ use postage::stream::Stream; use project::{ DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId, debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, + toolchain_store::ToolchainStoreEvent, }; use remote::{RemoteClientDelegate, RemoteConnectionOptions, remote_client::ConnectionIdentifier}; use schemars::JsonSchema; @@ -1275,6 +1276,19 @@ impl Workspace { }, ) .detach(); + if let Some(toolchain_store) = project.read(cx).toolchain_store() { + cx.subscribe_in( + &toolchain_store, + window, + |workspace, _, event, window, cx| match event { + ToolchainStoreEvent::CustomToolchainsModified => { + workspace.serialize_workspace(window, cx); + } + _ => {} + }, + ) + .detach(); + } cx.on_focus_lost(window, |this, window, cx| { let focus_handle = this.focus_handle(cx); @@ -1565,6 +1579,16 @@ impl Workspace { })? .await; } + if let Some(workspace) = serialized_workspace.as_ref() { + project_handle.update(cx, |this, cx| { + for (scope, toolchains) in &workspace.user_toolchains { + for toolchain in toolchains { + this.add_toolchain(toolchain.clone(), scope.clone(), cx); + } + } + })?; + } + let window = if let Some(window) = requesting_window { let centered_layout = serialized_workspace .as_ref() @@ -5240,10 +5264,16 @@ impl Workspace { .read(cx) .all_source_breakpoints(cx) }); + let user_toolchains = self + .project + .read(cx) + .user_toolchains(cx) + .unwrap_or_default(); let center_group = build_serialized_pane_group(&self.center.root, window, cx); let docks = build_serialized_docks(self, window, cx); let window_bounds = Some(SerializedWindowBounds(window.window_bounds())); + let serialized_workspace = SerializedWorkspace { id: database_id, location, @@ -5256,6 +5286,7 @@ impl Workspace { session_id: self.session_id.clone(), breakpoints, window_id: Some(window.window_handle().window_id().as_u64()), + user_toolchains, }; window.spawn(cx, async move |_| { From a6a111cadd34d79ef55d5b70d563178bbaffd965 Mon Sep 17 00:00:00 2001 From: chbk Date: Sat, 6 Sep 2025 01:36:36 +0200 Subject: [PATCH 623/744] Highlight labels in Go (#37673) Release Notes: - Highlight labels in Go | Zed 0.202.7 | With this PR | | --- | --- | | go-0 202 7 | go-pr | --- crates/languages/src/go/highlights.scm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/languages/src/go/highlights.scm b/crates/languages/src/go/highlights.scm index bb0eaab88a1c0c79a04496d453831cf396d706b6..5d630cbdfc746b56320cd5083222897d84dbf528 100644 --- a/crates/languages/src/go/highlights.scm +++ b/crates/languages/src/go/highlights.scm @@ -4,6 +4,8 @@ (field_identifier) @property (package_identifier) @namespace +(label_name) @label + (keyed_element . (literal_element From 23dc1f5ea4061591ed44121e8a4ba191f7e7b647 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Sep 2025 18:09:50 -0700 Subject: [PATCH 624/744] Disable foreign keys in sqlite when running migrations (#37572) Closes #37473 ### Background Previously, we enabled foreign keys at all times for our sqlite database that we use for client-side state. The problem with this is that In sqlite, `alter table` is somewhat limited, so for many migrations, you must *recreate* the table: create a new table called e.g. `workspace__2`, then copy all of the data from `workspaces` into `workspace__2`, then delete the old `workspaces` table and rename `workspaces__2` to `workspaces`. The way foreign keys work in sqlite, when we delete the old table, all of its associated records in other tables will be deleted due to `on delete cascade` clauses. Unfortunately, one of the types of associated records that can be deleted are `editors`, which sometimes store unsaved text. It is very bad to delete these records, as they are the *only* place that this unsaved text is stored. This has already happened multiple times as we have migrated tables as we develop Zed, but I caused it to happened again in https://github.com/zed-industries/zed/pull/36714. ### The Fix The Sqlite docs recommend a multi-step approach to migrations where you: * disable foreign keys * start a transaction * create a new table * populate the new table with data from the old table * delete the old table * rename the new table to the old name * run a foreign key check * if it passes, commit the transaction * enable foreign keys In this PR, I've adjusted our sqlite migration code path to follow this pattern more closely. Specifically, we disable foreign key checks before running migrations, run a foreign key check before committing, and then enable foreign key checks after the migrations are done. In addition, I've added a generic query that we run *before* running the foreign key check that explicitly deletes any rows that have dangling foreign keys. This way, we avoid failing the migration (and breaking the app) if a migration deletes data that *does* cause associated records to need to be deleted. But now, in the common case where we migrate old data in the new table and keep the ids, all of the associated data will be preserved. Release Notes: - Fixed a bug where workspace state would be lost when upgrading from Zed 0.201.x. or below. --- Cargo.lock | 1 + crates/sqlez/Cargo.toml | 1 + crates/sqlez/src/migrations.rs | 51 ++++++++++++++++++++-- crates/sqlez/src/thread_safe_connection.rs | 11 +++++ crates/workspace/src/persistence.rs | 3 ++ 5 files changed, 64 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 295c3a83c52e3b355a8e43e9d36c09149fdc694f..f4c94f8078b1ab392ed1a50e15c71dab1921f0a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15347,6 +15347,7 @@ dependencies = [ "futures 0.3.31", "indoc", "libsqlite3-sys", + "log", "parking_lot", "smol", "sqlformat", diff --git a/crates/sqlez/Cargo.toml b/crates/sqlez/Cargo.toml index 16a3adebae24e0573f9e7ada18bc9259ff588ad1..6eb75aa171979283325d22300f95d584cee2cffb 100644 --- a/crates/sqlez/Cargo.toml +++ b/crates/sqlez/Cargo.toml @@ -14,6 +14,7 @@ collections.workspace = true futures.workspace = true indoc.workspace = true libsqlite3-sys.workspace = true +log.workspace = true parking_lot.workspace = true smol.workspace = true sqlformat.workspace = true diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index 2429ddeb4127591b56fb74a9c84884d9dc5f378f..567d82f9afe22ea4ab126c0989891c5d603879fd 100644 --- a/crates/sqlez/src/migrations.rs +++ b/crates/sqlez/src/migrations.rs @@ -59,6 +59,7 @@ impl Connection { let mut store_completed_migration = self .exec_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?; + let mut did_migrate = false; for (index, migration) in migrations.iter().enumerate() { let migration = sqlformat::format(migration, &sqlformat::QueryParams::None, Default::default()); @@ -70,9 +71,7 @@ impl Connection { &sqlformat::QueryParams::None, Default::default(), ); - if completed_migration == migration - || migration.trim().starts_with("-- ALLOW_MIGRATION_CHANGE") - { + if completed_migration == migration { // Migration already run. Continue continue; } else if should_allow_migration_change(index, &completed_migration, &migration) @@ -91,12 +90,58 @@ impl Connection { } self.eager_exec(&migration)?; + did_migrate = true; store_completed_migration((domain, index, migration))?; } + if did_migrate { + self.delete_rows_with_orphaned_foreign_key_references()?; + self.exec("PRAGMA foreign_key_check;")?()?; + } + Ok(()) }) } + + /// Delete any rows that were orphaned by a migration. This is needed + /// because we disable foreign key constraints during migrations, so + /// that it's possible to re-create a table with the same name, without + /// deleting all associated data. + fn delete_rows_with_orphaned_foreign_key_references(&self) -> Result<()> { + let foreign_key_info: Vec<(String, String, String, String)> = self.select( + r#" + SELECT DISTINCT + schema.name as child_table, + foreign_keys.[from] as child_key, + foreign_keys.[table] as parent_table, + foreign_keys.[to] as parent_key + FROM sqlite_schema schema + JOIN pragma_foreign_key_list(schema.name) foreign_keys + WHERE + schema.type = 'table' AND + schema.name NOT LIKE "sqlite_%" + "#, + )?()?; + + if !foreign_key_info.is_empty() { + log::info!( + "Found {} foreign key relationships to check", + foreign_key_info.len() + ); + } + + for (child_table, child_key, parent_table, parent_key) in foreign_key_info { + self.exec(&format!( + " + DELETE FROM {child_table} + WHERE {child_key} IS NOT NULL and {child_key} NOT IN + (SELECT {parent_key} FROM {parent_table}) + " + ))?()?; + } + + Ok(()) + } } #[cfg(test)] diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index 58d3afe78fb4d8b211c48c0ae1f9f72af74ad5c1..482905ac817bf94fcb64cb858b784c94283b686c 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -95,6 +95,14 @@ impl ThreadSafeConnectionBuilder { let mut migration_result = anyhow::Result::<()>::Err(anyhow::anyhow!("Migration never run")); + let foreign_keys_enabled: bool = + connection.select_row::("PRAGMA foreign_keys")?() + .unwrap_or(None) + .map(|enabled| enabled != 0) + .unwrap_or(false); + + connection.exec("PRAGMA foreign_keys = OFF;")?()?; + for _ in 0..MIGRATION_RETRIES { migration_result = connection .with_savepoint("thread_safe_multi_migration", || M::migrate(connection)); @@ -104,6 +112,9 @@ impl ThreadSafeConnectionBuilder { } } + if foreign_keys_enabled { + connection.exec("PRAGMA foreign_keys = ON;")?()?; + } migration_result }) .await?; diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index d674f6dd4d56ba95a664ac7d9e4ebf25969e2125..797c4796830ff767a0213058c417bb3a764c6bec 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -699,6 +699,9 @@ impl Domain for WorkspaceDb { PRIMARY KEY (workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) ) STRICT;), + sql!( + DROP TABLE ssh_connections; + ), ]; // Allow recovering from bad migration that was initially shipped to nightly From 47a475681f43718d750b484af9c3cc189c1ac58e Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Fri, 5 Sep 2025 22:22:55 -0600 Subject: [PATCH 625/744] Optimize Chunks::seek when offset is in current chunk (#37659) Release Notes: - N/A --- crates/rope/src/rope.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 33886854220862c60153dc3ea1f02180c62212a3..9185b5baa300af93ec7ceb3e951ae6ba71772721 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -639,18 +639,20 @@ impl<'a> Chunks<'a> { pub fn seek(&mut self, mut offset: usize) { offset = offset.clamp(self.range.start, self.range.end); - let bias = if self.reversed { - Bias::Left + if self.reversed { + if offset > self.chunks.end() { + self.chunks.seek_forward(&offset, Bias::Left); + } else if offset <= *self.chunks.start() { + self.chunks.seek(&offset, Bias::Left); + } } else { - Bias::Right + if offset >= self.chunks.end() { + self.chunks.seek_forward(&offset, Bias::Right); + } else if offset < *self.chunks.start() { + self.chunks.seek(&offset, Bias::Right); + } }; - if offset >= self.chunks.end() { - self.chunks.seek_forward(&offset, bias); - } else { - self.chunks.seek(&offset, bias); - } - self.offset = offset; } From 8c9442ad11691004278607d83a4205807c644e82 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Sat, 6 Sep 2025 10:46:08 +0530 Subject: [PATCH 626/744] language_models: Skip empty delta text content in OpenAI and OpenAI compatible provider (#37626) Closes #37302 Related: #37614 In case of open_ai_compatible providers like Zhipu AI and z.ai they return empty content along with usage data. below is the example json captured from z.ai. We now ignore empty content returned by providers now to avoid this issue where we would return the same empty content back to provider which would error out. ``` OpenAI Stream Response JSON: { "id": "2025090518465610d80dc21e66426d", "created": 1757069216, "model": "glm-4.5", "choices": [ { "index": 0, "finish_reason": "tool_calls", "delta": { "role": "assistant", "content": "" } } ], "usage": { "prompt_tokens": 7882, "completion_tokens": 150, "total_tokens": 8032, "prompt_tokens_details": { "cached_tokens": 7881 } } } ``` Release Notes: - Skip empty delta text content in OpenAI and OpenAI compatible provider Signed-off-by: Umesh Yadav --- crates/language_models/src/provider/open_ai.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 4348fd42110b2554de801b812a7b001dc49ad06e..cfd43033515e2c3527c8d0dfbf1267fb96793819 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -586,7 +586,9 @@ impl OpenAiEventMapper { }; if let Some(content) = choice.delta.content.clone() { - events.push(Ok(LanguageModelCompletionEvent::Text(content))); + if !content.is_empty() { + events.push(Ok(LanguageModelCompletionEvent::Text(content))); + } } if let Some(tool_calls) = choice.delta.tool_calls.as_ref() { From 1f37fbd0511fcafd6b39e9de4d6d6db7244453dd Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Sat, 6 Sep 2025 11:12:15 +0530 Subject: [PATCH 627/744] language_models: Use `/models/user` for fetching OpenRouter models (#37534) This PR switches the OpenRouter integration from fetching all models to fetching only the models specified in the user's account preferences. This will help improve the experience **The Problem** The previous implementation used the `/models` endpoint, which returned an exhaustive list of all models supported by OpenRouter. This resulted in a long and cluttered model selection dropdown in Zed, making it difficult for users to find the models they actually use. **The Solution** We now use the `/models/user` endpoint. This API call returns a curated list based on the models and providers the user has selected in their [OpenRouter dashboard](https://openrouter.ai/models). Ref: [OpenRouter API Docs for User-Filtered Models](https://openrouter.ai/docs/api-reference/list-models-filtered-by-user-provider-preferences) Release Notes: - language_models: Support OpenRouter user preferences for available models --- .../src/provider/open_router.rs | 25 +++++++++++++++---- crates/open_router/src/open_router.rs | 8 ++++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 9138f6b82e7e74e9e6a7468306b2f5cf6768987e..f73a97e6426f80e1ad8d1b8214e16bf361d0f0ce 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -92,7 +92,7 @@ pub struct State { api_key_from_env: bool, http_client: Arc, available_models: Vec, - fetch_models_task: Option>>, + fetch_models_task: Option>>, settings: OpenRouterSettings, _subscription: Subscription, } @@ -178,20 +178,35 @@ impl State { }) } - fn fetch_models(&mut self, cx: &mut Context) -> Task> { + fn fetch_models( + &mut self, + cx: &mut Context, + ) -> Task> { let settings = &AllLanguageModelSettings::get_global(cx).open_router; let http_client = self.http_client.clone(); let api_url = settings.api_url.clone(); - + let Some(api_key) = self.api_key.clone() else { + return Task::ready(Err(LanguageModelCompletionError::NoApiKey { + provider: PROVIDER_NAME, + })); + }; cx.spawn(async move |this, cx| { - let models = list_models(http_client.as_ref(), &api_url) + let models = list_models(http_client.as_ref(), &api_url, &api_key) .await - .map_err(|e| anyhow::anyhow!("OpenRouter error: {:?}", e))?; + .map_err(|e| { + LanguageModelCompletionError::Other(anyhow::anyhow!( + "OpenRouter error: {:?}", + e + )) + })?; this.update(cx, |this, cx| { this.available_models = models; cx.notify(); }) + .map_err(|e| LanguageModelCompletionError::Other(e))?; + + Ok(()) }) } diff --git a/crates/open_router/src/open_router.rs b/crates/open_router/src/open_router.rs index dfaa49746d093810924f744cd1aeb3e8747ddb00..cbc6c243d87c8f9ea3d0186dbecb8f0ac2e10a90 100644 --- a/crates/open_router/src/open_router.rs +++ b/crates/open_router/src/open_router.rs @@ -529,12 +529,16 @@ pub async fn stream_completion( pub async fn list_models( client: &dyn HttpClient, api_url: &str, + api_key: &str, ) -> Result, OpenRouterError> { - let uri = format!("{api_url}/models"); + let uri = format!("{api_url}/models/user"); let request_builder = HttpRequest::builder() .method(Method::GET) .uri(uri) - .header("Accept", "application/json"); + .header("Accept", "application/json") + .header("Authorization", format!("Bearer {}", api_key)) + .header("HTTP-Referer", "https://zed.dev") + .header("X-Title", "Zed Editor"); let request = request_builder .body(AsyncBody::default()) From 777ce7cc97b161feef64f67481b76415a2d848b4 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 6 Sep 2025 10:37:59 +0300 Subject: [PATCH 628/744] Fixed LSP binary info not being shown in full (#37682) Follow-up of https://github.com/zed-industries/zed/pull/37083 Closes https://github.com/zed-industries/zed/issues/37677 Release Notes: - Fixed LSP binary info not being shown in full --- crates/language_tools/src/lsp_log_view.rs | 18 ++++++++++++------ crates/project/src/lsp_store/log_store.rs | 14 -------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/crates/language_tools/src/lsp_log_view.rs b/crates/language_tools/src/lsp_log_view.rs index b1f1e5c4f62b4c14b88cdd3de27a1624c7c7158f..fb63ab9a99147328c4987bd80b698ef4a477f013 100644 --- a/crates/language_tools/src/lsp_log_view.rs +++ b/crates/language_tools/src/lsp_log_view.rs @@ -325,7 +325,7 @@ impl LspLogView { let server_info = format!( "* Server: {NAME} (id {ID}) -* Binary: {BINARY:#?} +* Binary: {BINARY} * Registered workspace folders: {WORKSPACE_FOLDERS} @@ -335,10 +335,10 @@ impl LspLogView { * Configuration: {CONFIGURATION}", NAME = info.name, ID = info.id, - BINARY = info.binary.as_ref().map_or_else( - || "Unknown".to_string(), - |bin| bin.path.as_path().to_string_lossy().to_string() - ), + BINARY = info + .binary + .as_ref() + .map_or_else(|| "Unknown".to_string(), |binary| format!("{binary:#?}")), WORKSPACE_FOLDERS = info.workspace_folders.join(", "), CAPABILITIES = serde_json::to_string_pretty(&info.capabilities) .unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")), @@ -990,10 +990,16 @@ impl Render for LspLogToolbarItemView { let server_id = server.server_id; let rpc_trace_enabled = server.rpc_trace_enabled; let log_view = log_view.clone(); + let label = match server.selected_entry { + LogKind::Rpc => RPC_MESSAGES, + LogKind::Trace => SERVER_TRACE, + LogKind::Logs => SERVER_LOGS, + LogKind::ServerInfo => SERVER_INFO, + }; PopoverMenu::new("LspViewSelector") .anchor(Corner::TopLeft) .trigger( - Button::new("language_server_menu_header", server.selected_entry.label()) + Button::new("language_server_menu_header", label) .icon(IconName::ChevronDown) .icon_size(IconSize::Small) .icon_color(Color::Muted), diff --git a/crates/project/src/lsp_store/log_store.rs b/crates/project/src/lsp_store/log_store.rs index 67a20dd6cd8b2f5d6ca48d7790fc0b2e60aff370..00098712bf0092a6795de2ed48c7ccf15925c555 100644 --- a/crates/project/src/lsp_store/log_store.rs +++ b/crates/project/src/lsp_store/log_store.rs @@ -16,11 +16,6 @@ const SEND_LINE: &str = "\n// Send:"; const RECEIVE_LINE: &str = "\n// Receive:"; const MAX_STORED_LOG_ENTRIES: usize = 2000; -const RPC_MESSAGES: &str = "RPC Messages"; -const SERVER_LOGS: &str = "Server Logs"; -const SERVER_TRACE: &str = "Server Trace"; -const SERVER_INFO: &str = "Server Info"; - pub fn init(on_headless_host: bool, cx: &mut App) -> Entity { let log_store = cx.new(|cx| LogStore::new(on_headless_host, cx)); cx.set_global(GlobalLogStore(log_store.clone())); @@ -216,15 +211,6 @@ impl LogKind { LanguageServerLogType::Rpc { .. } => Self::Rpc, } } - - pub fn label(&self) -> &'static str { - match self { - LogKind::Rpc => RPC_MESSAGES, - LogKind::Trace => SERVER_TRACE, - LogKind::Logs => SERVER_LOGS, - LogKind::ServerInfo => SERVER_INFO, - } - } } impl LogStore { From 1d828b6ac6028e5ffa421a14b08172ec86bd175d Mon Sep 17 00:00:00 2001 From: Marco Groot <60631182+marcogroot@users.noreply.github.com> Date: Sat, 6 Sep 2025 20:29:34 +1000 Subject: [PATCH 629/744] Fix broken link in `CONTRIBUTING.md` (#37688) Can see currently the link is dead currently, but this changes fixes locally https://github.com/user-attachments/assets/e01d9c47-e91e-4c24-8285-01e3b45583b9 Release Notes: - N/A --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 407ba002c7bc5a75c922faa72f1f270c62e82410..1c0b1e363ed0f04ff33c070a4a84815cece78545 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,7 +65,7 @@ If you would like to add a new icon to the Zed icon theme, [open a Discussion](h ## Bird's-eye view of Zed -We suggest you keep the [zed glossary](docs/src/development/GLOSSARY.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase. +We suggest you keep the [zed glossary](docs/src/development/glossary.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase. Zed is made up of several smaller crates - let's go over those you're most likely to interact with: From 065518577eb589cad60945c69428494f0ed01757 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 6 Sep 2025 16:37:21 +0300 Subject: [PATCH 630/744] Fix the tasks docs (#37699) Closes https://github.com/zed-industries/zed/issues/37698 Release Notes: - N/A --- docs/src/tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/tasks.md b/docs/src/tasks.md index bff3eac86048752be50f8fd605bc5b76677ca0c0..e530f568cdce0fb8e1da059b4b841fac7049e8fd 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -45,7 +45,7 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to // Whether to show the task line in the output of the spawned task, defaults to `true`. "show_summary": true, // Whether to show the command line in the output of the spawned task, defaults to `true`. - "show_output": true + "show_command": true // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. // "tags": [] } From 84f166fc85e1675fb76183ad6f212891d596c38d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 6 Sep 2025 16:39:21 +0300 Subject: [PATCH 631/744] Tweak word completions more (#37697) Follow-up of https://github.com/zed-industries/zed/pull/37352 Closes https://github.com/zed-industries/zed/issues/37132 * disabled word completions in the agent panel's editor * if not disabled, allow to trigger word completions with an action even if the completions threshold is not reached Release Notes: - Fixed word completions appearing in the agent panel's editor and not appearing when triggered with the action before the completion threshold is reached --- crates/agent_ui/src/message_editor.rs | 1 + crates/editor/src/code_context_menus.rs | 2 +- crates/editor/src/editor.rs | 42 ++++++++++++---- crates/editor/src/editor_tests.rs | 64 +++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 9 deletions(-) diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 45e7529ec21c576354a556bdc27112da4d57e085..6f0ad2767a46fb23b40e0116fd9cf85f06c28aca 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -125,6 +125,7 @@ pub(crate) fn create_editor( cx, ); editor.set_placeholder_text("Message the agent – @ to include context", cx); + editor.disable_word_completions(); editor.set_show_indent_guides(false, cx); editor.set_soft_wrap(); editor.set_use_modal_editing(true); diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 6d57048985955730bef2c7840d645c87b56915fc..18fce84dbca9cfe845d0912295ed22929ccb9cf7 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -251,7 +251,7 @@ enum MarkdownCacheKey { pub enum CompletionsMenuSource { Normal, SnippetChoices, - Words, + Words { ignore_threshold: bool }, } // TODO: There should really be a wrapper around fuzzy match tasks that does this. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2374c8d6875f05608aa800de660fb3602ed35988..b1f9bde6ddba09a77ca386793847552f85d5be96 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1030,6 +1030,7 @@ pub struct Editor { inline_diagnostics_update: Task<()>, inline_diagnostics_enabled: bool, diagnostics_enabled: bool, + word_completions_enabled: bool, inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>, soft_wrap_mode_override: Option, hard_wrap: Option, @@ -2163,6 +2164,7 @@ impl Editor { }, inline_diagnostics_enabled: full_mode, diagnostics_enabled: full_mode, + word_completions_enabled: full_mode, inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints), inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), gutter_hovered: false, @@ -4892,8 +4894,15 @@ impl Editor { }); match completions_source { - Some(CompletionsMenuSource::Words) => { - self.show_word_completions(&ShowWordCompletions, window, cx) + Some(CompletionsMenuSource::Words { .. }) => { + self.open_or_update_completions_menu( + Some(CompletionsMenuSource::Words { + ignore_threshold: false, + }), + None, + window, + cx, + ); } Some(CompletionsMenuSource::Normal) | Some(CompletionsMenuSource::SnippetChoices) @@ -5401,7 +5410,14 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.open_or_update_completions_menu(Some(CompletionsMenuSource::Words), None, window, cx); + self.open_or_update_completions_menu( + Some(CompletionsMenuSource::Words { + ignore_threshold: true, + }), + None, + window, + cx, + ); } pub fn show_completions( @@ -5450,9 +5466,13 @@ impl Editor { drop(multibuffer_snapshot); + let mut ignore_word_threshold = false; let provider = match requested_source { Some(CompletionsMenuSource::Normal) | None => self.completion_provider.clone(), - Some(CompletionsMenuSource::Words) => None, + Some(CompletionsMenuSource::Words { ignore_threshold }) => { + ignore_word_threshold = ignore_threshold; + None + } Some(CompletionsMenuSource::SnippetChoices) => { log::error!("bug: SnippetChoices requested_source is not handled"); None @@ -5573,10 +5593,12 @@ impl Editor { .as_ref() .is_none_or(|query| !query.chars().any(|c| c.is_digit(10))); - let omit_word_completions = match &query { - Some(query) => query.chars().count() < completion_settings.words_min_length, - None => completion_settings.words_min_length != 0, - }; + let omit_word_completions = !self.word_completions_enabled + || (!ignore_word_threshold + && match &query { + Some(query) => query.chars().count() < completion_settings.words_min_length, + None => completion_settings.words_min_length != 0, + }); let (mut words, provider_responses) = match &provider { Some(provider) => { @@ -17121,6 +17143,10 @@ impl Editor { self.inline_diagnostics.clear(); } + pub fn disable_word_completions(&mut self) { + self.word_completions_enabled = false; + } + pub fn diagnostics_enabled(&self) -> bool { self.diagnostics_enabled && self.mode.is_full() } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index f4569b436488728f197183b27c63b2706881c8cb..36405079b8e241ed068eb32289d64064f16df39c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -14278,6 +14278,26 @@ async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppCont } }); + cx.update_editor(|editor, window, cx| { + editor.show_word_completions(&ShowWordCompletions, window, cx); + }); + cx.executor().run_until_parked(); + cx.update_editor(|editor, window, cx| { + if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() + { + assert_eq!(completion_menu_entries(menu), &["wowser", "wowen", "wow"], "Even though the threshold is not met, invoking word completions with an action should provide the completions"); + } else { + panic!("expected completion menu to be open after the word completions are called with an action"); + } + + editor.cancel(&Cancel, window, cx); + }); + cx.update_editor(|editor, _, _| { + if editor.context_menu.borrow_mut().is_some() { + panic!("expected completion menu to be hidden after canceling"); + } + }); + cx.simulate_keystroke("o"); cx.executor().run_until_parked(); cx.update_editor(|editor, _, _| { @@ -14300,6 +14320,50 @@ async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppCont }); } +#[gpui::test] +async fn test_word_completions_disabled(cx: &mut TestAppContext) { + init_test(cx, |language_settings| { + language_settings.defaults.completions = Some(CompletionSettings { + words: WordsCompletionMode::Enabled, + words_min_length: 0, + lsp: true, + lsp_fetch_timeout_ms: 0, + lsp_insert_mode: LspInsertMode::Insert, + }); + }); + + let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await; + cx.update_editor(|editor, _, _| { + editor.disable_word_completions(); + }); + cx.set_state(indoc! {"ˇ + wow + wowen + wowser + "}); + cx.simulate_keystroke("w"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _, _| { + if editor.context_menu.borrow_mut().is_some() { + panic!( + "expected completion menu to be hidden, as words completion are disabled for this editor" + ); + } + }); + + cx.update_editor(|editor, window, cx| { + editor.show_word_completions(&ShowWordCompletions, window, cx); + }); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _, _| { + if editor.context_menu.borrow_mut().is_some() { + panic!( + "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor" + ); + } + }); +} + fn gen_text_edit(params: &CompletionParams, text: &str) -> Option { let position = || lsp::Position { line: params.text_document_position.position.line, From e04473dd2612a63504aca730b3f5a51ad72fdc2c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 6 Sep 2025 19:51:51 +0300 Subject: [PATCH 632/744] Revert "gpui: Skip `test` attribute expansion for rust-analyzer (#37611)" (#37705) This reverts commit 4124bedab796d2ac0a1e57f8b94f72500969797a. With the new annotation, r-a starts to skip the tasks that are marked with `gpui::test` and when it fully loads, it starts to return module-only tasks: https://github.com/user-attachments/assets/5af3e3e4-91b7-4f19-aab0-ed7f186e5f74 Release Notes: - N/A --- crates/gpui/src/gpui.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 0858cb014e33da354eb8a6488982b913b76d2b52..3c4ee41c16ab7cfc5e42007291e330282b330ecb 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -121,14 +121,6 @@ mod seal { pub trait Sealed {} } -// This allows r-a to skip expanding the gpui test macro which should -// reduce resource usage a bit as the test attribute is special cased -// to be treated as a no-op. -#[cfg(rust_analyzer)] -pub use core::prelude::v1::test; -#[cfg(not(rust_analyzer))] -pub use gpui_macros::test; - pub use action::*; pub use anyhow::Result; pub use app::*; @@ -142,7 +134,7 @@ pub use elements::*; pub use executor::*; pub use geometry::*; pub use global::*; -pub use gpui_macros::{AppContext, IntoElement, Render, VisualContext, register_action}; +pub use gpui_macros::{AppContext, IntoElement, Render, VisualContext, register_action, test}; pub use http_client; pub use input::*; pub use inspector::*; From 1552afd8bf8da873e43e31088e2c56bfdaf691c7 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sat, 6 Sep 2025 15:38:48 -0400 Subject: [PATCH 633/744] docs: Use `#action` throughout `configuring-zed.md` (#37709) Release Notes: - N/A --- docs/src/configuring-zed.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index e245b3ca2facecb097b315f28d98ef2ea5a20048..56b4de832862439b93d8a0359dbf8284226e1671 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -116,7 +116,7 @@ Non-negative `float` values ## Allow Rewrap -- Description: Controls where the `editor::Rewrap` action is allowed in the current language scope +- Description: Controls where the {#action editor::Rewrap} action is allowed in the current language scope - Setting: `allow_rewrap` - Default: `"in_comments"` @@ -2192,7 +2192,7 @@ Example: ## Go to Definition Fallback -- Description: What to do when the "go to definition" action fails to find a definition +- Description: What to do when the {#action editor::GoToDefinition} action fails to find a definition - Setting: `go_to_definition_fallback` - Default: `"find_all_references"` @@ -2383,7 +2383,7 @@ Example: **Options** -Run the `icon theme selector: toggle` action in the command palette to see a current list of valid icon themes names. +Run the {#action icon_theme_selector::Toggle} action in the command palette to see a current list of valid icon themes names. ### Light @@ -2393,7 +2393,7 @@ Run the `icon theme selector: toggle` action in the command palette to see a cur **Options** -Run the `icon theme selector: toggle` action in the command palette to see a current list of valid icon themes names. +Run the {#action icon_theme_selector::Toggle} action in the command palette to see a current list of valid icon themes names. ## Image Viewer @@ -2832,7 +2832,7 @@ Configuration object for defining settings profiles. Example: - Double-clicking on the file - Double-clicking on the tab header - - Using the `project_panel::OpenPermanent` action + - Using the {#action project_panel::OpenPermanent} action - Editing the file - Dragging the file to a different pane @@ -4053,7 +4053,7 @@ Example command to set the title: `echo -e "\e]2;New Title\007";` **Options** -Run the `theme selector: toggle` action in the command palette to see a current list of valid themes names. +Run the {#action theme_selector::Toggle} action in the command palette to see a current list of valid themes names. ### Light @@ -4063,7 +4063,7 @@ Run the `theme selector: toggle` action in the command palette to see a current **Options** -Run the `theme selector: toggle` action in the command palette to see a current list of valid themes names. +Run the {#action theme_selector::Toggle} action in the command palette to see a current list of valid themes names. ## Title Bar From 5c30578c4961d3766ee991b119658b02741b85e5 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Sun, 7 Sep 2025 02:01:55 +0530 Subject: [PATCH 634/744] linux: Fix IME preedit text not showing in Terminal on Wayland (#37701) Closes https://github.com/zed-industries/zed/issues/37268 Release Notes: - Fixed an issue where IME preedit text was not showing in the Terminal on Wayland. --- crates/terminal_view/src/terminal_element.rs | 12 ++++----- crates/terminal_view/src/terminal_view.rs | 28 ++++++++++++-------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 5bbf5ad36b3de89514d92ce9e305988817cec32f..a786aa20e60f28b1f22bd1c9e8d993098aa96de4 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1192,8 +1192,8 @@ impl Element for TerminalElement { bounds.origin + Point::new(layout.gutter, px(0.)) - Point::new(px(0.), scroll_top); let marked_text_cloned: Option = { - let ime_state = self.terminal_view.read(cx); - ime_state.marked_text.clone() + let ime_state = &self.terminal_view.read(cx).ime_state; + ime_state.as_ref().map(|state| state.marked_text.clone()) }; let terminal_input_handler = TerminalInputHandler { @@ -1421,11 +1421,9 @@ impl InputHandler for TerminalInputHandler { _window: &mut Window, cx: &mut App, ) { - if let Some(range) = new_marked_range { - self.terminal_view.update(cx, |view, view_cx| { - view.set_marked_text(new_text.to_string(), range, view_cx); - }); - } + self.terminal_view.update(cx, |view, view_cx| { + view.set_marked_text(new_text.to_string(), new_marked_range, view_cx); + }); } fn unmark_text(&mut self, _window: &mut Window, cx: &mut App) { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 2548a7c24460be3161147b69e30c6191ba5dd2e6..08caf9a4ef1c0b49dbfa8f8f2578f00ddb130ee0 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -62,6 +62,11 @@ use std::{ time::Duration, }; +struct ImeState { + marked_text: String, + marked_range_utf16: Option>, +} + const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const TERMINAL_SCROLLBAR_WIDTH: Pixels = px(12.); @@ -138,8 +143,7 @@ pub struct TerminalView { scroll_handle: TerminalScrollHandle, show_scrollbar: bool, hide_scrollbar_task: Option>, - marked_text: Option, - marked_range_utf16: Option>, + ime_state: Option, _subscriptions: Vec, _terminal_subscriptions: Vec, } @@ -263,8 +267,7 @@ impl TerminalView { show_scrollbar: !Self::should_autohide_scrollbar(cx), hide_scrollbar_task: None, cwd_serialized: false, - marked_text: None, - marked_range_utf16: None, + ime_state: None, _subscriptions: vec![ focus_in, focus_out, @@ -323,24 +326,27 @@ impl TerminalView { pub(crate) fn set_marked_text( &mut self, text: String, - range: Range, + range: Option>, cx: &mut Context, ) { - self.marked_text = Some(text); - self.marked_range_utf16 = Some(range); + self.ime_state = Some(ImeState { + marked_text: text, + marked_range_utf16: range, + }); cx.notify(); } /// Gets the current marked range (UTF-16). pub(crate) fn marked_text_range(&self) -> Option> { - self.marked_range_utf16.clone() + self.ime_state + .as_ref() + .and_then(|state| state.marked_range_utf16.clone()) } /// Clears the marked (pre-edit) text state. pub(crate) fn clear_marked_text(&mut self, cx: &mut Context) { - if self.marked_text.is_some() { - self.marked_text = None; - self.marked_range_utf16 = None; + if self.ime_state.is_some() { + self.ime_state = None; cx.notify(); } } From 29def012a18af534d17ded2febc77841ce3af601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Sun, 7 Sep 2025 14:09:35 +0800 Subject: [PATCH 635/744] windows: Update Windows keymap (#37721) Pickup the changes from #37009 Release Notes: - N/A --- assets/keymaps/default-windows.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 1c9f1281882dc136daa7a3912d3d92b3516a4441..d10451ac856e201033490da260d4e21b40cf718b 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -644,6 +644,7 @@ "alt-save": "workspace::SaveAll", "ctrl-k s": "workspace::SaveAll", "ctrl-k m": "language_selector::Toggle", + "ctrl-m ctrl-m": "toolchain::AddToolchain", "escape": "workspace::Unfollow", "ctrl-k ctrl-left": "workspace::ActivatePaneLeft", "ctrl-k ctrl-right": "workspace::ActivatePaneRight", @@ -1075,6 +1076,13 @@ "tab": "channel_modal::ToggleMode" } }, + { + "context": "ToolchainSelector", + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-a": "toolchain::AddToolchain" + } + }, { "context": "FileFinder || (FileFinder > Picker > Editor)", "use_key_equivalents": true, From 0ef7ee172fd8dd4d0638a9731140cb3a74344918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Sun, 7 Sep 2025 14:45:41 +0800 Subject: [PATCH 636/744] windows: Remove some unused keys from the keymap (#37722) AFAIK, we dont handle these keys on Windows. Release Notes: - N/A --- assets/keymaps/default-windows.json | 37 ----------------------------- 1 file changed, 37 deletions(-) diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index d10451ac856e201033490da260d4e21b40cf718b..de0d97b52e2b0fe9bac931cb46debc812a56a70b 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -25,7 +25,6 @@ "ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }], "ctrl-shift-w": "workspace::CloseWindow", "shift-escape": "workspace::ToggleZoom", - "open": "workspace::Open", "ctrl-o": "workspace::Open", "ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }], "ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }], @@ -68,18 +67,13 @@ "ctrl-k q": "editor::Rewrap", "ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }], "ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], - "cut": "editor::Cut", "shift-delete": "editor::Cut", "ctrl-x": "editor::Cut", - "copy": "editor::Copy", "ctrl-insert": "editor::Copy", "ctrl-c": "editor::Copy", - "paste": "editor::Paste", "shift-insert": "editor::Paste", "ctrl-v": "editor::Paste", - "undo": "editor::Undo", "ctrl-z": "editor::Undo", - "redo": "editor::Redo", "ctrl-y": "editor::Redo", "ctrl-shift-z": "editor::Redo", "up": "editor::MoveUp", @@ -138,7 +132,6 @@ "ctrl-shift-enter": "editor::NewlineAbove", "ctrl-k ctrl-z": "editor::ToggleSoftWrap", "ctrl-k z": "editor::ToggleSoftWrap", - "find": "buffer_search::Deploy", "ctrl-f": "buffer_search::Deploy", "ctrl-h": "buffer_search::DeployReplace", "ctrl-shift-.": "assistant::QuoteSelection", @@ -177,7 +170,6 @@ "context": "Markdown", "use_key_equivalents": true, "bindings": { - "copy": "markdown::Copy", "ctrl-c": "markdown::Copy" } }, @@ -225,7 +217,6 @@ "bindings": { "ctrl-enter": "assistant::Assist", "ctrl-s": "workspace::Save", - "save": "workspace::Save", "ctrl-shift-,": "assistant::InsertIntoEditor", "shift-enter": "assistant::Split", "ctrl-r": "assistant::CycleMessageRole", @@ -272,7 +263,6 @@ "context": "AgentPanel > Markdown", "use_key_equivalents": true, "bindings": { - "copy": "markdown::CopyAsMarkdown", "ctrl-c": "markdown::CopyAsMarkdown" } }, @@ -367,7 +357,6 @@ "context": "PromptLibrary", "use_key_equivalents": true, "bindings": { - "new": "rules_library::NewRule", "ctrl-n": "rules_library::NewRule", "ctrl-shift-s": "rules_library::ToggleDefaultRule" } @@ -381,7 +370,6 @@ "enter": "search::SelectNextMatch", "shift-enter": "search::SelectPreviousMatch", "alt-enter": "search::SelectAllMatches", - "find": "search::FocusSearch", "ctrl-f": "search::FocusSearch", "ctrl-h": "search::ToggleReplace", "ctrl-l": "search::ToggleSelection" @@ -408,7 +396,6 @@ "use_key_equivalents": true, "bindings": { "escape": "project_search::ToggleFocus", - "shift-find": "search::FocusSearch", "ctrl-shift-f": "search::FocusSearch", "ctrl-shift-h": "search::ToggleReplace", "alt-r": "search::ToggleRegex" // vscode @@ -472,14 +459,12 @@ "forward": "pane::GoForward", "f3": "search::SelectNextMatch", "shift-f3": "search::SelectPreviousMatch", - "shift-find": "project_search::ToggleFocus", "ctrl-shift-f": "project_search::ToggleFocus", "shift-alt-h": "search::ToggleReplace", "alt-l": "search::ToggleSelection", "alt-enter": "search::SelectAllMatches", "alt-c": "search::ToggleCaseSensitive", "alt-w": "search::ToggleWholeWord", - "alt-find": "project_search::ToggleFilters", "alt-f": "project_search::ToggleFilters", "alt-r": "search::ToggleRegex", // "ctrl-shift-alt-x": "search::ToggleRegex", @@ -579,25 +564,19 @@ "context": "Workspace", "use_key_equivalents": true, "bindings": { - "alt-open": ["projects::OpenRecent", { "create_new_window": false }], // Change the default action on `menu::Confirm` by setting the parameter // "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }], "ctrl-r": ["projects::OpenRecent", { "create_new_window": false }], - "shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], // Change to open path modal for existing remote connection by setting the parameter // "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]", "ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], "shift-alt-b": "branches::OpenRecent", "shift-alt-enter": "toast::RunAction", "ctrl-shift-`": "workspace::NewTerminal", - "save": "workspace::Save", "ctrl-s": "workspace::Save", "ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat", - "shift-save": "workspace::SaveAs", "ctrl-shift-s": "workspace::SaveAs", - "new": "workspace::NewFile", "ctrl-n": "workspace::NewFile", - "shift-new": "workspace::NewWindow", "ctrl-shift-n": "workspace::NewWindow", "ctrl-`": "terminal_panel::Toggle", "f10": ["app_menu::OpenApplicationMenu", "Zed"], @@ -621,7 +600,6 @@ "shift-alt-0": "workspace::ResetOpenDocksSize", "ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }], "ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }], - "shift-find": "pane::DeploySearch", "ctrl-shift-f": "pane::DeploySearch", "ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }], "ctrl-shift-t": "pane::ReopenClosedItem", @@ -641,7 +619,6 @@ "ctrl-shift-g": "git_panel::ToggleFocus", "ctrl-shift-d": "debug_panel::ToggleFocus", "ctrl-shift-/": "agent::ToggleFocus", - "alt-save": "workspace::SaveAll", "ctrl-k s": "workspace::SaveAll", "ctrl-k m": "language_selector::Toggle", "ctrl-m ctrl-m": "toolchain::AddToolchain", @@ -849,9 +826,7 @@ "bindings": { "left": "outline_panel::CollapseSelectedEntry", "right": "outline_panel::ExpandSelectedEntry", - "alt-copy": "outline_panel::CopyPath", "shift-alt-c": "outline_panel::CopyPath", - "shift-alt-copy": "workspace::CopyRelativePath", "ctrl-shift-alt-c": "workspace::CopyRelativePath", "ctrl-alt-r": "outline_panel::RevealInFileManager", "space": "outline_panel::OpenSelectedEntry", @@ -867,21 +842,14 @@ "bindings": { "left": "project_panel::CollapseSelectedEntry", "right": "project_panel::ExpandSelectedEntry", - "new": "project_panel::NewFile", "ctrl-n": "project_panel::NewFile", - "alt-new": "project_panel::NewDirectory", "alt-n": "project_panel::NewDirectory", - "cut": "project_panel::Cut", "ctrl-x": "project_panel::Cut", - "copy": "project_panel::Copy", "ctrl-insert": "project_panel::Copy", "ctrl-c": "project_panel::Copy", - "paste": "project_panel::Paste", "shift-insert": "project_panel::Paste", "ctrl-v": "project_panel::Paste", - "alt-copy": "project_panel::CopyPath", "shift-alt-c": "project_panel::CopyPath", - "shift-alt-copy": "workspace::CopyRelativePath", "ctrl-k ctrl-shift-c": "workspace::CopyRelativePath", "enter": "project_panel::Rename", "f2": "project_panel::Rename", @@ -893,7 +861,6 @@ "ctrl-alt-r": "project_panel::RevealInFileManager", "ctrl-shift-enter": "project_panel::OpenWithSystem", "alt-d": "project_panel::CompareMarkedFiles", - "shift-find": "project_panel::NewSearchInDirectory", "ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory", "shift-down": "menu::SelectNext", "shift-up": "menu::SelectPrevious", @@ -1118,10 +1085,8 @@ "use_key_equivalents": true, "bindings": { "ctrl-alt-space": "terminal::ShowCharacterPalette", - "copy": "terminal::Copy", "ctrl-insert": "terminal::Copy", "ctrl-shift-c": "terminal::Copy", - "paste": "terminal::Paste", "shift-insert": "terminal::Paste", "ctrl-shift-v": "terminal::Paste", "ctrl-enter": "assistant::InlineAssist", @@ -1137,7 +1102,6 @@ "ctrl-w": ["terminal::SendKeystroke", "ctrl-w"], "ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"], "ctrl-shift-a": "editor::SelectAll", - "find": "buffer_search::Deploy", "ctrl-shift-f": "buffer_search::Deploy", "ctrl-shift-l": "terminal::Clear", "ctrl-shift-w": "pane::CloseActiveItem", @@ -1218,7 +1182,6 @@ "use_key_equivalents": true, "bindings": { "ctrl-f": "search::FocusSearch", - "alt-find": "keymap_editor::ToggleKeystrokeSearch", "alt-f": "keymap_editor::ToggleKeystrokeSearch", "alt-c": "keymap_editor::ToggleConflictFilter", "enter": "keymap_editor::EditBinding", From 76aaf6a8fe1992ec57114633e022c9771756bfaa Mon Sep 17 00:00:00 2001 From: Bruno Taschenbier <139721757+tastenbier@users.noreply.github.com> Date: Sun, 7 Sep 2025 17:00:58 +0000 Subject: [PATCH 637/744] Fix docs for `tabs.close_position` in `default.json` (#37729) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minor docs fix. Seems like 0a4ff2f47536c872ebd1ac3e672538a6251832e8 accidentally added "hidden" to the docs of both – `close_position` and `show_close_button`. Release Notes: - N/A Co-authored-by: tastenbier <> --- assets/settings/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 0b5481bd4e4e2177302e38199bb66e87471d2904..63a11403d3dd4b30926a6a1f32e86dadf3804054 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -962,7 +962,7 @@ // Show git status colors in the editor tabs. "git_status": false, // Position of the close button on the editor tabs. - // One of: ["right", "left", "hidden"] + // One of: ["right", "left"] "close_position": "right", // Whether to show the file icon for a tab. "file_icons": false, From 0e33a3afe0f37b1a7a37fec6b068abe9dd984009 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Sun, 7 Sep 2025 11:16:49 -0600 Subject: [PATCH 638/744] zeta: Check whether data collection is allowed for recent edit history (#37680) Also: * Adds tests for can_collect_data. * Temporarily removes collection of diagnostics. Release Notes: - Edit Prediction: Fixed a bug where requests were marked eligible for data collection despite the recent edit history in the request involving files that may not be open source. The requests affected by this bug will not be used in training data. --- Cargo.lock | 1 + .../zed/src/zed/edit_prediction_registry.rs | 7 +- crates/zeta/Cargo.toml | 1 + crates/zeta/src/input_excerpt.rs | 8 +- crates/zeta/src/license_detection.rs | 1 - crates/zeta/src/zeta.rs | 882 ++++++++++++------ crates/zeta_cli/src/main.rs | 35 +- 7 files changed, 595 insertions(+), 340 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4c94f8078b1ab392ed1a50e15c71dab1921f0a3..dbcea05ea9bc52288defc8c299d82eb508337544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20841,6 +20841,7 @@ dependencies = [ "language_model", "log", "menu", + "parking_lot", "postage", "project", "rand 0.9.1", diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index 4f009ccb0b1197f11b034ac48b89dd37b6f41278..ae26427fc6547079b163235f5d1c3df26a489795 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -8,7 +8,7 @@ use settings::SettingsStore; use std::{cell::RefCell, rc::Rc, sync::Arc}; use supermaven::{Supermaven, SupermavenCompletionProvider}; use ui::Window; -use zeta::{ProviderDataCollection, ZetaEditPredictionProvider}; +use zeta::ZetaEditPredictionProvider; pub fn init(client: Arc, user_store: Entity, cx: &mut App) { let editors: Rc, AnyWindowHandle>>> = Rc::default(); @@ -214,11 +214,8 @@ fn assign_edit_prediction_provider( }); } - let data_collection = - ProviderDataCollection::new(zeta.clone(), singleton_buffer, cx); - let provider = - cx.new(|_| zeta::ZetaEditPredictionProvider::new(zeta, data_collection)); + cx.new(|_| zeta::ZetaEditPredictionProvider::new(zeta, singleton_buffer)); editor.set_edit_prediction_provider(Some(provider), window, cx); } diff --git a/crates/zeta/Cargo.toml b/crates/zeta/Cargo.toml index a9c2a7619f4db22e51c014672aa2100b30a2539a..09bcfa7f542ce9c01802c9cebc11dfc9a8da2542 100644 --- a/crates/zeta/Cargo.toml +++ b/crates/zeta/Cargo.toml @@ -72,6 +72,7 @@ gpui = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } indoc.workspace = true language = { workspace = true, features = ["test-support"] } +parking_lot.workspace = true reqwest_client = { workspace = true, features = ["test-support"] } rpc = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } diff --git a/crates/zeta/src/input_excerpt.rs b/crates/zeta/src/input_excerpt.rs index dd1bbed1d72e8668e9ed55c9b66b911addfcdd43..06bff5b1bea0f099b2ccd98605ac5de5bb5e6360 100644 --- a/crates/zeta/src/input_excerpt.rs +++ b/crates/zeta/src/input_excerpt.rs @@ -1,6 +1,6 @@ use crate::{ CURSOR_MARKER, EDITABLE_REGION_END_MARKER, EDITABLE_REGION_START_MARKER, START_OF_FILE_MARKER, - tokens_for_bytes, + guess_token_count, }; use language::{BufferSnapshot, Point}; use std::{fmt::Write, ops::Range}; @@ -22,7 +22,7 @@ pub fn excerpt_for_cursor_position( let mut remaining_edit_tokens = editable_region_token_limit; while let Some(parent) = snapshot.syntax_ancestor(scope_range.clone()) { - let parent_tokens = tokens_for_bytes(parent.byte_range().len()); + let parent_tokens = guess_token_count(parent.byte_range().len()); let parent_point_range = Point::new( parent.start_position().row as u32, parent.start_position().column as u32, @@ -99,7 +99,7 @@ fn expand_range( if remaining_tokens > 0 && expanded_range.start.row > 0 { expanded_range.start.row -= 1; let line_tokens = - tokens_for_bytes(snapshot.line_len(expanded_range.start.row) as usize); + guess_token_count(snapshot.line_len(expanded_range.start.row) as usize); remaining_tokens = remaining_tokens.saturating_sub(line_tokens); expanded = true; } @@ -107,7 +107,7 @@ fn expand_range( if remaining_tokens > 0 && expanded_range.end.row < snapshot.max_point().row { expanded_range.end.row += 1; expanded_range.end.column = snapshot.line_len(expanded_range.end.row); - let line_tokens = tokens_for_bytes(expanded_range.end.column as usize); + let line_tokens = guess_token_count(expanded_range.end.column as usize); remaining_tokens = remaining_tokens.saturating_sub(line_tokens); expanded = true; } diff --git a/crates/zeta/src/license_detection.rs b/crates/zeta/src/license_detection.rs index 5f207a44e8bd2028e6a2b416e978f101cfe5bd57..e06e1577a66cc160efa00213b80c6ca407f7be85 100644 --- a/crates/zeta/src/license_detection.rs +++ b/crates/zeta/src/license_detection.rs @@ -358,7 +358,6 @@ impl LicenseDetectionWatcher { #[cfg(test)] mod tests { - use fs::FakeFs; use gpui::TestAppContext; use serde_json::json; diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 3851d16755783209fd9da4f468a494779a7d9fe7..dfcf98f025c2e020d6545efca64d4ab12579e370 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -29,7 +29,7 @@ use gpui::{ use http_client::{AsyncBody, HttpClient, Method, Request, Response}; use input_excerpt::excerpt_for_cursor_position; use language::{ - Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToOffset, ToPoint, text_diff, + Anchor, Buffer, BufferSnapshot, EditPreview, File, OffsetRangeExt, ToOffset, ToPoint, text_diff, }; use language_model::{LlmApiToken, RefreshLlmTokenListener}; use project::{Project, ProjectPath}; @@ -65,7 +65,6 @@ const ZED_PREDICT_DATA_COLLECTION_CHOICE: &str = "zed_predict_data_collection_ch const MAX_CONTEXT_TOKENS: usize = 150; const MAX_REWRITE_TOKENS: usize = 350; const MAX_EVENT_TOKENS: usize = 500; -const MAX_DIAGNOSTIC_GROUPS: usize = 10; /// Maximum number of events to track. const MAX_EVENT_COUNT: usize = 16; @@ -216,7 +215,7 @@ pub struct Zeta { client: Arc, shown_completions: VecDeque, rated_completions: HashSet, - data_collection_choice: Entity, + data_collection_choice: DataCollectionChoice, llm_token: LlmApiToken, _llm_token_subscription: Subscription, /// Whether an update to a newer version of Zed is required to continue using Zeta. @@ -271,10 +270,7 @@ impl Zeta { fn new(client: Arc, user_store: Entity, cx: &mut Context) -> Self { let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx); - - let data_collection_choice = Self::load_data_collection_choices(); - let data_collection_choice = cx.new(|_| data_collection_choice); - + let data_collection_choice = Self::load_data_collection_choice(); Self { projects: HashMap::default(), client, @@ -408,7 +404,6 @@ impl Zeta { project: &Entity, buffer: &Entity, cursor: language::Anchor, - can_collect_data: bool, cx: &mut Context, perform_predict_edits: F, ) -> Task>> @@ -422,15 +417,25 @@ impl Zeta { let buffer_snapshotted_at = Instant::now(); let snapshot = self.report_changes_for_buffer(&buffer, project, cx); let zeta = cx.entity(); - let events = self.get_or_init_zeta_project(project, cx).events.clone(); let client = self.client.clone(); let llm_token = self.llm_token.clone(); let app_version = AppVersion::global(cx); - let git_info = if let (true, Some(file)) = (can_collect_data, snapshot.file()) { - git_info_for_file(project, &ProjectPath::from_file(file.as_ref(), cx), cx) + let zeta_project = self.get_or_init_zeta_project(project, cx); + let mut events = Vec::with_capacity(zeta_project.events.len()); + events.extend(zeta_project.events.iter().cloned()); + let events = Arc::new(events); + + let (git_info, can_collect_file) = if let Some(file) = snapshot.file() { + let can_collect_file = self.can_collect_file(file, cx); + let git_info = if can_collect_file { + git_info_for_file(project, &ProjectPath::from_file(file.as_ref(), cx), cx) + } else { + None + }; + (git_info, can_collect_file) } else { - None + (None, false) }; let full_path: Arc = snapshot @@ -440,25 +445,35 @@ impl Zeta { let full_path_str = full_path.to_string_lossy().to_string(); let cursor_point = cursor.to_point(&snapshot); let cursor_offset = cursor_point.to_offset(&snapshot); - let make_events_prompt = move || prompt_for_events(&events, MAX_EVENT_TOKENS); + let prompt_for_events = { + let events = events.clone(); + move || prompt_for_events_impl(&events, MAX_EVENT_TOKENS) + }; let gather_task = gather_context( - project, full_path_str, &snapshot, cursor_point, - make_events_prompt, - can_collect_data, - git_info, + prompt_for_events, cx, ); cx.spawn(async move |this, cx| { let GatherContextOutput { - body, + mut body, editable_range, + included_events_count, } = gather_task.await?; let done_gathering_context_at = Instant::now(); + let included_events = &events[events.len() - included_events_count..events.len()]; + body.can_collect_data = can_collect_file + && this + .read_with(cx, |this, cx| this.can_collect_events(included_events, cx)) + .unwrap_or(false); + if body.can_collect_data { + body.git_info = git_info; + } + log::debug!( "Events:\n{}\nExcerpt:\n{:?}", body.input_events, @@ -563,10 +578,8 @@ impl Zeta { response: PredictEditsResponse, cx: &mut Context, ) -> Task>> { - use std::future::ready; - - self.request_completion_impl(project, buffer, position, false, cx, |_params| { - ready(Ok((response, None))) + self.request_completion_impl(project, buffer, position, cx, |_params| { + std::future::ready(Ok((response, None))) }) } @@ -575,17 +588,9 @@ impl Zeta { project: &Entity, buffer: &Entity, position: language::Anchor, - can_collect_data: bool, cx: &mut Context, ) -> Task>> { - self.request_completion_impl( - project, - buffer, - position, - can_collect_data, - cx, - Self::perform_predict_edits, - ) + self.request_completion_impl(project, buffer, position, cx, Self::perform_predict_edits) } pub fn perform_predict_edits( @@ -954,7 +959,58 @@ impl Zeta { new_snapshot } - fn load_data_collection_choices() -> DataCollectionChoice { + fn can_collect_file(&self, file: &Arc, cx: &App) -> bool { + self.data_collection_choice.is_enabled() && self.is_file_open_source(file, cx) + } + + fn can_collect_events(&self, events: &[Event], cx: &App) -> bool { + if !self.data_collection_choice.is_enabled() { + return false; + } + let mut last_checked_file = None; + for event in events { + match event { + Event::BufferChange { + old_snapshot, + new_snapshot, + .. + } => { + if let Some(old_file) = old_snapshot.file() + && let Some(new_file) = new_snapshot.file() + { + if let Some(last_checked_file) = last_checked_file + && Arc::ptr_eq(last_checked_file, old_file) + && Arc::ptr_eq(last_checked_file, new_file) + { + continue; + } + if !self.can_collect_file(old_file, cx) { + return false; + } + if !Arc::ptr_eq(old_file, new_file) && !self.can_collect_file(new_file, cx) + { + return false; + } + last_checked_file = Some(new_file); + } else { + return false; + } + } + } + } + true + } + + fn is_file_open_source(&self, file: &Arc, cx: &App) -> bool { + if !file.is_local() || file.is_private() { + return false; + } + self.license_detection_watchers + .get(&file.worktree_id(cx)) + .is_some_and(|watcher| watcher.is_project_open_source()) + } + + fn load_data_collection_choice() -> DataCollectionChoice { let choice = KEY_VALUE_STORE .read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE) .log_err() @@ -970,6 +1026,17 @@ impl Zeta { None => DataCollectionChoice::NotAnswered, } } + + fn toggle_data_collection_choice(&mut self, cx: &mut Context) { + self.data_collection_choice = self.data_collection_choice.toggle(); + let new_choice = self.data_collection_choice; + db::write_and_log(cx, move || { + KEY_VALUE_STORE.write_kvp( + ZED_PREDICT_DATA_COLLECTION_CHOICE.into(), + new_choice.is_enabled().to_string(), + ) + }); + } } pub struct PerformPredictEditsParams { @@ -1026,48 +1093,19 @@ fn git_info_for_file( pub struct GatherContextOutput { pub body: PredictEditsBody, pub editable_range: Range, + pub included_events_count: usize, } pub fn gather_context( - project: &Entity, full_path_str: String, snapshot: &BufferSnapshot, cursor_point: language::Point, - make_events_prompt: impl FnOnce() -> String + Send + 'static, - can_collect_data: bool, - git_info: Option, + prompt_for_events: impl FnOnce() -> (String, usize) + Send + 'static, cx: &App, ) -> Task> { - let local_lsp_store = project.read(cx).lsp_store().read(cx).as_local(); - let diagnostic_groups: Vec<(String, serde_json::Value)> = - if can_collect_data && let Some(local_lsp_store) = local_lsp_store { - snapshot - .diagnostic_groups(None) - .into_iter() - .filter_map(|(language_server_id, diagnostic_group)| { - let language_server = - local_lsp_store.running_language_server_for_id(language_server_id)?; - let diagnostic_group = diagnostic_group.resolve::(snapshot); - let language_server_name = language_server.name().to_string(); - let serialized = serde_json::to_value(diagnostic_group).unwrap(); - Some((language_server_name, serialized)) - }) - .collect::>() - } else { - Vec::new() - }; - cx.background_spawn({ let snapshot = snapshot.clone(); async move { - let diagnostic_groups = if diagnostic_groups.is_empty() - || diagnostic_groups.len() >= MAX_DIAGNOSTIC_GROUPS - { - None - } else { - Some(diagnostic_groups) - }; - let input_excerpt = excerpt_for_cursor_position( cursor_point, &full_path_str, @@ -1075,15 +1113,15 @@ pub fn gather_context( MAX_REWRITE_TOKENS, MAX_CONTEXT_TOKENS, ); - let input_events = make_events_prompt(); + let (input_events, included_events_count) = prompt_for_events(); let editable_range = input_excerpt.editable_range.to_offset(&snapshot); let body = PredictEditsBody { input_events, input_excerpt: input_excerpt.prompt, - can_collect_data, - diagnostic_groups, - git_info, + can_collect_data: false, + diagnostic_groups: None, + git_info: None, outline: None, speculated_output: None, }; @@ -1091,18 +1129,19 @@ pub fn gather_context( Ok(GatherContextOutput { body, editable_range, + included_events_count, }) } }) } -fn prompt_for_events(events: &VecDeque, mut remaining_tokens: usize) -> String { +fn prompt_for_events_impl(events: &[Event], mut remaining_tokens: usize) -> (String, usize) { let mut result = String::new(); - for event in events.iter().rev() { + for (ix, event) in events.iter().rev().enumerate() { let event_string = event.to_prompt(); - let event_tokens = tokens_for_bytes(event_string.len()); + let event_tokens = guess_token_count(event_string.len()); if event_tokens > remaining_tokens { - break; + return (result, ix); } if !result.is_empty() { @@ -1111,7 +1150,7 @@ fn prompt_for_events(events: &VecDeque, mut remaining_tokens: usize) -> S result.insert_str(0, &event_string); remaining_tokens -= event_tokens; } - result + return (result, events.len()); } struct RegisteredBuffer { @@ -1222,6 +1261,7 @@ impl DataCollectionChoice { } } + #[must_use] pub fn toggle(&self) -> DataCollectionChoice { match self { Self::Enabled => Self::Disabled, @@ -1240,79 +1280,6 @@ impl From for DataCollectionChoice { } } -pub struct ProviderDataCollection { - /// When set to None, data collection is not possible in the provider buffer - choice: Option>, - license_detection_watcher: Option>, -} - -impl ProviderDataCollection { - pub fn new(zeta: Entity, buffer: Option>, cx: &mut App) -> Self { - let choice_and_watcher = buffer.and_then(|buffer| { - let file = buffer.read(cx).file()?; - - if !file.is_local() || file.is_private() { - return None; - } - - let zeta = zeta.read(cx); - let choice = zeta.data_collection_choice.clone(); - - let license_detection_watcher = zeta - .license_detection_watchers - .get(&file.worktree_id(cx)) - .cloned()?; - - Some((choice, license_detection_watcher)) - }); - - if let Some((choice, watcher)) = choice_and_watcher { - ProviderDataCollection { - choice: Some(choice), - license_detection_watcher: Some(watcher), - } - } else { - ProviderDataCollection { - choice: None, - license_detection_watcher: None, - } - } - } - - pub fn can_collect_data(&self, cx: &App) -> bool { - self.is_data_collection_enabled(cx) && self.is_project_open_source() - } - - pub fn is_data_collection_enabled(&self, cx: &App) -> bool { - self.choice - .as_ref() - .is_some_and(|choice| choice.read(cx).is_enabled()) - } - - fn is_project_open_source(&self) -> bool { - self.license_detection_watcher - .as_ref() - .is_some_and(|watcher| watcher.is_project_open_source()) - } - - pub fn toggle(&mut self, cx: &mut App) { - if let Some(choice) = self.choice.as_mut() { - let new_choice = choice.update(cx, |choice, _cx| { - let new_choice = choice.toggle(); - *choice = new_choice; - new_choice - }); - - db::write_and_log(cx, move || { - KEY_VALUE_STORE.write_kvp( - ZED_PREDICT_DATA_COLLECTION_CHOICE.into(), - new_choice.is_enabled().to_string(), - ) - }); - } - } -} - async fn llm_token_retry( llm_token: &LlmApiToken, client: &Arc, @@ -1343,24 +1310,23 @@ async fn llm_token_retry( pub struct ZetaEditPredictionProvider { zeta: Entity, + singleton_buffer: Option>, pending_completions: ArrayVec, next_pending_completion_id: usize, current_completion: Option, - /// None if this is entirely disabled for this provider - provider_data_collection: ProviderDataCollection, last_request_timestamp: Instant, } impl ZetaEditPredictionProvider { pub const THROTTLE_TIMEOUT: Duration = Duration::from_millis(300); - pub fn new(zeta: Entity, provider_data_collection: ProviderDataCollection) -> Self { + pub fn new(zeta: Entity, singleton_buffer: Option>) -> Self { Self { zeta, + singleton_buffer, pending_completions: ArrayVec::new(), next_pending_completion_id: 0, current_completion: None, - provider_data_collection, last_request_timestamp: Instant::now(), } } @@ -1384,21 +1350,29 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { } fn data_collection_state(&self, cx: &App) -> DataCollectionState { - let is_project_open_source = self.provider_data_collection.is_project_open_source(); - - if self.provider_data_collection.is_data_collection_enabled(cx) { - DataCollectionState::Enabled { - is_project_open_source, + if let Some(buffer) = &self.singleton_buffer + && let Some(file) = buffer.read(cx).file() + { + let is_project_open_source = self.zeta.read(cx).is_file_open_source(file, cx); + if self.zeta.read(cx).data_collection_choice.is_enabled() { + DataCollectionState::Enabled { + is_project_open_source, + } + } else { + DataCollectionState::Disabled { + is_project_open_source, + } } } else { - DataCollectionState::Disabled { - is_project_open_source, - } + return DataCollectionState::Disabled { + is_project_open_source: false, + }; } } fn toggle_data_collection(&mut self, cx: &mut App) { - self.provider_data_collection.toggle(cx); + self.zeta + .update(cx, |zeta, cx| zeta.toggle_data_collection_choice(cx)); } fn usage(&self, cx: &App) -> Option { @@ -1456,7 +1430,6 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { let pending_completion_id = self.next_pending_completion_id; self.next_pending_completion_id += 1; - let can_collect_data = self.provider_data_collection.can_collect_data(cx); let last_request_timestamp = self.last_request_timestamp; let task = cx.spawn(async move |this, cx| { @@ -1469,7 +1442,7 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { let completion_request = this.update(cx, |this, cx| { this.last_request_timestamp = Instant::now(); this.zeta.update(cx, |zeta, cx| { - zeta.request_completion(&project, &buffer, position, can_collect_data, cx) + zeta.request_completion(&project, &buffer, position, cx) }) }); @@ -1638,10 +1611,11 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { } } -fn tokens_for_bytes(bytes: usize) -> usize { - /// Typical number of string bytes per token for the purposes of limiting model input. This is - /// intentionally low to err on the side of underestimating limits. - const BYTES_PER_TOKEN_GUESS: usize = 3; +/// Typical number of string bytes per token for the purposes of limiting model input. This is +/// intentionally low to err on the side of underestimating limits. +const BYTES_PER_TOKEN_GUESS: usize = 3; + +fn guess_token_count(bytes: usize) -> usize { bytes / BYTES_PER_TOKEN_GUESS } @@ -1654,11 +1628,15 @@ mod tests { use http_client::FakeHttpClient; use indoc::indoc; use language::Point; + use parking_lot::Mutex; + use serde_json::json; use settings::SettingsStore; use util::path; use super::*; + const BSD_0_TXT: &str = include_str!("../license_examples/0bsd.txt"); + #[gpui::test] async fn test_edit_prediction_basic_interpolation(cx: &mut TestAppContext) { let buffer = cx.new(|cx| Buffer::local("Lorem ipsum dolor", cx)); @@ -1778,77 +1756,65 @@ mod tests { #[gpui::test] async fn test_clean_up_diff(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - client::init_settings(cx); - Project::init_settings(cx); - }); + init_test(cx); - let edits = edits_for_prediction( - indoc! {" - fn main() { - let word_1 = \"lorem\"; - let range = word.len()..word.len(); - } - "}, + assert_eq!( + apply_edit_prediction( + indoc! {" + fn main() { + let word_1 = \"lorem\"; + let range = word.len()..word.len(); + } + "}, + indoc! {" + <|editable_region_start|> + fn main() { + let word_1 = \"lorem\"; + let range = word_1.len()..word_1.len(); + } + + <|editable_region_end|> + "}, + cx, + ) + .await, indoc! {" - <|editable_region_start|> fn main() { let word_1 = \"lorem\"; let range = word_1.len()..word_1.len(); } - - <|editable_region_end|> "}, - cx, - ) - .await; - assert_eq!( - edits, - [ - (Point::new(2, 20)..Point::new(2, 20), "_1".to_string()), - (Point::new(2, 32)..Point::new(2, 32), "_1".to_string()), - ] ); - let edits = edits_for_prediction( - indoc! {" - fn main() { - let story = \"the quick\" - } - "}, + assert_eq!( + apply_edit_prediction( + indoc! {" + fn main() { + let story = \"the quick\" + } + "}, + indoc! {" + <|editable_region_start|> + fn main() { + let story = \"the quick brown fox jumps over the lazy dog\"; + } + + <|editable_region_end|> + "}, + cx, + ) + .await, indoc! {" - <|editable_region_start|> fn main() { let story = \"the quick brown fox jumps over the lazy dog\"; } - - <|editable_region_end|> "}, - cx, - ) - .await; - assert_eq!( - edits, - [ - ( - Point::new(1, 26)..Point::new(1, 26), - " brown fox jumps over the lazy dog".to_string() - ), - (Point::new(1, 27)..Point::new(1, 27), ";".to_string()), - ] ); } #[gpui::test] 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); - client::init_settings(cx); - Project::init_settings(cx); - }); + init_test(cx); let buffer_content = "lorem\n"; let completion_response = indoc! {" @@ -1860,98 +1826,404 @@ mod tests { <|editable_region_end|> ```"}; - let http_client = FakeHttpClient::create(move |req| async move { - match (req.method(), req.uri().path()) { - (&Method::POST, "/client/llm_tokens") => Ok(http_client::Response::builder() - .status(200) - .body( - serde_json::to_string(&CreateLlmTokenResponse { - token: LlmToken("the-llm-token".to_string()), - }) - .unwrap() - .into(), - ) - .unwrap()), - (&Method::POST, "/predict_edits/v2") => Ok(http_client::Response::builder() - .status(200) - .body( - serde_json::to_string(&PredictEditsResponse { - request_id: Uuid::parse_str("7e86480f-3536-4d2c-9334-8213e3445d45") - .unwrap(), - output_excerpt: completion_response.to_string(), - }) - .unwrap() - .into(), - ) - .unwrap()), - _ => Ok(http_client::Response::builder() - .status(404) - .body("Not Found".into()) - .unwrap()), - } + assert_eq!( + apply_edit_prediction(buffer_content, completion_response, cx).await, + "lorem\nipsum" + ); + } + + #[gpui::test] + async fn test_can_collect_data(cx: &mut TestAppContext) { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree(path!("/project"), json!({ "LICENSE": BSD_0_TXT })) + .await; + + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/project/src/main.rs"), cx) + }) + .await + .unwrap(); + + let (zeta, captured_request, _) = make_test_zeta(&project, cx).await; + zeta.update(cx, |zeta, _cx| { + zeta.data_collection_choice = DataCollectionChoice::Enabled }); - let client = cx.update(|cx| Client::new(Arc::new(FakeSystemClock::new()), http_client, cx)); - cx.update(|cx| { - RefreshLlmTokenListener::register(client.clone(), cx); + run_edit_prediction(&buffer, &project, &zeta, cx).await; + assert_eq!( + captured_request.lock().clone().unwrap().can_collect_data, + true + ); + + zeta.update(cx, |zeta, _cx| { + zeta.data_collection_choice = DataCollectionChoice::Disabled }); - // Construct the fake server to authenticate. - let _server = FakeServer::for_client(42, &client, cx).await; + + run_edit_prediction(&buffer, &project, &zeta, cx).await; + assert_eq!( + captured_request.lock().clone().unwrap().can_collect_data, + false + ); + } + + #[gpui::test] + async fn test_no_data_collection_for_remote_file(cx: &mut TestAppContext) { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + let project = Project::test(fs.clone(), [], cx).await; + + let buffer = cx.new(|_cx| { + Buffer::remote( + language::BufferId::new(1).unwrap(), + 1, + language::Capability::ReadWrite, + "fn main() {\n println!(\"Hello\");\n}", + ) + }); + + let (zeta, captured_request, _) = make_test_zeta(&project, cx).await; + zeta.update(cx, |zeta, _cx| { + zeta.data_collection_choice = DataCollectionChoice::Enabled + }); + + run_edit_prediction(&buffer, &project, &zeta, cx).await; + assert_eq!( + captured_request.lock().clone().unwrap().can_collect_data, + false + ); + } + + #[gpui::test] + async fn test_no_data_collection_for_private_file(cx: &mut TestAppContext) { + init_test(cx); + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/project"), + json!({ + "LICENSE": BSD_0_TXT, + ".env": "SECRET_KEY=secret" + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let buffer = cx.new(|cx| Buffer::local(buffer_content, cx)); - let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0))); + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/project/.env", cx) + }) + .await + .unwrap(); - let zeta = cx.new(|cx| Zeta::new(client, project.read(cx).user_store(), cx)); - let completion_task = zeta.update(cx, |zeta, cx| { - zeta.request_completion(&project, &buffer, cursor, false, cx) + let (zeta, captured_request, _) = make_test_zeta(&project, cx).await; + zeta.update(cx, |zeta, _cx| { + zeta.data_collection_choice = DataCollectionChoice::Enabled + }); + + run_edit_prediction(&buffer, &project, &zeta, cx).await; + assert_eq!( + captured_request.lock().clone().unwrap().can_collect_data, + false + ); + } + + #[gpui::test] + async fn test_no_data_collection_for_untitled_buffer(cx: &mut TestAppContext) { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + let project = Project::test(fs.clone(), [], cx).await; + let buffer = cx.new(|cx| Buffer::local("", cx)); + + let (zeta, captured_request, _) = make_test_zeta(&project, cx).await; + zeta.update(cx, |zeta, _cx| { + zeta.data_collection_choice = DataCollectionChoice::Enabled + }); + + run_edit_prediction(&buffer, &project, &zeta, cx).await; + assert_eq!( + captured_request.lock().clone().unwrap().can_collect_data, + false + ); + } + + #[gpui::test] + async fn test_no_data_collection_when_closed_source(cx: &mut TestAppContext) { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree(path!("/project"), json!({ "main.rs": "fn main() {}" })) + .await; + + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/project/main.rs", cx) + }) + .await + .unwrap(); + + let (zeta, captured_request, _) = make_test_zeta(&project, cx).await; + zeta.update(cx, |zeta, _cx| { + zeta.data_collection_choice = DataCollectionChoice::Enabled + }); + + run_edit_prediction(&buffer, &project, &zeta, cx).await; + assert_eq!( + captured_request.lock().clone().unwrap().can_collect_data, + false + ); + } + + #[gpui::test] + async fn test_data_collection_status_changes_on_move(cx: &mut TestAppContext) { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/open_source_worktree"), + json!({ "LICENSE": BSD_0_TXT, "main.rs": "" }), + ) + .await; + fs.insert_tree(path!("/closed_source_worktree"), json!({ "main.rs": "" })) + .await; + + let project = Project::test( + fs.clone(), + [ + path!("/open_source_worktree").as_ref(), + path!("/closed_source_worktree").as_ref(), + ], + cx, + ) + .await; + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/open_source_worktree/main.rs"), cx) + }) + .await + .unwrap(); + + let (zeta, captured_request, _) = make_test_zeta(&project, cx).await; + zeta.update(cx, |zeta, _cx| { + zeta.data_collection_choice = DataCollectionChoice::Enabled }); - let completion = completion_task.await.unwrap().unwrap(); + run_edit_prediction(&buffer, &project, &zeta, cx).await; + assert_eq!( + captured_request.lock().clone().unwrap().can_collect_data, + true + ); + + let closed_source_file = project + .update(cx, |project, cx| { + let worktree2 = project + .worktree_for_root_name("closed_source_worktree", cx) + .unwrap(); + worktree2.update(cx, |worktree2, cx| { + worktree2.load_file(Path::new("main.rs"), cx) + }) + }) + .await + .unwrap() + .file; + buffer.update(cx, |buffer, cx| { - buffer.edit(completion.edits.iter().cloned(), None, cx) + buffer.file_updated(closed_source_file, cx); }); + + run_edit_prediction(&buffer, &project, &zeta, cx).await; assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "lorem\nipsum" + captured_request.lock().clone().unwrap().can_collect_data, + false ); } - async fn edits_for_prediction( + #[gpui::test] + async fn test_no_data_collection_for_events_in_uncollectable_buffers(cx: &mut TestAppContext) { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/worktree1"), + json!({ "LICENSE": BSD_0_TXT, "main.rs": "", "other.rs": "" }), + ) + .await; + fs.insert_tree(path!("/worktree2"), json!({ "private.rs": "" })) + .await; + + let project = Project::test( + fs.clone(), + [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()], + cx, + ) + .await; + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/worktree1/main.rs"), cx) + }) + .await + .unwrap(); + let private_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/worktree2/file.rs"), cx) + }) + .await + .unwrap(); + + let (zeta, captured_request, _) = make_test_zeta(&project, cx).await; + zeta.update(cx, |zeta, _cx| { + zeta.data_collection_choice = DataCollectionChoice::Enabled + }); + + run_edit_prediction(&buffer, &project, &zeta, cx).await; + assert_eq!( + captured_request.lock().clone().unwrap().can_collect_data, + true + ); + + // this has a side effect of registering the buffer to watch for edits + run_edit_prediction(&private_buffer, &project, &zeta, cx).await; + assert_eq!( + captured_request.lock().clone().unwrap().can_collect_data, + false + ); + + private_buffer.update(cx, |private_buffer, cx| { + private_buffer.edit([(0..0, "An edit for the history!")], None, cx); + }); + + run_edit_prediction(&buffer, &project, &zeta, cx).await; + assert_eq!( + captured_request.lock().clone().unwrap().can_collect_data, + false + ); + + // make an edit that uses too many bytes, causing private_buffer edit to not be able to be + // included + buffer.update(cx, |buffer, cx| { + buffer.edit( + [(0..0, " ".repeat(MAX_EVENT_TOKENS * BYTES_PER_TOKEN_GUESS))], + None, + cx, + ); + }); + + run_edit_prediction(&buffer, &project, &zeta, cx).await; + assert_eq!( + captured_request.lock().clone().unwrap().can_collect_data, + true + ); + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + client::init_settings(cx); + Project::init_settings(cx); + }); + } + + async fn apply_edit_prediction( buffer_content: &str, completion_response: &str, cx: &mut TestAppContext, - ) -> Vec<(Range, String)> { - let completion_response = completion_response.to_string(); - let http_client = FakeHttpClient::create(move |req| { - let completion = completion_response.clone(); - async move { - match (req.method(), req.uri().path()) { - (&Method::POST, "/client/llm_tokens") => Ok(http_client::Response::builder() - .status(200) - .body( - serde_json::to_string(&CreateLlmTokenResponse { - token: LlmToken("the-llm-token".to_string()), - }) - .unwrap() - .into(), - ) - .unwrap()), - (&Method::POST, "/predict_edits/v2") => Ok(http_client::Response::builder() - .status(200) - .body( - serde_json::to_string(&PredictEditsResponse { - request_id: Uuid::new_v4(), - output_excerpt: completion, - }) - .unwrap() - .into(), - ) - .unwrap()), - _ => Ok(http_client::Response::builder() - .status(404) - .body("Not Found".into()) - .unwrap()), + ) -> String { + let fs = project::FakeFs::new(cx.executor()); + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let buffer = cx.new(|cx| Buffer::local(buffer_content, cx)); + let (zeta, _, response) = make_test_zeta(&project, cx).await; + *response.lock() = completion_response.to_string(); + let edit_prediction = run_edit_prediction(&buffer, &project, &zeta, cx).await; + buffer.update(cx, |buffer, cx| { + buffer.edit(edit_prediction.edits.iter().cloned(), None, cx) + }); + buffer.read_with(cx, |buffer, _| buffer.text()) + } + + async fn run_edit_prediction( + buffer: &Entity, + project: &Entity, + zeta: &Entity, + cx: &mut TestAppContext, + ) -> EditPrediction { + let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0))); + zeta.update(cx, |zeta, cx| zeta.register_buffer(buffer, &project, cx)); + cx.background_executor.run_until_parked(); + let completion_task = zeta.update(cx, |zeta, cx| { + zeta.request_completion(&project, buffer, cursor, cx) + }); + completion_task.await.unwrap().unwrap() + } + + async fn make_test_zeta( + project: &Entity, + cx: &mut TestAppContext, + ) -> ( + Entity, + Arc>>, + Arc>, + ) { + let default_response = indoc! {" + ```main.rs + <|start_of_file|> + <|editable_region_start|> + hello world + <|editable_region_end|> + ```" + }; + let captured_request: Arc>> = Arc::new(Mutex::new(None)); + let completion_response: Arc> = + Arc::new(Mutex::new(default_response.to_string())); + let http_client = FakeHttpClient::create({ + let captured_request = captured_request.clone(); + let completion_response = completion_response.clone(); + move |req| { + let captured_request = captured_request.clone(); + let completion_response = completion_response.clone(); + async move { + match (req.method(), req.uri().path()) { + (&Method::POST, "/client/llm_tokens") => { + Ok(http_client::Response::builder() + .status(200) + .body( + serde_json::to_string(&CreateLlmTokenResponse { + token: LlmToken("the-llm-token".to_string()), + }) + .unwrap() + .into(), + ) + .unwrap()) + } + (&Method::POST, "/predict_edits/v2") => { + let mut request_body = String::new(); + req.into_body().read_to_string(&mut request_body).await?; + *captured_request.lock() = + Some(serde_json::from_str(&request_body).unwrap()); + Ok(http_client::Response::builder() + .status(200) + .body( + serde_json::to_string(&PredictEditsResponse { + request_id: Uuid::new_v4(), + output_excerpt: completion_response.lock().clone(), + }) + .unwrap() + .into(), + ) + .unwrap()) + } + _ => Ok(http_client::Response::builder() + .status(404) + .body("Not Found".into()) + .unwrap()), + } } } }); @@ -1960,25 +2232,23 @@ mod tests { cx.update(|cx| { RefreshLlmTokenListener::register(client.clone(), cx); }); - // Construct the fake server to authenticate. let _server = FakeServer::for_client(42, &client, cx).await; - let fs = project::FakeFs::new(cx.executor()); - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let buffer = cx.new(|cx| Buffer::local(buffer_content, cx)); - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); - let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0))); - let zeta = cx.new(|cx| Zeta::new(client, project.read(cx).user_store(), cx)); - let completion_task = zeta.update(cx, |zeta, cx| { - zeta.request_completion(&project, &buffer, cursor, false, cx) + let zeta = cx.new(|cx| { + let mut zeta = Zeta::new(client, project.read(cx).user_store(), cx); + + let worktrees = project.read(cx).worktrees(cx).collect::>(); + for worktree in worktrees { + let worktree_id = worktree.read(cx).id(); + zeta.license_detection_watchers + .entry(worktree_id) + .or_insert_with(|| Rc::new(LicenseDetectionWatcher::new(&worktree, cx))); + } + + zeta }); - let completion = completion_task.await.unwrap().unwrap(); - completion - .edits - .iter() - .map(|(old_range, new_text)| (old_range.to_point(&snapshot), new_text.clone())) - .collect::>() + (zeta, captured_request, completion_response) } fn to_completion_edits( diff --git a/crates/zeta_cli/src/main.rs b/crates/zeta_cli/src/main.rs index e66eeed80920a0c31c5c06e119e17d418fbc294c..e7cec26b19358056cee4c8e253c54c0b2c794b33 100644 --- a/crates/zeta_cli/src/main.rs +++ b/crates/zeta_cli/src/main.rs @@ -189,30 +189,17 @@ async fn get_context( Some(events) => events.read_to_string().await?, None => String::new(), }; - // Enable gathering extra data not currently needed for edit predictions - let can_collect_data = true; - let git_info = None; - let mut gather_context_output = cx - .update(|cx| { - gather_context( - &project, - full_path_str, - &snapshot, - clipped_cursor, - move || events, - can_collect_data, - git_info, - cx, - ) - })? - .await; - - // Disable data collection for these requests, as this is currently just used for evals - if let Ok(gather_context_output) = gather_context_output.as_mut() { - gather_context_output.body.can_collect_data = false - } - - gather_context_output + let prompt_for_events = move || (events, 0); + cx.update(|cx| { + gather_context( + full_path_str, + &snapshot, + clipped_cursor, + prompt_for_events, + cx, + ) + })? + .await } pub async fn open_buffer_with_language_server( From 69bdef38ecef673609e1acf1e7a6d79f5d4d44d3 Mon Sep 17 00:00:00 2001 From: Liu Jinyi Date: Mon, 8 Sep 2025 02:33:17 +0800 Subject: [PATCH 639/744] editor: Fix inconsistent search behavior for untitled/temporary tabs (#37086) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #37597 Release Notes: - N/A --- ## Problem When using "Tab Switcher: Toggle All", temporary files (untitled buffers without associated file paths) cannot be searched by their displayed content. This creates an inconsistent user experience where: - **UI Display**: Shows dynamic titles based on the first line of content (up to 40 characters) - **Search Text**: Only searches for the static text "untitled" ### Example - A temporary file containing `Hello World` is displayed as "Hello World" in the tab - However, searching for "Hello" in Tab Switcher returns no results - Only searching for "untitled" will find this temporary file ## Root Cause The issue stems from inconsistent title generation logic between display and search: 1. **Display Title** (`items.rs:724`): Uses `self.title(cx)` → `MultiBuffer::title()` → `buffer_content_title()` - Returns the first line of content (max 40 chars) for temporary files 2. **Search Text** (`items.rs:650-656`): Uses `tab_content_text()` method - Returns hardcoded "untitled" for files without paths ## Solution Modified the `tab_content_text()` method in `crates/editor/src/items.rs` to use the same logic as the displayed title for consistency: ```rust fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString { if let Some(path) = path_for_buffer(&self.buffer, detail, true, cx) { path.to_string_lossy().to_string().into() } else { // Use the same logic as the displayed title for consistency self.buffer.read(cx).title(cx).to_string().into() } } ``` --- crates/editor/src/items.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 8a07939cf47529d6a7d94b20bd22d7278b3e9d24..48c3a8a41a802392b7dc20d5b935bc7acc3bc10a 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -651,7 +651,8 @@ impl Item for Editor { if let Some(path) = path_for_buffer(&self.buffer, detail, true, cx) { path.to_string_lossy().to_string().into() } else { - "untitled".into() + // Use the same logic as the displayed title for consistency + self.buffer.read(cx).title(cx).to_string().into() } } From 9450bcad25156f373e3b81a7a13d3ae5211a0537 Mon Sep 17 00:00:00 2001 From: marius851000 Date: Mon, 8 Sep 2025 06:26:01 +0200 Subject: [PATCH 640/744] ollama: Properly format tool calls fed back to the model (#34750) Fix an issue that resulted in Ollama models not being able to not being able to access the input of the commands they executed (only being able to access the result). This properly return the function history as shown in https://github.com/ollama/ollama/blob/main/docs/api.md#chat-request-with-history-with-tools Previously, function input where not returned and result where returned as a "user" role. Release Notes: - ollama: Improved format when returning tool results to the models --- crates/language_models/src/provider/ollama.rs | 121 +++++++++++------- crates/ollama/src/ollama.rs | 4 + 2 files changed, 79 insertions(+), 46 deletions(-) diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index 8975115d907875569f63e4247cf7edcdbcb91f8a..a80cacfc4a02521af74b32c34cc3360e9665a7d9 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -11,8 +11,8 @@ use language_model::{ LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason, TokenUsage, }; use ollama::{ - ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OllamaFunctionTool, - OllamaToolCall, get_models, show_model, stream_chat_completion, + ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OllamaFunctionCall, + OllamaFunctionTool, OllamaToolCall, get_models, show_model, stream_chat_completion, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -282,59 +282,85 @@ impl OllamaLanguageModel { fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest { let supports_vision = self.model.supports_vision.unwrap_or(false); - ChatRequest { - model: self.model.name.clone(), - messages: request - .messages - .into_iter() - .map(|msg| { - let images = if supports_vision { - msg.content - .iter() - .filter_map(|content| match content { - MessageContent::Image(image) => Some(image.source.to_string()), - _ => None, - }) - .collect::>() - } else { - vec![] - }; - - match msg.role { - Role::User => ChatMessage::User { + let mut messages = Vec::with_capacity(request.messages.len()); + + for mut msg in request.messages.into_iter() { + let images = if supports_vision { + msg.content + .iter() + .filter_map(|content| match content { + MessageContent::Image(image) => Some(image.source.to_string()), + _ => None, + }) + .collect::>() + } else { + vec![] + }; + + match msg.role { + Role::User => { + for tool_result in msg + .content + .extract_if(.., |x| matches!(x, MessageContent::ToolResult(..))) + { + match tool_result { + MessageContent::ToolResult(tool_result) => { + messages.push(ChatMessage::Tool { + tool_name: tool_result.tool_name.to_string(), + content: tool_result.content.to_str().unwrap_or("").to_string(), + }) + } + _ => unreachable!("Only tool result should be extracted"), + } + } + if !msg.content.is_empty() { + messages.push(ChatMessage::User { content: msg.string_contents(), images: if images.is_empty() { None } else { Some(images) }, - }, - Role::Assistant => { - let content = msg.string_contents(); - let thinking = - msg.content.into_iter().find_map(|content| match content { - MessageContent::Thinking { text, .. } if !text.is_empty() => { - Some(text) - } - _ => None, - }); - ChatMessage::Assistant { - content, - tool_calls: None, - images: if images.is_empty() { - None - } else { - Some(images) - }, - thinking, + }) + } + } + Role::Assistant => { + let content = msg.string_contents(); + let mut thinking = None; + let mut tool_calls = Vec::new(); + for content in msg.content.into_iter() { + match content { + MessageContent::Thinking { text, .. } if !text.is_empty() => { + thinking = Some(text) } + MessageContent::ToolUse(tool_use) => { + tool_calls.push(OllamaToolCall::Function(OllamaFunctionCall { + name: tool_use.name.to_string(), + arguments: tool_use.input, + })); + } + _ => (), } - Role::System => ChatMessage::System { - content: msg.string_contents(), - }, } - }) - .collect(), + messages.push(ChatMessage::Assistant { + content, + tool_calls: Some(tool_calls), + images: if images.is_empty() { + None + } else { + Some(images) + }, + thinking, + }) + } + Role::System => messages.push(ChatMessage::System { + content: msg.string_contents(), + }), + } + } + ChatRequest { + model: self.model.name.clone(), + messages, keep_alive: self.model.keep_alive.clone().unwrap_or_default(), stream: true, options: Some(ChatOptions { @@ -483,6 +509,9 @@ fn map_to_language_model_completion_events( ChatMessage::System { content } => { events.push(Ok(LanguageModelCompletionEvent::Text(content))); } + ChatMessage::Tool { content, .. } => { + events.push(Ok(LanguageModelCompletionEvent::Text(content))); + } ChatMessage::Assistant { content, tool_calls, diff --git a/crates/ollama/src/ollama.rs b/crates/ollama/src/ollama.rs index 64cd1cc0cbc06607ee9b3b72ee81cbeb9489c344..3c935d2152556393829f648abe31a717b239ce76 100644 --- a/crates/ollama/src/ollama.rs +++ b/crates/ollama/src/ollama.rs @@ -117,6 +117,10 @@ pub enum ChatMessage { System { content: String, }, + Tool { + tool_name: String, + content: String, + }, } #[derive(Serialize, Deserialize, Debug)] From b35959f4c2e5b1550a32ce8fc0dff57415eb11bc Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Mon, 8 Sep 2025 10:20:26 +0530 Subject: [PATCH 641/744] agent_ui: Fix `context_server` duplication when name is updated (#35403) Closes #35400 | Before | After | |--------|--------| |